This commit is contained in:
parent
1cd890d871
commit
f773b5c101
19 changed files with 865 additions and 181 deletions
|
|
@ -26,6 +26,7 @@ class _FakeRein(Rein):
|
|||
assert str(profile.ref) == PROFILE
|
||||
assert sandbox.sandbox_id == "sbx1"
|
||||
assert sandbox.reachability.get("workspace_dir") == "/tmp/ws"
|
||||
assert "target_repo" not in inputs
|
||||
return {"session": "s1"}
|
||||
|
||||
def dispatch_tool(self, session, tool_call: ToolCall) -> ToolResult:
|
||||
|
|
@ -50,6 +51,9 @@ class _FakeRein(Rein):
|
|||
resolved_model="claude-sonnet-4-6",
|
||||
)
|
||||
|
||||
def cleanup_session(self, session):
|
||||
self.calls.append("cleanup_session")
|
||||
|
||||
|
||||
def _fake_status(sandbox_id: str = "sbx1") -> SandboxStatus:
|
||||
now = datetime.now(UTC)
|
||||
|
|
@ -86,7 +90,12 @@ def test_run_execution_creates_and_destroys_sandbox() -> None:
|
|||
|
||||
result = run_execution(_request(), rein=rein, manager=manager)
|
||||
|
||||
assert rein.calls == ["start_session", "dispatch_tool", "end_session"]
|
||||
assert rein.calls == [
|
||||
"start_session",
|
||||
"dispatch_tool",
|
||||
"end_session",
|
||||
"cleanup_session",
|
||||
]
|
||||
manager.create.assert_called_once()
|
||||
manager.destroy.assert_called_once_with("sbx1")
|
||||
assert result.ok is True
|
||||
|
|
@ -106,13 +115,15 @@ def test_run_execution_normalizes_execution_failure_and_tears_down() -> None:
|
|||
def dispatch_tool(self, session, tool_call):
|
||||
raise RuntimeError("boom")
|
||||
|
||||
result = run_execution(_request(), rein=_FailingRein(), manager=manager)
|
||||
rein = _FailingRein()
|
||||
result = run_execution(_request(), rein=rein, manager=manager)
|
||||
|
||||
assert result.ok is False
|
||||
assert result.evidence.outcome == "failed"
|
||||
assert result.evidence.failure_stage == "execution"
|
||||
assert result.evidence.error == "execution failed; inspect direct caller error"
|
||||
assert result.tool_error == "boom"
|
||||
assert rein.calls == ["start_session", "cleanup_session"]
|
||||
manager.destroy.assert_called_once_with("sbx1")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import json
|
||||
import os
|
||||
import subprocess
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
|
|
@ -7,6 +8,7 @@ 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 test_is_rein_subclass() -> None:
|
||||
|
|
@ -15,8 +17,10 @@ def test_is_rein_subclass() -> None:
|
|||
|
||||
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()
|
||||
rein._bin(transport)
|
||||
|
||||
|
||||
def test_start_session_writes_task_file_and_captures_head(tmp_path) -> None:
|
||||
|
|
@ -26,24 +30,32 @@ def test_start_session_writes_task_file_and_captures_head(tmp_path) -> None:
|
|||
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=ProfileCatalog().resolve("harness.agent-dev-local@1.0.0")[0],
|
||||
inputs={"title": "t", "description": "d"},
|
||||
sandbox=sandbox,
|
||||
sandbox = SandboxHandle(
|
||||
sandbox_id="abc",
|
||||
host="localhost",
|
||||
reachability={"pid": str(os.getpid()), "workspace_dir": str(repo)},
|
||||
)
|
||||
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"]
|
||||
assert session["head_before"] == "abc123"
|
||||
assert session["transport"].workspace == str(repo)
|
||||
task_spec = json.loads(open(session["task_file"]).read())
|
||||
assert task_spec["title"] == "t"
|
||||
assert task_spec["target_repo"] == str(repo)
|
||||
rein.cleanup_session(session)
|
||||
assert not os.path.exists(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(ValueError, match="target_repo"):
|
||||
with pytest.raises(RuntimeError, match="reachability"):
|
||||
rein.start_session(profile={}, inputs={"title": "t", "description": "d"}, sandbox=sandbox)
|
||||
|
||||
|
||||
|
|
@ -53,30 +65,31 @@ def test_dispatch_tool_invokes_agent_harness_cli() -> None:
|
|||
tool_profile="green-commit-only",
|
||||
budget_tokens=1234,
|
||||
)
|
||||
session = {"task_file": "/tmp/task.json"}
|
||||
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}
|
||||
|
||||
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"))
|
||||
result = rein.dispatch_tool(session, ToolCall(name="run_task"))
|
||||
|
||||
argv = run.call_args.args[0]
|
||||
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()
|
||||
session = {"task_file": "/tmp/task.json"}
|
||||
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}
|
||||
|
||||
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"))
|
||||
result = rein.dispatch_tool(session, ToolCall(name="run_task"))
|
||||
|
||||
assert result.ok is False
|
||||
assert result.error == "boom"
|
||||
|
|
@ -88,14 +101,24 @@ def test_end_session_detects_new_commit(tmp_path) -> None:
|
|||
subprocess.run(["git", "init", "-q"], cwd=repo, check=True)
|
||||
subprocess.run(["git", "commit", "-q", "--allow-empty", "-m", "init"], cwd=repo, check=True)
|
||||
|
||||
from glas_harness.reins._shared import git_head
|
||||
|
||||
rein = ReinAharness()
|
||||
head_before = git_head(str(repo))
|
||||
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)
|
||||
|
||||
summary = rein.end_session({"target_repo": str(repo), "head_before": head_before})
|
||||
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
|
||||
|
|
@ -107,12 +130,17 @@ def test_end_session_no_new_commit(tmp_path) -> None:
|
|||
subprocess.run(["git", "init", "-q"], cwd=repo, check=True)
|
||||
subprocess.run(["git", "commit", "-q", "--allow-empty", "-m", "init"], cwd=repo, check=True)
|
||||
|
||||
from glas_harness.reins._shared import git_head
|
||||
|
||||
rein = ReinAharness()
|
||||
head = git_head(str(repo))
|
||||
head = subprocess.run(
|
||||
["git", "-C", str(repo), "rev-parse", "HEAD"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
).stdout.strip()
|
||||
|
||||
summary = rein.end_session({"target_repo": str(repo), "head_before": head})
|
||||
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"
|
||||
|
||||
|
|
@ -121,20 +149,22 @@ def test_dispatch_tool_streams_events_when_enabled() -> None:
|
|||
import json
|
||||
|
||||
rein = ReinAharness(stream_tool_events=True)
|
||||
session = {"task_file": "/tmp/task.json"}
|
||||
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),
|
||||
]
|
||||
fake_result = MagicMock(returncode=0, stdout="\n".join(stdout_lines), stderr="")
|
||||
transport.run.return_value = MagicMock(
|
||||
returncode=0, stdout="\n".join(stdout_lines), 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"))
|
||||
result = rein.dispatch_tool(session, ToolCall(name="run_task"))
|
||||
|
||||
argv = run.call_args.args[0]
|
||||
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"
|
||||
|
|
@ -145,13 +175,28 @@ def test_dispatch_tool_streams_events_when_enabled() -> None:
|
|||
|
||||
def test_dispatch_tool_no_stream_flag_by_default() -> None:
|
||||
rein = ReinAharness()
|
||||
session = {"task_file": "/tmp/task.json"}
|
||||
fake_result = MagicMock(returncode=0, stdout="ok", stderr="")
|
||||
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"):
|
||||
with patch("glas_harness.reins.rein_aharness.subprocess.run", return_value=fake_result) as run:
|
||||
result = rein.dispatch_tool(session, ToolCall(name="run_task"))
|
||||
result = rein.dispatch_tool(session, ToolCall(name="run_task"))
|
||||
|
||||
argv = run.call_args.args[0]
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import json
|
||||
import os
|
||||
import subprocess
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
|
|
@ -7,6 +8,7 @@ import pytest
|
|||
from glas_harness.contract import Rein, SandboxHandle, ToolCall
|
||||
from glas_harness.profiles import ProfileCatalog
|
||||
from glas_harness.reins.rein_openweights import ReinOpenWeights, ReinOpenWeightsNotInstalled
|
||||
from glas_harness.transport import ExecutionTransport, TransportError
|
||||
|
||||
|
||||
def test_is_rein_subclass() -> None:
|
||||
|
|
@ -15,8 +17,10 @@ def test_is_rein_subclass() -> None:
|
|||
|
||||
def test_bin_raises_if_not_installed() -> None:
|
||||
rein = ReinOpenWeights(cli_bin="does-not-exist-binary")
|
||||
transport = MagicMock(spec=ExecutionTransport)
|
||||
transport.resolve_executable.side_effect = TransportError("missing")
|
||||
with pytest.raises(ReinOpenWeightsNotInstalled):
|
||||
rein._bin()
|
||||
rein._bin(transport)
|
||||
|
||||
|
||||
def test_start_session_writes_task_file_and_captures_head(tmp_path) -> None:
|
||||
|
|
@ -26,37 +30,45 @@ def test_start_session_writes_task_file_and_captures_head(tmp_path) -> None:
|
|||
subprocess.run(["git", "commit", "-q", "--allow-empty", "-m", "init"], cwd=repo, check=True)
|
||||
|
||||
rein = ReinOpenWeights()
|
||||
sandbox = SandboxHandle(sandbox_id="abc", host="localhost", reachability={"workspace_dir": str(repo)})
|
||||
session = rein.start_session(
|
||||
profile=ProfileCatalog().resolve("harness.agent-dev-openweights-local@1.0.0")[0],
|
||||
inputs={"title": "t", "description": "d"},
|
||||
sandbox=sandbox,
|
||||
sandbox = SandboxHandle(
|
||||
sandbox_id="abc",
|
||||
host="localhost",
|
||||
reachability={"pid": str(os.getpid()), "workspace_dir": str(repo)},
|
||||
)
|
||||
with patch.object(ExecutionTransport, "git_head", return_value="abc123"):
|
||||
session = rein.start_session(
|
||||
profile=ProfileCatalog().resolve("harness.agent-dev-openweights-local@1.0.0")[0],
|
||||
inputs={"title": "t", "description": "d"},
|
||||
sandbox=sandbox,
|
||||
)
|
||||
|
||||
assert session["target_repo"] == str(repo)
|
||||
assert session["head_before"]
|
||||
assert session["head_before"] == "abc123"
|
||||
assert session["transport"].workspace == str(repo)
|
||||
task_spec = json.loads(open(session["task_file"]).read())
|
||||
assert task_spec["title"] == "t"
|
||||
assert task_spec["target_repo"] == str(repo)
|
||||
rein.cleanup_session(session)
|
||||
assert not os.path.exists(session["task_file"])
|
||||
|
||||
|
||||
def test_start_session_requires_resolvable_target_repo() -> None:
|
||||
rein = ReinOpenWeights()
|
||||
sandbox = SandboxHandle(sandbox_id="abc", host="localhost", reachability={})
|
||||
with pytest.raises(ValueError, match="target_repo"):
|
||||
with pytest.raises(RuntimeError, match="reachability"):
|
||||
rein.start_session(profile={}, inputs={"title": "t", "description": "d"}, sandbox=sandbox)
|
||||
|
||||
|
||||
def test_dispatch_tool_invokes_rein_openweights_cli() -> None:
|
||||
rein = ReinOpenWeights()
|
||||
session = {"task_file": "/tmp/task.json"}
|
||||
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}
|
||||
|
||||
fake_result = MagicMock(returncode=0, stdout="ok", stderr="")
|
||||
with patch.object(rein, "_bin", return_value="/usr/bin/rein-openweights"):
|
||||
with patch("glas_harness.reins.rein_openweights.subprocess.run", return_value=fake_result) as run:
|
||||
result = rein.dispatch_tool(session, ToolCall(name="run_task"))
|
||||
result = rein.dispatch_tool(session, ToolCall(name="run_task"))
|
||||
|
||||
argv = run.call_args.args[0]
|
||||
argv = transport.run.call_args.args[0]
|
||||
assert argv[:3] == ["/usr/bin/rein-openweights", "run", "--task-file"]
|
||||
assert "--no-hub" in argv
|
||||
assert result.ok is True
|
||||
|
|
@ -70,14 +82,14 @@ def test_dispatch_tool_passes_model_when_set() -> None:
|
|||
budget_tokens=1234,
|
||||
tool_profile="green-commit-only",
|
||||
)
|
||||
session = {"task_file": "/tmp/task.json"}
|
||||
fake_result = MagicMock(returncode=0, stdout="ok", stderr="")
|
||||
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/rein-openweights"):
|
||||
with patch("glas_harness.reins.rein_openweights.subprocess.run", return_value=fake_result) as run:
|
||||
rein.dispatch_tool(session, ToolCall(name="run_task"))
|
||||
rein.dispatch_tool(session, ToolCall(name="run_task"))
|
||||
|
||||
argv = run.call_args.args[0]
|
||||
argv = transport.run.call_args.args[0]
|
||||
assert "--model" in argv
|
||||
assert "meta-llama/llama-3.1-70b-instruct" in argv
|
||||
assert argv[argv.index("--max-turns") + 1] == "8"
|
||||
|
|
@ -87,17 +99,34 @@ def test_dispatch_tool_passes_model_when_set() -> None:
|
|||
|
||||
def test_dispatch_tool_reports_failure() -> None:
|
||||
rein = ReinOpenWeights()
|
||||
session = {"task_file": "/tmp/task.json"}
|
||||
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}
|
||||
|
||||
fake_result = MagicMock(returncode=1, stdout="", stderr="boom")
|
||||
with patch.object(rein, "_bin", return_value="/usr/bin/rein-openweights"):
|
||||
with patch("glas_harness.reins.rein_openweights.subprocess.run", return_value=fake_result):
|
||||
result = rein.dispatch_tool(session, ToolCall(name="run_task"))
|
||||
result = rein.dispatch_tool(session, ToolCall(name="run_task"))
|
||||
|
||||
assert result.ok is False
|
||||
assert result.error == "boom"
|
||||
|
||||
|
||||
def test_dispatch_tool_enforces_outer_timeout() -> None:
|
||||
rein = ReinOpenWeights()
|
||||
transport = MagicMock(spec=ExecutionTransport)
|
||||
transport.run.side_effect = subprocess.TimeoutExpired("rein-openweights", 30)
|
||||
session = {
|
||||
"task_file": "/tmp/task.json",
|
||||
"transport": transport,
|
||||
"timeout_seconds": 30,
|
||||
}
|
||||
|
||||
with patch.object(rein, "_bin", return_value="/usr/bin/rein-openweights"):
|
||||
with pytest.raises(subprocess.TimeoutExpired):
|
||||
rein.dispatch_tool(session, ToolCall(name="run_task"))
|
||||
|
||||
assert transport.run.call_args.kwargs["timeout"] == 30
|
||||
|
||||
|
||||
def test_end_session_detects_new_commit(tmp_path) -> None:
|
||||
repo = tmp_path / "repo"
|
||||
repo.mkdir()
|
||||
|
|
@ -105,12 +134,31 @@ def test_end_session_detects_new_commit(tmp_path) -> None:
|
|||
subprocess.run(["git", "commit", "-q", "--allow-empty", "-m", "init"], cwd=repo, check=True)
|
||||
|
||||
rein = ReinOpenWeights()
|
||||
from glas_harness.reins._shared import git_head
|
||||
|
||||
head_before = git_head(str(repo))
|
||||
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)
|
||||
|
||||
summary = rein.end_session({"target_repo": str(repo), "head_before": head_before})
|
||||
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_cleanup_session_removes_generated_task_file() -> None:
|
||||
rein = ReinOpenWeights()
|
||||
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")
|
||||
|
|
|
|||
135
tests/test_transport.py
Normal file
135
tests/test_transport.py
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
import json
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from glas_harness.contract import SandboxHandle
|
||||
from glas_harness.transport import ExecutionTransport, TransportError, transport_from_sandbox
|
||||
|
||||
|
||||
def test_local_namespace_transport_requires_pid_and_workspace(tmp_path: Path) -> None:
|
||||
sandbox = SandboxHandle(
|
||||
sandbox_id="sbx",
|
||||
host="localhost",
|
||||
reachability={"pid": "4321", "workspace_dir": str(tmp_path)},
|
||||
)
|
||||
transport = transport_from_sandbox(sandbox)
|
||||
|
||||
assert transport.kind == "local_namespace"
|
||||
assert transport.workspace == str(tmp_path)
|
||||
assert transport.command(["git", "status"]) == [
|
||||
"nsenter",
|
||||
"--target",
|
||||
"4321",
|
||||
"--mount",
|
||||
"--pid",
|
||||
"--net",
|
||||
"--uts",
|
||||
"--ipc",
|
||||
"--",
|
||||
"sh",
|
||||
"-c",
|
||||
'cd "$1" && shift && exec "$@"',
|
||||
"sh",
|
||||
str(tmp_path),
|
||||
"git",
|
||||
"status",
|
||||
]
|
||||
|
||||
|
||||
def test_remote_transport_wraps_command_without_local_shell() -> None:
|
||||
sandbox = SandboxHandle(
|
||||
sandbox_id="sbx",
|
||||
host="sandboxer01",
|
||||
reachability={"ssh": "agent@sandboxer01", "remote_dir": "/tmp/sbx"},
|
||||
)
|
||||
transport = transport_from_sandbox(sandbox)
|
||||
|
||||
assert transport.kind == "ssh"
|
||||
assert transport.command(["git", "-C", "/tmp/sbx", "status"]) == [
|
||||
"ssh",
|
||||
"agent@sandboxer01",
|
||||
"sh -c 'cd \"$1\" && shift && exec \"$@\"' sh /tmp/sbx git -C /tmp/sbx status",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"reachability",
|
||||
[
|
||||
{},
|
||||
{"workspace_dir": "/tmp/ws"},
|
||||
{"pid": "12"},
|
||||
{"ssh": "agent@host"},
|
||||
{"remote_dir": "/tmp/ws"},
|
||||
{
|
||||
"pid": "12",
|
||||
"workspace_dir": "/tmp/ws",
|
||||
"ssh": "agent@host",
|
||||
"remote_dir": "/tmp/ws",
|
||||
},
|
||||
],
|
||||
)
|
||||
def test_incomplete_or_ambiguous_reachability_fails_closed(reachability) -> None:
|
||||
with pytest.raises(TransportError):
|
||||
transport_from_sandbox(
|
||||
SandboxHandle(sandbox_id="sbx", host="localhost", reachability=reachability)
|
||||
)
|
||||
|
||||
|
||||
def test_local_task_file_is_private_and_removable(tmp_path: Path) -> None:
|
||||
(tmp_path / ".git").mkdir()
|
||||
transport = ExecutionTransport(
|
||||
kind="local_namespace", workspace=str(tmp_path), pid=123
|
||||
)
|
||||
task_path = transport.write_task_file({"title": "bounded", "description": "safe"})
|
||||
path = Path(task_path)
|
||||
|
||||
assert path.parent == tmp_path / ".git"
|
||||
assert path.stat().st_mode & 0o777 == 0o600
|
||||
assert json.loads(path.read_text())["title"] == "bounded"
|
||||
|
||||
transport.remove_file(task_path)
|
||||
assert not path.exists()
|
||||
|
||||
|
||||
def test_remote_task_file_uses_ssh_stdin_and_cleanup() -> None:
|
||||
transport = ExecutionTransport(
|
||||
kind="ssh", workspace="/tmp/sbx", ssh_target="agent@sandboxer01"
|
||||
)
|
||||
completed = MagicMock(returncode=0, stdout="", stderr="")
|
||||
with patch("glas_harness.transport.subprocess.run", return_value=completed) as run:
|
||||
task_path = transport.write_task_file({"title": "bounded"})
|
||||
transport.remove_file(task_path)
|
||||
|
||||
create = run.call_args_list[0]
|
||||
assert create.args[0][:2] == ["ssh", "agent@sandboxer01"]
|
||||
assert create.kwargs["input"] == '{"title": "bounded"}'
|
||||
assert "cat >" in create.args[0][2]
|
||||
assert "rm -f" in run.call_args_list[1].args[0][2]
|
||||
|
||||
|
||||
def test_resolve_executable_fails_inside_transport() -> None:
|
||||
transport = ExecutionTransport(
|
||||
kind="ssh", workspace="/tmp/sbx", ssh_target="agent@sandboxer01"
|
||||
)
|
||||
with patch.object(
|
||||
ExecutionTransport,
|
||||
"run",
|
||||
return_value=MagicMock(returncode=127, stdout="", stderr="not found"),
|
||||
):
|
||||
with pytest.raises(TransportError, match="not installed inside"):
|
||||
transport.resolve_executable("rein-aharness")
|
||||
|
||||
|
||||
def test_git_head_fails_closed_on_transport_error() -> None:
|
||||
transport = ExecutionTransport(
|
||||
kind="ssh", workspace="/tmp/sbx", ssh_target="agent@sandboxer01"
|
||||
)
|
||||
with patch.object(
|
||||
ExecutionTransport,
|
||||
"run",
|
||||
return_value=MagicMock(returncode=1, stdout="", stderr="permission denied"),
|
||||
):
|
||||
with pytest.raises(TransportError, match="inside the selected sandbox"):
|
||||
transport.git_head()
|
||||
Loading…
Add table
Add a link
Reference in a new issue