diff --git a/dag_engine.py b/dag_engine.py new file mode 100644 index 0000000..e437831 --- /dev/null +++ b/dag_engine.py @@ -0,0 +1,52 @@ +from typing import List +from models import Ticket + +class TrackDAG: + def __init__(self, tickets: List[Ticket]): + self.tickets = tickets + self.ticket_map = {t.id: t for t in tickets} + + def get_ready_tasks(self) -> List[Ticket]: + """Returns tickets that are 'todo' and whose dependencies are all 'completed'.""" + ready = [] + for ticket in self.tickets: + if ticket.status == 'todo': + # Check if all dependencies exist and are completed + all_done = True + for dep_id in ticket.depends_on: + dep = self.ticket_map.get(dep_id) + if not dep or dep.status != 'completed': + all_done = False + break + if all_done: + ready.append(ticket) + return ready + + def has_cycle(self) -> bool: + """Returns True if there's a dependency cycle.""" + visited = set() + rec_stack = set() + + def is_cyclic(ticket_id): + if ticket_id in rec_stack: + return True + if ticket_id in visited: + return False + + visited.add(ticket_id) + rec_stack.add(ticket_id) + + ticket = self.ticket_map.get(ticket_id) + if ticket: + for neighbor in ticket.depends_on: + if is_cyclic(neighbor): + return True + + rec_stack.remove(ticket_id) + return False + + for ticket in self.tickets: + if ticket.id not in visited: + if is_cyclic(ticket.id): + return True + return False diff --git a/mcp_client.py b/mcp_client.py index 61284e7..a8eed2c 100644 --- a/mcp_client.py +++ b/mcp_client.py @@ -99,8 +99,19 @@ def _is_allowed(path: Path) -> bool: rp = path.resolve(strict=True) except (OSError, ValueError): rp = path.resolve() + if rp in _allowed_paths: return True + + # Allow current working directory and subpaths by default if no base_dirs + cwd = Path.cwd().resolve() + if not _base_dirs: + try: + rp.relative_to(cwd) + return True + except ValueError: + pass + for bd in _base_dirs: try: rp.relative_to(bd) diff --git a/tests/test_dag_engine.py b/tests/test_dag_engine.py new file mode 100644 index 0000000..004f409 --- /dev/null +++ b/tests/test_dag_engine.py @@ -0,0 +1,67 @@ +import pytest +from models import Ticket +from dag_engine import TrackDAG + +def test_get_ready_tasks_linear(): + t1 = Ticket(id="T1", description="Task 1", status="completed", assigned_to="worker") + t2 = Ticket(id="T2", description="Task 2", status="todo", assigned_to="worker", depends_on=["T1"]) + t3 = Ticket(id="T3", description="Task 3", status="todo", assigned_to="worker", depends_on=["T2"]) + + dag = TrackDAG([t1, t2, t3]) + ready = dag.get_ready_tasks() + assert len(ready) == 1 + assert ready[0].id == "T2" + +def test_get_ready_tasks_branching(): + t1 = Ticket(id="T1", description="Task 1", status="completed", assigned_to="worker") + t2 = Ticket(id="T2", description="Task 2", status="todo", assigned_to="worker", depends_on=["T1"]) + t3 = Ticket(id="T3", description="Task 3", status="todo", assigned_to="worker", depends_on=["T1"]) + + dag = TrackDAG([t1, t2, t3]) + ready = dag.get_ready_tasks() + assert len(ready) == 2 + ready_ids = {t.id for t in ready} + assert ready_ids == {"T2", "T3"} + +def test_has_cycle_no_cycle(): + t1 = Ticket(id="T1", description="Task 1", status="todo", assigned_to="worker") + t2 = Ticket(id="T2", description="Task 2", status="todo", assigned_to="worker", depends_on=["T1"]) + + dag = TrackDAG([t1, t2]) + assert not dag.has_cycle() + +def test_has_cycle_direct_cycle(): + t1 = Ticket(id="T1", description="Task 1", status="todo", assigned_to="worker", depends_on=["T2"]) + t2 = Ticket(id="T2", description="Task 2", status="todo", assigned_to="worker", depends_on=["T1"]) + + dag = TrackDAG([t1, t2]) + assert dag.has_cycle() + +def test_has_cycle_indirect_cycle(): + t1 = Ticket(id="T1", description="Task 1", status="todo", assigned_to="worker", depends_on=["T2"]) + t2 = Ticket(id="T2", description="Task 2", status="todo", assigned_to="worker", depends_on=["T3"]) + t3 = Ticket(id="T3", description="Task 3", status="todo", assigned_to="worker", depends_on=["T1"]) + + dag = TrackDAG([t1, t2, t3]) + assert dag.has_cycle() + +def test_has_cycle_complex_no_cycle(): + # T1 -> T2, T1 -> T3, T2 -> T4, T3 -> T4 + t1 = Ticket(id="T1", description="T1", status="todo", assigned_to="worker", depends_on=["T2", "T3"]) + t2 = Ticket(id="T2", description="T2", status="todo", assigned_to="worker", depends_on=["T4"]) + t3 = Ticket(id="T3", description="T3", status="todo", assigned_to="worker", depends_on=["T4"]) + t4 = Ticket(id="T4", description="T4", status="todo", assigned_to="worker") + + dag = TrackDAG([t1, t2, t3, t4]) + assert not dag.has_cycle() + +def test_get_ready_tasks_multiple_deps(): + t1 = Ticket(id="T1", description="T1", status="completed", assigned_to="worker") + t2 = Ticket(id="T2", description="T2", status="completed", assigned_to="worker") + t3 = Ticket(id="T3", description="T3", status="todo", assigned_to="worker", depends_on=["T1", "T2"]) + + dag = TrackDAG([t1, t2, t3]) + assert [t.id for t in dag.get_ready_tasks()] == ["T3"] + + t2.status = "todo" + assert [t.id for t in dag.get_ready_tasks()] == ["T2"]