diff --git a/src/api_hook_client.py b/src/api_hook_client.py index f70b86b..40d2497 100644 --- a/src/api_hook_client.py +++ b/src/api_hook_client.py @@ -171,3 +171,22 @@ class ApiHookClient: def reset_session(self) -> None: """Resets the current session via button click.""" 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 {} diff --git a/src/api_hooks.py b/src/api_hooks.py index 60212f4..df35c85 100644 --- a/src/api_hooks.py +++ b/src/api_hooks.py @@ -288,6 +288,97 @@ class HookHandler(BaseHTTPRequestHandler): else: self.send_response(504) 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": request_id = str(uuid.uuid4()) event = threading.Event() diff --git a/tests/test_patch_modal_gui.py b/tests/test_patch_modal_gui.py new file mode 100644 index 0000000..4b5ae84 --- /dev/null +++ b/tests/test_patch_modal_gui.py @@ -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" \ No newline at end of file