never ends

This commit is contained in:
2026-03-05 20:39:56 -05:00
parent 2c5476dc5d
commit d2481b2de7
7 changed files with 129 additions and 163 deletions

View File

@@ -15,7 +15,7 @@ paths = [
"C:\\projects\\manual_slop\\tests\\artifacts\\temp_livetoolssim.toml", "C:\\projects\\manual_slop\\tests\\artifacts\\temp_livetoolssim.toml",
"C:\\projects\\manual_slop\\tests\\artifacts\\temp_liveexecutionsim.toml", "C:\\projects\\manual_slop\\tests\\artifacts\\temp_liveexecutionsim.toml",
] ]
active = "C:\\projects\\manual_slop\\tests\\artifacts\\temp_livetoolssim.toml" active = "C:\\projects\\manual_slop\\tests\\artifacts\\temp_project.toml"
[gui.show_windows] [gui.show_windows]
"Context Hub" = true "Context Hub" = true

View File

@@ -8,5 +8,5 @@ active = "main"
[discussions.main] [discussions.main]
git_commit = "" git_commit = ""
last_updated = "2026-03-05T20:14:45" last_updated = "2026-03-05T20:31:48"
history = [] history = []

View File

@@ -15,10 +15,8 @@ class ApiHookClient:
headers = {} headers = {}
if self.api_key: if self.api_key:
headers["X-API-KEY"] = self.api_key headers["X-API-KEY"] = self.api_key
if method not in ('GET', 'POST', 'DELETE'): if method not in ('GET', 'POST', 'DELETE'):
raise ValueError(f"Unsupported HTTP method: {method}") raise ValueError(f"Unsupported HTTP method: {method}")
try: try:
if method == 'GET': if method == 'GET':
response = requests.get(url, headers=headers, timeout=timeout) response = requests.get(url, headers=headers, timeout=timeout)
@@ -26,12 +24,11 @@ class ApiHookClient:
response = requests.post(url, json=data, headers=headers, timeout=timeout) response = requests.post(url, json=data, headers=headers, timeout=timeout)
elif method == 'DELETE': elif method == 'DELETE':
response = requests.delete(url, headers=headers, timeout=timeout) response = requests.delete(url, headers=headers, timeout=timeout)
if response.status_code == 200: if response.status_code == 200:
return response.json() return response.json()
return None return None
except Exception as e: except Exception as e:
# Silently ignore connection errors unless we are in a wait loop # Silently ignore connection errors unless we are in a wait loop
return None return None
def wait_for_server(self, timeout: int = 15) -> bool: def wait_for_server(self, timeout: int = 15) -> bool:
@@ -48,15 +45,14 @@ class ApiHookClient:
"""Checks the health of the hook server.""" """Checks the health of the hook server."""
res = self._make_request('GET', '/status') res = self._make_request('GET', '/status')
if res is None: if res is None:
# For backward compatibility with tests expecting ConnectionError # For backward compatibility with tests expecting ConnectionError
# But our _make_request handles it. Let's return empty if failed. # But our _make_request handles it. Let's return empty if failed.
return {} return {}
return res return res
def post_project(self, project_data: dict) -> dict[str, Any]: def post_project(self, project_data: dict) -> dict[str, Any]:
return self._make_request('POST', '/api/project', data=project_data) or {} return self._make_request('POST', '/api/project', data=project_data) or {}
def get_project(self) -> dict[str, Any]: def get_project(self) -> dict[str, Any]:
"""Retrieves the current project state.""" """Retrieves the current project state."""
return self._make_request('GET', '/api/project') or {} return self._make_request('GET', '/api/project') or {}
@@ -69,7 +65,6 @@ class ApiHookClient:
"""Updates the session history.""" """Updates the session history."""
return self._make_request('POST', '/api/session', data={"session": {"entries": session_entries}}) or {} return self._make_request('POST', '/api/session', data={"session": {"entries": session_entries}}) or {}
def get_events(self) -> list[dict[str, Any]]: def get_events(self) -> list[dict[str, Any]]:
res = self._make_request('GET', '/api/events') res = self._make_request('GET', '/api/events')
return res.get("events", []) if res else [] return res.get("events", []) if res else []
@@ -111,13 +106,11 @@ class ApiHookClient:
state = self.get_gui_state() state = self.get_gui_state()
if item in state: if item in state:
return state[item] return state[item]
# Fallback for thinking/live/prior which are in diagnostics
# Fallback for thinking/live/prior which are in diagnostics
diag = self.get_gui_diagnostics() diag = self.get_gui_diagnostics()
if diag and item in diag: if diag and item in diag:
return diag[item] return diag[item]
# Map common indicator tags to diagnostics keys
# Map common indicator tags to diagnostics keys
mapping = { mapping = {
"thinking_indicator": "thinking", "thinking_indicator": "thinking",
"operations_live_indicator": "live", "operations_live_indicator": "live",
@@ -126,7 +119,6 @@ class ApiHookClient:
key = mapping.get(item) key = mapping.get(item)
if diag and key and key in diag: if diag and key and key in diag:
return diag[key] return diag[key]
return None return None
def get_text_value(self, item_tag: str) -> str | None: def get_text_value(self, item_tag: str) -> str | None:

View File

@@ -5,50 +5,46 @@ from unittest.mock import patch, MagicMock
from src.gemini_cli_adapter import GeminiCliAdapter from src.gemini_cli_adapter import GeminiCliAdapter
class TestGeminiCliAdapterParity(unittest.TestCase): class TestGeminiCliAdapterParity(unittest.TestCase):
def setUp(self) -> None: def setUp(self) -> None:
self.adapter = GeminiCliAdapter(binary_path="gemini") self.adapter = GeminiCliAdapter(binary_path="gemini")
def tearDown(self) -> None: def tearDown(self) -> None:
pass pass
def test_count_tokens_fallback(self) -> None: def test_count_tokens_fallback(self) -> None:
contents = ["Hello", "world!"] contents = ["Hello", "world!"]
estimated = self.adapter.count_tokens(contents) estimated = self.adapter.count_tokens(contents)
self.assertEqual(estimated, 3) self.assertEqual(estimated, 3)
@patch('src.gemini_cli_adapter.subprocess.Popen') @patch('src.gemini_cli_adapter.subprocess.Popen')
def test_send_starts_subprocess_with_model(self, mock_popen: MagicMock) -> None: def test_send_starts_subprocess_with_model(self, mock_popen: MagicMock) -> None:
mock_process = MagicMock() mock_process = MagicMock()
mock_process.communicate.return_value = ('{"type": "message", "content": "hi"}', '') mock_process.communicate.return_value = ('{"type": "message", "content": "hi"}', '')
mock_process.returncode = 0 mock_process.returncode = 0
mock_popen.return_value = mock_process mock_popen.return_value = mock_process
self.adapter.send("test", model="gemini-2.0-flash")
args, _ = mock_popen.call_args
cmd_list = args[0]
self.assertIn("-m", cmd_list)
self.assertIn("gemini-2.0-flash", cmd_list)
self.adapter.send("test", model="gemini-2.0-flash") @patch('src.gemini_cli_adapter.subprocess.Popen')
def test_send_parses_tool_calls_from_streaming_json(self, mock_popen: MagicMock) -> None:
args, _ = mock_popen.call_args tool_call_json = {
cmd_list = args[0] "type": "tool_use",
self.assertIn("-m", cmd_list) "tool_name": "list_directory",
self.assertIn("gemini-2.0-flash", cmd_list) "parameters": {"path": "."},
"tool_id": "call_abc"
@patch('src.gemini_cli_adapter.subprocess.Popen') }
def test_send_parses_tool_calls_from_streaming_json(self, mock_popen: MagicMock) -> None: mock_process = MagicMock()
tool_call_json = { stdout_output = (
"type": "tool_use", json.dumps(tool_call_json) + "\n" +
"tool_name": "list_directory", '{"type": "message", "content": "I listed the files."}'
"parameters": {"path": "."}, )
"tool_id": "call_abc" mock_process.communicate.return_value = (stdout_output, '')
} mock_process.returncode = 0
mock_popen.return_value = mock_process
mock_process = MagicMock() result = self.adapter.send("msg")
stdout_output = ( self.assertEqual(len(result["tool_calls"]), 1)
json.dumps(tool_call_json) + "\n" + self.assertEqual(result["tool_calls"][0]["name"], "list_directory")
'{"type": "message", "content": "I listed the files."}' self.assertEqual(result["text"], "I listed the files.")
)
mock_process.communicate.return_value = (stdout_output, '')
mock_process.returncode = 0
mock_popen.return_value = mock_process
result = self.adapter.send("msg")
self.assertEqual(len(result["tool_calls"]), 1)
self.assertEqual(result["tool_calls"][0]["name"], "list_directory")
self.assertEqual(result["text"], "I listed the files.")

View File

@@ -5,46 +5,38 @@ from src.gemini_cli_adapter import GeminiCliAdapter
from src import mcp_client from src import mcp_client
def test_gemini_cli_context_bleed_prevention() -> None: def test_gemini_cli_context_bleed_prevention() -> None:
import src.ai_client as ai_client import src.ai_client as ai_client
ai_client._gemini_cli_adapter = None ai_client._gemini_cli_adapter = None
with patch('src.gemini_cli_adapter.subprocess.Popen') as mock_popen:
with patch('src.gemini_cli_adapter.subprocess.Popen') as mock_popen: adapter = GeminiCliAdapter()
adapter = GeminiCliAdapter() mock_process = MagicMock()
stdout_output = (
mock_process = MagicMock() '{"type": "message", "role": "user", "content": "Echoed user prompt"}' + "\n" +
stdout_output = ( '{"type": "message", "role": "model", "content": "Model response"}'
'{"type": "message", "role": "user", "content": "Echoed user prompt"}' + "\n" + )
'{"type": "message", "role": "model", "content": "Model response"}' mock_process.communicate.return_value = (stdout_output, '')
) mock_process.returncode = 0
mock_process.communicate.return_value = (stdout_output, '') mock_popen.return_value = mock_process
mock_process.returncode = 0 result = adapter.send("msg")
mock_popen.return_value = mock_process assert result["text"] == "Model response"
result = adapter.send("msg")
assert result["text"] == "Model response"
def test_gemini_cli_parameter_resilience() -> None: def test_gemini_cli_parameter_resilience() -> None:
from src import mcp_client from src import mcp_client
with patch('src.mcp_client.read_file', return_value="content") as mock_read:
with patch('src.mcp_client.read_file', return_value="content") as mock_read: mcp_client.dispatch("read_file", {"file_path": "aliased.txt"})
mcp_client.dispatch("read_file", {"file_path": "aliased.txt"}) mock_read.assert_called_once_with("aliased.txt")
mock_read.assert_called_once_with("aliased.txt") with patch('src.mcp_client.list_directory', return_value="files") as mock_list:
mcp_client.dispatch("list_directory", {"dir_path": "aliased_dir"})
with patch('src.mcp_client.list_directory', return_value="files") as mock_list: mock_list.assert_called_once_with("aliased_dir")
mcp_client.dispatch("list_directory", {"dir_path": "aliased_dir"})
mock_list.assert_called_once_with("aliased_dir")
def test_gemini_cli_loop_termination() -> None: def test_gemini_cli_loop_termination() -> None:
import src.ai_client as ai_client import src.ai_client as ai_client
ai_client._gemini_cli_adapter = None ai_client._gemini_cli_adapter = None
with patch('src.gemini_cli_adapter.subprocess.Popen') as mock_popen:
with patch('src.gemini_cli_adapter.subprocess.Popen') as mock_popen: mock_process = MagicMock()
mock_process = MagicMock() mock_process.communicate.return_value = ('{"type": "message", "content": "Final answer", "tool_calls": []}', "")
mock_process.communicate.return_value = ('{"type": "message", "content": "Final answer", "tool_calls": []}', "") mock_process.returncode = 0
mock_process.returncode = 0 mock_popen.return_value = mock_process
mock_popen.return_value = mock_process ai_client.set_provider("gemini_cli", "gemini-2.0-flash")
result = ai_client.send("context", "prompt")
ai_client.set_provider("gemini_cli", "gemini-2.0-flash") assert result == "Final answer"
result = ai_client.send("context", "prompt")
assert result == "Final answer"

View File

@@ -2,30 +2,26 @@ from typing import Any
from unittest.mock import patch, MagicMock from unittest.mock import patch, MagicMock
def test_send_invokes_adapter_send() -> None: def test_send_invokes_adapter_send() -> None:
import src.ai_client as ai_client import src.ai_client as ai_client
ai_client._gemini_cli_adapter = None ai_client._gemini_cli_adapter = None
with patch('src.gemini_cli_adapter.subprocess.Popen') as mock_popen:
with patch('src.gemini_cli_adapter.subprocess.Popen') as mock_popen: mock_process = MagicMock()
mock_process = MagicMock() mock_process.communicate.return_value = ('{"type": "message", "content": "Hello from mock adapter"}', '')
mock_process.communicate.return_value = ('{"type": "message", "content": "Hello from mock adapter"}', '') mock_process.returncode = 0
mock_process.returncode = 0 mock_popen.return_value = mock_process
mock_popen.return_value = mock_process ai_client.set_provider("gemini_cli", "gemini-2.0-flash")
res = ai_client.send("context", "msg")
ai_client.set_provider("gemini_cli", "gemini-2.0-flash") assert res == "Hello from mock adapter"
res = ai_client.send("context", "msg")
assert res == "Hello from mock adapter"
def test_get_history_bleed_stats() -> None: def test_get_history_bleed_stats() -> None:
import src.ai_client as ai_client import src.ai_client as ai_client
ai_client._gemini_cli_adapter = None ai_client._gemini_cli_adapter = None
with patch('src.gemini_cli_adapter.subprocess.Popen') as mock_popen:
with patch('src.gemini_cli_adapter.subprocess.Popen') as mock_popen: mock_process = MagicMock()
mock_process = MagicMock() mock_process.communicate.return_value = ('{"type": "message", "content": "txt"}', '')
mock_process.communicate.return_value = ('{"type": "message", "content": "txt"}', '') mock_process.returncode = 0
mock_process.returncode = 0 mock_popen.return_value = mock_process
mock_popen.return_value = mock_process ai_client.set_provider("gemini_cli", "gemini-2.0-flash")
ai_client.send("context", "msg")
ai_client.set_provider("gemini_cli", "gemini-2.0-flash") stats = ai_client.get_history_bleed_stats()
ai_client.send("context", "msg") assert stats["provider"] == "gemini_cli"
stats = ai_client.get_history_bleed_stats()
assert stats["provider"] == "gemini_cli"

View File

@@ -10,43 +10,33 @@ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
from src import ai_client from src import ai_client
def test_token_usage_tracking() -> None: def test_token_usage_tracking() -> None:
ai_client.reset_session() ai_client.reset_session()
with patch("src.ai_client._ensure_gemini_client"), \
with patch("src.ai_client._ensure_gemini_client"), \ patch("src.ai_client._gemini_client") as mock_client:
patch("src.ai_client._gemini_client") as mock_client: mock_chat = MagicMock()
mock_client.chats.create.return_value = mock_chat
mock_chat = MagicMock() mock_usage = SimpleNamespace(
mock_client.chats.create.return_value = mock_chat prompt_token_count=100,
candidates_token_count=50,
mock_usage = SimpleNamespace( total_token_count=150,
prompt_token_count=100, cached_content_token_count=20
candidates_token_count=50, )
total_token_count=150, mock_part = SimpleNamespace(text="Mock Response", function_call=None)
cached_content_token_count=20 mock_content = SimpleNamespace(parts=[mock_part])
) mock_candidate = SimpleNamespace()
mock_candidate.content = mock_content
mock_part = SimpleNamespace(text="Mock Response", function_call=None) mock_candidate.finish_reason = SimpleNamespace(name="STOP")
mock_content = SimpleNamespace(parts=[mock_part]) mock_response = SimpleNamespace()
mock_response.candidates = [mock_candidate]
mock_candidate = SimpleNamespace() mock_response.usage_metadata = mock_usage
mock_candidate.content = mock_content mock_response.text = "Mock Response"
mock_candidate.finish_reason = SimpleNamespace(name="STOP") mock_chat.send_message.return_value = mock_response
ai_client.set_provider("gemini", "gemini-2.5-flash-lite")
mock_response = SimpleNamespace() ai_client.send("Context", "Hello")
mock_response.candidates = [mock_candidate] comms = ai_client.get_comms_log()
mock_response.usage_metadata = mock_usage response_entries = [e for e in comms if e.get("direction") == "IN" and e["kind"] == "response"]
mock_response.text = "Mock Response" assert len(response_entries) > 0
usage = response_entries[0]["payload"]["usage"]
mock_chat.send_message.return_value = mock_response assert usage["input_tokens"] == 100
assert usage["output_tokens"] == 50
ai_client.set_provider("gemini", "gemini-2.5-flash-lite") assert usage["cache_read_input_tokens"] == 20
ai_client.send("Context", "Hello")
comms = ai_client.get_comms_log()
response_entries = [e for e in comms if e.get("direction") == "IN" and e["kind"] == "response"]
assert len(response_entries) > 0
usage = response_entries[0]["payload"]["usage"]
assert usage["input_tokens"] == 100
assert usage["output_tokens"] == 50
assert usage["cache_read_input_tokens"] == 20