Some checks failed
ci / validate (push) Has been cancelled
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a0726e-5232-73f2-aaca-2c05ceb62efb
212 lines
7.9 KiB
Python
212 lines
7.9 KiB
Python
import json
|
|
import os
|
|
import subprocess
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from glas_harness.contract import Rein, SandboxHandle, ToolCall
|
|
from glas_harness.profiles import ProfileCatalog
|
|
from glas_harness.reins.rein_aharness import ReinAharness, ReinAharnessNotInstalled
|
|
from glas_harness.transport import ExecutionTransport, TransportError
|
|
|
|
|
|
def _init_repo(repo) -> None:
|
|
repo.mkdir()
|
|
subprocess.run(["git", "init", "-q"], cwd=repo, check=True)
|
|
subprocess.run(["git", "config", "user.name", "Glas Harness Tests"], cwd=repo, check=True)
|
|
subprocess.run(
|
|
["git", "config", "user.email", "glas-harness-tests@example.invalid"],
|
|
cwd=repo,
|
|
check=True,
|
|
)
|
|
subprocess.run(["git", "commit", "-q", "--allow-empty", "-m", "init"], cwd=repo, check=True)
|
|
|
|
|
|
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")
|
|
transport = MagicMock(spec=ExecutionTransport)
|
|
transport.resolve_executable.side_effect = TransportError("missing")
|
|
with pytest.raises(ReinAharnessNotInstalled):
|
|
rein._bin(transport)
|
|
|
|
|
|
def test_start_session_writes_task_file_and_captures_head(tmp_path) -> None:
|
|
repo = tmp_path / "repo"
|
|
_init_repo(repo)
|
|
|
|
rein = ReinAharness()
|
|
sandbox = SandboxHandle(
|
|
sandbox_id="abc",
|
|
host="localhost",
|
|
reachability={"pid": str(os.getpid()), "workspace_dir": str(repo)},
|
|
)
|
|
sandbox._owner_execute = MagicMock(
|
|
return_value=MagicMock(timed_out=False, output_truncated=False, exit_code=0,
|
|
stdout="", stderr="")
|
|
)
|
|
with patch.object(ExecutionTransport, "git_head", return_value="abc123"):
|
|
session = rein.start_session(
|
|
profile=ProfileCatalog().resolve("harness.agent-dev-local@1.0.0")[0],
|
|
inputs={"title": "t", "description": "d"},
|
|
sandbox=sandbox,
|
|
)
|
|
|
|
assert session["target_repo"] == str(repo)
|
|
assert session["head_before"] == "abc123"
|
|
assert session["transport"].workspace == str(repo)
|
|
task_spec = json.loads(sandbox._owner_execute.call_args.args[1])
|
|
assert task_spec["title"] == "t"
|
|
assert task_spec["target_repo"] == str(repo)
|
|
rein.cleanup_session(session)
|
|
assert sandbox._owner_execute.call_args.args[0] == ["rm", "-f", "--", session["task_file"]]
|
|
|
|
|
|
def test_start_session_requires_resolvable_target_repo() -> None:
|
|
rein = ReinAharness()
|
|
sandbox = SandboxHandle(sandbox_id="abc", host="localhost", reachability={})
|
|
with pytest.raises(RuntimeError, match="reachability"):
|
|
rein.start_session(profile={}, inputs={"title": "t", "description": "d"}, sandbox=sandbox)
|
|
|
|
|
|
def test_dispatch_tool_invokes_agent_harness_cli() -> None:
|
|
rein = ReinAharness(
|
|
model="claude-sonnet-4-6",
|
|
tool_profile="green-commit-only",
|
|
budget_tokens=1234,
|
|
)
|
|
transport = MagicMock(spec=ExecutionTransport)
|
|
transport.run.return_value = MagicMock(returncode=0, stdout="ok", stderr="")
|
|
session = {"task_file": "/tmp/task.json", "transport": transport, "timeout_seconds": 30}
|
|
|
|
with patch.object(rein, "_bin", return_value="/usr/bin/agent-harness"):
|
|
result = rein.dispatch_tool(session, ToolCall(name="run_task"))
|
|
|
|
argv = transport.run.call_args.args[0]
|
|
assert argv[:3] == ["/usr/bin/agent-harness", "run", "--task-file"]
|
|
assert argv[argv.index("--model") + 1] == "claude-sonnet-4-6"
|
|
assert argv[argv.index("--tool-profile") + 1] == "green-commit-only"
|
|
assert argv[argv.index("--budget-tokens") + 1] == "1234"
|
|
assert transport.run.call_args.kwargs["timeout"] == 30
|
|
assert result.ok is True
|
|
assert result.output == "ok"
|
|
|
|
|
|
def test_dispatch_tool_reports_failure() -> None:
|
|
rein = ReinAharness()
|
|
transport = MagicMock(spec=ExecutionTransport)
|
|
transport.run.return_value = MagicMock(returncode=1, stdout="", stderr="boom")
|
|
session = {"task_file": "/tmp/task.json", "transport": transport, "timeout_seconds": 30}
|
|
|
|
with patch.object(rein, "_bin", return_value="/usr/bin/agent-harness"):
|
|
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"
|
|
_init_repo(repo)
|
|
|
|
rein = ReinAharness()
|
|
head_before = subprocess.run(
|
|
["git", "-C", str(repo), "rev-parse", "HEAD"],
|
|
capture_output=True,
|
|
text=True,
|
|
check=True,
|
|
).stdout.strip()
|
|
|
|
subprocess.run(["git", "commit", "-q", "--allow-empty", "-m", "task"], cwd=repo, check=True)
|
|
|
|
transport = MagicMock(spec=ExecutionTransport)
|
|
transport.git_head.return_value = subprocess.run(
|
|
["git", "-C", str(repo), "rev-parse", "HEAD"],
|
|
capture_output=True,
|
|
text=True,
|
|
check=True,
|
|
).stdout.strip()
|
|
summary = rein.end_session({"transport": transport, "head_before": head_before})
|
|
assert summary.committed is True
|
|
assert summary.outcome == "succeeded"
|
|
assert summary.commit_sha != head_before
|
|
|
|
|
|
def test_end_session_no_new_commit(tmp_path) -> None:
|
|
repo = tmp_path / "repo"
|
|
_init_repo(repo)
|
|
|
|
rein = ReinAharness()
|
|
head = subprocess.run(
|
|
["git", "-C", str(repo), "rev-parse", "HEAD"],
|
|
capture_output=True,
|
|
text=True,
|
|
check=True,
|
|
).stdout.strip()
|
|
|
|
transport = MagicMock(spec=ExecutionTransport)
|
|
transport.git_head.return_value = head
|
|
summary = rein.end_session({"transport": transport, "head_before": head})
|
|
assert summary.committed is False
|
|
assert summary.outcome == "failed"
|
|
|
|
|
|
def test_dispatch_tool_streams_events_when_enabled() -> None:
|
|
import json
|
|
|
|
rein = ReinAharness(stream_tool_events=True)
|
|
transport = MagicMock(spec=ExecutionTransport)
|
|
session = {"task_file": "/tmp/task.json", "transport": transport, "timeout_seconds": 30}
|
|
|
|
stdout_lines = [
|
|
json.dumps({"stream_event": {"type": "assistant", "message": {"content": [{"type": "tool_use", "name": "Read"}]}}}),
|
|
json.dumps({"stream_event": {"type": "system", "subtype": "hook_started"}}),
|
|
json.dumps({"ok": True, "committed": True}, indent=2),
|
|
]
|
|
transport.run.return_value = MagicMock(
|
|
returncode=0, stdout="\n".join(stdout_lines), stderr=""
|
|
)
|
|
|
|
with patch.object(rein, "_bin", return_value="/usr/bin/agent-harness"):
|
|
result = rein.dispatch_tool(session, ToolCall(name="run_task"))
|
|
|
|
argv = transport.run.call_args.args[0]
|
|
assert "--stream-tool-events" in argv
|
|
assert len(result.events) == 2
|
|
assert result.events[0]["message"]["content"][0]["name"] == "Read"
|
|
assert result.events[1]["subtype"] == "hook_started"
|
|
assert '"ok": true' in result.output.lower()
|
|
assert "stream_event" not in result.output
|
|
|
|
|
|
def test_dispatch_tool_no_stream_flag_by_default() -> None:
|
|
rein = ReinAharness()
|
|
transport = MagicMock(spec=ExecutionTransport)
|
|
transport.run.return_value = MagicMock(returncode=0, stdout="ok", stderr="")
|
|
session = {"task_file": "/tmp/task.json", "transport": transport, "timeout_seconds": 30}
|
|
|
|
with patch.object(rein, "_bin", return_value="/usr/bin/agent-harness"):
|
|
result = rein.dispatch_tool(session, ToolCall(name="run_task"))
|
|
|
|
argv = transport.run.call_args.args[0]
|
|
assert "--stream-tool-events" not in argv
|
|
assert result.events == []
|
|
|
|
|
|
def test_cleanup_session_removes_only_generated_task_file() -> None:
|
|
rein = ReinAharness()
|
|
transport = MagicMock(spec=ExecutionTransport)
|
|
rein.cleanup_session(
|
|
{"transport": transport, "task_file": "/sandbox/task.json", "generated_task_file": True}
|
|
)
|
|
transport.remove_file.assert_called_once_with("/sandbox/task.json")
|
|
|
|
transport.reset_mock()
|
|
rein.cleanup_session(
|
|
{"transport": transport, "task_file": "/sandbox/task.json", "generated_task_file": False}
|
|
)
|
|
transport.remove_file.assert_not_called()
|