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.'

This commit is contained in:
2026-02-23 12:38:29 -05:00
parent 0070f61a40
commit dc64493f42
4 changed files with 44 additions and 14 deletions

View File

@@ -5,11 +5,13 @@ import logging
import session_logger import session_logger
class HookServerInstance(HTTPServer): class HookServerInstance(HTTPServer):
"""Custom HTTPServer that carries a reference to the main App instance."""
def __init__(self, server_address, RequestHandlerClass, app): def __init__(self, server_address, RequestHandlerClass, app):
super().__init__(server_address, RequestHandlerClass) super().__init__(server_address, RequestHandlerClass)
self.app = app self.app = app
class HookHandler(BaseHTTPRequestHandler): class HookHandler(BaseHTTPRequestHandler):
"""Handles incoming HTTP requests for the API hooks."""
def do_GET(self): def do_GET(self):
app = self.server.app app = self.server.app
session_logger.log_api_hook("GET", self.path, "") session_logger.log_api_hook("GET", self.path, "")
@@ -22,12 +24,15 @@ class HookHandler(BaseHTTPRequestHandler):
self.send_response(200) self.send_response(200)
self.send_header('Content-Type', 'application/json') self.send_header('Content-Type', 'application/json')
self.end_headers() 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': elif self.path == '/api/session':
self.send_response(200) self.send_response(200)
self.send_header('Content-Type', 'application/json') self.send_header('Content-Type', 'application/json')
self.end_headers() 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: else:
self.send_response(404) self.send_response(404)
self.end_headers() self.end_headers()
@@ -46,13 +51,16 @@ class HookHandler(BaseHTTPRequestHandler):
self.send_response(200) self.send_response(200)
self.send_header('Content-Type', 'application/json') self.send_header('Content-Type', 'application/json')
self.end_headers() 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': 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_response(200)
self.send_header('Content-Type', 'application/json') self.send_header('Content-Type', 'application/json')
self.end_headers() 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': elif self.path == '/api/gui':
if not hasattr(app, '_pending_gui_tasks'): if not hasattr(app, '_pending_gui_tasks'):
app._pending_gui_tasks = [] app._pending_gui_tasks = []
@@ -65,7 +73,8 @@ class HookHandler(BaseHTTPRequestHandler):
self.send_response(200) self.send_response(200)
self.send_header('Content-Type', 'application/json') self.send_header('Content-Type', 'application/json')
self.end_headers() self.end_headers()
self.wfile.write(json.dumps({'status': 'queued'}).encode('utf-8')) self.wfile.write(
json.dumps({'status': 'queued'}).encode('utf-8'))
else: else:
self.send_response(404) self.send_response(404)
self.end_headers() self.end_headers()

12
gui.py
View File

@@ -24,8 +24,8 @@ from ai_client import ProviderError
import shell_runner import shell_runner
import session_logger import session_logger
import project_manager import project_manager
import theme
import api_hooks import api_hooks
import theme
CONFIG_PATH = Path("config.toml") CONFIG_PATH = Path("config.toml")
PROVIDERS = ["gemini", "anthropic"] PROVIDERS = ["gemini", "anthropic"]
@@ -396,8 +396,12 @@ def _parse_history_entries(history: list[str], roles: list[str] | None = None) -
class App: class App:
def __init__(self): def __init__(self):
self.config = load_config() self.config = load_config()
self.test_hooks_enabled = '--enable-test-hooks' in sys.argv or os.environ.get('SLOP_TEST_HOOKS') == '1' # Controls whether API hooks are enabled, based on CLI arg or env var
self.hook_server = api_hooks.HookServer(self) 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 ---- # ---- global settings from config.toml ----
ai_cfg = self.config.get("ai", {}) ai_cfg = self.config.get("ai", {})
@@ -474,7 +478,9 @@ class App:
self._pending_history_adds_lock = threading.Lock() self._pending_history_adds_lock = threading.Lock()
# API GUI Hooks Queue # API GUI Hooks Queue
# Tasks (e.g., set_value, click) to be executed on the main DPG thread
self._pending_gui_tasks: list[dict] = [] self._pending_gui_tasks: list[dict] = []
# Lock for _pending_gui_tasks to ensure thread safety
self._pending_gui_tasks_lock = threading.Lock() self._pending_gui_tasks_lock = threading.Lock()
# Blink state # Blink state

View File

@@ -40,7 +40,7 @@ _seq_lock = threading.Lock()
_comms_fh = None # file handle: logs/comms_<ts>.log _comms_fh = None # file handle: logs/comms_<ts>.log
_tool_fh = None # file handle: logs/toolcalls_<ts>.log _tool_fh = None # file handle: logs/toolcalls_<ts>.log
_api_fh = None # file handle: logs/apihooks_<ts>.log _api_fh = None # file handle: logs/apihooks_<ts>.log - API hook calls
def _now_ts() -> str: def _now_ts() -> str:

View File

@@ -58,26 +58,41 @@ def test_ipc_server_starts_and_responds():
assert "session" in data assert "session" in data
# Test project POST # 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: with urllib.request.urlopen(req) as response:
assert response.status == 200 assert response.status == 200
assert app_mock.project == {"foo": "bar"} assert app_mock.project == {"foo": "bar"}
# Test session POST # 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: with urllib.request.urlopen(req) as response:
assert response.status == 200 assert response.status == 200
assert app_mock.disc_entries == [{"role": "User", "content": "hi"}] assert app_mock.disc_entries == [{"role": "User", "content": "hi"}]
# Test GUI queue hook # 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: with urllib.request.urlopen(req) as response:
assert response.status == 200 assert response.status == 200
# Instead of checking DPG (since we aren't running the real main loop in tests), # Instead of checking DPG (since we aren't running the real main loop in tests),
# check if it got queued in app_mock # check if it got queued in app_mock
assert hasattr(app_mock, '_pending_gui_tasks') assert hasattr(app_mock, '_pending_gui_tasks')
assert len(app_mock._pending_gui_tasks) == 1 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: finally:
server.stop() server.stop()