- src/glas_harness/contract.py: Rein ABC (start_session/dispatch_tool/
end_session), SandboxHandle/ToolCall/ToolResult, per docs/harness-contract.md.
- reins/rein_aharness.py: adapter around the rein-aharness CLI. Collapses
a whole `agent-harness run` into one dispatch_tool call for now (no
per-tool hooks yet — tracked in rein-aharness HARNESS-WP-0002-T03);
verifies success via new-commit detection, mirroring rein-aharness's
own success signal.
- gateway.py + cli.py: resolves a sand-boxer sandbox, runs one task
through a rein, tears the sandbox down.
- registry/reins/*.yaml (rein-aharness implemented, rein-openweights
planned) and profiles/harness.agent-dev{,-local}.yaml, pairing with
sand-boxer's profile.agent-dev and the new profile.bwrap-local.
Tested against a real local git repo + mocked SandboxManager/CLI
subprocess (10 tests, all passing). A real live run against the actual
Claude Code CLI is deliberately left for a human-triggered follow-up —
not executed autonomously since it spends real API credits/credentials.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
103 lines
3.8 KiB
Python
103 lines
3.8 KiB
Python
import json
|
|
import subprocess
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from glas_harness.contract import Rein, SandboxHandle, ToolCall
|
|
from glas_harness.reins.rein_aharness import ReinAharness, ReinAharnessNotInstalled
|
|
|
|
|
|
def test_is_rein_subclass() -> None:
|
|
assert issubclass(ReinAharness, Rein)
|
|
|
|
|
|
def test_bin_raises_if_not_installed() -> None:
|
|
rein = ReinAharness(cli_bin="does-not-exist-binary")
|
|
with pytest.raises(ReinAharnessNotInstalled):
|
|
rein._bin()
|
|
|
|
|
|
def test_start_session_writes_task_file_and_captures_head(tmp_path) -> None:
|
|
repo = tmp_path / "repo"
|
|
repo.mkdir()
|
|
subprocess.run(["git", "init", "-q"], cwd=repo, check=True)
|
|
subprocess.run(["git", "commit", "-q", "--allow-empty", "-m", "init"], cwd=repo, check=True)
|
|
|
|
rein = ReinAharness()
|
|
sandbox = SandboxHandle(sandbox_id="abc", host="localhost", reachability={"workspace_dir": str(repo)})
|
|
session = rein.start_session(
|
|
profile={"id": "harness.agent-dev-local"},
|
|
inputs={"title": "t", "description": "d"},
|
|
sandbox=sandbox,
|
|
)
|
|
|
|
assert session["target_repo"] == str(repo)
|
|
assert session["head_before"]
|
|
task_spec = json.loads(open(session["task_file"]).read())
|
|
assert task_spec["title"] == "t"
|
|
assert task_spec["target_repo"] == str(repo)
|
|
|
|
|
|
def test_start_session_requires_resolvable_target_repo() -> None:
|
|
rein = ReinAharness()
|
|
sandbox = SandboxHandle(sandbox_id="abc", host="localhost", reachability={})
|
|
with pytest.raises(ValueError, match="target_repo"):
|
|
rein.start_session(profile={}, inputs={"title": "t", "description": "d"}, sandbox=sandbox)
|
|
|
|
|
|
def test_dispatch_tool_invokes_agent_harness_cli() -> None:
|
|
rein = ReinAharness()
|
|
session = {"task_file": "/tmp/task.json"}
|
|
|
|
fake_result = MagicMock(returncode=0, stdout="ok", stderr="")
|
|
with patch.object(rein, "_bin", return_value="/usr/bin/agent-harness"):
|
|
with patch("glas_harness.reins.rein_aharness.subprocess.run", return_value=fake_result) as run:
|
|
result = rein.dispatch_tool(session, ToolCall(name="run_task"))
|
|
|
|
argv = run.call_args.args[0]
|
|
assert argv[:3] == ["/usr/bin/agent-harness", "run", "--task-file"]
|
|
assert result.ok is True
|
|
assert result.output == "ok"
|
|
|
|
|
|
def test_dispatch_tool_reports_failure() -> None:
|
|
rein = ReinAharness()
|
|
session = {"task_file": "/tmp/task.json"}
|
|
|
|
fake_result = MagicMock(returncode=1, stdout="", stderr="boom")
|
|
with patch.object(rein, "_bin", return_value="/usr/bin/agent-harness"):
|
|
with patch("glas_harness.reins.rein_aharness.subprocess.run", return_value=fake_result):
|
|
result = rein.dispatch_tool(session, ToolCall(name="run_task"))
|
|
|
|
assert result.ok is False
|
|
assert result.error == "boom"
|
|
|
|
|
|
def test_end_session_detects_new_commit(tmp_path) -> None:
|
|
repo = tmp_path / "repo"
|
|
repo.mkdir()
|
|
subprocess.run(["git", "init", "-q"], cwd=repo, check=True)
|
|
subprocess.run(["git", "commit", "-q", "--allow-empty", "-m", "init"], cwd=repo, check=True)
|
|
|
|
rein = ReinAharness()
|
|
head_before = rein._git_head(str(repo))
|
|
|
|
subprocess.run(["git", "commit", "-q", "--allow-empty", "-m", "task"], cwd=repo, check=True)
|
|
|
|
summary = rein.end_session({"target_repo": str(repo), "head_before": head_before})
|
|
assert summary["committed"] == "True"
|
|
assert summary["commit_sha"] != head_before
|
|
|
|
|
|
def test_end_session_no_new_commit(tmp_path) -> None:
|
|
repo = tmp_path / "repo"
|
|
repo.mkdir()
|
|
subprocess.run(["git", "init", "-q"], cwd=repo, check=True)
|
|
subprocess.run(["git", "commit", "-q", "--allow-empty", "-m", "init"], cwd=repo, check=True)
|
|
|
|
rein = ReinAharness()
|
|
head = rein._git_head(str(repo))
|
|
|
|
summary = rein.end_session({"target_repo": str(repo), "head_before": head})
|
|
assert summary["committed"] == "False"
|