Private
Public Access
0
0

fix(sim): defensive .setdefault('paths', []) in test_context_sim_live

This commit is contained in:
2026-06-10 11:33:15 -04:00
parent c729f8adaf
commit 4660b8c874
4 changed files with 26 additions and 79 deletions
+1 -1
View File
@@ -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'])
+3 -12
View File
@@ -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]:
"""
+20 -61
View File
@@ -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()
+2 -5
View File
@@ -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."""