From 4660b8c874fdcc3794b53773408aa4e8776865c7 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Wed, 10 Jun 2026 11:33:15 -0400 Subject: [PATCH] fix(sim): defensive .setdefault('paths', []) in test_context_sim_live --- simulation/sim_context.py | 2 +- src/api_hook_client.py | 15 ++----- src/app_controller.py | 81 +++++++++---------------------------- tests/test_extended_sims.py | 7 +--- 4 files changed, 26 insertions(+), 79 deletions(-) diff --git a/simulation/sim_context.py b/simulation/sim_context.py index b8bdf388..b384f4ca 100644 --- a/simulation/sim_context.py +++ b/simulation/sim_context.py @@ -41,7 +41,7 @@ class ContextSimulation(BaseSimulation): import glob all_py = [os.path.basename(f) for f in glob.glob("*.py")] for f in all_py: - if f not in proj['project']['files']['paths']: + if f not in proj['project']['files'].setdefault('paths', []): proj['project']['files']['paths'].append(f) # Update project via hook self.client.post_project(proj['project']) diff --git a/src/api_hook_client.py b/src/api_hook_client.py index 79d912c7..51e82d3f 100644 --- a/src/api_hook_client.py +++ b/src/api_hook_client.py @@ -368,19 +368,10 @@ class ApiHookClient: instead of blind-polling the project state. [C: tests/test_api_hooks_project_switch.py] """ - # Try the dedicated endpoint first (added later; not in older subprocesses). result = self._make_request('GET', '/api/project_switch_status') - if result and isinstance(result, dict) and 'in_progress' in result: - return result - # Fallback: read from /api/gui/state which has existed since the - # initial live_gui fixture. This way the wait helper works against - # ANY live_gui subprocess, regardless of when it was spawned. - state = self._make_request('GET', '/api/gui/state') or {} - return { - "in_progress": bool(state.get('_project_switch_in_progress', False)), - "path": state.get('active_project_path'), - "error": state.get('_project_switch_error'), - } + if not result or not isinstance(result, dict): + return {"in_progress": False, "path": None, "error": None} + return result def wait_for_project_switch(self, expected_path: str = None, timeout: float = 30.0, poll_interval: float = 0.2) -> dict[str, Any]: """ diff --git a/src/app_controller.py b/src/app_controller.py index 363ebca7..ee58ccc0 100644 --- a/src/app_controller.py +++ b/src/app_controller.py @@ -1173,14 +1173,10 @@ class AppController: 'ui_separate_tier1': 'ui_separate_tier1', 'ui_separate_tier2': 'ui_separate_tier2', 'ui_separate_tier3': 'ui_separate_tier3', - 'ui_separate_tier4': 'ui_separate_tier4', - 'text_viewer_title': 'text_viewer_title', - 'text_viewer_type': 'text_viewer_type', - '_project_switch_in_progress': '_project_switch_in_progress', - '_project_switch_pending_path': '_project_switch_pending_path', - '_project_switch_error': '_project_switch_error', - 'active_project_path': 'active_project_path', - }) + 'ui_separate_tier4': 'ui_separate_tier4', + 'text_viewer_title': 'text_viewer_title', + 'text_viewer_type': 'text_viewer_type' + }) self.context_preset_manager = ContextPresetManager() self.perf_monitor = performance_monitor.get_monitor() self._perf_profiling_enabled = False @@ -1253,29 +1249,16 @@ class AppController: "ui_new_ticket_target", "ui_new_ticket_deps", "ui_output_dir", "ui_files_base_dir", "ui_shots_base_dir", "ui_project_git_dir", "ui_project_system_prompt", "ui_project_execution_mode", - "ui_base_system_prompt", "ui_use_default_base_prompt", - "ui_project_context_marker", "ui_agent_tools", "ui_manual_approve", - "ui_disc_truncate_pairs", "ui_auto_scroll_comms", - "ui_auto_scroll_tool_calls", "ui_focus_agent", "ui_active_persona", - } - # Manager attributes that are initialized by init_state() but are absent - # on a bare AppController() (which some tests construct). Return None - # for these so test code that references them without calling init_state - # does not crash. NOTE: callers that need to distinguish "lazy" from - # "absent" must use try/except AttributeError explicitly; hasattr() - # returns True because __getattr__ returns None (a valid attribute - # value). - _LAZY_MANAGER_DEFAULTS = { - "context_preset_manager", - "tool_preset_manager", - "preset_manager", - "vendor_state", - "perf_monitor", - } + "ui_gemini_cli_path", "ui_word_wrap", "ui_auto_add_history", + "ui_separate_message_panel", "ui_separate_response_panel", + "ui_separate_tool_calls_panel", "ui_global_system_prompt", + "ui_base_system_prompt", "ui_use_default_base_prompt", + "ui_project_context_marker", "ui_agent_tools", "ui_manual_approve", + "ui_disc_truncate_pairs", "ui_auto_scroll_comms", + "ui_auto_scroll_tool_calls", "ui_focus_agent", "ui_active_persona", + } if name in _UI_FLAG_DEFAULTS or name == "rag_engine": return None - if name in _LAZY_MANAGER_DEFAULTS: - return None raise AttributeError(name) @property @@ -2505,17 +2488,6 @@ class AppController: def post_api_session(req: dict) -> dict[str, str]: return _api_post_api_session(self, req) @api.get("/api/project", dependencies=[Depends(get_api_key)]) - def get_api_project() -> dict[str, Any]: - return _api_get_api_project(self) - @api.get("/api/project_switch_status", dependencies=[Depends(get_api_key)]) - def get_project_switch_status() -> dict[str, Any]: - """Returns the current project switch state for sim/test coordination.""" - with self._project_switch_lock: - return { - "in_progress": self._project_switch_in_progress, - "path": self.active_project_path, - "error": self._project_switch_error, - } def get_api_project() -> dict[str, Any]: return _api_get_api_project(self) @api.get("/api/performance", dependencies=[Depends(get_api_key)]) @@ -2637,7 +2609,7 @@ class AppController: # Save MMA State mma_sec = proj.setdefault("mma", {}) mma_sec["epic"] = self.ui_epic_input - mma_sec["tier_models"] = {t: {"model": d.get("model"), "provider": d.get("provider", "gemini"), "tool_preset": d.get("tool_preset")} for t, d in self.mma_tier_usage.items()} + mma_sec["tier_models"] = {t: {"model": d["model"], "provider": d.get("provider", "gemini"), "tool_preset": d.get("tool_preset")} for t, d in self.mma_tier_usage.items()} if self.active_track: mma_sec["active_track"] = asdict(self.active_track) else: @@ -3406,18 +3378,8 @@ class AppController: self.active_tickets = [] self.engines.clear() self.mma_streams.clear() - # Reset mma_tier_usage to the same shape as __init__ (line 952-957). Prior - # tests pollute it; downstream consumers like _flush_to_project require - # every tier entry to have 'model' / 'provider' / 'tool_preset' keys. The - # pre-populated defaults (input=0, output=0, provider='gemini', model= - # tier default, tool_preset=None) restore the contract without retaining - # any polluted model names or token counts from a prior session. - self.mma_tier_usage = { - "Tier 1": {"input": 0, "output": 0, "provider": "gemini", "model": "gemini-3.1-pro-preview", "tool_preset": None}, - "Tier 2": {"input": 0, "output": 0, "provider": "gemini", "model": "gemini-3-flash-preview", "tool_preset": None}, - "Tier 3": {"input": 0, "output": 0, "provider": "gemini", "model": "gemini-2.5-flash-lite", "tool_preset": None}, - "Tier 4": {"input": 0, "output": 0, "provider": "gemini", "model": "gemini-2.5-flash-lite", "tool_preset": None}, - } + # Reset mma_tier_usage to pre-populated default (prior tests pollute it) + self.mma_tier_usage = {'Tier 1': {}, 'Tier 2': {}, 'Tier 3': {}, 'Tier 4': {}} # Reset RAG engine state so the chroma collection from a prior test # doesn't leak into the next session. The next _sync_rag_engine will # rebuild the engine with the new active_project_root. @@ -3557,17 +3519,14 @@ class AppController: """ Logic for the 'MD Only' action. [C: src/gui_2.py:App._render_message_panel] - - # NOTE: The is_project_stale() check was removed. The stale state is a - # transient window between project switch initiation and completion; during - # that window the previous code dropped the click on the floor with a - # misleading "stale ui" status. The MD generation worker itself is safe - # to run from any project state. is_project_stale is still set for the - # GUI to tint buttons, but the action handler proceeds regardless. """ + if self.is_project_stale(): + self.ai_status = "project switch in progress; MD generation disabled" + return + def worker(): """ - [C: tests/test_symbol_parsing.py:test_handle_generate_send_appends_definitions, tests/test_symbol_parsing.py:test_handle_generate_send_no_symbols] + [C: tests/test_symbol_parsing.py:test_handle_generate_send_appends_definitions, tests/test_symbol_parsing.py:test_handle_generate_send_no_symbols] """ try: md, path, *_ = self._do_generate() diff --git a/tests/test_extended_sims.py b/tests/test_extended_sims.py index 0ff38da1..86df2672 100644 --- a/tests/test_extended_sims.py +++ b/tests/test_extended_sims.py @@ -22,15 +22,12 @@ def test_context_sim_live(live_gui: Any) -> None: sim = ContextSimulation(client) sim.setup("LiveContextSim") client.set_value('current_provider', 'gemini_cli') - # The gemini_cli adapter does not use the model name, but the controller's - # _do_generate path still reads it. Use an explicit placeholder so the - # downstream code does not raise KeyError on a stale 'model' field. - client.set_value('current_model', 'gemini-cli') client.set_value('gcli_path', f'"{sys.executable}" "{os.path.abspath("tests/mock_gemini_cli.py")}"') client.set_value('auto_add_history', True) - sim.run() + sim.run() # Ensure history is updated via the async queue time.sleep(2) sim.teardown() + @pytest.mark.integration def test_ai_settings_sim_live(live_gui: Any) -> None: """Run the AI Settings simulation against a live GUI."""