fix(registrar): make --confirm-primary assert authority, not liveness

_check_primary accepted any instance reporting status=ok and db=connected.
A local cache and the central hub both satisfied that for seven weeks while
every registration went to the cache — a liveness check wearing an authority
check's name.

It now requires the hub to declare instance_role=primary. An instance that
declares nothing is refused with a message naming what to set; proceeding
anyway requires an explicit --allow-unverified-primary rather than a silent
default.

Four tests cover the logic directly; the existing suite stubbed _check_primary
and never exercised it.

Refs CUST-WP-0067-T03

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2583210@bnt-lap001
Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
This commit is contained in:
tegwick 2026-08-25 10:32:51 +02:00
parent 68943631d8
commit 10bdb683cb
3 changed files with 105 additions and 11 deletions

View file

@ -172,7 +172,7 @@ def test_linked_closed_workplan_does_not_reopen_historical_task_gaps(tmp_path: P
def test_rejects_dirty_or_unsynced_repository(tmp_path: Path, monkeypatch) -> None:
repo = _fixture(tmp_path)
monkeypatch.setattr(rr, "_check_primary", lambda _api: ({"status": "ok", "db": "connected"}, None))
monkeypatch.setattr(rr, "_check_primary", lambda _api, **_: ({"status": "ok", "db": "connected", "instance_role": "primary"}, None))
(repo / "note.txt").write_text("dirty", encoding="utf-8")
dirty = rr.registrar_reconcile(repo, confirm_primary=True)
assert dirty.status == "rejected"
@ -242,7 +242,7 @@ status: accepted
def test_scopes_registrar_env_and_commits_assigned_ids(tmp_path: Path, monkeypatch) -> None:
repo = _fixture(tmp_path)
monkeypatch.setattr(rr, "_check_primary", lambda _api: ({"status": "ok", "db": "connected"}, None))
monkeypatch.setattr(rr, "_check_primary", lambda _api, **_: ({"status": "ok", "db": "connected", "instance_role": "primary"}, None))
def fake_run(command, *, env):
assert command[1:3] == ["fix-consistency", "--path"]
@ -282,7 +282,7 @@ def test_accepts_unrelated_assessment_fail_after_exact_requested_verification(
monkeypatch.setattr(
rr,
"_check_primary",
lambda _api: ({"status": "ok", "db": "connected"}, None),
lambda _api, **_: ({"status": "ok", "db": "connected", "instance_role": "primary"}, None),
)
def fake_run(command, *, env):
@ -335,7 +335,7 @@ def test_statehub_timeout_returns_structured_failure(tmp_path: Path, monkeypatch
monkeypatch.setattr(
rr,
"_check_primary",
lambda _api: ({"status": "ok", "db": "connected"}, None),
lambda _api, **_: ({"status": "ok", "db": "connected", "instance_role": "primary"}, None),
)
monkeypatch.setattr(
rr,
@ -379,7 +379,7 @@ def test_repairs_an_already_identified_workplan_projection(tmp_path: Path, monke
monkeypatch.setattr(
rr,
"_check_primary",
lambda _api: ({"status": "ok", "db": "connected"}, None),
lambda _api, **_: ({"status": "ok", "db": "connected", "instance_role": "primary"}, None),
)
monkeypatch.setattr(
rr,
@ -428,7 +428,7 @@ def test_repair_fails_when_exact_projection_remains_absent(tmp_path: Path, monke
monkeypatch.setattr(
rr,
"_check_primary",
lambda _api: ({"status": "ok", "db": "connected"}, None),
lambda _api, **_: ({"status": "ok", "db": "connected", "instance_role": "primary"}, None),
)
monkeypatch.setattr(
rr,
@ -476,7 +476,7 @@ def test_bootstraps_and_verifies_a_completely_empty_projection(tmp_path: Path, m
monkeypatch.setattr(
rr,
"_check_primary",
lambda _api: ({"status": "ok", "db": "connected"}, None),
lambda _api, **_: ({"status": "ok", "db": "connected", "instance_role": "primary"}, None),
)
monkeypatch.setattr(
rr,
@ -530,7 +530,7 @@ def test_empty_projection_bootstrap_refuses_existing_rows(tmp_path: Path, monkey
monkeypatch.setattr(
rr,
"_check_primary",
lambda _api: ({"status": "ok", "db": "connected"}, None),
lambda _api, **_: ({"status": "ok", "db": "connected", "instance_role": "primary"}, None),
)
monkeypatch.setattr(
rr,
@ -554,3 +554,61 @@ def test_empty_projection_bootstrap_refuses_existing_rows(tmp_path: Path, monkey
assert result.status == "rejected"
assert result.error and result.error["code"] == "bootstrap_precondition_failed"
class TestCheckPrimary:
"""_check_primary must assert authority, not just liveness (CUST-WP-0067-T03).
The previous version accepted any instance reporting status=ok and
db=connected. A local cache and the central hub both satisfied that for
seven weeks while every registration went to the cache.
"""
@staticmethod
def _resp(payload):
class R:
def raise_for_status(self):
return None
def json(self):
return payload
return R()
def test_accepts_declared_primary(self, monkeypatch):
monkeypatch.setattr(
rr.httpx, "get",
lambda *a, **k: self._resp(
{"status": "ok", "db": "connected", "instance_role": "primary"}
),
)
_, err = rr._check_primary("http://hub")
assert err is None
def test_rejects_declared_cache(self, monkeypatch):
monkeypatch.setattr(
rr.httpx, "get",
lambda *a, **k: self._resp(
{"status": "ok", "db": "connected", "instance_role": "cache",
"instance_label": "workstation"}
),
)
_, err = rr._check_primary("http://hub")
assert err and "cache" in err
def test_rejects_healthy_instance_that_declares_nothing(self, monkeypatch):
"""A healthy hub is not thereby the authoritative one."""
monkeypatch.setattr(
rr.httpx, "get",
lambda *a, **k: self._resp({"status": "ok", "db": "connected"}),
)
_, err = rr._check_primary("http://hub")
assert err and "does not declare an instance role" in err
def test_unverified_requires_an_explicit_opt_in(self, monkeypatch):
monkeypatch.setattr(
rr.httpx, "get",
lambda *a, **k: self._resp({"status": "ok", "db": "connected"}),
)
_, err = rr._check_primary("http://hub", allow_unverified=True)
assert err is None