Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a053ff-1d6f-7fe2-ac1c-a6eb40a42a0c
484 lines
18 KiB
Python
484 lines
18 KiB
Python
from __future__ import annotations
|
|
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
from repo_manager.identifiers import (
|
|
_projection_migration_preflight,
|
|
derive_work_record_uuid,
|
|
load_fleet_namespace,
|
|
migrate_repository_identifier_files,
|
|
migrate_repository_projection,
|
|
plan_identifier_migration,
|
|
plan_identifier_migration_batch,
|
|
scan_live_identifier_collisions,
|
|
verify_identifier_migration_batch,
|
|
verify_identifier_migration_plan,
|
|
)
|
|
|
|
|
|
def test_declared_fleet_namespace_is_helixforge() -> None:
|
|
assert load_fleet_namespace() == "helixforge"
|
|
|
|
|
|
def test_projection_preflight_rejects_unproven_assignment() -> None:
|
|
result = _projection_migration_preflight(
|
|
[{"action": "assign", "record_id": "ONE-WP-0001"}],
|
|
["http://hub.test"],
|
|
)
|
|
|
|
assert result["ok"] is False
|
|
assert result["errors"] == [
|
|
{
|
|
"scope": "ONE-WP-0001",
|
|
"reason": "projection-aware approval does not yet support UUID assignments",
|
|
}
|
|
]
|
|
|
|
|
|
def test_projection_preflight_accepts_mixed_convergent_states(monkeypatch) -> None:
|
|
mappings = [
|
|
{
|
|
"action": "replace",
|
|
"kind": "workplan",
|
|
"record_id": "ONE-WP-0001",
|
|
"current_uuid": "11111111-1111-4111-8111-111111111111",
|
|
"derived_uuid": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
|
},
|
|
{
|
|
"action": "replace",
|
|
"kind": "task",
|
|
"record_id": "ONE-WP-0001-T01",
|
|
"current_uuid": "22222222-2222-4222-8222-222222222222",
|
|
"derived_uuid": "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
|
|
},
|
|
]
|
|
|
|
def projection_get(self, url):
|
|
value = str(url)
|
|
present = value.endswith(
|
|
(mappings[0]["current_uuid"], mappings[1]["derived_uuid"])
|
|
)
|
|
return httpx.Response(200 if present else 404, request=httpx.Request("GET", url))
|
|
|
|
monkeypatch.setattr(httpx.Client, "get", projection_get)
|
|
result = _projection_migration_preflight(mappings, ["http://hub.test"])
|
|
|
|
assert result["ok"] is True
|
|
projection = result["projections"][0]
|
|
assert projection["mode"] == "mixed_convergence"
|
|
assert projection["state_counts"]["legacy_source"] == 1
|
|
assert projection["state_counts"]["derived_target"] == 1
|
|
|
|
|
|
def test_projection_migration_client_requires_primary_and_exact_seal(tmp_path: Path) -> None:
|
|
repo = tmp_path / "one"
|
|
path = repo / "workplans" / "one.md"
|
|
_workplan(path, "ONE-WP-0001", "active")
|
|
subprocess.run(["git", "init"], cwd=repo, check=True, capture_output=True)
|
|
subprocess.run(["git", "add", "."], cwd=repo, check=True, capture_output=True)
|
|
subprocess.run(
|
|
[
|
|
"git",
|
|
"-c",
|
|
"user.name=Test",
|
|
"-c",
|
|
"user.email=test@example.com",
|
|
"commit",
|
|
"-m",
|
|
"seed",
|
|
],
|
|
cwd=repo,
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
_push_fixture_to_upstream(repo, tmp_path / "migration-remote.git")
|
|
plan = plan_identifier_migration(tmp_path, "helixforge")
|
|
|
|
def handler(request: httpx.Request) -> httpx.Response:
|
|
if request.url.path == "/state/health":
|
|
return httpx.Response(
|
|
200,
|
|
json={
|
|
"status": "ok",
|
|
"instance_role": "primary",
|
|
"instance_label": "railiance01",
|
|
},
|
|
)
|
|
assert request.url.path == "/identifier-migrations/repositories/one/apply"
|
|
assert request.headers["idempotency-key"].endswith(plan["plan_sha256"])
|
|
return httpx.Response(
|
|
200, json={"schema": "state-hub.identifier-migration-apply.v1"}
|
|
)
|
|
|
|
result = migrate_repository_projection(
|
|
plan,
|
|
repo_slug="one",
|
|
confirm_plan_sha256=plan["plan_sha256"],
|
|
api_base="http://hub.test",
|
|
transport=httpx.MockTransport(handler),
|
|
)
|
|
assert result["ok"] is True
|
|
assert result["direction"] == "forward"
|
|
|
|
with pytest.raises(ValueError, match="confirm-plan-sha256"):
|
|
migrate_repository_projection(
|
|
plan,
|
|
repo_slug="one",
|
|
confirm_plan_sha256="0" * 64,
|
|
api_base="http://hub.test",
|
|
transport=httpx.MockTransport(handler),
|
|
)
|
|
|
|
|
|
def _workplan(path: Path, identifier: str, status: str, task_status: str = "todo") -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(
|
|
f"""---
|
|
id: {identifier}
|
|
title: Test
|
|
status: {status}
|
|
---
|
|
|
|
## Task
|
|
|
|
```task
|
|
id: {identifier}-T01
|
|
status: {task_status}
|
|
```
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
|
|
def test_uuid_derivation_is_stable_and_namespace_scoped() -> None:
|
|
first = derive_work_record_uuid("helixforge", "RMGR-WP-0005")
|
|
assert str(first) == "6dcf854e-9229-569d-90f0-2d9df235a61d"
|
|
assert derive_work_record_uuid("helixforge", "RMGR-WP-0005") == first
|
|
assert derive_work_record_uuid("client-a", "RMGR-WP-0005") != first
|
|
with pytest.raises(ValueError):
|
|
derive_work_record_uuid("Client A", "RMGR-WP-0005")
|
|
|
|
|
|
def test_preflight_blocks_live_collision_but_ignores_archived_history(tmp_path: Path) -> None:
|
|
one = tmp_path / "one"
|
|
two = tmp_path / "two"
|
|
_workplan(one / "workplans" / "one.md", "SHARED-WP-0001", "active")
|
|
_workplan(two / "workplans" / "two.md", "SHARED-WP-0001", "ready")
|
|
report = scan_live_identifier_collisions(tmp_path)
|
|
assert report["ok"] is False
|
|
assert "SHARED-WP-0001" in report["collisions"]
|
|
|
|
_workplan(two / "workplans" / "two.md", "SHARED-WP-0001", "archived")
|
|
report = scan_live_identifier_collisions(tmp_path)
|
|
assert report["ok"] is True
|
|
|
|
|
|
def test_migration_plan_preserves_mapping_and_is_atomic_per_repo(tmp_path: Path) -> None:
|
|
one = tmp_path / "one"
|
|
two = tmp_path / "two"
|
|
_workplan(one / "workplans" / "one.md", "ONE-WP-0001", "active")
|
|
_workplan(two / "workplans" / "two.md", "TWO-WP-0001", "ready", task_status="done")
|
|
|
|
report = plan_identifier_migration(tmp_path, "fleet-a")
|
|
|
|
assert report["ok"] is True
|
|
assert report["apply_policy"] == "all-or-nothing per repository"
|
|
assert report["totals"] == {
|
|
"repositories": 2,
|
|
"eligible": 2,
|
|
"skipped": 0,
|
|
"records": 3,
|
|
"replace": 0,
|
|
"assign": 3,
|
|
"unchanged": 0,
|
|
}
|
|
one_plan = next(item for item in report["repositories"] if item["repo"] == "one")
|
|
assert one_plan["atomic_unit"] is True
|
|
assert {item["record_id"] for item in one_plan["mappings"]} == {
|
|
"ONE-WP-0001",
|
|
"ONE-WP-0001-T01",
|
|
}
|
|
assert all(item["current_uuid"] is None for item in one_plan["mappings"])
|
|
assert len(report["plan_sha256"]) == 64
|
|
assert report["generated_at"].endswith("Z")
|
|
|
|
|
|
def test_migration_plan_skips_entire_repo_affected_by_collision(tmp_path: Path) -> None:
|
|
one = tmp_path / "one"
|
|
two = tmp_path / "two"
|
|
_workplan(one / "workplans" / "one.md", "SHARED-WP-0001", "active")
|
|
_workplan(one / "workplans" / "safe.md", "ONE-WP-0002", "active")
|
|
_workplan(two / "workplans" / "two.md", "SHARED-WP-0001", "ready")
|
|
|
|
report = plan_identifier_migration(tmp_path, "fleet-a")
|
|
|
|
assert report["ok"] is False
|
|
assert report["totals"]["skipped"] == 2
|
|
one_plan = next(item for item in report["repositories"] if item["repo"] == "one")
|
|
assert one_plan["eligible"] is False
|
|
assert any(item["record_id"] == "ONE-WP-0002" for item in one_plan["mappings"])
|
|
assert any(item["reason"] == "live identifier collision" for item in one_plan["blockers"])
|
|
|
|
|
|
def test_migration_verification_detects_tampering_and_source_drift(tmp_path: Path) -> None:
|
|
repo = tmp_path / "one"
|
|
path = repo / "workplans" / "one.md"
|
|
_workplan(path, "ONE-WP-0001", "active")
|
|
subprocess.run(["git", "init"], cwd=repo, check=True, capture_output=True)
|
|
subprocess.run(["git", "add", "."], cwd=repo, check=True, capture_output=True)
|
|
subprocess.run(
|
|
["git", "-c", "user.name=Test", "-c", "user.email=test@example.com", "commit", "-m", "seed"],
|
|
cwd=repo,
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
plan = plan_identifier_migration(tmp_path, "helixforge")
|
|
assert verify_identifier_migration_plan(plan)["ok"] is True
|
|
assert verify_identifier_migration_plan(plan, repo_slug="one")["ok"] is True
|
|
assert verify_identifier_migration_plan(plan, repo_slug="missing")["ok"] is False
|
|
|
|
path.write_text(path.read_text(encoding="utf-8") + "\nchanged\n", encoding="utf-8")
|
|
verification = verify_identifier_migration_plan(plan)
|
|
assert verification["ok"] is False
|
|
assert any(
|
|
error["reason"] == "authoritative source changed after planning"
|
|
for error in verification["errors"]
|
|
)
|
|
|
|
plan["namespace"] = "tampered"
|
|
assert any(
|
|
error["reason"] == "plan SHA-256 mismatch"
|
|
for error in verify_identifier_migration_plan(plan)["errors"]
|
|
)
|
|
|
|
|
|
def test_identifier_file_migration_dry_run_forward_and_reverse(tmp_path: Path) -> None:
|
|
repo = tmp_path / "one"
|
|
path = repo / "workplans" / "one.md"
|
|
_workplan(path, "ONE-WP-0001", "active")
|
|
old_workplan = "11111111-1111-4111-8111-111111111111"
|
|
old_task = "22222222-2222-4222-8222-222222222222"
|
|
original = path.read_text(encoding="utf-8")
|
|
original = original.replace(
|
|
"status: active\n---",
|
|
f'status: active\nstate_hub_workstream_id: "{old_workplan}"\n---',
|
|
).replace(
|
|
"status: todo\n```",
|
|
f'status: todo\nstate_hub_task_id: "{old_task}"\n```',
|
|
)
|
|
path.write_text(original, encoding="utf-8")
|
|
plan = plan_identifier_migration(tmp_path, "helixforge")
|
|
seal = plan["plan_sha256"]
|
|
|
|
report = migrate_repository_identifier_files(
|
|
plan,
|
|
repo_slug="one",
|
|
confirm_plan_sha256=seal,
|
|
)
|
|
assert report["executed"] is False
|
|
assert report["replacements"] == 2
|
|
assert path.read_text(encoding="utf-8") == original
|
|
|
|
report = migrate_repository_identifier_files(
|
|
plan,
|
|
repo_slug="one",
|
|
confirm_plan_sha256=seal,
|
|
execute=True,
|
|
)
|
|
assert report["executed"] is True
|
|
migrated = path.read_text(encoding="utf-8")
|
|
assert str(derive_work_record_uuid("helixforge", "ONE-WP-0001")) in migrated
|
|
assert str(derive_work_record_uuid("helixforge", "ONE-WP-0001-T01")) in migrated
|
|
assert old_workplan not in migrated
|
|
assert old_task not in migrated
|
|
|
|
report = migrate_repository_identifier_files(
|
|
plan,
|
|
repo_slug="one",
|
|
confirm_plan_sha256=seal,
|
|
direction="reverse",
|
|
execute=True,
|
|
)
|
|
assert report["direction"] == "reverse"
|
|
assert path.read_text(encoding="utf-8") == original
|
|
|
|
|
|
def test_identifier_file_migration_requires_exact_plan_confirmation(tmp_path: Path) -> None:
|
|
repo = tmp_path / "one"
|
|
_workplan(repo / "workplans" / "one.md", "ONE-WP-0001", "active")
|
|
plan = plan_identifier_migration(tmp_path, "helixforge")
|
|
|
|
with pytest.raises(ValueError, match="confirm-plan-sha256"):
|
|
migrate_repository_identifier_files(
|
|
plan,
|
|
repo_slug="one",
|
|
confirm_plan_sha256="0" * 64,
|
|
)
|
|
|
|
|
|
def _push_fixture_to_upstream(repo: Path, remote: Path) -> None:
|
|
subprocess.run(["git", "init", "--bare", remote], check=True, capture_output=True)
|
|
subprocess.run(["git", "remote", "add", "origin", str(remote)], cwd=repo, check=True)
|
|
subprocess.run(["git", "push", "-u", "origin", "HEAD:main"], cwd=repo, check=True)
|
|
|
|
|
|
def test_migration_batch_plan_pins_clean_synchronized_repo(
|
|
tmp_path: Path, monkeypatch
|
|
) -> None:
|
|
repo = tmp_path / "one"
|
|
path = repo / "workplans" / "one.md"
|
|
_workplan(path, "ONE-WP-0001", "active")
|
|
path.write_text(
|
|
path.read_text(encoding="utf-8")
|
|
.replace(
|
|
"status: active\n---",
|
|
'status: active\nstate_hub_workstream_id: "11111111-1111-4111-8111-111111111111"\n---',
|
|
)
|
|
.replace(
|
|
"status: todo\n```",
|
|
'status: todo\nstate_hub_task_id: "22222222-2222-4222-8222-222222222222"\n```',
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
subprocess.run(["git", "init"], cwd=repo, check=True, capture_output=True)
|
|
subprocess.run(["git", "add", "."], cwd=repo, check=True, capture_output=True)
|
|
subprocess.run(
|
|
["git", "-c", "user.name=Test", "-c", "user.email=test@example.com", "commit", "-m", "seed"],
|
|
cwd=repo,
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
_push_fixture_to_upstream(repo, tmp_path / "remote.git")
|
|
plan = plan_identifier_migration(tmp_path, "helixforge")
|
|
|
|
current_uuids = {
|
|
mapping["current_uuid"] for mapping in plan["repositories"][0]["mappings"]
|
|
}
|
|
|
|
def projection_get(self, url):
|
|
status = 200 if any(str(url).endswith(str(value)) for value in current_uuids) else 404
|
|
return httpx.Response(status, request=httpx.Request("GET", url))
|
|
|
|
monkeypatch.setattr(httpx.Client, "get", projection_get)
|
|
batch = plan_identifier_migration_batch(
|
|
plan,
|
|
repo_slugs=["one"],
|
|
projection_api_bases=["http://hub-one.test", "http://hub-two.test"],
|
|
)
|
|
|
|
assert batch["ok"] is True
|
|
assert batch["ready_for_approval"] is True
|
|
assert batch["apply_authorized"] is False
|
|
assert batch["approval_required"] is True
|
|
assert batch["projection_api_bases"] == [
|
|
"http://hub-one.test",
|
|
"http://hub-two.test",
|
|
]
|
|
assert batch["repositories"][0]["projection_preflight"]["ok"] is True
|
|
assert batch["repositories"][0]["git_preflight"]["upstream"] == "origin/main"
|
|
assert len(batch["batch_sha256"]) == 64
|
|
|
|
def projection_drift(self, url):
|
|
return httpx.Response(404, request=httpx.Request("GET", url))
|
|
|
|
monkeypatch.setattr(httpx.Client, "get", projection_drift)
|
|
verification = verify_identifier_migration_batch(batch, plan=plan)
|
|
assert verification["ok"] is False
|
|
assert verification["ready_for_decision"] is False
|
|
|
|
|
|
def test_migration_batch_plan_rejects_projection_gap(tmp_path: Path, monkeypatch) -> None:
|
|
repo = tmp_path / "one"
|
|
path = repo / "workplans" / "one.md"
|
|
_workplan(path, "ONE-WP-0001", "active")
|
|
path.write_text(
|
|
path.read_text(encoding="utf-8")
|
|
.replace(
|
|
"status: active\n---",
|
|
'status: active\nstate_hub_workstream_id: "11111111-1111-4111-8111-111111111111"\n---',
|
|
)
|
|
.replace(
|
|
"status: todo\n```",
|
|
'status: todo\nstate_hub_task_id: "22222222-2222-4222-8222-222222222222"\n```',
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
subprocess.run(["git", "init"], cwd=repo, check=True, capture_output=True)
|
|
subprocess.run(["git", "add", "."], cwd=repo, check=True, capture_output=True)
|
|
subprocess.run(
|
|
["git", "-c", "user.name=Test", "-c", "user.email=test@example.com", "commit", "-m", "seed"],
|
|
cwd=repo,
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
_push_fixture_to_upstream(repo, tmp_path / "remote.git")
|
|
plan = plan_identifier_migration(tmp_path, "helixforge")
|
|
|
|
def projection_get(self, url):
|
|
return httpx.Response(404, request=httpx.Request("GET", url))
|
|
|
|
monkeypatch.setattr(httpx.Client, "get", projection_get)
|
|
batch = plan_identifier_migration_batch(
|
|
plan,
|
|
repo_slugs=["one"],
|
|
projection_api_bases=["http://hub.test"],
|
|
)
|
|
|
|
assert batch["ok"] is False
|
|
assert batch["ready_for_approval"] is False
|
|
assert batch["repositories"][0]["projection_preflight"]["ok"] is False
|
|
assert any("neither legacy nor derived" in error["reason"] for error in batch["errors"])
|
|
|
|
|
|
def test_migration_batch_plan_rejects_dirty_or_duplicate_scope(tmp_path: Path) -> None:
|
|
repo = tmp_path / "one"
|
|
path = repo / "workplans" / "one.md"
|
|
_workplan(path, "ONE-WP-0001", "active")
|
|
subprocess.run(["git", "init"], cwd=repo, check=True, capture_output=True)
|
|
subprocess.run(["git", "add", "."], cwd=repo, check=True, capture_output=True)
|
|
subprocess.run(
|
|
["git", "-c", "user.name=Test", "-c", "user.email=test@example.com", "commit", "-m", "seed"],
|
|
cwd=repo,
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
_push_fixture_to_upstream(repo, tmp_path / "remote.git")
|
|
plan = plan_identifier_migration(tmp_path, "helixforge")
|
|
path.write_text(path.read_text(encoding="utf-8") + "\ndrift\n", encoding="utf-8")
|
|
|
|
batch = plan_identifier_migration_batch(plan, repo_slugs=["one", "one"])
|
|
|
|
assert batch["ok"] is False
|
|
assert batch["ready_for_approval"] is False
|
|
assert any(error["reason"] == "repository list contains duplicates" for error in batch["errors"])
|
|
assert any(error["reason"] == "worktree is not clean" for error in batch["errors"])
|
|
|
|
|
|
def test_migration_batch_verification_repeats_preflight_and_detects_tampering(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
repo = tmp_path / "one"
|
|
path = repo / "workplans" / "one.md"
|
|
_workplan(path, "ONE-WP-0001", "active")
|
|
subprocess.run(["git", "init"], cwd=repo, check=True, capture_output=True)
|
|
subprocess.run(["git", "add", "."], cwd=repo, check=True, capture_output=True)
|
|
subprocess.run(
|
|
["git", "-c", "user.name=Test", "-c", "user.email=test@example.com", "commit", "-m", "seed"],
|
|
cwd=repo,
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
_push_fixture_to_upstream(repo, tmp_path / "remote.git")
|
|
plan = plan_identifier_migration(tmp_path, "helixforge")
|
|
batch = plan_identifier_migration_batch(plan, repo_slugs=["one"])
|
|
|
|
assert verify_identifier_migration_batch(batch, plan=plan)["ok"] is True
|
|
|
|
batch["totals"]["records"] = 999
|
|
verification = verify_identifier_migration_batch(batch, plan=plan)
|
|
assert verification["ok"] is False
|
|
assert any(error["reason"] == "batch SHA-256 mismatch" for error in verification["errors"])
|