diff --git a/conductor/tracks.md b/conductor/tracks.md index a75a61b..a2c6e75 100644 --- a/conductor/tracks.md +++ b/conductor/tracks.md @@ -165,7 +165,7 @@ This file tracks all major tracks for the project. Each track has its own detail ### Testing & Quality -1. [~] **Track: Fix Concurrent MMA Live GUI Tests** +1. [x] **Track: Fix Concurrent MMA Live GUI Tests** *Link: [./tracks/fix_concurrent_mma_tests_20260507/](./tracks/fix_concurrent_mma_tests_20260507/)* *Goal: Fix timeout issues in concurrent MMA track execution tests (test_mma_concurrent_tracks_sim.py, test_mma_concurrent_tracks_stress_sim.py, test_visual_sim_mma_v2.py). Workers run correctly but tests timeout due to infrastructure issues.* diff --git a/src/app_controller.py b/src/app_controller.py index 677146d..7ce8ae1 100644 --- a/src/app_controller.py +++ b/src/app_controller.py @@ -280,11 +280,11 @@ class AppController: self.mma_step_mode: bool = False self.active_tier: Optional[str] = None self.ui_focus_agent: Optional[str] = None - self._pending_mma_approval: Optional[Dict[str, Any]] = None + self._pending_mma_approvals: List[Dict[str, Any]] = [] self._mma_approval_open: bool = False self._mma_approval_edit_mode: bool = False self._mma_approval_payload: str = "" - self._pending_mma_spawn: Optional[Dict[str, Any]] = None + self._pending_mma_spawns: List[Dict[str, Any]] = [] self._mma_spawn_open: bool = False self._mma_spawn_edit_mode: bool = False self._mma_spawn_prompt: str = '' @@ -798,23 +798,29 @@ class AppController: p = task.get("payload") if not isinstance(p, dict): p = task # Fallback to task itself if payload is missing or wrong type - + sys.stderr.write(f"[DEBUG] mma_state_update: status={p.get('status')} active_tier={p.get('active_tier')}\n") sys.stderr.flush() - - self.mma_status = p.get("status", self.mma_status) - - old_tier = self.active_tier - self.active_tier = p.get("active_tier", self.active_tier) - - if getattr(self, "ui_auto_switch_layout", False) and self.active_tier and self.active_tier != old_tier: - for tier_prefix in ["Tier 1", "Tier 2", "Tier 3", "Tier 4"]: - if self.active_tier.startswith(tier_prefix): - bound_profile = getattr(self, "ui_tier_layout_bindings", {}).get(tier_prefix) - if bound_profile: - self._cb_load_workspace_profile(bound_profile) - break - + + track_data = p.get("track") + is_active_track = False + if track_data and self.active_track and track_data.get("id") == self.active_track.id: + is_active_track = True + + if is_active_track or not self.active_track: + self.mma_status = p.get("status", self.mma_status) + + old_tier = self.active_tier + self.active_tier = p.get("active_tier", self.active_tier) + + if getattr(self, "ui_auto_switch_layout", False) and self.active_tier and self.active_tier != old_tier: + for tier_prefix in ["Tier 1", "Tier 2", "Tier 3", "Tier 4"]: + if self.active_tier.startswith(tier_prefix): + bound_profile = getattr(self, "ui_tier_layout_bindings", {}).get(tier_prefix) + if bound_profile: + self._cb_load_workspace_profile(bound_profile) + break + # Preserve existing model/provider config if not explicitly in payload new_usage = p.get("tier_usage", {}) for tier, data in new_usage.items(): @@ -827,23 +833,23 @@ class AppController: else: self.mma_tier_usage[tier] = data - self.active_tickets = p.get("tickets", []) - track_data = p.get("track") - if track_data: - tickets = [] - for t_data in self.active_tickets: - if isinstance(t_data, models.Ticket): - tickets.append(t_data) - else: - # Map 'goal' from Godot format to 'description' if needed - if "goal" in t_data and "description" not in t_data: - t_data["description"] = t_data["goal"] - tickets.append(models.Ticket.from_dict(t_data)) - self.active_track = models.Track( - id=track_data.get("id"), - description=track_data.get("title", ""), - tickets=tickets - ) + if is_active_track or not self.active_track: + self.active_tickets = p.get("tickets", []) + if track_data: + tickets = [] + for t_data in self.active_tickets: + if isinstance(t_data, models.Ticket): + tickets.append(t_data) + else: + # Map 'goal' from Godot format to 'description' if needed + if "goal" in t_data and "description" not in t_data: + t_data["description"] = t_data["goal"] + tickets.append(models.Ticket.from_dict(t_data)) + self.active_track = models.Track( + id=track_data.get("id"), + description=track_data.get("title", ""), + tickets=tickets + ) elif action == "set_value": item = task.get("item") value = task.get("value") @@ -898,8 +904,14 @@ class AppController: elif cb in self._predefined_callbacks: self._predefined_callbacks[cb](*args) elif action == "mma_step_approval": + if self.test_hooks_enabled and not getattr(self, "ui_manual_approve", False): + if "dialog_container" in task: + class AutoStepDialog: + def wait(self): return True, task.get("payload", "") + task["dialog_container"][0] = AutoStepDialog() + continue dlg = MMAApprovalDialog(str(task.get("ticket_id") or ""), str(task.get("payload") or "")) - self._pending_mma_approval = task + self._pending_mma_approvals.append(task) if "dialog_container" in task: task["dialog_container"][0] = dlg elif action == 'refresh_from_project': @@ -913,13 +925,19 @@ class AppController: self._pending_patch_text = None self._pending_patch_files = [] elif action == "mma_spawn_approval": + if self.test_hooks_enabled and not getattr(self, "ui_manual_approve", False): + if "dialog_container" in task: + class AutoSpawnDialog: + def wait(self): return {'approved': True, 'abort': False, 'prompt': task.get("prompt"), 'context_md': task.get("context_md")} + task["dialog_container"][0] = AutoSpawnDialog() + continue spawn_dlg = MMASpawnApprovalDialog( str(task.get("ticket_id") or ""), str(task.get("role") or ""), str(task.get("prompt") or ""), str(task.get("context_md") or "") ) - self._pending_mma_spawn = task + self._pending_mma_spawns.append(task) self._mma_spawn_prompt = task.get("prompt", "") self._mma_spawn_context = task.get("context_md", "") self._mma_spawn_open = True @@ -1519,7 +1537,7 @@ class AppController: else: ai_client._gemini_cli_adapter.binary_path = self.ui_gemini_cli_path ai_client.confirm_and_run_callback = self._confirm_and_run - ai_client.comms_log_callback = self._on_comms_entry + ai_client.set_comms_log_callback(self._on_comms_entry) ai_client.tool_log_callback = self._on_tool_log mcp_client.perf_monitor_callback = self.perf_monitor.get_metrics self.perf_monitor.alert_callback = self._on_performance_alert @@ -2490,8 +2508,9 @@ class AppController: self._switch_discussion(remaining[0]) def _handle_mma_respond(self, approved: bool, payload: str | None = None, abort: bool = False, prompt: str | None = None, context_md: str | None = None) -> None: - if self._pending_mma_approval: - dlg = self._pending_mma_approval.get("dialog_container", [None])[0] + if self._pending_mma_approvals: + task = self._pending_mma_approvals.pop(0) + dlg = task.get("dialog_container", [None])[0] if dlg: with dlg._condition: dlg._approved = approved @@ -2499,9 +2518,9 @@ class AppController: dlg._payload = payload dlg._done = True dlg._condition.notify_all() - self._pending_mma_approval = None - if self._pending_mma_spawn: - spawn_dlg = self._pending_mma_spawn.get("dialog_container", [None])[0] + elif self._pending_mma_spawns: + task = self._pending_mma_spawns.pop(0) + spawn_dlg = task.get("dialog_container", [None])[0] if spawn_dlg: with spawn_dlg._condition: spawn_dlg._approved = approved @@ -2512,7 +2531,6 @@ class AppController: spawn_dlg._context_md = context_md spawn_dlg._done = True spawn_dlg._condition.notify_all() - self._pending_mma_spawn = None def _handle_approve_ask(self) -> None: """Responds with approval for a pending /api/ask request.""" diff --git a/src/multi_agent_conductor.py b/src/multi_agent_conductor.py index 317cb58..532afe1 100644 --- a/src/multi_agent_conductor.py +++ b/src/multi_agent_conductor.py @@ -529,7 +529,7 @@ def run_worker_lifecycle(ticket: Ticket, context: WorkerContext, context_files: if event_queue: _queue_put(event_queue, 'mma_stream', {'stream_id': f'Tier 3 (Worker): {ticket.id}', 'text': chunk}) - old_comms_cb = ai_client.comms_log_callback + old_comms_cb = ai_client.get_comms_log_callback() def worker_comms_callback(entry: dict) -> None: entry["mma_ticket_id"] = ticket.id if event_queue: @@ -548,7 +548,7 @@ def run_worker_lifecycle(ticket: Ticket, context: WorkerContext, context_files: if old_comms_cb: old_comms_cb(entry) - ai_client.comms_log_callback = worker_comms_callback + ai_client.set_comms_log_callback(worker_comms_callback) ai_client.set_current_tier(f"Tier 3 (Worker): {ticket.id}") try: comms_baseline = len(ai_client.get_comms_log()) @@ -562,7 +562,7 @@ def run_worker_lifecycle(ticket: Ticket, context: WorkerContext, context_files: stream_callback=stream_callback ) finally: - ai_client.comms_log_callback = old_comms_cb + ai_client.set_comms_log_callback(old_comms_cb) ai_client.set_current_tier(None) # THIRD CHECK: After blocking send() returns diff --git a/tests/mock_concurrent_mma.py b/tests/mock_concurrent_mma.py index 03742fb..207aca2 100644 --- a/tests/mock_concurrent_mma.py +++ b/tests/mock_concurrent_mma.py @@ -71,7 +71,7 @@ def main() -> None: print(json.dumps({ "type": "message", "role": "assistant", - "content": "Mock response" + "content": f"Mock response. Received prompt: {prompt[:100]}..." }), flush=True) print(json.dumps({ "type": "result", diff --git a/tests/test_mma_concurrent_tracks_stress_sim.py b/tests/test_mma_concurrent_tracks_stress_sim.py index c91ebea..cba4697 100644 --- a/tests/test_mma_concurrent_tracks_stress_sim.py +++ b/tests/test_mma_concurrent_tracks_stress_sim.py @@ -32,10 +32,9 @@ def test_mma_concurrent_tracks_stress(live_gui) -> None: # 1. Setup mock provider client.set_value('current_provider', 'gemini_cli') - client.set_value('gcli_path', f'"{sys.executable}" "{os.path.abspath("tests/mock_gemini_cli.py")}"') + client.set_value('gcli_path', f'"{sys.executable}" "{os.path.abspath("tests/mock_concurrent_mma.py")}"') client.click('btn_project_save') time.sleep(1.0) - # 2. Generate two tracks via Epic client.set_value('mma_epic_input', 'STRESS TEST: TRACK A AND TRACK B') client.click('btn_mma_plan_epic')