B5: sample-remove verb — index-only drop, this-bank scope, confirm-on-last-reference
This commit is contained in:
@@ -638,6 +638,124 @@ static void testDeserializeNamedBankCollidingWithPoolIsDisambiguated() {
|
||||
if (back2) CHECK(back2->serialize() == back->serialize());
|
||||
}
|
||||
|
||||
// --- B5: sample-remove (this-bank + latent all-banks) + last-reference query ------
|
||||
//
|
||||
// removeSample drops a Sample's index entry (index-only, non-destructive to the file).
|
||||
// ThisBank (default, surfaced) drops from one named source bank; AllBanks (latent seam)
|
||||
// purges the id book-wide. hashReferencedElsewhere backs the confirm-on-last-reference
|
||||
// guardrail: does any OTHER bank still hold the content hash?
|
||||
|
||||
static void testRemoveDropsTargetEntry() {
|
||||
BankBook book;
|
||||
CHECK(book.createBank("drums", "Drums"));
|
||||
CHECK(book.bank("drums")->index.add(sampleWith("kick")) == AddResult::Added);
|
||||
CHECK(book.bank("drums")->index.add(sampleWith("snare")) == AddResult::Added);
|
||||
|
||||
// Remove the kick from drums: dropped from the target bank, the sibling survives.
|
||||
CHECK(book.removeSample("id-kick", "drums") == RemoveResult::Removed);
|
||||
CHECK(book.bank("drums")->index.query("id-kick") == nullptr); // dropped
|
||||
CHECK(book.bank("drums")->index.query("id-snare") != nullptr); // sibling kept
|
||||
CHECK(book.bank("drums")->index.size() == 1);
|
||||
}
|
||||
|
||||
static void testRemoveThisBankLeavesSameHashInAnotherBank() {
|
||||
// Copy a sample into two banks (same hash in both), then remove from one under the
|
||||
// default this-bank scope: the OTHER bank's entry survives — no cross-bank cascade.
|
||||
BankBook book;
|
||||
CHECK(book.createBank("drums", "Drums"));
|
||||
CHECK(book.pool().index.add(sampleWith("clap", "clap-hash")) == AddResult::Added);
|
||||
CHECK(book.copySample("id-clap", kPoolBankId, "drums") == TransferResult::Copied);
|
||||
|
||||
CHECK(book.removeSample("id-clap", kPoolBankId, RemoveScope::ThisBank) ==
|
||||
RemoveResult::Removed);
|
||||
CHECK(book.pool().index.query("id-clap") == nullptr); // removed from pool
|
||||
CHECK(book.bank("drums")->index.query("id-clap") != nullptr); // drums copy survives
|
||||
CHECK(book.bank("drums")->index.findByHash("clap-hash") != nullptr);
|
||||
}
|
||||
|
||||
static void testRemoveFromPoolAllowedContainerPrivilegesHold() {
|
||||
// Pool CONTENTS are removable (the pool must not be a roach-motel); the pool
|
||||
// CONTAINER privileges (un-deletable / un-renamable / un-evacuable) are untouched.
|
||||
BankBook book;
|
||||
CHECK(book.pool().index.add(sampleWith("loop")) == AddResult::Added);
|
||||
|
||||
CHECK(book.removeSample("id-loop", kPoolBankId) == RemoveResult::Removed);
|
||||
CHECK(book.pool().index.empty()); // content removed
|
||||
|
||||
// Container privileges still enforced.
|
||||
CHECK(!book.deleteBank(kPoolBankId));
|
||||
CHECK(!book.renameBank(kPoolBankId, "NotPool"));
|
||||
CHECK(!book.evacuate(kPoolBankId));
|
||||
CHECK(book.size() == 1);
|
||||
CHECK(book.pool().displayName == std::string(kPoolBankName));
|
||||
}
|
||||
|
||||
static void testRemoveRejectionsNoMutation() {
|
||||
BankBook book;
|
||||
CHECK(book.createBank("drums", "Drums"));
|
||||
CHECK(book.bank("drums")->index.add(sampleWith("kick")) == AddResult::Added);
|
||||
|
||||
// Unknown bank → honest rejection, no mutation.
|
||||
CHECK(book.removeSample("id-kick", "ghost") == RemoveResult::RejectedUnknownBank);
|
||||
CHECK(book.bank("drums")->index.query("id-kick") != nullptr); // untouched
|
||||
|
||||
// Absent sample (right bank, wrong id) → honest rejection, no mutation.
|
||||
CHECK(book.removeSample("id-missing", "drums") == RemoveResult::RejectedSampleAbsent);
|
||||
CHECK(book.bank("drums")->index.size() == 1);
|
||||
}
|
||||
|
||||
static void testHashReferencedElsewhere() {
|
||||
BankBook book;
|
||||
CHECK(book.createBank("drums", "Drums"));
|
||||
CHECK(book.createBank("hits", "Hits"));
|
||||
|
||||
// Same-bank-only: the hash lives ONLY in drums → not referenced elsewhere (true
|
||||
// last-reference; removing from drums would orphan the file).
|
||||
CHECK(book.bank("drums")->index.add(sampleWith("solo", "solo-hash")) == AddResult::Added);
|
||||
CHECK(!book.hashReferencedElsewhere("solo-hash", "drums"));
|
||||
|
||||
// Copied-to-two-banks: the hash lives in drums AND hits → referenced elsewhere from
|
||||
// either vantage (removing from one leaves the other's reference intact).
|
||||
CHECK(book.bank("drums")->index.add(sampleWith("dup-d", "dup-hash")) == AddResult::Added);
|
||||
CHECK(book.bank("hits")->index.add(sampleWith("dup-h", "dup-hash")) == AddResult::Added);
|
||||
CHECK(book.hashReferencedElsewhere("dup-hash", "drums")); // hits still holds it
|
||||
CHECK(book.hashReferencedElsewhere("dup-hash", "hits")); // drums still holds it
|
||||
|
||||
// A hash present in NO bank is referenced nowhere.
|
||||
CHECK(!book.hashReferencedElsewhere("absent-hash", "drums"));
|
||||
|
||||
// An empty hash never matches (mirrors findByHash) → reads as not-referenced-else,
|
||||
// the safe confirm-eliciting direction for an unhashed sample.
|
||||
CHECK(book.pool().index.add(sampleWith("nohash", "")) == AddResult::Added);
|
||||
CHECK(!book.hashReferencedElsewhere("", "drums"));
|
||||
}
|
||||
|
||||
static void testRemoveAllBanksLatentScope() {
|
||||
// The latent all-banks seam (fork R-A): unsurfaced in the UI but live at the model
|
||||
// level. Purges the id from EVERY bank that holds it in one act; fromBankId ignored.
|
||||
BankBook book;
|
||||
CHECK(book.createBank("drums", "Drums"));
|
||||
CHECK(book.createBank("hits", "Hits"));
|
||||
CHECK(book.pool().index.add(sampleWith("clap", "clap-hash")) == AddResult::Added);
|
||||
CHECK(book.copySample("id-clap", kPoolBankId, "drums") == TransferResult::Copied);
|
||||
CHECK(book.copySample("id-clap", kPoolBankId, "hits") == TransferResult::Copied);
|
||||
// The id now lives in all three banks.
|
||||
CHECK(book.pool().index.query("id-clap") != nullptr);
|
||||
CHECK(book.bank("drums")->index.query("id-clap") != nullptr);
|
||||
CHECK(book.bank("hits")->index.query("id-clap") != nullptr);
|
||||
|
||||
// AllBanks purge — fromBankId is ignored (pass a nonexistent bank to prove it).
|
||||
CHECK(book.removeSample("id-clap", "ignored-bank", RemoveScope::AllBanks) ==
|
||||
RemoveResult::Removed);
|
||||
CHECK(book.pool().index.query("id-clap") == nullptr);
|
||||
CHECK(book.bank("drums")->index.query("id-clap") == nullptr);
|
||||
CHECK(book.bank("hits")->index.query("id-clap") == nullptr);
|
||||
|
||||
// A second all-banks purge of the now-absent id is an honest no-op rejection.
|
||||
CHECK(book.removeSample("id-clap", "ignored-bank", RemoveScope::AllBanks) ==
|
||||
RemoveResult::RejectedSampleAbsent);
|
||||
}
|
||||
|
||||
int main() {
|
||||
testPoolSeededAndDefaults();
|
||||
testPoolPrivileges();
|
||||
@@ -667,6 +785,12 @@ int main() {
|
||||
testDeserializeCoalescesDuplicateFoldedNames();
|
||||
testDeserializeCoalescesMultipleCollisions();
|
||||
testDeserializeNamedBankCollidingWithPoolIsDisambiguated();
|
||||
testRemoveDropsTargetEntry();
|
||||
testRemoveThisBankLeavesSameHashInAnotherBank();
|
||||
testRemoveFromPoolAllowedContainerPrivilegesHold();
|
||||
testRemoveRejectionsNoMutation();
|
||||
testHashReferencedElsewhere();
|
||||
testRemoveAllBanksLatentScope();
|
||||
|
||||
if (g_fail == 0) std::printf("All tests passed.\n");
|
||||
return g_fail ? 1 : 0;
|
||||
|
||||
Reference in New Issue
Block a user