chore(conductor): Archive gemini_cli_headless_20260224 track and update tests

This commit is contained in:
2026-02-25 18:39:36 -05:00
parent 1c78febd16
commit 94e41d20ff
15 changed files with 173 additions and 71 deletions

View File

@@ -1,10 +1,19 @@
{ {
"hooks": [ "hooks": {
{ "BeforeTool": [
"name": "manual-slop-bridge", {
"type": "command", "matcher": "*",
"event": "BeforeTool", "hooks": [
"command": "python C:/projects/manual_slop/scripts/cli_tool_bridge.py" {
} "name": "manual-slop-bridge",
] "type": "command",
"command": "python C:/projects/manual_slop/scripts/cli_tool_bridge.py"
}
]
}
]
},
"hooksConfig": {
"enabled": true
}
} }

View File

@@ -20,4 +20,5 @@ To serve as an expert-level utility for personal developer use on small projects
- **Performance Diagnostics:** Built-in telemetry for FPS, Frame Time, and CPU usage, with a dedicated Diagnostics Panel and AI API hooks for performance analysis. - **Performance Diagnostics:** Built-in telemetry for FPS, Frame Time, and CPU usage, with a dedicated Diagnostics Panel and AI API hooks for performance analysis.
- **Automated UX Verification:** A robust IPC mechanism via API hooks and a modular simulation suite allows for human-like simulation walkthroughs and automated regression testing of the full GUI lifecycle across multiple specialized scenarios. - **Automated UX Verification:** A robust IPC mechanism via API hooks and a modular simulation suite allows for human-like simulation walkthroughs and automated regression testing of the full GUI lifecycle across multiple specialized scenarios.
- **Headless Backend Service:** Optional headless mode allowing the core AI and tool execution logic to run as a decoupled REST API service (FastAPI), optimized for Docker and server-side environments (e.g., Unraid). - **Headless Backend Service:** Optional headless mode allowing the core AI and tool execution logic to run as a decoupled REST API service (FastAPI), optimized for Docker and server-side environments (e.g., Unraid).
- **Remote Confirmation Protocol:** A non-blocking, ID-based challenge/response mechanism for approving AI actions via the REST API, enabling remote "Human-in-the-Loop" safety. - **Remote Confirmation Protocol:** A non-blocking, ID-based challenge/response mechanism for approving AI actions via the REST API, enabling remote "Human-in-the-Loop" safety.
- **Gemini CLI Integration:** Allows using the `gemini` CLI as a headless backend provider. This enables leveraging Gemini subscriptions with advanced features like persistent sessions, while maintaining full "Human-in-the-Loop" safety through a dedicated bridge for synchronous tool call approvals within the Manual Slop GUI.

View File

@@ -18,6 +18,7 @@
- **google-genai:** For Google Gemini API interaction and explicit context caching. - **google-genai:** For Google Gemini API interaction and explicit context caching.
- **anthropic:** For Anthropic Claude API interaction, supporting ephemeral prompt caching. - **anthropic:** For Anthropic Claude API interaction, supporting ephemeral prompt caching.
- **Gemini CLI:** Integrated as a headless backend provider, utilizing a custom subprocess adapter and bridge script for tool execution control.
## Configuration & Tooling ## Configuration & Tooling
@@ -32,3 +33,4 @@
## Architectural Patterns ## Architectural Patterns
- **Event-Driven Metrics:** Uses a custom `EventEmitter` to decouple API lifecycle events from UI rendering, improving performance and responsiveness. - **Event-Driven Metrics:** Uses a custom `EventEmitter` to decouple API lifecycle events from UI rendering, improving performance and responsiveness.
- **Synchronous IPC Approval Flow:** A specialized bridge mechanism that allows headless AI providers (like Gemini CLI) to synchronously request and receive human approval for tool calls via the GUI's REST API hooks.

View File

@@ -35,7 +35,4 @@ This file tracks all major tracks for the project. Each track has its own detail
--- ---
- [x] **Track: Support gemini cli headless as an alternative to the raw client_api route. So that they user may use their gemini subscription and gemini cli features within manual slop for a more discliplined and visually enriched UX.**
*Link: [./tracks/gemini_cli_headless_20260224/](./tracks/gemini_cli_headless_20260224/)*

View File

@@ -1,5 +1,5 @@
[ai] [ai]
provider = "gemini" provider = "gemini_cli"
model = "gemini-2.5-flash-lite" model = "gemini-2.5-flash-lite"
temperature = 0.0 temperature = 0.0
max_tokens = 8192 max_tokens = 8192

View File

@@ -12,7 +12,7 @@ ViewportPos=43,95
ViewportId=0x78C57832 ViewportId=0x78C57832
Size=897,649 Size=897,649
Collapsed=0 Collapsed=0
DockId=0x00000007,0 DockId=0x00000006,0
[Window][Files] [Window][Files]
ViewportPos=3125,170 ViewportPos=3125,170
@@ -33,7 +33,7 @@ DockId=0x0000000A,0
Pos=0,17 Pos=0,17
Size=1680,730 Size=1680,730
Collapsed=0 Collapsed=0
DockId=0x00000007,0 DockId=0x00000006,0
[Window][Provider] [Window][Provider]
ViewportPos=43,95 ViewportPos=43,95
@@ -41,7 +41,7 @@ ViewportId=0x78C57832
Pos=0,651 Pos=0,651
Size=897,468 Size=897,468
Collapsed=0 Collapsed=0
DockId=0x00000007,0 DockId=0x00000006,0
[Window][Message] [Window][Message]
Pos=0,749 Pos=0,749
@@ -79,9 +79,9 @@ DockId=0x0000000F,2
[Window][Theme] [Window][Theme]
Pos=0,17 Pos=0,17
Size=432,858 Size=588,400
Collapsed=0 Collapsed=0
DockId=0x00000007,2 DockId=0x00000005,1
[Window][Text Viewer - Entry #7] [Window][Text Viewer - Entry #7]
Pos=379,324 Pos=379,324
@@ -89,16 +89,16 @@ Size=900,700
Collapsed=0 Collapsed=0
[Window][Diagnostics] [Window][Diagnostics]
Pos=863,794 Pos=590,17
Size=817,406 Size=530,1183
Collapsed=0 Collapsed=0
DockId=0x00000006,0 DockId=0x0000000E,1
[Window][Context Hub] [Window][Context Hub]
Pos=0,17 Pos=0,17
Size=432,858 Size=588,400
Collapsed=0 Collapsed=0
DockId=0x00000007,0 DockId=0x00000005,0
[Window][AI Settings Hub] [Window][AI Settings Hub]
Pos=406,17 Pos=406,17
@@ -107,28 +107,28 @@ Collapsed=0
DockId=0x0000000D,0 DockId=0x0000000D,0
[Window][Discussion Hub] [Window][Discussion Hub]
Pos=863,17 Pos=1122,17
Size=817,775 Size=558,1183
Collapsed=0 Collapsed=0
DockId=0x00000005,0 DockId=0x00000004,0
[Window][Operations Hub] [Window][Operations Hub]
Pos=434,17 Pos=590,17
Size=427,858 Size=530,1183
Collapsed=0 Collapsed=0
DockId=0x0000000E,0 DockId=0x0000000E,0
[Window][Files & Media] [Window][Files & Media]
Pos=0,17 Pos=0,419
Size=432,858 Size=588,781
Collapsed=0 Collapsed=0
DockId=0x00000007,1 DockId=0x00000006,1
[Window][AI Settings] [Window][AI Settings]
Pos=0,877 Pos=0,419
Size=861,323 Size=588,781
Collapsed=0 Collapsed=0
DockId=0x00000013,0 DockId=0x00000006,0
[Docking][Data] [Docking][Data]
DockNode ID=0x00000008 Pos=3125,170 Size=593,1157 Split=Y DockNode ID=0x00000008 Pos=3125,170 Size=593,1157 Split=Y
@@ -136,18 +136,16 @@ DockNode ID=0x00000008 Pos=3125,170 Size=593,1157 Split=Y
DockNode ID=0x0000000A Parent=0x00000008 SizeRef=1029,145 Selected=0xDF822E02 DockNode ID=0x0000000A Parent=0x00000008 SizeRef=1029,145 Selected=0xDF822E02
DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,17 Size=1680,1183 Split=Y DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,17 Size=1680,1183 Split=Y
DockNode ID=0x0000000C Parent=0xAFC85805 SizeRef=1362,1041 Split=X Selected=0x5D11106F DockNode ID=0x0000000C Parent=0xAFC85805 SizeRef=1362,1041 Split=X Selected=0x5D11106F
DockNode ID=0x00000003 Parent=0x0000000C SizeRef=861,1183 Split=X DockNode ID=0x00000003 Parent=0x0000000C SizeRef=1120,1183 Split=X
DockNode ID=0x0000000B Parent=0x00000003 SizeRef=404,1186 Split=Y Selected=0xF4139CA2 DockNode ID=0x0000000B Parent=0x00000003 SizeRef=404,1186 Split=Y Selected=0xF4139CA2
DockNode ID=0x00000002 Parent=0x0000000B SizeRef=1029,1119 Split=Y Selected=0xF4139CA2 DockNode ID=0x00000002 Parent=0x0000000B SizeRef=1029,1119 Split=X Selected=0xF4139CA2
DockNode ID=0x00000010 Parent=0x00000002 SizeRef=432,328 Split=X Selected=0x8CA2375C DockNode ID=0x00000007 Parent=0x00000002 SizeRef=588,858 Split=Y Selected=0x8CA2375C
DockNode ID=0x00000007 Parent=0x00000010 SizeRef=432,858 CentralNode=1 Selected=0x8CA2375C DockNode ID=0x00000005 Parent=0x00000007 SizeRef=295,400 Selected=0xF4139CA2
DockNode ID=0x0000000E Parent=0x00000010 SizeRef=427,858 Selected=0x418C7449 DockNode ID=0x00000006 Parent=0x00000007 SizeRef=295,781 CentralNode=1 Selected=0x7BD57D6A
DockNode ID=0x00000013 Parent=0x00000002 SizeRef=432,323 Selected=0x7BD57D6A DockNode ID=0x0000000E Parent=0x00000002 SizeRef=530,858 Selected=0x418C7449
DockNode ID=0x00000001 Parent=0x0000000B SizeRef=1029,775 Selected=0x8B4EBFA6 DockNode ID=0x00000001 Parent=0x0000000B SizeRef=1029,775 Selected=0x8B4EBFA6
DockNode ID=0x0000000D Parent=0x00000003 SizeRef=435,1186 Selected=0x363E93D6 DockNode ID=0x0000000D Parent=0x00000003 SizeRef=435,1186 Selected=0x363E93D6
DockNode ID=0x00000004 Parent=0x0000000C SizeRef=817,1183 Split=Y Selected=0x418C7449 DockNode ID=0x00000004 Parent=0x0000000C SizeRef=558,1183 Selected=0x6F2B5B04
DockNode ID=0x00000005 Parent=0x00000004 SizeRef=837,775 Selected=0x6F2B5B04
DockNode ID=0x00000006 Parent=0x00000004 SizeRef=837,406 Selected=0xB4CBF21A
DockNode ID=0x0000000F Parent=0xAFC85805 SizeRef=1362,451 Selected=0xDD6419BC DockNode ID=0x0000000F Parent=0xAFC85805 SizeRef=1362,451 Selected=0xDD6419BC
;;;<<<Layout_655921752_Default>>>;;; ;;;<<<Layout_655921752_Default>>>;;;

View File

@@ -27,3 +27,6 @@ search_files = true
get_file_summary = true get_file_summary = true
web_search = true web_search = true
fetch_url = true fetch_url = true
[gemini_cli]
binary_path = "\"C:\\projects\\manual_slop\\.venv\\Scripts\\python.exe\" \"C:\\projects\\manual_slop\\tests\\mock_gemini_cli.py\""

View File

@@ -4,12 +4,12 @@ roles = [
"Vendor API", "Vendor API",
"System", "System",
] ]
active = "main" active = "testing gemini cli 2"
auto_add = true auto_add = true
[discussions.main] [discussions.main]
git_commit = "" git_commit = ""
last_updated = "2026-02-25T13:33:02" last_updated = "2026-02-25T18:29:54"
history = [ history = [
"@1772042443.1159382\nUser:\nStress test entry 0 Stress test entry 0 Stress test entry 0 Stress test entry 0 Stress test entry 0", "@1772042443.1159382\nUser:\nStress test entry 0 Stress test entry 0 Stress test entry 0 Stress test entry 0 Stress test entry 0",
"@1772042443.1159382\nUser:\nStress test entry 1 Stress test entry 1 Stress test entry 1 Stress test entry 1 Stress test entry 1", "@1772042443.1159382\nUser:\nStress test entry 1 Stress test entry 1 Stress test entry 1 Stress test entry 1 Stress test entry 1",
@@ -62,3 +62,27 @@ history = [
"@1772042443.1159382\nUser:\nStress test entry 48 Stress test entry 48 Stress test entry 48 Stress test entry 48 Stress test entry 48", "@1772042443.1159382\nUser:\nStress test entry 48 Stress test entry 48 Stress test entry 48 Stress test entry 48 Stress test entry 48",
"@1772042443.1159382\nUser:\nStress test entry 49 Stress test entry 49 Stress test entry 49 Stress test entry 49 Stress test entry 49", "@1772042443.1159382\nUser:\nStress test entry 49 Stress test entry 49 Stress test entry 49 Stress test entry 49 Stress test entry 49",
] ]
[discussions."testing gemini cli"]
git_commit = ""
last_updated = "2026-02-25T18:29:49"
history = [
"@2026-02-25T14:38:14\nUser:\nyo gemini cli you there?",
"@2026-02-25T14:38:21\nAI:\nTool execution was denied. Decision: deny",
"@2026-02-25T14:39:03\nSystem:\n[PERFORMANCE ALERT] Frame time high: 7956.5ms. Please consider optimizing recent changes or reducing load.",
"@2026-02-25T14:39:42\nSystem:\n[PERFORMANCE ALERT] Frame time high: 9193.6ms. Please consider optimizing recent changes or reducing load.",
"@2026-02-25T14:41:03\nSystem:\n[PERFORMANCE ALERT] Frame time high: 6645.0ms. Please consider optimizing recent changes or reducing load.",
"@2026-02-25T14:41:37\nSystem:\n[PERFORMANCE ALERT] Frame time high: 10553.5ms. Please consider optimizing recent changes or reducing load.",
]
[discussions."testing gemini cli 2"]
git_commit = ""
last_updated = "2026-02-25T18:38:17"
history = [
"@2026-02-25T18:35:22\nAI:\nTool execution was denied. Decision: deny",
"@2026-02-25T18:35:44\nUser:\nPlease read test.txt",
"@2026-02-25T18:35:45\nAI:\nI read the file. It contains: 'Hello from mock!'",
"@2026-02-25T18:35:46\nUser:\nDeny me",
"@2026-02-25T18:35:46\nAI:\nTool execution was denied. Decision: deny",
"@2026-02-25T18:38:17\nUser:\nDeny me",
]

View File

@@ -19,11 +19,12 @@ class TestGeminiCliAdapter(unittest.TestCase):
def test_send_starts_subprocess_with_correct_args(self, mock_popen): def test_send_starts_subprocess_with_correct_args(self, mock_popen):
""" """
Verify that send(message) correctly starts the subprocess with Verify that send(message) correctly starts the subprocess with
--output-format stream-json and the provided message. --output-format stream-json and the provided message via stdin.
""" """
# Setup mock process with a minimal valid JSONL termination # Setup mock process with a minimal valid JSONL termination
process_mock = MagicMock() process_mock = MagicMock()
process_mock.stdout = io.StringIO(json.dumps({"type": "result", "usage": {}}) + "\n") process_mock.stdout = io.StringIO(json.dumps({"type": "result", "usage": {}}) + "\n")
process_mock.stdin = MagicMock()
process_mock.poll.return_value = 0 process_mock.poll.return_value = 0
process_mock.wait.return_value = 0 process_mock.wait.return_value = 0
mock_popen.return_value = process_mock mock_popen.return_value = process_mock
@@ -40,10 +41,16 @@ class TestGeminiCliAdapter(unittest.TestCase):
self.assertIn("gemini", cmd) self.assertIn("gemini", cmd)
self.assertIn("--output-format", cmd) self.assertIn("--output-format", cmd)
self.assertIn("stream-json", cmd) self.assertIn("stream-json", cmd)
self.assertIn(message, cmd) # Message should NOT be in cmd now
self.assertNotIn(message, cmd)
# Verify message was written to stdin
process_mock.stdin.write.assert_called_once_with(message)
process_mock.stdin.close.assert_called_once()
# Check process configuration # Check process configuration
self.assertEqual(kwargs.get('stdout'), subprocess.PIPE) self.assertEqual(kwargs.get('stdout'), subprocess.PIPE)
self.assertEqual(kwargs.get('stdin'), subprocess.PIPE)
self.assertEqual(kwargs.get('text'), True) self.assertEqual(kwargs.get('text'), True)
@patch('subprocess.Popen') @patch('subprocess.Popen')

View File

@@ -84,3 +84,76 @@ def test_gemini_cli_full_integration(live_gui):
time.sleep(1.0) time.sleep(1.0)
assert final_message_received, "Final message from mock CLI was not found in the GUI history" assert final_message_received, "Final message from mock CLI was not found in the GUI history"
def test_gemini_cli_rejection_and_history(live_gui):
"""
Integration test for the Gemini CLI provider: Rejection flow and history.
"""
client = ApiHookClient("http://127.0.0.1:8999")
# 1. Setup paths and configure the GUI
mock_script = os.path.abspath("tests/mock_gemini_cli.py")
cli_cmd = f'"{sys.executable}" "{mock_script}"'
client.set_value("current_provider", "gemini_cli")
client.set_value("gcli_path", cli_cmd)
# 2. Trigger a message that will be denied
print("[TEST] Sending user message (to be denied)...")
client.set_value("ai_input", "Deny me")
client.click("btn_gen_send")
# 3. Wait for 'ask_received' and respond with rejection
request_id = None
timeout = 15
start_time = time.time()
while time.time() - start_time < timeout:
for ev in client.get_events():
if ev.get("type") == "ask_received":
request_id = ev.get("request_id")
break
if request_id: break
time.sleep(0.5)
assert request_id is not None
print("[TEST] Responding to ask with REJECTION")
requests.post("http://127.0.0.1:8999/api/ask/respond",
json={"request_id": request_id, "response": {"approved": False}})
# 4. Verify rejection message in history
print("[TEST] Waiting for rejection message in history...")
rejection_found = False
start_time = time.time()
while time.time() - start_time < timeout:
session = client.get_session()
entries = session.get("session", {}).get("entries", [])
for entry in entries:
if "Tool execution was denied. Decision: deny" in entry.get("content", ""):
rejection_found = True
break
if rejection_found: break
time.sleep(1.0)
assert rejection_found, "Rejection message not found in history"
# 5. Send a follow-up message and verify history grows
print("[TEST] Sending follow-up message...")
client.set_value("ai_input", "What happened?")
client.click("btn_gen_send")
# Wait for mock to finish (it will just return a message)
time.sleep(2)
session = client.get_session()
entries = session.get("session", {}).get("entries", [])
# Should have:
# 1. User: Deny me
# 2. AI: Tool execution was denied...
# 3. User: What happened?
# 4. AI: ...
print(f"[TEST] Final history length: {len(entries)}")
for i, entry in enumerate(entries):
print(f" {i}: {entry.get('role')} - {entry.get('content')[:30]}...")
assert len(entries) >= 4

View File

@@ -4,15 +4,10 @@ import requests
import pytest import pytest
from api_hook_client import ApiHookClient from api_hook_client import ApiHookClient
def test_api_ask_synchronous_flow(live_gui): def test_api_ask_client_method(live_gui):
""" """
Tests the full synchronous lifecycle of the /api/ask endpoint: Tests the request_confirmation method in ApiHookClient.
1. A client makes a blocking request.
2. An event is emitted with a unique request_id.
3. A separate agent responds to that request_id.
4. The original blocking request completes with the provided data.
""" """
# The live_gui fixture starts the Manual Slop application with hooks on 8999.
client = ApiHookClient("http://127.0.0.1:8999") client = ApiHookClient("http://127.0.0.1:8999")
# Drain existing events # Drain existing events
@@ -22,14 +17,11 @@ def test_api_ask_synchronous_flow(live_gui):
def make_blocking_request(): def make_blocking_request():
try: try:
# This POST will block until we call /api/ask/respond # This call should block until we respond
# Note: /api/ask returns {'status': 'ok', 'response': ...} results["response"] = client.request_confirmation(
resp = requests.post( tool_name="powershell",
"http://127.0.0.1:8999/api/ask", args={"command": "echo hello"}
json={"prompt": "Should we proceed with the refactor?"},
timeout=10
) )
results["response"] = resp.json()
except Exception as e: except Exception as e:
results["error"] = str(e) results["error"] = str(e)
@@ -37,7 +29,7 @@ def test_api_ask_synchronous_flow(live_gui):
t = threading.Thread(target=make_blocking_request) t = threading.Thread(target=make_blocking_request)
t.start() t.start()
# Poll for the 'ask_received' event to find the generated request_id # Poll for the 'ask_received' event
request_id = None request_id = None
start_time = time.time() start_time = time.time()
while time.time() - start_time < 5: while time.time() - start_time < 5:
@@ -52,8 +44,8 @@ def test_api_ask_synchronous_flow(live_gui):
assert request_id is not None, "Timed out waiting for 'ask_received' event" assert request_id is not None, "Timed out waiting for 'ask_received' event"
# Respond to the task via the respond endpoint # Respond
expected_response = {"approved": True, "message": "Proceeding as requested."} expected_response = {"approved": True}
resp = requests.post( resp = requests.post(
"http://127.0.0.1:8999/api/ask/respond", "http://127.0.0.1:8999/api/ask/respond",
json={ json={
@@ -63,11 +55,7 @@ def test_api_ask_synchronous_flow(live_gui):
) )
assert resp.status_code == 200 assert resp.status_code == 200
# Join the thread and verify the original request received the correct data
t.join(timeout=5) t.join(timeout=5)
assert not t.is_alive(), "Background thread failed to unblock" assert not t.is_alive()
assert results["error"] is None, f"Request failed: {results['error']}" assert results["error"] is None
assert results["response"] == expected_response
# The /api/ask endpoint returns {'status': 'ok', 'response': expected_response}
assert results["response"]["status"] == "ok"
assert results["response"]["response"] == expected_response