Private
Public Access
0
0
Files
manual_slop/simulation/sim_base.py
T
ed 72f8f466fe fix(sim+api): proper wait loops, project switch endpoint, drop stale check
Three real fixes for the sim test + the live_gui coordination layer:

1. /api/project_switch_status endpoint in src/app_controller.py.
   The wait helper had been calling this endpoint but it did not exist;
   the helper always received a 404, fell back to {in_progress: False},
   and returned immediately even when a switch was in flight. Added the
   endpoint that reads _project_switch_in_progress, active_project_path,
   and _project_switch_error from the controller.

2. simulation/sim_base.py: replace time.sleep(2.0)/time.sleep(1.5) in
   the setup() with wait_io_pool_idle and wait_for_project_switch so
   the test does not click btn_md_only while a project switch is in
   flight. Also added the wait calls to sim_context.py for the same
   reason.

3. src/app_controller.py _handle_md_only: removed the is_project_stale()
   early-return. The stale state is a transient window during which the
   previous code dropped the click on the floor with a misleading
   'stale ui' status. The MD generation worker is safe to run from any
   project state; the action handler now always proceeds.

4. tests/test_extended_sims.py: set current_model to 'gemini-cli' so
   _do_generate does not raise KeyError('model') when the test
   overrides provider to gemini_cli.

KNOWN ISSUE: test_context_sim_live still fails with status
'switching to: temp_livecontextsim' after a 60s wait. The click
appears to be re-triggering a project switch via the GUI's render
loop. Root cause investigation deferred; the sim is async and the
test path is fragile.
2026-06-10 00:31:22 -04:00

154 lines
7.0 KiB
Python

"""
Base Simulation Framework - Abstract base class for GUI automation tests.
This module provides the foundation for all simulation-based tests in the
Manual Slop test suite. Simulations act as external "puppeteers" that drive
the GUI through the ApiHookClient HTTP interface.
Architecture:
- BaseSimulation: Abstract base class with setup/teardown lifecycle
- WorkflowSimulator: High-level workflow operations (project setup, file mgmt)
- ApiHookClient: Low-level HTTP client for Hook API communication
Typical Usage:
class MySimulation(BaseSimulation):
def run(self) -> None:
self.client.set_value('mma_epic_input', 'My epic description')
self.client.click('btn_mma_plan_epic')
# Poll for completion...
status = self.client.get_mma_status()
assert status['mma_status'] == 'done'
if __name__ == '__main__':
run_sim(MySimulation)
Lifecycle:
1. setup() - Connects to GUI, resets session, scaffolds temp project
2. run() - Implemented by subclass with simulation logic
3. teardown() - Cleanup (optional file retention for debugging)
Prerequisites:
- GUI must be running with --enable-test-hooks flag
- HookServer must be listening on http://127.0.0.1:8999
Thread Safety:
- Simulations are designed to run in the main thread
- ApiHookClient handles its own connection pooling
See Also:
- simulation/workflow_sim.py for WorkflowSimulator
- tests/conftest.py for live_gui pytest fixture
- docs/guide_simulations.md for full simulation documentation
"""
import sys
import os
import time
from typing import Any, Optional
from api_hook_client import ApiHookClient
from simulation.workflow_sim import WorkflowSimulator
# Ensure project root and src/ are in path
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
sys.path.append(project_root)
sys.path.append(os.path.join(project_root, "src"))
class BaseSimulation:
def __init__(self, client: ApiHookClient = None) -> None:
if client is None:
self.client = ApiHookClient()
else:
self.client = client
self.sim = WorkflowSimulator(self.client)
self.project_path = None
def setup(self, project_name: str = "SimProject") -> None:
"""
[C: simulation/sim_execution.py:ExecutionSimulation.setup, tests/test_extended_sims.py:test_ai_settings_sim_live, tests/test_extended_sims.py:test_context_sim_live, tests/test_extended_sims.py:test_execution_sim_live, tests/test_extended_sims.py:test_tools_sim_live, tests/test_sim_base.py:test_base_simulation_setup]
"""
print("\n[BaseSim] Connecting to GUI...")
if not self.client.wait_for_server(timeout=5):
raise RuntimeError("Could not connect to GUI. Ensure it is running with --enable-test-hooks")
self.client.clear_events()
self.client.set_value("auto_add_history", True)
# Wait for propagation
_start = time.time()
while time.time() - _start < 5.0:
if self.client.get_value("auto_add_history") is True:
break
time.sleep(0.1)
print("[BaseSim] Resetting session...")
self.client.click("btn_reset")
# Wait for the reset to fully complete (session reset is async via io_pool).
self.client.wait_io_pool_idle(timeout=10.0)
git_dir = os.path.abspath(".")
self.project_path = os.path.abspath(f"tests/artifacts/temp_{project_name.lower()}.toml")
if os.path.exists(self.project_path):
os.remove(self.project_path)
print(f"[BaseSim] Scaffolding Project: {project_name}")
self.sim.setup_new_project(project_name, git_dir, self.project_path)
# CRITICAL: wait for the project switch to fully complete. The switch
# is async via the ProjectSwitchState machine, NOT the io_pool, so
# wait_io_pool_idle does not suffice. Without this wait, subsequent
# clicks like btn_md_only hit the "is_project_stale" early-return and
# the test fails with a misleading "stale ui" status.
self.client.wait_for_project_switch(expected_path=self.project_path, timeout=30.0)
# Standard test settings
self.client.set_value("current_provider", "gemini")
self.client.set_value("current_model", "gemini-2.5-flash-lite")
self.client.wait_io_pool_idle(timeout=10.0)
self.client.wait_io_pool_idle(timeout=10.0)
def teardown(self) -> None:
"""
[C: tests/test_extended_sims.py:test_ai_settings_sim_live, tests/test_extended_sims.py:test_context_sim_live, tests/test_extended_sims.py:test_execution_sim_live, tests/test_extended_sims.py:test_tools_sim_live]
"""
if self.project_path and os.path.exists(self.project_path):
# We keep it for debugging if it failed, but usually we'd clean up
# os.remove(self.project_path)
pass
print("[BaseSim] Teardown complete.")
def get_value(self, tag: str) -> Any:
"""
[C: simulation/sim_context.py:ContextSimulation.run, simulation/sim_execution.py:ExecutionSimulation.run, simulation/workflow_sim.py:WorkflowSimulator.run_discussion_turn_async, simulation/workflow_sim.py:WorkflowSimulator.wait_for_ai_response, tests/smoke_status_hook.py:test_status_hook, tests/smoke_status_hook.py:wait_for_value, tests/test_auto_switch_sim.py:test_auto_switch_sim, tests/test_deepseek_infra.py:test_gui_provider_list_via_hooks, tests/test_extended_sims.py:test_ai_settings_sim_live, tests/test_gui2_parity.py:test_gui2_click_hook_works, tests/test_gui2_parity.py:test_gui2_set_value_hook_works, tests/test_rag_phase4_final_verify.py:test_phase4_final_verify, tests/test_rag_phase4_stress.py:test_rag_large_codebase_verification_sim, tests/test_rag_visual_sim.py:test_rag_full_lifecycle_sim, tests/test_rag_visual_sim.py:test_rag_settings_persistence_sim, tests/test_selectable_ui.py:test_selectable_label_stability, tests/test_system_prompt_sim.py:test_system_prompt_sim, tests/test_undo_redo_sim.py:test_undo_redo_context_mutation, tests/test_undo_redo_sim.py:test_undo_redo_discussion_mutation, tests/test_undo_redo_sim.py:test_undo_redo_lifecycle, tests/test_workspace_profiles_sim.py:test_workspace_profiles_restoration]
"""
return self.client.get_value(tag)
def wait_for_event(self, event_type: str, timeout: int = 5) -> Optional[dict]:
"""
[C: simulation/sim_execution.py:ExecutionSimulation.run, tests/test_z_negative_flows.py:test_mock_error_result, tests/test_z_negative_flows.py:test_mock_malformed_json, tests/test_z_negative_flows.py:test_mock_timeout]
"""
return self.client.wait_for_event(event_type, timeout)
def assert_panel_visible(self, panel_tag: str, msg: str = None) -> None:
pass
def wait_for_element(self, tag: str, timeout: int = 2) -> bool:
start = time.time()
while time.time() - start < timeout:
try:
# If we can get_value without error, it's likely there
self.client.get_value(tag)
return True
except:
time.sleep(0.1)
return False
def run_sim(sim_class: type) -> None:
"""
Helper to run a simulation class standalone.
[C: simulation/sim_context.py:module, simulation/sim_execution.py:module, simulation/sim_tools.py:module]
"""
sim = sim_class()
try:
sim.setup()
sim.run()
print(f"\n[SUCCESS] {sim_class.__name__} completed successfully.")
except Exception as e:
print(f"\n[FAILURE] {sim_class.__name__} failed: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
finally:
sim.teardown()