feat: support mixed identifier convergence
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a053ff-1d6f-7fe2-ac1c-a6eb40a42a0c
This commit is contained in:
parent
4901b6d623
commit
5789e8c520
5 changed files with 313 additions and 11 deletions
|
|
@ -11,6 +11,7 @@ from repo_manager.identifiers import (
|
|||
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,
|
||||
|
|
@ -38,6 +39,101 @@ def test_projection_preflight_rejects_unproven_assignment() -> None:
|
|||
]
|
||||
|
||||
|
||||
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": "railliance01",
|
||||
},
|
||||
)
|
||||
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(
|
||||
|
|
@ -335,7 +431,7 @@ def test_migration_batch_plan_rejects_projection_gap(tmp_path: Path, monkeypatch
|
|||
assert batch["ok"] is False
|
||||
assert batch["ready_for_approval"] is False
|
||||
assert batch["repositories"][0]["projection_preflight"]["ok"] is False
|
||||
assert any("current UUID=200" in error["reason"] for error in batch["errors"])
|
||||
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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue