Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
294 lines
9.6 KiB
Python
294 lines
9.6 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import subprocess
|
|
from datetime import date
|
|
from pathlib import Path
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
from repo_manager.commands.task import add_adhoc_task, mutate_task
|
|
from repo_manager.commands.workplan import create_workplan
|
|
from repo_manager.identifiers import (
|
|
derive_work_record_uuid,
|
|
ensure_missing_work_record_identifiers,
|
|
)
|
|
from repo_manager.projection_sync import sync_repository_projection
|
|
|
|
|
|
def _git(repo: Path, *args: str) -> None:
|
|
subprocess.run(["git", *args], cwd=repo, check=True, capture_output=True)
|
|
|
|
|
|
def _fixture(tmp_path: Path, *, remote: bool = False) -> 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: active
|
|
owner: codex
|
|
---
|
|
|
|
# First
|
|
|
|
## Existing task
|
|
|
|
```task
|
|
id: P-WP-0001-T01
|
|
status: todo
|
|
priority: high
|
|
```
|
|
|
|
Existing task details.
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
_git(repo, "add", ".")
|
|
_git(repo, "commit", "-m", "seed")
|
|
if remote:
|
|
bare = tmp_path / "remote.git"
|
|
subprocess.run(["git", "init", "--bare", str(bare)], check=True, capture_output=True)
|
|
_git(repo, "remote", "add", "origin", str(bare))
|
|
_git(repo, "push", "-u", "origin", "HEAD")
|
|
return repo
|
|
|
|
|
|
def test_adhoc_ids_are_canonical_and_stable() -> None:
|
|
identifier = "P-WP-ADHOC-2026-08-30"
|
|
task_id = f"{identifier}-T01"
|
|
assert derive_work_record_uuid("helixforge", identifier) == derive_work_record_uuid(
|
|
"helixforge", identifier
|
|
)
|
|
assert derive_work_record_uuid("helixforge", task_id).version == 5
|
|
|
|
|
|
def test_missing_assignment_preserves_existing_uuid(tmp_path: Path) -> None:
|
|
repo = _fixture(tmp_path)
|
|
existing = "11111111-1111-4111-8111-111111111111"
|
|
path = repo / "workplans" / "P-WP-0001-first.md"
|
|
text = path.read_text(encoding="utf-8").replace(
|
|
"owner: codex\n", f'owner: codex\nstate_hub_workstream_id: "{existing}"\n'
|
|
)
|
|
path.write_text(text, encoding="utf-8")
|
|
|
|
result = ensure_missing_work_record_identifiers(repo)
|
|
|
|
assert result["ok"] is True
|
|
assert {item["record_id"] for item in result["assignments"]} == {"P-WP-0001-T01"}
|
|
assert existing in path.read_text(encoding="utf-8")
|
|
|
|
|
|
def test_create_adhoc_uses_conventional_filename_and_uuid(tmp_path: Path) -> None:
|
|
repo = _fixture(tmp_path)
|
|
result = create_workplan(
|
|
repo,
|
|
"P-WP-ADHOC-2026-08-30",
|
|
"Ad Hoc — 2026-08-30",
|
|
"Small fixes.",
|
|
)
|
|
|
|
assert result.status == "applied"
|
|
path = repo / "workplans" / "ADHOC-2026-08-30.md"
|
|
assert path.is_file()
|
|
assert str(derive_work_record_uuid("helixforge", "P-WP-ADHOC-2026-08-30")) in path.read_text(
|
|
encoding="utf-8"
|
|
)
|
|
|
|
|
|
def test_task_add_update_and_adhoc_replay(tmp_path: Path, monkeypatch) -> None:
|
|
repo = _fixture(tmp_path)
|
|
monkeypatch.setenv("RM_IDEMPOTENCY_PATH", str(tmp_path / "idempotency.json"))
|
|
added = mutate_task(
|
|
repo,
|
|
None,
|
|
operation="add",
|
|
workplan_id="P-WP-0001",
|
|
title="Second task",
|
|
description="Do the second thing.",
|
|
priority="high",
|
|
)
|
|
assert added.status == "applied"
|
|
assert added.evidence["task_id"] == "P-WP-0001-T02"
|
|
|
|
updated = mutate_task(
|
|
repo,
|
|
"P-WP-0001-T02",
|
|
operation="update",
|
|
status="progress",
|
|
needs_human=True,
|
|
intervention_note="Review the external effect.",
|
|
)
|
|
assert updated.status == "applied"
|
|
text = (repo / "workplans" / "P-WP-0001-first.md").read_text(encoding="utf-8")
|
|
assert "status: progress" in text
|
|
assert "needs_human: true" in text
|
|
|
|
first = add_adhoc_task(
|
|
repo,
|
|
"Tiny fix",
|
|
"Complete the tiny fix.",
|
|
on_date=date(2026, 8, 30),
|
|
idempotency_key="adhoc-1",
|
|
)
|
|
replay = add_adhoc_task(
|
|
repo,
|
|
"Tiny fix",
|
|
"Complete the tiny fix.",
|
|
on_date=date(2026, 8, 30),
|
|
idempotency_key="adhoc-1",
|
|
)
|
|
assert first.status == replay.status == "applied"
|
|
assert first.evidence["task_id"] == replay.evidence["task_id"]
|
|
|
|
|
|
def test_sync_uses_two_requests_and_exact_pushed_commit(tmp_path: Path) -> None:
|
|
repo = _fixture(tmp_path, remote=True)
|
|
seen: list[str] = []
|
|
|
|
def handler(request: httpx.Request) -> httpx.Response:
|
|
seen.append(request.url.path)
|
|
if request.url.path == "/state/health":
|
|
return httpx.Response(
|
|
200,
|
|
json={"status": "ok", "instance_role": "primary", "instance_label": "railiance01"},
|
|
)
|
|
payload = __import__("json").loads(request.content)
|
|
assert (
|
|
payload["expected_commit"]
|
|
== subprocess.run(
|
|
["git", "rev-parse", "HEAD"], cwd=repo, check=True, capture_output=True, text=True
|
|
).stdout.strip()
|
|
)
|
|
return httpx.Response(
|
|
200,
|
|
json=_receipt(payload),
|
|
)
|
|
|
|
result = sync_repository_projection(
|
|
repo,
|
|
api_base="http://hub.test",
|
|
push=True,
|
|
transport=httpx.MockTransport(handler),
|
|
)
|
|
|
|
assert result["ok"] is True
|
|
assert result["requests"] == 2
|
|
assert seen == ["/state/health", "/repos/pilot/work-record-projection/reconcile"]
|
|
|
|
|
|
def test_sync_never_assigns_or_commits_over_dirty_workplans(tmp_path: Path) -> None:
|
|
repo = _fixture(tmp_path, remote=True)
|
|
path = repo / "workplans" / "P-WP-0001-first.md"
|
|
original = path.read_text(encoding="utf-8")
|
|
path.write_text(original.replace("First\n", "Locally edited\n", 1), encoding="utf-8")
|
|
|
|
result = sync_repository_projection(repo, api_base="http://hub.test", push=True)
|
|
|
|
assert result["ok"] is False
|
|
assert result["status"] == "pending_commit"
|
|
assert "state_hub_workstream_id" not in path.read_text(encoding="utf-8")
|
|
assert (
|
|
subprocess.run(
|
|
["git", "log", "-1", "--pretty=%s"],
|
|
cwd=repo,
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
).stdout.strip()
|
|
== "seed"
|
|
)
|
|
|
|
|
|
def test_sync_queues_instead_of_writing_to_wrong_instance(tmp_path: Path) -> None:
|
|
repo = _fixture(tmp_path, remote=True)
|
|
|
|
def handler(_request: httpx.Request) -> httpx.Response:
|
|
return httpx.Response(
|
|
200,
|
|
json={"status": "ok", "instance_role": "cache", "instance_label": "workstation"},
|
|
)
|
|
|
|
result = sync_repository_projection(
|
|
repo,
|
|
api_base="http://cache.test",
|
|
push=True,
|
|
transport=httpx.MockTransport(handler),
|
|
)
|
|
|
|
assert result["ok"] is True
|
|
assert result["status"] == "queued"
|
|
assert result["reason"] == "wrong_instance"
|
|
assert Path(result["pending_path"]).is_file()
|
|
|
|
|
|
def _receipt(payload, **overrides):
|
|
return {
|
|
"schema": "state-hub.repository-projection-reconcile.v1",
|
|
"instance_role": "primary", "instance_label": "railiance01",
|
|
"expected_commit": payload["expected_commit"],
|
|
"derived_commit": payload["expected_commit"],
|
|
"outcome": {"repo_slug": "pilot", "commit": payload["expected_commit"],
|
|
"status": "applied", "counts": {"created": 1}},
|
|
**overrides,
|
|
}
|
|
|
|
|
|
def test_reviewed_retry_has_distinct_key_and_identical_retry_replays(tmp_path):
|
|
repo = _fixture(tmp_path, remote=True)
|
|
seen = {}
|
|
keys = []
|
|
|
|
def handler(request):
|
|
if request.url.path == "/state/health":
|
|
return httpx.Response(200, json={"instance_role": "primary", "instance_label": "railiance01"})
|
|
payload = json.loads(request.content)
|
|
key = request.headers["Idempotency-Key"]
|
|
keys.append(key)
|
|
if key in seen and seen[key] != payload:
|
|
return httpx.Response(409, json={"detail": "idempotency key reused for different payload"})
|
|
seen[key] = payload
|
|
result = _receipt(payload)
|
|
result["outcome"]["status"] = "applied" if payload["acknowledge_retirements"] else "refused"
|
|
return httpx.Response(200, json=result)
|
|
|
|
kwargs = {"api_base": "http://hub.test", "push": True, "transport": httpx.MockTransport(handler)}
|
|
refused = sync_repository_projection(repo, **kwargs)
|
|
admitted = sync_repository_projection(repo, acknowledge_retirements=True, **kwargs)
|
|
replay = sync_repository_projection(repo, acknowledge_retirements=True, **kwargs)
|
|
assert refused["status"] == "refused"
|
|
assert admitted["status"] == replay["status"] == "applied"
|
|
assert keys[0] != keys[1] == keys[2]
|
|
assert len(seen) == 2
|
|
|
|
|
|
@pytest.mark.parametrize("overrides", [
|
|
{"expected_commit": "f" * 40}, {"derived_commit": "e" * 40},
|
|
{"instance_role": "cache"}, {"instance_label": "workstation"},
|
|
{"schema": "unrelated-receipt/v1"}, {"outcome": {"status": "applied"}},
|
|
])
|
|
def test_sync_refuses_inconsistent_receipt(tmp_path, overrides):
|
|
repo = _fixture(tmp_path, remote=True)
|
|
|
|
def handler(request):
|
|
if request.url.path == "/state/health":
|
|
return httpx.Response(200, json={"instance_role": "primary", "instance_label": "railiance01"})
|
|
return httpx.Response(200, json=_receipt(json.loads(request.content), **overrides))
|
|
|
|
result = sync_repository_projection(repo, api_base="http://hub.test", push=True,
|
|
transport=httpx.MockTransport(handler))
|
|
assert result["ok"] is False
|
|
assert result["status"] == "invalid_receipt"
|