Private
Public Access
0
0

fix(imports): break models<->dag_engine circular dependency

Track.get_executable_tickets (in models.py) called TrackDAG at
runtime, forcing a top-level import of src.dag_engine into models.py
and creating a 2-cycle that broke whichever module loaded second
(Ticket was not yet defined when models.py loaded first; TrackDAG
was not yet defined when dag_engine.py loaded first).

Fix: hoist the method out of the Track dataclass and into a free
function get_executable_tickets(track) in dag_engine.py. models.py
no longer needs TrackDAG at all, so the cycle is one-directional
(models -> dag_engine) and resolves cleanly in any import order.

Tests updated:
- tests/test_mma_models.py: import get_executable_tickets and call
  it instead of track.get_executable_tickets() (4 call sites)
- tests/test_conductor_engine_v2.py: comment update

Verified both import orders resolve cleanly:
  forward:  import src.models; import src.dag_engine  -> OK
  reverse:  import src.dag_engine; import src.models  -> OK
34 tests pass (test_mma_models, test_dag_engine, test_execution_engine,
test_arch_boundary_phase3, test_track_state_schema).
This commit is contained in:
2026-06-06 13:30:18 -04:00
parent 9e4fac496d
commit ca254bac41
4 changed files with 19 additions and 18 deletions
+1 -1
View File
@@ -55,7 +55,7 @@ def test_conductor_engine_run_executes_tickets_in_order(monkeypatch: pytest.Monk
vlogger.log_state("T1 Status Final", "todo", ticket1.status)
vlogger.log_state("T2 Status Final", "todo", ticket2.status)
# Track.get_executable_tickets() should be called repeatedly until all are done
# get_executable_tickets(track) should be called repeatedly until all are done
# T1 should run first, then T2.
assert mock_lifecycle.call_count == 2
assert ticket1.status == "completed"
+6 -5
View File
@@ -1,4 +1,5 @@
from src.models import Ticket, Track, WorkerContext
from src.dag_engine import get_executable_tickets
def test_ticket_instantiation() -> None:
"""
@@ -117,7 +118,7 @@ def test_track_get_executable_tickets() -> None:
"""
Verifies that track.get_executable_tickets() returns only 'todo' tickets
Verifies that get_executable_tickets(track) returns only 'todo' tickets
whose dependencies are all 'completed'.
"""
# T1: todo, no deps -> executable
@@ -133,7 +134,7 @@ def test_track_get_executable_tickets() -> None:
# 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 = get_executable_tickets(track)
executable_ids = [t.id for t in executable]
assert "T1" in executable_ids
assert "T6" in executable_ids
@@ -160,18 +161,18 @@ def test_track_get_executable_tickets_complex() -> None:
# 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 = get_executable_tickets(track)
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 = get_executable_tickets(track)
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 = get_executable_tickets(track)
executable_ids = sorted([t.id for t in executable])
assert executable_ids == ["T3"]