From 7b9fdaa7342e9a00a024a5d28a6517d9a2c39ee3 Mon Sep 17 00:00:00 2001 From: tegwick Date: Sun, 23 Aug 2026 11:58:43 +0200 Subject: [PATCH] fix(registrar): bound slow consistency runs Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a02b22-9638-76d2-bbff-b7ea1770b118 --- docs/repository-standards_v0.1.md | 4 +++ .../commands/registrar_reconcile.py | 24 ++++++++++++-- tests/test_registrar_reconcile.py | 33 +++++++++++++++++++ ...gistrar-consolidation-deterministic-ids.md | 6 ++++ 4 files changed, 65 insertions(+), 2 deletions(-) diff --git a/docs/repository-standards_v0.1.md b/docs/repository-standards_v0.1.md index 259bbd5..d3721df 100644 --- a/docs/repository-standards_v0.1.md +++ b/docs/repository-standards_v0.1.md @@ -86,6 +86,10 @@ the consistency report, but do not block assignment of unrelated canonical records. Empty-projection bootstrap remains a full-repository operation and therefore still requires the complete identity set to pass. +The child consistency pass has a 15-minute ceiling. Exceeding it returns a +structured `statehub_timeout` result with output tails and the post-timeout +missing-ID scan; it must not terminate the caller with an uncaught traceback. + Work: `RMGR-WP-0005-T01`. ## Coding-assistant commit provenance diff --git a/src/repo_manager/commands/registrar_reconcile.py b/src/repo_manager/commands/registrar_reconcile.py index 5c8dda5..6703270 100644 --- a/src/repo_manager/commands/registrar_reconcile.py +++ b/src/repo_manager/commands/registrar_reconcile.py @@ -25,6 +25,7 @@ from repo_manager.parse.workplan import parse_workplan_file from repo_manager.record_identity import scan_record_identities LOCK_PATH = Path("/tmp/repo-manager-identifier-registrar.lock") +STATEHUB_TIMEOUT_SECONDS = 900 @dataclass @@ -382,7 +383,7 @@ def _run_statehub(command: list[str], *, env: dict[str, str]) -> subprocess.Comp capture_output=True, text=True, check=False, - timeout=300, + timeout=STATEHUB_TIMEOUT_SECONDS, env=env, ) @@ -573,7 +574,26 @@ def registrar_reconcile( ] if bootstrap_empty_projection: command.append("--bootstrap-empty-projection") - completed = _run_statehub(command, env=child_env) + try: + completed = _run_statehub(command, env=child_env) + except subprocess.TimeoutExpired as exc: + stdout = exc.stdout.decode() if isinstance(exc.stdout, bytes) else (exc.stdout or "") + stderr = exc.stderr.decode() if isinstance(exc.stderr, bytes) else (exc.stderr or "") + evidence["statehub_stdout_tail"] = stdout[-4000:] + evidence["statehub_stderr_tail"] = stderr[-2000:] + evidence["missing_after"] = _missing_identifiers(repo) + return RegistrarResult( + "failed", + evidence, + { + "code": "statehub_timeout", + "message": ( + "State Hub reconciliation exceeded " + f"{STATEHUB_TIMEOUT_SECONDS} seconds" + ), + }, + cid, + ) evidence["statehub_exit_code"] = completed.returncode evidence["statehub_stdout_tail"] = completed.stdout[-4000:] diff --git a/tests/test_registrar_reconcile.py b/tests/test_registrar_reconcile.py index a5fd278..56c6ffd 100644 --- a/tests/test_registrar_reconcile.py +++ b/tests/test_registrar_reconcile.py @@ -8,6 +8,12 @@ import pytest from repo_manager.commands import registrar_reconcile as rr +@pytest.fixture(autouse=True) +def isolate_registrar_lock(tmp_path: Path, monkeypatch) -> None: + """Keep unit tests independent from a live workstation registrar run.""" + monkeypatch.setattr(rr, "LOCK_PATH", tmp_path / "registrar.lock") + + def _git(repo: Path, *args: str) -> None: subprocess.run(["git", *args], cwd=repo, check=True, capture_output=True) @@ -324,6 +330,33 @@ def test_accepts_unrelated_assessment_fail_after_exact_requested_verification( assert result.evidence["requested_projection"]["expected_tasks"] == 1 +def test_statehub_timeout_returns_structured_failure(tmp_path: Path, monkeypatch) -> None: + repo = _fixture(tmp_path) + monkeypatch.setattr( + rr, + "_check_primary", + lambda _api: ({"status": "ok", "db": "connected"}, None), + ) + monkeypatch.setattr( + rr, + "_run_statehub", + lambda command, *, env: (_ for _ in ()).throw( + subprocess.TimeoutExpired(command, rr.STATEHUB_TIMEOUT_SECONDS, output="partial") + ), + ) + + result = rr.registrar_reconcile( + repo, + statehub_bin="statehub", + confirm_primary=True, + ) + + assert result.status == "failed" + assert result.error and result.error["code"] == "statehub_timeout" + assert result.evidence["statehub_stdout_tail"] == "partial" + assert result.evidence["missing_after"] == result.evidence["missing_before"] + + def test_repairs_an_already_identified_workplan_projection(tmp_path: Path, monkeypatch) -> None: repo = _fixture(tmp_path) workplan = repo / "workplans" / "DEMO-WP-0001.md" diff --git a/workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md b/workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md index 83ac1ae..ac78a9b 100644 --- a/workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md +++ b/workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md @@ -316,6 +316,12 @@ A requested defect still fails closed; unrelated history stays in evidence; empty-projection bootstrap retains the full-repository gate. Regression coverage proves all three cases. +The first Custodian proof passed the repaired identity gate but exceeded the +old five-minute child timeout without writing an identifier. The wrapper now +allows 15 minutes for large legacy repositories, returns structured timeout +evidence instead of a traceback, and isolates its unit-test lock from live +registrar activity. + ## Derive identifiers deterministically ```task