feat(tier4): Add patch modal GUI integration and API hooks
This commit is contained in:
@@ -171,3 +171,22 @@ class ApiHookClient:
|
|||||||
def reset_session(self) -> None:
|
def reset_session(self) -> None:
|
||||||
"""Resets the current session via button click."""
|
"""Resets the current session via button click."""
|
||||||
self.click("btn_reset")
|
self.click("btn_reset")
|
||||||
|
|
||||||
|
def trigger_patch(self, patch_text: str, file_paths: list[str]) -> dict[str, Any]:
|
||||||
|
"""Triggers the patch modal to show in the GUI."""
|
||||||
|
return self._make_request('POST', '/api/patch/trigger', data={
|
||||||
|
"patch_text": patch_text,
|
||||||
|
"file_paths": file_paths
|
||||||
|
}) or {}
|
||||||
|
|
||||||
|
def apply_patch(self) -> dict[str, Any]:
|
||||||
|
"""Applies the pending patch."""
|
||||||
|
return self._make_request('POST', '/api/patch/apply') or {}
|
||||||
|
|
||||||
|
def reject_patch(self) -> dict[str, Any]:
|
||||||
|
"""Rejects the pending patch."""
|
||||||
|
return self._make_request('POST', '/api/patch/reject') or {}
|
||||||
|
|
||||||
|
def get_patch_status(self) -> dict[str, Any]:
|
||||||
|
"""Gets the current patch modal status."""
|
||||||
|
return self._make_request('GET', '/api/patch/status') or {}
|
||||||
|
|||||||
@@ -288,6 +288,97 @@ class HookHandler(BaseHTTPRequestHandler):
|
|||||||
else:
|
else:
|
||||||
self.send_response(504)
|
self.send_response(504)
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
|
elif self.path == "/api/patch/trigger":
|
||||||
|
patch_text = data.get("patch_text", "")
|
||||||
|
file_paths = data.get("file_paths", [])
|
||||||
|
event = threading.Event()
|
||||||
|
result = {"status": "queued"}
|
||||||
|
def trigger_patch():
|
||||||
|
try:
|
||||||
|
app._pending_patch_text = patch_text
|
||||||
|
app._pending_patch_files = file_paths
|
||||||
|
app._show_patch_modal = True
|
||||||
|
result["status"] = "ok"
|
||||||
|
except Exception as e:
|
||||||
|
result["status"] = "error"
|
||||||
|
result["error"] = str(e)
|
||||||
|
finally:
|
||||||
|
event.set()
|
||||||
|
lock = _get_app_attr(app, "_pending_gui_tasks_lock")
|
||||||
|
tasks = _get_app_attr(app, "_pending_gui_tasks")
|
||||||
|
if lock and tasks is not None:
|
||||||
|
with lock: tasks.append({"action": "custom_callback", "callback": trigger_patch})
|
||||||
|
if event.wait(timeout=10):
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-Type", "application/json")
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(json.dumps(result).encode("utf-8"))
|
||||||
|
else:
|
||||||
|
self.send_response(504)
|
||||||
|
self.end_headers()
|
||||||
|
elif self.path == "/api/patch/apply":
|
||||||
|
event = threading.Event()
|
||||||
|
result = {"status": "done"}
|
||||||
|
def apply_patch():
|
||||||
|
try:
|
||||||
|
if hasattr(app, "_apply_pending_patch"):
|
||||||
|
app._apply_pending_patch()
|
||||||
|
else:
|
||||||
|
result["status"] = "no_method"
|
||||||
|
except Exception as e:
|
||||||
|
result["status"] = "error"
|
||||||
|
result["error"] = str(e)
|
||||||
|
finally:
|
||||||
|
event.set()
|
||||||
|
lock = _get_app_attr(app, "_pending_gui_tasks_lock")
|
||||||
|
tasks = _get_app_attr(app, "_pending_gui_tasks")
|
||||||
|
if lock and tasks is not None:
|
||||||
|
with lock: tasks.append({"action": "custom_callback", "callback": apply_patch})
|
||||||
|
if event.wait(timeout=10):
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-Type", "application/json")
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(json.dumps(result).encode("utf-8"))
|
||||||
|
else:
|
||||||
|
self.send_response(504)
|
||||||
|
self.end_headers()
|
||||||
|
elif self.path == "/api/patch/reject":
|
||||||
|
event = threading.Event()
|
||||||
|
result = {"status": "done"}
|
||||||
|
def reject_patch():
|
||||||
|
try:
|
||||||
|
app._show_patch_modal = False
|
||||||
|
app._pending_patch_text = None
|
||||||
|
app._pending_patch_files = []
|
||||||
|
except Exception as e:
|
||||||
|
result["status"] = "error"
|
||||||
|
result["error"] = str(e)
|
||||||
|
finally:
|
||||||
|
event.set()
|
||||||
|
lock = _get_app_attr(app, "_pending_gui_tasks_lock")
|
||||||
|
tasks = _get_app_attr(app, "_pending_gui_tasks")
|
||||||
|
if lock and tasks is not None:
|
||||||
|
with lock: tasks.append({"action": "custom_callback", "callback": reject_patch})
|
||||||
|
if event.wait(timeout=10):
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-Type", "application/json")
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(json.dumps(result).encode("utf-8"))
|
||||||
|
else:
|
||||||
|
self.send_response(504)
|
||||||
|
self.end_headers()
|
||||||
|
elif self.path == "/api/patch/status":
|
||||||
|
show_modal = _get_app_attr(app, "_show_patch_modal", False)
|
||||||
|
patch_text = _get_app_attr(app, "_pending_patch_text", None)
|
||||||
|
patch_files = _get_app_attr(app, "_pending_patch_files", [])
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-Type", "application/json")
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(json.dumps({
|
||||||
|
"show_modal": show_modal,
|
||||||
|
"patch_text": patch_text,
|
||||||
|
"file_paths": patch_files
|
||||||
|
}).encode("utf-8"))
|
||||||
elif self.path == "/api/ask":
|
elif self.path == "/api/ask":
|
||||||
request_id = str(uuid.uuid4())
|
request_id = str(uuid.uuid4())
|
||||||
event = threading.Event()
|
event = threading.Event()
|
||||||
|
|||||||
84
tests/test_patch_modal_gui.py
Normal file
84
tests/test_patch_modal_gui.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import pytest
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
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 src import api_hook_client
|
||||||
|
|
||||||
|
@pytest.mark.integration
|
||||||
|
@pytest.mark.timeout(120)
|
||||||
|
def test_patch_modal_appears_on_trigger(live_gui) -> None:
|
||||||
|
"""
|
||||||
|
Test that triggering a patch shows the modal in the GUI.
|
||||||
|
Uses live_gui fixture to start the GUI with test hooks enabled.
|
||||||
|
"""
|
||||||
|
proc, _ = live_gui
|
||||||
|
client = api_hook_client.ApiHookClient()
|
||||||
|
|
||||||
|
if not client.wait_for_server(timeout=15):
|
||||||
|
pytest.skip("GUI server not available")
|
||||||
|
|
||||||
|
sample_patch = """--- a/test_file.py
|
||||||
|
+++ b/test_file.py
|
||||||
|
@@ -1,3 +1,4 @@
|
||||||
|
def hello():
|
||||||
|
- print("old")
|
||||||
|
+ print("new")
|
||||||
|
+ print("extra")
|
||||||
|
return True"""
|
||||||
|
|
||||||
|
result = client.trigger_patch(sample_patch, ["test_file.py"])
|
||||||
|
assert result.get("status") == "ok", f"Failed to trigger patch: {result}"
|
||||||
|
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
status = client.get_patch_status()
|
||||||
|
assert status.get("show_modal") == True, "Patch modal should be visible"
|
||||||
|
assert status.get("patch_text") == sample_patch
|
||||||
|
assert "test_file.py" in status.get("file_paths", [])
|
||||||
|
|
||||||
|
result = client.reject_patch()
|
||||||
|
assert result.get("status") == "done"
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
status = client.get_patch_status()
|
||||||
|
assert status.get("show_modal") == False, "Patch modal should be closed after reject"
|
||||||
|
|
||||||
|
@pytest.mark.integration
|
||||||
|
@pytest.mark.timeout(120)
|
||||||
|
def test_patch_apply_modal_workflow(live_gui) -> None:
|
||||||
|
"""
|
||||||
|
Test the full patch apply workflow: trigger -> apply -> verify modal closes.
|
||||||
|
"""
|
||||||
|
proc, _ = live_gui
|
||||||
|
client = api_hook_client.ApiHookClient()
|
||||||
|
|
||||||
|
if not client.wait_for_server(timeout=15):
|
||||||
|
pytest.skip("GUI server not available")
|
||||||
|
|
||||||
|
sample_patch = """--- a/example.py
|
||||||
|
+++ b/example.py
|
||||||
|
@@ -5,3 +5,4 @@
|
||||||
|
def calculate():
|
||||||
|
x = 1
|
||||||
|
- return x
|
||||||
|
+ return x + 1
|
||||||
|
+ # added line"""
|
||||||
|
|
||||||
|
result = client.trigger_patch(sample_patch, ["example.py"])
|
||||||
|
assert result.get("status") == "ok"
|
||||||
|
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
status = client.get_patch_status()
|
||||||
|
assert status.get("show_modal") == True
|
||||||
|
|
||||||
|
result = client.apply_patch()
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
status = client.get_patch_status()
|
||||||
|
assert status.get("show_modal") == False, "Patch modal should close after apply"
|
||||||
Reference in New Issue
Block a user