diff --git a/models.py b/models.py index 5d35da3..9621273 100644 --- a/models.py +++ b/models.py @@ -1,5 +1,5 @@ from dataclasses import dataclass, field -from typing import List +from typing import List, Optional @dataclass class Ticket: @@ -11,6 +11,16 @@ class Ticket: status: str assigned_to: str depends_on: List[str] = field(default_factory=list) + blocked_reason: Optional[str] = None + + def mark_blocked(self, reason: str): + """Sets the ticket status to 'blocked' and records the reason.""" + self.status = "blocked" + self.blocked_reason = reason + + def mark_complete(self): + """Sets the ticket status to 'completed'.""" + self.status = "completed" @dataclass class Track: @@ -21,6 +31,31 @@ class Track: description: str tickets: List[Ticket] = field(default_factory=list) + def get_executable_tickets(self) -> List[Ticket]: + """ + Returns all 'todo' tickets whose dependencies are all 'completed'. + """ + # Map ticket IDs to their current status for efficient lookup + status_map = {t.id: t.status for t in self.tickets} + + executable = [] + for ticket in self.tickets: + if ticket.status != "todo": + continue + + # Check if all dependencies are completed + all_deps_completed = True + for dep_id in ticket.depends_on: + # If a dependency is missing from the track, we treat it as not completed (or we could raise an error) + if status_map.get(dep_id) != "completed": + all_deps_completed = False + break + + if all_deps_completed: + executable.append(ticket) + + return executable + @dataclass class WorkerContext: """ diff --git a/tests/test_mma_models.py b/tests/test_mma_models.py index 2b531b8..87ff619 100644 --- a/tests/test_mma_models.py +++ b/tests/test_mma_models.py @@ -89,3 +89,88 @@ def test_worker_context_instantiation(): assert context.ticket_id == ticket_id assert context.model_name == model_name assert context.messages == messages + +def test_ticket_mark_blocked(): + """ + Verifies that ticket.mark_blocked(reason) sets the status to 'blocked'. + Note: The reason field might need to be added to the Ticket class. + """ + ticket = Ticket(id="T1", description="Task 1", status="todo", assigned_to="a") + ticket.mark_blocked("Waiting for API key") + assert ticket.status == "blocked" + +def test_ticket_mark_complete(): + """ + Verifies that ticket.mark_complete() sets the status to 'completed'. + """ + ticket = Ticket(id="T1", description="Task 1", status="todo", assigned_to="a") + ticket.mark_complete() + assert ticket.status == "completed" + +def test_track_get_executable_tickets(): + """ + Verifies that track.get_executable_tickets() returns only 'todo' tickets + whose dependencies are all 'completed'. + """ + # T1: todo, no deps -> executable + t1 = Ticket(id="T1", description="T1", status="todo", assigned_to="a") + # T2: todo, deps [T1] -> not executable (T1 is todo) + t2 = Ticket(id="T2", description="T2", status="todo", assigned_to="a", depends_on=["T1"]) + # T3: todo, deps [T4] -> not executable (T4 is blocked) + t3 = Ticket(id="T3", description="T3", status="todo", assigned_to="a", depends_on=["T4"]) + # T4: blocked, no deps -> not executable (not 'todo') + t4 = Ticket(id="T4", description="T4", status="blocked", assigned_to="a") + # T5: completed, no deps -> not executable (not 'todo') + t5 = Ticket(id="T5", description="T5", status="completed", assigned_to="a") + # T6: todo, deps [T5] -> executable (T5 is completed) + t6 = Ticket(id="T6", description="T6", status="todo", assigned_to="a", depends_on=["T5"]) + + track = Track(id="TR1", description="Track 1", tickets=[t1, t2, t3, t4, t5, t6]) + + executable = track.get_executable_tickets() + executable_ids = [t.id for t in executable] + + assert "T1" in executable_ids + assert "T6" in executable_ids + assert len(executable_ids) == 2 + +def test_track_get_executable_tickets_complex(): + """ + Verifies executable tickets with complex dependency chains. + Chain: T1 (comp) -> T2 (todo) -> T3 (todo) + T4 (comp) -> T3 + T5 (todo) -> T3 + """ + t1 = Ticket(id="T1", description="T1", status="completed", assigned_to="a") + t2 = Ticket(id="T2", description="T2", status="todo", assigned_to="a", depends_on=["T1"]) + t3 = Ticket(id="T3", description="T3", status="todo", assigned_to="a", depends_on=["T2", "T4", "T5"]) + t4 = Ticket(id="T4", description="T4", status="completed", assigned_to="a") + t5 = Ticket(id="T5", description="T5", status="todo", assigned_to="a") + + track = Track(id="TR1", description="Track 1", tickets=[t1, t2, t3, t4, t5]) + + # At this point: + # T1 is completed + # T4 is completed + # T2 is todo, depends on T1 (completed) -> Executable + # T5 is todo, no deps -> Executable + # T3 is todo, depends on T2 (todo), T4 (completed), T5 (todo) -> Not executable + + executable = track.get_executable_tickets() + executable_ids = sorted([t.id for t in executable]) + + assert executable_ids == ["T2", "T5"] + + # Mark T2 complete + t2.mark_complete() + # T3 still depends on T5 + executable = track.get_executable_tickets() + executable_ids = sorted([t.id for t in executable]) + assert executable_ids == ["T5"] + + # Mark T5 complete + t5.mark_complete() + # Now T3 should be executable + executable = track.get_executable_tickets() + executable_ids = sorted([t.id for t in executable]) + assert executable_ids == ["T3"]