feat(RMGR-WP-0008): add workplan and register receiving surfaces
This commit is contained in:
parent
5502afc1fd
commit
859df9aae7
15 changed files with 1501 additions and 11 deletions
|
|
@ -55,11 +55,11 @@ def _clear_dual_run_cache():
|
|||
dual_run.reload_config()
|
||||
|
||||
|
||||
def test_flags_and_pilot(monkeypatch: pytest.MonkeyPatch):
|
||||
def test_flags_and_pilot(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.delenv("RM_WRITEBACK", raising=False)
|
||||
monkeypatch.delenv("RM_RECONCILE", raising=False)
|
||||
monkeypatch.delenv("RM_PILOT_REPOS", raising=False)
|
||||
monkeypatch.delenv("RM_DUAL_RUN_CONFIG", raising=False)
|
||||
monkeypatch.setenv("RM_DUAL_RUN_CONFIG", str(tmp_path / "missing-dual-run.yaml"))
|
||||
dual_run.reload_config()
|
||||
assert dual_run.writeback_enabled() is False
|
||||
|
||||
|
|
|
|||
144
tests/test_register_command.py
Normal file
144
tests/test_register_command.py
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
"""Shared repository register spine coverage for RMGR-WP-0008-T05."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
from repo_manager.commands.register import mutate_register_entry
|
||||
from repo_manager.observe import observe_repository
|
||||
from repo_manager.parse.register import SUPPORTED_REGISTER_KINDS
|
||||
|
||||
|
||||
def _git(repo: Path, *args: str) -> None:
|
||||
subprocess.run(["git", *args], cwd=repo, check=True, capture_output=True)
|
||||
|
||||
|
||||
def _fixture(tmp_path: Path) -> Path:
|
||||
repo = tmp_path / "pilot"
|
||||
repo.mkdir()
|
||||
_git(repo, "init")
|
||||
_git(repo, "config", "user.email", "test@example.com")
|
||||
_git(repo, "config", "user.name", "Test")
|
||||
(repo / ".repo-classification.yaml").write_text(
|
||||
"repo_classification:\n category: tooling\n domain: infotech\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
_git(repo, "add", ".")
|
||||
_git(repo, "commit", "-m", "seed")
|
||||
return repo
|
||||
|
||||
|
||||
def test_all_register_kinds_are_file_backed_and_indexed(tmp_path: Path) -> None:
|
||||
repo = _fixture(tmp_path)
|
||||
payloads = {
|
||||
"sbom-inventory": {"package_name": "pyyaml", "ecosystem": "pypi"},
|
||||
"repo-goals": {"description": "Keep repository records authoritative."},
|
||||
"upstream-contributions": {"type": "patch"},
|
||||
"technical-debt": {"severity": "medium"},
|
||||
"extension-points": {"ep_type": "api"},
|
||||
"register-entries": {"register_kind": "custom"},
|
||||
}
|
||||
|
||||
for sequence, kind in enumerate(sorted(SUPPORTED_REGISTER_KINDS), start=1):
|
||||
result = mutate_register_entry(
|
||||
repo,
|
||||
kind,
|
||||
f"ENTRY-{sequence:02d}",
|
||||
operation="upsert",
|
||||
title=f"{kind} entry",
|
||||
data=payloads[kind],
|
||||
)
|
||||
assert result.status == "applied", result
|
||||
assert result.evidence["git_sha"]
|
||||
document = yaml.safe_load((repo / "registers" / f"{kind}.yaml").read_text())
|
||||
assert document["schema"] == "repo-manager.register.v0"
|
||||
assert document["kind"] == kind
|
||||
|
||||
snapshot, index = observe_repository(repo, slug="pilot")
|
||||
records = [record for record in index.work_records if record.kind.startswith("register:")]
|
||||
assert snapshot["index"]["register_entry_count"] == 6
|
||||
assert len(records) == 6
|
||||
|
||||
|
||||
def test_update_note_defer_and_idempotency(tmp_path: Path, monkeypatch) -> None:
|
||||
repo = _fixture(tmp_path)
|
||||
monkeypatch.setenv("RM_IDEMPOTENCY_PATH", str(tmp_path / "idempotency.json"))
|
||||
created = mutate_register_entry(
|
||||
repo,
|
||||
"technical-debt",
|
||||
"TD-001",
|
||||
operation="upsert",
|
||||
title="Coupled persistence",
|
||||
data={"severity": "high"},
|
||||
idempotency_key="td-create",
|
||||
)
|
||||
replay = mutate_register_entry(
|
||||
repo,
|
||||
"technical-debt",
|
||||
"TD-001",
|
||||
operation="upsert",
|
||||
title="Coupled persistence",
|
||||
data={"severity": "high"},
|
||||
idempotency_key="td-create",
|
||||
)
|
||||
assert replay.evidence["git_sha"] == created.evidence["git_sha"]
|
||||
|
||||
updated = mutate_register_entry(
|
||||
repo,
|
||||
"technical-debt",
|
||||
"TD-001",
|
||||
operation="upsert",
|
||||
status="in_progress",
|
||||
data={"severity": "critical"},
|
||||
)
|
||||
assert updated.status == "applied"
|
||||
noted = mutate_register_entry(
|
||||
repo,
|
||||
"technical-debt",
|
||||
"TD-001",
|
||||
operation="note",
|
||||
note="Migration owner assigned.",
|
||||
note_author="operator",
|
||||
)
|
||||
assert noted.status == "applied"
|
||||
deferred = mutate_register_entry(
|
||||
repo,
|
||||
"technical-debt",
|
||||
"TD-001",
|
||||
operation="defer",
|
||||
)
|
||||
assert deferred.status == "applied"
|
||||
|
||||
document = yaml.safe_load((repo / "registers" / "technical-debt.yaml").read_text())
|
||||
entry = document["entries"][0]
|
||||
assert entry["status"] == "deferred"
|
||||
assert entry["severity"] == "critical"
|
||||
assert entry["notes"][0]["author"] == "operator"
|
||||
|
||||
|
||||
def test_schema_and_safety_validation(tmp_path: Path) -> None:
|
||||
repo = _fixture(tmp_path)
|
||||
missing = mutate_register_entry(
|
||||
repo,
|
||||
"sbom-inventory",
|
||||
"PKG-01",
|
||||
operation="upsert",
|
||||
title="package",
|
||||
data={"package_name": "demo"},
|
||||
)
|
||||
assert missing.status == "rejected"
|
||||
assert missing.error and "ecosystem" in missing.error["message"]
|
||||
|
||||
protected = mutate_register_entry(
|
||||
repo,
|
||||
"technical-debt",
|
||||
"TD-001",
|
||||
operation="upsert",
|
||||
title="Debt",
|
||||
data={"id": "replacement"},
|
||||
)
|
||||
assert protected.status == "rejected"
|
||||
assert protected.error and protected.error["code"] == "validation_error"
|
||||
168
tests/test_workplan_command.py
Normal file
168
tests/test_workplan_command.py
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
"""Governed workplan mutation coverage for RMGR-WP-0008-T01."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from repo_manager.commands.workplan import archive_workplan, create_workplan, update_workplan
|
||||
from repo_manager.gitops import head_sha
|
||||
from repo_manager.index_store import default_index_path, load_index
|
||||
from repo_manager.observe import observe_repository
|
||||
|
||||
|
||||
def _git(repo: Path, *args: str) -> None:
|
||||
subprocess.run(["git", *args], cwd=repo, check=True, capture_output=True)
|
||||
|
||||
|
||||
def _fixture(tmp_path: Path) -> Path:
|
||||
repo = tmp_path / "pilot"
|
||||
repo.mkdir()
|
||||
_git(repo, "init")
|
||||
_git(repo, "config", "user.email", "test@example.com")
|
||||
_git(repo, "config", "user.name", "Test")
|
||||
(repo / "workplans").mkdir()
|
||||
(repo / ".repo-classification.yaml").write_text(
|
||||
"repo_classification:\n category: tooling\n domain: infotech\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(repo / "workplans" / "P-WP-0001-first.md").write_text(
|
||||
"""---
|
||||
id: P-WP-0001
|
||||
type: workplan
|
||||
title: "First"
|
||||
domain: infotech
|
||||
repo: pilot
|
||||
status: proposed
|
||||
owner: codex
|
||||
topic_slug: infotech
|
||||
created: "2026-08-21"
|
||||
updated: "2026-08-21"
|
||||
state_hub_workstream_id: "11111111-1111-4111-8111-111111111111"
|
||||
---
|
||||
|
||||
# First
|
||||
|
||||
## Task
|
||||
|
||||
```task
|
||||
id: P-WP-0001-T01
|
||||
status: todo
|
||||
priority: high
|
||||
```
|
||||
|
||||
Do it.
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
_git(repo, "add", ".")
|
||||
_git(repo, "commit", "-m", "seed")
|
||||
return repo
|
||||
|
||||
|
||||
def test_create_workplan_commits_indexes_and_meters(tmp_path: Path, monkeypatch) -> None:
|
||||
repo = _fixture(tmp_path)
|
||||
meter = tmp_path / "meter.jsonl"
|
||||
monkeypatch.setenv("RM_METER_PATH", str(meter))
|
||||
|
||||
result = create_workplan(
|
||||
repo,
|
||||
"P-WP-0002",
|
||||
"Second plan",
|
||||
"Deliver the second governed slice.",
|
||||
status="ready",
|
||||
correlation_id="00000000-0000-4000-8000-000000000002",
|
||||
repo_slug="pilot",
|
||||
)
|
||||
|
||||
assert result.status == "applied"
|
||||
assert result.evidence["git_sha"] == head_sha(repo)
|
||||
path = repo / "workplans" / "P-WP-0002-second-plan.md"
|
||||
assert path.is_file()
|
||||
text = path.read_text(encoding="utf-8")
|
||||
assert "status: ready" in text
|
||||
assert "Deliver the second governed slice." in text
|
||||
_snapshot, index = observe_repository(repo, slug="pilot")
|
||||
assert next(r for r in index.work_records if r.id == "P-WP-0002").status == "ready"
|
||||
stored = load_index(default_index_path(repo))
|
||||
event = next(e for e in stored.events if e.get("type") == "repo.command.applied")
|
||||
assert event["command"] == "repo.work.create_workplan"
|
||||
assert '"kind": "workplan_create"' in meter.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_update_by_uuid_is_idempotent_and_honors_expected_head(tmp_path: Path, monkeypatch) -> None:
|
||||
repo = _fixture(tmp_path)
|
||||
monkeypatch.setenv("RM_IDEMPOTENCY_PATH", str(tmp_path / "idempotency.json"))
|
||||
before = head_sha(repo)
|
||||
|
||||
rejected = update_workplan(
|
||||
repo,
|
||||
"11111111-1111-4111-8111-111111111111",
|
||||
status="active",
|
||||
expected_head_sha="0" * 40,
|
||||
)
|
||||
assert rejected.status == "rejected"
|
||||
assert rejected.error and rejected.error["code"] == "precondition_failed"
|
||||
|
||||
applied = update_workplan(
|
||||
repo,
|
||||
"11111111-1111-4111-8111-111111111111",
|
||||
title="Renamed",
|
||||
status="active",
|
||||
expected_head_sha=before,
|
||||
idempotency_key="workplan-update-1",
|
||||
)
|
||||
assert applied.status == "applied"
|
||||
assert applied.evidence["git_sha"] != before
|
||||
text = (repo / "workplans" / "P-WP-0001-first.md").read_text(encoding="utf-8")
|
||||
assert 'title: "Renamed"' in text
|
||||
assert "status: active" in text
|
||||
|
||||
replay = update_workplan(
|
||||
repo,
|
||||
"11111111-1111-4111-8111-111111111111",
|
||||
title="Renamed",
|
||||
status="active",
|
||||
expected_head_sha=before,
|
||||
idempotency_key="workplan-update-1",
|
||||
)
|
||||
assert replay.status == "applied"
|
||||
assert replay.evidence["git_sha"] == applied.evidence["git_sha"]
|
||||
|
||||
|
||||
def test_delete_is_guarded_recoverable_archive(tmp_path: Path) -> None:
|
||||
repo = _fixture(tmp_path)
|
||||
|
||||
rejected = archive_workplan(repo, "P-WP-0001")
|
||||
assert rejected.status == "rejected"
|
||||
assert rejected.error and rejected.error["code"] == "confirmation_required"
|
||||
assert (repo / "workplans" / "P-WP-0001-first.md").is_file()
|
||||
|
||||
applied = archive_workplan(repo, "P-WP-0001", confirm_archive=True)
|
||||
assert applied.status == "applied"
|
||||
assert not (repo / "workplans" / "P-WP-0001-first.md").exists()
|
||||
archived = list((repo / "workplans" / "archived").glob("*-P-WP-0001-first.md"))
|
||||
assert len(archived) == 1
|
||||
assert "status: archived" in archived[0].read_text(encoding="utf-8")
|
||||
_snapshot, index = observe_repository(repo)
|
||||
record = next(r for r in index.work_records if r.id == "P-WP-0001")
|
||||
assert record.status == "archived"
|
||||
assert record.source_path.startswith("workplans/archived/")
|
||||
|
||||
|
||||
def test_create_rejects_unsafe_filename_and_duplicate_id(tmp_path: Path) -> None:
|
||||
repo = _fixture(tmp_path)
|
||||
|
||||
unsafe = create_workplan(
|
||||
repo,
|
||||
"P-WP-0002",
|
||||
"Second",
|
||||
"Goal",
|
||||
filename="../outside.md",
|
||||
)
|
||||
assert unsafe.status == "rejected"
|
||||
assert unsafe.error and unsafe.error["code"] == "validation_error"
|
||||
|
||||
duplicate = create_workplan(repo, "P-WP-0001", "Duplicate", "Goal")
|
||||
assert duplicate.status == "rejected"
|
||||
assert duplicate.error and duplicate.error["code"] == "conflict"
|
||||
Loading…
Add table
Add a link
Reference in a new issue