feat(prune): R3 guarded deletion — confirm-to-delete + panel button
Extend BANK_PRUNE_FOLDER from report-only to dry-run→confirm-with-manifest→ delete exactly the pure-core orphan set (fresh recompute, stale entries skipped). Windows routes to Recycle Bin (SHFileOperation+FOF_ALLOWUNDO), else unlink. New pure prune_button module + footer button dispatching the action id. No ext-state write, no undo point (deletion is not REAPER-undoable).
This commit is contained in:
@@ -324,6 +324,65 @@ static void testReportEmptyOrphanSet() {
|
||||
CHECK(!r.truncated);
|
||||
}
|
||||
|
||||
// --- pruneDeletePlan (R3 staleness guard) ------------------------------------
|
||||
//
|
||||
// plan = confirmed ∩ freshOrphans, in confirmed order. Guards BOTH directions so
|
||||
// "what was shown is what is deleted" holds after a recompute at delete time.
|
||||
|
||||
// No change between confirm and delete: the plan is the confirmed set exactly, in order.
|
||||
static void testDeletePlanStableEqualsConfirmed() {
|
||||
const std::vector<std::string> confirmed{"b/a.wav", "b/b.wav", "b/c.wav"};
|
||||
const std::vector<std::string> fresh{"b/a.wav", "b/b.wav", "b/c.wav"};
|
||||
const std::vector<std::string> plan = pruneDeletePlan(confirmed, fresh);
|
||||
CHECK((plan == confirmed)); // exact set AND order
|
||||
}
|
||||
|
||||
// A confirmed file that VANISHED (gone from fresh present -> not a fresh orphan) is a
|
||||
// skip: it drops out of the plan. Would FAIL if the plan ignored freshOrphans.
|
||||
static void testDeletePlanSkipsVanishedFile() {
|
||||
const std::vector<std::string> confirmed{"b/a.wav", "b/gone.wav", "b/c.wav"};
|
||||
const std::vector<std::string> fresh{"b/a.wav", "b/c.wav"}; // gone.wav disappeared
|
||||
const std::vector<std::string> plan = pruneDeletePlan(confirmed, fresh);
|
||||
CHECK((plan == std::vector<std::string>{"b/a.wav", "b/c.wav"}));
|
||||
CHECK(!contains(plan, "b/gone.wav"));
|
||||
}
|
||||
|
||||
// A confirmed file that became REFERENCED between confirm and delete drops OUT of the
|
||||
// fresh orphan set (pruneOrphans excludes it), so the plan skips it — never deletes a
|
||||
// now-referenced file. Modelled here as its absence from `fresh`. Load-bearing safety.
|
||||
static void testDeletePlanSkipsNowReferencedFile() {
|
||||
const std::vector<std::string> confirmed{"b/x.wav", "b/y.wav"};
|
||||
const std::vector<std::string> fresh{"b/x.wav"}; // y.wav now referenced -> not a fresh orphan
|
||||
const std::vector<std::string> plan = pruneDeletePlan(confirmed, fresh);
|
||||
CHECK((plan == std::vector<std::string>{"b/x.wav"}));
|
||||
}
|
||||
|
||||
// A NEWLY-APPEARED orphan (in fresh, NOT in confirmed) is NEVER swept: it was not shown,
|
||||
// so it must not be deleted without its own confirm. Would FAIL if plan = fresh.
|
||||
static void testDeletePlanNeverSweepsUnconfirmed() {
|
||||
const std::vector<std::string> confirmed{"b/a.wav"};
|
||||
const std::vector<std::string> fresh{"b/a.wav", "b/new_orphan.wav"};
|
||||
const std::vector<std::string> plan = pruneDeletePlan(confirmed, fresh);
|
||||
CHECK((plan == std::vector<std::string>{"b/a.wav"}));
|
||||
CHECK(!contains(plan, "b/new_orphan.wav"));
|
||||
}
|
||||
|
||||
// Empty inputs: an empty confirmed (nothing shown) -> empty plan regardless of fresh; an
|
||||
// empty fresh (everything went stale) -> empty plan (all skipped).
|
||||
static void testDeletePlanEmptyInputs() {
|
||||
CHECK(pruneDeletePlan({}, {"b/a.wav"}).empty());
|
||||
CHECK(pruneDeletePlan({"b/a.wav"}, {}).empty());
|
||||
CHECK(pruneDeletePlan({}, {}).empty());
|
||||
}
|
||||
|
||||
// Duplicate spellings in confirmed are de-duplicated in the plan (mirror of pruneOrphans).
|
||||
static void testDeletePlanDeduplicatesConfirmed() {
|
||||
const std::vector<std::string> confirmed{"b/a.wav", "b/a.wav", "b/b.wav"};
|
||||
const std::vector<std::string> fresh{"b/a.wav", "b/b.wav"};
|
||||
const std::vector<std::string> plan = pruneDeletePlan(confirmed, fresh);
|
||||
CHECK((plan == std::vector<std::string>{"b/a.wav", "b/b.wav"}));
|
||||
}
|
||||
|
||||
int main() {
|
||||
testNullTestAllReferenced();
|
||||
testFormulaMixedPopulations();
|
||||
@@ -346,6 +405,12 @@ int main() {
|
||||
testReportTruncatesListButKeepsExactCountAndSize();
|
||||
testReportUncappedWhenCapZero();
|
||||
testReportEmptyOrphanSet();
|
||||
testDeletePlanStableEqualsConfirmed();
|
||||
testDeletePlanSkipsVanishedFile();
|
||||
testDeletePlanSkipsNowReferencedFile();
|
||||
testDeletePlanNeverSweepsUnconfirmed();
|
||||
testDeletePlanEmptyInputs();
|
||||
testDeletePlanDeduplicatesConfirmed();
|
||||
|
||||
if (g_fail == 0) std::printf("prune_reconcile_tests: ALL PASS\n");
|
||||
else std::printf("prune_reconcile_tests: %d FAILURE(S)\n", g_fail);
|
||||
|
||||
Reference in New Issue
Block a user