test: simplify external editor GUI tests, fix process detection
This commit is contained in:
+172
-309
@@ -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"])
|
||||
Reference in New Issue
Block a user