chore(conductor): Archive gemini_cli_headless_20260224 track and update tests
This commit is contained in:
@@ -1,10 +1,19 @@
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"name": "manual-slop-bridge",
|
||||
"type": "command",
|
||||
"event": "BeforeTool",
|
||||
"command": "python C:/projects/manual_slop/scripts/cli_tool_bridge.py"
|
||||
}
|
||||
]
|
||||
"hooks": {
|
||||
"BeforeTool": [
|
||||
{
|
||||
"matcher": "*",
|
||||
"hooks": [
|
||||
{
|
||||
"name": "manual-slop-bridge",
|
||||
"type": "command",
|
||||
"command": "python C:/projects/manual_slop/scripts/cli_tool_bridge.py"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"hooksConfig": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,3 +21,4 @@ To serve as an expert-level utility for personal developer use on small projects
|
||||
- **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).
|
||||
- **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.
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
- **google-genai:** For Google Gemini API interaction and explicit context 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
|
||||
|
||||
@@ -32,3 +33,4 @@
|
||||
## Architectural Patterns
|
||||
|
||||
- **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.
|
||||
|
||||
@@ -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/)*
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[ai]
|
||||
provider = "gemini"
|
||||
provider = "gemini_cli"
|
||||
model = "gemini-2.5-flash-lite"
|
||||
temperature = 0.0
|
||||
max_tokens = 8192
|
||||
|
||||
@@ -12,7 +12,7 @@ ViewportPos=43,95
|
||||
ViewportId=0x78C57832
|
||||
Size=897,649
|
||||
Collapsed=0
|
||||
DockId=0x00000007,0
|
||||
DockId=0x00000006,0
|
||||
|
||||
[Window][Files]
|
||||
ViewportPos=3125,170
|
||||
@@ -33,7 +33,7 @@ DockId=0x0000000A,0
|
||||
Pos=0,17
|
||||
Size=1680,730
|
||||
Collapsed=0
|
||||
DockId=0x00000007,0
|
||||
DockId=0x00000006,0
|
||||
|
||||
[Window][Provider]
|
||||
ViewportPos=43,95
|
||||
@@ -41,7 +41,7 @@ ViewportId=0x78C57832
|
||||
Pos=0,651
|
||||
Size=897,468
|
||||
Collapsed=0
|
||||
DockId=0x00000007,0
|
||||
DockId=0x00000006,0
|
||||
|
||||
[Window][Message]
|
||||
Pos=0,749
|
||||
@@ -79,9 +79,9 @@ DockId=0x0000000F,2
|
||||
|
||||
[Window][Theme]
|
||||
Pos=0,17
|
||||
Size=432,858
|
||||
Size=588,400
|
||||
Collapsed=0
|
||||
DockId=0x00000007,2
|
||||
DockId=0x00000005,1
|
||||
|
||||
[Window][Text Viewer - Entry #7]
|
||||
Pos=379,324
|
||||
@@ -89,16 +89,16 @@ Size=900,700
|
||||
Collapsed=0
|
||||
|
||||
[Window][Diagnostics]
|
||||
Pos=863,794
|
||||
Size=817,406
|
||||
Pos=590,17
|
||||
Size=530,1183
|
||||
Collapsed=0
|
||||
DockId=0x00000006,0
|
||||
DockId=0x0000000E,1
|
||||
|
||||
[Window][Context Hub]
|
||||
Pos=0,17
|
||||
Size=432,858
|
||||
Size=588,400
|
||||
Collapsed=0
|
||||
DockId=0x00000007,0
|
||||
DockId=0x00000005,0
|
||||
|
||||
[Window][AI Settings Hub]
|
||||
Pos=406,17
|
||||
@@ -107,28 +107,28 @@ Collapsed=0
|
||||
DockId=0x0000000D,0
|
||||
|
||||
[Window][Discussion Hub]
|
||||
Pos=863,17
|
||||
Size=817,775
|
||||
Pos=1122,17
|
||||
Size=558,1183
|
||||
Collapsed=0
|
||||
DockId=0x00000005,0
|
||||
DockId=0x00000004,0
|
||||
|
||||
[Window][Operations Hub]
|
||||
Pos=434,17
|
||||
Size=427,858
|
||||
Pos=590,17
|
||||
Size=530,1183
|
||||
Collapsed=0
|
||||
DockId=0x0000000E,0
|
||||
|
||||
[Window][Files & Media]
|
||||
Pos=0,17
|
||||
Size=432,858
|
||||
Pos=0,419
|
||||
Size=588,781
|
||||
Collapsed=0
|
||||
DockId=0x00000007,1
|
||||
DockId=0x00000006,1
|
||||
|
||||
[Window][AI Settings]
|
||||
Pos=0,877
|
||||
Size=861,323
|
||||
Pos=0,419
|
||||
Size=588,781
|
||||
Collapsed=0
|
||||
DockId=0x00000013,0
|
||||
DockId=0x00000006,0
|
||||
|
||||
[Docking][Data]
|
||||
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
|
||||
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=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=0x00000002 Parent=0x0000000B SizeRef=1029,1119 Split=Y Selected=0xF4139CA2
|
||||
DockNode ID=0x00000010 Parent=0x00000002 SizeRef=432,328 Split=X Selected=0x8CA2375C
|
||||
DockNode ID=0x00000007 Parent=0x00000010 SizeRef=432,858 CentralNode=1 Selected=0x8CA2375C
|
||||
DockNode ID=0x0000000E Parent=0x00000010 SizeRef=427,858 Selected=0x418C7449
|
||||
DockNode ID=0x00000013 Parent=0x00000002 SizeRef=432,323 Selected=0x7BD57D6A
|
||||
DockNode ID=0x00000002 Parent=0x0000000B SizeRef=1029,1119 Split=X Selected=0xF4139CA2
|
||||
DockNode ID=0x00000007 Parent=0x00000002 SizeRef=588,858 Split=Y Selected=0x8CA2375C
|
||||
DockNode ID=0x00000005 Parent=0x00000007 SizeRef=295,400 Selected=0xF4139CA2
|
||||
DockNode ID=0x00000006 Parent=0x00000007 SizeRef=295,781 CentralNode=1 Selected=0x7BD57D6A
|
||||
DockNode ID=0x0000000E Parent=0x00000002 SizeRef=530,858 Selected=0x418C7449
|
||||
DockNode ID=0x00000001 Parent=0x0000000B SizeRef=1029,775 Selected=0x8B4EBFA6
|
||||
DockNode ID=0x0000000D Parent=0x00000003 SizeRef=435,1186 Selected=0x363E93D6
|
||||
DockNode ID=0x00000004 Parent=0x0000000C SizeRef=817,1183 Split=Y Selected=0x418C7449
|
||||
DockNode ID=0x00000005 Parent=0x00000004 SizeRef=837,775 Selected=0x6F2B5B04
|
||||
DockNode ID=0x00000006 Parent=0x00000004 SizeRef=837,406 Selected=0xB4CBF21A
|
||||
DockNode ID=0x00000004 Parent=0x0000000C SizeRef=558,1183 Selected=0x6F2B5B04
|
||||
DockNode ID=0x0000000F Parent=0xAFC85805 SizeRef=1362,451 Selected=0xDD6419BC
|
||||
|
||||
;;;<<<Layout_655921752_Default>>>;;;
|
||||
|
||||
@@ -27,3 +27,6 @@ search_files = true
|
||||
get_file_summary = true
|
||||
web_search = 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\""
|
||||
|
||||
@@ -4,12 +4,12 @@ roles = [
|
||||
"Vendor API",
|
||||
"System",
|
||||
]
|
||||
active = "main"
|
||||
active = "testing gemini cli 2"
|
||||
auto_add = true
|
||||
|
||||
[discussions.main]
|
||||
git_commit = ""
|
||||
last_updated = "2026-02-25T13:33:02"
|
||||
last_updated = "2026-02-25T18:29:54"
|
||||
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 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 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",
|
||||
]
|
||||
|
||||
@@ -19,11 +19,12 @@ class TestGeminiCliAdapter(unittest.TestCase):
|
||||
def test_send_starts_subprocess_with_correct_args(self, mock_popen):
|
||||
"""
|
||||
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
|
||||
process_mock = MagicMock()
|
||||
process_mock.stdout = io.StringIO(json.dumps({"type": "result", "usage": {}}) + "\n")
|
||||
process_mock.stdin = MagicMock()
|
||||
process_mock.poll.return_value = 0
|
||||
process_mock.wait.return_value = 0
|
||||
mock_popen.return_value = process_mock
|
||||
@@ -40,10 +41,16 @@ class TestGeminiCliAdapter(unittest.TestCase):
|
||||
self.assertIn("gemini", cmd)
|
||||
self.assertIn("--output-format", 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
|
||||
self.assertEqual(kwargs.get('stdout'), subprocess.PIPE)
|
||||
self.assertEqual(kwargs.get('stdin'), subprocess.PIPE)
|
||||
self.assertEqual(kwargs.get('text'), True)
|
||||
|
||||
@patch('subprocess.Popen')
|
||||
|
||||
@@ -84,3 +84,76 @@ def test_gemini_cli_full_integration(live_gui):
|
||||
time.sleep(1.0)
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -4,15 +4,10 @@ import requests
|
||||
import pytest
|
||||
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:
|
||||
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.
|
||||
Tests the request_confirmation method in ApiHookClient.
|
||||
"""
|
||||
# The live_gui fixture starts the Manual Slop application with hooks on 8999.
|
||||
client = ApiHookClient("http://127.0.0.1:8999")
|
||||
|
||||
# Drain existing events
|
||||
@@ -22,14 +17,11 @@ def test_api_ask_synchronous_flow(live_gui):
|
||||
|
||||
def make_blocking_request():
|
||||
try:
|
||||
# This POST will block until we call /api/ask/respond
|
||||
# Note: /api/ask returns {'status': 'ok', 'response': ...}
|
||||
resp = requests.post(
|
||||
"http://127.0.0.1:8999/api/ask",
|
||||
json={"prompt": "Should we proceed with the refactor?"},
|
||||
timeout=10
|
||||
# This call should block until we respond
|
||||
results["response"] = client.request_confirmation(
|
||||
tool_name="powershell",
|
||||
args={"command": "echo hello"}
|
||||
)
|
||||
results["response"] = resp.json()
|
||||
except Exception as 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.start()
|
||||
|
||||
# Poll for the 'ask_received' event to find the generated request_id
|
||||
# Poll for the 'ask_received' event
|
||||
request_id = None
|
||||
start_time = time.time()
|
||||
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"
|
||||
|
||||
# Respond to the task via the respond endpoint
|
||||
expected_response = {"approved": True, "message": "Proceeding as requested."}
|
||||
# Respond
|
||||
expected_response = {"approved": True}
|
||||
resp = requests.post(
|
||||
"http://127.0.0.1:8999/api/ask/respond",
|
||||
json={
|
||||
@@ -63,11 +55,7 @@ def test_api_ask_synchronous_flow(live_gui):
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
# Join the thread and verify the original request received the correct data
|
||||
t.join(timeout=5)
|
||||
assert not t.is_alive(), "Background thread failed to unblock"
|
||||
assert results["error"] is None, f"Request failed: {results['error']}"
|
||||
|
||||
# The /api/ask endpoint returns {'status': 'ok', 'response': expected_response}
|
||||
assert results["response"]["status"] == "ok"
|
||||
assert results["response"]["response"] == expected_response
|
||||
assert not t.is_alive()
|
||||
assert results["error"] is None
|
||||
assert results["response"] == expected_response
|
||||
|
||||
Reference in New Issue
Block a user