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>
121 lines
3.4 KiB
Python
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",
|
|
}
|