119 lines
3.6 KiB
Python
119 lines
3.6 KiB
Python
|
|
import subprocess
|
||
|
|
from unittest.mock import MagicMock
|
||
|
|
|
||
|
|
from rein_openweights.budget import BudgetTracker
|
||
|
|
from rein_openweights.loop import run_loop
|
||
|
|
|
||
|
|
|
||
|
|
def _init_repo(tmp_path):
|
||
|
|
subprocess.run(["git", "init", "-q"], cwd=tmp_path, check=True)
|
||
|
|
subprocess.run(["git", "config", "user.email", "t@example.com"], cwd=tmp_path, check=True)
|
||
|
|
subprocess.run(["git", "config", "user.name", "t"], cwd=tmp_path, check=True)
|
||
|
|
subprocess.run(["git", "commit", "-q", "--allow-empty", "-m", "init"], cwd=tmp_path, check=True)
|
||
|
|
return tmp_path
|
||
|
|
|
||
|
|
|
||
|
|
def _response(message, total_tokens=100):
|
||
|
|
return {
|
||
|
|
"choices": [{"message": message}],
|
||
|
|
"usage": {"total_tokens": total_tokens},
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def test_run_loop_executes_tool_call_then_finishes(tmp_path):
|
||
|
|
repo = _init_repo(tmp_path)
|
||
|
|
client = MagicMock()
|
||
|
|
client.chat.side_effect = [
|
||
|
|
_response(
|
||
|
|
{
|
||
|
|
"role": "assistant",
|
||
|
|
"content": None,
|
||
|
|
"tool_calls": [
|
||
|
|
{
|
||
|
|
"id": "call_1",
|
||
|
|
"function": {
|
||
|
|
"name": "write_file",
|
||
|
|
"arguments": '{"path": "NOTES.md", "content": "hi"}',
|
||
|
|
},
|
||
|
|
}
|
||
|
|
],
|
||
|
|
}
|
||
|
|
),
|
||
|
|
_response(
|
||
|
|
{
|
||
|
|
"role": "assistant",
|
||
|
|
"tool_calls": [
|
||
|
|
{
|
||
|
|
"id": "call_2",
|
||
|
|
"function": {
|
||
|
|
"name": "git_add_commit",
|
||
|
|
"arguments": '{"message": "add notes"}',
|
||
|
|
},
|
||
|
|
}
|
||
|
|
],
|
||
|
|
}
|
||
|
|
),
|
||
|
|
_response({"role": "assistant", "content": "All set. DONE"}),
|
||
|
|
]
|
||
|
|
|
||
|
|
result = run_loop(client, repo, "t", "d", budget=BudgetTracker(total=10_000))
|
||
|
|
|
||
|
|
assert result.finished is True
|
||
|
|
assert result.turns == 3
|
||
|
|
assert (repo / "NOTES.md").read_text() == "hi"
|
||
|
|
log = subprocess.run(
|
||
|
|
["git", "-C", str(repo), "log", "--oneline"], capture_output=True, text=True
|
||
|
|
).stdout
|
||
|
|
assert "add notes" in log
|
||
|
|
|
||
|
|
|
||
|
|
def test_run_loop_stops_when_budget_exceeded(tmp_path):
|
||
|
|
repo = _init_repo(tmp_path)
|
||
|
|
client = MagicMock()
|
||
|
|
client.chat.return_value = _response({"role": "assistant", "content": "still going"}, total_tokens=100)
|
||
|
|
|
||
|
|
result = run_loop(client, repo, "t", "d", budget=BudgetTracker(total=50))
|
||
|
|
|
||
|
|
assert result.error is not None
|
||
|
|
assert "budget" in result.error.lower()
|
||
|
|
|
||
|
|
|
||
|
|
def test_run_loop_stops_at_max_turns(tmp_path):
|
||
|
|
repo = _init_repo(tmp_path)
|
||
|
|
client = MagicMock()
|
||
|
|
client.chat.return_value = _response(
|
||
|
|
{
|
||
|
|
"role": "assistant",
|
||
|
|
"tool_calls": [
|
||
|
|
{"id": "c", "function": {"name": "git_status", "arguments": "{}"}}
|
||
|
|
],
|
||
|
|
}
|
||
|
|
)
|
||
|
|
|
||
|
|
result = run_loop(client, repo, "t", "d", max_turns=2)
|
||
|
|
|
||
|
|
assert result.turns == 2
|
||
|
|
assert result.error == "max_turns exceeded"
|
||
|
|
|
||
|
|
|
||
|
|
def test_run_loop_reports_tool_error_without_crashing(tmp_path):
|
||
|
|
repo = _init_repo(tmp_path)
|
||
|
|
client = MagicMock()
|
||
|
|
client.chat.side_effect = [
|
||
|
|
_response(
|
||
|
|
{
|
||
|
|
"role": "assistant",
|
||
|
|
"tool_calls": [
|
||
|
|
{"id": "c", "function": {"name": "read_file", "arguments": '{"path": "missing.txt"}'}}
|
||
|
|
],
|
||
|
|
}
|
||
|
|
),
|
||
|
|
_response({"role": "assistant", "content": "done anyway DONE"}),
|
||
|
|
]
|
||
|
|
|
||
|
|
result = run_loop(client, repo, "t", "d")
|
||
|
|
|
||
|
|
assert result.finished is True
|
||
|
|
tool_messages = [m for m in result.transcript if m.get("role") == "tool"]
|
||
|
|
assert "error" in tool_messages[0]["content"]
|