test: simplify external editor GUI tests, fix process detection

This commit is contained in:
2026-05-07 21:15:45 -04:00
parent f137295e92
commit 6a615a2d20
+172 -309
View File
@@ -1,12 +1,4 @@
"""Integration tests for external editor GUI functionality.
These tests verify the external editor integration. The GUI subprocess reads
config.toml from the live_gui workspace - tests/artifacts/live_gui_workspace/config.toml
To verify VSCode launch:
1. Run these tests - they set up the workspace config properly
2. Watch for VSCode to open with --diff view
"""
"""Integration tests for external editor GUI functionality."""
import pytest
import time
import sys
@@ -16,7 +8,6 @@ import subprocess
from pathlib import Path
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
@@ -24,228 +15,7 @@ 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 vscode_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"}
}
}
def setup_external_editor_in_workspace():
"""Set up external editor config in the live_gui workspace."""
workspace_config = Path("tests/artifacts/live_gui_workspace/config.toml")
if workspace_config.exists():
import tomllib
with open(workspace_config, "rb") as f:
config = tomllib.load(f)
else:
config = {}
config.setdefault("tools", {})
config["tools"]["text_editors"] = {
"vscode": {
"path": "C:\\apps\\Microsoft VS Code\\Code.exe",
"diff_args": ["--new-window", "--diff"]
}
}
config["tools"]["default_editor"] = {"default_editor": "vscode"}
import tomli_w
with open(workspace_config, "wb") as f:
tomli_w.dump(config, f)
@pytest.mark.integration
@pytest.mark.timeout(120)
def test_vscode_launches_with_diff_view(live_gui):
"""
Test that VSCode launches with --diff view when external editor is configured.
This test:
1. Sets up external editor config in the live_gui workspace config.toml
2. Triggers a patch modal
3. Clicks the external editor button
4. Watches for VSCode process to appear
"""
proc, _ = live_gui
client = api_hook_client.ApiHookClient()
if not client.wait_for_server(timeout=15):
pytest.skip("GUI server not available")
# Setup external editor config in the workspace that GUI reads
setup_external_editor_in_workspace()
# Kill any existing VSCode
subprocess.run(
["powershell", "-Command", "Stop-Process", "-Name", "Code", "-Force", "-ErrorAction", "SilentlyContinue"],
timeout=10
)
time.sleep(1)
before = get_vscode_processes()
print(f"\n=== VSCODE LAUNCH TEST ===")
print(f"VSCode before: {'running' if before else 'not running'}")
print(f"Config written to: tests/artifacts/live_gui_workspace/config.toml")
# Trigger patch modal
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()
print(f"Patch modal visible: {state.get('_show_patch_modal')}")
print("Clicking 'Open in External Editor' button...")
client.click("btn_open_external_editor")
# Check error message which would indicate what went wrong
time.sleep(1)
state_after_click = client.get_gui_state()
error_msg = state_after_click.get("_patch_error_message", "")
external_clicked = state_after_click.get("_external_editor_clicked", "NOT_FOUND")
print(f"Error message: '{error_msg}'")
print(f"Handler called: _external_editor_clicked = {external_clicked}")
# Wait for VSCode to launch
print("Waiting 5 seconds for VSCode to launch...")
time.sleep(5)
after = get_vscode_processes()
print(f"VSCode after: {'running' if after else 'not running'}")
if "Code" in after:
print("\nSUCCESS: VSCode launched with --diff view!")
else:
print("\nNOTE: VSCode may not have launched within timeout")
print("Try increasing wait time or check config")
client.push_event("hide_patch_modal", {})
time.sleep(1)
# Cleanup
subprocess.run(
["powershell", "-Command", "Stop-Process", "-Name", "Code", "-Force", "-ErrorAction", "SilentlyContinue"],
timeout=10
)
@pytest.mark.integration
@pytest.mark.timeout(30)
def test_verify_command_format():
"""Verify command format without GUI."""
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"\nCommand: {cmd}")
assert "--diff" in cmd
assert "--new-window" in cmd
assert "Code.exe" in cmd[0]
print("Format: CORRECT")
finally:
os.unlink(orig)
os.unlink(mod)
if __name__ == "__main__":
pytest.main([__file__, "-v", "-s"])
"""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"],
["powershell", "-Command", "Get-Process Code* -ErrorAction SilentlyContinue | Format-Table -AutoSize"],
capture_output=True, text=True, timeout=5
)
return result.stdout
@@ -273,19 +43,24 @@ def test_external_editor_config():
@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.
"""
def test_vscode_launches_with_diff_view(live_gui):
"""Test that clicking external editor button launches VSCode."""
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())
subprocess.run(
["powershell", "-Command", "Stop-Process -Name 'Code*' -Force -ErrorAction SilentlyContinue"],
timeout=10
)
time.sleep(1)
before = get_vscode_processes()
print(f"\n=== VSCODE LAUNCH TEST ===")
print(f"VSCode before:\n{before if before.strip() else 'not running'}")
sample_patch = """--- a/test.py
+++ b/test.py
@@ -1,2 +1,3 @@
@@ -295,86 +70,42 @@ def test_patch_modal_shows_with_configured_editor(live_gui, monkeypatch):
+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", {})
print(f"Patch modal visible: {state.get('_show_patch_modal')}")
print("Clicking 'Open in External Editor' button...")
result = client.click("btn_open_external_editor")
print(f"Click API result: {result}")
time.sleep(1)
state_after = client.get_gui_state()
error_msg = state_after.get("_patch_error_message", "")
print(f"Error message: '{error_msg}'")
print("Waiting 5 seconds for VSCode to launch...")
time.sleep(5)
after = get_vscode_processes()
print(f"VSCode after:\n{after if after.strip() else 'not running'}")
@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.
"""
def test_verify_command_format():
"""Verify command format without GUI."""
from src.external_editor import ExternalEditorLauncher, ExternalEditorConfig, TextEditorConfig
config = ExternalEditorConfig(
editors={
"vscode": TextEditorConfig(
@@ -386,15 +117,147 @@ def test_verify_vscode_command_format():
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"\nCommand: {cmd}")
assert "--diff" in cmd
assert "--new-window" in cmd
assert "Code.exe" in cmd[0]
print("Format: CORRECT")
finally:
os.unlink(orig)
os.unlink(mod)
@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")
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."""
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')...")
client.click("btn_open_external_editor")
time.sleep(2)
state = client.get_gui_state()
error = state.get("_patch_error_message", "")
print(f"Error after click: '{error}'")
print("Button click sent.")
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."""
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"],
@@ -415,4 +278,4 @@ def test_verify_vscode_command_format():
if __name__ == "__main__":
pytest.main([__file__, "-v", "-s"])
pytest.main([__file__, "-v", "-s"])