feat: converge mixed identifier projections
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a053ff-1d6f-7fe2-ac1c-a6eb40a42a0c
This commit is contained in:
parent
6f64f1aab8
commit
151fdcf397
5 changed files with 256 additions and 25 deletions
|
|
@ -204,6 +204,8 @@ async def test_repository_migration_cascades_and_reverses(test_engine):
|
|||
result = await apply_repository_identifier_migration(session, plan, "test-repo")
|
||||
assert result.direction == "forward"
|
||||
assert result.replacements == 2
|
||||
assert result.migrated == 2
|
||||
assert result.already_derived == 0
|
||||
assert result.assignments_deferred == 1
|
||||
|
||||
async with factory() as session:
|
||||
|
|
@ -277,6 +279,103 @@ async def test_repository_migration_cascades_and_reverses(test_engine):
|
|||
assert all(alias.reversed_at is not None for alias in aliases)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_repository_migration_accepts_mixed_legacy_and_derived_projection(test_engine):
|
||||
factory = async_sessionmaker(test_engine, class_=AsyncSession, expire_on_commit=False)
|
||||
ids = await _seed_projection(factory, "mixed-repo")
|
||||
plan = _sealed_plan("mixed-repo", ids["workplan"], ids["task"])
|
||||
task_new = _derived("TEST-WP-0001-T01")
|
||||
|
||||
# Forge reconciliation reached the task before the sealed repository
|
||||
# migration. The parent remains legacy, so the repository is genuinely
|
||||
# mixed rather than already migrated as one unit.
|
||||
async with factory() as session:
|
||||
await session.execute(
|
||||
text("UPDATE tasks SET id = :new_id, record_id = :record_id WHERE id = :old_id"),
|
||||
{
|
||||
"old_id": ids["task"],
|
||||
"new_id": task_new,
|
||||
"record_id": "TEST-WP-0001-T01",
|
||||
},
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
async with factory() as session:
|
||||
result = await apply_repository_identifier_migration(session, plan, "mixed-repo")
|
||||
assert result.replacements == 2
|
||||
assert result.migrated == 1
|
||||
assert result.already_derived == 1
|
||||
|
||||
async with factory() as session:
|
||||
aliases = list((await session.execute(select(WorkRecordIdentifierAlias))).scalars())
|
||||
assert len(aliases) == 2
|
||||
assert all(alias.migration_status == "applied" for alias in aliases)
|
||||
|
||||
# Retry is a verified idempotent convergence, not a conflicting target.
|
||||
async with factory() as session:
|
||||
retried = await apply_repository_identifier_migration(session, plan, "mixed-repo")
|
||||
assert retried.migrated == 0
|
||||
assert retried.already_derived == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_repository_migration_rejects_neither_legacy_nor_derived(test_engine):
|
||||
factory = async_sessionmaker(test_engine, class_=AsyncSession, expire_on_commit=False)
|
||||
ids = await _seed_projection(factory, "missing-repo")
|
||||
plan = _sealed_plan(
|
||||
"missing-repo", ids["workplan"], ids["task"], task_source_override=uuid.uuid4()
|
||||
)
|
||||
|
||||
async with factory() as session:
|
||||
with pytest.raises(IdentifierMigrationError, match="neither legacy nor derived task"):
|
||||
await apply_repository_identifier_migration(session, plan, "missing-repo")
|
||||
|
||||
async with factory() as session:
|
||||
assert await session.scalar(
|
||||
text("SELECT count(*) FROM work_record_identifier_aliases")
|
||||
) == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_identifier_migration_http_apply_and_reverse(client, test_engine, monkeypatch):
|
||||
from api.routers import identifier_migrations
|
||||
|
||||
monkeypatch.setattr(
|
||||
identifier_migrations.settings, "state_hub_instance_role", "primary"
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
identifier_migrations.settings, "state_hub_instance_label", "railliance01"
|
||||
)
|
||||
factory = async_sessionmaker(test_engine, class_=AsyncSession, expire_on_commit=False)
|
||||
ids = await _seed_projection(factory, "http-migration-repo")
|
||||
plan = _sealed_plan("http-migration-repo", ids["workplan"], ids["task"])
|
||||
body = {
|
||||
"plan": plan,
|
||||
"expected_plan_sha256": plan["plan_sha256"],
|
||||
"primary_confirmed": True,
|
||||
}
|
||||
|
||||
response = await client.post(
|
||||
"/identifier-migrations/repositories/http-migration-repo/apply", json=body
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json()["result"] == {
|
||||
"repo_slug": "http-migration-repo",
|
||||
"plan_sha256": plan["plan_sha256"],
|
||||
"direction": "forward",
|
||||
"replacements": 2,
|
||||
"migrated": 2,
|
||||
"already_derived": 0,
|
||||
"assignments_deferred": 1,
|
||||
}
|
||||
|
||||
response = await client.post(
|
||||
"/identifier-migrations/repositories/http-migration-repo/reverse", json=body
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json()["result"]["direction"] == "reverse"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_repository_migration_accepts_protected_prior_slug(test_engine):
|
||||
factory = async_sessionmaker(test_engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
|
@ -340,7 +439,7 @@ async def test_repository_migration_is_atomic_when_a_source_is_missing(test_engi
|
|||
)
|
||||
|
||||
async with factory() as session:
|
||||
with pytest.raises(IdentifierMigrationError, match="source task"):
|
||||
with pytest.raises(IdentifierMigrationError, match="neither legacy nor derived task"):
|
||||
await apply_repository_identifier_migration(session, plan, "atomic-repo")
|
||||
|
||||
async with factory() as session:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue