rein-openweights/tests/test_tools.py
tegwick 4ee612140b Bootstrap rein-openweights: minimal agentic loop over OpenRouter (REIN-OW-WP-0001)
Implements T01-T04:
- tools.py: green-commit-only-equivalent tool surface (read/write/edit/
  glob/grep + git status/diff/log/add+commit), path-traversal guarded.
- openrouter_client.py: own minimal chat-completions client — llm-connect's
  OpenRouterAdapter takes a single prompt string and never surfaces
  tool_calls, so it can't drive a multi-turn tool-calling loop without a
  breaking change to its frozen Core ABC. llm-connect stays an optional
  dependency (pyproject.toml), not load-bearing.
- loop.py: plan -> tool call -> observe -> repeat, budget- and
  turn-bounded, tool errors reported back to the model instead of
  crashing the loop.
- credentials.py: own OpenBao AppRole/ambient-token acquisition, per
  glas-harness ADR-002 (Option B) — glas-harness does not broker this.
- runner.py/hub.py: commit-verified success criterion + State Hub
  progress/token reporting, mirroring rein-aharness's model.

26 tests, all mocked at the httpx/subprocess boundary — no real
OpenRouter or OpenBao calls made. T05 (Forgejo repo creation) stays
open, deferred to the operator.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 13:37:09 +02:00

121 lines
3.4 KiB
Python

import subprocess
import pytest
from rein_openweights.tools import (
ToolPolicyError,
call_tool,
edit_file,
git_add_commit,
git_diff,
git_log,
git_status,
glob_files,
grep_files,
openai_tool_schemas,
read_file,
write_file,
)
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 test_write_then_read_file(tmp_path):
repo = _init_repo(tmp_path)
write_file(repo, "a.txt", "hello")
assert read_file(repo, "a.txt") == "hello"
def test_edit_file_replaces_first_occurrence(tmp_path):
repo = _init_repo(tmp_path)
write_file(repo, "a.txt", "foo foo")
edit_file(repo, "a.txt", "foo", "bar")
assert read_file(repo, "a.txt") == "bar foo"
def test_edit_file_missing_old_raises(tmp_path):
repo = _init_repo(tmp_path)
write_file(repo, "a.txt", "hello")
with pytest.raises(ToolPolicyError, match="old string not found"):
edit_file(repo, "a.txt", "nope", "x")
def test_path_traversal_is_blocked(tmp_path):
repo = _init_repo(tmp_path)
with pytest.raises(ToolPolicyError, match="escapes repo root"):
read_file(repo, "../outside.txt")
def test_glob_files(tmp_path):
repo = _init_repo(tmp_path)
write_file(repo, "src/a.py", "1")
write_file(repo, "src/b.py", "2")
write_file(repo, "src/c.txt", "3")
result = glob_files(repo, "**/*.py")
assert "src/a.py" in result
assert "src/b.py" in result
assert "src/c.txt" not in result
def test_grep_files(tmp_path):
repo = _init_repo(tmp_path)
write_file(repo, "a.txt", "hello world\nneedle here\n")
result = grep_files(repo, "needle")
assert "a.txt:2:needle here" in result
def test_grep_files_no_matches(tmp_path):
repo = _init_repo(tmp_path)
write_file(repo, "a.txt", "hello")
assert grep_files(repo, "zzz") == "(no matches)"
def test_git_status_diff_log_and_commit(tmp_path):
repo = _init_repo(tmp_path)
write_file(repo, "a.txt", "hello")
assert "a.txt" in git_status(repo)
git_add_commit(repo, "add a.txt")
assert git_status(repo) == "(clean)"
assert "add a.txt" in git_log(repo)
def test_git_diff_after_edit(tmp_path):
repo = _init_repo(tmp_path)
write_file(repo, "a.txt", "hello")
git_add_commit(repo, "add a.txt")
edit_file(repo, "a.txt", "hello", "goodbye")
assert "goodbye" in git_diff(repo)
def test_call_tool_dispatches_by_name(tmp_path):
repo = _init_repo(tmp_path)
call_tool(repo, "write_file", {"path": "a.txt", "content": "x"})
assert read_file(repo, "a.txt") == "x"
def test_call_tool_unknown_name_raises(tmp_path):
repo = _init_repo(tmp_path)
with pytest.raises(ToolPolicyError, match="unknown tool"):
call_tool(repo, "delete_everything", {})
def test_openai_tool_schemas_cover_all_tools():
schemas = openai_tool_schemas()
names = {s["function"]["name"] for s in schemas}
assert names == {
"read_file",
"write_file",
"edit_file",
"glob_files",
"grep_files",
"git_status",
"git_diff",
"git_log",
"git_add_commit",
}