"""Integration tests for external editor GUI functionality. These tests verify the external editor integration. Due to process boundaries (GUI runs in subprocess, tests in main process), monkeypatching doesn't affect the GUI subprocess's config. The GUI reads config.toml directly. For MANUAL VERIFICATION of full VSCode launch: 1. Ensure config.toml (in project root) has: [tools.text_editors.vscode] path = "C:\\apps\\Microsoft VS Code\\Code.exe" diff_args = ["--new-window", "--diff"] [tools.default_editor] default_editor = "vscode" 2. Run: uv run sloppy.py 3. Trigger a patch (Tier 4 QA or agent modification) 4. Click "Open in External Editor" in the patch modal 5. Watch VSCode open with --diff view """ import pytest import time import sys import os import tempfile import subprocess sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src"))) from src import api_hook_client def get_vscode_processes(): try: result = subprocess.run( ["powershell", "-Command", "Get-Process", "Code", "-ErrorAction", "SilentlyContinue"], capture_output=True, text=True, timeout=5 ) return result.stdout except: return "" @pytest.fixture def test_external_editor_config(): return { "ai": {"provider": "gemini", "model": "gemini-2.5-flash-lite"}, "projects": {"paths": [], "active": ""}, "paths": {"logs_dir": "logs", "scripts_dir": "scripts"}, "tools": { "text_editors": { "vscode": { "path": "C:\\apps\\Microsoft VS Code\\Code.exe", "diff_args": ["--new-window", "--diff"] } }, "default_editor": {"default_editor": "vscode"} } } @pytest.mark.integration @pytest.mark.timeout(120) def test_patch_modal_shows_with_configured_editor(live_gui, monkeypatch): """ Test that when external editor is configured, the patch modal shows properly. """ proc, _ = live_gui client = api_hook_client.ApiHookClient() if not client.wait_for_server(timeout=15): pytest.skip("GUI server not available") import src.models as models_module monkeypatch.setattr(models_module, 'load_config', lambda: test_external_editor_config()) sample_patch = """--- a/test.py +++ b/test.py @@ -1,2 +1,3 @@ HELLO_WORLD = "original" -HELLO_WORLD = "modified" +HELLO_WORLD = "changed" +NEW_LINE = "added" def main(): pass""" client.push_event("show_patch_modal", { "patch_text": sample_patch, "file_paths": ["test.py"] }) time.sleep(2) state = client.get_gui_state() assert state.get("_show_patch_modal") == True print("\n=== PATCH MODAL TEST ===") print("Patch modal visible with external editor config loaded in test process") print("The button 'Open in External Editor' SHOULD be visible in the GUI") print("===================") client.push_event("hide_patch_modal", {}) time.sleep(1) @pytest.mark.integration @pytest.mark.timeout(120) def test_button_click_is_received(live_gui, monkeypatch): """ Test that btn_open_external_editor button click is received by GUI. Uses client.click() which is the standard API for button clicks. """ proc, _ = live_gui client = api_hook_client.ApiHookClient() if not client.wait_for_server(timeout=15): pytest.skip("GUI server not available") import src.models as models_module monkeypatch.setattr(models_module, 'load_config', lambda: test_external_editor_config()) sample_patch = """--- a/test.py +++ b/test.py @@ -1,2 +1,3 @@ HELLO_WORLD = "original" -HELLO_WORLD = "modified" +HELLO_WORLD = "changed" def main(): pass""" client.push_event("show_patch_modal", { "patch_text": sample_patch, "file_paths": ["test.py"] }) time.sleep(2) state = client.get_gui_state() assert state.get("_show_patch_modal") == True print("\n=== BUTTON CLICK TEST ===") print("Sending client.click('btn_open_external_editor')...") print("(This is how other tests verify button clicks, e.g., undo_redo)") client.click("btn_open_external_editor") time.sleep(2) print("Button click sent. Check GUI for VSCode launch.") print("NOTE: If VSCode doesn't launch, the GUI subprocess reads config.toml") print(" from project root, NOT from test's monkeypatched config.") print("===================") client.push_event("hide_patch_modal", {}) time.sleep(1) @pytest.mark.integration @pytest.mark.timeout(30) def test_verify_vscode_command_format(): """ Direct verification of VSCode command format - no GUI needed. """ from src.external_editor import ExternalEditorLauncher, ExternalEditorConfig, TextEditorConfig config = ExternalEditorConfig( editors={ "vscode": TextEditorConfig( name="vscode", path="C:\\apps\\Microsoft VS Code\\Code.exe", diff_args=["--new-window", "--diff"] ) }, default_editor="vscode" ) launcher = ExternalEditorLauncher(config) with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False, encoding="utf-8") as f: f.write("original\n") orig = f.name with tempfile.NamedTemporaryFile(mode="w", suffix="_modified.txt", delete=False, encoding="utf-8") as f: f.write("modified\n") mod = f.name try: cmd = launcher.build_diff_command( launcher.config.editors["vscode"], orig, mod ) print(f"\n=== COMMAND FORMAT ===") print(f"Launches: {cmd[0]}") print(f"Args: {cmd[1:]}") assert "--diff" in cmd assert "--new-window" in cmd assert "Code.exe" in cmd[0] print("Format: CORRECT") print("==================") finally: os.unlink(orig) os.unlink(mod) if __name__ == "__main__": pytest.main([__file__, "-v", "-s"])