From 44c2585f952638fdad91e264844ab819837eef71 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Mon, 23 Feb 2026 12:11:01 -0500 Subject: [PATCH] feat(api): Add lightweight HTTP server for API hooks --- api_hooks.py | 41 +++++++++++++++++++++++++++++++++++++++++ gui.py | 5 +++++ tests/test_hooks.py | 22 ++++++++++++++++++++++ 3 files changed, 68 insertions(+) create mode 100644 api_hooks.py diff --git a/api_hooks.py b/api_hooks.py new file mode 100644 index 0000000..1bbfd93 --- /dev/null +++ b/api_hooks.py @@ -0,0 +1,41 @@ +import json +import threading +from http.server import HTTPServer, BaseHTTPRequestHandler +import logging + +class HookHandler(BaseHTTPRequestHandler): + def do_GET(self): + if self.path == '/status': + self.send_response(200) + self.send_header('Content-Type', 'application/json') + self.end_headers() + self.wfile.write(json.dumps({'status': 'ok'}).encode('utf-8')) + else: + self.send_response(404) + self.end_headers() + + def log_message(self, format, *args): + logging.info("Hook API: " + format % args) + +class HookServer: + def __init__(self, app, port=8999): + self.app = app + self.port = port + self.server = None + self.thread = None + + def start(self): + if not getattr(self.app, 'test_hooks_enabled', False): + return + self.server = HTTPServer(('127.0.0.1', self.port), HookHandler) + self.thread = threading.Thread(target=self.server.serve_forever, daemon=True) + self.thread.start() + logging.info(f"Hook server started on port {self.port}") + + def stop(self): + if self.server: + self.server.shutdown() + self.server.server_close() + if self.thread: + self.thread.join() + logging.info("Hook server stopped") diff --git a/gui.py b/gui.py index 9176163..a132423 100644 --- a/gui.py +++ b/gui.py @@ -25,6 +25,7 @@ import shell_runner import session_logger import project_manager import theme +import api_hooks CONFIG_PATH = Path("config.toml") PROVIDERS = ["gemini", "anthropic"] @@ -396,6 +397,7 @@ class App: def __init__(self): self.config = load_config() self.test_hooks_enabled = '--enable-test-hooks' in sys.argv or os.environ.get('SLOP_TEST_HOOKS') == '1' + self.hook_server = api_hooks.HookServer(self) # ---- global settings from config.toml ---- ai_cfg = self.config.get("ai", {}) @@ -2057,6 +2059,8 @@ class App: self._rebuild_projects_list() self._rebuild_discussion_selector() self._fetch_models(self.current_provider) + + self.hook_server.start() while dpg.is_dearpygui_running(): # Show any pending confirmation dialog on the main thread safely @@ -2184,6 +2188,7 @@ class App: dpg.save_init_file("dpg_layout.ini") session_logger.close_session() ai_client.cleanup() # Destroy active API caches to stop billing + self.hook_server.stop() dpg.destroy_context() diff --git a/tests/test_hooks.py b/tests/test_hooks.py index db67ab9..731a1ea 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -4,6 +4,11 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) import pytest from unittest.mock import patch import gui +import api_hooks +import urllib.request +import json +import threading +import time def test_hooks_enabled_via_cli(): with patch.object(sys, 'argv', ['gui.py', '--enable-test-hooks']): @@ -22,3 +27,20 @@ def test_hooks_enabled_via_env(): with patch.dict(os.environ, {'SLOP_TEST_HOOKS': '1'}): app = gui.App() assert app.test_hooks_enabled is True + +def test_ipc_server_starts_and_responds(): + app_mock = gui.App() + app_mock.test_hooks_enabled = True + server = api_hooks.HookServer(app_mock, port=8999) + server.start() + + # Wait for server to start + time.sleep(0.5) + try: + req = urllib.request.Request("http://127.0.0.1:8999/status") + with urllib.request.urlopen(req) as response: + assert response.status == 200 + data = json.loads(response.read().decode()) + assert data.get("status") == "ok" + finally: + server.stop()