feat(banks): bindable multi-bank action family — create/rename/delete/evacuate/activate/move/copy + full-height toggles (B3)
This commit is contained in:
@@ -405,6 +405,60 @@ static void testLoadMalformedBanksDegradesWithoutLegacyFallback() {
|
||||
CHECK(loaded.pool().index.query("id-stale") == nullptr); // legacy did not leak
|
||||
}
|
||||
|
||||
// --- B3: active-bank cycle ordering (pure free function) -------------------
|
||||
// nextBankId(orderedIds, current) is the pure decision behind the "cycle active
|
||||
// bank" action: given the book's ordered bank ids (pool-first) + the current active
|
||||
// id, return the next id in ordinal order, wrapping pool -> named -> ... -> pool.
|
||||
|
||||
static void testCycleOrderingWrapAround() {
|
||||
// pool -> drums -> hits -> (wrap) pool. Exercises every step + the wrap.
|
||||
const std::vector<std::string> ids = {kPoolBankId, "drums", "hits"};
|
||||
CHECK(nextBankId(ids, kPoolBankId) == "drums");
|
||||
CHECK(nextBankId(ids, "drums") == "hits");
|
||||
CHECK(nextBankId(ids, "hits") == std::string(kPoolBankId)); // wrap past the last
|
||||
}
|
||||
|
||||
static void testCyclePoolOnlyStaysPool() {
|
||||
// A pool-only book (no named banks) cycles to itself — the single id wraps to
|
||||
// itself. The action becomes a no-op activation, which is correct.
|
||||
const std::vector<std::string> ids = {kPoolBankId};
|
||||
CHECK(nextBankId(ids, kPoolBankId) == std::string(kPoolBankId));
|
||||
}
|
||||
|
||||
static void testCycleUnknownActiveResolvesToFirst() {
|
||||
// A stale/unknown active id (e.g. the active bank was just deleted and the
|
||||
// ordered list already dropped it) resolves to the first id — a sane home to jump
|
||||
// to rather than "" — matching nextModeId's fallback.
|
||||
const std::vector<std::string> ids = {kPoolBankId, "drums"};
|
||||
CHECK(nextBankId(ids, "ghost") == std::string(kPoolBankId));
|
||||
}
|
||||
|
||||
static void testCycleEmptyListYieldsEmpty() {
|
||||
// Degenerate guard: an empty list has nothing to cycle to. (A real BankBook always
|
||||
// seeds the pool, so this cannot arise from the book — but the pure helper must not
|
||||
// index into an empty vector.)
|
||||
const std::vector<std::string> ids;
|
||||
CHECK(nextBankId(ids, kPoolBankId).empty());
|
||||
}
|
||||
|
||||
static void testCycleMatchesBookOrdinalOrder() {
|
||||
// Integration-flavoured but still pure: drive the cycle off a real book's banks()
|
||||
// order and confirm one full loop lands back on the pool, activating each bank in
|
||||
// ordinal order. This is exactly what the action does (build ids from banks(),
|
||||
// call nextBankId, setActiveBank).
|
||||
BankBook book;
|
||||
CHECK(book.createBank("a", "A"));
|
||||
CHECK(book.createBank("b", "B")); // ordinals: pool 0, a 1, b 2
|
||||
|
||||
std::vector<std::string> ids;
|
||||
for (const Bank& bk : book.banks()) ids.push_back(bk.id);
|
||||
|
||||
std::string cur = book.activeBankId(); // pool
|
||||
cur = nextBankId(ids, cur); CHECK(cur == "a");
|
||||
cur = nextBankId(ids, cur); CHECK(cur == "b");
|
||||
cur = nextBankId(ids, cur); CHECK(cur == std::string(kPoolBankId)); // full loop
|
||||
}
|
||||
|
||||
static void testActiveBankResolveAfterCorruptPersistedId() {
|
||||
// A book blob whose activeBank names no bank resolves to the pool (defensive).
|
||||
const char* json =
|
||||
@@ -435,6 +489,11 @@ int main() {
|
||||
testLoadEmptyWhenNeither();
|
||||
testLoadMalformedBanksDegradesWithoutLegacyFallback();
|
||||
testActiveBankResolveAfterCorruptPersistedId();
|
||||
testCycleOrderingWrapAround();
|
||||
testCyclePoolOnlyStaysPool();
|
||||
testCycleUnknownActiveResolvesToFirst();
|
||||
testCycleEmptyListYieldsEmpty();
|
||||
testCycleMatchesBookOrdinalOrder();
|
||||
|
||||
if (g_fail == 0) std::printf("All tests passed.\n");
|
||||
return g_fail ? 1 : 0;
|
||||
|
||||
Reference in New Issue
Block a user