import pytest from typing import Any import time import sys import os # Ensure project root is in path 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 api_hook_client import ApiHookClient from simulation.sim_context import ContextSimulation from simulation.sim_ai_settings import AISettingsSimulation from simulation.sim_tools import ToolsSimulation from simulation.sim_execution import ExecutionSimulation @pytest.mark.integration def test_context_sim_live(live_gui: Any) -> None: """Run the Context & Chat simulation against a live GUI.""" client = ApiHookClient() assert client.wait_for_server(timeout=10) sim = ContextSimulation(client) sim.setup("LiveContextSim") 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('auto_add_history', True) 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.""" client = ApiHookClient() assert client.wait_for_server(timeout=10) sim = AISettingsSimulation(client) sim.setup("LiveAISettingsSim") client.set_value('current_provider', 'gemini_cli') client.set_value('gcli_path', f'"{sys.executable}" "{os.path.abspath("tests/mock_gemini_cli.py")}"') # Expect gemini_cli as the provider client.set_value('auto_add_history', True) assert client.get_value('current_provider') == 'gemini_cli' sim.run() sim.teardown() @pytest.mark.integration def test_tools_sim_live(live_gui: Any) -> None: """Run the Tools & Search simulation against a live GUI.""" client = ApiHookClient() assert client.wait_for_server(timeout=10) sim = ToolsSimulation(client) sim.setup("LiveToolsSim") 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('auto_add_history', True) sim.run() # Ensure history is updated via the async queue time.sleep(2) sim.teardown() @pytest.mark.integration def test_execution_sim_live(live_gui: Any) -> None: """Run the Execution & Modals simulation against a live GUI.""" client = ApiHookClient() assert client.wait_for_server(timeout=10) sim = ExecutionSimulation(client) sim.setup("LiveExecutionSim") # Enable manual approval to test modals client.set_value('manual_approve', True) client.set_value('current_provider', 'gemini') client.set_value('current_model', 'gemini-2.5-flash-lite') client.set_value('auto_add_history', True) sim.run() time.sleep(2) sim.teardown() def test_render_response_panel_defers_set_window_focus() -> None: """Regression test for the GUI subprocess STATUS_STACK_OVERFLOW crash. Captures the root cause of the `test_execution_sim_live` failure: the `render_response_panel` function used to call `imgui.set_window_focus("Response")` directly during the render frame. On Windows, the GUI subprocess's main thread has only 1.94 MB of stack (set by Python's PE header). imgui-bundle's native focus call uses ~2-3 MB of C stack, which exceeds the committed size and triggers `0xC00000FD = STATUS_STACK_OVERFLOW`. The contract enforced here: the render body MUST defer the call to the next frame's idle phase via a one-shot `_pending_focus_response` flag. The fix mirrors the existing `_autofocus_response_tab` pattern (see `src/gui_2.py:5353-5356`). Note: the source function is `render_response_panel` (no underscore). The `_render_response_panel` label in the bug report is the perf-monitor component name, not the function name. """ import re gui_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src", "gui_2.py")) with open(gui_path, "r", encoding="utf-8") as f: src = f.read() # Extract the render_response_panel function body. Non-greedy match up # to the next top-level def (or end of file) so nested defs/comments # inside the body are not treated as the boundary. match = re.search( r"def render_response_panel\(app: App\) -> None:.*?(?=\ndef |\Z)", src, re.DOTALL, ) assert match is not None, "render_response_panel function not found in src/gui_2.py" body = match.group(0) rest = src[:match.start()] + src[match.end():] # 1. The render body MUST NOT call imgui.set_window_focus("Response") # directly. A direct call during the render frame exhausts the 1.94 MB # main thread stack of the GUI subprocess and causes STATUS_STACK_OVERFLOW. assert 'imgui.set_window_focus("Response")' not in body, ( "render_response_panel still calls imgui.set_window_focus('Response') directly. " "This exhausts the 1.94 MB main thread stack of the GUI subprocess and causes " "STATUS_STACK_OVERFLOW (0xC00000FD). Defer the call to the next frame's idle " "phase by setting app._pending_focus_response = True and handling it in the " "main render loop, mirroring the _autofocus_response_tab pattern." ) # 2. The render body MUST signal the deferral by setting the flag. assert "_pending_focus_response = True" in body, ( "render_response_panel must set _pending_focus_response = True (e.g. " "app._pending_focus_response = True) when _trigger_blink fires. The deferred " "handler in the main render loop will consume the flag on the next frame's " "idle phase and then clear it." ) # 3. The main render flow (everything outside render_response_panel) MUST # contain a deferred handler that reads the flag and calls # imgui.set_window_focus("Response") when the flag is set. assert "app._pending_focus_response" in rest, ( "No reference to app._pending_focus_response found outside render_response_panel. " "The main render loop must read the flag and call imgui.set_window_focus('Response') " "when it is set, then clear the flag." ) assert 'imgui.set_window_focus("Response")' in rest, ( "No imgui.set_window_focus('Response') call found outside render_response_panel. " "The deferred handler in the main render flow must invoke it when the flag is set." )