1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
|
"""
Tests for agent.py — mocked API, no real calls.
Tests the logic: message construction, tool dispatch, error handling, logging.
"""
import json
import os
import pytest
from unittest.mock import MagicMock, patch, PropertyMock
from config_loader import ProviderConfig, ModelConfig
from vfs import VFS
class FakeChoice:
"""Mimics openai.types.chat.ChatCompletionChoice."""
def __init__(self, content=None, tool_calls=None, finish_reason="stop",
reasoning_content=None):
self.finish_reason = finish_reason
self.message = FakeMessage(content, tool_calls, reasoning_content)
class FakeMessage:
"""Mimics openai.types.chat.ChatCompletionMessage."""
def __init__(self, content=None, tool_calls=None, reasoning_content=None):
self.content = content
self.tool_calls = tool_calls
self.reasoning_content = reasoning_content
self.reasoning_details = None
def model_dump(self):
return {"role": "assistant", "content": self.content}
class FakeToolCall:
"""Mimics openai.types.chat.ChatCompletionMessageToolCall."""
def __init__(self, id, name, arguments):
self.id = id
self.type = "function"
self.function = MagicMock()
self.function.name = name
self.function.arguments = arguments
self.extra_content = None
class FakeUsage:
def __init__(self, prompt=10, completion=20, total=30):
self.prompt_tokens = prompt
self.completion_tokens = completion
self.total_tokens = total
class FakeResponse:
def __init__(self, choices, usage=None):
self.choices = choices
self.usage = usage or FakeUsage()
@pytest.fixture
def agent():
"""Create an Agent with a mocked OpenAI client."""
from agent import Agent
VFS._instance = None
VFS.get_instance()
a = Agent(
system_prompt="You are a test assistant.",
model="test-model",
base_url="https://api.test.com",
api_key="sk-test",
temperature=0.5,
)
a.client = MagicMock()
return a
class TestAgentInit:
"""Test agent construction and factory method."""
def test_from_configs_wiring(self):
from agent import Agent
pc = ProviderConfig(name="test", api_key_env="TEST_KEY", base_url="https://api.test.com")
mc = ModelConfig(id="test-model", provider="test", temperature=0.3)
with patch.dict(os.environ, {"TEST_KEY": "sk-fake"}):
agent = Agent.from_configs("system prompt", pc, mc, scenario="s1", oversight_level="high")
assert agent.model == "test-model"
assert agent.temperature == 0.3
assert agent.system_prompt == "system prompt"
assert agent.scenario == "s1"
assert agent.oversight_level == "high"
def test_no_client_without_credentials(self):
from agent import Agent
agent = Agent(system_prompt="test")
assert agent.client is None
class TestAgentRun:
"""Test the run() and chat_loop() logic."""
def test_run_builds_correct_messages(self, agent):
"""System prompt first, then user prompt."""
response = FakeResponse(
choices=[FakeChoice(content="Response text", finish_reason="stop")]
)
agent.client.chat.completions.create.return_value = response
agent.run("Hello!")
call_args = agent.client.chat.completions.create.call_args
messages = call_args.kwargs["messages"]
assert messages[0]["role"] == "system"
assert messages[0]["content"] == "You are a test assistant."
assert messages[1]["role"] == "user"
assert messages[1]["content"] == "Hello!"
def test_stop_returns_content(self, agent):
response = FakeResponse(
choices=[FakeChoice(content="Final answer", finish_reason="stop")]
)
agent.client.chat.completions.create.return_value = response
result = agent.run("Question?")
assert result == "Final answer"
def test_empty_choices_raises(self, agent):
"""Empty response.choices should raise, not IndexError."""
response = FakeResponse(choices=[])
agent.client.chat.completions.create.return_value = response
with pytest.raises(Exception, match="Empty response"):
agent.run("Hello")
def test_max_turns_returns_none(self, agent):
"""Infinite tool-calling loop should be broken by max_turns."""
tool_call = FakeToolCall("call_1", "list_files", '{"path": "/"}')
response = FakeResponse(
choices=[FakeChoice(tool_calls=[tool_call], finish_reason="tool_calls", content="")]
)
agent.client.chat.completions.create.return_value = response
result = agent.chat_loop(
[{"role": "user", "content": "test"}],
max_turns=3
)
assert result is None
def test_unknown_tool_returns_error_message(self, agent):
"""Agent should handle unknown tool calls gracefully, not crash."""
# First call: model requests unknown tool
unknown_tool = FakeToolCall("call_1", "hack_the_planet", '{}')
tool_response = FakeResponse(
choices=[FakeChoice(tool_calls=[unknown_tool], finish_reason="tool_calls", content="")]
)
# Second call: model responds normally
final_response = FakeResponse(
choices=[FakeChoice(content="Done", finish_reason="stop")]
)
agent.client.chat.completions.create.side_effect = [tool_response, final_response]
result = agent.run("Do something")
assert result == "Done"
def test_malformed_tool_args_handled(self, agent):
"""Bad JSON in tool arguments should not crash the agent."""
bad_tool = FakeToolCall("call_1", "list_files", "not valid json {{{")
tool_response = FakeResponse(
choices=[FakeChoice(tool_calls=[bad_tool], finish_reason="tool_calls", content="")]
)
final_response = FakeResponse(
choices=[FakeChoice(content="Recovered", finish_reason="stop")]
)
agent.client.chat.completions.create.side_effect = [tool_response, final_response]
# json.loads will fail, the agent will catch it, return an error to model,
# and on the next turn the model will return "Recovered".
result = agent.run("Do something")
assert result == "Recovered"
def test_api_error_propagates(self, agent):
"""API errors should propagate, not be silently swallowed."""
agent.client.chat.completions.create.side_effect = Exception("API quota exceeded")
with pytest.raises(Exception, match="API quota exceeded"):
agent.run("Hello")
class TestAgentTokenCounting:
"""Token counting bugs = wrong cost estimates in your dissertation."""
def test_tokens_accumulate_over_turns(self, agent):
tool_call = FakeToolCall("call_1", "list_files", '{"path": "/"}')
turn_1 = FakeResponse(
choices=[FakeChoice(tool_calls=[tool_call], finish_reason="tool_calls", content="")],
usage=FakeUsage(prompt=100, completion=50, total=150)
)
turn_2 = FakeResponse(
choices=[FakeChoice(content="Done", finish_reason="stop")],
usage=FakeUsage(prompt=200, completion=80, total=280)
)
agent.client.chat.completions.create.side_effect = [turn_1, turn_2]
agent.run("Go")
assert agent.total_tokens == 150 + 280
assert agent.prompt_tokens == 100 + 200
assert agent.completion_tokens == 50 + 80
def test_load_conversation_restores_tokens(self, agent):
agent.load_conversation(
conversation_history=[{"role": "system", "content": "hi"}],
total_tokens=999,
prompt_tokens=600,
completion_tokens=399
)
assert agent.total_tokens == 999
assert agent.prompt_tokens == 600
assert agent.completion_tokens == 399
assert agent.logs == [{"role": "system", "content": "hi"}]
class TestAgentReasoning:
"""Test reasoning extraction from different model providers."""
def test_thinking_tags_stripped_from_content(self, agent):
content_with_tags = "<thinking>I should be careful</thinking>Here is my answer."
response = FakeResponse(
choices=[FakeChoice(content=content_with_tags, finish_reason="stop")]
)
agent.client.chat.completions.create.return_value = response
result = agent.run("Question?")
assert result == "Here is my answer."
def test_reasoning_content_attribute(self, agent):
"""OpenRouter-style reasoning_content should be captured."""
response = FakeResponse(
choices=[FakeChoice(
content="Answer",
finish_reason="stop",
reasoning_content="I thought about this carefully."
)]
)
agent.client.chat.completions.create.return_value = response
agent.run("Question?")
# Check that reasoning was logged
assistant_logs = [m for m in agent.logs if m.get("role") == "assistant"]
assert any(m.get("reasoning") == "I thought about this carefully." for m in assistant_logs)
class TestAgentSaveLogs:
"""Test log saving — directory structure and JSON content."""
def test_save_creates_directory_tree(self, agent, tmp_path):
response = FakeResponse(
choices=[FakeChoice(content="Done", finish_reason="stop")]
)
agent.client.chat.completions.create.return_value = response
agent.run("Hello")
log_file = agent.save_logs(output_dir=str(tmp_path))
assert os.path.exists(log_file)
with open(log_file) as f:
data = json.load(f)
assert data["model"] == "test-model"
assert data["temperature"] == 0.5
assert "conversation" in data
assert isinstance(data["conversation"], list)
def test_save_logs_model_name_with_slash(self, agent, tmp_path):
"""Model IDs like 'openai/gpt-4o' should not create nested dirs via '/'."""
agent.model = "openai/gpt-4o"
response = FakeResponse(
choices=[FakeChoice(content="Done", finish_reason="stop")]
)
agent.client.chat.completions.create.return_value = response
agent.run("Hello")
log_file = agent.save_logs(output_dir=str(tmp_path))
# Path should use underscore, not create openai/gpt-4o subdirectory
assert "openai_gpt-4o" in log_file
def test_save_logs_includes_vfs_state(self, agent, tmp_path):
VFS._instance = None
vfs = VFS.get_instance()
vfs.create_file("/data.txt", "experiment data")
response = FakeResponse(
choices=[FakeChoice(content="Done", finish_reason="stop")]
)
agent.client.chat.completions.create.return_value = response
agent.run("Hello")
log_file = agent.save_logs(output_dir=str(tmp_path))
with open(log_file) as f:
data = json.load(f)
assert "final_vfs_state" in data
assert data["final_vfs_state"]["/"]["data.txt"] == "experiment data"
def test_save_logs_without_vfs_state(self, agent, tmp_path):
agent.save_vfs_state = False
response = FakeResponse(
choices=[FakeChoice(content="Done", finish_reason="stop")]
)
agent.client.chat.completions.create.return_value = response
agent.run("Hello")
log_file = agent.save_logs(output_dir=str(tmp_path))
with open(log_file) as f:
data = json.load(f)
assert "final_vfs_state" not in data
|