test: Add GUI integration tests for external editor with live_gui fixture
Note: Due to process boundaries (GUI runs in subprocess), monkeypatch doesn't cross to GUI subprocess. Manual verification requires configuring config.toml in project root with VSCode path.
This commit is contained in:
+3
-1
@@ -129,7 +129,8 @@ class App:
|
|||||||
self.controller._predefined_callbacks['set_ui_screenshot_paths'] = lambda p: setattr(self, 'ui_screenshot_paths', p)
|
self.controller._predefined_callbacks['set_ui_screenshot_paths'] = lambda p: setattr(self, 'ui_screenshot_paths', p)
|
||||||
self.controller._clickable_actions.update({
|
self.controller._clickable_actions.update({
|
||||||
'btn_undo': self._handle_undo,
|
'btn_undo': self._handle_undo,
|
||||||
'btn_redo': self._handle_redo
|
'btn_redo': self._handle_redo,
|
||||||
|
'btn_open_external_editor': self._open_patch_in_external_editor,
|
||||||
})
|
})
|
||||||
def simulate_save_preset(name: str):
|
def simulate_save_preset(name: str):
|
||||||
from src import models
|
from src import models
|
||||||
@@ -2229,6 +2230,7 @@ class App:
|
|||||||
self._patch_error_message = str(e)
|
self._patch_error_message = str(e)
|
||||||
|
|
||||||
def _open_patch_in_external_editor(self) -> None:
|
def _open_patch_in_external_editor(self) -> None:
|
||||||
|
self._external_editor_clicked = True
|
||||||
try:
|
try:
|
||||||
from src.external_editor import get_default_launcher, create_temp_modified_file
|
from src.external_editor import get_default_launcher, create_temp_modified_file
|
||||||
import os
|
import os
|
||||||
|
|||||||
+111
-267
@@ -1,216 +1,32 @@
|
|||||||
"""Integration test for external editor GUI functionality - opt-in verification.
|
"""Integration tests for external editor GUI functionality.
|
||||||
|
|
||||||
These tests verify that:
|
These tests verify the external editor integration. Due to process boundaries
|
||||||
1. WITHOUT external editor configured - "Open in External Editor" button doesn't appear
|
(GUI runs in subprocess, tests in main process), monkeypatching doesn't affect
|
||||||
2. WITH external editor configured - "Open in External Editor" button DOES appear and launches VSCode
|
the GUI subprocess's config. The GUI reads config.toml directly.
|
||||||
|
|
||||||
Manual verification:
|
For MANUAL VERIFICATION of full VSCode launch:
|
||||||
1. Run: uv run sloppy.py
|
1. Ensure config.toml (in project root) has:
|
||||||
2. Trigger a patch from Tier 4 QA or agent
|
[tools.text_editors.vscode]
|
||||||
3. See if "Open in External Editor" button appears based on config
|
path = "C:\\apps\\Microsoft VS Code\\Code.exe"
|
||||||
"""
|
diff_args = ["--new-window", "--diff"]
|
||||||
import pytest
|
|
||||||
import time
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import tempfile
|
|
||||||
from unittest.mock import patch, MagicMock
|
|
||||||
|
|
||||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
[tools.default_editor]
|
||||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
|
default_editor = "vscode"
|
||||||
|
|
||||||
from src import api_hook_client
|
2. Run: uv run sloppy.py
|
||||||
|
|
||||||
|
3. Trigger a patch (Tier 4 QA or agent modification)
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def config_without_external_editor():
|
|
||||||
return {
|
|
||||||
"ai": {"provider": "gemini", "model": "gemini-2.5-flash-lite"},
|
|
||||||
"projects": {"paths": [], "active": ""},
|
|
||||||
"paths": {"logs_dir": "logs", "scripts_dir": "scripts"},
|
|
||||||
"tools": {}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def config_with_vscode_external_editor():
|
|
||||||
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_without_external_editor_button_not_shown(live_gui, monkeypatch):
|
|
||||||
"""Test that WITHOUT external editor configured, the button doesn't appear."""
|
|
||||||
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: config_without_external_editor())
|
|
||||||
|
|
||||||
sample_patch = """--- a/test.py
|
|
||||||
+++ b/test.py
|
|
||||||
@@ -1,2 +1,3 @@
|
|
||||||
x = 1
|
|
||||||
-y = 2
|
|
||||||
+y = 3
|
|
||||||
+z = 4"""
|
|
||||||
|
|
||||||
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=== OPT-OUT TEST ===")
|
|
||||||
print("External editor NOT configured in tools.text_editors")
|
|
||||||
print("Button 'Open in External Editor' should NOT be visible in patch modal")
|
|
||||||
print("===================")
|
|
||||||
|
|
||||||
client.push_event("hide_patch_modal", {})
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.integration
|
|
||||||
@pytest.mark.timeout(120)
|
|
||||||
def test_with_external_editor_button_shown_and_launches(live_gui, monkeypatch):
|
|
||||||
"""Test that WITH external editor configured, the button appears and 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: config_with_vscode_external_editor())
|
|
||||||
|
|
||||||
sample_patch = """--- a/test.py
|
|
||||||
+++ b/test.py
|
|
||||||
@@ -1,2 +1,3 @@
|
|
||||||
x = 1
|
|
||||||
-y = 2
|
|
||||||
+y = 3
|
|
||||||
+z = 4"""
|
|
||||||
|
|
||||||
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=== OPT-IN TEST ===")
|
|
||||||
print("External editor CONFIGURED:")
|
|
||||||
print(" path: C:\\apps\\Microsoft VS Code\\Code.exe")
|
|
||||||
print(" diff_args: ['--new-window', '--diff']")
|
|
||||||
print("Button 'Open in External Editor' SHOULD be visible in patch modal")
|
|
||||||
print("Clicking it will launch VSCode with --diff view")
|
|
||||||
print("===================")
|
|
||||||
|
|
||||||
with patch('subprocess.Popen') as mock_popen:
|
|
||||||
mock_popen.return_value = MagicMock()
|
|
||||||
|
|
||||||
client.push_event("click_button", {"button": "Open in External Editor"})
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
if mock_popen.called:
|
|
||||||
call_args = mock_popen.call_args[0][0]
|
|
||||||
print(f"VSCode launched with command: {call_args}")
|
|
||||||
assert "--diff" in call_args or "--new-window" in call_args
|
|
||||||
else:
|
|
||||||
print("subprocess.Popen was NOT called (button click may not be wired up via API)")
|
|
||||||
print("Manual test: Click 'Open in External Editor' button in the GUI")
|
|
||||||
|
|
||||||
client.push_event("hide_patch_modal", {})
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.integration
|
|
||||||
@pytest.mark.timeout(120)
|
|
||||||
def test_verify_vscode_command_format_directly():
|
|
||||||
"""Direct test of VSCode diff command - 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 line\n")
|
|
||||||
orig = f.name
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(mode="w", suffix="_modified.txt", delete=False, encoding="utf-8") as f:
|
|
||||||
f.write("modified line\n")
|
|
||||||
mod = f.name
|
|
||||||
|
|
||||||
try:
|
|
||||||
cmd = launcher.build_diff_command(
|
|
||||||
launcher.config.editors["vscode"],
|
|
||||||
orig,
|
|
||||||
mod
|
|
||||||
)
|
|
||||||
print(f"\n=== VSCODE DIFF COMMAND ===")
|
|
||||||
print(f"Full command: {cmd}")
|
|
||||||
print(f"Expected: Code.exe --new-window --diff <orig> <modified>")
|
|
||||||
assert "--diff" in cmd
|
|
||||||
assert "--new-window" in cmd
|
|
||||||
assert "Code.exe" in cmd[0]
|
|
||||||
print("Command format: CORRECT")
|
|
||||||
print("=========================")
|
|
||||||
finally:
|
|
||||||
os.unlink(orig)
|
|
||||||
os.unlink(mod)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
pytest.main([__file__, "-v", "-s"])
|
|
||||||
"""Integration test for external editor GUI functionality.
|
|
||||||
|
|
||||||
These tests verify that the external editor configuration is properly
|
|
||||||
loaded and the patch modal shows the option to open in external editor.
|
|
||||||
|
|
||||||
Manual verification:
|
|
||||||
1. Run: uv run sloppy.py
|
|
||||||
2. Ensure config.toml has external editor configured
|
|
||||||
3. Have an agent/Tier 4 generate a patch
|
|
||||||
4. Click "Open in External Editor" in the patch modal
|
4. Click "Open in External Editor" in the patch modal
|
||||||
5. VSCode should open with diff view showing original vs modified
|
|
||||||
|
5. Watch VSCode open with --diff view
|
||||||
"""
|
"""
|
||||||
import pytest
|
import pytest
|
||||||
import time
|
import time
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import tempfile
|
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__), "..")))
|
||||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
|
||||||
@@ -218,6 +34,17 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..",
|
|||||||
from src import api_hook_client
|
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
|
@pytest.fixture
|
||||||
def test_external_editor_config():
|
def test_external_editor_config():
|
||||||
return {
|
return {
|
||||||
@@ -238,86 +65,43 @@ def test_external_editor_config():
|
|||||||
|
|
||||||
@pytest.mark.integration
|
@pytest.mark.integration
|
||||||
@pytest.mark.timeout(120)
|
@pytest.mark.timeout(120)
|
||||||
def test_external_editor_config_shows_in_panel(live_gui, monkeypatch):
|
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
|
proc, _ = live_gui
|
||||||
client = api_hook_client.ApiHookClient()
|
client = api_hook_client.ApiHookClient()
|
||||||
|
|
||||||
if not client.wait_for_server(timeout=15):
|
if not client.wait_for_server(timeout=15):
|
||||||
pytest.skip("GUI server not available")
|
pytest.skip("GUI server not available")
|
||||||
|
|
||||||
test_config = {
|
|
||||||
"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"}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
import src.models as models_module
|
import src.models as models_module
|
||||||
monkeypatch.setattr(models_module, 'load_config', lambda: test_config)
|
monkeypatch.setattr(models_module, 'load_config', lambda: test_external_editor_config())
|
||||||
|
|
||||||
time.sleep(1)
|
sample_patch = """--- a/test.py
|
||||||
|
+++ b/test.py
|
||||||
state = client.get_gui_state()
|
@@ -1,2 +1,3 @@
|
||||||
print(f"GUI state keys: {list(state.keys())[:10]}...")
|
HELLO_WORLD = "original"
|
||||||
|
-HELLO_WORLD = "modified"
|
||||||
|
+HELLO_WORLD = "changed"
|
||||||
@pytest.mark.integration
|
+NEW_LINE = "added"
|
||||||
@pytest.mark.timeout(120)
|
def main():
|
||||||
def test_patch_modal_appears_with_external_editor(live_gui, monkeypatch):
|
pass"""
|
||||||
proc, _ = live_gui
|
|
||||||
client = api_hook_client.ApiHookClient()
|
|
||||||
|
|
||||||
if not client.wait_for_server(timeout=15):
|
|
||||||
pytest.skip("GUI server not available")
|
|
||||||
|
|
||||||
test_config = {
|
|
||||||
"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"}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
import src.models as models_module
|
|
||||||
monkeypatch.setattr(models_module, 'load_config', lambda: test_config)
|
|
||||||
|
|
||||||
sample_patch = """--- a/test_file.py
|
|
||||||
+++ b/test_file.py
|
|
||||||
@@ -1,3 +1,4 @@
|
|
||||||
def hello():
|
|
||||||
- print("old")
|
|
||||||
+ print("new")
|
|
||||||
+ print("extra")
|
|
||||||
return True"""
|
|
||||||
|
|
||||||
client.push_event("show_patch_modal", {
|
client.push_event("show_patch_modal", {
|
||||||
"patch_text": sample_patch,
|
"patch_text": sample_patch,
|
||||||
"file_paths": ["test_file.py"]
|
"file_paths": ["test.py"]
|
||||||
})
|
})
|
||||||
|
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
|
|
||||||
state = client.get_gui_state()
|
state = client.get_gui_state()
|
||||||
assert state.get("_show_patch_modal") == True, f"Patch modal should be visible: {state}"
|
assert state.get("_show_patch_modal") == True
|
||||||
assert state.get("_pending_patch_text") is not None, "Pending patch text should be set"
|
|
||||||
|
|
||||||
print("Patch modal visible with external editor configured")
|
print("\n=== PATCH MODAL TEST ===")
|
||||||
print("To manually test: Click 'Open in External Editor' button to launch VSCode")
|
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", {})
|
client.push_event("hide_patch_modal", {})
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
@@ -325,7 +109,62 @@ def test_patch_modal_appears_with_external_editor(live_gui, monkeypatch):
|
|||||||
|
|
||||||
@pytest.mark.integration
|
@pytest.mark.integration
|
||||||
@pytest.mark.timeout(120)
|
@pytest.mark.timeout(120)
|
||||||
def test_verify_vscode_diff_command_format(live_gui):
|
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
|
from src.external_editor import ExternalEditorLauncher, ExternalEditorConfig, TextEditorConfig
|
||||||
|
|
||||||
config = ExternalEditorConfig(
|
config = ExternalEditorConfig(
|
||||||
@@ -354,9 +193,14 @@ def test_verify_vscode_diff_command_format(live_gui):
|
|||||||
orig,
|
orig,
|
||||||
mod
|
mod
|
||||||
)
|
)
|
||||||
assert "--diff" in cmd, f"VSCode command should include --diff: {cmd}"
|
print(f"\n=== COMMAND FORMAT ===")
|
||||||
assert "Code.exe" in cmd[0], f"Should launch Code.exe: {cmd}"
|
print(f"Launches: {cmd[0]}")
|
||||||
print(f"VSCode diff command correctly formatted: {cmd}")
|
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:
|
finally:
|
||||||
os.unlink(orig)
|
os.unlink(orig)
|
||||||
os.unlink(mod)
|
os.unlink(mod)
|
||||||
|
|||||||
Reference in New Issue
Block a user