Ignore generated WORK-RECORDS.md/.custodian-brief.md in the registrar git precondition, commit identifier writebacks even when registration is incomplete, and name the remaining records in the error. Add config/state-hub-access.yaml as the single source for the AGENTS.md port map, rendered by rmgr scaffold and refreshed with --refresh-hub-access. Assistant: grok Assistant-Session: 01a04996-76e8-7f53-b971-1885cfbed436
810 lines
27 KiB
Python
810 lines
27 KiB
Python
from __future__ import annotations
|
|
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
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)
|
|
|
|
|
|
def _fixture(tmp_path: Path) -> Path:
|
|
bare = tmp_path / "remote.git"
|
|
repo = tmp_path / "demo"
|
|
_git(tmp_path, "init", "--bare", str(bare))
|
|
_git(tmp_path, "clone", str(bare), str(repo))
|
|
_git(repo, "config", "user.name", "Test")
|
|
_git(repo, "config", "user.email", "test@example.com")
|
|
(repo / "workplans").mkdir()
|
|
(repo / "workplans" / "DEMO-WP-0001.md").write_text(
|
|
"""---
|
|
id: DEMO-WP-0001
|
|
type: workplan
|
|
title: Demo
|
|
status: active
|
|
---
|
|
|
|
## Task
|
|
|
|
```task
|
|
id: DEMO-WP-0001-T01
|
|
status: todo
|
|
priority: high
|
|
```
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
_git(repo, "add", ".")
|
|
_git(repo, "commit", "-m", "seed")
|
|
_git(repo, "push", "-u", "origin", "HEAD")
|
|
return repo
|
|
|
|
|
|
def test_requires_explicit_primary_confirmation(tmp_path: Path) -> None:
|
|
repo = _fixture(tmp_path)
|
|
result = rr.registrar_reconcile(repo)
|
|
assert result.status == "rejected"
|
|
assert result.error and result.error["code"] == "confirmation_required"
|
|
|
|
|
|
def test_unrelated_historical_collision_does_not_block_scoped_request(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
repo = _fixture(tmp_path)
|
|
for number, task_uuid in (
|
|
(2, "22222222-2222-4222-8222-222222222222"),
|
|
(3, "33333333-3333-4333-8333-333333333333"),
|
|
):
|
|
(repo / "workplans" / f"DEMO-WP-000{number}.md").write_text(
|
|
f"""---
|
|
id: DEMO-WP-000{number}
|
|
type: workplan
|
|
title: Historical {number}
|
|
status: finished
|
|
state_hub_workstream_id: "{number}{'1' * 7}-1111-4111-8111-111111111111"
|
|
---
|
|
|
|
## Historical task
|
|
|
|
```task
|
|
id: DEMO-WP-9999-T01
|
|
status: done
|
|
priority: low
|
|
state_hub_task_id: "{task_uuid}"
|
|
```
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
result = rr.registrar_reconcile(repo)
|
|
|
|
assert result.status == "rejected"
|
|
assert result.error and result.error["code"] == "confirmation_required"
|
|
assert result.evidence["record_identity"]["identity_collisions"]
|
|
assert result.evidence["blocking_identity_collisions"] == []
|
|
|
|
|
|
def test_requested_collision_still_fails_closed(tmp_path: Path) -> None:
|
|
repo = _fixture(tmp_path)
|
|
second = repo / "workplans" / "DEMO-WP-0002.md"
|
|
second.write_text(
|
|
"""---
|
|
id: DEMO-WP-0002
|
|
type: workplan
|
|
title: Conflicting request
|
|
status: active
|
|
---
|
|
|
|
## Conflicting task
|
|
|
|
```task
|
|
id: DEMO-WP-0001-T01
|
|
status: todo
|
|
priority: high
|
|
```
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
result = rr.registrar_reconcile(repo)
|
|
|
|
assert result.status == "rejected"
|
|
assert result.error and result.error["code"] == "record_identity_collision"
|
|
assert result.evidence["blocking_identity_collisions"][0]["id"] == "DEMO-WP-0001-T01"
|
|
|
|
|
|
def test_requested_invalid_identifier_fails_before_registration(tmp_path: Path) -> None:
|
|
repo = _fixture(tmp_path)
|
|
workplan = repo / "workplans" / "DEMO-WP-0001.md"
|
|
workplan.write_text(
|
|
workplan.read_text(encoding="utf-8").replace("DEMO-WP-0001", "DEMO-INVALID"),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
result = rr.registrar_reconcile(repo)
|
|
|
|
assert result.status == "rejected"
|
|
assert result.error and result.error["code"] == "record_identifier_invalid"
|
|
assert result.evidence["blocking_invalid_identifiers"][0]["id"] == "DEMO-INVALID"
|
|
|
|
|
|
def test_unlinked_closed_workplan_is_registrar_work(tmp_path: Path) -> None:
|
|
repo = _fixture(tmp_path)
|
|
workplan = repo / "workplans" / "DEMO-WP-0001.md"
|
|
workplan.write_text(
|
|
workplan.read_text(encoding="utf-8").replace("status: active", "status: finished"),
|
|
encoding="utf-8",
|
|
)
|
|
assert rr._missing_identifiers(repo) == {
|
|
"workplans": ["DEMO-WP-0001"],
|
|
"tasks": ["DEMO-WP-0001-T01"],
|
|
"intakes": [],
|
|
"decisions": [],
|
|
}
|
|
|
|
|
|
def test_linked_closed_workplan_does_not_reopen_historical_task_gaps(tmp_path: Path) -> None:
|
|
repo = _fixture(tmp_path)
|
|
workplan = repo / "workplans" / "DEMO-WP-0001.md"
|
|
text = workplan.read_text(encoding="utf-8")
|
|
text = text.replace(
|
|
"status: active\n---",
|
|
'status: finished\nstate_hub_workstream_id: "11111111-1111-4111-8111-111111111111"\n---',
|
|
)
|
|
workplan.write_text(text, encoding="utf-8")
|
|
assert rr._missing_identifiers(repo) == {
|
|
"workplans": [],
|
|
"tasks": [],
|
|
"intakes": [],
|
|
"decisions": [],
|
|
}
|
|
|
|
|
|
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", "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"
|
|
assert dirty.error and dirty.error["code"] == "git_precondition_failed"
|
|
assert "note.txt" in (dirty.error.get("message") or "")
|
|
|
|
|
|
def test_generated_index_alone_does_not_fail_git_precondition(tmp_path: Path, monkeypatch) -> None:
|
|
repo = _fixture(tmp_path)
|
|
(repo / "WORK-RECORDS.md").write_text("# generated\n", encoding="utf-8")
|
|
monkeypatch.setattr(
|
|
rr,
|
|
"_check_primary",
|
|
lambda _api, **_: ({"status": "ok", "db": "connected", "instance_role": "primary"}, None),
|
|
)
|
|
|
|
def fake_run(command, *, env):
|
|
workplan = repo / "workplans" / "DEMO-WP-0001.md"
|
|
text = workplan.read_text(encoding="utf-8")
|
|
text = text.replace(
|
|
"status: active\n---",
|
|
'status: active\nstate_hub_workstream_id: "11111111-1111-4111-8111-111111111111"\n---',
|
|
)
|
|
text = text.replace(
|
|
"priority: high\n```",
|
|
'priority: high\nstate_hub_task_id: "22222222-2222-4222-8222-222222222222"\n```',
|
|
)
|
|
workplan.write_text(text, encoding="utf-8")
|
|
return subprocess.CompletedProcess(command, 0, "ok", "")
|
|
|
|
monkeypatch.setattr(rr, "_run_statehub", fake_run)
|
|
result = rr.registrar_reconcile(repo, statehub_bin="statehub", confirm_primary=True)
|
|
assert result.status == "applied"
|
|
assert "WORK-RECORDS.md" in result.evidence.get("committed_paths", [])
|
|
|
|
|
|
def test_incomplete_run_commits_writebacks_and_names_the_record(
|
|
tmp_path: Path, monkeypatch
|
|
) -> None:
|
|
repo = _fixture(tmp_path)
|
|
monkeypatch.setattr(
|
|
rr,
|
|
"_check_primary",
|
|
lambda _api, **_: ({"status": "ok", "db": "connected", "instance_role": "primary"}, None),
|
|
)
|
|
|
|
def fake_run(command, *, env):
|
|
workplan = repo / "workplans" / "DEMO-WP-0001.md"
|
|
text = workplan.read_text(encoding="utf-8")
|
|
text = text.replace(
|
|
"status: active\n---",
|
|
'status: active\nstate_hub_workstream_id: "11111111-1111-4111-8111-111111111111"\n---',
|
|
)
|
|
workplan.write_text(text, encoding="utf-8")
|
|
(repo / "WORK-RECORDS.md").write_text("# generated after mint\n", encoding="utf-8")
|
|
return subprocess.CompletedProcess(
|
|
command,
|
|
1,
|
|
"! task DEMO-WP-0001-T01 not created: 500 Internal Server Error: Internal Server Error",
|
|
"",
|
|
)
|
|
|
|
monkeypatch.setattr(rr, "_run_statehub", fake_run)
|
|
result = rr.registrar_reconcile(repo, statehub_bin="statehub", confirm_primary=True)
|
|
assert result.status == "failed"
|
|
assert result.error and result.error["code"] == "registration_incomplete"
|
|
assert "DEMO-WP-0001-T01" in (result.error.get("message") or "")
|
|
assert result.error.get("records") == ["DEMO-WP-0001-T01"]
|
|
assert "not created:" in (result.error.get("message") or "")
|
|
subject = subprocess.run(
|
|
["git", "log", "-1", "--format=%s"], cwd=repo, capture_output=True, text=True, check=True
|
|
).stdout.strip()
|
|
assert subject == "chore(registrar): assign State Hub identifiers"
|
|
workplan = (repo / "workplans" / "DEMO-WP-0001.md").read_text(encoding="utf-8")
|
|
assert "11111111-1111-4111-8111-111111111111" in workplan
|
|
|
|
|
|
def test_bootstrap_source_invalid_names_the_record(tmp_path: Path, monkeypatch) -> None:
|
|
repo = _fixture(tmp_path)
|
|
monkeypatch.setattr(
|
|
rr,
|
|
"_check_primary",
|
|
lambda _api, **_: ({"status": "ok", "db": "connected", "instance_role": "primary"}, None),
|
|
)
|
|
monkeypatch.setattr(
|
|
rr,
|
|
"_check_empty_repo_projection",
|
|
lambda _api, _slug: ({"repo_id": "repo-1", "workplan_count": 0}, None),
|
|
)
|
|
result = rr.registrar_reconcile(
|
|
repo,
|
|
statehub_bin="statehub",
|
|
confirm_primary=True,
|
|
bootstrap_empty_projection=True,
|
|
)
|
|
assert result.status == "rejected"
|
|
assert result.error and result.error["code"] == "bootstrap_source_invalid"
|
|
assert "DEMO-WP-0001" in (result.error.get("message") or "")
|
|
assert result.error.get("record") == "DEMO-WP-0001"
|
|
|
|
|
|
def test_missing_scan_includes_intakes_and_decisions(tmp_path: Path) -> None:
|
|
repo = _fixture(tmp_path)
|
|
records = repo / "intakes" / "records.md"
|
|
records.parent.mkdir()
|
|
records.write_text(
|
|
"""# Records
|
|
|
|
```yaml
|
|
id: DEMO-IN-0001
|
|
kind: intake
|
|
title: Intake
|
|
status: open
|
|
```
|
|
|
|
```yaml
|
|
id: DEMO-DEC-0001
|
|
kind: decision
|
|
title: Decision
|
|
status: open
|
|
```
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
missing = rr._missing_identifiers(repo)
|
|
assert missing["intakes"] == ["DEMO-IN-0001"]
|
|
assert missing["decisions"] == ["DEMO-DEC-0001"]
|
|
|
|
|
|
def test_missing_scan_includes_lowercase_top_level_record_files(tmp_path: Path) -> None:
|
|
repo = _fixture(tmp_path)
|
|
(repo / "intakes.md").write_text(
|
|
"""# Intakes
|
|
|
|
```yaml
|
|
id: DEMO-IN-0002
|
|
kind: intake
|
|
title: Standalone intake
|
|
status: open
|
|
```
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
(repo / "decisions.md").write_text(
|
|
"""# Decisions
|
|
|
|
```yaml
|
|
id: DEMO-DEC-0002
|
|
kind: decision
|
|
title: Standalone decision
|
|
status: accepted
|
|
```
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
missing = rr._missing_identifiers(repo)
|
|
|
|
assert missing["intakes"] == ["DEMO-IN-0002"]
|
|
assert missing["decisions"] == ["DEMO-DEC-0002"]
|
|
|
|
|
|
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", "instance_role": "primary"}, None))
|
|
|
|
def fake_run(command, *, env):
|
|
assert command[1:3] == ["fix-consistency", "--path"]
|
|
assert env["STATEHUB_REGISTRAR"] == "1"
|
|
workplan = repo / "workplans" / "DEMO-WP-0001.md"
|
|
text = workplan.read_text(encoding="utf-8")
|
|
text = text.replace("status: active\n---", 'status: active\nstate_hub_workstream_id: "11111111-1111-4111-8111-111111111111"\n---')
|
|
text = text.replace("priority: high\n```", 'priority: high\nstate_hub_task_id: "22222222-2222-4222-8222-222222222222"\n```')
|
|
workplan.write_text(text, encoding="utf-8")
|
|
return subprocess.CompletedProcess(command, 0, "ok", "")
|
|
|
|
monkeypatch.setattr(rr, "_run_statehub", fake_run)
|
|
|
|
result = rr.registrar_reconcile(
|
|
repo,
|
|
statehub_bin="statehub",
|
|
confirm_primary=True,
|
|
)
|
|
|
|
assert result.status == "applied"
|
|
assert result.evidence["missing_after"] == {
|
|
"workplans": [],
|
|
"tasks": [],
|
|
"intakes": [],
|
|
"decisions": [],
|
|
}
|
|
subject = subprocess.run(
|
|
["git", "log", "-1", "--format=%s"], cwd=repo, capture_output=True, text=True, check=True
|
|
).stdout.strip()
|
|
assert subject == "chore(registrar): assign State Hub identifiers"
|
|
|
|
|
|
def test_accepts_unrelated_assessment_fail_after_exact_requested_verification(
|
|
tmp_path: Path, monkeypatch
|
|
) -> None:
|
|
repo = _fixture(tmp_path)
|
|
monkeypatch.setattr(
|
|
rr,
|
|
"_check_primary",
|
|
lambda _api, **_: ({"status": "ok", "db": "connected", "instance_role": "primary"}, None),
|
|
)
|
|
|
|
def fake_run(command, *, env):
|
|
workplan = repo / "workplans" / "DEMO-WP-0001.md"
|
|
text = workplan.read_text(encoding="utf-8")
|
|
text = text.replace(
|
|
"status: active\n---",
|
|
'status: active\nstate_hub_workstream_id: "11111111-1111-4111-8111-111111111111"\n---',
|
|
)
|
|
text = text.replace(
|
|
"priority: high\n```",
|
|
'priority: high\nstate_hub_task_id: "22222222-2222-4222-8222-222222222222"\n```',
|
|
)
|
|
workplan.write_text(text, encoding="utf-8")
|
|
return subprocess.CompletedProcess(command, 1, "unrelated C-03 remains", "")
|
|
|
|
monkeypatch.setattr(rr, "_run_statehub", fake_run)
|
|
monkeypatch.setattr(
|
|
rr,
|
|
"_verify_full_projection",
|
|
lambda _api, workplans, tasks, records: (
|
|
{
|
|
"expected_workplans": len(workplans),
|
|
"expected_tasks": len(tasks),
|
|
"expected_intakes": len(records["intake"]),
|
|
"expected_decisions": len(records["decision"]),
|
|
"missing_workplans": [],
|
|
"missing_tasks": [],
|
|
"missing_intakes": [],
|
|
"missing_decisions": [],
|
|
},
|
|
None,
|
|
),
|
|
)
|
|
|
|
result = rr.registrar_reconcile(
|
|
repo,
|
|
statehub_bin="statehub",
|
|
confirm_primary=True,
|
|
)
|
|
|
|
assert result.status == "applied"
|
|
assert result.evidence["requested_projection_verified"] is True
|
|
assert result.evidence["requested_projection"]["expected_workplans"] == 1
|
|
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", "instance_role": "primary"}, 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"
|
|
text = workplan.read_text(encoding="utf-8")
|
|
text = text.replace(
|
|
"status: active\n---",
|
|
'status: active\nstate_hub_workstream_id: "11111111-1111-4111-8111-111111111111"\n---',
|
|
)
|
|
text = text.replace(
|
|
"priority: high\n```",
|
|
'priority: high\nstate_hub_task_id: "22222222-2222-4222-8222-222222222222"\n```',
|
|
)
|
|
workplan.write_text(text, encoding="utf-8")
|
|
brief = repo / ".custodian-brief.md"
|
|
brief.write_text("authoritative local brief\n", encoding="utf-8")
|
|
_git(repo, "add", ".")
|
|
_git(repo, "commit", "-m", "add authoritative identifiers")
|
|
_git(repo, "push")
|
|
|
|
monkeypatch.setattr(
|
|
rr,
|
|
"_check_primary",
|
|
lambda _api, **_: ({"status": "ok", "db": "connected", "instance_role": "primary"}, None),
|
|
)
|
|
monkeypatch.setattr(
|
|
rr,
|
|
"_projection_exists",
|
|
lambda _api, projection_id: ({"id": projection_id}, None),
|
|
)
|
|
|
|
def fake_run(command, *, env):
|
|
assert env["STATEHUB_REGISTRAR"] == "1"
|
|
brief.write_text("generated from partial projection\n", encoding="utf-8")
|
|
return subprocess.CompletedProcess(command, 1, "legacy stale references remain", "")
|
|
|
|
monkeypatch.setattr(rr, "_run_statehub", fake_run)
|
|
|
|
result = rr.registrar_reconcile(
|
|
repo,
|
|
statehub_bin="statehub",
|
|
confirm_primary=True,
|
|
repair_workplan="DEMO-WP-0001",
|
|
)
|
|
|
|
assert result.status == "applied"
|
|
assert result.evidence["repair_projection_verified"] is True
|
|
assert result.evidence["repair_projection_id"] == "11111111-1111-4111-8111-111111111111"
|
|
assert result.evidence["restored_generated_paths"] == [".custodian-brief.md"]
|
|
assert brief.read_text(encoding="utf-8") == "authoritative local brief\n"
|
|
|
|
|
|
def test_repair_fails_when_exact_projection_remains_absent(tmp_path: Path, monkeypatch) -> None:
|
|
repo = _fixture(tmp_path)
|
|
workplan = repo / "workplans" / "DEMO-WP-0001.md"
|
|
text = workplan.read_text(encoding="utf-8")
|
|
text = text.replace(
|
|
"status: active\n---",
|
|
'status: active\nstate_hub_workstream_id: "11111111-1111-4111-8111-111111111111"\n---',
|
|
)
|
|
text = text.replace(
|
|
"priority: high\n```",
|
|
'priority: high\nstate_hub_task_id: "22222222-2222-4222-8222-222222222222"\n```',
|
|
)
|
|
workplan.write_text(text, encoding="utf-8")
|
|
_git(repo, "add", ".")
|
|
_git(repo, "commit", "-m", "add authoritative identifiers")
|
|
_git(repo, "push")
|
|
|
|
monkeypatch.setattr(
|
|
rr,
|
|
"_check_primary",
|
|
lambda _api, **_: ({"status": "ok", "db": "connected", "instance_role": "primary"}, None),
|
|
)
|
|
monkeypatch.setattr(
|
|
rr,
|
|
"_projection_exists",
|
|
lambda _api, _projection_id: ({}, "workplan projection returned 404"),
|
|
)
|
|
monkeypatch.setattr(
|
|
rr,
|
|
"_run_statehub",
|
|
lambda command, *, env: subprocess.CompletedProcess(
|
|
command, 1, "stale reference remains", ""
|
|
),
|
|
)
|
|
|
|
result = rr.registrar_reconcile(
|
|
repo,
|
|
statehub_bin="statehub",
|
|
confirm_primary=True,
|
|
repair_workplan="DEMO-WP-0001",
|
|
)
|
|
|
|
assert result.status == "failed"
|
|
assert result.error and result.error["code"] == "registration_incomplete"
|
|
assert result.evidence["repair_projection_verified"] is False
|
|
assert "requested_projection_verified" not in result.evidence
|
|
|
|
|
|
def test_bootstraps_and_verifies_a_completely_empty_projection(tmp_path: Path, monkeypatch) -> None:
|
|
repo = _fixture(tmp_path)
|
|
workplan = repo / "workplans" / "DEMO-WP-0001.md"
|
|
text = workplan.read_text(encoding="utf-8")
|
|
text = text.replace(
|
|
"status: active\n---",
|
|
'status: active\nstate_hub_workstream_id: "11111111-1111-4111-8111-111111111111"\n---',
|
|
)
|
|
text = text.replace(
|
|
"priority: high\n```",
|
|
'priority: high\nstate_hub_task_id: "22222222-2222-4222-8222-222222222222"\n```',
|
|
)
|
|
workplan.write_text(text, encoding="utf-8")
|
|
_git(repo, "add", ".")
|
|
_git(repo, "commit", "-m", "add authoritative identifiers")
|
|
_git(repo, "push")
|
|
|
|
monkeypatch.setattr(
|
|
rr,
|
|
"_check_primary",
|
|
lambda _api, **_: ({"status": "ok", "db": "connected", "instance_role": "primary"}, None),
|
|
)
|
|
monkeypatch.setattr(
|
|
rr,
|
|
"_check_empty_repo_projection",
|
|
lambda _api, _slug: ({"repo_id": "repo-1", "workplan_count": 0}, None),
|
|
)
|
|
monkeypatch.setattr(
|
|
rr,
|
|
"_verify_full_projection",
|
|
lambda _api, workplans, tasks, records: (
|
|
{
|
|
"expected_workplans": len(workplans),
|
|
"expected_tasks": len(tasks),
|
|
"expected_intakes": len(records["intake"]),
|
|
"expected_decisions": len(records["decision"]),
|
|
"missing_workplans": [],
|
|
"missing_tasks": [],
|
|
"missing_intakes": [],
|
|
"missing_decisions": [],
|
|
},
|
|
None,
|
|
),
|
|
)
|
|
|
|
def fake_run(command, *, env):
|
|
assert command[-1] == "--bootstrap-empty-projection"
|
|
assert env["STATEHUB_REGISTRAR"] == "1"
|
|
return subprocess.CompletedProcess(command, 2, "bootstrap warnings", "")
|
|
|
|
monkeypatch.setattr(rr, "_run_statehub", fake_run)
|
|
|
|
result = rr.registrar_reconcile(
|
|
repo,
|
|
statehub_bin="statehub",
|
|
confirm_primary=True,
|
|
bootstrap_empty_projection=True,
|
|
)
|
|
|
|
assert result.status == "applied"
|
|
assert result.evidence["bootstrap_source"] == {
|
|
"workplans": 1,
|
|
"tasks": 1,
|
|
"intakes": 0,
|
|
"decisions": 0,
|
|
}
|
|
assert result.evidence["bootstrap_projection_verified"] is True
|
|
|
|
|
|
def test_empty_projection_bootstrap_refuses_existing_rows(tmp_path: Path, monkeypatch) -> None:
|
|
repo = _fixture(tmp_path)
|
|
monkeypatch.setattr(
|
|
rr,
|
|
"_check_primary",
|
|
lambda _api, **_: ({"status": "ok", "db": "connected", "instance_role": "primary"}, None),
|
|
)
|
|
monkeypatch.setattr(
|
|
rr,
|
|
"_check_empty_repo_projection",
|
|
lambda _api, _slug: (
|
|
{"repo_id": "repo-1", "workplan_count": 1},
|
|
"target repository projection is not empty",
|
|
),
|
|
)
|
|
monkeypatch.setattr(
|
|
rr,
|
|
"_run_statehub",
|
|
lambda *_args, **_kwargs: pytest.fail("non-empty bootstrap must not run State Hub"),
|
|
)
|
|
|
|
result = rr.registrar_reconcile(
|
|
repo,
|
|
confirm_primary=True,
|
|
bootstrap_empty_projection=True,
|
|
)
|
|
|
|
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
|
|
|
|
|
|
class TestWorkplanBindings:
|
|
"""Registration must record the backing file in one pass (CUST-WP-0068-T07).
|
|
|
|
`backing_filename` is what lets the read model tell a file-backed workplan
|
|
from a hub-only orphan. It used to be written only by a later
|
|
`fix-consistency` run, so freshly registered workplans stayed unbound and
|
|
35% of central's workplans recorded no backing file.
|
|
"""
|
|
|
|
@staticmethod
|
|
def _repo(tmp_path):
|
|
repo = tmp_path / "demo"
|
|
(repo / "workplans" / "archived").mkdir(parents=True)
|
|
(repo / "workplans" / "DEMO-WP-0001-a.md").write_text(
|
|
'---\nid: DEMO-WP-0001\ntype: workplan\nstatus: active\n'
|
|
'state_hub_workstream_id: "11111111-1111-5111-8111-111111111111"\n---\n\n# a\n',
|
|
encoding="utf-8",
|
|
)
|
|
(repo / "workplans" / "archived" / "DEMO-WP-0002-b.md").write_text(
|
|
'---\nid: DEMO-WP-0002\ntype: workplan\nstatus: finished\n'
|
|
'state_hub_workstream_id: "22222222-2222-5222-8222-222222222222"\n---\n\n# b\n',
|
|
encoding="utf-8",
|
|
)
|
|
(repo / "workplans" / "DEMO-WP-0003-unregistered.md").write_text(
|
|
"---\nid: DEMO-WP-0003\ntype: workplan\nstatus: proposed\n---\n\n# c\n",
|
|
encoding="utf-8",
|
|
)
|
|
return repo
|
|
|
|
def test_binds_every_identified_workplan(self, tmp_path, monkeypatch):
|
|
repo = self._repo(tmp_path)
|
|
sent = {}
|
|
|
|
class R:
|
|
def raise_for_status(self): return None
|
|
def json(self): return {"updated": 2, "received": 2}
|
|
|
|
def fake_put(url, json=None, timeout=None):
|
|
sent["url"] = url
|
|
sent["bindings"] = json["bindings"]
|
|
return R()
|
|
|
|
monkeypatch.setattr(rr.httpx, "put", fake_put)
|
|
result = rr._sync_workplan_bindings(repo, "http://hub", "demo")
|
|
|
|
assert result["ok"] and result["updated"] == 2
|
|
assert sent["url"].endswith("/workplans/index/bindings")
|
|
by_id = {b["workplan_id"][:8]: b for b in sent["bindings"]}
|
|
# The workplan with no identifier cannot be bound and must be skipped,
|
|
# not sent with a null id.
|
|
assert set(by_id) == {"11111111", "22222222"}
|
|
assert by_id["11111111"]["relative_path"] == "workplans/DEMO-WP-0001-a.md"
|
|
assert by_id["11111111"]["archived"] is False
|
|
assert by_id["22222222"]["archived"] is True
|
|
assert by_id["22222222"]["status"] == "finished"
|
|
|
|
def test_non_canonical_status_is_omitted_not_guessed(self, tmp_path, monkeypatch):
|
|
"""The schema does not normalise, so a bad status 422s the whole batch."""
|
|
repo = self._repo(tmp_path)
|
|
(repo / "workplans" / "DEMO-WP-0004-legacy.md").write_text(
|
|
'---\nid: DEMO-WP-0004\ntype: workplan\nstatus: done\n'
|
|
'state_hub_workstream_id: "44444444-4444-5444-8444-444444444444"\n---\n\n# d\n',
|
|
encoding="utf-8",
|
|
)
|
|
sent = {}
|
|
|
|
class R:
|
|
def raise_for_status(self): return None
|
|
def json(self): return {"updated": 3, "received": 3}
|
|
|
|
monkeypatch.setattr(
|
|
rr.httpx, "put",
|
|
lambda url, json=None, timeout=None: (sent.update(b=json["bindings"]), R())[1],
|
|
)
|
|
rr._sync_workplan_bindings(repo, "http://hub", "demo")
|
|
legacy = [b for b in sent["b"] if b["workplan_id"].startswith("44444444")][0]
|
|
assert legacy["status"] is None
|
|
|
|
def test_binding_failure_never_fails_registration(self, tmp_path, monkeypatch):
|
|
"""Identifiers are already minted and committed; a bind can be retried."""
|
|
repo = self._repo(tmp_path)
|
|
|
|
def boom(*a, **k):
|
|
raise rr.httpx.HTTPError("hub unreachable")
|
|
|
|
monkeypatch.setattr(rr.httpx, "put", boom)
|
|
result = rr._sync_workplan_bindings(repo, "http://hub", "demo")
|
|
assert result["ok"] is False and "unreachable" in result["error"]
|
|
|
|
def test_reports_records_the_hub_does_not_have(self, tmp_path, monkeypatch):
|
|
repo = self._repo(tmp_path)
|
|
|
|
class R:
|
|
def raise_for_status(self): return None
|
|
def json(self): return {"updated": 1, "received": 2}
|
|
|
|
monkeypatch.setattr(rr.httpx, "put", lambda *a, **k: R())
|
|
result = rr._sync_workplan_bindings(repo, "http://hub", "demo")
|
|
assert result["unbound"] == 1
|