From d92065c14889783e0228a4eaa382a6ddac60bb6b Mon Sep 17 00:00:00 2001 From: Amantux Date: Mon, 20 Jul 2026 16:30:53 +0000 Subject: [PATCH] Test catalog rebuild survives a missing tape (roadmap #11) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The review asked for CI proving a blank DB rebuilds from tape "including partially corrupt and missing tapes". The blank-DB rebuild (test_lost_db_recovery_full_scenario) and corrupt/incomplete-metadata cases (missing/invalid manifest, missing shard) were already covered and CI-run; the one uncovered clause was a physically MISSING tape. Add test_rebuild_with_a_missing_tape_recovers_available_subset: seed two tapes, wipe the DB, then rebuild from only ONE — asserts the available tape's files are recovered, the missing tape's are gracefully left unrecovered (no fabrication, no crash), and the run still completes. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_012K5Zjc2bggcyJtFbz2D4Pw --- tests/unit/test_catalog_rebuild_worker.py | 28 +++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/unit/test_catalog_rebuild_worker.py b/tests/unit/test_catalog_rebuild_worker.py index 86d3928..d6ac5cb 100644 --- a/tests/unit/test_catalog_rebuild_worker.py +++ b/tests/unit/test_catalog_rebuild_worker.py @@ -454,3 +454,31 @@ def test_dry_run_first_raises_when_not_safe_to_enqueue(rebuild_env: dict[str, ob assert str(exc_info.value) == SAFE_REBUILD_PREFLIGHT_ERROR assert barcode not in str(exc_info.value) + + +def test_rebuild_with_a_missing_tape_recovers_available_subset( + rebuild_env: dict[str, object], +) -> None: + """DR with a MISSING tape (roadmap #11): rebuilding from only the tapes actually + available recovers their files and gracefully leaves the absent tape's files + unrecovered — no crash, run still completes.""" + barcodes = rebuild_env["context"].library.get_all_barcodes()[:2] + seeded_one = _seed_tape(rebuild_env, barcodes[0]) + seeded_two = _seed_tape(rebuild_env, barcodes[1]) + _populate_catalog_from_seed(rebuild_env, seeded_one) + _populate_catalog_from_seed(rebuild_env, seeded_two) + + session = rebuild_env["context"].catalog.session + session.query(PathMappingRow).delete() + session.query(NasFileRecordRow).delete() + session.query(NasDatasetRow).delete() + session.commit() + + # Only tape 0 is available for the rebuild; tape 1 is "missing". + result = rebuild_env["worker"].auto_plan_and_execute([barcodes[0]], triggered_by="operator") + + assert result.status is RebuildRunStatus.COMPLETED + assert result.files_recovered == 2 # tape 0's files only + assert rebuild_env["context"].catalog.get_nas_file_record(f"{barcodes[0]}-file-0") is not None + # The missing tape's records remain unrecovered — no silent fabrication, no crash. + assert rebuild_env["context"].catalog.get_nas_file_record(f"{barcodes[1]}-file-1") is None