"""CAS-protected stale worker retirement (#980). Covers the acceptance criteria: the registry CAS token is stable across time and row order but moves on any retirement-relevant change, plan mutates nothing, apply fails closed on drift, every target is revalidated inside the retirement transaction, and live / ambiguous / incomplete rows are preserved. The regression test that matters most is ``test_snapshot_at_would_have_moved_the_token``: it demonstrates the exact defect the R3-C review found — ``mcp_fleet_snapshot._consistency_token`` changes when only the observation time changed — and proves the new token does not. All identifiers are synthetic. """ from __future__ import annotations import os import tempfile import unittest from datetime import datetime, timedelta, timezone from typing import Any import mcp_fleet_retirement as retire import mcp_fleet_snapshot as fleet import mcp_worker_identity as mwi NOW = datetime(2026, 7, 30, 7, 0, 0, tzinfo=timezone.utc) TTL = 900.0 REPO = "/synthetic/repo/Gitea-Tools" _ROW_COLUMNS = ( "worker_identity", "client_name", "client_instance_id", "session_id", "generation_id", "role", "profile", "namespace", "remote", "repository_binding", "pid", "process_identity", "transport", "token_fingerprint", "started_at", "last_heartbeat_at", "heartbeat_ttl_seconds", "fencing_epoch", "status", "fleet_run_id", "authenticated_account", "instance_id_provenance", ) def _registry() -> mwi.WorkerRegistry: handle, path = tempfile.mkstemp(suffix=".sqlite3") os.close(handle) os.unlink(path) return mwi.WorkerRegistry(path) def _row( *, identity: str, pid: int | None, instance: str = "inst-codex-20260730T070000Z-0123456789ab", session: str = "sess-a", generation: str = "gen-a", namespace: str = "author", heartbeat: datetime | None = None, status: str = mwi.STATUS_ACTIVE, repository_binding: str | None = REPO, ttl: float = TTL, **overrides: Any, ) -> dict[str, Any]: """One synthetic ``worker_registrations`` row.""" record = { "worker_identity": identity, "client_name": "codex", "client_instance_id": instance, "session_id": session, "generation_id": generation, "role": namespace, "profile": f"prgs-{namespace}", "namespace": namespace, "remote": "prgs", "repository_binding": repository_binding, "pid": pid, "process_identity": f"pid-{pid}" if pid is not None else None, "transport": "stdio", "token_fingerprint": None, "started_at": "2026-07-30T06:00:00Z", "last_heartbeat_at": mwi._ts(heartbeat or (NOW - timedelta(seconds=7200))), "heartbeat_ttl_seconds": ttl, "fencing_epoch": 1, "status": status, "fleet_run_id": "run-canary", "authenticated_account": "synthetic-user", "instance_id_provenance": fleet.INSTANCE_ID_PROVENANCE_TRUSTED, } record.update(overrides) return record def _dead(_pid: int | None) -> bool: return False def _alive(_pid: int | None) -> bool: return True def _selective(alive_pids: set[int]): def probe(pid: int | None) -> bool: return pid in alive_pids return probe def _plan(rows, *, probe=_dead, now=NOW, **kwargs): return retire.plan_stale_worker_retirement( rows, now=now, pid_alive_probe=probe, canonical_repository=REPO, **kwargs, ) def _register(registry: mwi.WorkerRegistry, row: dict[str, Any]) -> None: """Insert a synthetic row directly, bypassing register()'s live-now stamps.""" columns = [name for name in _ROW_COLUMNS if name in row] placeholders = ", ".join("?" for _ in columns) with registry._tx() as conn: conn.execute( f"INSERT INTO worker_registrations ({', '.join(columns)}) " f"VALUES ({placeholders})", [row[name] for name in columns], ) class RegistryFingerprintStabilityTests(unittest.TestCase): """AC: identical contents observed at different times produce one token.""" def test_same_contents_different_observation_times_same_token(self): rows = [_row(identity="w-1", pid=101), _row(identity="w-2", pid=102)] self.assertEqual( retire.registry_fingerprint(rows), retire.registry_fingerprint(rows) ) # Recompute after the clock has moved a full hour: the token is derived # from content only, so it cannot notice. plan_early = _plan(rows, now=NOW) plan_late = _plan(rows, now=NOW + timedelta(hours=1)) self.assertEqual( plan_early["registry_fingerprint"], plan_late["registry_fingerprint"] ) def test_snapshot_at_would_have_moved_the_token(self): """Regression: the old time-seeded derivation moved, the new one does not.""" rows = [_row(identity="w-1", pid=101)] old_early = fleet._consistency_token(rows, "2026-07-30T07:00:00Z") old_late = fleet._consistency_token(rows, "2026-07-30T07:00:01Z") self.assertNotEqual( old_early, old_late, "the #980 defect: one second of observation drift changed the token", ) self.assertEqual( retire.registry_fingerprint(rows), retire.registry_fingerprint(rows) ) def test_row_order_does_not_change_the_token(self): rows = [ _row(identity="w-1", pid=101), _row(identity="w-2", pid=102), _row(identity="w-3", pid=103), ] self.assertEqual( retire.registry_fingerprint(rows), retire.registry_fingerprint(list(reversed(rows))), ) def test_numeric_typing_does_not_change_the_token(self): as_float = [_row(identity="w-1", pid=101, heartbeat_ttl_seconds=900.0)] as_int = [_row(identity="w-1", pid=101, heartbeat_ttl_seconds=900)] self.assertEqual( retire.registry_fingerprint(as_float), retire.registry_fingerprint(as_int), ) def test_retirement_relevant_changes_move_the_token(self): base = [_row(identity="w-1", pid=101)] baseline = retire.registry_fingerprint(base) mutations = { "row added": base + [_row(identity="w-2", pid=102)], "row removed": [], "status changed": [_row(identity="w-1", pid=101, status="released")], "identity changed": [ _row( identity="w-1", pid=101, instance="inst-codex-20260730T070000Z-ffffffffffff", ) ], "ownership changed": [_row(identity="w-1", pid=101, session="sess-other")], "generation changed": [ _row(identity="w-1", pid=101, generation="gen-other") ], "pid changed": [_row(identity="w-1", pid=999)], "repository binding changed": [ _row(identity="w-1", pid=101, repository_binding="/elsewhere") ], } for label, rows in mutations.items(): with self.subTest(change=label): self.assertNotEqual(baseline, retire.registry_fingerprint(rows)) def test_heartbeat_change_moves_the_token(self): base = [_row(identity="w-1", pid=101)] moved = [_row(identity="w-1", pid=101, heartbeat=NOW - timedelta(seconds=30))] self.assertNotEqual( retire.registry_fingerprint(base), retire.registry_fingerprint(moved) ) def test_ttl_change_moves_the_token(self): base = [_row(identity="w-1", pid=101)] moved = [_row(identity="w-1", pid=101, ttl=60.0)] self.assertNotEqual( retire.registry_fingerprint(base), retire.registry_fingerprint(moved) ) class PlanEligibilityTests(unittest.TestCase): """AC: only conclusively stale orphans are selected; everything else stays.""" def test_plan_selects_dead_stale_orphan(self): plan = _plan([_row(identity="w-1", pid=101)]) self.assertEqual(plan["candidate_count"], 1) self.assertEqual(plan["candidate_worker_identities"], ["w-1"]) self.assertEqual(plan["candidates"][0]["reason_code"], retire.REASON_ELIGIBLE) self.assertFalse(plan["mutation_performed"]) self.assertTrue(plan["read_only"]) def test_plan_performs_no_mutation(self): registry = _registry() _register(registry, _row(identity="w-1", pid=101)) before = registry.list_workers(status=None) plan = _plan(before) after = registry.list_workers(status=None) self.assertEqual(plan["candidate_count"], 1) self.assertEqual(before, after) self.assertEqual([r["status"] for r in after], [mwi.STATUS_ACTIVE]) self.assertFalse(plan["mutation_performed"]) def test_live_worker_is_preserved(self): row = _row(identity="w-live", pid=101, heartbeat=NOW - timedelta(seconds=10)) plan = _plan([row], probe=_alive) self.assertEqual(plan["candidate_count"], 0) self.assertEqual( plan["preserved"][0]["reason_code"], retire.REASON_WORKER_LIVE ) def test_fresh_heartbeat_with_dead_pid_still_fails_closed(self): """A dead pid withdraws liveness; the unexpired heartbeat still preserves.""" row = _row(identity="w-fresh", pid=101, heartbeat=NOW - timedelta(seconds=10)) plan = _plan([row], probe=_dead) self.assertEqual(plan["candidate_count"], 0) self.assertEqual( plan["preserved"][0]["reason_code"], retire.REASON_HEARTBEAT_FRESH ) def test_unprobeable_pid_is_preserved(self): plan = _plan([_row(identity="w-1", pid=101)], probe=lambda _pid: None) self.assertEqual(plan["candidate_count"], 0) self.assertEqual( plan["preserved"][0]["reason_code"], retire.REASON_PID_UNKNOWN ) def test_incomplete_legacy_identity_is_preserved(self): """AC: incomplete legacy identities remain fail-closed.""" rows = [ _row(identity="w-nopid", pid=None, instance="legacy-pid-27833"), _row(identity="w-nosession", pid=102, session=""), ] plan = _plan(rows) self.assertEqual(plan["candidate_count"], 0) self.assertEqual( {p["reason_code"] for p in plan["preserved"]}, {retire.REASON_INCOMPLETE_IDENTITY}, ) detail = next( p["detail"] for p in plan["preserved"] if p["worker_identity"] == "w-nopid" ) self.assertIn("pid", detail) def test_unparsable_heartbeat_is_preserved(self): rows = [_row(identity="w-1", pid=101, last_heartbeat_at="not-a-stamp")] plan = _plan(rows) self.assertEqual(plan["candidate_count"], 0) self.assertEqual( plan["preserved"][0]["reason_code"], retire.REASON_UNPARSABLE_HEARTBEAT ) def test_identity_shared_with_live_worker_is_preserved(self): """Two rows, one live: the dead one shares session evidence, so it stays.""" rows = [ _row( identity="w-live", pid=101, session="sess-shared", heartbeat=NOW - timedelta(seconds=5), ), _row(identity="w-dead", pid=102, session="sess-shared"), ] plan = _plan(rows, probe=_selective({101})) self.assertEqual(plan["candidate_count"], 0) codes = {p["reason_code"] for p in plan["preserved"]} self.assertIn(retire.REASON_CONFLICTING_IDENTITY, codes) def test_foreign_or_missing_repository_binding_is_preserved(self): rows = [ _row(identity="w-foreign", pid=101, repository_binding="/other/repo"), _row(identity="w-unbound", pid=102, repository_binding=None), ] plan = _plan(rows) self.assertEqual(plan["candidate_count"], 0) self.assertEqual( {p["reason_code"] for p in plan["preserved"]}, {retire.REASON_FOREIGN_REPOSITORY}, ) def test_active_workflow_owner_is_preserved(self): rows = [_row(identity="w-1", pid=101, session="sess-leased")] plan = _plan(rows, protected_session_ids=["sess-leased"]) self.assertEqual(plan["candidate_count"], 0) self.assertEqual( plan["preserved"][0]["reason_code"], retire.REASON_PROTECTED_OWNER ) def test_terminal_rows_are_not_retired_again(self): rows = [_row(identity="w-1", pid=101, status=mwi.STATUS_RELEASED)] plan = _plan(rows) self.assertEqual(plan["candidate_count"], 0) self.assertEqual( plan["preserved"][0]["reason_code"], retire.REASON_ALREADY_TERMINAL ) def test_mixed_fleet_produces_correct_per_worker_outcomes(self): rows = [ _row(identity="w-stale-1", pid=101, session="s1", generation="g1"), _row(identity="w-stale-2", pid=102, session="s2", generation="g2"), _row( identity="w-live", pid=103, session="s3", generation="g3", heartbeat=NOW - timedelta(seconds=5), ), _row(identity="w-nopid", pid=None, session="s4", generation="g4"), _row( identity="w-foreign", pid=105, session="s5", generation="g5", repository_binding="/other", ), _row( identity="w-terminal", pid=106, session="s6", generation="g6", status=mwi.STATUS_SUPERSEDED, ), ] plan = _plan(rows, probe=_selective({103})) self.assertEqual( sorted(plan["candidate_worker_identities"]), ["w-stale-1", "w-stale-2"] ) by_identity = { p["worker_identity"]: p["reason_code"] for p in plan["preserved"] } self.assertEqual(by_identity["w-live"], retire.REASON_WORKER_LIVE) self.assertEqual(by_identity["w-nopid"], retire.REASON_INCOMPLETE_IDENTITY) self.assertEqual(by_identity["w-foreign"], retire.REASON_FOREIGN_REPOSITORY) self.assertEqual(by_identity["w-terminal"], retire.REASON_ALREADY_TERMINAL) self.assertEqual(plan["assessed_count"], 6) self.assertEqual(plan["preserved_count"], 4) def test_plan_is_deterministic_for_identical_contents(self): rows = [_row(identity="w-1", pid=101), _row(identity="w-2", pid=102)] first = _plan(rows) second = _plan(list(reversed(rows)), now=NOW + timedelta(minutes=5)) self.assertEqual(first["registry_fingerprint"], second["registry_fingerprint"]) self.assertEqual( first["candidate_fingerprint"], second["candidate_fingerprint"] ) self.assertEqual( sorted(first["candidate_worker_identities"]), sorted(second["candidate_worker_identities"]), ) class ApplyCasTests(unittest.TestCase): """AC: apply is compare-and-swap protected and revalidates every target.""" def setUp(self) -> None: self.registry = _registry() self.probe = _dead def _rows(self): return self.registry.list_workers(status=None) def _apply(self, plan, *, probe=None, identities=None, **kwargs): chosen = probe or self.probe return self.registry.retire_stale_workers( expected_registry_fingerprint=plan["registry_fingerprint"], expected_candidate_fingerprint=plan["candidate_fingerprint"], worker_identities=( identities if identities is not None else plan["candidate_worker_identities"] ), fingerprint_fn=retire.registry_fingerprint, plan_fn=lambda rows: _plan(rows, probe=chosen, **kwargs), retired_by="synthetic-user/prgs-reconciler", now=NOW, ) def test_matching_token_retires_the_planned_set(self): _register(self.registry, _row(identity="w-1", pid=101, session="s1")) _register(self.registry, _row(identity="w-2", pid=102, session="s2")) plan = _plan(self._rows(), probe=self.probe) result = self._apply(plan) self.assertEqual(result["outcome"], "applied") self.assertTrue(result["mutation_performed"]) self.assertEqual(result["retired_count"], 2) statuses = {r["worker_identity"]: r["status"] for r in self._rows()} self.assertEqual( statuses, {"w-1": mwi.STATUS_RETIRED, "w-2": mwi.STATUS_RETIRED} ) retired_row = self.registry.get("w-1") self.assertEqual(retired_row["retired_at"], mwi._ts(NOW)) self.assertEqual(retired_row["retired_by"], "synthetic-user/prgs-reconciler") def test_moved_registry_token_retires_zero_workers(self): _register(self.registry, _row(identity="w-1", pid=101)) plan = _plan(self._rows(), probe=self.probe) # An unrelated registration lands between plan and apply. _register(self.registry, _row(identity="w-2", pid=102, session="s2")) result = self._apply(plan) self.assertEqual(result["outcome"], retire.OUTCOME_REGISTRY_MOVED) self.assertEqual(result["retired_count"], 0) self.assertFalse(result["mutation_performed"]) self.assertTrue(all(r["status"] == mwi.STATUS_ACTIVE for r in self._rows())) def test_moved_candidate_set_retires_zero_workers(self): """Registry unchanged, but the eligibility verdict is no longer the same.""" _register(self.registry, _row(identity="w-1", pid=101)) plan = _plan(self._rows(), probe=self.probe) # The row did not change; the process came back (pid probes alive), so # revalidation inside the transaction finds no candidates at all. result = self._apply(plan, probe=_alive) self.assertEqual(result["outcome"], retire.OUTCOME_CANDIDATES_MOVED) self.assertEqual(result["retired_count"], 0) self.assertFalse(result["mutation_performed"]) self.assertEqual(self.registry.get("w-1")["status"], mwi.STATUS_ACTIVE) def test_worker_that_becomes_live_between_plan_and_apply_is_preserved(self): _register(self.registry, _row(identity="w-dead", pid=101, session="s1")) _register(self.registry, _row(identity="w-back", pid=102, session="s2")) plan = _plan(self._rows(), probe=self.probe) self.assertEqual(len(plan["candidate_worker_identities"]), 2) # w-back's process is alive by the time apply runs, so the candidate # fingerprint moves and nothing at all is retired. result = self._apply(plan, probe=_selective({102})) self.assertEqual(result["outcome"], retire.OUTCOME_CANDIDATES_MOVED) self.assertEqual(result["retired_count"], 0) self.assertEqual(self.registry.get("w-back")["status"], mwi.STATUS_ACTIVE) self.assertEqual(self.registry.get("w-dead")["status"], mwi.STATUS_ACTIVE) def test_worker_that_becomes_ambiguous_between_plan_and_apply_is_preserved(self): _register(self.registry, _row(identity="w-1", pid=101, session="s1")) plan = _plan(self._rows(), probe=self.probe) result = self._apply(plan, probe=lambda _pid: None) self.assertEqual(result["outcome"], retire.OUTCOME_CANDIDATES_MOVED) self.assertEqual(result["retired_count"], 0) self.assertEqual(self.registry.get("w-1")["status"], mwi.STATUS_ACTIVE) def test_apply_revalidates_and_will_not_narrow_the_approved_set(self): """A protection appearing after plan moves the CAS, so nothing is retired.""" _register(self.registry, _row(identity="w-1", pid=101, session="s1")) _register( self.registry, _row(identity="w-protected", pid=102, session="sess-leased") ) unprotected_plan = _plan(self._rows(), probe=self.probe) self.assertEqual(unprotected_plan["candidate_count"], 2) result = self._apply( unprotected_plan, protected_session_ids=["sess-leased"] ) self.assertEqual(result["outcome"], retire.OUTCOME_CANDIDATES_MOVED) self.assertEqual(result["retired_count"], 0) self.assertEqual( self.registry.get("w-protected")["status"], mwi.STATUS_ACTIVE ) def test_target_outside_the_current_plan_is_preserved(self): _register( self.registry, _row(identity="w-1", pid=101, session="s1", generation="g1"), ) _register( self.registry, _row( identity="w-live", pid=102, session="s2", generation="g2", heartbeat=NOW - timedelta(seconds=5), ), ) probe = _selective({102}) plan = _plan(self._rows(), probe=probe) self.assertEqual(plan["candidate_worker_identities"], ["w-1"]) # A caller that appends a live identity to the approved list gets it # preserved with the live reason code; only the real candidate retires. result = self._apply(plan, probe=probe, identities=["w-1", "w-live"]) self.assertEqual(result["outcome"], "applied") self.assertEqual(result["retired_count"], 1) self.assertEqual(result["retired"][0]["worker_identity"], "w-1") preserved = { p["worker_identity"]: p["reason_code"] for p in result["preserved"] } self.assertEqual(preserved["w-live"], retire.REASON_WORKER_LIVE) self.assertEqual(self.registry.get("w-live")["status"], mwi.STATUS_ACTIVE) def test_missing_registration_is_reported_not_invented(self): _register(self.registry, _row(identity="w-1", pid=101)) plan = _plan(self._rows(), probe=self.probe) result = self._apply(plan, identities=["w-1", "w-ghost"]) self.assertEqual(result["retired_count"], 1) preserved = { p["worker_identity"]: p["reason_code"] for p in result["preserved"] } self.assertEqual(preserved["w-ghost"], "registration_missing") def test_reapplying_a_completed_plan_is_a_safe_no_op(self): _register(self.registry, _row(identity="w-1", pid=101)) plan = _plan(self._rows(), probe=self.probe) first = self._apply(plan) self.assertEqual(first["retired_count"], 1) second = self._apply(plan) self.assertEqual(second["outcome"], retire.OUTCOME_ALREADY_RETIRED) self.assertTrue(second["idempotent"]) self.assertEqual(second["retired_count"], 0) self.assertFalse(second["mutation_performed"]) self.assertEqual(self.registry.get("w-1")["status"], mwi.STATUS_RETIRED) def test_empty_target_list_reports_nothing_requested(self): _register(self.registry, _row(identity="w-1", pid=101)) plan = _plan(self._rows(), probe=self.probe) result = self._apply(plan, identities=[]) self.assertEqual(result["outcome"], retire.OUTCOME_NOTHING_REQUESTED) self.assertFalse(result["mutation_performed"]) self.assertEqual(self.registry.get("w-1")["status"], mwi.STATUS_ACTIVE) def test_transaction_failure_cannot_report_partial_success(self): _register(self.registry, _row(identity="w-1", pid=101, session="s1")) _register(self.registry, _row(identity="w-2", pid=102, session="s2")) plan = _plan(self._rows(), probe=self.probe) calls = {"n": 0} def exploding_plan(rows): calls["n"] += 1 raise RuntimeError("synthetic revalidation failure") result = self.registry.retire_stale_workers( expected_registry_fingerprint=plan["registry_fingerprint"], expected_candidate_fingerprint=plan["candidate_fingerprint"], worker_identities=plan["candidate_worker_identities"], fingerprint_fn=retire.registry_fingerprint, plan_fn=exploding_plan, retired_by="synthetic-user/prgs-reconciler", now=NOW, ) self.assertEqual(calls["n"], 1) self.assertFalse(result["success"]) self.assertEqual(result["outcome"], "transaction_failed") self.assertEqual(result["retired_count"], 0) self.assertFalse(result["mutation_performed"]) self.assertTrue( all(r["status"] == mwi.STATUS_ACTIVE for r in self._rows()), "a failed transaction must roll back every retirement", ) def test_structured_result_fields_are_accurate(self): _register(self.registry, _row(identity="w-1", pid=101)) plan = _plan(self._rows(), probe=self.probe) blocked = self._apply(plan, probe=_alive) self.assertFalse(blocked["mutation_performed"]) self.assertEqual(blocked["retired"], []) self.assertEqual(blocked["requested_count"], 1) self.assertEqual( blocked["expected_registry_fingerprint"], plan["registry_fingerprint"] ) applied = self._apply(plan) self.assertTrue(applied["mutation_performed"]) self.assertEqual(applied["acting_identity"], "synthetic-user/prgs-reconciler") self.assertEqual(applied["retired"][0]["reason_code"], retire.REASON_ELIGIBLE) self.assertIn("evidence", applied["retired"][0]) class PostRetirementFleetCompatibilityTests(unittest.TestCase): """AC: retired rows leave the stale count and never fake fleet safety.""" def test_retired_rows_are_historical_not_stale(self): registry = _registry() _register( registry, _row(identity="w-1", pid=101, session="s1", generation="g1") ) _register( registry, _row( identity="w-live", pid=102, session="s2", generation="g2", instance="legacy-pid-27833", instance_id_provenance=fleet.INSTANCE_ID_PROVENANCE_LEGACY, heartbeat=NOW - timedelta(seconds=5), ), ) probe = _selective({102}) before = fleet.snapshot_instance_fleet( registry.list_workers(status=None), now=NOW, pid_alive_probe=probe, canonical_repository=REPO, ) self.assertEqual(before["stale_worker_count"], 1) plan = _plan(registry.list_workers(status=None), probe=probe) result = registry.retire_stale_workers( expected_registry_fingerprint=plan["registry_fingerprint"], expected_candidate_fingerprint=plan["candidate_fingerprint"], worker_identities=plan["candidate_worker_identities"], fingerprint_fn=retire.registry_fingerprint, plan_fn=lambda rows: _plan(rows, probe=probe), retired_by="synthetic-user/prgs-reconciler", now=NOW, ) self.assertEqual(result["retired_count"], 1) after = fleet.snapshot_instance_fleet( registry.list_workers(status=None), now=NOW, pid_alive_probe=probe, canonical_repository=REPO, ) self.assertEqual(after["stale_worker_count"], 0) self.assertEqual(after["live_worker_count"], 1) self.assertEqual(after["historical_worker_count"], 1) # The surviving live worker still carries a legacy instance identity, so # the fleet must not be declared safe. self.assertFalse(after["live_fleet_safe"]) self.assertIn( fleet.CLASS_LEGACY_INCOMPLETE, {f["classification"] for f in after["active_blockers"]}, ) def test_existing_snapshot_shape_is_unchanged(self): rows = [_row(identity="w-1", pid=101)] snapshot = fleet.snapshot_instance_fleet( rows, now=NOW, pid_alive_probe=_dead, canonical_repository=REPO ) for key in ( "consistency_token", "registry_revision", "snapshot_at", "live_workers", "stale_workers", "historical_workers", "findings", "live_fleet_safe", ): self.assertIn(key, snapshot) self.assertTrue(snapshot["registry_revision"].startswith("fleetrev-")) self.assertTrue(snapshot["read_only"]) self.assertFalse(snapshot.get("mutation_performed", False)) class CapabilityExposureTests(unittest.TestCase): """AC: only controller/reconciler reach the surface; author policy unchanged.""" def test_capability_map_declares_controller_role(self): import task_capability_map as tcm for task in ( "plan_stale_worker_retirement", "gitea_plan_stale_worker_retirement", "apply_stale_worker_retirement", "gitea_apply_stale_worker_retirement", ): with self.subTest(task=task): self.assertEqual(tcm.required_permission(task), "gitea.read") self.assertEqual(tcm.required_role(task), "controller") def test_ordinary_author_capability_resolution_is_unaffected(self): import task_capability_map as tcm self.assertEqual(tcm.required_permission("work_issue"), "gitea.pr.create") self.assertEqual(tcm.required_role("work_issue"), "author") self.assertEqual(tcm.required_permission("create_pr"), "gitea.pr.create") self.assertEqual(tcm.required_role("create_pr"), "author") self.assertEqual(tcm.required_permission("commit_files"), "gitea.repo.commit") self.assertEqual(tcm.required_permission("push_branch"), "gitea.branch.push") self.assertEqual( tcm.required_permission("snapshot_instance_fleet"), "gitea.read" ) def test_no_new_gitea_write_permission_is_introduced(self): import task_capability_map as tcm for task in ("plan_stale_worker_retirement", "apply_stale_worker_retirement"): with self.subTest(task=task): self.assertNotIn( tcm.required_permission(task), { "gitea.pr.approve", "gitea.pr.merge", "gitea.pr.review", "gitea.branch.push", "gitea.repo.commit", }, ) class SurfaceRegistrationTests(unittest.TestCase): """AC: the dry-run and apply modes are exposed as sanctioned native tools.""" def test_tools_are_registered_and_role_restricted(self): import inspect import gitea_mcp_server as server for name in ( "gitea_plan_stale_worker_retirement", "gitea_apply_stale_worker_retirement", ): with self.subTest(tool=name): tool = getattr(server, name) self.assertTrue(callable(tool)) source = inspect.getsource(tool) # Both tools route their role check through the shared guard. self.assertIn(f'_retirement_role_block("{name}")', source) guard = inspect.getsource(server._retirement_role_block) self.assertIn('{"controller", "reconciler"}', guard) self.assertIn("required_roles", guard) apply_source = inspect.getsource(server.gitea_apply_stale_worker_retirement) self.assertIn("registry_fingerprint", apply_source) self.assertIn("candidate_fingerprint", apply_source) self.assertIn("_retirement_runtime_block", apply_source) self.assertIn("classify_cohort", apply_source) # Revalidation inside the transaction re-reads lease protection rather # than reusing the pre-transaction snapshot. self.assertIn("_retirement_revalidation_plan", apply_source) revalidation = inspect.getsource(server._retirement_revalidation_plan) self.assertIn("_retirement_protected_owners", revalidation) self.assertIn("raise RuntimeError", revalidation) def test_plan_tool_declares_itself_read_only(self): import inspect import gitea_mcp_server as server source = inspect.getsource(server.gitea_plan_stale_worker_retirement) self.assertIn("read_only", source) self.assertNotIn("retire_stale_workers", source) def test_docs_exist(self): root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) path = os.path.join(root, "docs", "stale-worker-retirement.md") self.assertTrue(os.path.isfile(path)) with open(path, encoding="utf-8") as handle: text = handle.read() for token in ( "registry_fingerprint", "candidate_fingerprint", "gitea_plan_stale_worker_retirement", "gitea_apply_stale_worker_retirement", "eligible_stale_orphan", "registry_revision_moved", "does not repair untrusted live identity", ): with self.subTest(token=token): self.assertIn(token, text) if __name__ == "__main__": # pragma: no cover unittest.main()