WIP: I HATE PYTHON
This commit is contained in:
@@ -1,155 +1,62 @@
|
||||
import unittest
|
||||
from unittest.mock import patch, MagicMock
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Ensure the project root is in sys.path to resolve imports correctly
|
||||
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
if project_root not in sys.path:
|
||||
sys.path.append(project_root)
|
||||
# Import the class to be tested
|
||||
from gemini_cli_adapter import GeminiCliAdapter
|
||||
import subprocess
|
||||
from unittest.mock import patch, MagicMock
|
||||
from src.gemini_cli_adapter import GeminiCliAdapter
|
||||
|
||||
class TestGeminiCliAdapterParity(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
"""Set up a fresh adapter instance and reset session state for each test."""
|
||||
self.adapter = GeminiCliAdapter(binary_path="gemini")
|
||||
|
||||
def setUp(self) -> None:
|
||||
"""Set up a fresh adapter instance and reset session state for each test."""
|
||||
# Patch session_logger to prevent file operations during tests
|
||||
self.session_logger_patcher = patch('gemini_cli_adapter.session_logger')
|
||||
self.mock_session_logger = self.session_logger_patcher.start()
|
||||
self.adapter = GeminiCliAdapter(binary_path="gemini")
|
||||
self.adapter.session_id = None
|
||||
self.adapter.last_usage = None
|
||||
self.adapter.last_latency = 0.0
|
||||
def tearDown(self) -> None:
|
||||
pass
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.session_logger_patcher.stop()
|
||||
def test_count_tokens_fallback(self) -> None:
|
||||
"""Test the character-based token estimation fallback."""
|
||||
contents = ["Hello", "world!"]
|
||||
estimated = self.adapter.count_tokens(contents)
|
||||
# "Hello\nworld!" is 12 chars. 12 // 4 = 3
|
||||
self.assertEqual(estimated, 3)
|
||||
|
||||
@patch('subprocess.Popen')
|
||||
def test_count_tokens_uses_estimation(self, mock_popen: MagicMock) -> None:
|
||||
"""
|
||||
Test that count_tokens uses character-based estimation.
|
||||
"""
|
||||
contents_to_count = ["This is the first line.", "This is the second line."]
|
||||
expected_chars = len("\n".join(contents_to_count))
|
||||
expected_tokens = expected_chars // 4
|
||||
token_count = self.adapter.count_tokens(contents=contents_to_count)
|
||||
self.assertEqual(token_count, expected_tokens)
|
||||
# Verify that NO subprocess was started for counting
|
||||
mock_popen.assert_not_called()
|
||||
@patch('subprocess.Popen')
|
||||
def test_send_starts_subprocess_with_model(self, mock_popen: MagicMock) -> None:
|
||||
"""Test that the send method correctly adds the -m <model> flag when a model is specified."""
|
||||
mock_process = MagicMock()
|
||||
mock_process.stdout = [b'{"kind": "message", "payload": "hi"}']
|
||||
mock_process.stderr = []
|
||||
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('subprocess.Popen')
|
||||
def test_send_with_safety_settings_no_flags_added(self, mock_popen: MagicMock) -> None:
|
||||
"""
|
||||
Test that the send method does NOT add --safety flags when safety_settings are provided,
|
||||
as this functionality is no longer supported via CLI flags.
|
||||
"""
|
||||
process_mock = MagicMock()
|
||||
jsonl_output = json.dumps({"type": "result", "usage": {}}) + "\n"
|
||||
process_mock.communicate.return_value = (jsonl_output, "")
|
||||
mock_popen.return_value = process_mock
|
||||
message_content = "User's prompt here."
|
||||
safety_settings = [
|
||||
{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_ONLY_HIGH"},
|
||||
{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"}
|
||||
]
|
||||
self.adapter.send(message=message_content, safety_settings=safety_settings)
|
||||
args, kwargs = mock_popen.call_args
|
||||
cmd_list = args[0]
|
||||
# Verify that no --safety flags were added to the command
|
||||
for part in cmd_list:
|
||||
self.assertNotIn("--safety", part)
|
||||
|
||||
# Verify that the message was passed correctly via communicate
|
||||
process_mock.communicate.assert_called_with(input=message_content)
|
||||
|
||||
@patch('subprocess.Popen')
|
||||
def test_send_without_safety_settings_no_flags(self, mock_popen: MagicMock) -> None:
|
||||
"""
|
||||
Test that when safety_settings is None or an empty list, no --safety flags are added.
|
||||
"""
|
||||
process_mock = MagicMock()
|
||||
jsonl_output = json.dumps({"type": "result", "usage": {}}) + "\n"
|
||||
process_mock.communicate.return_value = (jsonl_output, "")
|
||||
mock_popen.return_value = process_mock
|
||||
message_content = "Another prompt."
|
||||
self.adapter.send(message=message_content, safety_settings=None)
|
||||
args_none, _ = mock_popen.call_args
|
||||
for part in args_none[0]:
|
||||
self.assertNotIn("--safety", part)
|
||||
|
||||
mock_popen.reset_mock()
|
||||
self.adapter.send(message=message_content, safety_settings=[])
|
||||
args_empty, _ = mock_popen.call_args
|
||||
for part in args_empty[0]:
|
||||
self.assertNotIn("--safety", part)
|
||||
|
||||
@patch('subprocess.Popen')
|
||||
def test_send_with_system_instruction_prepended_to_stdin(self, mock_popen: MagicMock) -> None:
|
||||
"""
|
||||
Test that the send method prepends the system instruction to the prompt
|
||||
sent via stdin, and does NOT add a --system flag to the command.
|
||||
"""
|
||||
process_mock = MagicMock()
|
||||
jsonl_output = json.dumps({"type": "result", "usage": {}}) + "\n"
|
||||
process_mock.communicate.return_value = (jsonl_output, "")
|
||||
mock_popen.return_value = process_mock
|
||||
message_content = "User's prompt here."
|
||||
system_instruction_text = "Some instruction"
|
||||
expected_input = f"{system_instruction_text}\n\n{message_content}"
|
||||
self.adapter.send(message=message_content, system_instruction=system_instruction_text)
|
||||
args, kwargs = mock_popen.call_args
|
||||
cmd_list = args[0]
|
||||
# Verify that the system instruction was prepended to the input sent to communicate
|
||||
process_mock.communicate.assert_called_with(input=expected_input)
|
||||
# Verify that no --system flag was added to the command
|
||||
for part in cmd_list:
|
||||
self.assertNotIn("--system", part)
|
||||
|
||||
@patch('subprocess.Popen')
|
||||
def test_send_with_model_parameter(self, mock_popen: MagicMock) -> None:
|
||||
"""
|
||||
Test that the send method correctly adds the -m <model> flag when a model is specified.
|
||||
"""
|
||||
process_mock = MagicMock()
|
||||
jsonl_output = json.dumps({"type": "result", "usage": {}}) + "\n"
|
||||
process_mock.communicate.return_value = (jsonl_output, "")
|
||||
mock_popen.return_value = process_mock
|
||||
message_content = "User's prompt here."
|
||||
model_name = "gemini-1.5-flash"
|
||||
self.adapter.send(message=message_content, model=model_name)
|
||||
args, kwargs = mock_popen.call_args
|
||||
cmd_list = args[0]
|
||||
# Verify that the -m <model> flag was added to the command
|
||||
self.assertIn("-m", cmd_list)
|
||||
self.assertIn(model_name, cmd_list)
|
||||
# Verify that the message was passed correctly via communicate
|
||||
process_mock.communicate.assert_called_with(input=message_content)
|
||||
|
||||
@patch('subprocess.Popen')
|
||||
def test_send_parses_tool_calls_from_streaming_json(self, mock_popen: MagicMock) -> None:
|
||||
"""
|
||||
Test that tool_use messages in the streaming JSON are correctly parsed.
|
||||
"""
|
||||
process_mock = MagicMock()
|
||||
mock_stdout_content = (
|
||||
json.dumps({"type": "init", "session_id": "session-123"}) + "\n" +
|
||||
json.dumps({"type": "chunk", "text": "I will call a tool. "}) + "\n" +
|
||||
json.dumps({"type": "tool_use", "name": "get_weather", "args": {"location": "London"}, "id": "call-456"}) + "\n" +
|
||||
json.dumps({"type": "result", "usage": {"total_tokens": 100}}) + "\n"
|
||||
)
|
||||
process_mock.communicate.return_value = (mock_stdout_content, "")
|
||||
mock_popen.return_value = process_mock
|
||||
|
||||
result = self.adapter.send(message="What is the weather?")
|
||||
|
||||
self.assertEqual(result["text"], "I will call a tool. ")
|
||||
self.assertEqual(len(result["tool_calls"]), 1)
|
||||
self.assertEqual(result["tool_calls"][0]["name"], "get_weather")
|
||||
self.assertEqual(result["tool_calls"][0]["args"], {"location": "London"})
|
||||
self.assertEqual(self.adapter.session_id, "session-123")
|
||||
self.assertEqual(self.adapter.last_usage, {"total_tokens": 100})
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@patch('subprocess.Popen')
|
||||
def test_send_parses_tool_calls_from_streaming_json(self, mock_popen: MagicMock) -> None:
|
||||
"""Test that tool_use messages in the streaming JSON are correctly parsed."""
|
||||
tool_call_json = {
|
||||
"kind": "tool_use",
|
||||
"payload": {
|
||||
"id": "call_abc",
|
||||
"name": "list_directory",
|
||||
"input": {"path": "."}
|
||||
}
|
||||
}
|
||||
|
||||
mock_process = MagicMock()
|
||||
mock_process.stdout = [
|
||||
(json.dumps(tool_call_json) + "\n").encode('utf-8'),
|
||||
b'{"kind": "message", "payload": "I listed the files."}'
|
||||
]
|
||||
mock_process.stderr = []
|
||||
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.")
|
||||
|
||||
Reference in New Issue
Block a user