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.
344 lines
14 KiB
Python
344 lines
14 KiB
Python
"""
|
|
ANTI-SIMPLIFICATION: These tests verify the core multi-agent execution engine, including dependency graph resolution, worker lifecycle, and context injection.
|
|
They MUST NOT be simplified, and their assertions on exact call counts and dependency ordering are critical for preventing regressions in the orchestrator.
|
|
"""
|
|
import pytest
|
|
from unittest.mock import MagicMock, patch
|
|
from src.mma import Ticket, Track, WorkerContext
|
|
from src import ai_client
|
|
from src.result_types import Result
|
|
|
|
def test_conductor_engine_initialization() -> None:
|
|
"""
|
|
|
|
|
|
Test that ConductorEngine can be initialized with a Track.
|
|
"""
|
|
track = Track(id="test_track", description="Test Track")
|
|
from src.multi_agent_conductor import ConductorEngine
|
|
engine = ConductorEngine(track=track, auto_queue=True)
|
|
assert engine.track == track
|
|
|
|
def test_conductor_engine_run_executes_tickets_in_order(monkeypatch: pytest.MonkeyPatch, vlogger) -> None:
|
|
"""
|
|
|
|
|
|
Test that run iterates through executable tickets and calls the worker lifecycle.
|
|
"""
|
|
ticket1 = Ticket(id="T1", description="Task 1", status="todo", assigned_to="worker1")
|
|
ticket2 = Ticket(id="T2", description="Task 2", status="todo", assigned_to="worker2", depends_on=["T1"])
|
|
track = Track(id="track1", description="Track 1", tickets=[ticket1, ticket2])
|
|
from src.multi_agent_conductor import ConductorEngine
|
|
engine = ConductorEngine(track=track, auto_queue=True)
|
|
|
|
vlogger.log_state("Ticket Count", 0, 2)
|
|
vlogger.log_state("T1 Status", "todo", "todo")
|
|
vlogger.log_state("T2 Status", "todo", "todo")
|
|
|
|
# Mock ai_client.send using monkeypatch
|
|
mock_send = MagicMock()
|
|
monkeypatch.setattr(ai_client, 'send', mock_send)
|
|
# We mock run_worker_lifecycle as it is expected to be in the same module
|
|
with patch("src.multi_agent_conductor.run_worker_lifecycle") as mock_lifecycle:
|
|
# Mocking lifecycle to mark ticket as complete so dependencies can be resolved
|
|
|
|
def side_effect(ticket, context, *args, **kwargs):
|
|
import threading
|
|
def do_work():
|
|
import time
|
|
time.sleep(0.1)
|
|
ticket.mark_complete()
|
|
threading.Thread(target=do_work).start()
|
|
return Result(data="Success")
|
|
mock_lifecycle.side_effect = side_effect
|
|
engine.run(max_ticks=20)
|
|
|
|
vlogger.log_state("T1 Status Final", "todo", ticket1.status)
|
|
vlogger.log_state("T2 Status Final", "todo", ticket2.status)
|
|
|
|
# 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"
|
|
assert ticket2.status == "completed"
|
|
# Verify sequence: T1 before T2
|
|
calls = mock_lifecycle.call_args_list
|
|
assert calls[0][0][0].id == "T1"
|
|
assert calls[1][0][0].id == "T2"
|
|
vlogger.finalize("Verify dependency execution order", "PASS", "T1 executed before T2")
|
|
|
|
def test_run_worker_lifecycle_calls_ai_client_send(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""
|
|
|
|
|
|
Test that run_worker_lifecycle triggers the AI client and updates ticket status on success.
|
|
"""
|
|
ticket = Ticket(id="T1", description="Task 1", status="todo", assigned_to="worker1")
|
|
context = WorkerContext(ticket_id="T1", model_name="test-model", messages=[])
|
|
from src.multi_agent_conductor import run_worker_lifecycle
|
|
# Mock ai_client.send using monkeypatch
|
|
mock_send = MagicMock()
|
|
monkeypatch.setattr(ai_client, 'send', mock_send)
|
|
mock_send.return_value = Result(data="Task complete. I have updated the file.")
|
|
result = run_worker_lifecycle(ticket, context)
|
|
assert result == "Task complete. I have updated the file."
|
|
assert ticket.status == "completed"
|
|
mock_send.assert_called_once()
|
|
# Check if description was passed to send()
|
|
args, kwargs = mock_send.call_args
|
|
# user_message is passed as a keyword argument
|
|
assert ticket.description in kwargs["user_message"]
|
|
|
|
def test_run_worker_lifecycle_context_injection(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""
|
|
|
|
|
|
Test that run_worker_lifecycle can take a context_files list and injects AST views into the prompt.
|
|
"""
|
|
ticket = Ticket(id="T1", description="Task 1", status="todo", assigned_to="worker1")
|
|
context = WorkerContext(ticket_id="T1", model_name="test-model", messages=[])
|
|
context_files = ["primary.py", "secondary.py"]
|
|
from src.multi_agent_conductor import run_worker_lifecycle
|
|
# Mock ai_client.send using monkeypatch
|
|
mock_send = MagicMock()
|
|
monkeypatch.setattr(ai_client, 'send', mock_send)
|
|
# We mock ASTParser which is expected to be imported in multi_agent_conductor
|
|
with patch("src.multi_agent_conductor.ASTParser") as mock_ast_parser_class, \
|
|
patch("builtins.open", new_callable=MagicMock) as mock_open:
|
|
# Setup open mock to return different content for different files
|
|
file_contents = {
|
|
"primary.py": "def primary(): pass",
|
|
"secondary.py": "def secondary(): pass"
|
|
}
|
|
|
|
def mock_open_side_effect(file, *args, **kwargs):
|
|
content = file_contents.get(file, "")
|
|
mock_file = MagicMock()
|
|
mock_file.read.return_value = content
|
|
mock_file.__enter__.return_value = mock_file
|
|
return mock_file
|
|
mock_open.side_effect = mock_open_side_effect
|
|
# Setup ASTParser mock
|
|
mock_ast_parser = mock_ast_parser_class.return_value
|
|
mock_ast_parser.get_curated_view.return_value = "CURATED VIEW"
|
|
mock_ast_parser.get_skeleton.return_value = "SKELETON VIEW"
|
|
mock_send.return_value = Result(data="Success")
|
|
run_worker_lifecycle(ticket, context, context_files=context_files)
|
|
# Verify ASTParser calls:
|
|
# First file (primary) should get curated view, others (secondary) get skeleton
|
|
mock_ast_parser.get_curated_view.assert_called_once_with("def primary(): pass", path="primary.py")
|
|
mock_ast_parser.get_skeleton.assert_called_once_with("def secondary(): pass", path="secondary.py")
|
|
# Verify user_message contains the views
|
|
_, kwargs = mock_send.call_args
|
|
user_message = kwargs["user_message"]
|
|
assert "CURATED VIEW" in user_message
|
|
assert "SKELETON VIEW" in user_message
|
|
assert "primary.py" in user_message
|
|
assert "secondary.py" in user_message
|
|
|
|
def test_run_worker_lifecycle_handles_blocked_response(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""
|
|
|
|
|
|
Test that run_worker_lifecycle marks the ticket as blocked if the AI indicates it cannot proceed.
|
|
"""
|
|
ticket = Ticket(id="T1", description="Task 1", status="todo", assigned_to="worker1")
|
|
context = WorkerContext(ticket_id="T1", model_name="test-model", messages=[])
|
|
from src.multi_agent_conductor import run_worker_lifecycle
|
|
# Mock ai_client.send using monkeypatch
|
|
mock_send = MagicMock()
|
|
monkeypatch.setattr(ai_client, 'send', mock_send)
|
|
# Simulate a response indicating a block
|
|
mock_send.return_value = Result(data="I am BLOCKED because I don't have enough information.")
|
|
run_worker_lifecycle(ticket, context)
|
|
assert ticket.status == "blocked"
|
|
assert "BLOCKED" in ticket.blocked_reason
|
|
|
|
def test_run_worker_lifecycle_step_mode_confirmation(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""
|
|
|
|
|
|
Test that run_worker_lifecycle passes confirm_execution to ai_client.send when step_mode is True.
|
|
Verify that if confirm_execution is called (simulated by mocking ai_client.send to call its callback),
|
|
the flow works as expected.
|
|
"""
|
|
ticket = Ticket(id="T1", description="Task 1", status="todo", assigned_to="worker1", step_mode=True)
|
|
context = WorkerContext(ticket_id="T1", model_name="test-model", messages=[])
|
|
from src.multi_agent_conductor import run_worker_lifecycle
|
|
# Mock ai_client.send using monkeypatch
|
|
mock_send = MagicMock()
|
|
monkeypatch.setattr(ai_client, 'send', mock_send)
|
|
|
|
# Important: confirm_spawn is called first if event_queue is present!
|
|
with patch("src.multi_agent_conductor.confirm_spawn") as mock_spawn, \
|
|
patch("src.multi_agent_conductor.confirm_execution") as mock_confirm:
|
|
mock_spawn.return_value = (True, "mock prompt", "mock context")
|
|
mock_confirm.return_value = True
|
|
|
|
def mock_send_side_effect(md_content, user_message, **kwargs):
|
|
callback = kwargs.get("pre_tool_callback")
|
|
if callback:
|
|
# Simulate calling it with some payload
|
|
callback('{"tool": "read_file", "args": {"path": "test.txt"}}')
|
|
return Result(data="Success")
|
|
mock_send.side_effect = mock_send_side_effect
|
|
|
|
mock_event_queue = MagicMock()
|
|
run_worker_lifecycle(ticket, context, event_queue=mock_event_queue)
|
|
|
|
# Verify confirm_spawn was called because event_queue was present
|
|
mock_spawn.assert_called_once()
|
|
# Verify confirm_execution was called
|
|
mock_confirm.assert_called_once()
|
|
assert ticket.status == "completed"
|
|
|
|
def test_run_worker_lifecycle_step_mode_rejection(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""
|
|
|
|
|
|
Verify that if confirm_execution returns False, the logic (in ai_client, which we simulate here)
|
|
would prevent execution. In run_worker_lifecycle, we just check if it's passed.
|
|
"""
|
|
ticket = Ticket(id="T1", description="Task 1", status="todo", assigned_to="worker1", step_mode=True)
|
|
context = WorkerContext(ticket_id="T1", model_name="test-model", messages=[])
|
|
from src.multi_agent_conductor import run_worker_lifecycle
|
|
# Mock ai_client.send using monkeypatch
|
|
mock_send = MagicMock()
|
|
monkeypatch.setattr(ai_client, 'send', mock_send)
|
|
with patch("src.multi_agent_conductor.confirm_spawn") as mock_spawn, \
|
|
patch("src.multi_agent_conductor.confirm_execution") as mock_confirm:
|
|
mock_spawn.return_value = (True, "mock prompt", "mock context")
|
|
mock_confirm.return_value = False
|
|
mock_send.return_value = Result(data="Task failed because tool execution was rejected.")
|
|
|
|
mock_event_queue = MagicMock()
|
|
run_worker_lifecycle(ticket, context, event_queue=mock_event_queue)
|
|
|
|
# Verify it was passed to send
|
|
args, kwargs = mock_send.call_args
|
|
assert kwargs["pre_tool_callback"] is not None
|
|
|
|
def test_conductor_engine_dynamic_parsing_and_execution(monkeypatch: pytest.MonkeyPatch, vlogger) -> None:
|
|
"""
|
|
|
|
|
|
Test that parse_json_tickets correctly populates the track and run executes them in dependency order.
|
|
"""
|
|
import json
|
|
from src.multi_agent_conductor import ConductorEngine
|
|
track = Track(id="dynamic_track", description="Dynamic Track")
|
|
engine = ConductorEngine(track=track, auto_queue=True)
|
|
tickets_json = json.dumps([
|
|
{
|
|
"id": "T1",
|
|
"description": "Initial task",
|
|
"status": "todo",
|
|
"assigned_to": "worker1",
|
|
"depends_on": []
|
|
},
|
|
{
|
|
"id": "T2",
|
|
"description": "Dependent task",
|
|
"status": "todo",
|
|
"assigned_to": "worker2",
|
|
"depends_on": ["T1"]
|
|
},
|
|
{
|
|
"id": "T3",
|
|
"description": "Another initial task",
|
|
"status": "todo",
|
|
"assigned_to": "worker3",
|
|
"depends_on": []
|
|
}
|
|
])
|
|
engine.parse_json_tickets(tickets_json)
|
|
|
|
vlogger.log_state("Parsed Ticket Count", 0, len(engine.track.tickets))
|
|
assert len(engine.track.tickets) == 3
|
|
assert engine.track.tickets[0].id == "T1"
|
|
assert engine.track.tickets[1].id == "T2"
|
|
assert engine.track.tickets[2].id == "T3"
|
|
# Mock ai_client.send using monkeypatch
|
|
mock_send = MagicMock()
|
|
monkeypatch.setattr(ai_client, 'send', mock_send)
|
|
# Mock run_worker_lifecycle to mark tickets as complete
|
|
with patch("src.multi_agent_conductor.run_worker_lifecycle") as mock_lifecycle:
|
|
def side_effect(ticket, context, *args, **kwargs):
|
|
import threading
|
|
def do_work():
|
|
import time
|
|
time.sleep(0.1)
|
|
ticket.mark_complete()
|
|
threading.Thread(target=do_work).start()
|
|
return Result(data="Success")
|
|
mock_lifecycle.side_effect = side_effect
|
|
engine.run(max_ticks=20)
|
|
assert mock_lifecycle.call_count == 3
|
|
# Verify dependency order: T1 must be called before T2
|
|
calls = [call[0][0].id for call in mock_lifecycle.call_args_list]
|
|
t1_idx = calls.index("T1")
|
|
t2_idx = calls.index("T2")
|
|
|
|
vlogger.log_state("T1 Sequence Index", "N/A", t1_idx)
|
|
vlogger.log_state("T2 Sequence Index", "N/A", t2_idx)
|
|
|
|
assert t1_idx < t2_idx
|
|
# T3 can be anywhere relative to T1 and T2, but T1 < T2 is mandatory
|
|
assert "T3" in calls
|
|
vlogger.finalize("Dynamic track parsing and dependency execution", "PASS", "Dependency chain T1 -> T2 honored.")
|
|
|
|
def test_run_worker_lifecycle_pushes_response_via_queue(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""
|
|
|
|
|
|
Test that run_worker_lifecycle pushes a 'response' event with the correct stream_id
|
|
via _queue_put when event_queue is provided.
|
|
"""
|
|
ticket = Ticket(id="T1", description="Task 1", status="todo", assigned_to="worker1")
|
|
context = WorkerContext(ticket_id="T1", model_name="test-model", messages=[])
|
|
mock_event_queue = MagicMock()
|
|
mock_send = MagicMock(return_value=Result(data="Task complete."))
|
|
monkeypatch.setattr(ai_client, 'send', mock_send)
|
|
monkeypatch.setattr(ai_client, 'reset_session', MagicMock())
|
|
from src.multi_agent_conductor import run_worker_lifecycle
|
|
with patch("src.multi_agent_conductor.confirm_spawn") as mock_spawn, \
|
|
patch("src.multi_agent_conductor._queue_put") as mock_queue_put:
|
|
mock_spawn.return_value = (True, "prompt", "context")
|
|
run_worker_lifecycle(ticket, context, event_queue=mock_event_queue)
|
|
# ANTI-SIMPLIFICATION: Ensure exactly one 'response' event is put in the queue to avoid duplication loops.
|
|
assert mock_queue_put.call_count >= 1
|
|
call_args = mock_queue_put.call_args_list[0][0]
|
|
assert call_args[1] == "response"
|
|
assert call_args[2]["stream_id"] == "Tier 3 (Worker): T1"
|
|
assert call_args[2]["text"] == "Task complete."
|
|
assert call_args[2]["status"] == "done"
|
|
assert ticket.status == "completed"
|
|
|
|
def test_run_worker_lifecycle_token_usage_from_comms_log(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""
|
|
|
|
|
|
Test that run_worker_lifecycle reads token usage from the comms log and
|
|
updates engine.tier_usage['Tier 3'] with real input/output token counts.
|
|
"""
|
|
ticket = Ticket(id="T1", description="Task 1", status="todo", assigned_to="worker1")
|
|
context = WorkerContext(ticket_id="T1", model_name="test-model", messages=[])
|
|
fake_comms = [
|
|
{"direction": "OUT", "kind": "request", "payload": {"message": "hello"}},
|
|
{"direction": "IN", "kind": "response", "payload": {"usage": {"input_tokens": 120, "output_tokens": 45}}},
|
|
]
|
|
monkeypatch.setattr(ai_client, 'send', MagicMock(return_value=Result(data="Done.")))
|
|
monkeypatch.setattr(ai_client, 'reset_session', MagicMock())
|
|
monkeypatch.setattr(ai_client, 'get_comms_log', MagicMock(side_effect=[
|
|
[], # baseline call (before send)
|
|
fake_comms, # after-send call
|
|
]))
|
|
from src.multi_agent_conductor import run_worker_lifecycle, ConductorEngine
|
|
track = Track(id="test_track", description="Test")
|
|
engine = ConductorEngine(track=track, auto_queue=True)
|
|
with patch("src.multi_agent_conductor.confirm_spawn") as mock_spawn, \
|
|
patch("src.multi_agent_conductor._queue_put"):
|
|
mock_spawn.return_value = (True, "prompt", "ctx")
|
|
run_worker_lifecycle(ticket, context, event_queue=MagicMock(), engine=engine)
|
|
assert engine.tier_usage["Tier 3"]["input"] == 120
|
|
assert engine.tier_usage["Tier 3"]["output"] == 45 |