feat(runtime): enforce governed mutation boundaries
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a06ba0-10aa-7ea0-b20a-4f3fac39efe9
This commit is contained in:
parent
e3c6124e22
commit
20e6f381f6
28 changed files with 1068 additions and 154 deletions
12
tests/conftest.py
Normal file
12
tests/conftest.py
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def isolated_runtime_state(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Keep repository locks and close evidence out of operator-owned state."""
|
||||
monkeypatch.setenv("REIN_AHARNESS_STATE_DIR", str(tmp_path / "runtime-state"))
|
||||
monkeypatch.setenv("AGENT_HARNESS_LEGACY_APPROACHES_UNTIL", "2099-12-31")
|
||||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date
|
||||
|
||||
from rein_aharness.approaches import (
|
||||
APPROACH_AGENT_SESSION,
|
||||
APPROACH_BRIEF_DAILY,
|
||||
|
|
@ -9,6 +11,8 @@ from rein_aharness.approaches import (
|
|||
APPROACH_FI_RESEARCH_BRIEF,
|
||||
APPROACH_MAIL_PIPELINE,
|
||||
APPROACH_UNMATCHED,
|
||||
execute_approach,
|
||||
legacy_approaches_enabled,
|
||||
select_approach,
|
||||
)
|
||||
from rein_aharness.ops_run_client import OpsRun
|
||||
|
|
@ -113,3 +117,26 @@ def test_fi_before_generic_automated() -> None:
|
|||
assert (
|
||||
select_approach(_run(labels=["research-brief"])) == APPROACH_FI_RESEARCH_BRIEF
|
||||
)
|
||||
|
||||
|
||||
def test_legacy_approaches_require_non_expired_explicit_date() -> None:
|
||||
today = date(2026, 9, 4)
|
||||
assert legacy_approaches_enabled("2026-09-04", today=today) is True
|
||||
assert legacy_approaches_enabled("2026-09-03", today=today) is False
|
||||
assert legacy_approaches_enabled("true", today=today) is False
|
||||
assert legacy_approaches_enabled("", today=today) is False
|
||||
|
||||
|
||||
def test_execute_approach_refuses_profile_absent_route_without_flag(
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
monkeypatch.delenv("AGENT_HARNESS_LEGACY_APPROACHES_UNTIL")
|
||||
|
||||
result = execute_approach(
|
||||
_run(labels=["research-brief"], target_repo="not-resolved"),
|
||||
report_to_hub=False,
|
||||
)
|
||||
|
||||
assert result.ok is False
|
||||
assert result.reopen is False
|
||||
assert "compatibility routing is disabled" in result.reason
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from rein_aharness.approaches import ApproachResult, APPROACH_FI_RESEARCH_BRIEF
|
||||
from rein_aharness.claim_loop import (
|
||||
|
|
@ -20,6 +22,7 @@ from rein_aharness.ops_run_client import (
|
|||
OpsRunConfig,
|
||||
OpsRunError,
|
||||
)
|
||||
from rein_aharness.repository_transaction import RepositoryTransaction
|
||||
|
||||
|
||||
def _claimed_run() -> OpsRun:
|
||||
|
|
@ -37,6 +40,41 @@ def _claimed_run() -> OpsRun:
|
|||
)
|
||||
|
||||
|
||||
def _profiled_case(
|
||||
tmp_path: Path,
|
||||
) -> tuple[Path, OpsRun, MagicMock]:
|
||||
repo = tmp_path / "freedom-intelligence"
|
||||
repo.mkdir()
|
||||
subprocess.run(["git", "init", "-q"], cwd=repo, check=True)
|
||||
(repo / "README.md").write_text("controlled target\n", encoding="utf-8")
|
||||
subprocess.run(["git", "add", "."], cwd=repo, check=True)
|
||||
subprocess.run(
|
||||
[
|
||||
"git",
|
||||
"-c",
|
||||
"user.email=test@example.invalid",
|
||||
"-c",
|
||||
"user.name=test",
|
||||
"commit",
|
||||
"-qm",
|
||||
"baseline",
|
||||
],
|
||||
cwd=repo,
|
||||
check=True,
|
||||
)
|
||||
|
||||
run = _claimed_run()
|
||||
run.harness_profile_ref = "harness.agent-dev-local@1.0.0"
|
||||
client = MagicMock(spec=ActivityCoreOpsClient)
|
||||
client.config = OpsRunConfig(
|
||||
worker_id="w",
|
||||
lease_seconds=90,
|
||||
repo_roots=(str(tmp_path),),
|
||||
)
|
||||
client.claim.return_value = [run]
|
||||
return repo, run, client
|
||||
|
||||
|
||||
def test_process_one_empty() -> None:
|
||||
client = MagicMock(spec=ActivityCoreOpsClient)
|
||||
client.config = OpsRunConfig(worker_id="w", lease_seconds=90)
|
||||
|
|
@ -231,12 +269,8 @@ def test_process_one_records_adapter_exception_without_unbound_result() -> None:
|
|||
client.fail.assert_not_called()
|
||||
|
||||
|
||||
def test_profiled_exception_after_lease_loss_skips_close() -> None:
|
||||
client = MagicMock(spec=ActivityCoreOpsClient)
|
||||
client.config = OpsRunConfig(worker_id="w", lease_seconds=90)
|
||||
run = _claimed_run()
|
||||
run.harness_profile_ref = "harness.agent-dev-local@1.0.0"
|
||||
client.claim.return_value = [run]
|
||||
def test_profiled_exception_after_lease_loss_skips_close(tmp_path: Path) -> None:
|
||||
repo, _run, client = _profiled_case(tmp_path)
|
||||
client.heartbeat.side_effect = OpsRunError("lease rejected", status_code=409)
|
||||
|
||||
def slow_profile(*_args, **_kwargs):
|
||||
|
|
@ -253,6 +287,8 @@ def test_profiled_exception_after_lease_loss_skips_close() -> None:
|
|||
assert result.reason.startswith("lease lost")
|
||||
client.complete.assert_not_called()
|
||||
client.fail.assert_not_called()
|
||||
with RepositoryTransaction(repo) as retry:
|
||||
assert retry.locked is True
|
||||
|
||||
|
||||
def test_poll_peek() -> None:
|
||||
|
|
@ -263,13 +299,11 @@ def test_poll_peek() -> None:
|
|||
assert rows[0]["approach"] == APPROACH_FI_RESEARCH_BRIEF
|
||||
|
||||
|
||||
def test_profiled_run_uses_glas_and_completes_with_full_result() -> None:
|
||||
client = MagicMock(spec=ActivityCoreOpsClient)
|
||||
client.config = OpsRunConfig(worker_id="w", lease_seconds=90)
|
||||
run = _claimed_run()
|
||||
run.harness_profile_ref = "harness.agent-dev-local@1.0.0"
|
||||
def test_profiled_run_uses_glas_and_completes_with_full_result(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
_repo, run, client = _profiled_case(tmp_path)
|
||||
run.approach_hint = "fi-research-brief"
|
||||
client.claim.return_value = [run]
|
||||
client.complete.return_value = OpsRun(
|
||||
id=run.id,
|
||||
activity_definition_id="def",
|
||||
|
|
@ -313,13 +347,12 @@ def test_profiled_run_uses_glas_and_completes_with_full_result() -> None:
|
|||
execute.assert_not_called()
|
||||
|
||||
|
||||
def test_profile_refusal_fails_terminally_without_legacy_fallback() -> None:
|
||||
client = MagicMock(spec=ActivityCoreOpsClient)
|
||||
client.config = OpsRunConfig(worker_id="w", lease_seconds=90)
|
||||
run = _claimed_run()
|
||||
def test_profile_refusal_fails_terminally_without_legacy_fallback(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
_repo, run, client = _profiled_case(tmp_path)
|
||||
run.harness_profile_ref = "harness.unknown@9.9.9"
|
||||
run.approach_hint = "fi-research-brief"
|
||||
client.claim.return_value = [run]
|
||||
client.fail.return_value = OpsRun(
|
||||
id=run.id,
|
||||
activity_definition_id="def",
|
||||
|
|
@ -346,6 +379,49 @@ def test_profile_refusal_fails_terminally_without_legacy_fallback() -> None:
|
|||
execute.assert_not_called()
|
||||
|
||||
|
||||
def test_profiled_signal_cancellation_releases_repository_lock(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
repo, _run, client = _profiled_case(tmp_path)
|
||||
|
||||
def cancel_during_gateway(*_args, **_kwargs):
|
||||
_cancel_active_run("signal")
|
||||
return {"ok": True, "evidence": {"outcome": "late-success"}}
|
||||
|
||||
with patch(
|
||||
"rein_aharness.claim_loop.execute_profiled_run",
|
||||
side_effect=cancel_during_gateway,
|
||||
):
|
||||
result = process_one(client)
|
||||
|
||||
assert result.ok is False
|
||||
assert result.reason == "execution cancelled (signal)"
|
||||
client.complete.assert_not_called()
|
||||
client.fail.assert_not_called()
|
||||
with RepositoryTransaction(repo) as retry:
|
||||
assert retry.locked is True
|
||||
|
||||
|
||||
def test_profiled_close_failure_happens_after_repository_lock_release(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
repo, _run, client = _profiled_case(tmp_path)
|
||||
client.complete.side_effect = OpsRunError("complete transport failed")
|
||||
gateway_result = {"ok": True, "evidence": {"outcome": "succeeded"}}
|
||||
|
||||
with patch(
|
||||
"rein_aharness.claim_loop.execute_profiled_run",
|
||||
return_value=gateway_result,
|
||||
):
|
||||
result = process_one(client)
|
||||
|
||||
assert result.ok is False
|
||||
assert result.reason.startswith("close ops_run failed:")
|
||||
assert "repository_transaction" in result.detail
|
||||
with RepositoryTransaction(repo) as retry:
|
||||
assert retry.locked is True
|
||||
|
||||
|
||||
def test_poll_peek_reports_authoritative_profile_route() -> None:
|
||||
client = MagicMock(spec=ActivityCoreOpsClient)
|
||||
run = _claimed_run()
|
||||
|
|
|
|||
67
tests/test_fi_research_brief.py
Normal file
67
tests/test_fi_research_brief.py
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
from rein_aharness import fi_research_brief
|
||||
|
||||
|
||||
def _repo(tmp_path: Path) -> Path:
|
||||
repo = tmp_path / "freedom-intelligence"
|
||||
repo.mkdir()
|
||||
subprocess.run(["git", "init", "-q"], cwd=repo, check=True)
|
||||
(repo / "README.md").write_text("baseline\n", encoding="utf-8")
|
||||
subprocess.run(["git", "add", "."], cwd=repo, check=True)
|
||||
subprocess.run(
|
||||
[
|
||||
"git",
|
||||
"-c",
|
||||
"user.email=test@example.invalid",
|
||||
"-c",
|
||||
"user.name=test",
|
||||
"commit",
|
||||
"-qm",
|
||||
"baseline",
|
||||
],
|
||||
cwd=repo,
|
||||
check=True,
|
||||
)
|
||||
return repo
|
||||
|
||||
|
||||
def test_fi_brief_never_pushes_even_when_legacy_env_requests_it(
|
||||
tmp_path: Path,
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
repo = _repo(tmp_path)
|
||||
monkeypatch.setenv("FI_RESEARCH_BRIEF_PUSH", "1")
|
||||
original_git = fi_research_brief._git
|
||||
calls: list[tuple[str, ...]] = []
|
||||
|
||||
def observed_git(target: Path, *args: str) -> str:
|
||||
calls.append(args)
|
||||
return original_git(target, *args)
|
||||
|
||||
monkeypatch.setattr(fi_research_brief, "_git", observed_git)
|
||||
result = fi_research_brief.run_fi_research_brief(
|
||||
repo,
|
||||
day=date(2026, 9, 4),
|
||||
report_to_hub=False,
|
||||
complete_fn=lambda _prompt: json.dumps(
|
||||
{
|
||||
"headline_deltas": ["No material delta after allowlist review."],
|
||||
"axis_a": [],
|
||||
"axis_b": [],
|
||||
"axis_c": [],
|
||||
"axis_d": [],
|
||||
"collection_candidates": [],
|
||||
"lab_implications": ["Recheck tomorrow."],
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
assert result.ok is True
|
||||
assert result.committed is True
|
||||
assert not any(args and args[0] == "push" for args in calls)
|
||||
|
|
@ -3,7 +3,14 @@ from __future__ import annotations
|
|||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from rein_aharness.metrics import record_execution, regenerate_summary
|
||||
import pytest
|
||||
|
||||
from rein_aharness.metrics import (
|
||||
external_metrics_dir,
|
||||
record_execution,
|
||||
record_external_execution,
|
||||
regenerate_summary,
|
||||
)
|
||||
|
||||
|
||||
def test_record_execution_writes_jsonl_and_summary(tmp_path: Path) -> None:
|
||||
|
|
@ -49,3 +56,89 @@ def test_summary_aggregates_multiple(tmp_path: Path) -> None:
|
|||
|
||||
def test_regenerate_summary_empty() -> None:
|
||||
assert regenerate_summary("x", [])["execution_count"] == 0
|
||||
|
||||
|
||||
def test_external_metrics_are_durable_and_projection_ready(tmp_path: Path) -> None:
|
||||
repo = tmp_path / "repo"
|
||||
repo.mkdir()
|
||||
state_dir = tmp_path / "state"
|
||||
|
||||
path = record_external_execution(
|
||||
repo,
|
||||
"coach",
|
||||
success=True,
|
||||
committed=True,
|
||||
head_after="abc123",
|
||||
metadata={"repository_grant_id": "grant-1"},
|
||||
session_id="transaction-1",
|
||||
state_dir=state_dir,
|
||||
)
|
||||
|
||||
directory = external_metrics_dir(repo, "coach", state_dir=state_dir)
|
||||
assert path == directory / "executions.jsonl"
|
||||
assert path.stat().st_mode & 0o777 == 0o600
|
||||
assert directory.stat().st_mode & 0o777 == 0o700
|
||||
record = json.loads(path.read_text(encoding="utf-8").strip())
|
||||
assert record["metadata"]["repository_grant_id"] == "grant-1"
|
||||
assert record["session_id"] == "transaction-1"
|
||||
projection = json.loads(
|
||||
(directory / "projection.json").read_text(encoding="utf-8")
|
||||
)
|
||||
assert projection["repository_name"] == "repo"
|
||||
assert projection["target_relative_directory"] == ".kaizen/metrics/coach"
|
||||
assert not (repo / ".kaizen").exists()
|
||||
|
||||
|
||||
def test_external_metrics_deduplicate_transaction_replay(tmp_path: Path) -> None:
|
||||
repo = tmp_path / "repo"
|
||||
repo.mkdir()
|
||||
state_dir = tmp_path / "state"
|
||||
|
||||
for _attempt in range(2):
|
||||
path = record_external_execution(
|
||||
repo,
|
||||
"coach",
|
||||
success=True,
|
||||
session_id="transaction-1",
|
||||
state_dir=state_dir,
|
||||
)
|
||||
|
||||
assert len(path.read_text(encoding="utf-8").splitlines()) == 1
|
||||
summary = json.loads((path.parent / "summary.json").read_text(encoding="utf-8"))
|
||||
assert summary["execution_count"] == 1
|
||||
|
||||
|
||||
def test_external_metrics_refuse_an_incomplete_ledger(tmp_path: Path) -> None:
|
||||
repo = tmp_path / "repo"
|
||||
repo.mkdir()
|
||||
state_dir = tmp_path / "state"
|
||||
directory = external_metrics_dir(repo, "coach", state_dir=state_dir)
|
||||
directory.mkdir(parents=True)
|
||||
ledger = directory / "executions.jsonl"
|
||||
ledger.write_text('{"incomplete":true}', encoding="utf-8")
|
||||
|
||||
with pytest.raises(OSError, match="incomplete record"):
|
||||
record_external_execution(
|
||||
repo,
|
||||
"coach",
|
||||
success=True,
|
||||
state_dir=state_dir,
|
||||
)
|
||||
|
||||
assert ledger.read_text(encoding="utf-8") == '{"incomplete":true}'
|
||||
|
||||
|
||||
def test_external_metrics_refuse_unsafe_projection_agent(tmp_path: Path) -> None:
|
||||
repo = tmp_path / "repo"
|
||||
repo.mkdir()
|
||||
state_dir = tmp_path / "state"
|
||||
|
||||
with pytest.raises(OSError, match="safe metrics projection"):
|
||||
record_external_execution(
|
||||
repo,
|
||||
"../escape",
|
||||
success=True,
|
||||
state_dir=state_dir,
|
||||
)
|
||||
|
||||
assert not state_dir.exists()
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from pathlib import Path
|
|||
|
||||
import pytest
|
||||
|
||||
from rein_aharness.metrics import external_metrics_dir
|
||||
from rein_aharness.repository_grant import RepositoryGrant, RepositoryGrantError
|
||||
from rein_aharness.repository_transaction import RepositoryTransaction
|
||||
from rein_aharness.runner import run_task
|
||||
|
|
@ -242,18 +243,129 @@ def test_taskspec_file_wraps_invalid_repository_grant(tmp_path: Path) -> None:
|
|||
TaskSpec.from_file(task_file)
|
||||
|
||||
|
||||
def test_runner_refuses_grant_before_adapter_dispatch_or_mutation(tmp_path: Path) -> None:
|
||||
def test_runner_accepts_granted_commit_and_keeps_checkout_clean(tmp_path: Path) -> None:
|
||||
repo = _make_repo(tmp_path)
|
||||
|
||||
class Adapter:
|
||||
def execute_prompt(self, prompt: str, config: object):
|
||||
docs = repo / "docs"
|
||||
docs.mkdir()
|
||||
(docs / "result.md").write_text("accepted\n", encoding="utf-8")
|
||||
subprocess.run(["git", "add", "."], cwd=repo, check=True)
|
||||
subprocess.run(
|
||||
[
|
||||
"git",
|
||||
"-c",
|
||||
"user.email=test@example.invalid",
|
||||
"-c",
|
||||
"user.name=test",
|
||||
"commit",
|
||||
"-qm",
|
||||
"accepted result",
|
||||
],
|
||||
cwd=repo,
|
||||
check=True,
|
||||
)
|
||||
from llm_connect.models import LLMResponse
|
||||
|
||||
return LLMResponse(
|
||||
content="done",
|
||||
model="fake",
|
||||
usage={},
|
||||
finish_reason="stop",
|
||||
)
|
||||
|
||||
result = run_task(
|
||||
TaskSpec(
|
||||
title="bounded change",
|
||||
description="update docs",
|
||||
target_repo=repo,
|
||||
repository_grant=RepositoryGrant.from_mapping(_grant()),
|
||||
),
|
||||
adapter=Adapter(),
|
||||
report_to_hub=False,
|
||||
write_metrics=True,
|
||||
)
|
||||
|
||||
assert result.ok is True
|
||||
assert result.committed is True
|
||||
assert result.transaction is not None
|
||||
assert result.transaction["acceptance"]["accepted"] is True
|
||||
assert result.transaction["acceptance"]["changed_paths"] == ["docs/result.md"]
|
||||
assert result.transaction["repository_grant"]["grant_id"]
|
||||
assert result.transaction["metrics"] == {
|
||||
"storage": "external",
|
||||
"session_id": result.transaction["transaction_id"],
|
||||
"projection_ready": True,
|
||||
}
|
||||
assert _git(repo, "status", "--porcelain=v2") == ""
|
||||
assert not (repo / ".kaizen" / "metrics").exists()
|
||||
metric_path = external_metrics_dir(repo, "coach") / "executions.jsonl"
|
||||
metric_record = json.loads(metric_path.read_text(encoding="utf-8").strip())
|
||||
assert metric_record["session_id"] == result.transaction["transaction_id"]
|
||||
|
||||
|
||||
def test_runner_rejects_commit_outside_repository_grant(tmp_path: Path) -> None:
|
||||
repo = _make_repo(tmp_path)
|
||||
|
||||
class Adapter:
|
||||
def execute_prompt(self, prompt: str, config: object):
|
||||
(repo / "UNRELATED.md").write_text("not granted\n", encoding="utf-8")
|
||||
subprocess.run(["git", "add", "."], cwd=repo, check=True)
|
||||
subprocess.run(
|
||||
[
|
||||
"git",
|
||||
"-c",
|
||||
"user.email=test@example.invalid",
|
||||
"-c",
|
||||
"user.name=test",
|
||||
"commit",
|
||||
"-qm",
|
||||
"ungranted result",
|
||||
],
|
||||
cwd=repo,
|
||||
check=True,
|
||||
)
|
||||
from llm_connect.models import LLMResponse
|
||||
|
||||
return LLMResponse(
|
||||
content="done",
|
||||
model="fake",
|
||||
usage={},
|
||||
finish_reason="stop",
|
||||
)
|
||||
|
||||
result = run_task(
|
||||
TaskSpec(
|
||||
title="bounded change",
|
||||
description="update docs",
|
||||
target_repo=repo,
|
||||
repository_grant=RepositoryGrant.from_mapping(_grant()),
|
||||
),
|
||||
adapter=Adapter(),
|
||||
report_to_hub=False,
|
||||
write_metrics=True,
|
||||
)
|
||||
|
||||
assert result.ok is False
|
||||
assert result.committed is True
|
||||
assert result.reason.startswith("repository acceptance failed: path-not-granted")
|
||||
assert result.transaction is not None
|
||||
assert "acceptance" not in result.transaction
|
||||
|
||||
|
||||
def test_runner_refuses_grant_when_durable_metrics_are_disabled(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
repo = _make_repo(tmp_path)
|
||||
called = False
|
||||
|
||||
class Adapter:
|
||||
def execute_prompt(self, prompt: str, config: object) -> None:
|
||||
def execute_prompt(self, prompt: str, config: object):
|
||||
nonlocal called
|
||||
called = True
|
||||
raise AssertionError("adapter must not be dispatched")
|
||||
|
||||
head_before = _git(repo, "rev-parse", "HEAD")
|
||||
status_before = _git(repo, "status", "--porcelain=v2")
|
||||
result = run_task(
|
||||
TaskSpec(
|
||||
title="bounded change",
|
||||
|
|
@ -267,11 +379,69 @@ def test_runner_refuses_grant_before_adapter_dispatch_or_mutation(tmp_path: Path
|
|||
)
|
||||
|
||||
assert result.ok is False
|
||||
assert result.committed is False
|
||||
assert result.reason.startswith("refused: repository_grant enforcement")
|
||||
assert "require durable external metrics" in result.reason
|
||||
assert called is False
|
||||
assert _git(repo, "rev-parse", "HEAD") == head_before
|
||||
assert _git(repo, "status", "--porcelain=v2") == status_before
|
||||
|
||||
|
||||
def test_runner_fails_granted_result_when_external_metrics_cannot_persist(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
repo = _make_repo(tmp_path)
|
||||
|
||||
class Adapter:
|
||||
def execute_prompt(self, prompt: str, config: object):
|
||||
docs = repo / "docs"
|
||||
docs.mkdir()
|
||||
(docs / "result.md").write_text("accepted\n", encoding="utf-8")
|
||||
subprocess.run(["git", "add", "."], cwd=repo, check=True)
|
||||
subprocess.run(
|
||||
[
|
||||
"git",
|
||||
"-c",
|
||||
"user.email=test@example.invalid",
|
||||
"-c",
|
||||
"user.name=test",
|
||||
"commit",
|
||||
"-qm",
|
||||
"accepted result",
|
||||
],
|
||||
cwd=repo,
|
||||
check=True,
|
||||
)
|
||||
from llm_connect.models import LLMResponse
|
||||
|
||||
return LLMResponse(
|
||||
content="done",
|
||||
model="fake",
|
||||
usage={},
|
||||
finish_reason="stop",
|
||||
)
|
||||
|
||||
def fail_metrics(*args: object, **kwargs: object) -> None:
|
||||
raise OSError("state volume unavailable")
|
||||
|
||||
monkeypatch.setattr(
|
||||
"rein_aharness.runner.metrics.record_external_execution",
|
||||
fail_metrics,
|
||||
)
|
||||
result = run_task(
|
||||
TaskSpec(
|
||||
title="bounded change",
|
||||
description="update docs",
|
||||
target_repo=repo,
|
||||
repository_grant=RepositoryGrant.from_mapping(_grant()),
|
||||
),
|
||||
adapter=Adapter(),
|
||||
report_to_hub=False,
|
||||
write_metrics=True,
|
||||
)
|
||||
|
||||
assert result.ok is False
|
||||
assert result.reason == "required external metrics persistence failed (OSError)"
|
||||
assert result.transaction is not None
|
||||
assert result.transaction["acceptance"]["accepted"] is True
|
||||
assert _git(repo, "status", "--porcelain=v2") == ""
|
||||
|
||||
|
||||
def _git(repo: Path, *args: str) -> str:
|
||||
|
|
|
|||
|
|
@ -78,9 +78,15 @@ def test_transaction_captures_clean_branch_and_remote_refs(tmp_path: Path) -> No
|
|||
|
||||
def test_dirty_baseline_is_refused_without_changing_user_files(tmp_path: Path) -> None:
|
||||
repo = _make_repo(tmp_path)
|
||||
subprocess.run(
|
||||
["git", "update-ref", "refs/remotes/origin/main", "HEAD"],
|
||||
cwd=repo,
|
||||
check=True,
|
||||
)
|
||||
changed = repo / "README.md"
|
||||
changed.write_text("operator change\n", encoding="utf-8")
|
||||
before = _status(repo)
|
||||
remote_refs_before = _remote_refs(repo)
|
||||
|
||||
with pytest.raises(DirtyRepositoryError) as excinfo:
|
||||
with RepositoryTransaction(repo, state_dir=tmp_path / "state"):
|
||||
|
|
@ -90,6 +96,7 @@ def test_dirty_baseline_is_refused_without_changing_user_files(tmp_path: Path) -
|
|||
assert excinfo.value.baseline.dirty_entries == 1
|
||||
assert changed.read_text(encoding="utf-8") == "operator change\n"
|
||||
assert _status(repo) == before
|
||||
assert _remote_refs(repo) == remote_refs_before
|
||||
|
||||
# Refusal released the lock; inspection can explicitly opt into dirty state.
|
||||
with RepositoryTransaction(
|
||||
|
|
@ -469,3 +476,18 @@ def _status(repo: Path) -> str:
|
|||
capture_output=True,
|
||||
text=True,
|
||||
).stdout
|
||||
|
||||
|
||||
def _remote_refs(repo: Path) -> str:
|
||||
return subprocess.run(
|
||||
[
|
||||
"git",
|
||||
"for-each-ref",
|
||||
"--format=%(refname) %(objectname)",
|
||||
"refs/remotes",
|
||||
],
|
||||
cwd=repo,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
).stdout
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import pytest
|
|||
import yaml
|
||||
|
||||
from rein_aharness.manifest import HARNESS_MAJOR
|
||||
from rein_aharness.repository_transaction import RepositoryTransaction
|
||||
from rein_aharness.runner import RunResult, run_task
|
||||
from rein_aharness.taskspec import TaskSpec, TaskSpecError
|
||||
|
||||
|
|
@ -148,6 +149,26 @@ def test_run_task_records_bounded_cancellation_without_session_output(tmp_path)
|
|||
assert result.reason == "execution cancelled (lease-loss)"
|
||||
|
||||
|
||||
def test_run_task_timeout_releases_repository_lock(tmp_path) -> None:
|
||||
repo = _make_repo(tmp_path)
|
||||
|
||||
class TimingOutAdapter:
|
||||
def execute_prompt(self, prompt, config):
|
||||
raise TimeoutError("adapter deadline elapsed")
|
||||
|
||||
result = run_task(
|
||||
_spec(repo),
|
||||
adapter=TimingOutAdapter(),
|
||||
report_to_hub=False,
|
||||
write_metrics=False,
|
||||
)
|
||||
|
||||
assert result.ok is False
|
||||
assert result.reason == "session failed: adapter deadline elapsed"
|
||||
with RepositoryTransaction(repo) as retry:
|
||||
assert retry.locked is True
|
||||
|
||||
|
||||
def test_run_task_refuses_unknown_tool_profile(tmp_path) -> None:
|
||||
repo = _make_repo(tmp_path)
|
||||
_write_manifest(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue