feat(ai): integrate GeminiCliAdapter into ai_client
This commit is contained in:
39
tests/test_ai_client_cli.py
Normal file
39
tests/test_ai_client_cli.py
Normal file
@@ -0,0 +1,39 @@
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
import ai_client
|
||||
|
||||
def test_ai_client_send_gemini_cli():
|
||||
"""
|
||||
Verifies that 'ai_client.send' correctly interacts with 'GeminiCliAdapter'
|
||||
when the 'gemini_cli' provider is specified.
|
||||
"""
|
||||
test_message = "Hello, this is a test prompt for the CLI adapter."
|
||||
test_response = "This is a dummy response from the Gemini CLI."
|
||||
|
||||
# Set provider to gemini_cli
|
||||
ai_client.set_provider("gemini_cli", "gemini-2.0-flash")
|
||||
|
||||
# 1. Mock 'ai_client.GeminiCliAdapter' (which we will add)
|
||||
with patch('ai_client.GeminiCliAdapter') as MockAdapterClass:
|
||||
mock_adapter_instance = MockAdapterClass.return_value
|
||||
mock_adapter_instance.send.return_value = test_response
|
||||
mock_adapter_instance.last_usage = {"total_tokens": 100}
|
||||
|
||||
# Verify that 'events' are emitted correctly
|
||||
with patch.object(ai_client.events, 'emit') as mock_emit:
|
||||
response = ai_client.send(
|
||||
md_content="<context></context>",
|
||||
user_message=test_message,
|
||||
base_dir="."
|
||||
)
|
||||
|
||||
# Check that the adapter's send method was called.
|
||||
mock_adapter_instance.send.assert_called()
|
||||
|
||||
# Verify that the expected lifecycle events were emitted.
|
||||
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
|
||||
|
||||
# Verify that the combined text returned by the adapter is returned by 'ai_client.send'.
|
||||
assert response == test_response
|
||||
122
tests/test_gemini_cli_adapter.py
Normal file
122
tests/test_gemini_cli_adapter.py
Normal file
@@ -0,0 +1,122 @@
|
||||
import unittest
|
||||
from unittest.mock import patch, MagicMock
|
||||
import json
|
||||
import subprocess
|
||||
import io
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Ensure the project root is in sys.path to resolve imports correctly
|
||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||
|
||||
from gemini_cli_adapter import GeminiCliAdapter
|
||||
|
||||
class TestGeminiCliAdapter(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.adapter = GeminiCliAdapter(binary_path="gemini")
|
||||
|
||||
@patch('subprocess.Popen')
|
||||
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.
|
||||
"""
|
||||
# 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.poll.return_value = 0
|
||||
process_mock.wait.return_value = 0
|
||||
mock_popen.return_value = process_mock
|
||||
|
||||
message = "Hello Gemini CLI"
|
||||
self.adapter.send(message)
|
||||
|
||||
# Verify subprocess.Popen call
|
||||
mock_popen.assert_called_once()
|
||||
args, kwargs = mock_popen.call_args
|
||||
cmd = args[0]
|
||||
|
||||
# Check mandatory CLI components
|
||||
self.assertIn("gemini", cmd)
|
||||
self.assertIn("--output-format", cmd)
|
||||
self.assertIn("stream-json", cmd)
|
||||
self.assertIn(message, cmd)
|
||||
|
||||
# Check process configuration
|
||||
self.assertEqual(kwargs.get('stdout'), subprocess.PIPE)
|
||||
self.assertEqual(kwargs.get('text'), True)
|
||||
|
||||
@patch('subprocess.Popen')
|
||||
def test_send_parses_jsonl_output(self, mock_popen):
|
||||
"""
|
||||
Verify that it correctly parses multiple JSONL 'message' events
|
||||
and returns the combined text.
|
||||
"""
|
||||
jsonl_output = [
|
||||
json.dumps({"type": "message", "text": "The quick brown "}),
|
||||
json.dumps({"type": "message", "text": "fox jumps."}),
|
||||
json.dumps({"type": "result", "usage": {"prompt_tokens": 5, "candidates_tokens": 5}})
|
||||
]
|
||||
stdout_content = "\n".join(jsonl_output) + "\n"
|
||||
|
||||
process_mock = MagicMock()
|
||||
process_mock.stdout = io.StringIO(stdout_content)
|
||||
# Mock poll sequence: running, running, finished
|
||||
process_mock.poll.side_effect = [None, None, 0]
|
||||
process_mock.wait.return_value = 0
|
||||
mock_popen.return_value = process_mock
|
||||
|
||||
result = self.adapter.send("test message")
|
||||
|
||||
self.assertEqual(result, "The quick brown fox jumps.")
|
||||
|
||||
@patch('subprocess.Popen')
|
||||
def test_send_handles_tool_use_events(self, mock_popen):
|
||||
"""
|
||||
Verify that it correctly handles 'tool_use' events in the stream
|
||||
by continuing to read until the final 'result' event.
|
||||
"""
|
||||
jsonl_output = [
|
||||
json.dumps({"type": "message", "text": "Calling tool..."}),
|
||||
json.dumps({"type": "tool_use", "name": "read_file", "args": {"path": "test.txt"}}),
|
||||
json.dumps({"type": "message", "text": "\nFile read successfully."}),
|
||||
json.dumps({"type": "result", "usage": {}})
|
||||
]
|
||||
stdout_content = "\n".join(jsonl_output) + "\n"
|
||||
|
||||
process_mock = MagicMock()
|
||||
process_mock.stdout = io.StringIO(stdout_content)
|
||||
process_mock.poll.side_effect = [None, None, None, 0]
|
||||
process_mock.wait.return_value = 0
|
||||
mock_popen.return_value = process_mock
|
||||
|
||||
result = self.adapter.send("read test.txt")
|
||||
|
||||
# Result should contain the combined text from all 'message' events
|
||||
self.assertEqual(result, "Calling tool...\nFile read successfully.")
|
||||
|
||||
@patch('subprocess.Popen')
|
||||
def test_send_captures_usage_metadata(self, mock_popen):
|
||||
"""
|
||||
Verify that usage data is extracted from the 'result' event.
|
||||
"""
|
||||
usage_data = {"total_tokens": 42}
|
||||
jsonl_output = [
|
||||
json.dumps({"type": "message", "text": "Finalizing"}),
|
||||
json.dumps({"type": "result", "usage": usage_data})
|
||||
]
|
||||
stdout_content = "\n".join(jsonl_output) + "\n"
|
||||
|
||||
process_mock = MagicMock()
|
||||
process_mock.stdout = io.StringIO(stdout_content)
|
||||
process_mock.poll.side_effect = [None, 0]
|
||||
process_mock.wait.return_value = 0
|
||||
mock_popen.return_value = process_mock
|
||||
|
||||
self.adapter.send("usage test")
|
||||
|
||||
# Verify the usage was captured in the adapter instance
|
||||
self.assertEqual(self.adapter.last_usage, usage_data)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user