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:
21
api_hooks.py
21
api_hooks.py
@@ -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
12
gui.py
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user