169 lines
5.3 KiB
Python
169 lines
5.3 KiB
Python
|
|
"""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"
|