Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions reclaim.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down
30 changes: 30 additions & 0 deletions tests/test_routing_reclaim.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()