From 6b18474cdf82e06ee44986fbe225c0c912830584 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Sat, 2 May 2026 13:57:40 -0400 Subject: [PATCH] test(sim): Add stress test for concurrent MMA tracks --- .../test_mma_concurrent_tracks_stress_sim.py | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 tests/test_mma_concurrent_tracks_stress_sim.py diff --git a/tests/test_mma_concurrent_tracks_stress_sim.py b/tests/test_mma_concurrent_tracks_stress_sim.py new file mode 100644 index 0000000..c91ebea --- /dev/null +++ b/tests/test_mma_concurrent_tracks_stress_sim.py @@ -0,0 +1,96 @@ +import pytest +import time +import sys +import os + +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src"))) + +from src import api_hook_client + +def _poll_mma_workers(client: api_hook_client.ApiHookClient, timeout: int, condition, label: str) -> tuple[bool, dict]: + """Poll get_mma_workers() until condition(workers) is True or timeout.""" + workers = {} + for i in range(timeout): + res = client.get_mma_workers() or {} + workers = res.get('workers', {}) + print(f"[SIM][{label}] t={i}s active_workers={list(workers.keys())}") + if condition(workers): + return True, workers + time.sleep(1) + return False, workers + +@pytest.mark.integration +@pytest.mark.timeout(600) +def test_mma_concurrent_tracks_stress(live_gui) -> None: + """ + Stress test: Start two tracks concurrently and verify they both progress + without crashing the GUI or losing state. + """ + client = api_hook_client.ApiHookClient() + assert client.wait_for_server(timeout=15), "Hook server did not start" + + # 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.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') + + # Wait for tracks to be generated and accepted + # Note: Epic planning usually generates multiple tracks. + start = time.time() + while time.time() - start < 60: + status = client.get_mma_status() + if status.get('proposed_tracks'): + break + time.sleep(1) + + client.click('btn_mma_accept_tracks') + time.sleep(2.0) + + # 3. Get track IDs + status = client.get_mma_status() + tracks = status.get('tracks', []) + assert len(tracks) >= 2, f"Need at least 2 tracks for stress test, found {len(tracks)}" + track_id_a = tracks[0]['id'] + track_id_b = tracks[1]['id'] + + # 4. Start both tracks + print(f"[SIM] Starting Track A: {track_id_a}") + client.click('btn_mma_load_track', user_data=track_id_a) + time.sleep(0.5) + client.click('btn_mma_start_track', user_data=track_id_a) + + print(f"[SIM] Starting Track B: {track_id_b}") + client.click('btn_mma_load_track', user_data=track_id_b) + time.sleep(0.5) + client.click('btn_mma_start_track', user_data=track_id_b) + + # 5. Verify workers from both tracks appear + # Workers are named "Tier 3 (Worker): " + ok, workers = _poll_mma_workers(client, timeout=30, label="wait-workers", + condition=lambda w: any(track_id_a in str(k) for k in w) and any(track_id_b in str(k) for k in w)) + + # Note: ticket_id might not contain track_id directly, but the mock epic generator + # usually includes some identifier. If not, we just check for multiple Tier 3 workers. + if not ok: + print("[SIM] WARNING: Could not explicitly find both tracks in worker names, checking for multiple workers instead.") + ok, workers = _poll_mma_workers(client, timeout=30, label="wait-multi-workers", + condition=lambda w: sum(1 for k in w if "Tier 3" in k) >= 2) + + assert ok, f"Did not see concurrent workers starting. Active: {list(workers.keys())}" + + # 6. Wait for completion + print("[SIM] Waiting for all workers to finish...") + ok, workers = _poll_mma_workers(client, timeout=120, label="wait-completion", + condition=lambda w: all("COMPLETED" in str(v) or "FAILED" in str(v) for v in w.values()) if w else False) + + # Final check: GUI should still be responsive + res = client.get_status() + assert res.get('status') == 'ok', "GUI crashed during concurrent execution" + + print("[SIM] MMA Concurrent Tracks stress test PASSED.")