Private
Public Access
refactor(ai_client): remove gemini_cli provider from ai_client
Drop the standalone Gemini CLI adapter from the AI client surface: delete the import, the PROVIDERS entry, the module state, the 3 functions (_list_gemini_cli_models, _send_cli_round_result, _send_gemini_cli), and the 8 dispatch branches. PROVIDERS now has 7 entries; _gemini_sdk remainder is unaffected.
This commit is contained in:
@@ -1,33 +0,0 @@
|
||||
from unittest.mock import patch, MagicMock
|
||||
from src import ai_client
|
||||
from src.result_types import Result
|
||||
|
||||
|
||||
def test_ai_client_send_gemini_cli() -> None:
|
||||
test_message = "Hello, this is a test prompt for the CLI adapter."
|
||||
test_response = "This is a dummy response from the Gemini CLI."
|
||||
ai_client.reset_session()
|
||||
ai_client.set_provider("gemini_cli", "gemini-2.5-flash-lite")
|
||||
with patch("src.ai_client.GeminiCliAdapter") as MockAdapterClass:
|
||||
mock_adapter_instance = MagicMock()
|
||||
mock_adapter_instance.send.return_value = {
|
||||
"text": test_response,
|
||||
"tool_calls": [],
|
||||
}
|
||||
mock_adapter_instance.last_usage = {"total_tokens": 100}
|
||||
mock_adapter_instance.last_latency = 0.5
|
||||
mock_adapter_instance.session_id = "test-session"
|
||||
MockAdapterClass.return_value = mock_adapter_instance
|
||||
ai_client._gemini_cli_adapter = mock_adapter_instance
|
||||
with patch.object(ai_client.events, "emit") as mock_emit:
|
||||
result = ai_client.send(
|
||||
md_content="<context></context>",
|
||||
user_message=test_message,
|
||||
base_dir=".",
|
||||
)
|
||||
mock_adapter_instance.send.assert_called()
|
||||
emitted_event_names = [call.args[0] for call in mock_emit.call_args_list]
|
||||
assert "request_start" in emitted_event_names
|
||||
assert "response_received" in emitted_event_names
|
||||
assert result.ok
|
||||
assert result.data == test_response
|
||||
@@ -1,85 +0,0 @@
|
||||
import json
|
||||
from unittest.mock import patch, MagicMock
|
||||
from src.gemini_cli_adapter import GeminiCliAdapter
|
||||
|
||||
|
||||
class TestGeminiCliAdapter:
|
||||
@patch("subprocess.Popen")
|
||||
def test_send_starts_subprocess_with_correct_args(
|
||||
self, mock_popen: MagicMock
|
||||
) -> None:
|
||||
adapter = GeminiCliAdapter(binary_path="gemini")
|
||||
mock_process = MagicMock()
|
||||
mock_process.communicate.return_value = (
|
||||
'{"type": "message", "content": "hello"}',
|
||||
"",
|
||||
)
|
||||
mock_process.returncode = 0
|
||||
mock_popen.return_value = mock_process
|
||||
adapter.send("test prompt")
|
||||
assert mock_popen.called
|
||||
args, kwargs = mock_popen.call_args
|
||||
cmd_list = args[0]
|
||||
assert "gemini" in cmd_list
|
||||
assert "--prompt" in cmd_list
|
||||
assert "--output-format" in cmd_list
|
||||
assert "stream-json" in cmd_list
|
||||
|
||||
@patch("subprocess.Popen")
|
||||
def test_send_parses_jsonl_output(self, mock_popen: MagicMock) -> None:
|
||||
adapter = GeminiCliAdapter()
|
||||
stdout_str = '{"type": "message", "content": "Hello "}\n{"type": "message", "content": "world!"}\n'
|
||||
mock_process = MagicMock()
|
||||
mock_process.communicate.return_value = (stdout_str, "")
|
||||
mock_process.returncode = 0
|
||||
mock_popen.return_value = mock_process
|
||||
result = adapter.send("msg")
|
||||
assert result["text"] == "Hello world!"
|
||||
|
||||
@patch("subprocess.Popen")
|
||||
def test_send_handles_tool_use_events(self, mock_popen: MagicMock) -> None:
|
||||
adapter = GeminiCliAdapter()
|
||||
tool_json = {
|
||||
"type": "tool_use",
|
||||
"tool_name": "read_file",
|
||||
"parameters": {"path": "test.txt"},
|
||||
"tool_id": "call_123",
|
||||
}
|
||||
stdout_str = json.dumps(tool_json) + "\n"
|
||||
mock_process = MagicMock()
|
||||
mock_process.communicate.return_value = (stdout_str, "")
|
||||
mock_process.returncode = 0
|
||||
mock_popen.return_value = mock_process
|
||||
result = adapter.send("msg")
|
||||
assert len(result["tool_calls"]) == 1
|
||||
assert result["tool_calls"][0]["name"] == "read_file"
|
||||
assert result["tool_calls"][0]["args"]["path"] == "test.txt"
|
||||
|
||||
@patch("subprocess.Popen")
|
||||
def test_send_captures_usage_metadata(self, mock_popen: MagicMock) -> None:
|
||||
adapter = GeminiCliAdapter()
|
||||
result_json = {"type": "result", "stats": {"total_tokens": 50}}
|
||||
stdout_str = json.dumps(result_json) + "\n"
|
||||
mock_process = MagicMock()
|
||||
mock_process.communicate.return_value = (stdout_str, "")
|
||||
mock_process.returncode = 0
|
||||
mock_popen.return_value = mock_process
|
||||
adapter.send("msg")
|
||||
assert adapter.last_usage is not None
|
||||
assert adapter.last_usage.get("total_tokens") == 50
|
||||
|
||||
@patch("subprocess.Popen")
|
||||
def test_full_flow_integration(self, mock_popen: MagicMock) -> None:
|
||||
adapter = GeminiCliAdapter()
|
||||
msg_json = {"type": "message", "content": "Final response"}
|
||||
result_json = {
|
||||
"type": "result",
|
||||
"stats": {"total_tokens": 25, "input_tokens": 10, "output_tokens": 15},
|
||||
}
|
||||
stdout_str = json.dumps(msg_json) + "\n" + json.dumps(result_json) + "\n"
|
||||
mock_process = MagicMock()
|
||||
mock_process.communicate.return_value = (stdout_str, "")
|
||||
mock_process.returncode = 0
|
||||
mock_popen.return_value = mock_process
|
||||
result = adapter.send("test")
|
||||
assert "Final response" in result["text"]
|
||||
@@ -1,49 +0,0 @@
|
||||
import unittest
|
||||
import json
|
||||
from unittest.mock import patch, MagicMock
|
||||
from src.gemini_cli_adapter import GeminiCliAdapter
|
||||
|
||||
class TestGeminiCliAdapterParity(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.adapter = GeminiCliAdapter(binary_path="gemini")
|
||||
|
||||
def tearDown(self) -> None:
|
||||
pass
|
||||
|
||||
def test_count_tokens_fallback(self) -> None:
|
||||
contents = ["Hello", "world!"]
|
||||
estimated = self.adapter.count_tokens(contents)
|
||||
self.assertEqual(estimated, 3)
|
||||
|
||||
@patch('src.gemini_cli_adapter.subprocess.Popen')
|
||||
def test_send_starts_subprocess_with_model(self, mock_popen: MagicMock) -> None:
|
||||
mock_process = MagicMock()
|
||||
mock_process.communicate.return_value = ('{"type": "message", "content": "hi"}', '')
|
||||
mock_process.returncode = 0
|
||||
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)
|
||||
|
||||
@patch('src.gemini_cli_adapter.subprocess.Popen')
|
||||
def test_send_parses_tool_calls_from_streaming_json(self, mock_popen: MagicMock) -> None:
|
||||
tool_call_json = {
|
||||
"type": "tool_use",
|
||||
"tool_name": "list_directory",
|
||||
"parameters": {"path": "."},
|
||||
"tool_id": "call_abc"
|
||||
}
|
||||
mock_process = MagicMock()
|
||||
stdout_output = (
|
||||
json.dumps(tool_call_json) + "\n" +
|
||||
'{"type": "message", "content": "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.")
|
||||
@@ -1,41 +0,0 @@
|
||||
from unittest.mock import patch, MagicMock
|
||||
from src.gemini_cli_adapter import GeminiCliAdapter
|
||||
from src import mcp_client
|
||||
from src.result_types import Result
|
||||
|
||||
def test_gemini_cli_context_bleed_prevention() -> None:
|
||||
import src.ai_client as ai_client
|
||||
ai_client._gemini_cli_adapter = None
|
||||
with patch('src.gemini_cli_adapter.subprocess.Popen') as mock_popen:
|
||||
adapter = GeminiCliAdapter()
|
||||
mock_process = MagicMock()
|
||||
stdout_output = (
|
||||
'{"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_popen.return_value = mock_process
|
||||
result = adapter.send("msg")
|
||||
assert result["text"] == "Model response"
|
||||
|
||||
def test_gemini_cli_parameter_resilience() -> None:
|
||||
with patch('src.mcp_client.read_file', return_value="content") as mock_read:
|
||||
mcp_client.dispatch("read_file", {"file_path": "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"})
|
||||
mock_list.assert_called_once_with("aliased_dir")
|
||||
|
||||
def test_gemini_cli_loop_termination() -> None:
|
||||
import src.ai_client as ai_client
|
||||
ai_client._gemini_cli_adapter = None
|
||||
with patch('src.gemini_cli_adapter.subprocess.Popen') as mock_popen:
|
||||
mock_process = MagicMock()
|
||||
mock_process.communicate.return_value = ('{"type": "message", "content": "Final answer", "tool_calls": []}', "")
|
||||
mock_process.returncode = 0
|
||||
mock_popen.return_value = mock_process
|
||||
ai_client.set_provider("gemini_cli", "gemini-2.0-flash")
|
||||
result = ai_client.send("context", "prompt")
|
||||
assert result.ok
|
||||
assert result.data == "Final answer"
|
||||
@@ -1,32 +0,0 @@
|
||||
from unittest.mock import MagicMock
|
||||
from src import ai_client
|
||||
from src.result_types import Result
|
||||
|
||||
|
||||
def test_gemini_cli_full_integration() -> None:
|
||||
ai_client.reset_session()
|
||||
ai_client.set_provider("gemini_cli", "gemini-2.0-flash")
|
||||
mock_adapter = MagicMock()
|
||||
mock_adapter.send.return_value = {
|
||||
"text": "Final integrated answer",
|
||||
"tool_calls": [],
|
||||
}
|
||||
mock_adapter.last_usage = {"total_tokens": 10}
|
||||
ai_client._gemini_cli_adapter = mock_adapter
|
||||
result = ai_client.send("context", "integrated test")
|
||||
assert result.ok
|
||||
assert "Final integrated answer" in result.data
|
||||
|
||||
|
||||
def test_gemini_cli_rejection_and_history() -> None:
|
||||
ai_client.reset_session()
|
||||
ai_client.set_provider("gemini_cli", "gemini-2.0-flash")
|
||||
mock_adapter = MagicMock()
|
||||
mock_adapter.send.return_value = {
|
||||
"text": "",
|
||||
"tool_calls": [{"name": "run_powershell", "args": {"script": "dir"}}],
|
||||
}
|
||||
mock_adapter.last_usage = {}
|
||||
ai_client._gemini_cli_adapter = mock_adapter
|
||||
result = ai_client.send("ctx", "msg", pre_tool_callback=lambda *a, **kw: None)
|
||||
assert result is not None
|
||||
@@ -1,15 +0,0 @@
|
||||
from unittest.mock import patch, MagicMock
|
||||
from src.result_types import Result
|
||||
|
||||
def test_send_invokes_adapter_send() -> None:
|
||||
import src.ai_client as ai_client
|
||||
ai_client._gemini_cli_adapter = None
|
||||
with patch('src.gemini_cli_adapter.subprocess.Popen') as mock_popen:
|
||||
mock_process = MagicMock()
|
||||
mock_process.communicate.return_value = ('{"type": "message", "content": "Hello from mock adapter"}', '')
|
||||
mock_process.returncode = 0
|
||||
mock_popen.return_value = mock_process
|
||||
ai_client.set_provider("gemini_cli", "gemini-2.0-flash")
|
||||
res = ai_client.send("context", "msg")
|
||||
assert res.ok
|
||||
assert res.data == "Hello from mock adapter"
|
||||
@@ -1,69 +0,0 @@
|
||||
import subprocess
|
||||
import json
|
||||
|
||||
|
||||
def get_message_content(stdout):
|
||||
for line in stdout.splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
obj = json.loads(line)
|
||||
if isinstance(obj, dict) and obj.get('type') == 'message':
|
||||
return obj.get('content', '')
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
return ''
|
||||
|
||||
|
||||
def run_mock(prompt):
|
||||
return subprocess.run(
|
||||
['uv', 'run', 'python', 'tests/mock_gemini_cli.py'],
|
||||
input=prompt,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd='.'
|
||||
)
|
||||
|
||||
|
||||
def test_epic_prompt_returns_track_json():
|
||||
result = run_mock('PATH: Epic Initialization — please produce tracks')
|
||||
assert result.returncode == 0
|
||||
assert 'function_call' not in result.stdout
|
||||
content = get_message_content(result.stdout)
|
||||
parsed = json.loads(content)
|
||||
assert isinstance(parsed, list)
|
||||
assert len(parsed) > 0
|
||||
for item in parsed:
|
||||
assert 'id' in item
|
||||
assert 'title' in item
|
||||
|
||||
|
||||
def test_sprint_prompt_returns_ticket_json():
|
||||
result = run_mock('Please generate the implementation tickets for this track.')
|
||||
assert result.returncode == 0
|
||||
assert 'function_call' not in result.stdout
|
||||
content = get_message_content(result.stdout)
|
||||
parsed = json.loads(content)
|
||||
assert isinstance(parsed, list)
|
||||
assert len(parsed) > 0
|
||||
for item in parsed:
|
||||
assert 'id' in item
|
||||
assert 'description' in item
|
||||
assert 'status' in item
|
||||
assert 'assigned_to' in item
|
||||
|
||||
|
||||
def test_worker_prompt_returns_plain_text():
|
||||
result = run_mock('Please read test.txt\nYou are assigned to Ticket T1.\nTask Description: do something')
|
||||
assert result.returncode == 0
|
||||
assert 'function_call' not in result.stdout
|
||||
content = get_message_content(result.stdout)
|
||||
assert content != ''
|
||||
|
||||
|
||||
def test_tool_result_prompt_returns_plain_text():
|
||||
result = run_mock('role: tool\nHere are the results: {"content": "done"}')
|
||||
assert result.returncode == 0
|
||||
content = get_message_content(result.stdout)
|
||||
assert content != ''
|
||||
Reference in New Issue
Block a user