diff --git a/reclaim.py b/reclaim.py index 2ccda44..d8f1a00 100644 --- a/reclaim.py +++ b/reclaim.py @@ -761,7 +761,7 @@ def verify_target(manifest, holds, canonical_roots=(), allowlist=None, opt_in=Fa if o.get("fortress_tar"): all_tars.append(o["fortress_tar"]) - held = any((h == project) or (source_folder and h in source_folder) for h in holds) + hold = hold_entry_for(holds, project, source_folder) # INV-E: re-prove the archive's recorded exclusions are still git-COVERED. The # manifest carries the same `exclusion` block a log does, so recheck_exclusions @@ -842,9 +842,13 @@ def verify_target(manifest, holds, canonical_roots=(), allowlist=None, opt_in=Fa verdict = DRIFTED reasons.append(f"live bytes ({live_bytes}) != archived bytes ({archived_bytes}) " f"— same count, content changed") - elif held: - verdict = HOLD - reasons.append("on holds list") + elif hold is not None: + # Typed hold tiering (docs/HOLDS_POLICY.md), identical to verify(): business -> + # HOLD; promote-pending/superseded -> HOLD until their evidence gate, then + # AWAITING_APPROVAL. (Was flat string matching, which raised TypeError on a typed + # dict hold — routed targets now tier correctly under a JSON holds file.) + verdict, hreason = classify_hold(hold, source_folder, live) + reasons.append(hreason) elif opt_in and not on_allowlist(project, source_folder, allowlist): verdict = KEEP reasons.append("not on reclaim allowlist (opt-in mode) — kept") diff --git a/tests/test_routing_reclaim.py b/tests/test_routing_reclaim.py index ffe6998..63ffd71 100644 --- a/tests/test_routing_reclaim.py +++ b/tests/test_routing_reclaim.py @@ -196,5 +196,35 @@ def test_aborts_when_any_tar_missing(self): self.assertTrue(os.path.isdir(folder)) # NOT deleted +class TestVerifyTargetTypedHolds(VerifyTargetBase): + """A routed target under a TYPED (dict) hold must tier correctly, not crash — the + coupling that surfaced when typed holds (PR #22) + verify_target (PR #21) both landed: + verify_target used to do string matching that raised TypeError on a dict hold.""" + def test_business_dict_hold_is_HOLD(self): + with tempfile.TemporaryDirectory() as d: + _, m = build_target(d) + r = reclaim.verify_target(m, [{"match": "proj", "reason": "business"}]) + self.assertEqual(r["verdict"], reclaim.HOLD) + + def test_promote_pending_no_receipt_is_HOLD(self): + with tempfile.TemporaryDirectory() as d: + _, m = build_target(d) + r = reclaim.verify_target( + m, [{"match": "proj", "reason": "promote-pending", "receipt_dir": "/no"}]) + self.assertEqual(r["verdict"], reclaim.HOLD) + + def test_promote_pending_valid_receipt_awaits_approval(self): + with tempfile.TemporaryDirectory() as d: + src, m = build_target(d) # 4 live files, future run -> otherwise SAFE + rd = os.path.join(d, "receipts") + os.makedirs(rd) + with open(os.path.join(rd, "r.json"), "w") as fh: + json.dump({"source_folder": src, "file_count": 4, "all_present": True, + "verified_at": "2099-01-01T00:00:00", "tool": "test"}, fh) + r = reclaim.verify_target( + m, [{"match": "proj", "reason": "promote-pending", "receipt_dir": rd}]) + self.assertEqual(r["verdict"], reclaim.AWAITING_APPROVAL) + + if __name__ == "__main__": unittest.main()