# ============================================================ # 审批文件→DB 存量迁移黄金测试(plan.md §3.3 / 矩阵 112 行剩余项) # 覆盖:幂等迁移、dry_run 不动库、缺文件空源、后端前置校验、迁移后可读回。 # ============================================================ from __future__ import annotations import copy import json import threading from pathlib import Path import pytest from sqlalchemy import func, select from sqlalchemy.orm import Session from server.agent_core.approval_migrate import migrate_file_to_db from server.agent_core.approval_store import ApprovalStore, token_digest from server.db.database import get_session, reset_engine from server.db.models import ( ApprovalEventRecord, ApprovalGrantRecord, ApprovalRequestRecord, ) @pytest.fixture def database_backend(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): monkeypatch.setenv("APS_APPROVAL_BACKEND", "database") monkeypatch.delenv("APS_APPROVAL_PATH", raising=False) monkeypatch.setenv("APS_DATABASE_URL", f"sqlite:///{(tmp_path / 'approval.db').as_posix()}") monkeypatch.setenv("APS_APPROVAL_DATABASE_ALLOW_SQLITE", "1") reset_engine() from server.agent_core import harness store = harness.configure_approval_store() yield store store.clear() reset_engine() def _file_store() -> dict: """合成文件后端数据:2 pending + 2 grants + 3 history 事件。""" pending = { "abc1111111": { "confirmId": "abc1111111", "sessionId": "s1", "action": "mes.dispatch", "power": "P3", "params": {"versionId": 1}, "paramsHash": "h1", "tenantUuid": "platform", "ownerUserId": 1001, "projectId": "p1", "worldKey": "w1", "evidenceRefs": ["schedule-version:1"], "createdAtEpoch": 1750000000.0, "expiresAtEpoch": 1750003600.0, }, "abc2222222": { "confirmId": "abc2222222", "sessionId": "s1", "action": "schedule.publish", "power": "P2", "params": {}, "paramsHash": "h2", "tenantUuid": "platform", "ownerUserId": 1002, "projectId": "p1", "createdAtEpoch": 1750000100.0, "expiresAtEpoch": 1750003700.0, }, } grants = { "tok1" * 8: { "confirmId": "abc1111111", "action": "mes.dispatch", "power": "P3", "tenantUuid": "platform", "projectId": "p1", "ownerUserId": 1001, "paramsHash": "h1", "status": "ACTIVE", "createdAtEpoch": 1750000000.0, "expiresAtEpoch": 1750003600.0, }, "tok2" * 8: { "confirmId": "abc2222222", "action": "schedule.publish", "power": "P2", "tenantUuid": "platform", "projectId": "p1", "ownerUserId": 1002, "paramsHash": "h2", "status": "CONSUMED", "createdAtEpoch": 1750000100.0, "expiresAtEpoch": 1750003700.0, "consumedAtEpoch": 1750000200.0, }, } history = [ {"confirmId": "abc1111111", "action": "mes.dispatch", "power": "P3", "status": "APPROVED", "tenantUuid": "platform", "projectId": "p1", "ownerUserId": 1001, "decidedAtEpoch": 1750000100.0}, {"confirmId": "abc2222222", "action": "schedule.publish", "power": "P2", "status": "APPROVED", "tenantUuid": "platform", "projectId": "p1", "ownerUserId": 1002, "decidedAtEpoch": 1750000200.0}, {"confirmId": "abc2222222", "action": "schedule.publish", "power": "P2", "status": "CONSUMED", "tenantUuid": "platform", "projectId": "p1", "ownerUserId": 1002, "decidedAtEpoch": 1750000300.0}, ] return {"schemaVersion": 1, "pending": pending, "grants": grants, "history": history} def _write_file_store(tmp_path: Path) -> Path: path = tmp_path / "approvals.json" path.write_text(json.dumps(_file_store(), ensure_ascii=False), encoding="utf-8") return path def _counts() -> dict: with get_session() as session: return { "pending": session.execute(select(func.count()).select_from(ApprovalRequestRecord)).scalar(), "grants": session.execute(select(func.count()).select_from(ApprovalGrantRecord)).scalar(), "events": session.execute(select(func.count()).select_from(ApprovalEventRecord)).scalar(), } def test_migrate_file_to_db_idempotent(database_backend, tmp_path: Path): """迁移全量 + 幂等:二次运行全部 skipped,不重复插入。""" path = _write_file_store(tmp_path) first = migrate_file_to_db(str(path)) assert first["pending"] == {"migrated": 2, "skipped": 0} assert first["grants"] == {"migrated": 2, "skipped": 0} assert first["events"] == {"migrated": 3, "skipped": 0} assert first["dryRun"] is False assert _counts() == {"pending": 2, "grants": 2, "events": 3} second = migrate_file_to_db(str(path)) assert second["pending"] == {"migrated": 0, "skipped": 2} assert second["grants"] == {"migrated": 0, "skipped": 2} assert second["events"] == {"migrated": 0, "skipped": 3} assert _counts() == {"pending": 2, "grants": 2, "events": 3} def test_migrate_dry_run_does_not_touch_db(database_backend, tmp_path: Path): """dry_run:报告数量但库内零写入。""" path = _write_file_store(tmp_path) report = migrate_file_to_db(str(path), dry_run=True) assert report["dryRun"] is True assert report["pending"]["migrated"] == 2 assert report["grants"]["migrated"] == 2 assert report["events"]["migrated"] == 3 assert _counts() == {"pending": 0, "grants": 0, "events": 0} def test_migrate_missing_file_empty_source(database_backend, tmp_path: Path): """缺文件 → 空源,全部 0,不抛错。""" report = migrate_file_to_db(str(tmp_path / "missing.json")) assert report["pending"] == {"migrated": 0, "skipped": 0} assert report["grants"] == {"migrated": 0, "skipped": 0} assert report["events"] == {"migrated": 0, "skipped": 0} def test_migrate_requires_database_backend(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): """未配置 database 后端 → 显式 RuntimeError(fail closed)。""" monkeypatch.delenv("APS_APPROVAL_BACKEND", raising=False) monkeypatch.delenv("APS_APPROVAL_PATH", raising=False) with pytest.raises(RuntimeError, match="APS_APPROVAL_BACKEND=database"): migrate_file_to_db(str(tmp_path / "approvals.json")) def test_migrated_data_readable_via_db_store(database_backend, tmp_path: Path): """迁移后 pending/历史可通过 database 后端读回(数据真实生效)。""" path = _write_file_store(tmp_path) migrate_file_to_db(str(path)) now = 1749990000.0 # 合成数据时间窗口内(expiresAt 1750003600 之前) pending_items = database_backend.pending_items( tenant_uuid="platform", project_id="p1", allowed=lambda _payload: True, now_epoch=now) assert len(pending_items) == 2 assert any(item["confirmId"] == "abc1111111" for item in pending_items) history = database_backend.history_items( tenant_uuid="platform", project_id="p1", allowed=lambda _payload: True, limit=50, now_epoch=now) assert len(history) == 3 assert all(item["status"] in ("APPROVED", "CONSUMED") for item in history) # ============================================================ # Round 67: fail-closed migration, digest normalization, atomicity # ============================================================ @pytest.fixture(autouse=True) def _migration_quiesced(monkeypatch: pytest.MonkeyPatch): monkeypatch.setenv("APS_APPROVAL_MIGRATION_QUIESCED", "1") def _write_payload(tmp_path: Path, payload: object, *, name: str = "round67-approvals.json") -> Path: path = tmp_path / name path.write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8") return path def _snapshot_db() -> dict: def payload(value: dict) -> str: return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) with get_session() as session: requests = session.execute( select(ApprovalRequestRecord).order_by(ApprovalRequestRecord.confirm_id) ).scalars().all() grants = session.execute( select(ApprovalGrantRecord).order_by(ApprovalGrantRecord.token_hash) ).scalars().all() events = session.execute( select(ApprovalEventRecord).order_by(ApprovalEventRecord.id) ).scalars().all() return { "requests": [( row.confirm_id, row.tenant_uuid, row.project_id, row.owner_user_id, row.action, row.power, row.params_hash, row.status, row.approval_step, row.required_approvals, row.created_at_epoch, row.expires_at_epoch, row.revision, payload(row.payload), ) for row in requests], "grants": [( row.token_hash, row.confirm_id, row.tenant_uuid, row.project_id, row.owner_user_id, row.action, row.params_hash, row.status, row.created_at_epoch, row.expires_at_epoch, row.consumed_at_epoch, row.revision, payload(row.payload), ) for row in grants], "events": [( row.id, row.confirm_id, row.tenant_uuid, row.project_id, row.owner_user_id, row.action, row.power, row.status, row.decided_at_epoch, payload(row.payload), ) for row in events], } def _real_file_p3_grant(tmp_path: Path) -> tuple[Path, str, dict]: path = tmp_path / "real-file-approvals.json" store = ApprovalStore(str(path)) record = { "confirmId": "round67-p3-confirm", "sessionId": "round67-session", "action": "mes.dispatch", "power": "P3", "params": {"versionId": 67}, "paramsHash": "round67-params-hash", "tenantUuid": "platform", "projectId": "round67-project", "ownerUserId": 6701, "worldKey": "round67-world", "requiredApprovals": 2, "approvalStep": 0, "approvals": [], "createdAtEpoch": 1_800_000_000.0, "expiresAtEpoch": 1_900_000_000.0, } assert store.stage(record, now_epoch=1_800_000_000.0) first = store.decide( record["confirmId"], approve=True, approver={"userId": 6702, "name": "A"}, allowed=lambda _record: True, now_epoch=1_800_000_010.0, grant_ttl_seconds=3600, ) assert first and first["needsSecondConfirm"] is True second = store.decide( record["confirmId"], approve=True, approver={"userId": 6703, "name": "B"}, allowed=lambda _record: True, now_epoch=1_800_000_020.0, grant_ttl_seconds=3600, ) assert second and second.get("executionGrant") return path, str(second["executionGrant"]), record def _terminal_grant_source(raw_token: str, *, action: str = "mes.dispatch") -> dict: grant = { "confirmId": "terminal-confirm", "sessionId": "terminal-session", "action": action, "power": "P3", "tenantUuid": "platform", "projectId": "terminal-project", "ownerUserId": 6801, "worldKey": "terminal-world", "paramsHash": "terminal-params", "approvals": [{"userId": 6802}, {"userId": 6803}], "status": "ACTIVE", "createdAtEpoch": 1_800_100_000.0, "expiresAtEpoch": 1_900_100_000.0, } event = { "confirmId": grant["confirmId"], "sessionId": grant["sessionId"], "action": action, "power": "P3", "status": "APPROVED", "tenantUuid": "platform", "projectId": grant["projectId"], "ownerUserId": grant["ownerUserId"], "worldKey": grant["worldKey"], "paramsHash": grant["paramsHash"], "decidedAtEpoch": 1_800_100_010.0, } return {"schemaVersion": 1, "pending": {}, "grants": {raw_token: grant}, "history": [event]} def _seed_terminal_request_and_grant(source: dict, token_hash: str, *, grant_action: str | None = None) -> None: raw_token, grant = next(iter(source["grants"].items())) del raw_token with get_session() as session: session.add(ApprovalRequestRecord( confirm_id=grant["confirmId"], tenant_uuid=grant["tenantUuid"], project_id=grant["projectId"], owner_user_id=grant["ownerUserId"], action=grant["action"], power=grant["power"], params_hash=grant["paramsHash"], status="APPROVED", approval_step=2, required_approvals=2, created_at_epoch=grant["createdAtEpoch"], expires_at_epoch=grant["expiresAtEpoch"], revision=1, payload=copy.deepcopy(grant), )) session.add(ApprovalGrantRecord( token_hash=token_hash, confirm_id=grant["confirmId"], tenant_uuid=grant["tenantUuid"], project_id=grant["projectId"], owner_user_id=grant["ownerUserId"], action=grant_action or grant["action"], params_hash=grant["paramsHash"], status="ACTIVE", created_at_epoch=grant["createdAtEpoch"], expires_at_epoch=grant["expiresAtEpoch"], consumed_at_epoch=None, revision=0, payload=copy.deepcopy(grant), )) session.commit() def test_real_file_p3_grant_migrates_as_digest_and_consumes_once(database_backend, tmp_path: Path): path, raw_grant, record = _real_file_p3_grant(tmp_path) migrate_file_to_db(str(path)) digest = token_digest(raw_grant) with get_session() as session: assert session.get(ApprovalGrantRecord, raw_grant) is None row = session.get(ApprovalGrantRecord, digest) assert row is not None assert row.confirm_id == record["confirmId"] rebuilt = session.get(ApprovalRequestRecord, record["confirmId"]) assert rebuilt is not None assert rebuilt.status == "APPROVED" assert raw_grant not in json.dumps(_snapshot_db(), ensure_ascii=False, sort_keys=True) kwargs = { "confirm_id": record["confirmId"], "action": record["action"], "params_hash": record["paramsHash"], "allowed": lambda _record: True, "decided_by": {"userId": 6704}, "now_epoch": 1_800_000_030.0, } assert database_backend.consume_grant(raw_grant, **kwargs) is True assert database_backend.consume_grant(raw_grant, **kwargs) is False def test_terminal_grant_without_pending_rebuilds_approved_request(database_backend, tmp_path: Path): source = _terminal_grant_source("terminal-token") migrate_file_to_db(str(_write_payload(tmp_path, source))) with get_session() as session: request = session.get(ApprovalRequestRecord, "terminal-confirm") assert request is not None assert request.status == "APPROVED" assert request.action == "mes.dispatch" assert request.required_approvals == 2 def _invalid_source(case: str) -> object: data = _file_store() if case == "top-level-list": return [] if case == "schema-version": data["schemaVersion"] = 99 elif case == "pending-container": data["pending"] = [] elif case == "pending-record": data["pending"] = {"bad": "not-an-object"} elif case == "grant-container": data["grants"] = [] elif case == "grant-record": data["grants"] = {"token": "not-an-object"} elif case == "history-container": data["history"] = {} elif case == "history-record": data["history"] = ["not-an-object"] elif case == "orphan-grant": data = _terminal_grant_source("orphan-token") data["history"] = [] elif case == "mismatched-pending-key": data["pending"]["abc1111111"]["confirmId"] = "different-confirm" return data @pytest.mark.parametrize("case", [ "top-level-list", "schema-version", "pending-container", "pending-record", "grant-container", "grant-record", "history-container", "history-record", "orphan-grant", "mismatched-pending-key", ]) def test_invalid_source_fails_closed_without_database_writes(database_backend, tmp_path: Path, case: str): path = tmp_path / f"invalid-{case}.json" path.write_text(json.dumps(_invalid_source(case), ensure_ascii=False), encoding="utf-8") with pytest.raises((RuntimeError, ValueError)) as exc: migrate_file_to_db(str(path)) assert str(exc.value) assert _counts() == {"pending": 0, "grants": 0, "events": 0} def test_corrupt_json_fails_closed_without_database_writes(database_backend, tmp_path: Path): path = tmp_path / "corrupt.json" path.write_text('{"schemaVersion": 1, "pending": ', encoding="utf-8") with pytest.raises((RuntimeError, ValueError)): migrate_file_to_db(str(path)) assert _counts() == {"pending": 0, "grants": 0, "events": 0} def test_migration_requires_explicit_quiesced_flag(database_backend, tmp_path: Path, monkeypatch: pytest.MonkeyPatch): path = _write_file_store(tmp_path) monkeypatch.delenv("APS_APPROVAL_MIGRATION_QUIESCED", raising=False) with pytest.raises(RuntimeError, match="QUIESCED|quiesc"): migrate_file_to_db(str(path)) assert _counts() == {"pending": 0, "grants": 0, "events": 0} def test_existing_request_immutable_conflict_rolls_back(database_backend, tmp_path: Path): data = {"schemaVersion": 1, "pending": {"conflict-confirm": { "confirmId": "conflict-confirm", "sessionId": "s", "action": "mes.dispatch", "power": "P2", "params": {}, "paramsHash": "expected-hash", "tenantUuid": "platform", "projectId": "p", "ownerUserId": 1, "createdAtEpoch": 1_800_000_000.0, "expiresAtEpoch": 1_900_000_000.0, }}, "grants": {}, "history": []} with get_session() as session: session.add(ApprovalRequestRecord( confirm_id="conflict-confirm", tenant_uuid="platform", project_id="p", owner_user_id=1, action="different.action", power="P2", params_hash="expected-hash", status="PENDING", approval_step=0, required_approvals=1, created_at_epoch=1_800_000_000.0, expires_at_epoch=1_900_000_000.0, revision=0, payload=copy.deepcopy(data["pending"]["conflict-confirm"]), )) session.commit() before = _snapshot_db() with pytest.raises((RuntimeError, ValueError)): migrate_file_to_db(str(_write_payload(tmp_path, data))) assert _snapshot_db() == before def test_database_state_may_advance_but_migration_never_regresses_it(database_backend, tmp_path: Path): path, raw_grant, record = _real_file_p3_grant(tmp_path) migrate_file_to_db(str(path)) assert database_backend.consume_grant( raw_grant, confirm_id=record["confirmId"], action=record["action"], params_hash=record["paramsHash"], allowed=lambda _record: True, decided_by={"userId": 6704}, now_epoch=1_800_000_030.0, ) with get_session() as session: before = session.get(ApprovalGrantRecord, token_digest(raw_grant)) expected = (before.status, before.consumed_at_epoch, before.revision) migrate_file_to_db(str(path)) with get_session() as session: after = session.get(ApprovalGrantRecord, token_digest(raw_grant)) assert (after.status, after.consumed_at_epoch, after.revision) == expected assert after.status == "CONSUMED" def test_consistent_legacy_raw_token_row_is_repaired_atomically(database_backend, tmp_path: Path): raw = "legacy-raw-token-round67" source = _terminal_grant_source(raw) _seed_terminal_request_and_grant(source, raw) migrate_file_to_db(str(_write_payload(tmp_path, source))) with get_session() as session: assert session.get(ApprovalGrantRecord, raw) is None repaired = session.get(ApprovalGrantRecord, token_digest(raw)) assert repaired is not None assert repaired.action == "mes.dispatch" grant = next(iter(source["grants"].values())) kwargs = { "confirm_id": grant["confirmId"], "action": grant["action"], "params_hash": grant["paramsHash"], "allowed": lambda _record: True, "decided_by": {"userId": 6704}, "now_epoch": 1_800_100_100.0, } assert database_backend.consume_grant(raw, **kwargs) is True assert database_backend.consume_grant(raw, **kwargs) is False def test_legacy_raw_token_payload_conflict_fails_without_changes( database_backend, tmp_path: Path, ): raw = "legacy-payload-conflict-round67" source = _terminal_grant_source(raw) _seed_terminal_request_and_grant(source, raw) with get_session() as session: row = session.get(ApprovalGrantRecord, raw) assert row is not None row.payload = {**copy.deepcopy(row.payload), "action": "forged.action"} session.commit() before = _snapshot_db() with pytest.raises(RuntimeError, match="payload immutable conflict"): migrate_file_to_db(str(_write_payload(tmp_path, source))) assert _snapshot_db() == before def test_inconsistent_legacy_raw_token_row_fails_without_changes(database_backend, tmp_path: Path): raw = "legacy-raw-token-conflict" source = _terminal_grant_source(raw) _seed_terminal_request_and_grant(source, raw, grant_action="different.action") before = _snapshot_db() with pytest.raises((RuntimeError, ValueError)): migrate_file_to_db(str(_write_payload(tmp_path, source))) assert _snapshot_db() == before def test_canonical_event_digest_preserves_same_second_near_events_and_is_idempotent(database_backend, tmp_path: Path): pending = { "confirmId": "event-confirm", "sessionId": "event-session", "action": "mes.dispatch", "power": "P3", "params": {}, "paramsHash": "event-hash", "tenantUuid": "platform", "projectId": "event-project", "ownerUserId": 1, "requiredApprovals": 2, "createdAtEpoch": 1_800_000_000.0, "expiresAtEpoch": 1_900_000_000.0, } common = { "confirmId": "event-confirm", "action": "mes.dispatch", "power": "P3", "status": "SOD_DENIED", "tenantUuid": "platform", "projectId": "event-project", "ownerUserId": 1, "decidedAtEpoch": 1_800_000_010.0, } events = [ {**common, "decidedBy": {"userId": 2}, "note": "first", "detail": {"n": 1}}, {**common, "decidedBy": {"userId": 3}, "note": "second", "detail": {"n": 2}}, ] path = _write_payload(tmp_path, { "schemaVersion": 1, "pending": {"event-confirm": pending}, "grants": {}, "history": events, }) migrate_file_to_db(str(path)) migrate_file_to_db(str(path)) with get_session() as session: rows = session.execute(select(ApprovalEventRecord).order_by(ApprovalEventRecord.id)).scalars().all() assert len(rows) == 2 assert {row.payload["note"] for row in rows} == {"first", "second"} assert {row.payload["decidedBy"]["userId"] for row in rows} == {2, 3} def _seed_unrelated_target() -> None: with get_session() as session: session.add(ApprovalRequestRecord( confirm_id="existing-target", tenant_uuid="platform", project_id="existing", owner_user_id=1, action="existing.action", power="P2", params_hash="existing-hash", status="PENDING", approval_step=0, required_approvals=1, created_at_epoch=1_800_000_000.0, expires_at_epoch=1_900_000_000.0, revision=0, payload={"confirmId": "existing-target", "action": "existing.action"}, )) session.add(ApprovalEventRecord( confirm_id="existing-target", tenant_uuid="platform", project_id="existing", owner_user_id=1, action="existing.action", power="P2", status="STAGED", decided_at_epoch=1_800_000_001.0, payload={"confirmId": "existing-target", "action": "existing.action", "status": "STAGED"}, )) session.commit() def test_late_event_construction_failure_rolls_back_all_tables(database_backend, tmp_path: Path, monkeypatch: pytest.MonkeyPatch): _seed_unrelated_target() before = _snapshot_db() source = _terminal_grant_source("late-construction-token") path = _write_payload(tmp_path, source) def explode_event(*_args, **_kwargs): raise RuntimeError("injected event construction failure") monkeypatch.setattr(ApprovalEventRecord, "__init__", explode_event) with pytest.raises(RuntimeError): migrate_file_to_db(str(path)) assert _snapshot_db() == before def test_flush_failure_rolls_back_all_tables_and_existing_target(database_backend, tmp_path: Path, monkeypatch: pytest.MonkeyPatch): _seed_unrelated_target() before = _snapshot_db() path = _write_payload(tmp_path, _terminal_grant_source("flush-failure-token")) original_flush = Session.flush def fail_flush(self, *_args, **_kwargs): if self.new: raise RuntimeError("injected flush failure") return original_flush(self, *_args, **_kwargs) monkeypatch.setattr(Session, "flush", fail_flush) with pytest.raises(RuntimeError): migrate_file_to_db(str(path)) monkeypatch.setattr(Session, "flush", original_flush) assert _snapshot_db() == before def test_source_digest_drift_after_flush_rolls_back(database_backend, tmp_path: Path, monkeypatch: pytest.MonkeyPatch): _seed_unrelated_target() before = _snapshot_db() path = _write_payload(tmp_path, _terminal_grant_source("source-drift-token")) original_flush = Session.flush mutated = False def drift_then_flush(self, *_args, **_kwargs): nonlocal mutated if self.new and not mutated: mutated = True changed = json.loads(path.read_text(encoding="utf-8")) changed["history"][0]["note"] = "changed-after-load" path.write_text(json.dumps(changed, ensure_ascii=False), encoding="utf-8") return original_flush(self, *_args, **_kwargs) monkeypatch.setattr(Session, "flush", drift_then_flush) with pytest.raises((RuntimeError, ValueError)): migrate_file_to_db(str(path)) monkeypatch.setattr(Session, "flush", original_flush) assert mutated is True assert _snapshot_db() == before def test_migration_holds_file_backend_lock_through_database_flush(database_backend, tmp_path: Path, monkeypatch: pytest.MonkeyPatch): path = _write_payload(tmp_path, _terminal_grant_source("lock-token")) writer = ApprovalStore(str(path)) original_flush = Session.flush flush_entered = threading.Event() release_flush = threading.Event() writer_started = threading.Event() writer_done = threading.Event() migration_errors: list[BaseException] = [] writer_results: list[bool] = [] def blocked_flush(self, *_args, **_kwargs): if self.new and not flush_entered.is_set(): flush_entered.set() if not release_flush.wait(5): raise RuntimeError("test did not release migration flush") return original_flush(self, *_args, **_kwargs) monkeypatch.setattr(Session, "flush", blocked_flush) def migrate() -> None: try: migrate_file_to_db(str(path)) except BaseException as exc: # noqa: BLE001 - captured for test-thread assertion migration_errors.append(exc) def write_file() -> None: writer_started.set() writer_results.append(writer.stage({ "confirmId": "writer-after-migration", "action": "schedule.publish", "power": "P2", "paramsHash": "writer-hash", "tenantUuid": "platform", "projectId": "terminal-project", "createdAtEpoch": 1_800_200_000.0, "expiresAtEpoch": 1_900_200_000.0, }, now_epoch=1_800_200_000.0)) writer_done.set() migration_thread = threading.Thread(target=migrate, daemon=True) writer_thread = threading.Thread(target=write_file, daemon=True) migration_thread.start() assert flush_entered.wait(5), "migration never reached database flush" writer_thread.start() assert writer_started.wait(2) blocked_by_same_lock = not writer_done.wait(0.25) release_flush.set() migration_thread.join(5) writer_thread.join(5) monkeypatch.setattr(Session, "flush", original_flush) assert blocked_by_same_lock, "file writer was not blocked by the migration lock" assert not migration_thread.is_alive() assert not writer_thread.is_alive() assert migration_errors == [] assert writer_results == [True]