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()