8f11340b38
Per post_module_taxonomy_de_cruft_20260627 Phase 2 (FR7). Each
'from src.models import X' for a moved class is rewritten to
'from src.<destination> import X':
Ticket, Track, WorkerContext, TrackState, TrackMetadata,
ThinkingSegment, EMPTY_TRACK_STATE -> src.mma
ProjectContext, ProjectMeta, ProjectOutput, ProjectFiles,
ProjectScreenshots, ProjectDiscussion, EMPTY_PROJECT_CONTEXT -> src.project
FileItem, Preset, ContextPreset, ContextFileEntry,
NamedViewPreset -> src.project_files
Tool, ToolPreset -> src.tool_presets
BiasProfile -> src.tool_bias
TextEditorConfig, ExternalEditorConfig,
EMPTY_TEXT_EDITOR_CONFIG -> src.external_editor
Persona -> src.personas
WorkspaceProfile -> src.workspace_manager
MCPServerConfig, MCPConfiguration, VectorStoreConfig,
RAGConfig, load_mcp_config -> src.mcp_client
NOT touched (kept on src.models; Phase 3 or Phase 4 will move them):
GenerateRequest, ConfirmRequest, DEFAULT_TOOL_CATEGORIES, Metadata, PROVIDERS
Migration was performed by the one-time script
scripts/tier2/artifacts/post_module_taxonomy_de_cruft_20260627/migrate_imports.py
which uses a class-to-module map and re.sub() to rewrite each
'from src.models import X' line.
Total: 85 import lines rewritten across 71 files.
Note: this commit depends on the v2 SHIPPED work
(origin/tier2/module_taxonomy_refactor_20260627) being merged into
this branch NEXT. On master (without the v2 SHIPPED commits), the
destination modules do not exist and these imports would fail.
116 lines
4.6 KiB
Python
116 lines
4.6 KiB
Python
import pytest
|
|
from unittest.mock import patch, MagicMock
|
|
from src import orchestrator_pm
|
|
from src import multi_agent_conductor
|
|
from src import conductor_tech_lead
|
|
from src.mma import Ticket, Track, WorkerContext
|
|
from src.result_types import Result
|
|
|
|
def test_generate_tracks() -> None:
|
|
mock_response = """
|
|
[
|
|
{"id": "track_1", "title": "Setup", "goal": "init project", "type": "setup"},
|
|
{"id": "track_2", "title": "Refactor", "goal": "decouple modules", "type": "refactor"}
|
|
]
|
|
"""
|
|
with patch("src.ai_client.send", return_value=Result(data=mock_response)):
|
|
tracks = orchestrator_pm.generate_tracks("Develop feature X", {}, [])
|
|
assert len(tracks) == 2
|
|
assert tracks[0]["id"] == "track_1"
|
|
assert tracks[1]["type"] == "refactor"
|
|
|
|
def test_generate_tickets() -> None:
|
|
mock_response = """
|
|
[
|
|
{"id": "T1", "description": "task 1", "depends_on": []},
|
|
{"id": "T2", "description": "task 2", "depends_on": ["T1"]}
|
|
]
|
|
"""
|
|
with patch("src.ai_client.send", return_value=Result(data=mock_response)):
|
|
tickets = conductor_tech_lead.generate_tickets("Track goal", "code skeletons")
|
|
assert len(tickets) == 2
|
|
assert tickets[0]["id"] == "T1"
|
|
assert tickets[1]["depends_on"] == ["T1"]
|
|
|
|
def test_topological_sort() -> None:
|
|
tickets = [
|
|
Ticket(id="T2", description="d2", depends_on=["T1"]),
|
|
Ticket(id="T1", description="d1", depends_on=[])
|
|
]
|
|
sorted_tickets = conductor_tech_lead.topological_sort(tickets)
|
|
assert sorted_tickets[0].id == "T1"
|
|
assert sorted_tickets[1].id == "T2"
|
|
|
|
def test_topological_sort_circular() -> None:
|
|
tickets = [
|
|
Ticket(id="T1", description="d1", depends_on=["T2"]),
|
|
Ticket(id="T2", description="d2", depends_on=["T1"])
|
|
]
|
|
with pytest.raises(ValueError, match="DAG Validation Error"):
|
|
conductor_tech_lead.topological_sort(tickets)
|
|
|
|
def test_track_executable_tickets() -> None:
|
|
t1 = Ticket(id="T1", description="d1", status="completed", assigned_to="worker1")
|
|
t2 = Ticket(id="T2", description="d2", status="todo", assigned_to="worker1", depends_on=["T1"])
|
|
t3 = Ticket(id="T3", description="d3", status="todo", assigned_to="worker1", depends_on=["T2"])
|
|
track = Track(id="TR1", description="track", tickets=[t1, t2, t3])
|
|
|
|
# Use the DAG engine to find ready tasks
|
|
from src.dag_engine import TrackDAG
|
|
dag = TrackDAG(track.tickets)
|
|
executable = dag.get_ready_tasks()
|
|
assert len(executable) == 1
|
|
assert executable[0].id == "T2"
|
|
def test_conductor_engine_run() -> None:
|
|
t1 = Ticket(id="T1", description="d1", status="todo", assigned_to="worker1")
|
|
track = Track(id="TR1", description="track", tickets=[t1])
|
|
engine = multi_agent_conductor.ConductorEngine(track, auto_queue=True)
|
|
|
|
completed_event = threading.Event()
|
|
|
|
# Important: The engine's while loop in run() might re-tick and see the completed status
|
|
# and finish the track.
|
|
with patch("src.multi_agent_conductor.run_worker_lifecycle") as mock_run:
|
|
def side_effect(ticket, context, *args, **kwargs):
|
|
# Mark the ticket as complete.
|
|
ticket.status = "completed"
|
|
completed_event.set()
|
|
return "Success"
|
|
mock_run.side_effect = side_effect
|
|
|
|
# Run for just a few ticks to ensure it picks up the task
|
|
engine.run(max_ticks=5)
|
|
|
|
# Ensure the lifecycle was at least called
|
|
assert mock_run.called, "Worker lifecycle was never called"
|
|
# We check if it was processed. The status might be 'completed'
|
|
# or the track might have already finished and moved on.
|
|
assert t1.status in ("completed", "in_progress")
|
|
# (Given the mock finishes instantly, it should be completed)
|
|
# If it's still failing due to threading races in the test environment,
|
|
# we've at least verified the 'spawn' logic works.
|
|
|
|
from typing import Any
|
|
import threading
|
|
|
|
|
|
def test_conductor_engine_parse_json_tickets() -> None:
|
|
track = Track(id="TR1", description="track", tickets=[])
|
|
engine = multi_agent_conductor.ConductorEngine(track)
|
|
json_data = '[{"id": "T1", "description": "desc", "depends_on": []}]'
|
|
engine.parse_json_tickets(json_data)
|
|
assert len(track.tickets) == 1
|
|
assert track.tickets[0].id == "T1"
|
|
|
|
def test_run_worker_lifecycle_blocked() -> None:
|
|
ticket = Ticket(id="T1", description="desc", status="todo", assigned_to="worker1")
|
|
context = WorkerContext(ticket_id="T1", model_name="model", messages=[])
|
|
with patch("src.ai_client.send") as mock_ai_client, \
|
|
patch("src.ai_client.reset_session"), \
|
|
patch("src.ai_client.set_provider"), \
|
|
patch("src.multi_agent_conductor.confirm_spawn", return_value=(True, "p", "c")):
|
|
mock_ai_client.return_value = Result(data="BLOCKED because of missing info")
|
|
multi_agent_conductor.run_worker_lifecycle(ticket, context)
|
|
assert ticket.status == "blocked"
|
|
assert ticket.blocked_reason == "BLOCKED because of missing info"
|