Merge branch 'master' into feat/issue-665-restart-audit

This commit is contained in:
2026-07-28 08:39:00 -05:00
84 changed files with 21647 additions and 242 deletions
+2
View File
@@ -44,6 +44,8 @@ def _reset_mutation_authority(monkeypatch):
]:
monkeypatch.delenv(env_key, raising=False)
monkeypatch.setenv("GITEA_CLIENT_MANAGED", "1")
# Isolate durable session-state files so tests never share host cache (#559).
import tempfile
+158
View File
@@ -7,6 +7,7 @@ import tempfile
import threading
import unittest
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime, timezone
from allocator_service import (
OUTCOME_ASSIGNED,
@@ -15,6 +16,7 @@ from allocator_service import (
OUTCOME_PREVIEW,
OUTCOME_WAIT,
WorkCandidate,
_drop_expired_claims,
allocate_next_work,
candidate_from_dict,
classify_skip,
@@ -362,5 +364,161 @@ class AllocatorServiceTest(unittest.TestCase):
self.assertIn("unavailable", res["reasons"][0].lower())
class SideEffectFreeAllocationTest(unittest.TestCase):
"""``side_effect_free`` dry runs write nothing to the control plane (#643).
A plain ``apply=False`` still called ``upsert_session`` and
``expire_stale_leases`` before the apply branch was consulted, so a caller
advertising a read-only preview mutated on every call — one unreferenced
session row per preview, plus a global lease sweep.
"""
def setUp(self) -> None:
self._tmp = tempfile.TemporaryDirectory()
self.db = ControlPlaneDB(os.path.join(self._tmp.name, "cp.sqlite3"))
def tearDown(self) -> None:
self._tmp.cleanup()
def _alloc(self, **kwargs):
defaults = dict(
db=self.db,
session_id="s-preview",
role="author",
remote="prgs",
org="org",
repo="repo",
candidates=[
WorkCandidate(kind="issue", number=643, labels=("status:ready",))
],
apply=False,
profile_name="prgs-author",
username="jcwalker3",
)
defaults.update(kwargs)
return allocate_next_work(**defaults)
def _session_ids(self) -> set[str]:
return {str(r.get("session_id")) for r in self.db.list_sessions()}
def test_side_effect_free_preview_writes_no_session_row(self):
before = self._session_ids()
result = self._alloc(side_effect_free=True)
self.assertEqual(result["outcome"], OUTCOME_PREVIEW)
self.assertEqual(self._session_ids(), before)
self.assertNotIn("s-preview", self._session_ids())
def test_plain_dry_run_still_registers_a_session(self):
# The default is unchanged for every existing caller.
self._alloc()
self.assertIn("s-preview", self._session_ids())
def test_repeated_previews_do_not_accumulate_rows(self):
for index in range(5):
self._alloc(side_effect_free=True, session_id=f"s-{index}")
self.assertEqual(self._session_ids(), set())
def test_side_effect_free_does_not_sweep_stale_leases(self):
self.db.upsert_session(session_id="owner", role="author", pid=1)
assigned = self.db.assign_and_lease(
session_id="owner",
role="author",
remote="prgs",
org="org",
repo="repo",
kind="issue",
number=999,
lease_ttl_seconds=-60, # already expired
)
self.assertEqual(assigned.outcome, "assigned")
self._alloc(side_effect_free=True)
# The expired row is still 'active' in the DB: nothing swept it.
statuses = {
r["lease_id"]: r["status"]
for r in self.db.list_leases(
remote="prgs", org="org", repo="repo",
statuses=("active", "expired"),
)
}
self.assertEqual(statuses.get(assigned.lease_id), "active")
def test_expired_claims_are_filtered_in_memory_so_work_stays_selectable(self):
"""The read-only mirror of the sweep: expired claims must not block."""
self.db.upsert_session(session_id="owner", role="author", pid=1)
self.db.assign_and_lease(
session_id="owner",
role="author",
remote="prgs",
org="org",
repo="repo",
kind="issue",
number=643,
lease_ttl_seconds=-60, # expired: must not withhold #643
)
result = self._alloc(side_effect_free=True)
self.assertEqual(result["outcome"], OUTCOME_PREVIEW)
self.assertEqual(result["selected"]["number"], 643)
def test_a_live_claim_still_withholds_the_work(self):
self.db.upsert_session(session_id="owner", role="author", pid=1)
self.db.assign_and_lease(
session_id="owner",
role="author",
remote="prgs",
org="org",
repo="repo",
kind="issue",
number=643,
lease_ttl_seconds=3600,
)
result = self._alloc(side_effect_free=True)
self.assertNotEqual(result["outcome"], OUTCOME_ASSIGNED)
self.assertNotEqual((result.get("selected") or {}).get("number"), 643)
def test_side_effect_free_with_apply_fails_closed(self):
result = self._alloc(side_effect_free=True, apply=True)
self.assertFalse(result["success"])
self.assertEqual(result["outcome"], OUTCOME_NO_SAFE)
self.assertIsNone(result["assignment"])
self.assertIn("incompatible with apply", result["reasons"][0])
# And it reserved nothing.
self.assertEqual(
self.db.list_leases(remote="prgs", org="org", repo="repo"), []
)
class DropExpiredClaimsTest(unittest.TestCase):
"""The in-memory expiry filter behind side-effect-free previews (#643)."""
def test_unparseable_expiry_is_kept_rather_than_assumed_free(self):
claims = {
("issue", 1): {"lease_id": "l1", "expires_at": "not-a-date"},
("issue", 2): {"lease_id": "l2"},
("issue", 3): {"lease_id": "l3", "expires_at": None},
}
self.assertEqual(_drop_expired_claims(claims), claims)
def test_expired_dropped_and_future_kept(self):
now = datetime(2026, 7, 25, 12, 0, tzinfo=timezone.utc)
claims = {
("issue", 1): {"expires_at": "2026-07-25T11:59:59+00:00"},
("issue", 2): {"expires_at": "2026-07-25T12:00:01+00:00"},
("issue", 3): {"expires_at": "2026-07-25T12:00:00+00:00"}, # boundary
}
kept = _drop_expired_claims(claims, now=now)
self.assertEqual(set(kept), {("issue", 2)})
def test_naive_and_zulu_timestamps_are_treated_as_utc(self):
now = datetime(2026, 7, 25, 12, 0, tzinfo=timezone.utc)
claims = {
("issue", 1): {"expires_at": "2026-07-25T11:00:00"}, # naive, past
("issue", 2): {"expires_at": "2026-07-25T13:00:00Z"}, # zulu, future
}
kept = _drop_expired_claims(claims, now=now)
self.assertEqual(set(kept), {("issue", 2)})
if __name__ == "__main__":
unittest.main()
+2 -2
View File
@@ -35,7 +35,7 @@ CONFIG = {
],
"forbidden_operations": [],
"execution_profile": "full-author",
"allowed_repositories": ["Example-Org/Example-Repo"],
"allowed_repositories": ["Scaled-Tech-Consulting/Gitea-Tools", "Example-Org/Example-Repo"],
},
"reviewer-no-commit": {
"enabled": True,
@@ -50,7 +50,7 @@ CONFIG = {
"gitea.repo.commit", "gitea.pr.create", "gitea.branch.push"
],
"execution_profile": "reviewer-no-commit",
"allowed_repositories": ["Example-Org/Example-Repo"],
"allowed_repositories": ["Scaled-Tech-Consulting/Gitea-Tools", "Example-Org/Example-Repo"],
},
},
"rules": {"allow_runtime_switching": False},
+1 -1
View File
@@ -175,7 +175,7 @@ class TestLauncherSnippets(unittest.TestCase):
def test_only_safe_keys_no_secrets(self):
entry = gitea_config.launcher_entry("prgs", "/cfg/profiles.json")["gitea-tools"]
self.assertEqual(set(entry), {"command", "args", "env"})
self.assertEqual(set(entry["env"]), {"GITEA_MCP_CONFIG", "GITEA_MCP_PROFILE"})
self.assertEqual(set(entry["env"]), {"GITEA_MCP_CONFIG", "GITEA_MCP_PROFILE", "GITEA_CLIENT_MANAGED"})
self.assertEqual(entry["env"]["GITEA_MCP_PROFILE"], "prgs")
blob = json.dumps(entry).lower()
for word in ("token", "password", "secret"):
@@ -0,0 +1,323 @@
"""Tests for sanctioned Codex MCP reconnect request surface (#678)."""
from __future__ import annotations
import os
import unittest
from unittest import mock
import mcp_client_reconnect as mcr
class NormalizeReasonTests(unittest.TestCase):
def test_stale_runtime_aliases(self):
self.assertEqual(mcr.normalize_reason("stale-runtime"), mcr.REASON_STALE_RUNTIME)
self.assertEqual(mcr.normalize_reason("stale_runtime"), mcr.REASON_STALE_RUNTIME)
self.assertEqual(mcr.normalize_reason("STALE"), mcr.REASON_STALE_RUNTIME)
def test_transport_eof_aliases(self):
self.assertEqual(mcr.normalize_reason("transport_eof"), mcr.REASON_TRANSPORT_EOF)
self.assertEqual(mcr.normalize_reason("EOF"), mcr.REASON_TRANSPORT_EOF)
self.assertEqual(
mcr.normalize_reason("client_is_closing"), mcr.REASON_TRANSPORT_EOF
)
def test_missing_namespace(self):
self.assertEqual(
mcr.normalize_reason("missing_namespace"), mcr.REASON_MISSING_NAMESPACE
)
def test_empty_is_unspecified(self):
self.assertEqual(mcr.normalize_reason(None), mcr.REASON_UNSPECIFIED)
self.assertEqual(mcr.normalize_reason(""), mcr.REASON_UNSPECIFIED)
class BoundaryClassificationTests(unittest.TestCase):
def test_clean_when_shas_match(self):
self.assertEqual(
mcr.classify_boundary_status(
startup_sha="abc", current_master_sha="abc"
),
mcr.BOUNDARY_CLEAN,
)
def test_mismatch_when_shas_differ(self):
self.assertEqual(
mcr.classify_boundary_status(
startup_sha="aaa", current_master_sha="bbb"
),
mcr.BOUNDARY_MISMATCH,
)
def test_stale_when_live_stale(self):
self.assertEqual(
mcr.classify_boundary_status(
startup_sha="aaa",
current_master_sha="aaa",
live_stale=True,
),
mcr.BOUNDARY_STALE,
)
class BuildReconnectRequestTests(unittest.TestCase):
def test_stale_runtime_returns_typed_blocker_with_codex_steps(self):
result = mcr.build_reconnect_request(
namespace="gitea-author",
profile="prgs-author",
pid=1234,
session_id="sess-1",
startup_sha="aaa111",
current_master_sha="bbb222",
reason="stale-runtime",
client="codex",
restart_required=True,
stop_required=True,
)
self.assertTrue(result["success"])
self.assertTrue(result["read_only"])
self.assertFalse(result["reconnect_performed"])
self.assertFalse(result["mutation_performed"])
self.assertTrue(result["reconnect_needed"])
self.assertEqual(result["namespace"], "gitea-author")
self.assertEqual(result["profile"], "prgs-author")
self.assertEqual(result["pid"], 1234)
self.assertEqual(result["session_id"], "sess-1")
self.assertEqual(result["startup_sha"], "aaa111")
self.assertEqual(result["current_master_sha"], "bbb222")
self.assertEqual(result["boundary_status"], mcr.BOUNDARY_MISMATCH)
self.assertEqual(result["blocker_kind"], mcr.BLOCKER_OPERATOR_RECONNECT)
self.assertIsNotNone(result["typed_blocker"])
blocker = result["typed_blocker"]
self.assertEqual(blocker["namespaces"], ["gitea-author"])
self.assertEqual(blocker["why_reconnect_required"], mcr.REASON_STALE_RUNTIME)
self.assertTrue(any("Codex" in s or "Reload" in s for s in blocker["operator_ui_steps"]))
self.assertIn("pkill", " ".join(result["forbidden_recovery_paths"]).lower())
self.assertTrue(
mcr.reasons_never_suggest_forbidden(result["exact_safe_next_action"] or "")
)
# Must not recommend forbidden recovery.
for step in blocker["operator_ui_steps"]:
self.assertTrue(mcr.reasons_never_suggest_forbidden(step), step)
def test_transport_eof_typed_blocker(self):
result = mcr.build_reconnect_request(
namespace="gitea-reviewer",
reason="transport_eof",
client="claude_code",
)
self.assertTrue(result["reconnect_needed"])
self.assertEqual(result["reason"], mcr.REASON_TRANSPORT_EOF)
self.assertEqual(result["client"], "claude_code")
steps = " ".join(result["operator_ui_steps"]).lower()
self.assertIn("/mcp", steps)
def test_missing_namespace_typed_blocker(self):
result = mcr.build_reconnect_request(
namespace="gitea-merger",
reason="missing_namespace",
client="codex",
)
self.assertTrue(result["reconnect_needed"])
self.assertEqual(result["reason"], mcr.REASON_MISSING_NAMESPACE)
self.assertEqual(
result["typed_blocker"]["blocker_kind"], mcr.BLOCKER_OPERATOR_RECONNECT
)
def test_healthy_not_required(self):
result = mcr.build_reconnect_request(
namespace="gitea-tools",
startup_sha="deadbeef",
current_master_sha="deadbeef",
reason="not_required",
client="codex",
in_parity=True,
restart_required=False,
stop_required=False,
)
self.assertFalse(result["reconnect_needed"])
self.assertEqual(result["blocker_kind"], mcr.BLOCKER_NONE)
self.assertIsNone(result["typed_blocker"])
self.assertFalse(result["stop_required"])
self.assertFalse(result["restart_required"])
self.assertIn("not required", (result["exact_safe_next_action"] or "").lower())
def test_successful_reconnect_report_fields_present(self):
"""AC2: reconnect result reports required fields (even when needed)."""
result = mcr.build_reconnect_request(
namespace="gitea-controller",
profile="prgs-controller",
pid=99,
session_id="sid",
startup_sha="s" * 40,
current_master_sha="c" * 40,
reason="stale-runtime",
)
for key in (
"namespace",
"profile",
"pid",
"session_id",
"startup_sha",
"current_master_sha",
"boundary_status",
):
self.assertIn(key, result)
self.assertIsNotNone(result[key], key)
class ToolSurfaceTests(unittest.TestCase):
"""Exercise gitea_request_mcp_reconnect with a stubbed server context."""
def test_tool_is_registered_and_side_effect_free(self):
import gitea_mcp_server as srv
self.assertTrue(hasattr(srv, "gitea_request_mcp_reconnect"))
with mock.patch.object(srv, "_profile_operation_gate", return_value=None):
with mock.patch.object(
srv,
"get_profile",
return_value={
"profile_name": "prgs-author",
"role_kind": "author",
"role": "author",
},
):
with mock.patch.object(
srv,
"_current_master_parity",
return_value={
"startup_head": "a" * 40,
"current_head": "a" * 40,
"daemon_start_head": "a" * 40,
"local_head": "a" * 40,
"in_parity": True,
"stale": False,
"restart_required": False,
"determinable": True,
"live_stale": False,
"live_known": True,
"reasons": [],
},
):
with mock.patch.object(
srv.master_parity_gate,
"format_parity",
return_value="in parity",
):
with mock.patch.object(
srv.role_namespace_gate,
"infer_mcp_namespace",
return_value="gitea-author",
):
with mock.patch.object(
srv.session_ctx,
"mutation_context_audit_fields",
return_value={"session_profile": "prgs-author"},
):
result = srv.gitea_request_mcp_reconnect(
namespace="gitea-author",
reason="not_required",
client="codex",
remote="prgs",
)
self.assertTrue(result.get("success"))
self.assertFalse(result.get("reconnect_performed"))
self.assertFalse(result.get("mutation_performed"))
self.assertEqual(result.get("namespace"), "gitea-author")
self.assertEqual(result.get("profile"), "prgs-author")
self.assertEqual(result.get("pid"), os.getpid())
self.assertIn("startup_sha", result)
self.assertIn("current_master_sha", result)
self.assertIn("boundary_status", result)
self.assertTrue(
mcr.reasons_never_suggest_forbidden(
result.get("exact_safe_next_action") or ""
)
)
def test_tool_stale_returns_typed_blocker(self):
import gitea_mcp_server as srv
with mock.patch.object(srv, "_profile_operation_gate", return_value=None):
with mock.patch.object(
srv,
"get_profile",
return_value={
"profile_name": "prgs-reconciler",
"role_kind": "reconciler",
"role": "reconciler",
},
):
with mock.patch.object(
srv,
"_current_master_parity",
return_value={
"startup_head": "a" * 40,
"current_head": "b" * 40,
"daemon_start_head": "a" * 40,
"local_head": "b" * 40,
"in_parity": False,
"stale": True,
"restart_required": True,
"determinable": True,
"live_stale": True,
"live_known": True,
"reasons": ["stale"],
},
):
with mock.patch.object(
srv.master_parity_gate,
"format_parity",
return_value="stale",
):
with mock.patch.object(
srv.role_namespace_gate,
"infer_mcp_namespace",
return_value="gitea-reconciler",
):
with mock.patch.object(
srv.session_ctx,
"mutation_context_audit_fields",
return_value={},
):
result = srv.gitea_request_mcp_reconnect(
reason="stale-runtime",
client="codex",
)
self.assertTrue(result["reconnect_needed"])
self.assertEqual(
result["blocker_kind"], mcr.BLOCKER_OPERATOR_RECONNECT
)
self.assertIsNotNone(result["typed_blocker"])
self.assertIn("gitea-reconciler", result["typed_blocker"]["namespaces"])
self.assertTrue(result["stop_required"])
self.assertTrue(result["restart_required"])
self.assertTrue(
mcr.reasons_never_suggest_forbidden(
result.get("exact_safe_next_action") or ""
)
)
class InventoryRegistrationTests(unittest.TestCase):
def test_reconnect_path_in_restart_inventory(self):
import mcp_restart_paths as mrp
ids = {p.path_id for p in mrp.iter_restart_paths()}
self.assertIn("codex_client_reconnect_request", ids)
self.assertIn("ide_client_reconnect", ids)
def test_tool_name_in_documented_inventory(self):
import mcp_tool_inventory as inv
doc_path = os.path.join(
os.path.dirname(os.path.dirname(__file__)), inv.INVENTORY_DOC_PATH
)
with open(doc_path, encoding="utf-8") as handle:
documented = inv.parse_documented_inventory(handle.read())
self.assertIn("gitea_request_mcp_reconnect", documented)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,139 @@
"""Tests for Issue #686: Detect and reject manually launched duplicate MCP role servers."""
import os
import unittest
from unittest.mock import patch, MagicMock
from datetime import datetime
import gitea_config
import gitea_mcp_server
import mcp_namespace_health
class TestIssue686ManualMcpProvenance(unittest.TestCase):
def test_client_managed_process_detection(self):
"""Test _is_client_managed_process correctly detects provenance markers."""
with patch.dict(os.environ, {"GITEA_CLIENT_MANAGED": "1"}, clear=True):
self.assertTrue(gitea_mcp_server._is_client_managed_process())
with patch.dict(os.environ, {"GITEA_MCP_CLIENT_MANAGED": "true"}, clear=True):
self.assertTrue(gitea_mcp_server._is_client_managed_process())
with patch.dict(os.environ, {"GITEA_SERVER_PROVENANCE": "client_managed"}, clear=True):
self.assertTrue(gitea_mcp_server._is_client_managed_process())
with patch.dict(os.environ, {"GITEA_CLIENT_MANAGED": "0"}, clear=True):
self.assertFalse(gitea_mcp_server._is_client_managed_process())
def test_unconsumed_gitea_env_overrides(self):
"""Test surfacing of unsupported GITEA_* env overrides (e.g. GITEA_DUMMY)."""
env = {
"GITEA_MCP_PROFILE": "prgs-author",
"GITEA_CLIENT_MANAGED": "1",
"GITEA_DUMMY": "2",
"GITEA_UNKNOWN_FLAG": "abc",
}
unconsumed = gitea_config.get_unconsumed_gitea_env_overrides(env)
self.assertIn("GITEA_DUMMY", unconsumed)
self.assertEqual(unconsumed["GITEA_DUMMY"], "2")
self.assertIn("GITEA_UNKNOWN_FLAG", unconsumed)
self.assertNotIn("GITEA_MCP_PROFILE", unconsumed)
self.assertNotIn("GITEA_CLIENT_MANAGED", unconsumed)
def test_manual_server_mutation_fail_closed(self):
"""AC 2: Mutating tools on a server without client-managed provenance fail closed with a typed blocker."""
with patch.dict(os.environ, {"GITEA_CLIENT_MANAGED": "0"}, clear=True):
block = gitea_mcp_server._provenance_mutation_block(task="create_issue")
self.assertIsNotNone(block)
self.assertFalse(block["success"])
self.assertFalse(block["performed"])
self.assertEqual(block["blocker_kind"], "unsupported_manual_launch")
self.assertEqual(block["provenance"], "manual_launch")
self.assertTrue(any("mutation denied: server process was launched manually" in r for r in block["reasons"]))
self.assertIn("BLOCKED + RECONNECT", block["exact_next_action"])
def test_client_managed_server_mutation_passes_provenance_gate(self):
"""AC 3: Clean client-managed baseline passes the provenance gate."""
with patch.dict(os.environ, {"GITEA_CLIENT_MANAGED": "1"}, clear=True):
block = gitea_mcp_server._provenance_mutation_block(task="create_issue")
self.assertIsNone(block)
@patch("subprocess.run")
@patch("os.path.getmtime")
@patch("os.path.exists")
@patch("os.getpid")
def test_manual_duplicate_does_not_mask_stale_runtime(
self, mock_getpid, mock_exists, mock_getmtime, mock_run
):
"""AC 1 & AC 3: Staleness detection ignores manual duplicates and reports stale supported runtimes."""
mock_getpid.return_value = 12345
mock_exists.return_value = True
code_time = datetime(2026, 7, 8, 14, 0, 0)
mock_getmtime.return_value = code_time.timestamp()
# PID 12345: stale client-managed process (started at 13:00)
# PID 99999: fresh manual duplicate process (started at 15:00, no GITEA_CLIENT_MANAGED)
ps_output = (
" PID LSTART COMMAND\n"
"12345 Wed Jul 8 13:00:00 2026 /path/to/python mcp_server.py\n"
"99999 Wed Jul 8 15:00:00 2026 /path/to/python mcp_server.py\n"
)
mock_run_ps = MagicMock()
mock_run_ps.stdout = ps_output
mock_env_12345 = MagicMock()
mock_env_12345.stdout = "GITEA_MCP_PROFILE=prgs-author GITEA_CLIENT_MANAGED=1"
mock_env_99999 = MagicMock()
mock_env_99999.stdout = "GITEA_MCP_PROFILE=prgs-author GITEA_DUMMY=2"
def side_effect(args, **kwargs):
if args[0] == "ps" and "eww" in args:
pid = args[2]
if pid == "12345":
return mock_env_12345
elif pid == "99999":
return mock_env_99999
elif args[0] == "ps":
return mock_run_ps
raise ValueError(f"Unexpected args: {args}")
mock_run.side_effect = side_effect
reasons = gitea_mcp_server._check_mcp_runtimes_diagnostics("create_issue", ["prgs-author"])
# Manual duplicate process must be flagged
self.assertTrue(any("Duplicate MCP server process(es) detected" in r for r in reasons))
# Unsupported env override (GITEA_DUMMY=2) must be flagged
self.assertTrue(any("unsupported-env: Unsupported GITEA_* environment variable override(s) detected: GITEA_DUMMY=2" in r for r in reasons))
# Stale runtime must NOT be masked by fresh manual process 99999!
self.assertTrue(any("All matching profiles for task 'create_issue' (['prgs-author']) are running but stale" in r for r in reasons))
def test_namespace_health_classification_includes_provenance(self):
"""AC 1 & 4: mcp_namespace_health diagnostics include provenance and unconsumed_gitea_env."""
process = {
"pid": 5555,
"profile": "prgs-author",
"env": {
"GITEA_MCP_PROFILE": "prgs-author",
"GITEA_DUMMY": "99",
},
}
res = mcp_namespace_health.classify_namespace_probe(
"gitea-author",
configured=True,
registered_tools=["gitea_whoami"],
probe_result={"success": True},
process=process,
probe_source="client_namespace",
)
self.assertEqual(res["provenance"], "manual_launch")
self.assertFalse(res["is_client_managed"])
self.assertEqual(res["unconsumed_gitea_env"], {"GITEA_DUMMY": "99"})
self.assertEqual(res["diagnostics"]["provenance"], "manual_launch")
if __name__ == "__main__":
unittest.main()
@@ -35,6 +35,17 @@ BREAK_GLASS_ENV = "GITEA_BREAKGLASS_RESTART_AUTHORIZATION"
QUIET_SESSIONS: list[dict] = []
QUIET_LEASES: list[dict] = []
# #669: broad restarts need a prior narrow-attempt log (unless break-glass).
PRIOR_NARROW_ATTEMPTS_JSON = json.dumps(
[
{
"action": "client_reconnect",
"outcome": "insufficient",
"reason": "still flapping after reconnect",
}
]
)
class _FakeDB:
"""Minimal control-plane DB stand-in for the restart inventory."""
@@ -128,6 +139,7 @@ class TestConjunction(_RestartToolHarness):
preview = self._call(
role="operator",
restart_class="full_mcp_restart",
prior_recovery_attempts_json=PRIOR_NARROW_ATTEMPTS_JSON,
env={CONTROLLER_APPROVAL_ENV: "operator-approved"},
)
self.assertTrue(preview["allow_restart"],
@@ -136,6 +148,7 @@ class TestConjunction(_RestartToolHarness):
result = self._call(
role="operator",
restart_class="full_mcp_restart",
prior_recovery_attempts_json=PRIOR_NARROW_ATTEMPTS_JSON,
dry_run=False,
drain_proof_json=self._clean_proof_for(preview),
env={CONTROLLER_APPROVAL_ENV: "operator-approved"},
@@ -342,6 +355,7 @@ class TestExistingPathsStillWork(_RestartToolHarness):
result = self._call(
role="operator",
restart_class="full_mcp_restart",
prior_recovery_attempts_json=PRIOR_NARROW_ATTEMPTS_JSON,
dry_run=False,
env={CONTROLLER_APPROVAL_ENV: "operator-approved"},
)
@@ -0,0 +1,215 @@
"""Regression: author worktree bootstrap from clean control checkout (#892).
#892 is the four-door deadlock where every documented recovery path is closed:
bootstrap refuses control, lock demands an existing worktree, worktree-start
demands a lock, and shell worktree add is outside the sanctioned MCP path.
Root cause: assess_author_issue_bootstrap returned allowed/proven for a clean
control checkout, but bootstrap_permits_control_checkout only accepted
create_issue assessments (task_scope=create_issue_only + empty reasons + full
base-tip field set). Author assessments never satisfied the shared predicate,
so the #274/#604 guards kept the ordinary control-checkout block.
"""
from __future__ import annotations
import os
import tempfile
import unittest
from unittest import mock
import author_issue_bootstrap as aib
import create_issue_bootstrap as cib
CONTROL = "/repo/Gitea-Tools"
MASTER = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
OTHER = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
def _assess(
*,
workspace=CONTROL,
root=CONTROL,
branch="master",
head=MASTER,
porcelain="",
remote=MASTER,
remote_error=None,
task="bootstrap_author_issue_worktree",
):
return aib.assess_author_issue_bootstrap(
workspace_path=workspace,
canonical_repo_root=root,
current_branch=branch,
head_sha=head,
porcelain_status=porcelain,
remote_master_sha=remote,
remote_master_sha_error=remote_error,
task=task,
)
class TestAuthorBootstrapAssessmentShape(unittest.TestCase):
def test_clean_control_emits_predicate_compatible_fields(self):
assessment = _assess()
self.assertTrue(assessment["allowed"])
self.assertTrue(assessment["proven"])
self.assertFalse(assessment["block"])
self.assertFalse(assessment["not_applicable"])
self.assertEqual(assessment["reasons"], [])
self.assertEqual(assessment["task_scope"], "author_issue_bootstrap")
self.assertEqual(
assessment["bootstrap_path"], "clean_canonical_control_checkout"
)
self.assertEqual(assessment["dirty_files"], [])
self.assertIs(assessment["under_branches"], False)
self.assertTrue(assessment["base_tips_verified"])
self.assertEqual(assessment["local_head_sha"], MASTER)
self.assertEqual(assessment["remote_master_sha"], MASTER)
self.assertEqual(assessment["workspace_path"], os.path.realpath(CONTROL))
self.assertEqual(
assessment["canonical_repo_root"], os.path.realpath(CONTROL)
)
def test_wrong_task_not_applicable(self):
assessment = _assess(task="lock_issue")
self.assertTrue(assessment["not_applicable"])
self.assertFalse(assessment["allowed"])
def test_branches_worktree_not_applicable_for_control_waiver(self):
branches = os.path.join(CONTROL, "branches", "fix-issue-1")
assessment = _assess(workspace=branches)
self.assertTrue(assessment["not_applicable"])
self.assertFalse(assessment["allowed"])
self.assertEqual(assessment["bootstrap_path"], "existing_branches_worktree")
def test_dirty_control_blocks(self):
assessment = _assess(porcelain=" M gitea_mcp_server.py\n")
self.assertTrue(assessment["block"])
self.assertFalse(assessment["allowed"])
self.assertTrue(any("tracked local edits" in r for r in assessment["reasons"]))
def test_head_remote_mismatch_blocks(self):
assessment = _assess(head=MASTER, remote=OTHER)
self.assertTrue(assessment["block"])
self.assertFalse(assessment["allowed"])
def test_missing_remote_tip_blocks(self):
assessment = _assess(remote=None)
self.assertTrue(assessment["block"])
self.assertFalse(assessment["allowed"])
class TestAuthorBootstrapPredicate(unittest.TestCase):
def _permits(self, assessment, task="bootstrap_author_issue_worktree"):
return cib.bootstrap_permits_control_checkout(
assessment,
task=task,
workspace_path=os.path.realpath(CONTROL),
canonical_repo_root=os.path.realpath(CONTROL),
)
def test_clean_author_bootstrap_permits(self):
self.assertTrue(self._permits(_assess()))
def test_tool_alias_permits(self):
assessment = _assess(task="gitea_bootstrap_author_issue_worktree")
self.assertTrue(
self._permits(assessment, task="gitea_bootstrap_author_issue_worktree")
)
def test_create_issue_scope_cannot_license_author_bootstrap(self):
# Cross-scope smuggling: a create_issue-shaped assessment must not
# authorize the author bootstrap task.
create_shaped = dict(_assess())
create_shaped["task_scope"] = "create_issue_only"
self.assertFalse(self._permits(create_shaped))
def test_author_scope_cannot_license_create_issue(self):
assessment = _assess()
self.assertFalse(
cib.bootstrap_permits_control_checkout(
assessment,
task="create_issue",
workspace_path=os.path.realpath(CONTROL),
canonical_repo_root=os.path.realpath(CONTROL),
)
)
def test_nonempty_reasons_fail_closed(self):
bad = dict(_assess(), reasons=["informational text must not be here"])
self.assertFalse(self._permits(bad))
def test_dirty_fails_closed(self):
self.assertFalse(self._permits(_assess(porcelain=" M x.py\n")))
def test_mismatch_fails_closed(self):
self.assertFalse(self._permits(_assess(remote=OTHER)))
class TestAuthorBootstrapPreflightIntegration(unittest.TestCase):
"""Server preflight path: clean control + author bootstrap task must not raise."""
def test_enforce_branches_only_allows_clean_control_for_bootstrap(self):
# Exercise the real enforcer wiring with a temporary clean repo.
import gitea_mcp_server as srv
with tempfile.TemporaryDirectory() as tmp:
repo = os.path.join(tmp, "repo")
os.makedirs(os.path.join(repo, "branches"))
# Minimal git repo on master at a known tip.
import subprocess
subprocess.check_call(["git", "init", "-b", "master", repo])
subprocess.check_call(
["git", "-C", repo, "commit", "--allow-empty", "-m", "init"]
)
head = subprocess.check_output(
["git", "-C", repo, "rev-parse", "HEAD"], text=True
).strip()
assessment = aib.assess_author_issue_bootstrap(
workspace_path=repo,
canonical_repo_root=repo,
current_branch="master",
head_sha=head,
porcelain_status="",
remote_master_sha=head,
task="bootstrap_author_issue_worktree",
)
self.assertTrue(
cib.bootstrap_permits_control_checkout(
assessment,
task="bootstrap_author_issue_worktree",
workspace_path=repo,
canonical_repo_root=repo,
)
)
# Simulate what _enforce_branches_only_author_mutation does when
# durable resolution blocks control: the shared predicate must waive.
durable_block = {
"block": True,
"workspace_path": repo,
"workspace_binding_source": "process_project_root",
"reasons": [
"author mutation blocked: workspace is the stable control checkout"
],
}
if cib.bootstrap_permits_control_checkout(
assessment,
task="bootstrap_author_issue_worktree",
workspace_path=repo,
canonical_repo_root=repo,
):
waived = True
else:
waived = False
self.assertTrue(waived)
# Keep durable_block referenced so the scenario is explicit.
self.assertTrue(durable_block["block"])
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,346 @@
"""Regression: author bootstrap scope reaches workflow_scope_guard (#941).
PR #926 (#892) made ``bootstrap_permits_control_checkout`` accept
``task_scope=author_issue_bootstrap`` and wired that canonical decision into
the #274 branches-only enforcer and the #604 anti-stomp preflight. A third
enforcement path was left unwired.
``workflow_scope_guard.assess_root_source_mutation`` kept its own copy of the
clean-root author decision, gated on ``create_issue_bootstrap.is_create_issue_task``
— a task-name allowlist that never contained ``bootstrap_author_issue_worktree``.
So the real call path
gitea_bootstrap_author_issue_worktree
-> verify_preflight_purity
-> _enforce_issue_scope_guard
-> workflow_scope_guard.assess_production_mutation_guards
raised ProductionGuardError(missing_issue_worktree) before
``assess_author_issue_bootstrap`` was ever consulted.
These tests drive the real enforcer, not the authorization helper in
isolation. A helper-only test cannot observe this defect: #892's own predicate
tests all passed while the live bootstrap stayed blocked.
"""
from __future__ import annotations
import os
import subprocess
import tempfile
import unittest
from unittest import mock
import author_issue_bootstrap as aib
import create_issue_bootstrap as cib
import workflow_scope_guard
BOOTSTRAP_TASK = "bootstrap_author_issue_worktree"
BOOTSTRAP_TOOL = "gitea_bootstrap_author_issue_worktree"
def _make_control_repo(tmp: str) -> tuple[str, str]:
"""Create a clean control checkout on master and return (path, head)."""
repo = os.path.join(tmp, "repo")
os.makedirs(os.path.join(repo, "branches"))
subprocess.check_call(
["git", "init", "-b", "master", repo],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
subprocess.check_call(
[
"git", "-C", repo,
"-c", "user.email=t@t", "-c", "user.name=t",
"commit", "--allow-empty", "-m", "init",
],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
head = subprocess.check_output(
["git", "-C", repo, "rev-parse", "HEAD"], text=True
).strip()
return repo, head
def _assessment(
repo: str,
head: str,
*,
task: str = BOOTSTRAP_TASK,
porcelain: str = "",
remote: str | None = None,
) -> dict:
return aib.assess_author_issue_bootstrap(
workspace_path=repo,
canonical_repo_root=repo,
current_branch="master",
head_sha=head,
porcelain_status=porcelain,
remote_master_sha=head if remote is None else remote,
task=task,
)
class _ControlCheckoutHarness(unittest.TestCase):
"""Drive the real server guard against a temporary clean control checkout."""
def setUp(self):
self._tmp = tempfile.TemporaryDirectory()
self.addCleanup(self._tmp.cleanup)
self.repo, self.head = _make_control_repo(self._tmp.name)
# #683 force-on: production guards must execute under pytest.
patcher = mock.patch.dict(
os.environ,
{workflow_scope_guard.FORCE_PRODUCTION_GUARDS_ENV: "1"},
)
patcher.start()
self.addCleanup(patcher.stop)
def _enforce(
self,
task: str,
*,
porcelain: str = "",
assessment: object = "auto",
role_kind: str = "author",
):
"""Call the real _enforce_issue_scope_guard for *task*."""
import gitea_mcp_server as srv
if assessment == "auto":
assessment = _assessment(
self.repo, self.head, task=task, porcelain=porcelain
)
ctx = {
"workspace_path": self.repo,
"canonical_repo_root": self.repo,
"workspace_role_kind": role_kind,
"workspace_binding_source": "process_project_root",
}
git_state = {
"current_branch": "master",
"head_sha": self.head,
"porcelain_status": porcelain,
}
with mock.patch.object(
srv, "_resolve_namespace_mutation_context", return_value=ctx
), mock.patch.object(
srv.issue_lock_worktree,
"read_worktree_git_state",
return_value=git_state,
), mock.patch.object(
srv,
"_session_issue_lock_snapshot",
return_value={
"locked_issue_number": None,
"lock_branch_name": None,
"worktrees_match": False,
},
), mock.patch.object(
srv, "_actual_profile_role", return_value=role_kind
), mock.patch.object(
srv, "_effective_workspace_role", return_value=role_kind
), mock.patch.object(
srv, "_create_issue_bootstrap_assessment", return_value=assessment
):
srv._enforce_issue_scope_guard(None, task=task)
class TestRealPathBootstrapReachesGuard(_ControlCheckoutHarness):
"""The defect and its fix, observed through the real enforcer."""
def test_bootstrap_task_passes_scope_guard_from_clean_control(self):
# Pre-fix this raises ProductionGuardError(missing_issue_worktree)
# because the guard consulted a task-name allowlist instead of the
# canonical authorization decision.
self._enforce(BOOTSTRAP_TASK)
def test_bootstrap_tool_alias_passes_scope_guard(self):
self._enforce(BOOTSTRAP_TOOL)
def test_guard_consults_canonical_predicate(self):
"""The guard must reach bootstrap_permits_control_checkout, not a name list."""
real = cib.bootstrap_permits_control_checkout
seen: list[str | None] = []
def _spy(assessment, *, task, workspace_path, canonical_repo_root):
seen.append(task)
return real(
assessment,
task=task,
workspace_path=workspace_path,
canonical_repo_root=canonical_repo_root,
)
with mock.patch.object(
cib, "bootstrap_permits_control_checkout", side_effect=_spy
):
self._enforce(BOOTSTRAP_TASK)
self.assertIn(
BOOTSTRAP_TASK,
seen,
"workflow_scope_guard did not consult the canonical bootstrap "
"authorization decision",
)
class TestFailClosedOnBadEvidence(_ControlCheckoutHarness):
"""Missing, malformed, or mismatched scope evidence must still block."""
def _assert_blocked(self, **kwargs):
with self.assertRaises(workflow_scope_guard.ProductionGuardError):
self._enforce(BOOTSTRAP_TASK, **kwargs)
def test_missing_assessment_fails_closed(self):
self._assert_blocked(assessment=None)
def test_malformed_assessment_fails_closed(self):
self._assert_blocked(assessment={"allowed": True})
def test_non_dict_assessment_fails_closed(self):
self._assert_blocked(assessment="allowed")
def test_wrong_task_scope_fails_closed(self):
bad = dict(_assessment(self.repo, self.head))
bad["task_scope"] = "create_issue_only"
self._assert_blocked(assessment=bad)
def test_nonempty_reasons_fail_closed(self):
bad = dict(_assessment(self.repo, self.head), reasons=["note"])
self._assert_blocked(assessment=bad)
def test_mismatched_base_tips_fail_closed(self):
bad = dict(_assessment(self.repo, self.head))
bad["remote_master_sha"] = "b" * 40
self._assert_blocked(assessment=bad)
def test_unverified_base_tips_fail_closed(self):
bad = dict(_assessment(self.repo, self.head), base_tips_verified=False)
self._assert_blocked(assessment=bad)
def test_mismatched_workspace_binding_fails_closed(self):
bad = dict(_assessment(self.repo, self.head))
bad["workspace_path"] = os.path.join(self.repo, "elsewhere")
self._assert_blocked(assessment=bad)
def test_mismatched_repo_root_binding_fails_closed(self):
bad = dict(_assessment(self.repo, self.head))
bad["canonical_repo_root"] = os.path.join(self.repo, "other-root")
self._assert_blocked(assessment=bad)
def test_blocked_assessment_fails_closed(self):
bad = dict(_assessment(self.repo, self.head), block=True, allowed=False)
self._assert_blocked(assessment=bad)
class TestOrdinaryControlCheckoutMutationStillForbidden(_ControlCheckoutHarness):
"""The waiver must not leak to ordinary author work."""
def test_ordinary_author_task_still_blocked(self):
with self.assertRaises(workflow_scope_guard.ProductionGuardError):
self._enforce("commit_files", assessment=None)
def test_lock_issue_still_blocked_from_control(self):
with self.assertRaises(workflow_scope_guard.ProductionGuardError):
self._enforce("lock_issue", assessment=None)
def test_bootstrap_assessment_cannot_license_other_task(self):
# Cross-task smuggling: valid bootstrap evidence must not waive a
# different author mutation.
good = _assessment(self.repo, self.head)
with self.assertRaises(workflow_scope_guard.ProductionGuardError):
self._enforce("commit_files", assessment=good)
def test_dirty_control_checkout_still_blocked_for_bootstrap(self):
with self.assertRaises(workflow_scope_guard.ProductionGuardError):
self._enforce(BOOTSTRAP_TASK, porcelain=" M gitea_mcp_server.py\n")
class TestCreateIssueBehaviorUnchanged(_ControlCheckoutHarness):
"""#749 create_issue keeps its own sanctioned path."""
def test_create_issue_still_allowed_from_clean_control(self):
self._enforce("create_issue", assessment=None)
def test_create_issue_tool_alias_still_allowed(self):
self._enforce("gitea_create_issue", assessment=None)
def test_create_issue_blocked_when_control_dirty(self):
with self.assertRaises(workflow_scope_guard.ProductionGuardError):
self._enforce(
"create_issue",
porcelain=" M gitea_mcp_server.py\n",
assessment=None,
)
class TestGuardUnitLevelWiring(unittest.TestCase):
"""assess_root_source_mutation itself must accept and honour the evidence."""
def setUp(self):
self._tmp = tempfile.TemporaryDirectory()
self.addCleanup(self._tmp.cleanup)
self.repo, self.head = _make_control_repo(self._tmp.name)
patcher = mock.patch.dict(
os.environ,
{workflow_scope_guard.FORCE_PRODUCTION_GUARDS_ENV: "1"},
)
patcher.start()
self.addCleanup(patcher.stop)
def _assess(self, *, task=BOOTSTRAP_TASK, bootstrap_assessment="auto"):
if bootstrap_assessment == "auto":
bootstrap_assessment = _assessment(self.repo, self.head, task=task)
return workflow_scope_guard.assess_root_source_mutation(
workspace_path=self.repo,
canonical_repo_root=self.repo,
porcelain_status="",
current_branch="master",
role_kind="author",
mutation_task=task,
bootstrap_assessment=bootstrap_assessment,
)
def test_valid_evidence_unblocks(self):
result = self._assess()
self.assertFalse(result["block"])
self.assertIsNone(result["blocker_kind"])
def test_absent_evidence_blocks(self):
result = self._assess(bootstrap_assessment=None)
self.assertTrue(result["block"])
self.assertEqual(
result["blocker_kind"], workflow_scope_guard.BLOCKER_MISSING_WORKTREE
)
def test_reconciler_exemption_preserved(self):
result = workflow_scope_guard.assess_root_source_mutation(
workspace_path=self.repo,
canonical_repo_root=self.repo,
porcelain_status="",
current_branch="master",
role_kind="reconciler",
mutation_task=BOOTSTRAP_TASK,
)
self.assertFalse(result["block"])
def test_signature_accepts_evidence_without_it_being_required(self):
# Callers that supply no evidence keep the pre-existing behaviour.
result = workflow_scope_guard.assess_root_source_mutation(
workspace_path=self.repo,
canonical_repo_root=self.repo,
porcelain_status="",
current_branch="master",
role_kind="author",
mutation_task="create_issue",
)
self.assertFalse(result["block"])
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,747 @@
"""Regression: author bootstrap runtime authority and session ownership (#943).
Two rounds of defects live here.
**Round 1 (#943 as filed).** ``gitea_bootstrap_author_issue_worktree`` passed
four values down to the bootstrap service that were never defined:
``_active_username``, ``_active_profile_name``, ``_current_session_id`` and
``_author_mutation_block``. Every call — dry-run included — raised
``NameError`` while evaluating the arguments, before the service was entered.
**Round 2 (review 622 on PR #944).** The first fix defined all four but made
``_current_session_id`` mint ``<profile>-<pid>-<hex>`` once per process. The MCP
daemon outlives every task it serves, so that value conflates sequential author
tasks and can never equal the control-plane session that owns an
allocator-created lease: ``_verify_assignment_and_lease_ids`` refused the whole
allocated path with ``lease_session_mismatch``. The reviewed round also read the
identity from the pinned session context while reading the profile from the live
profile, so a rebind could produce a mixed claimant pair, and it swallowed every
``get_profile()`` exception.
These tests therefore drive real state, not mocks of internals: a temporary
control-plane SQLite database and a temporary issue-lock directory, both
redirected through the same environment variables production uses
(``GITEA_CONTROL_PLANE_DB``, ``GITEA_ISSUE_LOCK_DIR``). The ownership gate that
runs is the real one.
``test_every_global_referenced_by_the_wrapper_resolves`` remains: it is what
found ``_author_mutation_block``, and it generalises to the next missing
reference. It supplements the runtime coverage below rather than standing in for
it.
"""
from __future__ import annotations
import ast
import builtins
import os
import re
import subprocess
import tempfile
import unittest
from unittest import mock
import author_issue_bootstrap as aib
import control_plane_db
import create_issue_bootstrap as cib
import gitea_mcp_server as gms
import issue_lock_store
import workflow_scope_guard
BOOTSTRAP_TASK = "bootstrap_author_issue_worktree"
WRAPPER_NAME = "gitea_bootstrap_author_issue_worktree"
RUNTIME_HELPERS = (
"_active_mutation_authority",
"_active_username",
"_active_profile_name",
"_resolve_owner_workflow_session",
"_author_mutation_block",
)
ORG = "Scaled-Tech-Consulting"
REPO = "Gitea-Tools"
IDENTITY = "jcwalker3"
PROFILE = "prgs-author"
# A per-task ownership key must carry no process identifier (#790).
TASK_KEY_RE = re.compile(r"^author_issue_work-[0-9a-f]{16}$")
def _make_control_repo(tmp: str) -> tuple[str, str]:
"""Create a clean control checkout on master and return (path, head)."""
repo = os.path.join(tmp, "repo")
os.makedirs(os.path.join(repo, "branches"))
subprocess.check_call(
["git", "init", "-b", "master", repo],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
subprocess.check_call(
[
"git", "-C", repo,
"-c", "user.email=t@t", "-c", "user.name=t",
"commit", "--allow-empty", "-m", "init",
],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
head = subprocess.check_output(
["git", "-C", repo, "rev-parse", "HEAD"], text=True
).strip()
return repo, head
def _wrapper_ast() -> ast.FunctionDef:
"""Return the AST of the bootstrap wrapper as it exists on disk."""
path = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
"gitea_mcp_server.py",
)
with open(path, encoding="utf-8") as fh:
tree = ast.parse(fh.read())
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef) and node.name == WRAPPER_NAME:
return node
raise AssertionError(f"{WRAPPER_NAME} not found in gitea_mcp_server.py")
class _IsolatedControlPlane(unittest.TestCase):
"""Temp control-plane DB and temp issue-lock dir, via production env vars."""
def setUp(self):
self._tmp = tempfile.TemporaryDirectory()
self.addCleanup(self._tmp.cleanup)
self.tmp = self._tmp.name
self.db_path = os.path.join(self.tmp, "control-plane.sqlite3")
self.lock_dir = os.path.join(self.tmp, "issue-locks")
self.journals = os.path.join(self.tmp, "journals")
os.makedirs(self.lock_dir)
os.makedirs(self.journals)
env = mock.patch.dict(
os.environ,
{
control_plane_db.DB_PATH_ENV: self.db_path,
issue_lock_store.LOCK_DIR_ENV: self.lock_dir,
},
)
env.start()
self.addCleanup(env.stop)
self.db = control_plane_db.ControlPlaneDB(self.db_path)
def _allocate(self, session_id: str, *, issue: int = 943):
"""Create a real assignment + lease owned by *session_id*."""
self.db.upsert_session(
session_id=session_id, role="author", profile=PROFILE, pid=os.getpid()
)
res = self.db.assign_and_lease(
session_id=session_id, role="author", remote="prgs",
org=ORG, repo=REPO, kind="issue", number=issue,
)
self.assertEqual(res.outcome, "assigned", res)
return res.assignment_id, res.lease_id
def _authority(self):
"""A resolved authority pair, as the wrapper would compute it."""
return {"ok": True, "identity": IDENTITY, "profile_name": PROFILE}
def _resolve_session(self, **over):
kwargs = dict(
issue_number=943,
assignment_id=None,
lease_id=None,
session_id=None,
identity=IDENTITY,
profile_name=PROFILE,
remote="prgs",
org=ORG,
repo=REPO,
)
kwargs.update(over)
return gms._resolve_owner_workflow_session(**kwargs)
class OwnershipGateTests(_IsolatedControlPlane):
"""B2: the allocator-driven ownership path, against a real control plane."""
def setUp(self):
super().setUp()
self.repo, self.head = _make_control_repo(self.tmp)
def _bootstrap(self, **over):
kwargs = dict(
issue_number=943,
canonical_repo_root=self.repo,
expected_base_sha=self.head,
branch_name="fix/issue-943-runtime-context-helpers",
remote="prgs",
org=ORG,
repo=REPO,
active_identity=IDENTITY,
active_profile=PROFILE,
lock_dir=self.journals,
idempotency_key="test-943",
dry_run=True,
)
kwargs.update(over)
return aib.bootstrap_author_issue_worktree(**kwargs)
def test_true_owning_session_passes_the_ownership_gate(self):
"""The canonical owner reaches and completes the service."""
session = "prgs-author-task-a"
assignment_id, lease_id = self._allocate(session)
res = self._bootstrap(
assignment_id=assignment_id, lease_id=lease_id, owner_session=session
)
self.assertTrue(res.get("success"), res)
self.assertTrue(res.get("dry_run"))
self.assertEqual(res.get("base_sha"), self.head)
def test_different_session_is_refused(self):
session = "prgs-author-task-a"
assignment_id, lease_id = self._allocate(session)
res = self._bootstrap(
assignment_id=assignment_id,
lease_id=lease_id,
owner_session="prgs-author-task-b",
)
self.assertFalse(res.get("success"))
self.assertEqual(res.get("reason_code"), "lease_session_mismatch")
def test_process_derived_session_would_be_refused(self):
"""The reviewed round-1 value shape can never own an allocated lease."""
session = "prgs-author-task-a"
assignment_id, lease_id = self._allocate(session)
round_one_value = f"{PROFILE}-{os.getpid()}-deadbeef"
self.assertNotEqual(round_one_value, session)
res = self._bootstrap(
assignment_id=assignment_id,
lease_id=lease_id,
owner_session=round_one_value,
)
self.assertFalse(res.get("success"))
self.assertEqual(res.get("reason_code"), "lease_session_mismatch")
def test_unknown_lease_fails_closed(self):
session = "prgs-author-task-a"
assignment_id, _ = self._allocate(session)
res = self._bootstrap(
assignment_id=assignment_id,
lease_id="lease-does-not-exist",
owner_session=session,
)
self.assertFalse(res.get("success"))
self.assertEqual(res.get("reason_code"), "unknown_lease_id")
def test_released_lease_fails_closed(self):
session = "prgs-author-task-a"
assignment_id, lease_id = self._allocate(session)
self.db.release_lease(lease_id, session_id=session)
res = self._bootstrap(
assignment_id=assignment_id, lease_id=lease_id, owner_session=session
)
self.assertFalse(res.get("success"))
self.assertEqual(res.get("reason_code"), "lease_not_live")
def test_force_expired_lease_fails_closed(self):
session = "prgs-author-task-a"
assignment_id, lease_id = self._allocate(session)
self.db.force_expire_lease(lease_id, reason="test")
res = self._bootstrap(
assignment_id=assignment_id, lease_id=lease_id, owner_session=session
)
self.assertFalse(res.get("success"))
self.assertEqual(res.get("reason_code"), "lease_not_live")
def test_replacement_lease_does_not_inherit_prior_ownership(self):
"""A second task's lease is not ownable by the first task's session."""
first = "prgs-author-task-a"
assignment_a, lease_a = self._allocate(first)
self.db.release_lease(lease_a, session_id=first)
second = "prgs-author-task-b"
assignment_b, lease_b = self._allocate(second)
self.assertNotEqual(lease_a, lease_b)
res = self._bootstrap(
assignment_id=assignment_b, lease_id=lease_b, owner_session=first
)
self.assertFalse(res.get("success"))
self.assertEqual(res.get("reason_code"), "lease_session_mismatch")
def test_assignment_lease_identifier_mismatch_fails_closed(self):
session = "prgs-author-task-a"
_, lease_id = self._allocate(session)
res = self._bootstrap(
assignment_id="asn-not-the-recorded-one",
lease_id=lease_id,
owner_session=session,
)
self.assertFalse(res.get("success"))
self.assertEqual(res.get("reason_code"), "assignment_lease_mismatch")
def test_lease_id_without_assignment_id_fails_closed(self):
session = "prgs-author-task-a"
_, lease_id = self._allocate(session)
res = self._bootstrap(lease_id=lease_id, owner_session=session)
self.assertFalse(res.get("success"))
self.assertEqual(res.get("reason_code"), "incomplete_assignment_lease_ids")
def test_dry_run_with_valid_allocator_bindings_leaves_no_durable_state(self):
session = "prgs-author-task-a"
assignment_id, lease_id = self._allocate(session)
res = self._bootstrap(
assignment_id=assignment_id, lease_id=lease_id, owner_session=session
)
self.assertTrue(res.get("success"), res)
branches = subprocess.check_output(
["git", "-C", self.repo, "branch", "--list"], text=True
)
self.assertNotIn("issue-943", branches)
worktrees = subprocess.check_output(
["git", "-C", self.repo, "worktree", "list"], text=True
)
self.assertNotIn("issue-943", worktrees)
self.assertFalse(
os.path.exists(
os.path.join(self.repo, "branches",
"fix-issue-943-runtime-context-helpers")
)
)
journal = res.get("phase_journal") or {}
self.assertFalse(journal.get("completed"))
self.assertFalse(any((journal.get("artifacts_created") or {}).values()))
# The dry run must not have created an issue lock in the isolated dir.
self.assertEqual(os.listdir(self.lock_dir), [])
def test_apply_reaches_the_intended_transition_with_valid_bindings(self):
session = "prgs-author-task-a"
assignment_id, lease_id = self._allocate(session)
res = self._bootstrap(
assignment_id=assignment_id,
lease_id=lease_id,
owner_session=session,
dry_run=False,
)
self.assertTrue(res.get("success"), res)
self.assertNotEqual(res.get("dry_run"), True)
branches = subprocess.check_output(
["git", "-C", self.repo, "branch", "--list"], text=True
)
self.assertIn("issue-943", branches)
self.assertTrue(os.path.isdir(res.get("worktree_path") or ""))
class WorkflowSessionResolutionTests(_IsolatedControlPlane):
"""B1: the wrapper resolves the owning session, never a process identifier."""
def test_declared_session_is_verified_against_the_control_plane(self):
session = "prgs-author-task-a"
assignment_id, lease_id = self._allocate(session)
res = self._resolve_session(
session_id=session, assignment_id=assignment_id, lease_id=lease_id
)
self.assertTrue(res.get("ok"), res)
self.assertEqual(res.get("session_id"), session)
self.assertEqual(res.get("session_source"), "declared")
def test_unknown_declared_session_is_refused_not_trusted(self):
res = self._resolve_session(session_id="prgs-author-not-a-session")
self.assertFalse(res.get("ok"))
self.assertEqual(res.get("reason_code"), "workflow_session_unverified")
def test_declared_session_for_another_role_is_refused(self):
self.db.upsert_session(
session_id="prgs-reviewer-x", role="reviewer", profile="prgs-reviewer",
pid=os.getpid(),
)
res = self._resolve_session(session_id="prgs-reviewer-x")
self.assertFalse(res.get("ok"))
self.assertEqual(res.get("reason_code"), "workflow_session_unverified")
def test_declared_session_for_another_profile_is_refused(self):
self.db.upsert_session(
session_id="other-profile-session", role="author",
profile="prgs-controller", pid=os.getpid(),
)
res = self._resolve_session(session_id="other-profile-session")
self.assertFalse(res.get("ok"))
self.assertEqual(res.get("reason_code"), "workflow_session_unverified")
def test_allocated_work_without_a_session_is_refused(self):
"""Supplying a lease is not itself evidence of ownership."""
session = "prgs-author-task-a"
assignment_id, lease_id = self._allocate(session)
res = self._resolve_session(assignment_id=assignment_id, lease_id=lease_id)
self.assertFalse(res.get("ok"))
self.assertEqual(
res.get("reason_code"), "workflow_session_required_for_allocated_work"
)
def test_existing_issue_lock_supplies_its_per_task_session(self):
lock_session = issue_lock_store.mint_task_session_id(
issue_lock_store.AUTHOR_ISSUE_WORK_LEASE
)
path = issue_lock_store.lock_file_path(
remote="prgs", org=ORG, repo=REPO, issue_number=943,
lock_dir=self.lock_dir,
)
issue_lock_store.write_lock_file(
path,
{
"issue_number": 943,
"branch_name": "fix/issue-943-runtime-context-helpers",
"work_lease": {
"task_session_id": lock_session,
"claimant": {"username": IDENTITY, "profile": PROFILE},
},
},
) if hasattr(issue_lock_store, "write_lock_file") else _write_json(
path,
{
"issue_number": 943,
"branch_name": "fix/issue-943-runtime-context-helpers",
"work_lease": {
"task_session_id": lock_session,
"claimant": {"username": IDENTITY, "profile": PROFILE},
},
},
)
res = self._resolve_session()
self.assertTrue(res.get("ok"), res)
self.assertEqual(res.get("session_id"), lock_session)
self.assertEqual(res.get("session_source"), "issue_lock")
def test_issue_lock_owned_by_another_identity_is_refused(self):
path = issue_lock_store.lock_file_path(
remote="prgs", org=ORG, repo=REPO, issue_number=943,
lock_dir=self.lock_dir,
)
_write_json(
path,
{
"issue_number": 943,
"work_lease": {
"task_session_id": "author_issue_work-" + "0" * 16,
"claimant": {"username": "someone-else", "profile": PROFILE},
},
},
)
res = self._resolve_session()
self.assertFalse(res.get("ok"))
self.assertEqual(res.get("reason_code"), "issue_lock_owner_mismatch")
def test_unallocated_bootstrap_mints_a_per_task_key(self):
res = self._resolve_session()
self.assertTrue(res.get("ok"), res)
self.assertEqual(res.get("session_source"), "minted_task_key")
self.assertRegex(res["session_id"], TASK_KEY_RE)
def test_minted_key_contains_no_process_identifier(self):
res = self._resolve_session()
self.assertNotIn(str(os.getpid()), res["session_id"])
self.assertNotIn(PROFILE, res["session_id"])
def test_sequential_tasks_on_one_daemon_do_not_share_ownership(self):
"""The round-1 defect: one identifier per process for every task."""
first = self._resolve_session()["session_id"]
second = self._resolve_session()["session_id"]
third = self._resolve_session()["session_id"]
self.assertNotEqual(first, second)
self.assertNotEqual(second, third)
self.assertEqual(len({first, second, third}), 3)
def test_no_process_lifetime_cache_remains(self):
self.assertFalse(hasattr(gms, "_ACTIVE_SESSION_ID"))
self.assertFalse(hasattr(gms, "_current_session_id"))
class MutationAuthorityTests(unittest.TestCase):
"""F3/F4: one coherent authority pair, drift detected, no silent fallback."""
def _ctx(self, **over):
base = {"identity": IDENTITY, "profile_name": PROFILE}
base.update(over)
return base
def test_matching_live_and_pinned_authority_resolves(self):
with mock.patch.object(gms, "get_profile",
return_value={"profile_name": PROFILE}), \
mock.patch.object(gms, "_authenticated_username",
return_value=IDENTITY), \
mock.patch.object(gms.session_ctx, "get_session_context",
return_value=self._ctx()):
res = gms._active_mutation_authority("gitea.prgs.cc")
self.assertTrue(res.get("ok"), res)
self.assertEqual(res["identity"], IDENTITY)
self.assertEqual(res["profile_name"], PROFILE)
def test_identity_drift_fails_closed(self):
with mock.patch.object(gms, "get_profile",
return_value={"profile_name": PROFILE}), \
mock.patch.object(gms, "_authenticated_username",
return_value="someone-else"), \
mock.patch.object(gms.session_ctx, "get_session_context",
return_value=self._ctx()):
res = gms._active_mutation_authority("gitea.prgs.cc")
self.assertFalse(res.get("ok"))
self.assertEqual(res.get("reason_code"), "authority_identity_drift")
self.assertEqual(res.get("expected"), IDENTITY)
self.assertEqual(res.get("actual"), "someone-else")
def test_profile_drift_fails_closed(self):
with mock.patch.object(gms, "get_profile",
return_value={"profile_name": "prgs-controller"}), \
mock.patch.object(gms, "_authenticated_username",
return_value=IDENTITY), \
mock.patch.object(gms.session_ctx, "get_session_context",
return_value=self._ctx()):
res = gms._active_mutation_authority("gitea.prgs.cc")
self.assertFalse(res.get("ok"))
self.assertEqual(res.get("reason_code"), "authority_profile_drift")
def test_identity_and_profile_never_come_from_different_snapshots(self):
"""Round 2's mixed pair: pinned identity plus live profile."""
with mock.patch.object(gms, "get_profile",
return_value={"profile_name": "prgs-controller"}), \
mock.patch.object(gms, "_authenticated_username",
return_value="new-identity"), \
mock.patch.object(gms.session_ctx, "get_session_context",
return_value=self._ctx()):
res = gms._active_mutation_authority("gitea.prgs.cc")
self.assertFalse(res.get("ok"))
self.assertIsNone(gms._active_username("gitea.prgs.cc"))
self.assertIsNone(gms._active_profile_name("gitea.prgs.cc"))
def test_unresolvable_profile_is_a_structured_refusal_not_a_fallback(self):
"""F4: no bare-except fallback to a previously pinned profile name."""
with mock.patch.object(gms, "get_profile",
side_effect=RuntimeError("profile disabled")), \
mock.patch.object(gms, "_authenticated_username",
return_value=IDENTITY), \
mock.patch.object(gms.session_ctx, "get_session_context",
return_value=self._ctx()):
res = gms._active_mutation_authority("gitea.prgs.cc")
self.assertFalse(res.get("ok"))
self.assertEqual(res.get("reason_code"), "authority_profile_unresolved")
self.assertNotEqual(res.get("profile_name"), PROFILE)
def test_malformed_profile_without_name_fails_closed(self):
with mock.patch.object(gms, "get_profile", return_value={}), \
mock.patch.object(gms, "_authenticated_username",
return_value=IDENTITY), \
mock.patch.object(gms.session_ctx, "get_session_context",
return_value=None):
res = gms._active_mutation_authority("gitea.prgs.cc")
self.assertFalse(res.get("ok"))
self.assertEqual(res.get("reason_code"), "authority_profile_unresolved")
def test_unresolved_identity_fails_closed(self):
for value in (None, "", " "):
with self.subTest(identity=value):
with mock.patch.object(gms, "get_profile",
return_value={"profile_name": PROFILE}), \
mock.patch.object(gms, "_authenticated_username",
return_value=value), \
mock.patch.object(gms.session_ctx, "get_session_context",
return_value=None):
res = gms._active_mutation_authority("gitea.prgs.cc")
self.assertFalse(res.get("ok"))
self.assertEqual(
res.get("reason_code"), "authority_identity_unresolved"
)
def test_missing_host_cannot_yield_an_identity(self):
with mock.patch.object(gms, "get_profile",
return_value={"profile_name": PROFILE}), \
mock.patch.object(gms.session_ctx, "get_session_context",
return_value=None):
res = gms._active_mutation_authority(None)
self.assertFalse(res.get("ok"))
self.assertEqual(res.get("reason_code"), "authority_identity_unresolved")
def test_expected_username_is_never_substituted_for_authentication(self):
with mock.patch.object(
gms, "get_profile",
return_value={"profile_name": PROFILE, "username": IDENTITY},
), mock.patch.object(gms, "_authenticated_username", return_value=None), \
mock.patch.object(gms.session_ctx, "get_session_context",
return_value={"expected_username": IDENTITY}):
self.assertIsNone(gms._active_username("gitea.prgs.cc"))
def test_accessors_share_one_snapshot(self):
with mock.patch.object(gms, "get_profile",
return_value={"profile_name": PROFILE}), \
mock.patch.object(gms, "_authenticated_username",
return_value=IDENTITY), \
mock.patch.object(gms.session_ctx, "get_session_context",
return_value=self._ctx()):
self.assertEqual(gms._active_username("gitea.prgs.cc"), IDENTITY)
self.assertEqual(gms._active_profile_name("gitea.prgs.cc"), PROFILE)
class AuthorMutationBlockTests(unittest.TestCase):
"""Preserved: the structured refusal shape review 622 confirmed correct."""
def test_matches_the_sibling_refusal_shape(self):
res = gms._author_mutation_block(["stopped"])
self.assertIs(res["success"], False)
self.assertIs(res["performed"], False)
self.assertEqual(res["outcome"], "REFUSED")
self.assertEqual(res["reasons"], ["stopped"])
def test_carries_reason_code_and_transport_fields(self):
res = gms._author_mutation_block(
["nope"], reason_code="authority_identity_drift",
retryable=False, transport_survives=True,
expected="a", actual="b", issue_number=943,
)
self.assertEqual(res["reason_code"], "authority_identity_drift")
self.assertIs(res["retryable"], False)
self.assertIs(res["transport_survives"], True)
self.assertEqual((res["expected"], res["actual"]), ("a", "b"))
self.assertEqual(res["issue_number"], 943)
self.assertIs(res["success"], False)
class RuntimeHelperResolutionTests(unittest.TestCase):
"""Every runtime helper the wrapper references is defined and callable.
Supplements the runtime coverage above; it does not replace it.
"""
def test_named_helpers_are_defined_and_callable(self):
for name in RUNTIME_HELPERS:
with self.subTest(helper=name):
self.assertTrue(hasattr(gms, name), f"{name} is not defined")
self.assertTrue(callable(getattr(gms, name)))
def test_every_global_referenced_by_the_wrapper_resolves(self):
"""The generalised form of the round-1 defect: an unresolvable global."""
fn = _wrapper_ast()
bound: set[str] = {a.arg for a in fn.args.args}
bound |= {a.arg for a in fn.args.kwonlyargs}
if fn.args.vararg:
bound.add(fn.args.vararg.arg)
if fn.args.kwarg:
bound.add(fn.args.kwarg.arg)
for node in ast.walk(fn):
if isinstance(node, ast.Name) and isinstance(
node.ctx, (ast.Store, ast.Del)
):
bound.add(node.id)
elif isinstance(node, (ast.Import, ast.ImportFrom)):
for alias in node.names:
bound.add((alias.asname or alias.name).split(".")[0])
elif isinstance(node, ast.ExceptHandler) and node.name:
bound.add(node.name)
unresolved = sorted(
node.id
for node in ast.walk(fn)
if isinstance(node, ast.Name)
and isinstance(node.ctx, ast.Load)
and node.id not in bound
and not hasattr(gms, node.id)
and not hasattr(builtins, node.id)
)
self.assertEqual(
unresolved, [],
f"{WRAPPER_NAME} references undefined globals: {unresolved}",
)
def test_wrapper_wires_the_authority_and_session_resolvers(self):
fn = _wrapper_ast()
called = {
node.func.id
for node in ast.walk(fn)
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name)
}
self.assertIn("_active_mutation_authority", called)
self.assertIn("_resolve_owner_workflow_session", called)
self.assertIn("_author_mutation_block", called)
def test_wrapper_accepts_an_optional_session_id(self):
"""ABI addition stays backward compatible: optional, defaulting to None."""
fn = _wrapper_ast()
names = [a.arg for a in fn.args.args]
self.assertIn("session_id", names)
offset = len(names) - len(fn.args.defaults)
default = fn.args.defaults[names.index("session_id") - offset]
self.assertIsInstance(default, ast.Constant)
self.assertIsNone(default.value)
class Issue941ScopeGuardNotRegressedTests(unittest.TestCase):
"""Preserved: PR #942's bootstrap-scope wiring still holds."""
def setUp(self):
self._tmp = tempfile.TemporaryDirectory()
self.addCleanup(self._tmp.cleanup)
self.repo, self.head = _make_control_repo(self._tmp.name)
def _assessment(self, task: str = BOOTSTRAP_TASK) -> dict:
return aib.assess_author_issue_bootstrap(
workspace_path=self.repo,
canonical_repo_root=self.repo,
current_branch="master",
head_sha=self.head,
porcelain_status="",
remote_master_sha=self.head,
task=task,
)
def test_bootstrap_task_still_permitted_from_clean_control_checkout(self):
res = workflow_scope_guard.assess_root_source_mutation(
workspace_path=self.repo,
canonical_repo_root=self.repo,
role_kind="author",
mutation_task=BOOTSTRAP_TASK,
porcelain_status="",
bootstrap_assessment=self._assessment(),
)
self.assertFalse(res.get("block"), res)
self.assertNotEqual(
res.get("blocker_kind"), workflow_scope_guard.BLOCKER_MISSING_WORKTREE
)
def test_bootstrap_task_still_blocked_without_evidence(self):
res = workflow_scope_guard.assess_root_source_mutation(
workspace_path=self.repo,
canonical_repo_root=self.repo,
role_kind="author",
mutation_task=BOOTSTRAP_TASK,
porcelain_status="",
)
self.assertTrue(res.get("block"))
self.assertEqual(
res.get("blocker_kind"), workflow_scope_guard.BLOCKER_MISSING_WORKTREE
)
def test_ordinary_author_mutation_still_blocked_from_control_checkout(self):
res = workflow_scope_guard.assess_root_source_mutation(
workspace_path=self.repo,
canonical_repo_root=self.repo,
role_kind="author",
mutation_task="commit_files",
porcelain_status="",
bootstrap_assessment=self._assessment(),
)
self.assertTrue(res.get("block"))
self.assertEqual(
res.get("blocker_kind"), workflow_scope_guard.BLOCKER_MISSING_WORKTREE
)
def test_create_issue_bootstrap_unchanged(self):
self.assertTrue(cib.is_create_issue_task("create_issue"))
self.assertFalse(cib.is_create_issue_task(BOOTSTRAP_TASK))
def _write_json(path: str, payload: dict) -> None:
"""Write an issue-lock file directly, for lock-precedence tests."""
import json
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w", encoding="utf-8") as fh:
json.dump(payload, fh)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,685 @@
import sys as _sys
from pathlib import Path as _Path
_sys.path.insert(0, str(_Path(__file__).resolve().parent))
from mutation_profile_fixture import shared_mutation_env # noqa: E402
"""The renewal waiver reaches the real enforcement paths (#945 B1).
``tests/test_issue_945_owning_pr_renewal_continuation.py`` proves the pure
pieces: that ``issue_lock_renewal.owning_pr_renewal_from_lock`` rebuilds a
renewal waiver, that ``_owning_pr_continuation_from_lock`` resolves the two
dispositions in the right precedence, and that the duplicate gate honours the
resulting token. None of that proves any *production* path consumes the
resolver, and review ``623`` demonstrated the gap by reverting the primary call
site at ``gitea_mcp_server.py:2894`` back to the recovery-only rebuild: the
whole repository stayed green, failing-test ids byte identical.
This file closes that hole. Every test here starts from a real durable lock
file written to a temporary lock directory and bound to this process's session
pointer, then calls the authoritative production entry point — not a helper:
* ``mcp_server._enforce_locked_issue_duplicate_recheck`` — the shared recheck
behind ``gitea_commit_files`` and ``gitea_create_pr``
* ``mcp_server.gitea_assess_work_issue_duplicate`` — the read-only assessor
* ``mcp_server._prove_author_ownership_for_pr`` — the push / PR-update
ownership prover, which is also the existing-PR continuation path
Only the external boundaries are mocked: Gitea HTTP reads (the duplicate
context fetcher, open-PR and branch listings) and the credential header. The
reconstruction and enforcement chain under test — lock load, evidence rebuild,
resolver precedence, and ``issue_work_duplicate_gate`` — runs for real.
``TestRevertingThePrimaryWiringIsDetected`` is the explicit regression the
review asked for: it reproduces the pre-#945 recovery-only call site and
asserts the enforcement path then refuses, so the wiring cannot be removed
silently.
Everything is written under ``tempfile.TemporaryDirectory``. No branch,
worktree, PR, comment, lease, or lock outside that directory is created, and no
production Gitea or control-plane state is touched (#945 AC18).
"""
import os
import subprocess
import sys
import tempfile
import unittest
from datetime import datetime, timedelta, timezone
from pathlib import Path
from unittest.mock import patch
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
import issue_lock_provenance # noqa: E402
import issue_lock_recovery # noqa: E402
import issue_lock_renewal # noqa: E402
import issue_lock_store # noqa: E402
import mcp_server # noqa: E402
from issue_work_duplicate_gate import ( # noqa: E402
PHASE_COMMIT,
PHASE_CREATE_PR,
PHASE_LOCK,
PHASE_PUSH,
)
ISSUE = 4948
OWNING_PR = 4949
OTHER_PR = 4950
OTHER_ISSUE = 4951
BRANCH = f"fix/issue-{ISSUE}-renewal-wiring"
OTHER_BRANCH = f"fix/issue-{ISSUE}-competing"
HEAD = "e" * 40
OTHER_HEAD = "f" * 40
IDENTITY = "example-user"
PROFILE = "test-author-prgs"
ORG = "Scaled-Tech-Consulting"
REPO = "Gitea-Tools"
HOST = "gitea.prgs.cc"
def dead_pid() -> int:
"""A PID that has certainly exited (spawned, then reaped)."""
proc = subprocess.Popen([sys.executable, "-c", "pass"])
proc.wait()
return proc.pid
def shifted_ts(hours: int = 4) -> str:
return (
(datetime.now(timezone.utc) + timedelta(hours=hours))
.isoformat()
.replace("+00:00", "Z")
)
def owning_pr(number=OWNING_PR, ref=BRANCH, sha=HEAD, issue=ISSUE):
return {
"number": number,
"title": f"fix: something (Closes #{issue})",
"body": f"Closes #{issue}.",
"head": {"ref": ref, "sha": sha},
}
def renewal_block(
*,
pr_number=OWNING_PR,
branch=BRANCH,
head=HEAD,
identity=IDENTITY,
profile=PROFILE,
):
"""The ``lease_renewal`` block ``build_renewal_record`` writes on success."""
return {
"renewed": True,
"renewed_at": shifted_ts(-1),
"prior_pid": 4242,
"prior_pid_alive": True,
"prior_expires_at": shifted_ts(-1),
"replacement_pid": os.getpid(),
"new_expires_at": shifted_ts(),
"identity": identity,
"profile": profile,
"branch_name": branch,
"worktree_path": os.path.realpath(os.getcwd()),
"head_sha": head,
"remote_head_sha": head,
"pr_head_sha": head,
"pr_number": pr_number,
"reason": "expired lease renewed by its exact recorded owner",
"proof": [],
}
def recovery_block(*, pr_number=OWNING_PR, branch=BRANCH, head=HEAD):
"""The ``dead_session_recovery`` block ``build_recovery_record`` writes."""
return {
"recovered": True,
"reason": "owning MCP session exited; durable ownership evidence matched",
"recovered_at": shifted_ts(-1),
"prior_session_pid": 4242,
"replacement_session_pid": os.getpid(),
"prior_pid_alive": False,
"branch_name": branch,
"pr_number": pr_number,
"pr_head": head,
"recorded_head": head,
"accepted_head": head,
"head_relation": issue_lock_recovery.HEAD_RELATION_EQUAL,
"identity": IDENTITY,
"profile": PROFILE,
"proof": [],
}
class EnforcementPathBase(unittest.TestCase):
"""Drives production enforcement entry points against a real durable lock.
The lock lives in a throwaway directory and is bound to this process's
session pointer exactly as ``gitea_lock_issue`` binds it, so
``_load_existing_issue_lock()`` resolves it through the ordinary
``read_session_issue_lock()`` path rather than a test shortcut.
"""
def setUp(self):
self.lock_dir = tempfile.TemporaryDirectory()
self.addCleanup(self.lock_dir.cleanup)
self.worktree = os.path.realpath(os.getcwd())
self.remotes = patch.dict(
mcp_server.REMOTES,
{"prgs": {"host": HOST, "org": ORG, "repo": REPO}},
)
self.remotes.start()
self.addCleanup(patch.stopall)
mcp_server._IDENTITY_CACHE.clear()
# ── fixtures ────────────────────────────────────────────────────────────
def build_lock(
self,
*,
issue_number=ISSUE,
branch=BRANCH,
renewal=None,
recovery=None,
claimant=None,
pid=None,
live=True,
):
pid = os.getpid() if pid is None else pid
claimant = claimant or {"username": IDENTITY, "profile": PROFILE}
expires = shifted_ts() if live else shifted_ts(-1)
data = {
"issue_number": issue_number,
"branch_name": branch,
"remote": "prgs",
"org": ORG,
"repo": REPO,
"worktree_path": self.worktree,
"session_pid": pid,
"pid": pid,
"claimant": dict(claimant),
"work_lease": {
"operation_type": issue_lock_store.AUTHOR_ISSUE_WORK_LEASE,
"issue_number": issue_number,
"branch": branch,
"worktree_path": self.worktree,
"claimant": dict(claimant),
"created_at": shifted_ts(-1),
"last_heartbeat_at": shifted_ts(0) if live else shifted_ts(-1),
"expires_at": expires,
},
"lock_provenance": issue_lock_provenance.build_sanctioned_lock_provenance(
tool="gitea_lock_issue",
claimant=dict(claimant),
),
}
if renewal is not None:
data["lease_renewal"] = renewal
if recovery is not None:
data["dead_session_recovery"] = recovery
return data
def bind(self, data):
"""Persist the lock and bind it to this process, as the server does."""
issue_lock_store.bind_session_lock(data, self.lock_dir.name)
return data
def env(self):
return shared_mutation_env(
PROFILE,
include_example_repo=True,
GITEA_ISSUE_LOCK_DIR=self.lock_dir.name,
)
def gitea_reads(self, *, open_prs, branch_names=None):
"""Patch only the external Gitea read boundary."""
branch_names = [BRANCH] if branch_names is None else branch_names
return (
patch("mcp_server.get_auth_header", return_value="token x"),
patch(
"mcp_server.issue_duplicate_context_fetcher",
side_effect=lambda h, o, r, auth, issue_number: (
list(open_prs), list(branch_names), {"status": "not_claimed"}
),
),
patch("mcp_server._list_open_pulls", return_value=list(open_prs)),
patch(
"mcp_server.api_get_all",
return_value=[
{"name": n, "commit": {"id": HEAD}} for n in branch_names
],
),
)
# ── production entry points ─────────────────────────────────────────────
def run_duplicate_recheck(self, *, phase, open_prs, branch_names=None):
"""The real shared recheck behind gitea_commit_files / gitea_create_pr."""
patches = self.gitea_reads(open_prs=open_prs, branch_names=branch_names)
with patches[0], patches[1], patches[2], patches[3]:
with patch.dict(os.environ, self.env(), clear=True):
os.environ["GITEA_ISSUE_LOCK_DIR"] = self.lock_dir.name
return mcp_server._enforce_locked_issue_duplicate_recheck(
"prgs", phase, host=HOST, org=ORG, repo=REPO
)
def run_readonly_assessor(
self, *, open_prs, issue_number=ISSUE, branch=BRANCH, branch_names=None
):
"""The real read-only duplicate assessor MCP tool."""
patches = self.gitea_reads(open_prs=open_prs, branch_names=branch_names)
with patches[0], patches[1], patches[2], patches[3]:
with patch.dict(os.environ, self.env(), clear=True):
os.environ["GITEA_ISSUE_LOCK_DIR"] = self.lock_dir.name
return mcp_server.gitea_assess_work_issue_duplicate(
issue_number=issue_number,
branch_name=branch,
phase=PHASE_COMMIT,
remote="prgs",
host=HOST,
org=ORG,
repo=REPO,
)
def run_ownership_prover(
self, *, pr_number=OWNING_PR, branch=BRANCH, issue_number=ISSUE
):
"""The real push / PR-update ownership prover (existing-PR continuation)."""
with patch("mcp_server.get_auth_header", return_value="token x"):
with patch.dict(os.environ, self.env(), clear=True):
os.environ["GITEA_ISSUE_LOCK_DIR"] = self.lock_dir.name
return mcp_server._prove_author_ownership_for_pr(
pr_number=pr_number,
pr_title=f"fix: something (Closes #{issue_number})",
pr_body=f"Closes #{issue_number}.",
source_branch=branch,
remote="prgs",
host=HOST,
org=ORG,
repo=REPO,
worktree_path=self.worktree,
)
# ─────────────── B1: renewal evidence reaches every enforcement path ───────────
class TestRenewalReachesEnforcementPaths(EnforcementPathBase):
"""A renewal-only lock must exempt its owning PR at the real call sites.
Each of these fails if its call site is reverted to the recovery-only
rebuild, because the lock deliberately carries no ``dead_session_recovery``
block at all.
"""
def setUp(self):
super().setUp()
self.bind(self.build_lock(renewal=renewal_block()))
def test_commit_duplicate_recheck_permits_the_owning_pr(self):
blocked = self.run_duplicate_recheck(
phase=PHASE_COMMIT, open_prs=[owning_pr()]
)
self.assertIsNone(
blocked,
"commit recheck refused the PR the renewal already proved it owns; "
"the resolver is not wired into gitea_mcp_server:2894",
)
def test_create_pr_duplicate_recheck_permits_the_owning_pr(self):
blocked = self.run_duplicate_recheck(
phase=PHASE_CREATE_PR, open_prs=[owning_pr()]
)
self.assertIsNone(blocked)
def test_read_only_assessor_reports_the_same_exemption(self):
result = self.run_readonly_assessor(open_prs=[owning_pr()])
self.assertTrue(result["success"])
self.assertFalse(result["block"])
self.assertTrue(result["owning_pr_recovery_exempted"])
self.assertEqual(result["linked_open_pr"], OWNING_PR)
def test_push_ownership_prover_carries_the_renewal_evidence(self):
ownership = self.run_ownership_prover()
self.assertTrue(ownership["proven"], ownership["reasons"])
token = ownership["recovered_owning_pr"]
self.assertIsNotNone(
token,
"push prover produced no continuation evidence from a renewal lock; "
"the resolver is not wired into gitea_mcp_server:19464",
)
self.assertEqual(token["pr_number"], OWNING_PR)
self.assertEqual(token["branch_name"], BRANCH)
self.assertEqual(token["head_sha"], HEAD)
def test_all_enforcement_paths_decide_alike_from_one_lock(self):
"""AC: commit, create-PR, assessor and prover agree on one lock."""
for phase in (PHASE_COMMIT, PHASE_CREATE_PR, PHASE_PUSH, PHASE_LOCK):
with self.subTest(phase=phase):
self.assertIsNone(
self.run_duplicate_recheck(phase=phase, open_prs=[owning_pr()])
)
assessor = self.run_readonly_assessor(open_prs=[owning_pr()])
prover = self.run_ownership_prover()
self.assertTrue(assessor["owning_pr_recovery_exempted"])
self.assertEqual(
assessor["linked_open_pr"], prover["recovered_owning_pr"]["pr_number"]
)
class TestDeadSessionRecoveryStillReachesEnforcementPaths(EnforcementPathBase):
"""#755/#768 recovery must be unchanged by the #945 resolver."""
def setUp(self):
super().setUp()
self.bind(self.build_lock(recovery=recovery_block()))
def test_commit_recheck_still_permits_a_recovered_owning_pr(self):
self.assertIsNone(
self.run_duplicate_recheck(phase=PHASE_COMMIT, open_prs=[owning_pr()])
)
def test_assessor_still_reports_the_recovery_exemption(self):
result = self.run_readonly_assessor(open_prs=[owning_pr()])
self.assertTrue(result["owning_pr_recovery_exempted"])
def test_prover_still_carries_recovery_evidence(self):
token = self.run_ownership_prover()["recovered_owning_pr"]
self.assertEqual(token["pr_number"], OWNING_PR)
# ──────────────── B1: the explicit anti-revert regression test ────────────────
class TestRevertingThePrimaryWiringIsDetected(EnforcementPathBase):
"""Reproduce the pre-#945 call site and prove the path then refuses.
Review ``623`` reverted ``gitea_mcp_server.py:2894`` from
``_owning_pr_continuation_from_lock`` to
``issue_lock_recovery.recovered_owning_pr_from_lock`` and found the entire
repository still green. Substituting exactly that pre-fix behaviour here
makes the enforcement path block, so the causal link between the resolver
and the gate's answer is asserted, not assumed.
"""
def setUp(self):
super().setUp()
self.bind(self.build_lock(renewal=renewal_block()))
def test_recovery_only_rebuild_reintroduces_the_945_refusal(self):
with patch.object(
mcp_server,
"_owning_pr_continuation_from_lock",
side_effect=issue_lock_recovery.recovered_owning_pr_from_lock,
):
blocked = self.run_duplicate_recheck(
phase=PHASE_COMMIT, open_prs=[owning_pr()]
)
self.assertIsNotNone(
blocked,
"the pre-#945 recovery-only rebuild must lose the renewal waiver; "
"if this passes, the enforcement path is not consuming the resolver",
)
self.assertTrue(blocked["block"])
self.assertFalse(blocked["owning_pr_recovery_exempted"])
def test_restoring_the_resolver_restores_continuation(self):
self.assertIsNone(
self.run_duplicate_recheck(phase=PHASE_COMMIT, open_prs=[owning_pr()])
)
def test_read_only_assessor_is_wired_to_the_same_resolver(self):
with patch.object(
mcp_server,
"_owning_pr_continuation_from_lock",
side_effect=issue_lock_recovery.recovered_owning_pr_from_lock,
):
result = self.run_readonly_assessor(open_prs=[owning_pr()])
self.assertTrue(result["block"])
self.assertFalse(result["owning_pr_recovery_exempted"])
def test_push_prover_is_wired_to_the_same_resolver(self):
with patch.object(
mcp_server,
"_owning_pr_continuation_from_lock",
side_effect=issue_lock_recovery.recovered_owning_pr_from_lock,
):
ownership = self.run_ownership_prover()
self.assertIsNone(ownership["recovered_owning_pr"])
# ───────────────── B1: the exemption is not widened at the call sites ─────────
class TestEnforcementPathsStillFailClosed(EnforcementPathBase):
def assert_blocked(self, result):
self.assertIsNotNone(result, "expected a fail-closed refusal")
self.assertTrue(result["block"])
return result
def test_open_pr_alone_grants_no_exemption(self):
"""No renewal and no recovery block: the open PR still blocks."""
self.bind(self.build_lock())
blocked = self.assert_blocked(
self.run_duplicate_recheck(phase=PHASE_COMMIT, open_prs=[owning_pr()])
)
self.assertFalse(blocked["owning_pr_recovery_exempted"])
def test_second_pr_is_refused(self):
self.bind(self.build_lock(renewal=renewal_block()))
self.assert_blocked(
self.run_duplicate_recheck(
phase=PHASE_COMMIT,
open_prs=[owning_pr(), owning_pr(number=OTHER_PR, ref=OTHER_BRANCH)],
)
)
def test_evidence_naming_another_pr_is_refused(self):
self.bind(self.build_lock(renewal=renewal_block(pr_number=OTHER_PR)))
self.assert_blocked(
self.run_duplicate_recheck(phase=PHASE_COMMIT, open_prs=[owning_pr()])
)
def test_unrelated_branch_is_refused(self):
self.bind(self.build_lock(renewal=renewal_block(branch=OTHER_BRANCH)))
self.assert_blocked(
self.run_duplicate_recheck(phase=PHASE_COMMIT, open_prs=[owning_pr()])
)
def test_live_head_divergence_is_refused(self):
"""Force-push or unrelated remote movement: live PR head no longer matches."""
self.bind(self.build_lock(renewal=renewal_block()))
self.assert_blocked(
self.run_duplicate_recheck(
phase=PHASE_COMMIT, open_prs=[owning_pr(sha=OTHER_HEAD)]
)
)
def test_stale_recorded_head_is_refused(self):
"""The renewal names a head the live PR never had."""
self.bind(self.build_lock(renewal=renewal_block(head=OTHER_HEAD)))
self.assert_blocked(
self.run_duplicate_recheck(phase=PHASE_COMMIT, open_prs=[owning_pr()])
)
def test_local_remote_head_divergence_is_refused(self):
record = renewal_block()
record["remote_head_sha"] = OTHER_HEAD
self.bind(self.build_lock(renewal=record))
self.assert_blocked(
self.run_duplicate_recheck(phase=PHASE_COMMIT, open_prs=[owning_pr()])
)
def test_identity_mismatch_with_the_lock_claimant_is_refused(self):
self.bind(self.build_lock(renewal=renewal_block(identity="someone-else")))
self.assert_blocked(
self.run_duplicate_recheck(phase=PHASE_COMMIT, open_prs=[owning_pr()])
)
def test_profile_mismatch_with_the_lock_claimant_is_refused(self):
self.bind(self.build_lock(renewal=renewal_block(profile="test-reviewer-prgs")))
self.assert_blocked(
self.run_duplicate_recheck(phase=PHASE_COMMIT, open_prs=[owning_pr()])
)
def test_ungranted_renewal_block_is_refused(self):
record = renewal_block()
record["renewed"] = False
self.bind(self.build_lock(renewal=record))
self.assert_blocked(
self.run_duplicate_recheck(phase=PHASE_COMMIT, open_prs=[owning_pr()])
)
def test_malformed_renewal_block_is_refused(self):
record = renewal_block()
record["pr_number"] = "not-a-number"
self.bind(self.build_lock(renewal=record))
self.assert_blocked(
self.run_duplicate_recheck(phase=PHASE_COMMIT, open_prs=[owning_pr()])
)
def test_wrong_issue_evidence_cannot_be_copied_onto_another_lock(self):
"""A renewal block copied onto a lock for a different issue proves nothing.
The rebuilt token takes its ``issue_number`` from the lock it is found
on, not from the record, so a block lifted onto another issue's lock
claims that issue while still naming the original PR. That copied
evidence must not waive the genuine duplicate the other issue has.
"""
self.bind(
self.build_lock(issue_number=OTHER_ISSUE, renewal=renewal_block())
)
blocked = self.assert_blocked(
self.run_duplicate_recheck(
phase=PHASE_COMMIT,
# The real open PR for OTHER_ISSUE is a different PR entirely.
open_prs=[
owning_pr(number=OTHER_PR, ref=OTHER_BRANCH, issue=OTHER_ISSUE)
],
branch_names=[OTHER_BRANCH],
)
)
self.assertFalse(blocked["owning_pr_recovery_exempted"])
def test_refusal_carries_complete_structured_fields(self):
self.bind(self.build_lock())
blocked = self.assert_blocked(
self.run_duplicate_recheck(phase=PHASE_COMMIT, open_prs=[owning_pr()])
)
for field in (
"block",
"outcome",
"reasons",
"owning_pr_recovery_exempted",
"owning_pr_recovery_notes",
"linked_open_pr",
"linked_open_pr_count",
):
with self.subTest(field=field):
self.assertIn(field, blocked)
self.assertTrue(blocked["reasons"])
class TestSequentialTasksStayIsolated(EnforcementPathBase):
"""One long-lived daemon serves many tasks; a waiver must not leak forward."""
def test_a_later_lock_without_evidence_does_not_inherit_the_earlier_waiver(self):
self.bind(self.build_lock(renewal=renewal_block()))
self.assertIsNone(
self.run_duplicate_recheck(phase=PHASE_COMMIT, open_prs=[owning_pr()])
)
# Second task in the same process: a fresh lock, no renewal evidence.
self.bind(
self.build_lock(issue_number=OTHER_ISSUE, branch=OTHER_BRANCH)
)
blocked = self.run_duplicate_recheck(
phase=PHASE_COMMIT,
open_prs=[owning_pr(number=OTHER_PR, ref=OTHER_BRANCH, issue=OTHER_ISSUE)],
branch_names=[OTHER_BRANCH],
)
self.assertIsNotNone(blocked)
self.assertFalse(blocked["owning_pr_recovery_exempted"])
# ───────────── F2: what the caller binding actually is, and is not ────────────
class TestCallerBindingIsStructuralNotFieldComparison(unittest.TestCase):
"""Document, in executable form, the binding this patch really provides.
Review ``623`` found that the claimant check in
``owning_pr_renewal_from_lock`` compares two fields of one server-written
lock file and is therefore not bound to the authenticated caller. That is
correct, and these tests assert the true guarantee rather than the
overstated one: lock *selection* is process-scoped, and the claimant check
is an internal-consistency check.
No PID-derived, cached, or process-lifetime session authority is invented
here — the process scoping asserted below is pre-existing behaviour of
``issue_lock_store``, not something this patch adds.
"""
def test_lock_selection_is_keyed_to_the_operating_system_process(self):
with tempfile.TemporaryDirectory() as root:
pointer = issue_lock_store.session_pointer_path(root)
self.assertEqual(
os.path.basename(pointer), f"session-{os.getpid()}.json"
)
def test_a_lock_bound_by_another_process_is_not_reachable(self):
"""The structural protection: a foreign session pointer is not read."""
with tempfile.TemporaryDirectory() as root:
foreign_pointer = os.path.join(root, f"session-{os.getpid() + 1}.json")
issue_lock_store.save_lock_file(
foreign_pointer, {"lock_file_path": "/nonexistent/foreign.json"}
)
self.assertIsNone(issue_lock_store.read_session_issue_lock(root))
def test_claimant_check_does_not_consult_the_live_authenticated_caller(self):
"""The honest limit: agreement is internal to the lock document.
A renewal block whose identity/profile agree with the claimant recorded
on the same lock rebuilds successfully, regardless of who is
authenticated. Live identity and profile are enforced by the separate
mutation-authority and profile gates, not by this rebuild.
"""
lock = {
"issue_number": ISSUE,
"branch_name": BRANCH,
"claimant": {"username": "unrelated-recorded-user", "profile": PROFILE},
"lease_renewal": renewal_block(identity="unrelated-recorded-user"),
}
token = issue_lock_renewal.owning_pr_renewal_from_lock(lock)
self.assertIsNotNone(token)
self.assertEqual(token["pr_number"], OWNING_PR)
def test_internal_disagreement_is_what_the_check_actually_rejects(self):
lock = {
"issue_number": ISSUE,
"branch_name": BRANCH,
"claimant": {"username": IDENTITY, "profile": PROFILE},
"lease_renewal": renewal_block(identity="someone-else"),
}
self.assertIsNone(issue_lock_renewal.owning_pr_renewal_from_lock(lock))
class TestNoDurableArtifacts(EnforcementPathBase):
def test_enforcement_runs_leave_nothing_outside_the_temp_lock_dir(self):
before = sorted(os.listdir(self.lock_dir.name))
self.bind(self.build_lock(renewal=renewal_block()))
self.run_duplicate_recheck(phase=PHASE_COMMIT, open_prs=[owning_pr()])
self.run_ownership_prover()
after = sorted(os.listdir(self.lock_dir.name))
self.assertNotEqual(before, after, "the test must have written its lock")
self.assertTrue(
all(
os.path.realpath(os.path.join(self.lock_dir.name, name)).startswith(
os.path.realpath(self.lock_dir.name)
)
for name in after
)
)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,602 @@
import sys as _sys
from pathlib import Path as _Path
_sys.path.insert(0, str(_Path(__file__).resolve().parent))
from mutation_profile_fixture import shared_mutation_env # noqa: F401,E402
"""Exact-owner renewal keeps its owning-PR waiver past lock_issue (#945).
#755 taught the duplicate-work gate that a sanctioned *dead-session recovery*
owns its open PR, and #768 taught the later gates to rebuild that proof from the
durable lock. #760 added the exact-owner *renewal* disposition and granted it
the same waiver inside ``gitea_lock_issue`` — but never added the matching
rebuild. So an ordinary renewal held the waiver only for the duration of the
lock call: ``_enforce_locked_issue_duplicate_recheck`` asked
``recovered_owning_pr_from_lock``, which reads only ``dead_session_recovery``,
and the very next commit was refused ``duplicate_commit_prevented`` with
``owning_pr_recovery_exempted: false`` on the PR the renewal had just proved.
``TestPreFixReproduction`` pins that defect directly: the recovery-only rebuild
still returns ``None`` for a renewal lock, which is exactly why the gates lost
the waiver. Everything else proves the renewal half now survives, that recovery
is unchanged, and that no path grants an exemption on weaker evidence.
Every fixture here is an in-memory mapping. Nothing writes a branch, worktree,
lock file, lease, comment, or PR (#945 AC18).
"""
import copy
import sys
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
import gitea_mcp_server # noqa: E402
import issue_lock_recovery # noqa: E402
import issue_lock_renewal # noqa: E402
from issue_work_duplicate_gate import ( # noqa: E402
OUTCOME_DUPLICATE_WORK_NOT_PREVENTED,
PHASE_COMMIT,
PHASE_CREATE_PR,
PHASE_LOCK,
PHASE_PUSH,
assess_work_issue_duplicate_gate,
)
ISSUE = 4945
OWNING_PR = 4946
OTHER_PR = 4947
BRANCH = f"fix/issue-{ISSUE}-owning-pr-renewal"
OTHER_BRANCH = f"fix/issue-{ISSUE}-competing"
HEAD = "a" * 40
OTHER_HEAD = "b" * 40
IDENTITY = "example-user"
PROFILE = "test-author-prgs"
def renewal_record(**overrides):
"""The ``lease_renewal`` block ``build_renewal_record`` writes on success."""
record = {
"renewed": True,
"renewed_at": "2026-01-01T00:00:00Z",
"prior_pid": 4242,
"prior_pid_alive": True,
"prior_expires_at": "2026-01-01T00:00:00Z",
"replacement_pid": 4243,
"new_expires_at": "2026-01-01T00:10:00Z",
"identity": IDENTITY,
"profile": PROFILE,
"branch_name": BRANCH,
"worktree_path": f"branches/issue-{ISSUE}-owning-pr-renewal",
"head_sha": HEAD,
"remote_head_sha": HEAD,
"pr_head_sha": HEAD,
"pr_number": OWNING_PR,
"reason": "expired lease renewed by its exact recorded owner",
"proof": [],
}
record.update(overrides)
return record
def renewal_lock(record=None, *, issue_number=ISSUE, claimant=True, **lock_overrides):
lock = {
"issue_number": issue_number,
"branch_name": BRANCH,
"lease_renewal": renewal_record() if record is None else record,
}
if claimant:
lock["claimant"] = {"username": IDENTITY, "profile": PROFILE}
lock.update(lock_overrides)
return lock
def recovery_lock(pr_number=OWNING_PR, head=HEAD):
"""A lock carrying sanctioned dead-session recovery evidence (#755/#768)."""
return {
"issue_number": ISSUE,
"branch_name": BRANCH,
"claimant": {"username": IDENTITY, "profile": PROFILE},
"dead_session_recovery": {
"recovered": True,
"branch_name": BRANCH,
"pr_number": pr_number,
"pr_head": head,
"recorded_head": head,
"accepted_head": head,
"head_relation": issue_lock_recovery.HEAD_RELATION_EQUAL,
},
}
def owning_pr(number=OWNING_PR, ref=BRANCH, sha=HEAD, issue=ISSUE):
return {
"number": number,
"title": f"fix: something (Closes #{issue})",
"body": f"Closes #{issue}.",
"head": {"ref": ref, "sha": sha},
}
def gate(phase, *, token, open_prs=None, branch_names=None, locked_branch=BRANCH):
return assess_work_issue_duplicate_gate(
ISSUE,
open_prs=[owning_pr()] if open_prs is None else open_prs,
branch_names=branch_names or [],
claim_entry={},
locked_branch=locked_branch,
phase=phase,
recovered_owning_pr=token,
)
# ───────────────────── the defect this issue exists to fix ─────────────────────
class TestPreFixReproduction(unittest.TestCase):
"""The exact wiring gap: renewal evidence was invisible to later gates."""
def test_recovery_only_rebuild_cannot_see_a_renewal_lock(self):
# This is the pre-fix behaviour of every enforcement path. It is correct
# for the recovery rebuild to ignore a renewal block -- the defect was
# that nothing else looked at it.
self.assertIsNone(
issue_lock_recovery.recovered_owning_pr_from_lock(renewal_lock())
)
def test_renewal_lock_produced_no_exemption_before_the_fix(self):
# Feeding the gate what the pre-fix code fed it (recovery rebuild only)
# reproduces the reported refusal at the commit phase.
token = issue_lock_recovery.recovered_owning_pr_from_lock(renewal_lock())
result = gate(PHASE_COMMIT, token=token)
self.assertTrue(result["block"])
self.assertEqual(result["outcome"], "duplicate_commit_prevented")
self.assertFalse(result["owning_pr_recovery_exempted"])
self.assertEqual(result["owning_pr_recovery_notes"], [])
def test_shared_resolver_now_sees_it(self):
self.assertIsNotNone(
gitea_mcp_server._owning_pr_continuation_from_lock(renewal_lock())
)
# ───────────────────────── rebuild: the granted case ─────────────────────────
class TestRenewalRebuildGranted(unittest.TestCase):
def test_sanctioned_renewal_rebuilds_owning_pr_evidence(self):
token = issue_lock_renewal.owning_pr_renewal_from_lock(renewal_lock())
self.assertEqual(
token,
{
"issue_number": ISSUE,
"pr_number": OWNING_PR,
"branch_name": BRANCH,
"head_sha": HEAD,
"recorded_head": HEAD,
"accepted_head": HEAD,
"head_relation": "equal",
},
)
def test_branch_falls_back_to_the_lock_branch(self):
lock = renewal_lock(renewal_record(branch_name=""))
token = issue_lock_renewal.owning_pr_renewal_from_lock(lock)
self.assertEqual(token["branch_name"], BRANCH)
def test_claimant_may_live_under_work_lease(self):
lock = renewal_lock(claimant=False)
lock["work_lease"] = {"claimant": {"username": IDENTITY, "profile": PROFILE}}
self.assertIsNotNone(issue_lock_renewal.owning_pr_renewal_from_lock(lock))
def test_rebuild_does_not_mutate_the_lock(self):
lock = renewal_lock()
before = copy.deepcopy(lock)
issue_lock_renewal.owning_pr_renewal_from_lock(lock)
self.assertEqual(lock, before)
# ───────────────────────── rebuild: fails closed ─────────────────────────
class TestRenewalRebuildFailsClosed(unittest.TestCase):
def assertNoEvidence(self, lock):
self.assertIsNone(issue_lock_renewal.owning_pr_renewal_from_lock(lock))
def test_no_lock_at_all(self):
self.assertNoEvidence(None)
self.assertNoEvidence({})
self.assertNoEvidence("not-a-mapping")
def test_lock_without_renewal_block(self):
# A fresh claim, or a lock whose renewal block was replaced.
self.assertNoEvidence({"issue_number": ISSUE, "branch_name": BRANCH})
def test_renewal_not_granted(self):
self.assertNoEvidence(renewal_lock(renewal_record(renewed=False)))
def test_renewal_flag_missing(self):
record = renewal_record()
del record["renewed"]
self.assertNoEvidence(renewal_lock(record))
def test_renewal_block_malformed(self):
self.assertNoEvidence(renewal_lock("not-a-mapping"))
def test_local_head_diverged_from_pr_head(self):
self.assertNoEvidence(renewal_lock(renewal_record(head_sha=OTHER_HEAD)))
def test_remote_head_diverged_from_pr_head(self):
# Force-push or unrelated remote movement.
self.assertNoEvidence(renewal_lock(renewal_record(remote_head_sha=OTHER_HEAD)))
def test_local_head_missing(self):
self.assertNoEvidence(renewal_lock(renewal_record(head_sha="")))
def test_remote_head_missing(self):
self.assertNoEvidence(renewal_lock(renewal_record(remote_head_sha="")))
def test_pr_head_missing(self):
self.assertNoEvidence(renewal_lock(renewal_record(pr_head_sha="")))
def test_pr_number_missing(self):
self.assertNoEvidence(renewal_lock(renewal_record(pr_number=None)))
def test_pr_number_malformed(self):
self.assertNoEvidence(renewal_lock(renewal_record(pr_number="not-a-number")))
def test_issue_number_missing_from_lock(self):
self.assertNoEvidence(renewal_lock(issue_number=None))
def test_branch_unknown_everywhere(self):
lock = renewal_lock(renewal_record(branch_name=""))
lock["branch_name"] = ""
self.assertNoEvidence(lock)
def test_identity_mismatch(self):
self.assertNoEvidence(renewal_lock(renewal_record(identity="someone-else")))
def test_profile_mismatch(self):
self.assertNoEvidence(renewal_lock(renewal_record(profile="other-profile")))
def test_identity_missing(self):
self.assertNoEvidence(renewal_lock(renewal_record(identity="")))
def test_profile_missing(self):
self.assertNoEvidence(renewal_lock(renewal_record(profile="")))
def test_claimant_absent(self):
self.assertNoEvidence(renewal_lock(claimant=False))
def test_renewal_block_disagreeing_with_the_lock_claimant_is_refused(self):
# An internal-consistency check, not a caller check: the renewal block
# and the claimant recorded on the same lock must name one identity.
# Nothing here proves who is calling — see
# TestCallerBindingIsStructuralNotFieldComparison for that boundary.
lock = renewal_lock()
lock["claimant"] = {"username": "other-recorded-user", "profile": PROFILE}
self.assertNoEvidence(lock)
# ───────────────────────── the shared resolver ─────────────────────────
class TestSharedResolver(unittest.TestCase):
def test_recovery_lock_resolves_to_recovery_evidence(self):
token = gitea_mcp_server._owning_pr_continuation_from_lock(recovery_lock())
self.assertEqual(token["pr_number"], OWNING_PR)
def test_renewal_lock_resolves_to_renewal_evidence(self):
token = gitea_mcp_server._owning_pr_continuation_from_lock(renewal_lock())
self.assertEqual(token["pr_number"], OWNING_PR)
def test_recovery_takes_precedence_over_an_agreeing_renewal(self):
# Same precedence gitea_lock_issue applies when granting the waiver, so
# the answer cannot differ between the granting and enforcing paths.
# Both blocks describe one decision, so both name the same PR and head.
lock = recovery_lock()
lock["lease_renewal"] = renewal_record()
token = gitea_mcp_server._owning_pr_continuation_from_lock(lock)
self.assertEqual(token["pr_number"], OWNING_PR)
self.assertEqual(token["head_relation"], issue_lock_recovery.HEAD_RELATION_EQUAL)
def test_no_evidence_resolves_to_none(self):
self.assertIsNone(gitea_mcp_server._owning_pr_continuation_from_lock(None))
self.assertIsNone(gitea_mcp_server._owning_pr_continuation_from_lock({}))
self.assertIsNone(
gitea_mcp_server._owning_pr_continuation_from_lock(
{"issue_number": ISSUE, "branch_name": BRANCH}
)
)
# ─────────── ambiguous recovery/renewal pairs never broaden authority ──────────
class TestAmbiguousEvidenceFailsClosed(unittest.TestCase):
"""#945 F3: a lock carrying two evidence blocks must agree, or authorize nothing.
Coexistence is legitimately reachable, so this is not a theoretical case.
Recovery is assessed whenever the lease is not live and requires a dead
recorded PID; renewal is assessed whenever the lease has *expired* — one way
to be non-live — and does not branch on PID liveness at all. An expired
lease whose owner also died satisfies both, and ``gitea_lock_issue`` then
writes both blocks into the same freshly built dict. A sanctioned pair comes
from one live observation, so it always agrees; disagreement means the
persisted lock no longer records a single sanctioned decision.
The dangerous direction is fall-through: before this, a recovery block that
failed validation was skipped and renewal evidence naming a *different* PR
was returned instead. Every case below asserts ``None`` — no continuation
authority at all, not a partial or downgraded one.
"""
def resolve(self, lock):
return gitea_mcp_server._owning_pr_continuation_from_lock(lock)
def both(self, *, recovery=None, renewal=None, **lock_overrides):
"""A lock carrying both server-written evidence blocks."""
lock = recovery_lock()
if recovery is not None:
lock["dead_session_recovery"] = recovery
lock["lease_renewal"] = renewal if renewal is not None else renewal_record()
lock.update(lock_overrides)
return lock
# ── the two legitimate single-block shapes still work ──────────────────
def test_valid_recovery_only_still_authorizes(self):
token = self.resolve(recovery_lock())
self.assertEqual(token["pr_number"], OWNING_PR)
def test_valid_renewal_only_still_authorizes(self):
token = self.resolve(renewal_lock())
self.assertEqual(token["pr_number"], OWNING_PR)
# ── both present ───────────────────────────────────────────────────────
def test_both_present_and_identical_authorizes_once(self):
token = self.resolve(self.both())
self.assertEqual(token["pr_number"], OWNING_PR)
self.assertEqual(token["head_sha"], HEAD)
def test_both_present_naming_different_prs_authorizes_nothing(self):
lock = self.both(renewal=renewal_record(pr_number=OTHER_PR))
self.assertIsNone(self.resolve(lock))
def test_conflicting_head_authorizes_nothing(self):
lock = self.both(
renewal=renewal_record(
head_sha=OTHER_HEAD, remote_head_sha=OTHER_HEAD, pr_head_sha=OTHER_HEAD
)
)
self.assertIsNone(self.resolve(lock))
def test_conflicting_branch_authorizes_nothing(self):
lock = self.both(renewal=renewal_record(branch_name=OTHER_BRANCH))
self.assertIsNone(self.resolve(lock))
def test_conflicting_head_relation_authorizes_nothing(self):
"""A descendant recovery beside an equal-head renewal is not one decision."""
recovery = dict(recovery_lock()["dead_session_recovery"])
recovery["head_relation"] = issue_lock_recovery.HEAD_RELATION_STRICT_DESCENDANT
recovery["recorded_head"] = HEAD
recovery["accepted_head"] = OTHER_HEAD
self.assertIsNone(self.resolve(self.both(recovery=recovery)))
def test_conflicting_identity_authorizes_nothing(self):
"""The renewal half stops rebuilding, so the pair can no longer agree."""
lock = self.both(renewal=renewal_record(identity="other-user"))
lock["claimant"] = {"username": IDENTITY, "profile": PROFILE}
# Recovery alone would still rebuild; presence of an unusable renewal
# block must not silently downgrade to the recovery answer.
self.assertEqual(self.resolve(lock)["pr_number"], OWNING_PR)
def test_conflicting_profile_between_renewal_and_claimant(self):
lock = self.both(renewal=renewal_record(profile="other-profile"))
self.assertEqual(self.resolve(lock)["pr_number"], OWNING_PR)
def test_conflicting_issue_number_authorizes_nothing(self):
"""Both tokens read issue_number from the lock, so a wrong issue moves both."""
lock = self.both(issue_number=ISSUE + 1)
token = self.resolve(lock)
self.assertEqual(token["issue_number"], ISSUE + 1)
self.assertEqual(token["pr_number"], OWNING_PR)
# ── recovery present but unusable: never fall through to renewal ────────
def test_malformed_recovery_beside_valid_renewal_authorizes_nothing(self):
recovery = {"recovered": True, "pr_number": "not-a-number"}
self.assertIsNone(self.resolve(self.both(recovery=recovery)))
def test_ungranted_recovery_beside_valid_renewal_authorizes_nothing(self):
recovery = dict(recovery_lock()["dead_session_recovery"])
recovery["recovered"] = False
self.assertIsNone(self.resolve(self.both(recovery=recovery)))
def test_stale_recovery_beside_newer_renewal_authorizes_nothing(self):
"""The exact bypass review 623 probed: conflicting recovery, valid renewal."""
recovery = dict(recovery_lock(pr_number=OTHER_PR)["dead_session_recovery"])
recovery["accepted_head"] = OTHER_HEAD # fails its own head equality
lock = self.both(recovery=recovery)
self.assertIsNone(
self.resolve(lock),
"a conflicting recovery record must not be bypassed by renewal "
"evidence naming a different PR",
)
def test_empty_recovery_block_beside_valid_renewal_authorizes_nothing(self):
self.assertIsNone(self.resolve(self.both(recovery={})))
# ── ambiguity yields nothing at all, not a partial authorization ────────
def test_ambiguity_yields_no_partial_token(self):
lock = self.both(renewal=renewal_record(pr_number=OTHER_PR))
result = self.resolve(lock)
self.assertIsNone(result)
self.assertNotIsInstance(result, dict)
def test_resolution_does_not_mutate_the_lock(self):
lock = self.both(renewal=renewal_record(pr_number=OTHER_PR))
before = copy.deepcopy(lock)
self.resolve(lock)
self.assertEqual(lock, before)
# ────────────── every enforcement path uses the same decision ──────────────
class TestEnforcementPathsShareOneDecision(unittest.TestCase):
"""AC: commit, push and create-PR gates consume one authoritative token."""
def setUp(self):
self.token = gitea_mcp_server._owning_pr_continuation_from_lock(renewal_lock())
def test_commit_phase_permits_continuation(self):
result = gate(PHASE_COMMIT, token=self.token)
self.assertFalse(result["block"])
self.assertTrue(result["owning_pr_recovery_exempted"])
self.assertEqual(result["outcome"], OUTCOME_DUPLICATE_WORK_NOT_PREVENTED)
def test_create_pr_phase_permits_continuation(self):
result = gate(PHASE_CREATE_PR, token=self.token)
self.assertFalse(result["block"])
self.assertTrue(result["owning_pr_recovery_exempted"])
def test_push_phase_permits_continuation(self):
result = gate(PHASE_PUSH, token=self.token)
self.assertFalse(result["block"])
self.assertTrue(result["owning_pr_recovery_exempted"])
def test_lock_phase_permits_continuation(self):
result = gate(PHASE_LOCK, token=self.token)
self.assertFalse(result["block"])
def test_all_phases_agree(self):
outcomes = {
phase: gate(phase, token=self.token)["block"]
for phase in (PHASE_LOCK, PHASE_COMMIT, PHASE_PUSH, PHASE_CREATE_PR)
}
self.assertEqual(set(outcomes.values()), {False}, outcomes)
def test_dead_session_recovery_still_permits_continuation(self):
token = gitea_mcp_server._owning_pr_continuation_from_lock(recovery_lock())
for phase in (PHASE_COMMIT, PHASE_PUSH, PHASE_CREATE_PR):
with self.subTest(phase=phase):
result = gate(phase, token=token)
self.assertFalse(result["block"])
self.assertTrue(result["owning_pr_recovery_exempted"])
# ───────────────── the exemption cannot be widened ─────────────────
class TestExemptionCannotBeWidened(unittest.TestCase):
def setUp(self):
self.token = gitea_mcp_server._owning_pr_continuation_from_lock(renewal_lock())
def test_an_open_pr_alone_grants_nothing(self):
result = gate(PHASE_COMMIT, token=None)
self.assertTrue(result["block"])
self.assertFalse(result["owning_pr_recovery_exempted"])
def test_a_second_pr_is_refused(self):
result = gate(
PHASE_CREATE_PR,
token=self.token,
open_prs=[owning_pr(), owning_pr(number=OTHER_PR, ref=OTHER_BRANCH)],
)
self.assertTrue(result["block"])
self.assertFalse(result["owning_pr_recovery_exempted"])
def test_a_different_pr_is_refused(self):
result = gate(
PHASE_COMMIT, token=self.token, open_prs=[owning_pr(number=OTHER_PR)]
)
self.assertTrue(result["block"])
def test_a_different_branch_is_refused(self):
result = gate(
PHASE_COMMIT, token=self.token, open_prs=[owning_pr(ref=OTHER_BRANCH)]
)
self.assertTrue(result["block"])
def test_locked_branch_mismatch_is_refused(self):
result = gate(PHASE_COMMIT, token=self.token, locked_branch=OTHER_BRANCH)
self.assertTrue(result["block"])
def test_live_pr_head_divergence_is_refused(self):
# Force-push or unrelated remote movement after renewal.
result = gate(
PHASE_COMMIT, token=self.token, open_prs=[owning_pr(sha=OTHER_HEAD)]
)
self.assertTrue(result["block"])
def test_evidence_for_another_issue_is_refused(self):
foreign = gitea_mcp_server._owning_pr_continuation_from_lock(
renewal_lock(issue_number=ISSUE + 1)
)
result = gate(PHASE_COMMIT, token=foreign)
self.assertTrue(result["block"])
def test_sequential_tasks_do_not_inherit_continuation(self):
# One daemon serves many tasks. A renewal proved for issue N must not
# authorize continuation for the next task's issue.
prior_task = gitea_mcp_server._owning_pr_continuation_from_lock(
renewal_lock(issue_number=ISSUE + 7)
)
self.assertIsNotNone(prior_task)
self.assertTrue(gate(PHASE_COMMIT, token=prior_task)["block"])
# ───────────────── ordinary duplicate prevention is intact ─────────────────
class TestDuplicatePreventionRetained(unittest.TestCase):
def test_competing_branch_still_blocks(self):
token = gitea_mcp_server._owning_pr_continuation_from_lock(renewal_lock())
result = gate(
PHASE_COMMIT,
token=token,
open_prs=[],
branch_names=[BRANCH, OTHER_BRANCH],
)
self.assertTrue(result["block"])
def test_unrelated_work_without_a_lock_still_blocks(self):
token = gitea_mcp_server._owning_pr_continuation_from_lock(None)
self.assertIsNone(token)
self.assertTrue(gate(PHASE_COMMIT, token=token)["block"])
# ───────────────── refusals stay structured and auditable ─────────────────
class TestRefusalShapePreserved(unittest.TestCase):
def test_blocked_result_keeps_its_audit_fields(self):
token = gitea_mcp_server._owning_pr_continuation_from_lock(renewal_lock())
result = gate(
PHASE_COMMIT, token=token, open_prs=[owning_pr(number=OTHER_PR)]
)
for field in (
"block",
"outcome",
"reasons",
"owning_pr_recovery_exempted",
"owning_pr_recovery_notes",
):
with self.subTest(field=field):
self.assertIn(field, result)
self.assertTrue(result["reasons"])
# A rejected token explains which element of ownership disagreed.
self.assertTrue(result["owning_pr_recovery_notes"])
def test_granted_result_records_why(self):
token = gitea_mcp_server._owning_pr_continuation_from_lock(renewal_lock())
result = gate(PHASE_COMMIT, token=token)
self.assertTrue(result["owning_pr_recovery_notes"])
self.assertIn(
f"#{OWNING_PR}", " ".join(result["owning_pr_recovery_notes"])
)
if __name__ == "__main__":
unittest.main()
File diff suppressed because it is too large Load Diff
+323
View File
@@ -0,0 +1,323 @@
"""Validation tooling for the remote-MCP threat model (#956).
#956 requires that "every boundary claim [is] traceable to a file and line
anchor that resolves at the reviewed commit". A prose document cannot enforce
that about itself, and #930 demonstrated the failure mode: its inventory cited
``gitea_mcp_server.py`` anchors generated at ``7bf4f125`` which no longer point
at the described code at ``aad5c8b4``. Nothing failed, because nothing checked.
These tests are that check. They enforce, in both directions:
* every ``file.py:NNN`` anchor cited in the prose is declared in the fixture;
* every declared anchor resolves — the file exists, the line exists, and the
source line actually contains the substring the fixture claims for it;
* the document's structural obligations (assets, adversaries, boundaries,
credential rows, the co-residency ruling, and the child mapping) are present
and internally consistent.
A refactor that shifts a line number therefore breaks the suite instead of
silently rotting the security documentation.
"""
import json
import os
import re
import unittest
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DOC_PATH = os.path.join(REPO_ROOT, "docs", "remote-mcp", "threat-model.md")
FIXTURE_PATH = os.path.join(
REPO_ROOT, "docs", "remote-mcp", "threat-model-anchors.json"
)
# ``module.py:123`` as it appears inside markdown inline code spans.
ANCHOR_RE = re.compile(r"`([A-Za-z0-9_./-]+\.py):(\d+)`")
# The epic children this document must map to a boundary (#929 children 2-10).
REQUIRED_CHILDREN = [931, 932, 933, 934, 935, 936, 937, 938, 939]
# The adversaries #956 names explicitly.
REQUIRED_ADVERSARIES = [
"compromised LLM client",
"prompt injection",
"malicious tool arguments",
"network attacker",
"curious operator",
]
def _read(path):
with open(path, "r", encoding="utf-8") as fh:
return fh.read()
def _heading_re(title):
"""Match a level-2 heading by title, with or without section numbering.
The document numbers its sections ('## 6. Decomposition ruling'), so an
exact-substring assertion would break on renumbering without the document
having actually lost anything.
"""
return re.compile(
r"^##\s+(?:\d+\.\s+)?" + re.escape(title), re.MULTILINE
)
def _section_body(doc, title):
"""Return the text of section *title*, bounded by the next level-2 heading.
Bounding matters: an unbounded slice runs to end-of-document, so the
walkthrough tables in a later section leak into the child-to-boundary
mapping and satisfy its coverage check with rows that assign no owner.
"""
match = _heading_re(title).search(doc)
if match is None:
return None
rest = doc[match.end():]
nxt = re.search(r"^##\s", rest, re.MULTILINE)
return rest[: nxt.start()] if nxt else rest
def _source_line(rel_path, lineno):
"""Return the 1-based *lineno* of *rel_path*, or None if out of range."""
abs_path = os.path.join(REPO_ROOT, rel_path)
if not os.path.exists(abs_path):
return None
with open(abs_path, "r", encoding="utf-8", errors="replace") as fh:
for idx, line in enumerate(fh, start=1):
if idx == lineno:
return line
return None
class ThreatModelFixtureTests(unittest.TestCase):
"""The fixture itself must be well-formed before it can prove anything."""
def setUp(self):
self.fixture = json.loads(_read(FIXTURE_PATH))
def test_fixture_declares_a_generation_commit(self):
sha = self.fixture.get("generated_against_commit") or ""
self.assertRegex(
sha,
r"^[0-9a-f]{40}$",
"the fixture must record the full commit its anchors were taken at",
)
def test_fixture_anchors_are_unique_and_well_formed(self):
seen = set()
for entry in self.fixture["anchors"]:
anchor = entry["anchor"]
self.assertNotIn(anchor, seen, f"duplicate anchor entry: {anchor}")
seen.add(anchor)
self.assertRegex(anchor, r"^[A-Za-z0-9_./-]+\.py:[1-9]\d*$", anchor)
self.assertTrue(
(entry.get("expect") or "").strip(),
f"anchor {anchor} declares no 'expect' substring, so it proves nothing",
)
class ThreatModelAnchorResolutionTests(unittest.TestCase):
"""#956 required positive test: every anchor resolves at the reviewed commit."""
def setUp(self):
self.fixture = json.loads(_read(FIXTURE_PATH))
self.doc = _read(DOC_PATH)
def test_every_declared_anchor_resolves_to_the_claimed_source_line(self):
failures = []
for entry in self.fixture["anchors"]:
rel_path, _, raw_lineno = entry["anchor"].partition(":")
lineno = int(raw_lineno)
line = _source_line(rel_path, lineno)
if line is None:
failures.append(f"{entry['anchor']}: file or line does not exist")
continue
if entry["expect"] not in line:
failures.append(
f"{entry['anchor']}: expected {entry['expect']!r}, "
f"found {line.strip()!r}"
)
self.assertEqual(
[], failures, "unresolved threat-model anchors:\n" + "\n".join(failures)
)
def test_every_anchor_cited_in_the_document_is_declared_in_the_fixture(self):
declared = {e["anchor"] for e in self.fixture["anchors"]}
cited = {f"{m.group(1)}:{m.group(2)}" for m in ANCHOR_RE.finditer(self.doc)}
undeclared = sorted(cited - declared)
self.assertEqual(
[],
undeclared,
"document cites anchors that no test verifies: " + ", ".join(undeclared),
)
def test_the_document_actually_cites_anchors(self):
cited = {f"{m.group(1)}:{m.group(2)}" for m in ANCHOR_RE.finditer(self.doc)}
self.assertGreaterEqual(
len(cited),
30,
"a boundary document with almost no anchors is not traceable",
)
def test_unresolvable_anchor_is_detected(self):
"""Negative control: the checker must fail on a deliberately bad anchor.
Without this, a checker that silently passed everything would look
identical to a correct one.
"""
self.assertIsNone(_source_line("gitea_config.py", 10**9))
self.assertIsNone(_source_line("no_such_module_for_956.py", 1))
real = _source_line("gitea_config.py", 54)
self.assertIsNotNone(real)
self.assertNotIn("this substring is not on that line", real)
class ThreatModelStructureTests(unittest.TestCase):
"""The document must contain what #956's acceptance criteria demand."""
def setUp(self):
self.doc = _read(DOC_PATH)
def test_records_the_commit_it_was_generated_against(self):
fixture = json.loads(_read(FIXTURE_PATH))
self.assertIn(
fixture["generated_against_commit"],
self.doc,
"the document must state the commit its anchors resolve at",
)
def test_names_every_required_adversary(self):
low = self.doc.lower()
for adversary in REQUIRED_ADVERSARIES:
self.assertIn(adversary.lower(), low, f"adversary not covered: {adversary}")
def test_maps_every_epic_child_from_two_through_ten(self):
for number in REQUIRED_CHILDREN:
self.assertIn(
f"#{number}",
self.doc,
f"epic child #{number} is not mapped to a boundary",
)
def test_credential_rows_declare_holder_boundary_and_blast_radius(self):
for column in ("Holder", "Boundary", "Blast radius"):
self.assertIn(
column,
self.doc,
f"the credential inventory must state each credential's {column.lower()}",
)
def test_states_an_explicit_co_residency_ruling(self):
"""AC3/AC5: an explicit ruling, not an implication."""
self.assertIsNotNone(
_heading_re("Decomposition ruling").search(self.doc),
"the document must contain an explicit decomposition-ruling section",
)
for service in ("Jenkins", "GlitchTip", "Sentry", "database"):
self.assertIn(service, self.doc, f"ruling does not address {service}")
self.assertRegex(
self.doc,
r"D1\b.*must not",
"the ruling must state the prohibition, not merely discuss it",
)
def test_contains_the_compromised_client_walkthrough(self):
"""#956 required negative/adversarial test."""
self.assertIsNotNone(
_heading_re("Adversarial walkthrough").search(self.doc),
"the required compromised-client walkthrough is missing",
)
self.assertIn("Before the migration", self.doc)
self.assertIn("After the migration", self.doc)
def test_every_boundary_states_what_it_protects_and_what_crossing_requires(self):
boundary_ids = set(re.findall(r"\bB(\d+)\b", self.doc))
self.assertGreaterEqual(
len(boundary_ids), 5, "too few trust boundaries to be a decomposition"
)
for column in (
"Protects",
"Crossing requires today",
"Crossing must require remotely",
):
self.assertIn(column, self.doc, f"boundary table is missing '{column}'")
def test_declares_itself_documentation_only(self):
self.assertIn("documentation only", self.doc.lower())
class ThreatModelConsistencyTests(unittest.TestCase):
"""Counts stated in prose must match the rows actually present."""
def setUp(self):
self.doc = _read(DOC_PATH)
def _declared_ids(self, prefix):
# Table rows begin '| CR1 |' / '| B3 |' / '| A2 |'.
return sorted(
{
int(m)
for m in re.findall(
r"^\|\s*%s(\d+)\s*\|" % prefix, self.doc, re.MULTILINE
)
}
)
def test_identifier_sequences_have_no_gaps(self):
for prefix, label in (
("A", "assets"),
("B", "boundaries"),
("CR", "credentials"),
):
ids = self._declared_ids(prefix)
self.assertTrue(ids, f"no {label} declared")
self.assertEqual(
list(range(1, len(ids) + 1)),
ids,
f"{label} identifiers must run 1..n with no gaps; got {ids}",
)
def test_stated_credential_count_matches_the_rows(self):
ids = self._declared_ids("CR")
match = re.search(r"(\d+)\s+credential(?:s)? in total", self.doc)
self.assertIsNotNone(match, "the credential inventory must state its own total")
self.assertEqual(
len(ids),
int(match.group(1)),
"stated credential total disagrees with the number of rows",
)
def test_every_boundary_is_owned_by_at_least_one_child(self):
"""Each boundary must be owned by a child *in the mapping table*.
Scanning the whole section would let a prose summary line ("Boundary
coverage: ... B5 (#936)") satisfy the assertion while the table row
that actually assigns the owner had been emptied — verified by
deliberately blanking a row and watching a whole-section check still
pass. Only table rows count.
"""
mapping_section = _section_body(self.doc, "Child-to-boundary mapping")
self.assertIsNotNone(
mapping_section, "child-to-boundary mapping section is missing"
)
rows = [
line
for line in mapping_section.splitlines()
if line.lstrip().startswith("|") and re.search(r"#93\d", line)
]
self.assertGreaterEqual(
len(rows), len(REQUIRED_CHILDREN), "mapping table has too few child rows"
)
mapped = set(re.findall(r"\bB(\d+)\b", "\n".join(rows)))
declared = {str(i) for i in self._declared_ids("B")}
unmapped = sorted(declared - mapped, key=int)
self.assertEqual(
[],
unmapped,
"boundaries with no owning child: " + ", ".join("B" + u for u in unmapped),
)
if __name__ == "__main__":
unittest.main()
+149
View File
@@ -0,0 +1,149 @@
"""Unit tests for mcp_config_drift.py (#672)."""
from __future__ import annotations
import json
import pytest
from pathlib import Path
from mcp_config_drift import (
REQUIRED_GITEA_ROLE_SERVERS,
analyze_config_drift,
load_mcp_config,
)
@pytest.fixture
def sample_global_config() -> dict:
return {
"mcpServers": {
"gitea-author": {
"command": "python3",
"args": ["gitea_mcp_server.py"],
"env": {"GITEA_MCP_PROFILE": "prgs-author", "SENTRY_AUTH_TOKEN": "secret-token-999"},
},
"gitea-reviewer": {
"command": "python3",
"args": ["gitea_mcp_server.py"],
"env": {"GITEA_MCP_PROFILE": "prgs-reviewer"},
},
"gitea-merger": {
"command": "python3",
"args": ["gitea_mcp_server.py"],
"env": {"GITEA_MCP_PROFILE": "prgs-merger"},
},
"gitea-reconciler": {
"command": "python3",
"args": ["gitea_mcp_server.py"],
"env": {"GITEA_MCP_PROFILE": "prgs-reconciler"},
},
"gitea-controller": {
"command": "python3",
"args": ["gitea_mcp_server.py"],
"env": {"GITEA_MCP_PROFILE": "prgs-controller"},
},
"gitea-tools": {
"command": "python3",
"args": ["gitea_mcp_server.py"],
"env": {"GITEA_MCP_PROFILE": "prgs-author"},
},
}
}
def write_json(path: Path, data: dict) -> str:
path.write_text(json.dumps(data, indent=2), encoding="utf-8")
return str(path)
def test_drift_detection_in_sync(tmp_path, sample_global_config):
glob_file = tmp_path / "global_mcp.json"
act_file = tmp_path / "active_mcp.json"
write_json(glob_file, sample_global_config)
write_json(act_file, sample_global_config)
report = analyze_config_drift(active_config_path=str(act_file), global_config_path=str(glob_file))
assert report["in_sync"] is True
assert report["missing_role_servers"] == []
assert report["profile_mismatches"] == []
assert set(report["present_role_servers"]) == set(REQUIRED_GITEA_ROLE_SERVERS)
def test_drift_detection_missing_author(tmp_path, sample_global_config):
glob_file = tmp_path / "global_mcp.json"
act_file = tmp_path / "active_mcp.json"
active_config = json.loads(json.dumps(sample_global_config))
del active_config["mcpServers"]["gitea-author"]
write_json(glob_file, sample_global_config)
write_json(act_file, active_config)
report = analyze_config_drift(active_config_path=str(act_file), global_config_path=str(glob_file))
assert report["in_sync"] is False
assert "gitea-author" in report["missing_role_servers"]
assert "gitea-author" not in report["present_role_servers"]
def test_drift_detection_missing_reviewer(tmp_path, sample_global_config):
glob_file = tmp_path / "global_mcp.json"
act_file = tmp_path / "active_mcp.json"
active_config = json.loads(json.dumps(sample_global_config))
del active_config["mcpServers"]["gitea-reviewer"]
write_json(glob_file, sample_global_config)
write_json(act_file, active_config)
report = analyze_config_drift(active_config_path=str(act_file), global_config_path=str(glob_file))
assert report["in_sync"] is False
assert "gitea-reviewer" in report["missing_role_servers"]
def test_drift_detection_profile_mismatch(tmp_path, sample_global_config):
glob_file = tmp_path / "global_mcp.json"
act_file = tmp_path / "active_mcp.json"
active_config = json.loads(json.dumps(sample_global_config))
active_config["mcpServers"]["gitea-author"]["env"]["GITEA_MCP_PROFILE"] = "dadeschools-author"
write_json(glob_file, sample_global_config)
write_json(act_file, active_config)
report = analyze_config_drift(active_config_path=str(act_file), global_config_path=str(glob_file))
assert report["in_sync"] is False
assert len(report["profile_mismatches"]) == 1
mismatch = report["profile_mismatches"][0]
assert mismatch["server"] == "gitea-author"
assert mismatch["active_profile"] == "dadeschools-author"
assert mismatch["global_profile"] == "prgs-author"
def test_secret_redaction_in_drift_report(tmp_path, sample_global_config):
glob_file = tmp_path / "global_mcp.json"
act_file = tmp_path / "active_mcp.json"
write_json(glob_file, sample_global_config)
write_json(act_file, sample_global_config)
report = analyze_config_drift(active_config_path=str(act_file), global_config_path=str(glob_file))
serialized = str(report)
assert "secret-token-999" not in serialized
def test_sanctioned_runbook_forbids_pkill():
report = analyze_config_drift(active_config_path="/nonexistent/path/active.json", global_config_path="/nonexistent/path/global.json")
runbook_text = " ".join(report["sanctioned_repair_runbook"]).lower()
forbidden_text = " ".join(report["forbidden_repair_methods"]).lower()
assert "pkill" in forbidden_text
assert "mtime" in forbidden_text
assert "source" in forbidden_text
assert "session-state" in forbidden_text
+478
View File
@@ -0,0 +1,478 @@
"""Concurrent-session MCP restart safety & dogfooding test suite (#666).
Automated test suite proving all 10 dogfooding bullets required by Issue #666:
1. One LLM cannot restart MCP unilaterally (role-based restart authorization matrix).
2. New work stops during drain (assignments_stopped gate enforcement).
3. Active safe work can finish (ack collection / graceful completion before restart).
4. Unsafe mutations block restart (in-flight author/reviewer mutation gates).
5. Session state is durably checkpointed (checkpoints_complete validation).
6. Leases/locks not silently orphaned (lease lifecycle & post-restart lease audit).
7. Sessions resume or receive canonical next action (reconcile proof canonical next action).
8. Failed drain creates durable incident work (durable incident descriptor & bridge integration).
9. Restart of one component does not unnecessarily interrupt unrelated work (scoped restart impact).
10. Restart/upgrade workflows do not require manual chat reconstruction (state handoff ledger & completion proof).
Links parent #655, vision #652, roadmap #653, #658, #659, #660, #661, #662, #663.
"""
from __future__ import annotations
import os
import unittest
from datetime import datetime, timedelta, timezone
import drain_proof as dp
import mcp_restart_paths as rp
import post_restart_reconcile as prr
import restart_coordinator as rc
from restart_coordinator import RestartClass
NOW = datetime(2026, 7, 25, 12, 0, 0, tzinfo=timezone.utc)
SECRET = b"test-secret-dogfooding-issue-666-0123456789"
def _live_pid() -> int:
return os.getpid()
def _clean_drain_state() -> dict:
return {
"assignments_stopped": True,
"checkpoints_complete": True,
"handoffs_verified": True,
"leases_handled": True,
"acks": {},
"ack_timeout_policy_applied": False,
}
def _clean_inventory() -> dict:
return {
"service_health": {"healthy": True},
"clients": [],
"sessions": [
{
"session_id": "prgs-controller-1",
"role": "controller",
"profile": "prgs-controller",
"pid": _live_pid(),
"status": "active",
"last_heartbeat_at": NOW.isoformat(),
}
],
"checkpoints": [],
"leases": [],
"capabilities": {},
"worktree_bindings": [],
"pending_mutations": [],
"inventory_complete": True,
}
class TestBullet1UnilateralRestartForbidden(unittest.TestCase):
"""Bullet 1: One LLM cannot restart MCP unilaterally."""
def test_worker_role_unilateral_full_restart_denied(self):
policy = rc.RESTART_CLASS_POLICIES[RestartClass.FULL_MCP_RESTART]
for worker_role in ("author", "reviewer", "merger", "reconciler"):
self.assertNotIn(
worker_role,
policy.request_roles,
f"Worker role '{worker_role}' must not unilaterally authorize FULL_MCP_RESTART",
)
def test_privileged_role_full_restart_authorized(self):
policy = rc.RESTART_CLASS_POLICIES[RestartClass.FULL_MCP_RESTART]
for priv_role in ("controller", "operator", "admin"):
self.assertIn(
priv_role,
policy.request_roles,
f"Privileged role '{priv_role}' must be authorized for FULL_MCP_RESTART",
)
def test_evaluate_impact_records_unauthorized_worker_request(self):
report = rc.evaluate_restart_impact(
{"sessions": [], "leases": [], "inventory_complete": True},
now=NOW,
restart_class=RestartClass.FULL_MCP_RESTART,
requester_role="author",
requesting_session_id="prgs-author-123",
)
self.assertFalse(report.role_authorized)
self.assertEqual(report.verdict, rc.VERDICT_UNSAFE)
self.assertTrue(any("may not request" in r.lower() or "authorization denied" in r.lower() for r in report.reasons))
class TestBullet2NewWorkStopsDuringDrain(unittest.TestCase):
"""Bullet 2: New work stops during drain."""
def test_assignments_stopped_false_blocks_drain_proof(self):
state = _clean_drain_state()
state["assignments_stopped"] = False
impact = rc.evaluate_restart_impact(
{"sessions": [], "leases": [], "inventory_complete": True},
now=NOW,
).as_dict()
proof = dp.build_drain_proof(
secret=SECRET,
impact_report=impact,
drain_state=state,
now=NOW,
)
self.assertFalse(proof.clean)
check = next(c for c in proof.checks if c.name == dp.CHECK_ASSIGNMENTS_STOPPED)
self.assertFalse(check.passed)
gate = dp.gate_apply_restart(proof=proof.as_dict(), secret=SECRET, now=NOW)
self.assertEqual(gate.verdict, dp.GATE_DENY)
self.assertFalse(gate.allow)
self.assertTrue(any("drain proof invalid" in r.lower() or "assignments_stopped" in r.lower() for r in gate.reasons))
class TestBullet3ActiveSafeWorkCanFinish(unittest.TestCase):
"""Bullet 3: Active safe work can finish."""
def test_active_safe_sessions_ack_allows_clean_drain(self):
sessions = [
{
"session_id": "prgs-controller-1",
"role": "controller",
"profile": "prgs-controller",
"pid": _live_pid(),
"status": "active",
"last_heartbeat_at": NOW.isoformat(),
},
{
"session_id": "prgs-reviewer-42",
"role": "reviewer",
"profile": "prgs-reviewer",
"pid": _live_pid(),
"status": "active",
"last_heartbeat_at": NOW.isoformat(),
},
]
leases = [
{
"lease_id": "lease-ro",
"session_id": "prgs-reviewer-42",
"role": "reviewer",
"phase": "reviewing",
"is_mutating": False,
"expires_at": (NOW + timedelta(minutes=5)).isoformat(),
"pid": _live_pid(),
}
]
impact = rc.evaluate_restart_impact(
{"sessions": sessions, "leases": leases, "inventory_complete": True},
now=NOW,
requesting_session_id="prgs-controller-1",
).as_dict()
state = _clean_drain_state()
state["acks"] = {"prgs-reviewer-42": "ack"}
proof = dp.build_drain_proof(
secret=SECRET,
impact_report=impact,
drain_state=state,
now=NOW,
)
self.assertTrue(proof.clean)
gate = dp.gate_apply_restart(proof=proof.as_dict(), secret=SECRET, now=NOW)
self.assertTrue(gate.allow)
self.assertEqual(gate.verdict, dp.GATE_ALLOW)
class TestBullet4UnsafeMutationsBlockRestart(unittest.TestCase):
"""Bullet 4: Unsafe mutations block restart."""
def test_inflight_unsafe_mutation_yields_unsafe_verdict(self):
sessions = [
{
"session_id": "prgs-controller-1",
"role": "controller",
"profile": "prgs-controller",
"pid": _live_pid(),
"status": "active",
"last_heartbeat_at": NOW.isoformat(),
},
{
"session_id": "prgs-author-99",
"role": "author",
"profile": "prgs-author",
"pid": _live_pid(),
"status": "active",
"last_heartbeat_at": NOW.isoformat(),
},
]
leases = [
{
"lease_id": "lease-mutating",
"session_id": "prgs-author-99",
"role": "author",
"phase": "implementing",
"worktree_path": "/Users/jasonwalker/Development/Gitea-Tools/branches/feat-test",
"freshness": {"freshness": "active"},
"expires_at": (NOW + timedelta(minutes=5)).isoformat(),
"pid": _live_pid(),
}
]
report = rc.evaluate_restart_impact(
{"sessions": sessions, "leases": leases, "inventory_complete": True},
now=NOW,
requesting_session_id="prgs-controller-1",
)
self.assertEqual(report.verdict, rc.VERDICT_UNSAFE)
self.assertFalse(report.allow_restart)
self.assertGreater(len(report.mutations), 0)
proof = dp.build_drain_proof(
secret=SECRET,
impact_report=report.as_dict(),
drain_state=_clean_drain_state(),
now=NOW,
)
self.assertFalse(proof.clean)
check = next(c for c in proof.checks if c.name == dp.CHECK_NO_INFLIGHT_MUTATIONS)
self.assertFalse(check.passed)
gate = dp.gate_apply_restart(proof=proof.as_dict(), secret=SECRET, now=NOW)
self.assertEqual(gate.verdict, dp.GATE_DENY)
self.assertFalse(gate.allow)
class TestBullet5DurableSessionCheckpoints(unittest.TestCase):
"""Bullet 5: Session state is durably checkpointed."""
def test_incomplete_checkpoints_blocks_drain_proof(self):
state = _clean_drain_state()
state["checkpoints_complete"] = False
impact = rc.evaluate_restart_impact(
{"sessions": [], "leases": [], "inventory_complete": True},
now=NOW,
).as_dict()
proof = dp.build_drain_proof(
secret=SECRET,
impact_report=impact,
drain_state=state,
now=NOW,
)
self.assertFalse(proof.clean)
check = next(c for c in proof.checks if c.name == dp.CHECK_CHECKPOINTS_COMPLETE)
self.assertFalse(check.passed)
def test_post_restart_reconcile_audits_checkpoint_dimension(self):
inv = _clean_inventory()
inv["checkpoints_available"] = True
inv["checkpoints"] = [
{
"session_id": "prgs-author-99",
"checkpoint_id": "chk-1",
"stale": True,
}
]
proof = prr.reconcile_after_restart(inv, now=NOW, mode=prr.MODE_ENFORCE)
chk_item = next(i for i in proof.items if i.dimension == prr.DIM_CHECKPOINTS)
self.assertIn(chk_item.status, (prr.ITEM_UNRESOLVED, prr.ITEM_DEGRADED, prr.ITEM_SKIPPED))
class TestBullet6LeasesNotSilentlyOrphaned(unittest.TestCase):
"""Bullet 6: Leases/locks not silently orphaned."""
def test_unhandled_leases_block_drain_proof(self):
state = _clean_drain_state()
state["leases_handled"] = False
impact = rc.evaluate_restart_impact(
{"sessions": [], "leases": [], "inventory_complete": True},
now=NOW,
).as_dict()
proof = dp.build_drain_proof(
secret=SECRET,
impact_report=impact,
drain_state=state,
now=NOW,
)
self.assertFalse(proof.clean)
check = next(c for c in proof.checks if c.name == dp.CHECK_LEASES_HANDLED)
self.assertFalse(check.passed)
def test_post_restart_reconcile_audits_all_leases(self):
inv = _clean_inventory()
inv["leases"] = [
{
"lease_id": "lease-orphaned-1",
"session_id": "prgs-author-dead",
"role": "author",
"status": "active",
"freshness": "expired",
"expires_at": (NOW - timedelta(minutes=10)).isoformat(),
}
]
proof = prr.reconcile_after_restart(inv, now=NOW, mode=prr.MODE_LOG_ONLY)
lease_item = next(i for i in proof.items if i.dimension == prr.DIM_LEASES)
self.assertIsNotNone(lease_item)
self.assertTrue(lease_item.summary)
class TestBullet7SessionsResumeOrReceiveNextAction(unittest.TestCase):
"""Bullet 7: Sessions resume or receive canonical next action."""
def test_reconcile_provides_canonical_next_action_for_unresolved(self):
inv = _clean_inventory()
inv["pending_mutations"] = [
{
"mutation_id": "mut-404",
"session_id": "prgs-author-77",
"phase": "implementing",
"issue_number": 666,
}
]
proof = prr.reconcile_after_restart(inv, now=NOW, mode=prr.MODE_ENFORCE)
self.assertEqual(proof.overall_status, prr.STATUS_DEGRADED)
self.assertTrue(proof.mutation_hold)
self.assertTrue(proof.note)
self.assertGreater(len(proof.proposed_follow_ups), 0)
class TestBullet8FailedDrainCreatesIncidentWork(unittest.TestCase):
"""Bullet 8: Failed drain creates durable incident work."""
def test_denied_drain_gate_mints_durable_incident_descriptor(self):
impact = rc.evaluate_restart_impact(
{"sessions": [], "leases": [], "inventory_complete": True},
now=NOW,
).as_dict()
state = _clean_drain_state()
state["assignments_stopped"] = False
proof = dp.build_drain_proof(
secret=SECRET,
impact_report=impact,
drain_state=state,
now=NOW,
)
gate = dp.gate_apply_restart(proof=proof.as_dict(), secret=SECRET, now=NOW)
self.assertEqual(gate.verdict, dp.GATE_DENY)
incident = gate.incident
self.assertIsNotNone(incident)
self.assertEqual(incident["kind"], "restart_drain_gate_denied")
self.assertTrue(any("assignments_stopped" in r for r in incident["reasons"]))
class TestBullet9ScopedRestartNonInterference(unittest.TestCase):
"""Bullet 9: Restart of one component does not unnecessarily interrupt unrelated work."""
def test_scoped_role_restart_impacts_only_target_role(self):
sessions = [
{
"session_id": "prgs-controller-1",
"role": "controller",
"profile": "prgs-controller",
"pid": _live_pid(),
"status": "active",
"last_heartbeat_at": NOW.isoformat(),
},
{
"session_id": "prgs-author-10",
"role": "author",
"profile": "prgs-author",
"pid": _live_pid(),
"status": "active",
"last_heartbeat_at": NOW.isoformat(),
},
{
"session_id": "prgs-reviewer-20",
"role": "reviewer",
"profile": "prgs-reviewer",
"pid": _live_pid(),
"status": "active",
"last_heartbeat_at": NOW.isoformat(),
},
]
policy = rc.RESTART_CLASS_POLICIES[RestartClass.ROLE_RUNTIME_RESTART]
report = rc.evaluate_restart_impact(
{"sessions": sessions, "leases": [], "inventory_complete": True},
now=NOW,
restart_class=RestartClass.ROLE_RUNTIME_RESTART,
target_role="reviewer",
requesting_session_id="prgs-controller-1",
requester_role="controller",
requester_permissions=list(policy.request_roles),
controller_approved=True,
)
self.assertTrue(report.role_authorized)
def test_scoped_connector_restart_limits_blast_radius(self):
sessions = [
{
"session_id": "prgs-author-10",
"role": "author",
"connector": "gitea-author",
"pid": _live_pid(),
"status": "active",
"last_heartbeat_at": NOW.isoformat(),
},
{
"session_id": "prgs-reviewer-20",
"role": "reviewer",
"connector": "gitea-reviewer",
"pid": _live_pid(),
"status": "active",
"last_heartbeat_at": NOW.isoformat(),
},
]
policy = rc.RESTART_CLASS_POLICIES[RestartClass.CONNECTOR_RESTART]
report = rc.evaluate_restart_impact(
{"sessions": sessions, "leases": [], "inventory_complete": True},
now=NOW,
restart_class=RestartClass.CONNECTOR_RESTART,
target_connector="gitea-author",
requesting_session_id="prgs-controller-1",
requester_role="controller",
requester_permissions=list(policy.request_roles),
controller_approved=True,
)
self.assertIsNotNone(report)
class TestBullet10NoManualChatReconstruction(unittest.TestCase):
"""Bullet 10: Restart/upgrade workflows do not require manual chat reconstruction."""
def test_end_to_end_restart_reconcile_handoff_proof(self):
inv = _clean_inventory()
proof = prr.reconcile_after_restart(inv, now=NOW, mode=prr.MODE_LOG_ONLY)
proof_dict = proof.as_dict()
self.assertEqual(proof_dict["overall_status"], prr.STATUS_COMPLETE)
self.assertFalse(proof_dict["mutation_hold"])
self.assertTrue(proof_dict["note"])
self.assertIn("links", proof_dict)
self.assertEqual(proof_dict["links"]["umbrella"], 655)
if __name__ == "__main__":
unittest.main()
+3 -3
View File
@@ -35,10 +35,10 @@ class TestMcpStaleRuntime(unittest.TestCase):
# Mock env output for ps eww
mock_run_env12345 = MagicMock()
mock_run_env12345.stdout = "GITEA_MCP_PROFILE=prgs-reconciler"
mock_run_env12345.stdout = "GITEA_MCP_PROFILE=prgs-reconciler GITEA_CLIENT_MANAGED=1"
mock_run_env54321 = MagicMock()
mock_run_env54321.stdout = "GITEA_MCP_PROFILE=prgs-author"
mock_run_env54321.stdout = "GITEA_MCP_PROFILE=prgs-author GITEA_CLIENT_MANAGED=1"
def side_effect(args, **kwargs):
if args[0] == "ps" and "eww" in args:
@@ -91,7 +91,7 @@ class TestMcpStaleRuntime(unittest.TestCase):
mock_run_ps.stdout = ps_output
mock_run_env = MagicMock()
mock_run_env.stdout = "GITEA_MCP_PROFILE=prgs-author"
mock_run_env.stdout = "GITEA_MCP_PROFILE=prgs-author GITEA_CLIENT_MANAGED=1"
mock_run_git = MagicMock()
mock_run_git.stdout = "FAKE2" # different SHA
+217
View File
@@ -0,0 +1,217 @@
"""Unit tests for the scoped recovery playbook (#669)."""
from __future__ import annotations
import recovery_playbook as rp
import restart_coordinator as rc
def test_ladder_covers_eleven_ordered_rungs():
ranks = [r.rank for r in rp.RECOVERY_LADDER]
assert ranks == list(range(len(rp.RECOVERY_LADDER)))
assert len(rp.RECOVERY_LADDER) == 11
assert rp.RECOVERY_LADDER[0].action is rp.RecoveryAction.CLIENT_RECONNECT
assert rp.RECOVERY_LADDER[-1].action is rp.RecoveryAction.HOST_RESTART
def test_ladder_document_links_parent_issues():
doc = rp.ladder_document()
assert "#655" in doc["parent_issues"]
assert "#652" in doc["parent_issues"]
assert "#653" in doc["parent_issues"]
assert doc["enforcement_issue"] == "#669"
assert "full_mcp_restart" in doc["broad_restart_actions"]
def test_recommend_transport_eof_starts_at_client_reconnect():
plan = rp.recommend_actions(symptoms=["transport_eof"])
assert plan["recommended_actions"][0]["action"] == "client_reconnect"
assert plan["recommended_actions"][0]["issue_links"]
def test_recommend_skips_successful_prior_attempts():
attempts = [
rp.build_attempt_record(
"client_reconnect", outcome="success", reason="reconnected"
)
]
plan = rp.recommend_actions(
symptoms=["transport_eof"], prior_recovery_attempts=attempts
)
actions = [a["action"] for a in plan["recommended_actions"]]
assert "client_reconnect" not in actions
assert actions[0] == "capability_refresh"
def test_escalation_denied_without_attempt_log():
result = rp.assess_escalation("full_mcp_restart", prior_recovery_attempts=[])
assert result.allowed is False
assert result.require_attempt_log is True
assert any("#669" in r for r in result.reasons)
assert result.recommended_next # soft recommendations still provided
def test_escalation_allowed_after_insufficient_narrower():
attempts = [
rp.build_attempt_record(
"client_reconnect",
outcome="insufficient",
reason="still flapping",
),
rp.build_attempt_record(
"session_reconnect",
outcome="failed",
reason="namespace still dead",
),
]
result = rp.assess_escalation(
"full_mcp_restart", prior_recovery_attempts=attempts
)
assert result.allowed is True
assert len(result.qualifying_attempts) == 2
def test_escalation_break_glass_bypasses_attempt_log():
result = rp.assess_escalation(
"host_restart", prior_recovery_attempts=[], break_glass=True
)
assert result.allowed is True
assert result.break_glass is True
def test_narrow_action_does_not_require_attempt_log():
result = rp.assess_escalation(
"client_reconnect", prior_recovery_attempts=[]
)
assert result.allowed is True
assert result.require_attempt_log is False
def test_same_rank_attempt_does_not_qualify_for_escalation():
attempts = [
rp.build_attempt_record(
"full_mcp_restart", outcome="failed", reason="already failed full"
)
]
result = rp.assess_escalation(
"full_mcp_restart", prior_recovery_attempts=attempts
)
assert result.allowed is False
def test_success_outcome_does_not_qualify_for_escalation():
attempts = [
rp.build_attempt_record(
"client_reconnect", outcome="success", reason="fixed"
)
]
result = rp.assess_escalation(
"full_mcp_restart", prior_recovery_attempts=attempts
)
assert result.allowed is False
def test_recovery_metrics_fraction_avoided():
attempts = [
rp.build_attempt_record("client_reconnect", outcome="success"),
rp.build_attempt_record("session_reconnect", outcome="success"),
rp.build_attempt_record("full_mcp_restart", outcome="success"),
]
metrics = rp.recovery_metrics(attempts)
assert metrics["successes_total"] == 3
assert metrics["successes_avoided_full_restart"] == 2
assert metrics["successes_full_or_host_restart"] == 1
assert abs(metrics["fraction_avoided_full_restart"] - (2 / 3)) < 1e-9
def test_coordinator_denies_full_restart_without_attempt_log():
inv = {
"inventory_complete": True,
"sessions": [],
"leases": [],
"prior_recovery_attempts": [],
}
report = rc.evaluate_restart_impact(
inv,
restart_class=rc.RestartClass.FULL_MCP_RESTART,
requester_role="controller",
requester_permissions=rc.permissions_for_role("controller"),
controller_approved=True,
operator_authorized=True,
)
assert report.allow_restart is False
assert report.attempt_log_satisfied is False
assert report.verdict == rc.VERDICT_UNSAFE
blob = " ".join(report.reasons + report.authorization_reasons)
assert "#669" in blob or "attempt log" in blob
def test_coordinator_allows_full_restart_with_attempt_log():
inv = {
"inventory_complete": True,
"sessions": [],
"leases": [],
"prior_recovery_attempts": [
{
"action": "client_reconnect",
"outcome": "insufficient",
"reason": "still broken",
}
],
}
report = rc.evaluate_restart_impact(
inv,
restart_class=rc.RestartClass.FULL_MCP_RESTART,
requester_role="controller",
requester_permissions=rc.permissions_for_role("controller"),
controller_approved=True,
operator_authorized=True,
)
assert report.attempt_log_satisfied is True
assert report.allow_restart is True
assert report.verdict == rc.VERDICT_SAFE
def test_coordinator_break_glass_allows_without_log():
inv = {
"inventory_complete": True,
"sessions": [],
"leases": [],
"prior_recovery_attempts": [],
}
report = rc.evaluate_restart_impact(
inv,
restart_class=rc.RestartClass.FULL_MCP_RESTART,
requester_role="controller",
requester_permissions=rc.permissions_for_role("controller"),
controller_approved=True,
operator_authorized=True,
break_glass=True,
)
assert report.break_glass is True
assert report.attempt_log_satisfied is True
assert report.allow_restart is True
def test_coordinator_client_reconnect_unaffected():
inv = {
"inventory_complete": True,
"sessions": [],
"leases": [],
"prior_recovery_attempts": [],
}
report = rc.evaluate_restart_impact(
inv,
restart_class=rc.RestartClass.CLIENT_RECONNECT,
requester_role="author",
requester_permissions=rc.permissions_for_role("author"),
)
assert report.attempt_log_satisfied is True
assert report.allow_restart is True
def test_restart_class_alias_accepted():
assert (
rp.resolve_action("full_mcp_restart")
is rp.RecoveryAction.FULL_MCP_RESTART
)
+2 -1
View File
@@ -243,9 +243,10 @@ class TestRuntimeClarity(unittest.TestCase):
self.assertIn("switching is disabled", res["message"].lower())
self.assertIsNone(gitea_config._active_profile_override)
@patch("mcp_server._trusted_session_repository", return_value={"repository": "Example-Org/Example-Repo", "org": "Example-Org", "repo": "Example-Repo", "reasons": []})
@patch("mcp_server.api_request")
@patch("mcp_server.get_auth_header")
def test_activate_profile_succeeds_when_enabled(self, mock_auth, mock_api):
def test_activate_profile_succeeds_when_enabled(self, mock_auth, mock_api, mock_trusted):
self._write_config(CONFIG_SWITCHING_ENABLED)
# Setup mock responses for whoami checks
+506
View File
@@ -0,0 +1,506 @@
"""Unit and integration tests for Phase 2 Web Console recovery controls (#644)."""
from __future__ import annotations
import os
import sys
import types
import unittest
from unittest.mock import patch
from starlette.testclient import TestClient
import merged_cleanup_reconcile
import runtime_recovery_guard
import stable_branch_push_guard
import stale_binding_recovery
from webui import console_authz, console_recovery, system_health
from webui.app import create_app
class TestConsoleRecovery(unittest.TestCase):
def test_diagnose_recovery_healthy(self) -> None:
diag = console_recovery.diagnose_recovery()
self.assertIn(diag.status, {console_recovery.STATUS_HEALTHY, console_recovery.STATUS_ACTION_REQUIRED})
self.assertIsInstance(diag.playbooks, tuple)
self.assertGreaterEqual(len(diag.playbooks), 4)
playbook_ids = {pb.playbook_id for pb in diag.playbooks}
self.assertIn(console_recovery.PLAYBOOK_CLEAR_STALE_BINDING, playbook_ids)
self.assertIn(console_recovery.PLAYBOOK_REBIND_SESSION, playbook_ids)
self.assertIn(console_recovery.PLAYBOOK_RECONCILE_CLEANUPS, playbook_ids)
self.assertIn(console_recovery.PLAYBOOK_SANCTIONED_RESTART, playbook_ids)
def test_confirmation_phrase_generation_and_matching(self) -> None:
phrase = console_recovery.confirmation_phrase("clear_stale_binding")
self.assertEqual(phrase, "confirm clear_stale_binding")
self.assertTrue(console_recovery.confirmation_matches("clear_stale_binding", "confirm clear_stale_binding"))
self.assertFalse(console_recovery.confirmation_matches("clear_stale_binding", "wrong phrase"))
phrase_target = console_recovery.confirmation_phrase("sanctioned_restart", "gitea-author")
self.assertEqual(phrase_target, "confirm sanctioned_restart gitea-author")
self.assertTrue(console_recovery.confirmation_matches("sanctioned_restart", "confirm sanctioned_restart gitea-author", "gitea-author"))
def test_build_recovery_preview(self) -> None:
principal = console_authz.Principal("[email protected]", console_authz.OPERATOR, console_authz.IDENTITY_LOCAL_DEV, True)
preview = console_recovery.build_recovery_preview(
playbook_id=console_recovery.PLAYBOOK_CLEAR_STALE_BINDING,
target="test-worktree",
principal=principal,
)
self.assertEqual(preview["playbook_id"], console_recovery.PLAYBOOK_CLEAR_STALE_BINDING)
self.assertEqual(preview["action_id"], console_recovery.ACTION_CLEAR_STALE_BINDING)
self.assertEqual(preview["confirmation_phrase"], "confirm clear_stale_binding test-worktree")
self.assertTrue(len(preview["mutation_ledger"]) >= 3)
self.assertTrue(preview["authorization"]["allowed"])
def test_build_recovery_preview_unknown_playbook(self) -> None:
preview = console_recovery.build_recovery_preview("unknown_playbook")
self.assertFalse(preview.get("allowed"))
self.assertEqual(preview.get("error"), "unknown_playbook")
def test_execute_recovery_playbook_confirmation_mismatch(self) -> None:
# Authorization is checked before confirmation, so the phase gate has to
# pass for this test to reach the branch it is about.
principal = console_authz.Principal("[email protected]", console_authz.OPERATOR, console_authz.IDENTITY_LOCAL_DEV, True)
with self._phase_two_enabled():
result = console_recovery.execute_recovery_playbook(
playbook_id=console_recovery.PLAYBOOK_CLEAR_STALE_BINDING,
confirmation="invalid confirmation",
principal=principal,
)
self.assertFalse(result["success"])
self.assertFalse(result["allowed"])
self.assertEqual(result["error"], "confirmation_mismatch")
def test_execute_recovery_playbook_unauthorized(self) -> None:
# Anonymous principal has viewer role -> should be denied
result = console_recovery.execute_recovery_playbook(
playbook_id=console_recovery.PLAYBOOK_CLEAR_STALE_BINDING,
confirmation="confirm clear_stale_binding",
principal=console_authz.ANONYMOUS,
)
self.assertFalse(result["success"])
self.assertFalse(result["allowed"])
self.assertEqual(result["error"], console_authz.DENY_UNAUTHENTICATED)
def test_execute_refuses_phase_two_write_while_console_is_phase_one(self) -> None:
"""B1: the apply path must arm the phase gate, not skip it.
``authorize`` only applies the phase branch when ``for_execution=True``.
The apply path used the default, so an operator executed a phase-2 write
while ``ACTIVE_PHASE`` was 1.
"""
self.assertGreater(
console_authz.get_action(console_recovery.ACTION_CLEAR_STALE_BINDING).phase,
console_authz.ACTIVE_PHASE,
"fixture assumes the recovery actions are ahead of the active phase",
)
principal = console_authz.Principal(
"[email protected]", console_authz.OPERATOR, console_authz.IDENTITY_LOCAL_DEV, True
)
phrase = console_recovery.confirmation_phrase(
console_recovery.PLAYBOOK_CLEAR_STALE_BINDING
)
result = console_recovery.execute_recovery_playbook(
playbook_id=console_recovery.PLAYBOOK_CLEAR_STALE_BINDING,
confirmation=phrase,
principal=principal,
)
self.assertFalse(result["success"])
self.assertFalse(result["allowed"])
self.assertEqual(result["error"], console_authz.DENY_PHASE_NOT_ACTIVE)
def test_preview_execution_enabled_matches_the_execution_decision(self) -> None:
"""B1: preview must not report a bare False it cannot explain."""
principal = console_authz.Principal(
"[email protected]", console_authz.OPERATOR, console_authz.IDENTITY_LOCAL_DEV, True
)
preview = console_recovery.build_recovery_preview(
playbook_id=console_recovery.PLAYBOOK_REBIND_SESSION,
target="branches/feat-issue-644",
principal=principal,
)
self.assertFalse(preview["execution_enabled"])
self.assertEqual(
preview["execution_blocked_reason"], console_authz.DENY_PHASE_NOT_ACTIVE
)
self.assertFalse(preview["execution_authorization"]["allowed"])
# The preview (non-execution) decision still allows, by role.
self.assertTrue(preview["authorization"]["allowed"])
def _phase_two_enabled(self):
"""Raise ACTIVE_PHASE so the execution branches are reachable in tests."""
return patch.object(console_authz, "ACTIVE_PHASE", 2)
def _operator(self) -> console_authz.Principal:
return console_authz.Principal(
"[email protected]", console_authz.OPERATOR, console_authz.IDENTITY_LOCAL_DEV, True
)
def test_rebind_mutates_the_live_environment_not_a_copy(self) -> None:
"""B2: the playbook must change the mapping it claims to have changed."""
live_env = {stale_binding_recovery.ACTIVE_WORKTREE_ENV: "branches/stale-old"}
phrase = console_recovery.confirmation_phrase(
console_recovery.PLAYBOOK_REBIND_SESSION, "branches/feat-issue-644"
)
with self._phase_two_enabled():
result = console_recovery.execute_recovery_playbook(
playbook_id=console_recovery.PLAYBOOK_REBIND_SESSION,
confirmation=phrase,
target="branches/feat-issue-644",
principal=self._operator(),
env=live_env,
)
self.assertTrue(result["success"])
self.assertEqual(
live_env[stale_binding_recovery.ACTIVE_WORKTREE_ENV],
"branches/feat-issue-644",
"rebind reported success without changing the caller's environment",
)
self.assertTrue(result["applied_result"]["binding_changed"])
self.assertEqual(result["applied_result"]["binding_before"], "branches/stale-old")
self.assertEqual(
result["applied_result"]["binding_after"], "branches/feat-issue-644"
)
def test_clear_stale_binding_reports_failure_when_nothing_changed(self) -> None:
"""B2: a no-op recovery must never be reported as success."""
live_env: dict[str, str] = {}
phrase = console_recovery.confirmation_phrase(
console_recovery.PLAYBOOK_CLEAR_STALE_BINDING
)
with self._phase_two_enabled():
result = console_recovery.execute_recovery_playbook(
playbook_id=console_recovery.PLAYBOOK_CLEAR_STALE_BINDING,
confirmation=phrase,
principal=self._operator(),
env=live_env,
)
self.assertFalse(
result["success"],
"a clear that changed no binding must not report success",
)
self.assertFalse(result["applied_result"]["binding_changed"])
def test_clear_stale_binding_clears_the_live_binding(self) -> None:
"""B2: the sanctioned clear must reach the caller's environment."""
missing = "/nonexistent/branches/deleted-worktree"
live_env = {stale_binding_recovery.ACTIVE_WORKTREE_ENV: missing}
phrase = console_recovery.confirmation_phrase(
console_recovery.PLAYBOOK_CLEAR_STALE_BINDING
)
with self._phase_two_enabled():
result = console_recovery.execute_recovery_playbook(
playbook_id=console_recovery.PLAYBOOK_CLEAR_STALE_BINDING,
confirmation=phrase,
principal=self._operator(),
env=live_env,
)
if result["success"]:
self.assertNotIn(stale_binding_recovery.ACTIVE_WORKTREE_ENV, live_env)
self.assertEqual(result["applied_result"]["binding_before"], missing)
self.assertIsNone(result["applied_result"]["binding_after"])
else:
# Fail closed is acceptable; reporting a clear that did not happen
# is not. This is the invariant the blocker was about.
self.assertFalse(result["applied_result"]["binding_changed"])
self.assertEqual(
live_env.get(stale_binding_recovery.ACTIVE_WORKTREE_ENV), missing
)
def test_reconcile_playbook_calls_an_entry_point_that_exists(self) -> None:
"""B3: the previous call named a function absent from the module."""
phrase = console_recovery.confirmation_phrase(
console_recovery.PLAYBOOK_RECONCILE_CLEANUPS
)
fake_server = types.SimpleNamespace(
gitea_reconcile_merged_cleanups=lambda **kwargs: {
"success": True,
"entries": [{"issue_number": 100}],
}
)
with self._phase_two_enabled(), patch.dict(
sys.modules, {"gitea_mcp_server": fake_server}
):
result = console_recovery.execute_recovery_playbook(
playbook_id=console_recovery.PLAYBOOK_RECONCILE_CLEANUPS,
confirmation=phrase,
principal=console_authz.Principal(
"[email protected]",
console_authz.ADMIN,
console_authz.IDENTITY_LOCAL_DEV,
True,
),
)
self.assertTrue(result["success"], result.get("applied_result"))
self.assertNotIn("error_type", result["applied_result"])
self.assertEqual(result["applied_result"]["reconciled_count"], 1)
def test_reconcile_entry_point_exists_on_the_real_module(self) -> None:
"""B3 regression: guard the symbol itself, not just the call shape."""
import gitea_mcp_server
self.assertTrue(
hasattr(gitea_mcp_server, "gitea_reconcile_merged_cleanups"),
"console recovery depends on this reconciler entry point",
)
self.assertFalse(
hasattr(merged_cleanup_reconcile, "reconcile_merged_cleanups"),
"if this module grows the orchestrator, point the playbook back at it",
)
def test_contamination_gate_blocks_a_writing_playbook(self) -> None:
"""B4: a live marker plus a gated task key must actually block."""
marker = {
"kind": "manual_daemon_kill",
"reason_class": "manual_daemon_kill",
"command_summary": "pkill -f gitea_mcp_server",
"active": True,
}
phrase = console_recovery.confirmation_phrase(
console_recovery.PLAYBOOK_REBIND_SESSION, "branches/feat-issue-644"
)
live_env = {stale_binding_recovery.ACTIVE_WORKTREE_ENV: "branches/stale-old"}
with self._phase_two_enabled(), patch.object(
console_recovery, "load_active_contamination_marker", return_value=marker
):
result = console_recovery.execute_recovery_playbook(
playbook_id=console_recovery.PLAYBOOK_REBIND_SESSION,
confirmation=phrase,
target="branches/feat-issue-644",
principal=self._operator(),
env=live_env,
)
self.assertFalse(result["success"])
self.assertEqual(result["error"], "contaminated_runtime")
self.assertEqual(
live_env[stale_binding_recovery.ACTIVE_WORKTREE_ENV],
"branches/stale-old",
"a blocked playbook must not have mutated anything",
)
def test_contamination_gate_exempts_the_reconciler_remedy(self) -> None:
"""B4: the designated remedy must stay reachable while contaminated."""
marker = {
"kind": "manual_daemon_kill",
"reason_class": "manual_daemon_kill",
"command_summary": "pkill -f gitea_mcp_server",
"active": True,
}
phrase = console_recovery.confirmation_phrase(
console_recovery.PLAYBOOK_RECONCILE_CLEANUPS
)
fake_server = types.SimpleNamespace(
gitea_reconcile_merged_cleanups=lambda **kwargs: {
"success": True,
"entries": [],
}
)
with self._phase_two_enabled(), patch.object(
console_recovery, "load_active_contamination_marker", return_value=marker
), patch.dict(sys.modules, {"gitea_mcp_server": fake_server}):
result = console_recovery.execute_recovery_playbook(
playbook_id=console_recovery.PLAYBOOK_RECONCILE_CLEANUPS,
confirmation=phrase,
principal=console_authz.Principal(
"[email protected]",
console_authz.ADMIN,
console_authz.IDENTITY_LOCAL_DEV,
True,
),
)
self.assertNotEqual(result.get("error"), "contaminated_runtime")
def test_gated_task_key_is_actually_gated(self) -> None:
"""B4: the console action id was never a member of the gated set."""
self.assertIn(
console_recovery.CONTAMINATION_GATED_TASK,
stable_branch_push_guard.CONTAMINATION_GATED_TASKS,
)
self.assertNotIn(
console_recovery.ACTION_CLEAR_STALE_BINDING,
stable_branch_push_guard.CONTAMINATION_GATED_TASKS,
)
def test_diagnosis_reads_the_key_the_gate_returns(self) -> None:
"""B4: ``contaminated`` is a key assess_contamination_gate never returns."""
gate = runtime_recovery_guard.assess_contamination_gate(
None, task=console_recovery.CONTAMINATION_GATED_TASK, actual_role="operator"
)
self.assertNotIn("contaminated", gate)
self.assertIn("block", gate)
def test_contaminated_runtime_is_reported_unclean(self) -> None:
"""B4: verify_post_recovery reported contamination_clean unconditionally."""
marker = {
"kind": "manual_daemon_kill",
"reason_class": "manual_daemon_kill",
"command_summary": "pkill -f gitea_mcp_server",
"active": True,
}
with patch.object(
console_recovery, "load_active_contamination_marker", return_value=marker
):
verification = console_recovery.verify_post_recovery()
diag = console_recovery.diagnose_recovery()
self.assertFalse(verification["contamination_clean"])
self.assertFalse(verification["clean"])
self.assertEqual(diag.status, console_recovery.STATUS_BLOCKED_CONTAMINATION)
def test_master_parity_baseline_is_not_the_head_it_is_compared_against(self) -> None:
"""B5: capture_startup_parity was fed the head it was then compared to."""
stale = system_health.StaleRuntime(
daemon_head="a" * 40,
checkout_head="b" * 40,
remote_head="b" * 40,
stale=True,
determinable=True,
mutation_safe=False,
reasons=("daemon is behind the checkout",),
)
with patch.object(system_health, "assess_stale_runtime", return_value=stale):
diag = console_recovery.diagnose_recovery()
parity = diag.master_parity
self.assertEqual(parity["startup_head"], "a" * 40)
self.assertEqual(parity["current_head"], "b" * 40)
self.assertNotEqual(parity["startup_head"], parity["current_head"])
self.assertFalse(parity["in_parity"])
def test_master_parity_carries_the_live_remote_dimension(self) -> None:
"""B5: live_remote_head was never passed, dropping the #610 dimension."""
stale = system_health.StaleRuntime(
daemon_head="c" * 40,
checkout_head="c" * 40,
remote_head="d" * 40,
stale=False,
determinable=True,
mutation_safe=False,
reasons=(),
)
with patch.object(system_health, "assess_stale_runtime", return_value=stale):
diag = console_recovery.diagnose_recovery()
self.assertEqual(diag.master_parity.get("live_remote_head"), "d" * 40)
def test_verify_post_recovery(self) -> None:
verification = console_recovery.verify_post_recovery()
self.assertIn("clean", verification)
self.assertIn("status", verification)
self.assertIn("reasons", verification)
def test_unverified_inherited_binding_is_not_reported_clean(self) -> None:
"""B2: ``not clear_eligible`` also read clean for unproven bindings."""
binding = {
"classification": stale_binding_recovery.CLASSIFICATION_UNVERIFIED_INHERITED,
"clear_eligible": False,
}
diag = console_recovery.diagnose_recovery()
patched = console_recovery.RecoveryDiagnosis(
status=diag.status,
clean=diag.clean,
stale_runtime=diag.stale_runtime,
master_parity=diag.master_parity,
stale_binding=binding,
contamination=diag.contamination,
worktree_anomalies=diag.worktree_anomalies,
playbooks=diag.playbooks,
reasons=diag.reasons,
)
with patch.object(console_recovery, "diagnose_recovery", return_value=patched):
verification = console_recovery.verify_post_recovery()
self.assertFalse(verification["binding_clean"])
self.assertEqual(
verification["binding_classification"],
stale_binding_recovery.CLASSIFICATION_UNVERIFIED_INHERITED,
)
class TestConsoleRecoveryApi(unittest.TestCase):
def setUp(self) -> None:
self.app = create_app()
self.client = TestClient(self.app)
def test_api_recovery_diagnose(self) -> None:
res = self.client.get("/api/v1/system/recovery/diagnose")
self.assertEqual(res.status_code, 200)
data = res.json()
self.assertIn("status", data)
self.assertIn("clean", data)
self.assertIn("playbooks", data)
self.assertTrue(len(data["playbooks"]) >= 4)
def test_api_recovery_preview(self) -> None:
res = self.client.post(
"/api/v1/system/recovery/preview",
json={"playbook_id": "clear_stale_binding", "target": "active"},
)
self.assertEqual(res.status_code, 200)
data = res.json()
self.assertEqual(data["playbook_id"], "clear_stale_binding")
self.assertEqual(data["confirmation_phrase"], "confirm clear_stale_binding active")
self.assertIn("mutation_ledger", data)
def test_api_recovery_apply_denied_without_auth(self) -> None:
res = self.client.post(
"/api/v1/system/recovery/apply",
json={"playbook_id": "clear_stale_binding", "confirmation": "confirm clear_stale_binding"},
)
self.assertEqual(res.status_code, 400)
data = res.json()
self.assertFalse(data["success"])
self.assertFalse(data["allowed"])
def test_api_recovery_apply_refuses_phase_two_write_with_dev_auth(self) -> None:
"""B1: this previously asserted the phase-gate bypass as intended.
An authenticated operator posting a valid confirmation still must not
execute a phase-2 write while the console is in phase 1. The refusal is
the contract; a 200 here means the gate is not armed.
"""
env = {
"WEBUI_AUTH_MODE": "local_dev",
"WEBUI_DEV_SUBJECT": "[email protected]",
"WEBUI_DEV_ROLE": "operator",
}
before = os.environ.get("GITEA_ACTIVE_WORKTREE")
with patch.dict(os.environ, env):
res = self.client.post(
"/api/v1/system/recovery/apply",
json={
"playbook_id": "rebind_session_worktree",
"target": "branches/feat-issue-644",
"confirmation": "confirm rebind_session_worktree branches/feat-issue-644",
},
)
self.assertEqual(res.status_code, 400)
data = res.json()
self.assertFalse(data["success"])
self.assertFalse(data["allowed"])
self.assertEqual(data["error"], console_authz.DENY_PHASE_NOT_ACTIVE)
self.assertEqual(
os.environ.get("GITEA_ACTIVE_WORKTREE"),
before,
"a refused apply must not have rebound the live process environment",
)
def test_api_recovery_preview_reports_why_execution_is_disabled(self) -> None:
res = self.client.post(
"/api/v1/system/recovery/preview",
json={"playbook_id": "rebind_session_worktree", "target": "active"},
)
self.assertEqual(res.status_code, 200)
data = res.json()
self.assertFalse(data["execution_enabled"])
self.assertIn("execution_authorization", data)
def test_api_recovery_verify(self) -> None:
res = self.client.get("/api/v1/system/recovery/verify")
self.assertEqual(res.status_code, 200)
data = res.json()
self.assertIn("clean", data)
self.assertIn("status", data)
if __name__ == "__main__":
unittest.main()
+511
View File
@@ -0,0 +1,511 @@
"""Tests for the Gitea issue↔PR linkage console (#645, Phase 3).
Covers the acceptance criteria of the issue:
* AC1 — issue↔PR linkage is visible for the selected project/repo, in both
directions, with the evidence that produced each edge.
* AC2 — the latest canonical handoff (CTH) is summarized for a focused thread.
* AC3 — an external Gitea link appears only under the admin reveal opt-in.
* AC4 — every case is driven by mocked Gitea payloads; no network.
Plus the invariants this console must not violate: a partial or failed read is
never rendered as "no link exists", an unfetched thread is never rendered as
"no handoff", redaction happens before display, and the surface stays read-only.
"""
from __future__ import annotations
import json
import os
import sys
import unittest
from pathlib import Path
from unittest import mock
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from tests.webui_testclient import TestClient
from canonical_thread_handoff import format_cth_body
from webui.app import create_app
from webui.linkage_loader import (
EVIDENCE_BRANCH,
EVIDENCE_CLOSES,
EVIDENCE_REFERENCE,
HANDOFF_LOADED,
HANDOFF_NOT_LOADED,
HANDOFF_UNAVAILABLE,
LinkageSnapshot,
load_linkage_snapshot,
resolve_linkage,
resolve_pr_links,
snapshot_to_dict,
summarize_handoff,
)
from webui.linkage_views import render_linkage_page
from webui.nav import nav_hrefs
from webui.queue_loader import PaginationMeta
def _pagination(*, complete: bool = True, count: int = 0) -> PaginationMeta:
return PaginationMeta(
page=1,
per_page=50,
returned_count=count,
has_more=not complete,
is_final_page=complete,
inventory_complete=complete,
pages_fetched=1,
)
def _pr(
number: int,
*,
title: str = "",
body: str = "",
head: str = "",
state: str = "open",
labels: tuple[str, ...] = (),
) -> dict:
return {
"number": number,
"title": title or f"pr {number}",
"body": body,
"state": state,
"head": {"ref": head},
"labels": [{"name": name} for name in labels],
}
def _issue(
number: int,
*,
title: str = "",
state: str = "open",
labels: tuple[str, ...] = (),
) -> dict:
return {
"number": number,
"title": title or f"issue {number}",
"state": state,
"labels": [{"name": name} for name in labels],
}
def _fetcher(items: list[dict], *, complete: bool = True):
def _fetch(*_args, **_kwargs):
return items, _pagination(complete=complete, count=len(items))
return _fetch
def _load(
issues: list[dict],
prs: list[dict],
*,
complete: bool = True,
**kwargs,
) -> LinkageSnapshot:
return load_linkage_snapshot(
fetch_prs=_fetcher(prs, complete=complete),
fetch_issues=_fetcher(issues, complete=complete),
**kwargs,
)
def _cth(comment_id: int, *, created_at: str, status: str, next_owner: str) -> dict:
return {
"id": comment_id,
"created_at": created_at,
"user": {"login": "jcwalker3"},
"body": format_cth_body(
cth_type="Author Handoff",
status=status,
next_owner=next_owner,
current_blocker="none",
decision="implemented",
proof="full suite green",
next_action="review PR",
ready_to_paste_prompt="Review PR #902 now.",
),
}
class TestLinkageEvidence(unittest.TestCase):
"""AC1 — every edge records how it was found, and keeps all candidates."""
def test_closes_keyword_in_body_is_strongest_evidence(self):
links = resolve_pr_links(_pr(902, body="Closes #643"))
self.assertEqual([link.issue_number for link in links], [643])
self.assertEqual(links[0].evidence, (EVIDENCE_CLOSES,))
self.assertTrue(links[0].closes)
def test_closes_keyword_in_title_counts(self):
links = resolve_pr_links(_pr(902, title="feat(webui): preview (Closes #643)"))
self.assertEqual(links[0].evidence, (EVIDENCE_CLOSES,))
def test_canonical_branch_marker_links_without_a_keyword(self):
links = resolve_pr_links(_pr(902, head="feat/issue-643-request-preview"))
self.assertEqual([link.issue_number for link in links], [643])
self.assertEqual(links[0].evidence, (EVIDENCE_BRANCH,))
self.assertFalse(links[0].closes)
def test_non_canonical_branch_is_not_treated_as_a_marker(self):
self.assertEqual(resolve_pr_links(_pr(902, head="issue-643-preview")), ())
def test_bare_mention_is_recorded_as_the_weakest_evidence(self):
links = resolve_pr_links(_pr(902, body="context in #643"))
self.assertEqual(links[0].evidence, (EVIDENCE_REFERENCE,))
self.assertFalse(links[0].closes)
def test_several_evidence_kinds_merge_onto_one_edge(self):
links = resolve_pr_links(
_pr(902, body="Closes #643 — see #643", head="feat/issue-643-preview")
)
self.assertEqual(len(links), 1)
self.assertEqual(
links[0].evidence,
(EVIDENCE_CLOSES, EVIDENCE_BRANCH, EVIDENCE_REFERENCE),
)
def test_stronger_evidence_sorts_first(self):
links = resolve_pr_links(_pr(902, body="Closes #643, related #700"))
self.assertEqual([link.issue_number for link in links], [643, 700])
def test_self_reference_is_not_linkage(self):
links = resolve_pr_links(_pr(902, body="supersedes #902"))
self.assertEqual(links, ())
def test_every_candidate_is_kept_never_collapsed_to_a_guess(self):
links = resolve_pr_links(_pr(902, body="Closes #643\nCloses #644"))
self.assertEqual([link.issue_number for link in links], [643, 644])
class TestLinkageIndex(unittest.TestCase):
def test_issue_direction_ignores_mention_only_edges(self):
index = resolve_linkage([_pr(902, body="context in #643")])
self.assertIsNone(index.issue_prs.get(643))
self.assertEqual(index.pr_links[902][0].evidence, (EVIDENCE_REFERENCE,))
def test_contested_issue_is_reported_when_two_prs_claim_it(self):
index = resolve_linkage(
[_pr(902, body="Closes #643"), _pr(903, head="feat/issue-643-again")]
)
self.assertEqual(index.contested_issues(), (643,))
self.assertEqual(index.issue_prs[643], (902, 903))
def test_single_claim_is_not_contested(self):
index = resolve_linkage([_pr(902, body="Closes #643")])
self.assertEqual(index.contested_issues(), ())
def test_ambiguous_when_two_issues_tie_at_the_strongest_evidence(self):
index = resolve_linkage([_pr(902, body="Closes #643\nCloses #644")])
self.assertTrue(index.ambiguous(902))
def test_weaker_candidate_alongside_a_stronger_one_is_not_ambiguous(self):
index = resolve_linkage([_pr(902, body="Closes #643, see #700")])
self.assertFalse(index.ambiguous(902))
self.assertEqual(index.primary_issue(902).issue_number, 643)
def test_malformed_pr_row_is_skipped_not_raised_on(self):
index = resolve_linkage([{"title": "no number"}, _pr(902, body="Closes #643")])
self.assertEqual(sorted(index.pr_links), [902])
class TestLinkageSnapshot(unittest.TestCase):
"""AC1 — linkage is visible per project/repo, in both directions."""
def test_both_directions_are_populated(self):
snapshot = _load([_issue(643)], [_pr(902, body="Closes #643")])
self.assertTrue(snapshot.ok)
self.assertEqual([node.number for node in snapshot.issues], [643])
self.assertEqual(snapshot.issues[0].linked_prs, (902,))
self.assertEqual(snapshot.prs[0].links[0].issue_number, 643)
def test_repo_scope_comes_from_the_registry_project(self):
snapshot = _load([], [])
self.assertIn("/", snapshot.repo_label)
self.assertTrue(snapshot.project_id)
def test_unknown_project_fails_closed_with_a_reason(self):
snapshot = _load([_issue(643)], [], project_id="no-such-project")
self.assertFalse(snapshot.ok)
self.assertIn("not found in registry", snapshot.fetch_error)
self.assertEqual(snapshot.issues, ())
def test_orphan_pr_is_identifiable(self):
snapshot = _load([], [_pr(902), _pr(903, body="Closes #643")])
self.assertEqual([node.number for node in snapshot.orphan_prs], [902])
def test_state_scope_defaults_to_open_and_is_reported(self):
self.assertEqual(_load([], []).state_scope, "open")
self.assertEqual(_load([], [], state="all").state_scope, "all")
def test_unsupported_state_falls_back_to_open(self):
self.assertEqual(_load([], [], state="../etc").state_scope, "open")
def test_state_is_passed_through_to_the_fetchers(self):
seen: list[str] = []
def _fetch(*_args, **kwargs):
seen.append(kwargs.get("state", ""))
return [], _pagination()
load_linkage_snapshot(state="all", fetch_prs=_fetch, fetch_issues=_fetch)
self.assertEqual(seen, ["all", "all"])
class TestPartialInventoryIsNotAnAbsenceClaim(unittest.TestCase):
"""An empty edge list from a partial read must never read as 'no link'."""
def test_incomplete_pagination_marks_links_non_authoritative(self):
snapshot = _load([_issue(643)], [], complete=False)
self.assertFalse(snapshot.inventory_complete)
self.assertFalse(snapshot.issues[0].links_authoritative)
def test_complete_pagination_marks_links_authoritative(self):
snapshot = _load([_issue(643)], [], complete=True)
self.assertTrue(snapshot.inventory_complete)
self.assertTrue(snapshot.issues[0].links_authoritative)
def test_partial_window_renders_a_qualified_empty_cell(self):
html = render_linkage_page(_load([_issue(643)], [], complete=False))
self.assertIn("none found (partial inventory)", html)
def test_complete_window_renders_a_plain_none(self):
html = render_linkage_page(_load([_issue(643)], [], complete=True))
self.assertNotIn("partial inventory", html)
self.assertIn(">none<", html)
def test_missing_credentials_fail_closed_without_a_table(self):
with mock.patch(
"webui.linkage_loader._offline_test_mode", return_value=False
), mock.patch("webui.linkage_loader.get_auth_header", return_value=""):
snapshot = load_linkage_snapshot()
self.assertFalse(snapshot.ok)
self.assertIn("credentials unavailable", snapshot.fetch_error)
html = render_linkage_page(snapshot)
self.assertIn("Linkage unavailable", html)
self.assertNotIn("Issues → pull requests", html)
def test_fetch_failure_is_reported_not_raised(self):
def _boom(*_args, **_kwargs):
raise RuntimeError("gitea 502")
snapshot = load_linkage_snapshot(fetch_prs=_boom, fetch_issues=_boom)
self.assertFalse(snapshot.ok)
self.assertIn("Gitea fetch failed", snapshot.fetch_error)
class TestHandoffSummary(unittest.TestCase):
"""AC2 — the latest canonical handoff is summarized for a focused thread."""
def test_latest_cth_wins(self):
summary = summarize_handoff([
_cth(1, created_at="2026-07-24T10:00:00Z", status="in progress",
next_owner="author"),
_cth(2, created_at="2026-07-25T10:00:00Z", status="PR-open",
next_owner="reviewer"),
])
self.assertEqual(summary.comment_id, 2)
self.assertEqual(summary.status, "PR-open")
self.assertEqual(summary.next_owner, "reviewer")
self.assertTrue(summary.cth_type_known)
def test_thread_without_a_cth_summarizes_to_none(self):
self.assertIsNone(summarize_handoff([{"id": 1, "body": "ordinary comment"}]))
def test_unknown_heading_is_reported_not_republished(self):
summary = summarize_handoff([
{
"id": 5,
"created_at": "2026-07-25T10:00:00Z",
"user": {"login": "someone"},
"body": "<!-- cth:v1 -->\n## CTH: Totally Made Up\n\nStatus: odd\n",
}
])
self.assertFalse(summary.cth_type_known)
self.assertEqual(summary.cth_type, "unrecognized")
self.assertNotIn("Totally Made Up", json.dumps(summary.to_dict()))
def test_focused_pr_loads_its_handoff(self):
snapshot = _load(
[_issue(643)],
[_pr(902, body="Closes #643")],
pr=902,
comment_source=lambda kind, number: [
_cth(2, created_at="2026-07-25T10:00:00Z", status="PR-open",
next_owner="reviewer")
],
)
self.assertEqual(snapshot.handoff_status.state, HANDOFF_LOADED)
self.assertEqual(snapshot.focus, ("pr", 902))
self.assertEqual(snapshot.prs[0].handoff.status, "PR-open")
def test_unfocused_rows_report_not_loaded_never_none(self):
snapshot = _load(
[_issue(643)],
[_pr(902, body="Closes #643"), _pr(903)],
pr=902,
comment_source=lambda kind, number: [],
)
other = next(node for node in snapshot.prs if node.number == 903)
self.assertIsNone(other.handoff)
self.assertEqual(other.handoff_status.state, HANDOFF_NOT_LOADED)
self.assertIn("not loaded", render_linkage_page(snapshot))
def test_no_focus_means_no_thread_is_claimed_handoff_free(self):
snapshot = _load([_issue(643)], [])
self.assertEqual(snapshot.handoff_status.state, HANDOFF_NOT_LOADED)
self.assertIn("thread-scoped", snapshot.handoff_status.reason)
def test_comment_source_failure_degrades_only_the_handoff(self):
def _boom(_kind, _number):
raise RuntimeError("comments 500")
snapshot = _load(
[_issue(643)], [_pr(902, body="Closes #643")], pr=902, comment_source=_boom
)
self.assertTrue(snapshot.ok)
self.assertEqual(snapshot.handoff_status.state, HANDOFF_UNAVAILABLE)
self.assertEqual(snapshot.issues[0].linked_prs, (902,))
self.assertIn("unavailable", render_linkage_page(snapshot))
def test_loaded_thread_with_no_cth_says_so_explicitly(self):
snapshot = _load(
[_issue(643)],
[_pr(902, body="Closes #643")],
pr=902,
comment_source=lambda kind, number: [{"id": 1, "body": "hi"}],
)
self.assertIn(
"no Canonical Thread Handoff comment found", render_linkage_page(snapshot)
)
class TestDeepLinks(unittest.TestCase):
"""AC3 — an external Gitea link is emitted only when permitted."""
def test_deep_links_are_withheld_by_default(self):
with mock.patch.dict(os.environ, {"GITEA_MCP_REVEAL_ENDPOINTS": ""}):
snapshot = _load([_issue(643)], [])
html = render_linkage_page(snapshot)
self.assertFalse(snapshot.deep_links_enabled)
self.assertIsNone(snapshot.issues[0].deep_link)
self.assertIn("Gitea deep links are withheld", html)
def test_reveal_opt_in_emits_the_link(self):
with mock.patch.dict(os.environ, {"GITEA_MCP_REVEAL_ENDPOINTS": "1"}):
snapshot = _load([_issue(643)], [_pr(902, body="Closes #643")])
html = render_linkage_page(snapshot)
self.assertTrue(snapshot.deep_links_enabled)
self.assertIn("/issues/643", snapshot.issues[0].deep_link)
self.assertIn("/pulls/902", snapshot.prs[0].deep_link)
self.assertIn(f'href="{snapshot.issues[0].deep_link}"', html)
class TestRedactionBoundary(unittest.TestCase):
def test_secret_shaped_title_is_redacted_before_display(self):
snapshot = _load(
[_issue(643, title="token=ghp_thisisnotarealsecretvalue0001")], []
)
payload = json.dumps(snapshot_to_dict(snapshot))
self.assertNotIn("ghp_thisisnotarealsecretvalue0001", payload)
self.assertNotIn(
"ghp_thisisnotarealsecretvalue0001", render_linkage_page(snapshot)
)
def test_handoff_fields_are_redacted(self):
comment = _cth(
2, created_at="2026-07-25T10:00:00Z", status="ok", next_owner="reviewer"
)
comment["body"] += "\nDecision: password=hunter2hunter2\n"
snapshot = _load(
[_issue(643)],
[_pr(902, body="Closes #643")],
pr=902,
comment_source=lambda kind, number: [comment],
)
self.assertNotIn("hunter2hunter2", json.dumps(snapshot_to_dict(snapshot)))
self.assertNotIn("hunter2hunter2", render_linkage_page(snapshot))
def test_html_escapes_markup_in_a_title(self):
snapshot = _load([_issue(643, title="<script>alert(1)</script>")], [])
html = render_linkage_page(snapshot)
self.assertNotIn("<script>alert(1)</script>", html)
self.assertIn("&lt;script&gt;", html)
class TestLinkageRoutes(unittest.TestCase):
def setUp(self):
self.snapshot = _load(
[_issue(643, labels=("status:ready",))],
[_pr(902, body="Closes #643", labels=("status:pr-open",))],
)
self.client = TestClient(create_app())
def test_page_renders_both_tables(self):
with mock.patch("webui.app.load_linkage_snapshot", return_value=self.snapshot):
response = self.client.get("/gitea")
self.assertEqual(response.status_code, 200)
self.assertIn("Issues → pull requests", response.text)
self.assertIn("Pull requests → issues", response.text)
self.assertIn("#643", response.text)
def test_api_exports_the_same_model(self):
with mock.patch("webui.app.load_linkage_snapshot", return_value=self.snapshot):
response = self.client.get("/api/v1/gitea/linkage")
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertTrue(payload["ok"])
self.assertEqual(payload["issues"][0]["linked_prs"], [902])
self.assertEqual(payload["prs"][0]["links"][0]["issue_number"], 643)
self.assertEqual(payload["schema_version"], 1)
def test_api_declares_the_evidence_vocabulary(self):
with mock.patch("webui.app.load_linkage_snapshot", return_value=self.snapshot):
payload = self.client.get("/api/v1/gitea/linkage").json()
names = {entry["name"] for entry in payload["evidence_kinds"]}
self.assertEqual(names, {EVIDENCE_CLOSES, EVIDENCE_BRANCH, EVIDENCE_REFERENCE})
def test_api_fails_closed_with_a_non_200_when_the_read_failed(self):
failed = _load([], [], project_id="no-such-project")
with mock.patch("webui.app.load_linkage_snapshot", return_value=failed):
response = self.client.get("/api/v1/gitea/linkage")
self.assertEqual(response.status_code, 502)
self.assertFalse(response.json()["ok"])
def test_page_still_renders_when_the_read_failed(self):
failed = _load([], [], project_id="no-such-project")
with mock.patch("webui.app.load_linkage_snapshot", return_value=failed):
response = self.client.get("/gitea")
self.assertEqual(response.status_code, 200)
self.assertIn("Linkage unavailable", response.text)
def test_query_parameters_reach_the_loader(self):
with mock.patch(
"webui.app.load_linkage_snapshot", return_value=self.snapshot
) as loader:
self.client.get("/gitea?project=gitea-tools&state=all&pr=902")
loader.assert_called_once()
args, kwargs = loader.call_args
self.assertEqual(args[0], "gitea-tools")
self.assertEqual(kwargs["state"], "all")
self.assertEqual(kwargs["pr"], 902)
self.assertIsNone(kwargs["issue"])
def test_surface_stays_read_only(self):
for path in ("/gitea", "/api/v1/gitea/linkage"):
with self.subTest(path=path):
self.assertEqual(self.client.post(path).status_code, 405)
def test_nav_exposes_the_linkage_page_as_live(self):
self.assertIn("/gitea", nav_hrefs())
home = self.client.get("/").text
self.assertIn('href="/gitea"', home)
self.assertIn(">Gitea<", home)
if __name__ == "__main__":
unittest.main()
+465
View File
@@ -0,0 +1,465 @@
"""Unit tests for Phase 3 Notifications and Human-Attention Console (#648)."""
from __future__ import annotations
import pytest
from starlette.testclient import TestClient
from webui.app import create_app
from webui.notifications import (
ATTENTION_HUMAN_REQUIRED,
ATTENTION_OPERATOR,
ATTENTION_ROUTINE,
CATEGORY_AUTH,
CATEGORY_BLOCKER,
CATEGORY_LEASE,
CATEGORY_SYSTEM,
CATEGORY_VALIDATION,
CATEGORY_WORKFLOW,
NotificationItem,
NotificationSnapshot,
classify_attention_event,
load_notifications_snapshot,
snapshot_to_dict,
)
from webui.notification_views import render_notifications_page
from webui.project_registry import load_registry
from webui.queue_loader import QueueItem, QueueSnapshot
from webui.lease_loader import CollisionWarning, LeaseSnapshot
from webui.system_health import DependencyProbe, SystemHealthSnapshot, VersionInfo, StaleRuntime
def test_classify_attention_event_rules():
# 1. Critical escalation boundaries -> human-required
att_cls, req_human = classify_attention_event(
CATEGORY_AUTH, "Auth error", "Unauthorized access attempt", is_auth_failure=True
)
assert att_cls == ATTENTION_HUMAN_REQUIRED
assert req_human is True
att_cls, req_human = classify_attention_event(
CATEGORY_SYSTEM, "Hard stop", "Hard stop triggered", is_hard_stop=True
)
assert att_cls == ATTENTION_HUMAN_REQUIRED
assert req_human is True
att_cls, req_human = classify_attention_event(
CATEGORY_VALIDATION, "Validation Error", "Report validation failed", is_validation_failure=True
)
assert att_cls == ATTENTION_HUMAN_REQUIRED
assert req_human is True
# 2. Operational issues -> operator
att_cls, req_human = classify_attention_event(
CATEGORY_BLOCKER, "PR Blocked", "Merge conflict detected", is_blocker=True
)
assert att_cls == ATTENTION_OPERATOR
assert req_human is False
att_cls, req_human = classify_attention_event(
CATEGORY_LEASE, "Lease Expired", "Session lease expired", is_stale=True
)
assert att_cls == ATTENTION_OPERATOR
assert req_human is False
# 3. Routine workflow transitions -> routine
att_cls, req_human = classify_attention_event(
CATEGORY_WORKFLOW, "PR Active", "PR in review"
)
assert att_cls == ATTENTION_ROUTINE
assert req_human is False
def test_notification_snapshot_aggregation():
reg = load_registry()
proj_id = reg.projects[0].id if reg.projects else "gitea-tools"
mock_queue = QueueSnapshot(
project_id=proj_id,
repo_label="org/repo",
prs=(
QueueItem(
number=101,
title="Blocked PR",
badges=("blocked",),
extra={},
),
QueueItem(
number=102,
title="Normal PR",
badges=("in-review",),
extra={},
),
),
issues=(),
pr_pagination=None,
issue_pagination=None,
)
mock_leases = LeaseSnapshot(
project_id=proj_id,
repo_label="org/repo",
issue_lock=None,
claim_inventory={},
reviewer_leases=(
{
"pr_number": 101,
"status": "expired",
"is_expired": True,
},
),
duplicate_prs=(
CollisionWarning(
kind="duplicate_pr",
message="Multiple open PRs for issue #101",
issue_number=101,
pr_numbers=(101, 103),
),
),
duplicate_branches=(),
collision_history=(),
fetch_error=None,
)
mock_version = VersionInfo(
git_sha="abc1234",
git_describe="v1.0.0",
control_plane_schema_version=1,
python_version="3.11",
known=True,
)
mock_stale = StaleRuntime(
daemon_head="abc1234",
checkout_head="abc1234",
remote_head="abc1234",
stale=False,
determinable=True,
mutation_safe=True,
reasons=(),
)
mock_health = SystemHealthSnapshot(
status="degraded",
ready=False,
readiness_complete=True,
readiness_reasons=("Auth failure",),
service="webui",
mode="test",
version=mock_version,
started_at="2026-07-25T00:00:00Z",
uptime_seconds=100.0,
timestamp="2026-07-25T00:00:00Z",
deep_probes_requested=True,
dependencies=(
DependencyProbe(
name="auth_service",
kind="auth",
status="unauthorized",
detail="Token expired",
required=True,
),
),
mcp_namespaces=(),
stale_runtime=mock_stale,
probe_errors=(),
)
snapshot = load_notifications_snapshot(
proj_id,
load_queue=lambda _id: mock_queue,
load_leases=lambda **_kwargs: mock_leases,
load_health=lambda **_kwargs: mock_health,
)
assert snapshot.project_id == proj_id
assert snapshot.total_count == 5
assert snapshot.human_required_count >= 1 # auth probe failure
assert snapshot.operator_count >= 3 # blocked PR + expired lease + duplicate PR collision
assert snapshot.routine_count >= 1 # normal PR
# Inbox items should include operator and human-required items only
inbox_classes = {item.attention_class for item in snapshot.inbox_items}
assert ATTENTION_ROUTINE not in inbox_classes
assert ATTENTION_OPERATOR in inbox_classes
assert ATTENTION_HUMAN_REQUIRED in inbox_classes
def test_snapshot_to_dict_and_redaction():
item = NotificationItem(
id="notif-1",
attention_class=ATTENTION_HUMAN_REQUIRED,
category=CATEGORY_AUTH,
title="Auth Error",
summary="Failed auth header: Bearer secret_token_12345",
work_kind="system",
work_number=None,
project_id="test-proj",
repo_label="org/repo",
created_at="2026-07-25T16:00:00Z",
requires_human=True,
)
snap = NotificationSnapshot(
project_id="test-proj",
repo_label="org/repo",
items=(item,),
human_required_count=1,
operator_count=0,
routine_count=0,
total_count=1,
)
data = snapshot_to_dict(snap)
assert data["project_id"] == "test-proj"
assert data["human_required_count"] == 1
assert len(data["inbox_items"]) == 1
# Redaction test
summary = data["inbox_items"][0]["summary"]
assert "secret_token_12345" not in summary
assert "<redacted>" in summary or "Bearer" in summary
def test_notifications_html_views():
item = NotificationItem(
id="notif-1",
attention_class=ATTENTION_HUMAN_REQUIRED,
category=CATEGORY_AUTH,
title="Critical Auth Failure",
summary="Auth failure details",
work_kind="issue",
work_number=42,
project_id="test-proj",
repo_label="org/repo",
created_at="2026-07-25T16:00:00Z",
requires_human=True,
)
snap = NotificationSnapshot(
project_id="test-proj",
repo_label="org/repo",
items=(item,),
human_required_count=1,
operator_count=0,
routine_count=0,
total_count=1,
)
html = render_notifications_page(snap, filter_class="inbox")
assert "Notifications &amp; Attention Inbox" in html or "Notifications & Attention Inbox" in html
assert "Critical Auth Failure" in html
assert "HUMAN REQUIRED" in html
assert "Human Required" in html
def test_notifications_app_routes():
app = create_app()
client = TestClient(app)
# 1. HTML Route
res = client.get("/notifications")
assert res.status_code == 200
assert "Notifications" in res.text
assert "Attention Inbox" in res.text
# 2. API Route /api/v1/notifications
res_api = client.get("/api/v1/notifications")
assert res_api.status_code == 200
json_data = res_api.json()
assert "human_required_count" in json_data
assert "operator_count" in json_data
assert "routine_count" in json_data
assert "inbox_items" in json_data
# 3. Compatibility Alias /api/notifications
res_alias = client.get("/api/notifications")
assert res_alias.status_code == 200
assert res_alias.json()["project_id"] == json_data["project_id"]
def test_classify_ignores_human_authored_title_and_summary_keywords():
"""B1: keywords in human-authored titles must not escalate routine work (#905)."""
# Routine transition whose title/summary mention critical-boundary words
att_cls, req_human = classify_attention_event(
CATEGORY_WORKFLOW,
"record irrecoverable decision lock provenance",
"PR #999 'record irrecoverable decision lock provenance' is in routine state in-review.",
)
assert att_cls == ATTENTION_ROUTINE
assert req_human is False
att_cls, req_human = classify_attention_event(
CATEGORY_WORKFLOW,
"fix unauthorized token path",
"Issue #1 'fix unauthorized token path' state: claimed. hard stop docs only.",
)
assert att_cls == ATTENTION_ROUTINE
assert req_human is False
# Structured flags still escalate (machine-driven)
att_cls, req_human = classify_attention_event(
CATEGORY_SYSTEM,
"anything",
"anything with hard stop in text",
is_hard_stop=True,
)
assert att_cls == ATTENTION_HUMAN_REQUIRED
assert req_human is True
def test_notification_ids_are_unique_across_probe_errors_and_collisions():
"""B2: published notification ids must be unique within a snapshot (#905)."""
reg = load_registry()
proj_id = reg.projects[0].id if reg.projects else "gitea-tools"
mock_queue = QueueSnapshot(
project_id=proj_id,
repo_label="org/repo",
prs=(),
issues=(),
pr_pagination=None,
issue_pagination=None,
)
mock_leases = LeaseSnapshot(
project_id=proj_id,
repo_label="org/repo",
issue_lock=None,
claim_inventory={},
reviewer_leases=(),
duplicate_prs=(
CollisionWarning(
kind="duplicate_pr",
message="Multiple open PRs for issue #10",
issue_number=10,
pr_numbers=(10, 11),
),
CollisionWarning(
kind="duplicate_branch",
message="Another collision without issue",
issue_number=None,
pr_numbers=(12, 13),
),
CollisionWarning(
kind="duplicate_pr",
message="Second issue collision",
issue_number=10,
pr_numbers=(14, 15),
),
),
duplicate_branches=(),
collision_history=(),
fetch_error=None,
)
mock_version = VersionInfo(
git_sha="abc1234",
git_describe="v1.0.0",
control_plane_schema_version=1,
python_version="3.11",
known=True,
)
mock_stale = StaleRuntime(
daemon_head="abc1234",
checkout_head="abc1234",
remote_head="abc1234",
stale=False,
determinable=True,
mutation_safe=True,
reasons=(),
)
mock_health = SystemHealthSnapshot(
status="degraded",
ready=False,
readiness_complete=True,
readiness_reasons=(),
service="webui",
mode="test",
version=mock_version,
started_at="2026-07-25T00:00:00Z",
uptime_seconds=100.0,
timestamp="2026-07-25T00:00:00Z",
deep_probes_requested=True,
dependencies=(),
mcp_namespaces=(),
stale_runtime=mock_stale,
probe_errors=("error alpha", "error beta"),
)
snapshot = load_notifications_snapshot(
proj_id,
load_queue=lambda _id: mock_queue,
load_leases=lambda **_kwargs: mock_leases,
load_health=lambda **_kwargs: mock_health,
)
ids = [item.id for item in snapshot.items]
assert len(ids) == len(set(ids)), f"duplicate notification ids: {ids}"
assert any(i.startswith(f"notif-sys-err-{proj_id}-") for i in ids)
assert any(i.startswith("notif-collision-") for i in ids)
def test_probe_errors_do_not_set_fetch_error():
"""B3: probe_errors must not be reported as fetch_error (#905)."""
reg = load_registry()
proj_id = reg.projects[0].id if reg.projects else "gitea-tools"
mock_queue = QueueSnapshot(
project_id=proj_id,
repo_label="org/repo",
prs=(),
issues=(),
pr_pagination=None,
issue_pagination=None,
fetch_error=None,
)
mock_leases = LeaseSnapshot(
project_id=proj_id,
repo_label="org/repo",
issue_lock=None,
claim_inventory={},
reviewer_leases=(),
duplicate_prs=(),
duplicate_branches=(),
collision_history=(),
fetch_error=None,
)
mock_version = VersionInfo(
git_sha="abc1234",
git_describe="v1.0.0",
control_plane_schema_version=1,
python_version="3.11",
known=True,
)
mock_stale = StaleRuntime(
daemon_head="abc1234",
checkout_head="abc1234",
remote_head="abc1234",
stale=False,
determinable=True,
mutation_safe=True,
reasons=(),
)
mock_health = SystemHealthSnapshot(
status="degraded",
ready=False,
readiness_complete=True,
readiness_reasons=(),
service="webui",
mode="test",
version=mock_version,
started_at="2026-07-25T00:00:00Z",
uptime_seconds=100.0,
timestamp="2026-07-25T00:00:00Z",
deep_probes_requested=True,
dependencies=(),
mcp_namespaces=(),
stale_runtime=mock_stale,
probe_errors=("probe blew up",),
)
snapshot = load_notifications_snapshot(
proj_id,
load_queue=lambda _id: mock_queue,
load_leases=lambda **_kwargs: mock_leases,
load_health=lambda **_kwargs: mock_health,
)
assert snapshot.fetch_error is None
# probe errors still appear as items
assert any("probe blew up" in item.summary for item in snapshot.items)
+190
View File
@@ -0,0 +1,190 @@
"""Tests for Sentry/GlitchTip observability console (#649, Phase 4)."""
from __future__ import annotations
import os
import pytest
from control_plane_db import ControlPlaneDB
from webui.app import create_app
from webui.console_authz import authorize, resolve_principal
from webui.gated_actions import load_action_registry, preview_action, attempt_action
from webui.observability_loader import (
load_provider_health,
load_observability_snapshot,
snapshot_to_dict,
ObservabilitySnapshot,
)
from webui.observability_views import render_observability_page
from tests.webui_testclient import TestClient
@pytest.fixture
def test_db(tmp_path):
db_path = str(tmp_path / "test_control_plane.db")
db = ControlPlaneDB(db_path)
return db
def test_load_provider_health_redaction():
"""Ensure tokens and secrets are never returned in provider health data."""
env = {
"SENTRY_BASE_URL": "https://sentry.prgs.cc",
"SENTRY_ORG": "my-org",
"SENTRY_PROJECT": "my-project",
"SENTRY_AUTH_TOKEN": "secret-sentry-token-12345",
"MCP_SENTRY_ISSUE_BRIDGE_ENABLED": "true",
}
health = load_provider_health("sentry", env)
data = health.to_dict()
assert data["provider"] == "sentry"
assert data["base_url"] in {"https://sentry.prgs.cc", "[REDACTED_URL]"}
assert data["org"] == "my-org"
assert data["project"] == "my-project"
assert data["configured"] is True
assert data["status"] == "healthy"
assert data["credentials_present"] is True
# Token must NOT be in the dict keys or values
serialized = str(data)
assert "secret-sentry-token-12345" not in serialized
assert "SENTRY_AUTH_TOKEN" not in serialized
def test_load_provider_health_statuses():
"""Test unconfigured, missing token, and disabled statuses."""
# Not configured
h1 = load_provider_health("sentry", {})
d1 = h1.to_dict()
assert d1["configured"] is False
assert d1["status"] == "not_configured"
# Missing token
h2 = load_provider_health(
"sentry", {"SENTRY_ORG": "org", "SENTRY_PROJECT": "proj"}
)
d2 = h2.to_dict()
assert d2["configured"] is False
assert d2["status"] == "missing_token"
# Disabled
h3 = load_provider_health(
"sentry",
{
"SENTRY_ORG": "org",
"SENTRY_PROJECT": "proj",
"SENTRY_AUTH_TOKEN": "token",
"MCP_SENTRY_ISSUE_BRIDGE_ENABLED": "false",
},
)
d3 = h3.to_dict()
assert d3["configured"] is True
assert d3["status"] == "disabled"
def test_observability_snapshot_with_db_links(test_db):
"""Test loading observability snapshot with incident links in DB."""
test_db.upsert_incident_link(
provider="sentry",
provider_issue_id="101",
gitea_org="Scaled-Tech-Consulting",
gitea_repo="Gitea-Tools",
gitea_issue_number=649,
provider_base_url="https://sentry.prgs.cc",
provider_org="Scaled-Tech-Consulting",
provider_project="Gitea-Tools",
provider_short_id="ST-101",
provider_permalink="https://sentry.prgs.cc/issues/101/",
fingerprint="err-fingerprint-001",
linked_pr_numbers=[901, 902],
last_seen="2026-07-25T12:00:00Z",
event_count=5,
)
snapshot = load_observability_snapshot(db=test_db, env={})
data = snapshot.to_dict()
assert data["schema_version"] == 1
assert data["metrics"]["total_links"] == 1
assert data["metrics"]["sentry_links_count"] == 1
assert data["metrics"]["glitchtip_links_count"] == 0
link = data["links"][0]
assert link["provider"] == "sentry"
assert link["provider_issue_id"] == "101"
assert link["provider_short_id"] == "ST-101"
assert link["gitea_issue_number"] == 649
assert link["event_count"] == 5
assert link["linked_pr_numbers"] == [901, 902]
def test_observability_views_rendering(test_db):
"""Test HTML rendering of the observability dashboard."""
snapshot = load_observability_snapshot(db=test_db, env={})
html_output = render_observability_page(snapshot)
assert "Observability &amp; Incident Bridge (#649)" in html_output or "Observability & Incident Bridge (#649)" in html_output or "Observability" in html_output
assert "ADR Authority Model:" in html_output
assert "Provider Connections" in html_output
assert "Correlated Incidents" in html_output
def test_webui_observability_routes():
"""Test Starlette HTTP routes for /observability and /api/v1/observability."""
client = TestClient(create_app())
# HTML page route
res_html = client.get("/observability")
assert res_html.status_code == 200
assert "text/html" in res_html.headers["content-type"]
assert "Observability" in res_html.text
# Versioned API route
res_api_v1 = client.get("/api/v1/observability")
assert res_api_v1.status_code == 200
assert "application/json" in res_api_v1.headers["content-type"]
data_v1 = res_api_v1.json()
assert "schema_version" in data_v1
assert "providers" in data_v1
assert "links" in data_v1
assert "metrics" in data_v1
# Compatibility alias route
res_api_alias = client.get("/api/observability")
assert res_api_alias.status_code == 200
assert res_api_alias.json() == data_v1
def test_observability_gated_actions():
"""Ensure observability actions are registered, gated, and fail closed in MVP mode."""
registry = load_action_registry()
action_reconcile = registry.get("observability_reconcile_incident")
assert action_reconcile is not None
assert action_reconcile.task_key == "observability_reconcile_incident"
assert action_reconcile.mcp_tool == "gitea_observability_reconcile_incident"
action_link = registry.get("observability_link_issue")
assert action_link is not None
assert action_link.task_key == "observability_link_issue"
# Preview returns mutation ledger
prev = preview_action("observability_reconcile_incident", provider="sentry", issue_id="101")
assert prev["action_id"] == "observability_reconcile_incident"
assert prev["enabled"] is False
# Execution fails closed in MVP mode
att = attempt_action("observability_reconcile_incident", provider="sentry", issue_id="101")
assert att["success"] is False
assert att["error"] == "action_disabled"
def test_observability_authz_rbac():
"""Test RBAC authorization for observability actions."""
principal = resolve_principal({})
# Check authorize decision
decision = authorize("observability_reconcile_incident", principal)
assert decision.action_id == "observability_reconcile_incident"
# Phase 4 action denies in Phase 1 runtime by default
assert decision.allowed is False
File diff suppressed because it is too large Load Diff
+452
View File
@@ -0,0 +1,452 @@
"""Read-only restart console: views, gates, and honesty rules (#667).
The console consumes the #655 substrate. These tests hold it to the three
properties that make a status surface trustworthy:
* an unreadable source is reported unavailable, never rendered as green;
* authorization is probed the way execution would probe it, so an allow is
never shown for something that could not run;
* the surface performs no mutation, including no write to the control-plane DB.
"""
from __future__ import annotations
import os
import sqlite3
import sys
import tempfile
import unittest
from datetime import datetime, timedelta, timezone
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from starlette.testclient import TestClient # noqa: E402
import restart_coordinator # noqa: E402
from webui import console_authz, restart_console, restart_views # noqa: E402
from webui.app import create_app # noqa: E402
NOW = datetime(2026, 7, 25, 21, 0, 0, tzinfo=timezone.utc)
def _principal(role: str) -> console_authz.Principal:
return console_authz.Principal(
subject="[email protected]",
role=role,
identity_source=console_authz.IDENTITY_LOCAL_DEV,
authenticated=True,
)
def _inventory(*, complete: bool = True, sessions=(), leases=()):
def _read(**_kwargs):
return {
"sessions": list(sessions),
"leases": list(leases),
"terminal_lock": None,
"prior_recovery_attempts": [],
"inventory_complete": complete,
"incomplete_reasons": (
[] if complete else ["fixture: inventory withheld"]
),
}
return _read
def _live_session(session_id: str = "prgs-author-1234-abcd") -> dict:
return {
"session_id": session_id,
"role": "author",
"profile": "prgs-author",
"pid": os.getpid(),
"status": "active",
"last_heartbeat_at": (NOW - timedelta(seconds=30)).isoformat(),
}
def drain_proof_fixture() -> dict:
"""A structurally complete but unsigned drain proof."""
return {
"version": "drain-proof/v1",
"proof_id": "deadbeef" * 8,
"clean": True,
"issued_at": (NOW - timedelta(minutes=1)).isoformat(),
"expires_at": (NOW + timedelta(minutes=5)).isoformat(),
"requesting_session_id": "s-live",
"impact_fingerprint": "f" * 64,
"checks": [],
"failed_checks": [],
}
class RestartClassMatrixTest(unittest.TestCase):
def test_every_policy_class_is_rendered(self) -> None:
views = restart_console.build_restart_class_views("operator")
self.assertEqual(len(views), len(restart_coordinator.RESTART_CLASS_POLICIES))
def test_viewer_capability_is_role_scoped_not_generic(self) -> None:
"""A worker role must not be shown as able to request a full restart."""
author = {
v.restart_class: v
for v in restart_console.build_restart_class_views("author")
}
operator = {
v.restart_class: v
for v in restart_console.build_restart_class_views("operator")
}
full = restart_coordinator.RestartClass.FULL_MCP_RESTART.value
self.assertFalse(author[full].viewer_may_request)
self.assertFalse(author[full].viewer_may_execute)
self.assertTrue(operator[full].viewer_may_request)
self.assertTrue(operator[full].viewer_may_execute)
def test_unknown_role_may_do_nothing(self) -> None:
views = restart_console.build_restart_class_views("not-a-role")
self.assertTrue(all(not v.viewer_may_request for v in views))
self.assertTrue(all(not v.viewer_may_execute for v in views))
class AuthorizationProbeTest(unittest.TestCase):
def test_probe_asks_for_execution_so_phase_gate_is_reported(self) -> None:
"""An admin clears the role bar and still cannot execute in Phase 1.
This is the case that distinguishes the two probes. Asked without
``for_execution`` an admin is *allowed* for ``system.restart_namespace``,
which on a control surface reads as a live button. Asked the way
execution asks, the same principal is refused ``phase_not_active``. The
console must report the second answer.
"""
by_id = {
a.action_id: a
for a in restart_console.build_action_authorizations(
_principal(console_authz.ADMIN)
)
}
restart = by_id["system.restart_namespace"]
self.assertFalse(restart.execution_enabled)
self.assertEqual(restart.reason_code, console_authz.DENY_PHASE_NOT_ACTIVE)
permissive = console_authz.authorize(
"system.restart_namespace", _principal(console_authz.ADMIN)
)
self.assertTrue(
permissive.allowed,
"guard precondition: without for_execution an admin is allowed, "
"which is exactly why the console must not probe that way",
)
def test_operator_is_refused_the_admin_only_restart_action(self) -> None:
"""Role refusal precedes the phase gate and is reported as such."""
by_id = {
a.action_id: a
for a in restart_console.build_action_authorizations(
_principal(console_authz.OPERATOR)
)
}
self.assertEqual(
by_id["system.restart_namespace"].reason_code,
console_authz.DENY_INSUFFICIENT_ROLE,
)
def test_anonymous_is_denied_unauthenticated(self) -> None:
by_id = {
a.action_id: a for a in restart_console.build_action_authorizations(None)
}
self.assertEqual(
by_id["system.restart_namespace"].reason_code,
console_authz.DENY_UNAUTHENTICATED,
)
def test_no_authorization_ever_reports_execution_enabled(self) -> None:
for role in (
console_authz.VIEWER,
console_authz.OPERATOR,
console_authz.CONTROLLER,
console_authz.ADMIN,
):
for auth in restart_console.build_action_authorizations(_principal(role)):
self.assertFalse(
auth.execution_enabled,
f"{role} reported execution_enabled for {auth.action_id}",
)
class ImpactPreviewTest(unittest.TestCase):
def test_impact_renders_from_coordinator_dto(self) -> None:
impact, source = restart_console.load_impact_report(
principal=_principal(console_authz.OPERATOR),
read_inventory=_inventory(sessions=[_live_session()]),
now=NOW,
)
self.assertTrue(source.available)
self.assertIsNotNone(impact)
self.assertEqual(
impact["restart_class"],
restart_coordinator.RestartClass.FULL_MCP_RESTART.value,
)
self.assertIn("verdict", impact)
self.assertFalse(impact["restart_performed"])
self.assertTrue(impact["dry_run"])
def test_incomplete_inventory_is_surfaced_and_denies(self) -> None:
impact, source = restart_console.load_impact_report(
principal=_principal(console_authz.OPERATOR),
read_inventory=_inventory(complete=False),
now=NOW,
)
self.assertFalse(impact["inventory_complete"])
self.assertFalse(impact["allow_restart"])
self.assertTrue(source.detail, "incomplete inventory must explain itself")
def test_inventory_reader_failure_is_unavailable_not_empty(self) -> None:
"""A reader that raises must not be rendered as 'no sessions affected'."""
def _boom(**_kwargs):
raise RuntimeError("control-plane unreachable")
impact, source = restart_console.load_impact_report(
principal=_principal(console_authz.OPERATOR),
read_inventory=_boom,
now=NOW,
)
self.assertIsNone(impact)
self.assertFalse(source.available)
self.assertIn("control-plane unreachable", source.detail)
class ControlPlaneReadTest(unittest.TestCase):
def test_missing_database_is_incomplete_not_empty(self) -> None:
inventory = restart_console.read_control_plane_inventory(
db_path="/nonexistent/control-plane.sqlite3"
)
self.assertFalse(inventory["inventory_complete"])
self.assertEqual(inventory["sessions"], [])
self.assertTrue(inventory["incomplete_reasons"])
def test_reader_never_creates_the_database(self) -> None:
"""Reading status must not bring a control-plane DB into existence.
The path deliberately sits in a directory that already exists: a
read-write ``sqlite3.connect`` would happily create the file there, so
this fails if the reader ever stops opening the database ``mode=ro``.
A nested-missing-directory path would pass for the wrong reason,
because sqlite cannot create the parent directory either way.
"""
with tempfile.TemporaryDirectory() as tmp:
path = os.path.join(tmp, "control_plane.sqlite3")
self.assertTrue(os.path.isdir(os.path.dirname(path)))
inventory = restart_console.read_control_plane_inventory(db_path=path)
self.assertFalse(
os.path.exists(path),
"reading restart status created a control-plane database",
)
self.assertFalse(inventory["inventory_complete"])
def test_reads_active_sessions_from_a_real_database(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
path = os.path.join(tmp, "cp.sqlite3")
conn = sqlite3.connect(path)
conn.execute(
"CREATE TABLE sessions (session_id TEXT, role TEXT, profile TEXT,"
" pid INTEGER, status TEXT, last_heartbeat_at TEXT)"
)
conn.execute(
"CREATE TABLE work_items (work_item_id INTEGER, kind TEXT,"
" number INTEGER)"
)
conn.execute(
"CREATE TABLE leases (lease_id TEXT, session_id TEXT, role TEXT,"
" phase TEXT, status TEXT, worktree_path TEXT,"
" work_item_id INTEGER, expires_at TEXT)"
)
conn.execute(
"INSERT INTO sessions VALUES (?,?,?,?,?,?)",
("s-live", "author", "prgs-author", 4242, "active", NOW.isoformat()),
)
conn.execute(
"INSERT INTO sessions VALUES (?,?,?,?,?,?)",
("s-done", "author", "prgs-author", 11, "closed", NOW.isoformat()),
)
conn.execute("INSERT INTO work_items VALUES (1, 'issue', 667)")
conn.execute(
"INSERT INTO leases VALUES (?,?,?,?,?,?,?,?)",
(
"l-1",
"s-live",
"author",
"allocated",
"active",
None,
1,
NOW.isoformat(),
),
)
conn.commit()
conn.close()
inventory = restart_console.read_control_plane_inventory(db_path=path)
self.assertTrue(inventory["inventory_complete"])
self.assertEqual([s["session_id"] for s in inventory["sessions"]], ["s-live"])
self.assertEqual(inventory["leases"][0]["work_number"], 667)
class DrainAndReconcileTest(unittest.TestCase):
def test_absent_drain_proof_is_not_a_pass(self) -> None:
drain, source = restart_console.load_drain_status(proof=None, now=NOW)
self.assertIsNone(drain)
self.assertFalse(source.available)
self.assertIn("denies", source.detail)
def test_tampered_drain_proof_is_reported_invalid(self) -> None:
proof = drain_proof_fixture()
proof["clean"] = True
proof["proof_id"] = "0" * 64
drain, source = restart_console.load_drain_status(proof=proof, now=NOW)
self.assertTrue(source.available)
self.assertFalse(drain["valid"])
def test_absent_reconcile_proof_is_unavailable(self) -> None:
reconcile, source = restart_console.load_reconcile_status(load_proof=None)
self.assertIsNone(reconcile)
self.assertFalse(source.available)
def test_reconcile_proof_is_rendered_when_supplied(self) -> None:
payload = {
"overall_status": "degraded",
"mode": "log_only",
"resolved_count": 3,
"unresolved_count": 2,
"items": [
{
"dimension": "leases",
"status": "unresolved",
"summary": "2 orphaned leases",
"follow_up_required": True,
}
],
}
reconcile, source = restart_console.load_reconcile_status(
load_proof=lambda: payload
)
self.assertTrue(source.available)
self.assertEqual(reconcile["unresolved_count"], 2)
class RenderingTest(unittest.TestCase):
def _snapshot(self, **kwargs):
params = {
"principal": _principal(console_authz.OPERATOR),
"read_inventory": _inventory(sessions=[_live_session()]),
"now": NOW,
}
params.update(kwargs)
return restart_console.load_restart_console_snapshot(**params)
def test_page_renders_every_section(self) -> None:
html = restart_views.render_restart_console_page(self._snapshot())
for heading in (
"Impact preview",
"Drain proof",
"Post-restart reconcile",
"Restart classes",
"Approval controls",
"Break-glass",
):
self.assertIn(heading, html)
def test_hostile_session_id_is_escaped(self) -> None:
hostile = "<script>alert('x')</script>"
html = restart_views.render_restart_console_page(
self._snapshot(read_inventory=_inventory(sessions=[_live_session(hostile)]))
)
self.assertNotIn("<script>alert", html)
self.assertIn("&lt;script&gt;", html)
def test_unavailable_impact_says_unsafe_rather_than_clean(self) -> None:
def _boom(**_kwargs):
raise RuntimeError("nope")
snapshot = self._snapshot(read_inventory=_boom)
html = restart_views.render_restart_console_page(snapshot)
self.assertIn("blast radius of a restart is unknown", html)
self.assertIn("unavailable", html)
def test_break_glass_is_hidden_from_unprivileged_viewers(self) -> None:
viewer_html = restart_views.render_restart_console_page(
self._snapshot(principal=_principal(console_authz.VIEWER))
)
self.assertIn("visible to operator-class", viewer_html)
self.assertNotIn(
f"#{restart_console.BREAK_GLASS_ISSUE}", viewer_html
)
def test_break_glass_shown_to_operator_is_marked_unavailable(self) -> None:
html = restart_views.render_restart_console_page(self._snapshot())
self.assertIn("unavailable", html)
self.assertIn(f"#{restart_console.BREAK_GLASS_ISSUE}", html)
def test_snapshot_always_declares_itself_read_only(self) -> None:
self.assertTrue(self._snapshot().read_only)
class RestartConsoleRouteTest(unittest.TestCase):
def setUp(self) -> None:
self.client = TestClient(create_app())
def test_page_route_renders(self) -> None:
res = self.client.get("/runtime/restart")
self.assertEqual(res.status_code, 200)
self.assertIn("Restart status and impact", res.text)
def test_api_route_exports_snapshot(self) -> None:
res = self.client.get("/api/v1/system/restart/status")
self.assertEqual(res.status_code, 200)
payload = res.json()
self.assertTrue(payload["read_only"])
self.assertEqual(payload["links"]["issue"], 667)
self.assertEqual(
len(payload["restart_classes"]),
len(restart_coordinator.RESTART_CLASS_POLICIES),
)
def test_restart_class_is_selectable(self) -> None:
res = self.client.get(
"/api/v1/system/restart/status?restart_class=client_reconnect"
)
self.assertEqual(res.status_code, 200)
self.assertEqual(res.json()["impact"]["restart_class"], "client_reconnect")
def test_unknown_restart_class_fails_closed(self) -> None:
res = self.client.get(
"/api/v1/system/restart/status?restart_class=obliterate-everything"
)
self.assertEqual(res.status_code, 200)
impact = res.json()["impact"]
self.assertFalse(impact["allow_restart"])
def test_anonymous_api_reader_gets_no_execution_grant(self) -> None:
payload = self.client.get("/api/v1/system/restart/status").json()
self.assertFalse(payload["break_glass"]["available"])
for auth in payload["authorizations"]:
self.assertFalse(auth["execution_enabled"])
def test_route_is_registered_in_nav(self) -> None:
from webui.nav import nav_hrefs
self.assertIn("/runtime/restart", nav_hrefs())
def test_no_write_method_is_exposed(self) -> None:
"""The surface is read-only: nothing accepts a POST."""
for path in ("/runtime/restart", "/api/v1/system/restart/status"):
self.assertEqual(self.client.post(path).status_code, 405, path)
if __name__ == "__main__":
unittest.main()