From dc64493f42a6af7f75d00cd3349bc88b475c2775 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Mon, 23 Feb 2026 12:38:29 -0500 Subject: [PATCH] fix(conductor): Apply review suggestions for track 'Add full api/hooks so that gemini cli can test, interact, and manipulate the state of the gui & program backend for automated testing.' --- api_hooks.py | 21 +++++++++++++++------ gui.py | 12 +++++++++--- session_logger.py | 2 +- tests/test_hooks.py | 23 +++++++++++++++++++---- 4 files changed, 44 insertions(+), 14 deletions(-) diff --git a/api_hooks.py b/api_hooks.py index 27a6160..1f70de2 100644 --- a/api_hooks.py +++ b/api_hooks.py @@ -5,11 +5,13 @@ import logging import session_logger class HookServerInstance(HTTPServer): + """Custom HTTPServer that carries a reference to the main App instance.""" def __init__(self, server_address, RequestHandlerClass, app): super().__init__(server_address, RequestHandlerClass) self.app = app class HookHandler(BaseHTTPRequestHandler): + """Handles incoming HTTP requests for the API hooks.""" def do_GET(self): app = self.server.app session_logger.log_api_hook("GET", self.path, "") @@ -22,12 +24,15 @@ class HookHandler(BaseHTTPRequestHandler): self.send_response(200) self.send_header('Content-Type', 'application/json') self.end_headers() - self.wfile.write(json.dumps({'project': app.project}).encode('utf-8')) + self.wfile.write( + json.dumps({'project': app.project}).encode('utf-8')) elif self.path == '/api/session': self.send_response(200) self.send_header('Content-Type', 'application/json') self.end_headers() - self.wfile.write(json.dumps({'session': {'entries': app.disc_entries}}).encode('utf-8')) + self.wfile.write( + json.dumps({'session': {'entries': app.disc_entries}}). + encode('utf-8')) else: self.send_response(404) self.end_headers() @@ -46,13 +51,16 @@ class HookHandler(BaseHTTPRequestHandler): self.send_response(200) self.send_header('Content-Type', 'application/json') self.end_headers() - self.wfile.write(json.dumps({'status': 'updated'}).encode('utf-8')) + self.wfile.write( + json.dumps({'status': 'updated'}).encode('utf-8')) elif self.path == '/api/session': - app.disc_entries = data.get('session', {}).get('entries', app.disc_entries) + app.disc_entries = data.get('session', {}).get( + 'entries', app.disc_entries) self.send_response(200) self.send_header('Content-Type', 'application/json') self.end_headers() - self.wfile.write(json.dumps({'status': 'updated'}).encode('utf-8')) + self.wfile.write( + json.dumps({'status': 'updated'}).encode('utf-8')) elif self.path == '/api/gui': if not hasattr(app, '_pending_gui_tasks'): app._pending_gui_tasks = [] @@ -65,7 +73,8 @@ class HookHandler(BaseHTTPRequestHandler): self.send_response(200) self.send_header('Content-Type', 'application/json') self.end_headers() - self.wfile.write(json.dumps({'status': 'queued'}).encode('utf-8')) + self.wfile.write( + json.dumps({'status': 'queued'}).encode('utf-8')) else: self.send_response(404) self.end_headers() diff --git a/gui.py b/gui.py index 29f62d8..abf9244 100644 --- a/gui.py +++ b/gui.py @@ -24,8 +24,8 @@ from ai_client import ProviderError import shell_runner import session_logger import project_manager -import theme import api_hooks +import theme CONFIG_PATH = Path("config.toml") PROVIDERS = ["gemini", "anthropic"] @@ -396,8 +396,12 @@ def _parse_history_entries(history: list[str], roles: list[str] | None = None) - 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) + # Controls whether API hooks are enabled, based on CLI arg or env var + self.test_hooks_enabled: bool = ( + '--enable-test-hooks' in sys.argv or + os.environ.get('SLOP_TEST_HOOKS') == '1') + # The API hook server instance + self.hook_server: api_hooks.HookServer = api_hooks.HookServer(self) # ---- global settings from config.toml ---- ai_cfg = self.config.get("ai", {}) @@ -474,7 +478,9 @@ class App: self._pending_history_adds_lock = threading.Lock() # API GUI Hooks Queue + # Tasks (e.g., set_value, click) to be executed on the main DPG thread self._pending_gui_tasks: list[dict] = [] + # Lock for _pending_gui_tasks to ensure thread safety self._pending_gui_tasks_lock = threading.Lock() # Blink state diff --git a/session_logger.py b/session_logger.py index 430acfc..dd2a32e 100644 --- a/session_logger.py +++ b/session_logger.py @@ -40,7 +40,7 @@ _seq_lock = threading.Lock() _comms_fh = None # file handle: logs/comms_.log _tool_fh = None # file handle: logs/toolcalls_.log -_api_fh = None # file handle: logs/apihooks_.log +_api_fh = None # file handle: logs/apihooks_.log - API hook calls def _now_ts() -> str: diff --git a/tests/test_hooks.py b/tests/test_hooks.py index f04027a..8e94a1a 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -58,26 +58,41 @@ def test_ipc_server_starts_and_responds(): assert "session" in data # Test project POST - req = urllib.request.Request("http://127.0.0.1:8999/api/project", method="POST", data=json.dumps({"project": {"foo": "bar"}}).encode("utf-8"), headers={'Content-Type': 'application/json'}) + project_data = {"project": {"foo": "bar"}} + req = urllib.request.Request( + "http://127.0.0.1:8999/api/project", + method="POST", + data=json.dumps(project_data).encode("utf-8"), + headers={'Content-Type': 'application/json'}) with urllib.request.urlopen(req) as response: assert response.status == 200 assert app_mock.project == {"foo": "bar"} # Test session POST - req = urllib.request.Request("http://127.0.0.1:8999/api/session", method="POST", data=json.dumps({"session": {"entries": [{"role": "User", "content": "hi"}]}}).encode("utf-8"), headers={'Content-Type': 'application/json'}) + session_data = {"session": {"entries": [{"role": "User", "content": "hi"}]}} + req = urllib.request.Request( + "http://127.0.0.1:8999/api/session", + method="POST", + data=json.dumps(session_data).encode("utf-8"), + headers={'Content-Type': 'application/json'}) with urllib.request.urlopen(req) as response: assert response.status == 200 assert app_mock.disc_entries == [{"role": "User", "content": "hi"}] # Test GUI queue hook - req = urllib.request.Request("http://127.0.0.1:8999/api/gui", method="POST", data=json.dumps({"action": "set_value", "item": "test_item", "value": "test_value"}).encode("utf-8"), headers={'Content-Type': 'application/json'}) + gui_data = {"action": "set_value", "item": "test_item", "value": "test_value"} + req = urllib.request.Request( + "http://127.0.0.1:8999/api/gui", + method="POST", + data=json.dumps(gui_data).encode("utf-8"), + headers={'Content-Type': 'application/json'}) with urllib.request.urlopen(req) as response: assert response.status == 200 # Instead of checking DPG (since we aren't running the real main loop in tests), # check if it got queued in app_mock assert hasattr(app_mock, '_pending_gui_tasks') assert len(app_mock._pending_gui_tasks) == 1 - assert app_mock._pending_gui_tasks[0] == {"action": "set_value", "item": "test_item", "value": "test_value"} + assert app_mock._pending_gui_tasks[0] == gui_data finally: server.stop()