From 8a03abac75cb5531beabf1f3986b72ff1234d492 Mon Sep 17 00:00:00 2001 From: 1445043649 <> Date: Wed, 13 May 2026 09:13:16 +0800 Subject: [PATCH] test(persistence): backfill --dry-run end-to-end no-write proof Snapshots workspace / membership / users.default_workspace_id row counts before backfill(dry_run=True), runs the orchestrator, asserts no counts changed. Pins the dry-run report's per-step semantics: Step 1 counts candidates without populating defaults, so Step 2's JOIN reports 0; Step 3 picks up all NULL business rows untouched. Co-Authored-By: Claude Opus 4.7 (1M context) --- backend/tests/test_backfill_workspace_id.py | 44 +++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/backend/tests/test_backfill_workspace_id.py b/backend/tests/test_backfill_workspace_id.py index f58c263f..02dd30ce 100644 --- a/backend/tests/test_backfill_workspace_id.py +++ b/backend/tests/test_backfill_workspace_id.py @@ -257,6 +257,50 @@ async def test_ensure_legacy_workspace_refuses_when_no_users(tmp_path): await _close() +async def test_backfill_dry_run_does_not_write(tmp_path): + """``backfill(..., dry_run=True)`` reports counts but writes nothing.""" + sf = await _init_engine(tmp_path) + try: + await _seed_user(sf, email="admin@example.com", system_role="admin") + user_id = await _seed_user(sf, email="helen@example.com") + await _seed_business_row(sf, ThreadMetaRow, thread_id="t-owned", user_id=user_id) + await _seed_business_row(sf, ThreadMetaRow, thread_id="t-orphan", user_id=None) + await _seed_business_row(sf, RunRow, run_id="r-owned", thread_id="t-owned", user_id=user_id) + + # Snapshot row counts BEFORE the dry run so we can confirm + # nothing changed AFTER. + async with sf() as session: + ws_before = len((await session.execute(select(WorkspaceRow))).scalars().all()) + mem_before = len((await session.execute(select(WorkspaceMembershipRow))).scalars().all()) + users_with_default_before = len((await session.execute(select(UserRow).where(UserRow.default_workspace_id.is_not(None)))).scalars().all()) + + report = await backfill(sf, dry_run=True) + assert report["dry_run"] is True + # Step 1 reports 2 candidates (admin + helen, both without default). + assert report["users_workspaces_created"] == 2 + # Step 2 reports 0 because Step 1 didn't actually populate + # users.default_workspace_id under dry_run — the JOIN comes up empty. + assert report["threads_meta_from_users"] == 0 + assert report["runs_from_users"] == 0 + # Step 3 reports the 3 NULL business rows (t-owned, t-orphan, r-owned). + assert report["legacy_workspace_created"] is True + assert report["threads_meta_legacy"] == 2 + assert report["runs_legacy"] == 1 + + # State did not change. + async with sf() as session: + ws_after = len((await session.execute(select(WorkspaceRow))).scalars().all()) + mem_after = len((await session.execute(select(WorkspaceMembershipRow))).scalars().all()) + users_with_default_after = len((await session.execute(select(UserRow).where(UserRow.default_workspace_id.is_not(None)))).scalars().all()) + rows = (await session.execute(select(ThreadMetaRow.workspace_id))).scalars().all() + assert ws_after == ws_before + assert mem_after == mem_before + assert users_with_default_after == users_with_default_before + assert all(w is None for w in rows) + finally: + await _close() + + async def test_full_backfill_orchestrator(tmp_path): """End-to-end: backfill() runs all three steps and reports per-step counts.""" sf = await _init_engine(tmp_path)