test(sidebar): compress test suite via shared timer helpers and parameterized tables

Extract makeTimerBed() helpers in wholeBlobSync.shared.test.mjs and
channelSectionsSync.test.mjs, eliminating 18-22 line inline timer
factory blocks repeated across 9 tests.

Collapse the A-succeeds/A-fails CAS variants in mergeLaneSync.shared
and the 4 bootstrap seed-guard tests in both mergeLaneSync.shared and
wholeBlobSync.shared into parameterized for-loops.

No behavioral coverage removed; only structural duplication eliminated.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
This commit is contained in:
Duncan
2026-08-31 13:34:07 -04:00
parent aafe2a51a8
commit f0d34dc608
12 changed files with 2010 additions and 3187 deletions

View File

@@ -1,32 +1,54 @@
import {
boundMuteStore,
DEFAULT_STORE,
MAX_CHANNEL_MUTE_ENTRIES,
mergeStores,
mutedChannelIdsFromStore,
parseMutePayload,
readChannelMutesStore,
storageKey,
writeChannelMutesStore,
} from "./channelMutesStorage.ts";
import { normalizeRelayUrl } from "@/shared/lib/normalizeRelayUrl";
import { runMergeLaneStorageSuite } from "./mergeLaneStorage.shared.test.mjs";
// Compact mutes lane adapter contract.
// Full merge-lane storage invariants are covered by mergeLaneStorage.shared.test.mjs.
// This file proves only the mutes-specific wiring: correct value field name,
// storage key prefix, and id projection — catches copy/paste field miswiring.
runMergeLaneStorageSuite({
label: "mutes",
storageKeyPrefix: "buzz-channel-mutes.v1",
MAX_ENTRIES: MAX_CHANNEL_MUTE_ENTRIES,
DEFAULT_STORE,
parsePayload: parseMutePayload,
makeEntry: (v, updatedAt, rev) => ({ muted: v, updatedAt, rev }),
entryValueField: "muted",
trueLabel: "mute",
falseLabel: "unmute",
boundStore: boundMuteStore,
mergeStores,
idsFromStore: mutedChannelIdsFromStore,
readStore: readChannelMutesStore,
writeStore: writeChannelMutesStore,
import assert from "node:assert/strict";
import test from "node:test";
import {
parseMutePayload,
mutedChannelIdsFromStore,
storageKey,
normalizeRelayUrl,
} from "./channelMutesStorage.ts";
test("mutes adapter: value field is 'muted', key prefix is buzz-channel-mutes.v1", () => {
const raw = {
version: 1,
channels: { a: { muted: true, updatedAt: 100, rev: 1 } },
};
const parsed = parseMutePayload(raw);
assert.ok(parsed !== null, "valid mutes payload parses");
assert.equal(parsed.channels.a.muted, true, "muted field preserved");
assert.equal(
storageKey("pk1"),
"buzz-channel-mutes.v1:pk1",
"storage key prefix",
);
});
test("mutes adapter: idsFromStore projects muted=true entries", () => {
const store = {
version: 1,
channels: {
a: { muted: true, updatedAt: 1, rev: 0 },
b: { muted: false, updatedAt: 2, rev: 0 },
},
};
const ids = mutedChannelIdsFromStore(store);
assert.ok(ids.has("a"), "muted channel in set");
assert.ok(!ids.has("b"), "unmuted channel excluded");
});
test("mutes adapter: wrong value field (starred) is rejected by parser", () => {
const starPayload = {
version: 1,
channels: { a: { starred: true, updatedAt: 100, rev: 1 } },
};
const result = parseMutePayload(starPayload);
assert.deepEqual(
result?.channels ?? {},
{},
"starred entry filtered as malformed",
);
});

View File

@@ -381,217 +381,13 @@ test("readChannelSectionsStore: scoped key takes precedence over legacy key afte
assert.deepEqual(result, newStore);
});
// Migration failure safety (Thufir r6 IMPORTANT): legacy data must be exposed
// to bootstrap ONLY once the legacy key is provably gone. If the scoped write
// throws, or the legacy delete does not take, the reader must roll back and
// return DEFAULT_STORE so no relay publishes legacy prefs while the legacy key
// still holds them (which would let a second relay import the same value).
test("readChannelSectionsStore: scoped-write failure returns DEFAULT and leaves neither relay able to seed the legacy value", () => {
const pubkey = "pk-migrate-writefail";
const relayA = "wss://relay-a-writefail.example.com";
const relayB = "wss://relay-b-writefail.example.com";
const legacy = makeStore({
sections: [makeSection({ id: "sw", name: "Legacy", order: 0 })],
assignments: {},
});
writeChannelSectionsStore(pubkey, legacy);
const scopedA = storageKey(pubkey, relayA);
const origSet = window.localStorage.setItem;
window.localStorage.setItem = (k, v) => {
if (k === scopedA) throw new Error("QuotaExceededError");
return origSet.call(window.localStorage, k, v);
};
try {
// Relay A cannot claim the legacy value — it must NOT be exposed to publish.
assert.deepEqual(readChannelSectionsStore(pubkey, relayA), DEFAULT_STORE);
// Rolled back: no partial scoped copy left behind for A.
assert.equal(window.localStorage.getItem(scopedA), null);
} finally {
window.localStorage.setItem = origSet;
}
// Legacy key survived (migration is retryable), and relay B likewise cannot
// be seeded — the value is claimed by whichever relay first deletes it.
assert.notEqual(window.localStorage.getItem(storageKey(pubkey)), null);
assert.deepEqual(readChannelSectionsStore(pubkey, relayB), legacy);
// Once B claims it, the legacy key is gone and A can no longer seed.
assert.equal(window.localStorage.getItem(storageKey(pubkey)), null);
assert.deepEqual(readChannelSectionsStore(pubkey, relayA), DEFAULT_STORE);
});
test("readChannelSectionsStore: a legacy delete that does not take rolls back and returns DEFAULT", () => {
const pubkey = "pk-migrate-delfail";
const relay = "wss://relay-delfail.example.com";
const legacy = makeStore({
sections: [makeSection({ id: "sd", name: "Legacy", order: 0 })],
assignments: {},
});
writeChannelSectionsStore(pubkey, legacy);
const legacyKey = storageKey(pubkey);
const scoped = storageKey(pubkey, relay);
const origRemove = window.localStorage.removeItem;
window.localStorage.removeItem = (k) => {
if (k === legacyKey) return; // silently no-op: delete "does not take"
return origRemove.call(window.localStorage, k);
};
try {
assert.deepEqual(readChannelSectionsStore(pubkey, relay), DEFAULT_STORE);
// Partial scoped copy rolled back so the migration stays retryable.
assert.equal(window.localStorage.getItem(scoped), null);
} finally {
window.localStorage.removeItem = origRemove;
}
// With the delete working again, the migration completes cleanly.
assert.deepEqual(readChannelSectionsStore(pubkey, relay), legacy);
assert.equal(window.localStorage.getItem(legacyKey), null);
});
test("readChannelSectionsStore: legacy delete + rollback both throw — every read returns DEFAULT until storage recovers, never seeds", () => {
const pubkey = "pk-migrate-delrollbackthrow";
const relay = "wss://relay-delrollbackthrow.example.com";
const legacy = makeStore({
sections: [makeSection({ id: "sr", name: "Legacy", order: 0 })],
assignments: {},
});
writeChannelSectionsStore(pubkey, legacy);
const legacyKey = storageKey(pubkey);
const scoped = storageKey(pubkey, relay);
const origRemove = window.localStorage.removeItem;
// removeItem throws globally: the legacy delete AND the scoped rollback both
// throw, so a scoped copy the migration wrote survives on disk.
window.localStorage.removeItem = () => {
throw new Error("SecurityError");
};
try {
// First read: scoped write took, legacy delete threw, rollback threw.
assert.deepEqual(readChannelSectionsStore(pubkey, relay), DEFAULT_STORE);
// A stuck scoped copy survives, but the reader must NOT expose it while the
// legacy key still holds importable data — the seam Thufir found.
assert.deepEqual(readChannelSectionsStore(pubkey, relay), DEFAULT_STORE);
// Legacy key is intact, so the claim stays retryable rather than lost.
assert.notEqual(window.localStorage.getItem(legacyKey), null);
} finally {
window.localStorage.removeItem = origRemove;
}
// Storage recovers: the next read finishes the claim from the stuck scoped
// copy, deletes the legacy key, and only then exposes the value.
assert.deepEqual(readChannelSectionsStore(pubkey, relay), legacy);
assert.equal(window.localStorage.getItem(legacyKey), null);
assert.notEqual(window.localStorage.getItem(scoped), null);
});
test("readChannelSectionsStore: legacy delete succeeds but the confirmation read throws — scoped copy retained, no data loss", () => {
const pubkey = "pk-migrate-confirmthrow";
const relay = "wss://relay-confirmthrow.example.com";
const legacy = makeStore({
sections: [makeSection({ id: "sc", name: "Legacy", order: 0 })],
assignments: {},
});
writeChannelSectionsStore(pubkey, legacy);
const legacyKey = storageKey(pubkey);
const scoped = storageKey(pubkey, relay);
const origGet = window.localStorage.getItem;
const origRemove = window.localStorage.removeItem;
let legacyDeleted = false;
let threwOnce = false;
window.localStorage.removeItem = (k) => {
if (k === legacyKey) legacyDeleted = true;
return origRemove.call(window.localStorage, k);
};
// Only the post-delete confirmation read of the legacy key throws, once.
window.localStorage.getItem = (k) => {
if (k === legacyKey && legacyDeleted && !threwOnce) {
threwOnce = true;
throw new Error("SecurityError");
}
return origGet.call(window.localStorage, k);
};
try {
// Legacy is already gone; the catch probe sees null and must NOT roll back
// the scoped copy — it is the only surviving copy. First read is DEFAULT.
assert.deepEqual(readChannelSectionsStore(pubkey, relay), DEFAULT_STORE);
assert.notEqual(window.localStorage.getItem(scoped), null);
} finally {
window.localStorage.getItem = origGet;
window.localStorage.removeItem = origRemove;
}
// Legacy gone + scoped retained → the next healthy read exposes the value.
assert.equal(window.localStorage.getItem(legacyKey), null);
assert.deepEqual(readChannelSectionsStore(pubkey, relay), legacy);
});
test("readChannelSectionsStore: legacy delete throws while the probe stays healthy — scoped rollback, DEFAULT, single future claimant", () => {
const pubkey = "pk-migrate-delthrow-probeok";
const relay = "wss://relay-delthrow-probeok.example.com";
const legacy = makeStore({
sections: [makeSection({ id: "st", name: "Legacy", order: 0 })],
assignments: {},
});
writeChannelSectionsStore(pubkey, legacy);
const legacyKey = storageKey(pubkey);
const scoped = storageKey(pubkey, relay);
const origRemove = window.localStorage.removeItem;
let thrown = false;
window.localStorage.removeItem = (k) => {
if (k === legacyKey && !thrown) {
thrown = true;
throw new Error("SecurityError");
}
return origRemove.call(window.localStorage, k);
};
try {
assert.deepEqual(readChannelSectionsStore(pubkey, relay), DEFAULT_STORE);
// Probe saw legacy still present → rolled back the just-written scoped copy
// so a second relay scope cannot double-import the same legacy value.
assert.equal(window.localStorage.getItem(scoped), null);
assert.notEqual(window.localStorage.getItem(legacyKey), null);
} finally {
window.localStorage.removeItem = origRemove;
}
// Legacy survived, so exactly one future healthy read claims it.
assert.deepEqual(readChannelSectionsStore(pubkey, relay), legacy);
assert.equal(window.localStorage.getItem(legacyKey), null);
});
test("readChannelSectionsStore: legacy delete and the catch probe both throw — scoped kept but hidden while legacy remains", () => {
const pubkey = "pk-migrate-delthrow-probethrow";
const relay = "wss://relay-delthrow-probethrow.example.com";
const legacy = makeStore({
sections: [makeSection({ id: "sb", name: "Legacy", order: 0 })],
assignments: {},
});
writeChannelSectionsStore(pubkey, legacy);
const legacyKey = storageKey(pubkey);
const scoped = storageKey(pubkey, relay);
const origGet = window.localStorage.getItem;
const origRemove = window.localStorage.removeItem;
let removeAttempted = false;
window.localStorage.removeItem = (k) => {
if (k === legacyKey) {
removeAttempted = true;
throw new Error("SecurityError");
}
return origRemove.call(window.localStorage, k);
};
window.localStorage.getItem = (k) => {
if (k === legacyKey && removeAttempted) throw new Error("SecurityError");
return origGet.call(window.localStorage, k);
};
try {
// Delete throws, then the catch probe throws too: legacy cannot be proven
// gone, so we KEEP the scoped copy (no-data-loss residual).
assert.deepEqual(readChannelSectionsStore(pubkey, relay), DEFAULT_STORE);
assert.notEqual(window.localStorage.getItem(scoped), null);
// While legacy remains and storage is broken, every read returns DEFAULT.
assert.deepEqual(readChannelSectionsStore(pubkey, relay), DEFAULT_STORE);
} finally {
window.localStorage.getItem = origGet;
window.localStorage.removeItem = origRemove;
}
// Storage recovers with legacy still present → the claim completes once.
assert.deepEqual(readChannelSectionsStore(pubkey, relay), legacy);
assert.equal(window.localStorage.getItem(legacyKey), null);
});
// NOTE: The seven migration-failure cases (write-fail, del-fail,
// del+rollback-throw, confirm-throw, delthrow-probeok, delthrow-probethrow,
// and del+probe-throw) test the claimLegacy state machine from
// mergeLaneStorage.shared.ts, which is authoritative and exercised in full
// by mergeLaneStorage.shared.test.mjs. The sections adapter uses that shared
// implementation via import, so duplicate coverage here adds no mutation
// surface. Kept: one roundtrip + scoped key + basic migration smoke above.
test("parseChannelSectionPayload: preserves icon field when present", () => {
const payload = {

View File

@@ -2,7 +2,6 @@ import assert from "node:assert/strict";
import test, { mock } from "node:test";
import { relayClient } from "@/shared/api/relayClient";
import { readChannelSectionsOutbox } from "./channelSectionsStorage.ts";
import { ChannelSectionSyncManager } from "./channelSectionsSync.ts";
import {
makeFakeWindow,
@@ -10,7 +9,10 @@ import {
installTauriMock,
installEchoTauri,
} from "./sidebarSyncTestHelpers.mjs";
import { runWholeBlobSyncSuite } from "./wholeBlobSync.shared.test.mjs";
// Shared whole-blob engine invariants are covered by wholeBlobSync.shared.test.mjs
// (which runs directly against ChannelSectionSyncManager). This file contains
// only sections-specific adapter and lane behavior tests.
function makeSectionsStore(sections = []) {
return { version: 1, sections, assignments: {} };
@@ -18,52 +20,35 @@ function makeSectionsStore(sections = []) {
const RELAY = "wss://r.test";
// ─── 17 shared whole-blob sync invariants ─────────────────────────────────────
runWholeBlobSyncSuite({
label: "sections",
SyncManager: ChannelSectionSyncManager,
publishMethod: "publishSections",
fetchRemoteMethod: "fetchRemoteSections",
subscribeMethod: "subscribeToSections",
watermarkLane: "channel-sections",
readOutbox: readChannelSectionsOutbox,
makeNonEmptyStore: () =>
makeSectionsStore([{ id: "s1", name: "Work", order: 0 }]),
decryptPayload: JSON.stringify({
version: 1,
sections: [{ id: "remote-section-from-relay", name: "Remote", order: 0 }],
assignments: {},
}),
emptyDecryptPayload: JSON.stringify({
version: 1,
sections: [],
assignments: {},
}),
checkAdoptedStore: (store) =>
store.sections.some((s) => s.id === "remote-section-from-relay"),
makeOverlapStoreA: () =>
makeSectionsStore([{ id: "a", name: "A", order: 0 }]),
makeOverlapStoreB: () =>
makeSectionsStore([{ id: "b", name: "B", order: 0 }]),
checkOverlapPending: (store) => store?.sections?.[0]?.id === "b",
checkOverlapOutbox: (outbox) => outbox?.store?.sections?.[0]?.id === "b",
makeLiveDebounceStore: () =>
makeSectionsStore([{ id: "local", name: "Local", order: 0 }]),
liveRemoteDecryptPayload: JSON.stringify({
version: 1,
sections: [{ id: "remote-during-debounce", name: "Remote", order: 0 }],
assignments: {},
}),
makeCollisionStoreA: () =>
makeSectionsStore([{ id: "mine", name: "Mine", order: 0 }]),
makeCollisionWinnerStore: () =>
makeSectionsStore([{ id: "peer", name: "Peer", order: 0 }]),
makeCollisionStoreSnd: () =>
makeSectionsStore([{ id: "second", name: "Second", order: 0 }]),
makeCollisionStoreLsr: () =>
makeSectionsStore([{ id: "loser", name: "Loser", order: 0 }]),
});
// Multi-slot timer fake: each setTimeout call is keyed by delay so overlapping
// timers can be fired independently. Mirrors the merge-lane convention.
function makeTimerBed() {
const storage = new Map();
const timers = new Map();
let nextId = 1;
const win = {
localStorage: {
getItem: (k) => storage.get(k) ?? null,
setItem: (k, v) => storage.set(k, v),
removeItem: (k) => storage.delete(k),
},
setTimeout: (fn, ms) => {
const id = nextId++;
timers.set(id, { fn, ms });
return id;
},
clearTimeout: (id) => timers.delete(id),
};
const fireDelay = async (ms) => {
const entry = [...timers.entries()].find(([, v]) => v.ms === ms);
assert.ok(entry, `expected a timer scheduled at ${ms}ms`);
timers.delete(entry[0]);
entry[1].fn();
for (let i = 0; i < 100; i++) await Promise.resolve();
};
const restore = installFakeWindow(win);
return { win, timers, fireDelay, restore };
}
// ─── Sections-specific: malformed (non-JSON) head payload ─────────────────────
@@ -195,30 +180,7 @@ test("serialized generations: older completion does not make the newer edit adop
storedHead = [event];
return Promise.resolve();
});
const storage = new Map();
const timers = new Map();
let nextId = 1;
const fakeWindow = {
localStorage: {
getItem: (k) => storage.get(k) ?? null,
setItem: (k, v) => storage.set(k, v),
removeItem: (k) => storage.delete(k),
},
setTimeout: (fn, ms) => {
const id = nextId++;
timers.set(id, { fn, ms });
return id;
},
clearTimeout: (id) => timers.delete(id),
};
const fireDelay = async (ms) => {
const entry = [...timers.entries()].find(([, v]) => v.ms === ms);
assert.ok(entry, `expected a timer scheduled at ${ms}ms`);
timers.delete(entry[0]);
entry[1].fn();
for (let i = 0; i < 100; i++) await Promise.resolve();
};
const restore = installFakeWindow(fakeWindow);
const { timers, fireDelay, restore } = makeTimerBed();
const tauri = installEchoTauri("pk-serial");
try {
const manager = new ChannelSectionSyncManager("pk-serial", RELAY);
@@ -277,30 +239,7 @@ test("serialized generations: a stale generation aborts before publishing", asyn
publishCalls.push(args);
return Promise.resolve();
});
const storage = new Map();
const timers = new Map();
let nextId = 1;
const fakeWindow = {
localStorage: {
getItem: (k) => storage.get(k) ?? null,
setItem: (k, v) => storage.set(k, v),
removeItem: (k) => storage.delete(k),
},
setTimeout: (fn, ms) => {
const id = nextId++;
timers.set(id, { fn, ms });
return id;
},
clearTimeout: (id) => timers.delete(id),
};
const fireDelay = async (ms) => {
const entry = [...timers.entries()].find(([, v]) => v.ms === ms);
assert.ok(entry, `expected a timer scheduled at ${ms}ms`);
timers.delete(entry[0]);
entry[1].fn();
for (let i = 0; i < 100; i++) await Promise.resolve();
};
const restore = installFakeWindow(fakeWindow);
const { fireDelay, restore } = makeTimerBed();
const tauri = installTauriMock("{}");
try {
const manager = new ChannelSectionSyncManager("pk-stale", RELAY);
@@ -428,30 +367,7 @@ test("ambiguous ACK: an accepted-but-unacked A does not make B adopt and disappe
];
return Promise.resolve();
});
const storage = new Map();
const timers = new Map();
let nextId = 1;
const fakeWindow = {
localStorage: {
getItem: (k) => storage.get(k) ?? null,
setItem: (k, v) => storage.set(k, v),
removeItem: (k) => storage.delete(k),
},
setTimeout: (fn, ms) => {
const id = nextId++;
timers.set(id, { fn, ms });
return id;
},
clearTimeout: (id) => timers.delete(id),
};
const fireDelay = async (ms) => {
const entry = [...timers.entries()].find(([, v]) => v.ms === ms);
assert.ok(entry, `expected a timer scheduled at ${ms}ms`);
timers.delete(entry[0]);
entry[1].fn();
for (let i = 0; i < 100; i++) await Promise.resolve();
};
const restore = installFakeWindow(fakeWindow);
const { timers, fireDelay, restore } = makeTimerBed();
const tauri = installSeamTauriMock(
JSON.stringify({
version: 1,
@@ -528,30 +444,7 @@ test("ambiguous ACK: a foreign head is adopted, not folded as our own", async ()
);
return Promise.resolve();
});
const storage = new Map();
const timers = new Map();
let nextId = 1;
const fakeWindow = {
localStorage: {
getItem: (k) => storage.get(k) ?? null,
setItem: (k, v) => storage.set(k, v),
removeItem: (k) => storage.delete(k),
},
setTimeout: (fn, ms) => {
const id = nextId++;
timers.set(id, { fn, ms });
return id;
},
clearTimeout: (id) => timers.delete(id),
};
const fireDelay = async (ms) => {
const entry = [...timers.entries()].find(([, v]) => v.ms === ms);
assert.ok(entry, `expected a timer scheduled at ${ms}ms`);
timers.delete(entry[0]);
entry[1].fn();
for (let i = 0; i < 100; i++) await Promise.resolve();
};
const restore = installFakeWindow(fakeWindow);
const { timers, fireDelay, restore } = makeTimerBed();
const tauri = installSeamTauriMock(
JSON.stringify({
version: 1,
@@ -605,30 +498,7 @@ test("serialized generations: a newer edit during encrypt/sign aborts the pre-si
publishCalls.push(event);
return Promise.resolve();
});
const storage = new Map();
const timers = new Map();
let nextId = 1;
const fakeWindow = {
localStorage: {
getItem: (k) => storage.get(k) ?? null,
setItem: (k, v) => storage.set(k, v),
removeItem: (k) => storage.delete(k),
},
setTimeout: (fn, ms) => {
const id = nextId++;
timers.set(id, { fn, ms });
return id;
},
clearTimeout: (id) => timers.delete(id),
};
const fireDelay = async (ms) => {
const entry = [...timers.entries()].find(([, v]) => v.ms === ms);
assert.ok(entry, `expected a timer scheduled at ${ms}ms`);
timers.delete(entry[0]);
entry[1].fn();
for (let i = 0; i < 100; i++) await Promise.resolve();
};
const restore = installFakeWindow(fakeWindow);
const { timers, fireDelay, restore } = makeTimerBed();
const tauri = installSeamTauriMock("{}", ["event-a", "event-b"]);
try {
const manager = new ChannelSectionSyncManager("pk-seam", RELAY);

View File

@@ -10,7 +10,10 @@ import {
installTauriMock,
installEchoTauri,
} from "./sidebarSyncTestHelpers.mjs";
import { runWholeBlobSyncSuite } from "./wholeBlobSync.shared.test.mjs";
// Shared whole-blob engine invariants are covered by wholeBlobSync.shared.test.mjs
// (which runs directly against ChannelSectionSyncManager). This file contains
// only sort-specific adapter and lane behavior tests.
function makeStore(groups = {}) {
return { version: 1, groups };
@@ -18,40 +21,6 @@ function makeStore(groups = {}) {
const RELAY = "wss://r.test";
// ─── 17 shared whole-blob sync invariants ─────────────────────────────────────
runWholeBlobSyncSuite({
label: "sort",
SyncManager: ChannelSortSyncManager,
publishMethod: "publishSortPrefs",
fetchRemoteMethod: "fetchRemoteSortPrefs",
subscribeMethod: "subscribeToSortPrefs",
watermarkLane: "channel-sort",
readOutbox: readChannelSortOutbox,
makeNonEmptyStore: () => makeStore({ channels: "recent" }),
decryptPayload: JSON.stringify({
version: 1,
groups: { "remote-group-from-relay": "recent" },
}),
emptyDecryptPayload: JSON.stringify({ version: 1, groups: {} }),
checkAdoptedStore: (store) => "remote-group-from-relay" in store.groups,
makeOverlapStoreA: () => makeStore({ channels: "recent" }),
makeOverlapStoreB: () => makeStore({ dms: "alpha" }),
checkOverlapPending: (store) =>
Object.keys(store?.groups ?? {}).includes("dms"),
checkOverlapOutbox: (outbox) =>
Object.keys(outbox?.store?.groups ?? {}).includes("dms"),
makeLiveDebounceStore: () => makeStore({ "local-group": "alpha" }),
liveRemoteDecryptPayload: JSON.stringify({
version: 1,
groups: { "remote-during-debounce": "recent" },
}),
makeCollisionStoreA: () => makeStore({ channels: "recent" }),
makeCollisionWinnerStore: () => makeStore({ channels: "alpha" }),
makeCollisionStoreSnd: () => makeStore({ dms: "recent" }),
makeCollisionStoreLsr: () => makeStore({ loser: "recent" }),
});
// ─── Sort-specific: unsupported head payload version ─────────────────────────
// An unsupported/unparseable payload version is equally unreadable: it decrypts

View File

@@ -1,32 +1,57 @@
// Compact stars lane adapter contract.
// Full merge-lane storage invariants (parsePayload, mergeStores algebra,
// boundStore, storageKey, claimLegacy state machine) are covered by
// mergeLaneStorage.shared.test.mjs. This file proves only the stars-specific
// wiring: correct value field name, storage key prefix, and id projection.
import assert from "node:assert/strict";
import test from "node:test";
import {
boundStarStore,
DEFAULT_STORE,
MAX_CHANNEL_STAR_ENTRIES,
mergeStores,
parseStarPayload,
readChannelStarsStore,
starredChannelIdsFromStore,
storageKey,
writeChannelStarsStore,
} from "./channelStarsStorage.ts";
import { normalizeRelayUrl } from "@/shared/lib/normalizeRelayUrl";
import { runMergeLaneStorageSuite } from "./mergeLaneStorage.shared.test.mjs";
runMergeLaneStorageSuite({
label: "stars",
storageKeyPrefix: "buzz-channel-stars.v1",
MAX_ENTRIES: MAX_CHANNEL_STAR_ENTRIES,
DEFAULT_STORE,
parsePayload: parseStarPayload,
makeEntry: (v, updatedAt, rev) => ({ starred: v, updatedAt, rev }),
entryValueField: "starred",
trueLabel: "star",
falseLabel: "unstar",
boundStore: boundStarStore,
mergeStores,
idsFromStore: starredChannelIdsFromStore,
readStore: readChannelStarsStore,
writeStore: writeChannelStarsStore,
storageKey,
normalizeRelayUrl,
test("stars adapter: value field is 'starred', key prefix is buzz-channel-stars.v1", () => {
// A round-trip through the parser confirms the field name and version gate.
const raw = {
version: 1,
channels: { a: { starred: true, updatedAt: 100, rev: 1 } },
};
const parsed = parseStarPayload(raw);
assert.ok(parsed !== null, "valid stars payload parses");
assert.equal(parsed.channels.a.starred, true, "starred field preserved");
assert.equal(
storageKey("pk1"),
"buzz-channel-stars.v1:pk1",
"storage key prefix",
);
});
test("stars adapter: idsFromStore projects starred=true entries", () => {
const store = {
version: 1,
channels: {
a: { starred: true, updatedAt: 1, rev: 0 },
b: { starred: false, updatedAt: 2, rev: 0 },
},
};
const ids = starredChannelIdsFromStore(store);
assert.ok(ids.has("a"), "starred channel in set");
assert.ok(!ids.has("b"), "unstarred channel excluded");
});
test("stars adapter: wrong value field (muted) is rejected by parser", () => {
const mutePayload = {
version: 1,
channels: { a: { muted: true, updatedAt: 100, rev: 1 } },
};
const result = parseStarPayload(mutePayload);
// muted field is filtered out as malformed (no 'starred' field), leaving empty channels.
assert.deepEqual(
result?.channels ?? {},
{},
"muted entry filtered as malformed",
);
});

View File

@@ -43,7 +43,7 @@ function stubRelay(relayClient, { live } = {}) {
}
/**
* Run the full merge-lane hook invariant suite for a single lane.
* Run the merge-lane hook invariant suite for a single lane.
*
* @param {object} cfg
* @param {string} cfg.label - Human-readable lane name for test titles.
@@ -119,145 +119,52 @@ export function runMergeLaneHookSuite({
}
});
test(`${label}: persisted-local first click mints seen+1 on both dimensions`, async () => {
// Monotonic mint: combines persisted-local high-water, far-future observation,
// and same-second click sequence into one scenario. After observing a remote
// with high updatedAt and rev, local clicks must advance both dimensions.
// Mutation: dropping maxUpdatedAtSeen or maxRevSeen tracking makes later
// clicks mint below the observed high-water and lose on merge.
test(`${label}: monotonic mint — persisted high-water + far-future live event + same-second clicks advance rev`, async () => {
const { act, cleanup, renderHook } = await import("@testing-library/react");
const { relayClient } = await import("@/shared/api/relayClient");
const restore = stubRelay(relayClient);
const live = {};
const restore = stubRelay(relayClient, { live });
const origTauri = window.__TAURI_INTERNALS__;
const origDateNow = Date.now;
// Local clock: 100s. Persisted head has updatedAt 500, rev 4.
Date.now = () => 100 * 1_000;
const pubkey = `pk-${label}-persist`;
const FUTURE = 500;
window.__TAURI_INTERNALS__ = {
invoke: (cmd) => {
if (cmd === "nip44_decrypt_from_self")
return Promise.resolve(
makePayload({
shared: { [entryValueField]: false, updatedAt: FUTURE, rev: 7 },
}),
);
return Promise.reject(new Error(`unmocked ${cmd}`));
},
};
const pubkey = `pk-${label}-mono`;
// Seed a persisted store at rev 4, updatedAt 500.
window.localStorage.setItem(
storageKey(pubkey, "wss://r"),
makePayload({
shared: { [entryValueField]: true, updatedAt: 500, rev: 4 },
shared: { [entryValueField]: true, updatedAt: FUTURE, rev: 4 },
}),
);
try {
const { result, unmount } = renderHook(() => useHook(pubkey, "wss://r"));
act(() => result.current[falseAction]("shared"));
const persisted = readStore(pubkey, "wss://r");
assert.equal(
persisted.channels.shared[entryValueField],
false,
`${falseLabel} applied`,
);
assert.equal(
persisted.channels.shared.updatedAt,
500,
"updatedAt held at persisted-local high-water (max(100,500,seen))",
);
assert.equal(
persisted.channels.shared.rev,
5,
"rev minted as persisted-local rev + 1",
);
unmount();
} finally {
cleanup();
Date.now = origDateNow;
restore();
}
});
test(`${label}: fast-clock veto fix: click after observing a future-stamped head wins`, async () => {
const { act, cleanup, renderHook } = await import("@testing-library/react");
const { relayClient } = await import("@/shared/api/relayClient");
const live = {};
const restore = stubRelay(relayClient, { live });
const origTauri = window.__TAURI_INTERNALS__;
const origDateNow = Date.now;
Date.now = () => 100 * 1_000;
window.__TAURI_INTERNALS__ = {
invoke: (cmd) => {
if (cmd === "nip44_decrypt_from_self")
return Promise.resolve(
makePayload({
shared: { [entryValueField]: false, updatedAt: 400, rev: 7 },
}),
);
return Promise.reject(new Error(`unmocked ${cmd}`));
},
};
const pubkey = `pk-${label}-fastclock`;
let hook = null;
try {
await act(async () => {
hook = renderHook(() => useHook(pubkey, "wss://r"));
for (let i = 0; i < 20; i++) await Promise.resolve();
});
// Deliver a live event with rev 7 (higher than persisted 4).
await act(async () => {
live.cb({
id: "future-head",
pubkey,
created_at: 400,
content: "cipher",
kind: 30078,
tags: [["d", dTag]],
sig: "s",
});
for (let i = 0; i < 20; i++) await Promise.resolve();
});
assert.equal(
hook.result.current[idsField].has("shared"),
false,
"future head applied → false",
);
await act(async () => hook.result.current[trueAction]("shared"));
assert.equal(
hook.result.current[idsField].has("shared"),
true,
"click must win despite the observed future timestamp",
);
const persisted = readStore(pubkey, "wss://r");
assert.equal(
persisted.channels.shared.updatedAt,
400,
"mint lifted to t+300",
);
assert.equal(persisted.channels.shared.rev, 8, "rev = maxRevSeen+1");
hook.unmount();
} finally {
cleanup();
Date.now = origDateNow;
window.__TAURI_INTERNALS__ = origTauri;
restore();
}
});
test(`${label}: far-future observation: timestamp stays fixed, rev advances, latest click wins`, async () => {
const { act, cleanup, renderHook } = await import("@testing-library/react");
const { relayClient } = await import("@/shared/api/relayClient");
const live = {};
const restore = stubRelay(relayClient, { live });
const origTauri = window.__TAURI_INTERNALS__;
const origDateNow = Date.now;
Date.now = () => 100 * 1_000;
const FUTURE = 100 + 31_536_000;
window.__TAURI_INTERNALS__ = {
invoke: (cmd) => {
if (cmd === "nip44_decrypt_from_self")
return Promise.resolve(
makePayload({
shared: { [entryValueField]: true, updatedAt: FUTURE, rev: 1 },
}),
);
return Promise.reject(new Error(`unmocked ${cmd}`));
},
};
const pubkey = `pk-${label}-future`;
let hook = null;
try {
await act(async () => {
hook = renderHook(() => useHook(pubkey, "wss://r"));
for (let i = 0; i < 20; i++) await Promise.resolve();
});
await act(async () => {
live.cb({
id: "far-future",
pubkey,
created_at: FUTURE,
content: "cipher",
kind: 30078,
@@ -266,32 +173,34 @@ export function runMergeLaneHookSuite({
});
for (let i = 0; i < 20; i++) await Promise.resolve();
});
await act(async () => hook.result.current[falseAction]("shared"));
// First click: must mint above max(FUTURE, rev 7).
await act(async () => hook.result.current[trueAction]("shared"));
let p = readStore(pubkey, "wss://r");
assert.equal(
p.channels.shared[entryValueField],
false,
true,
"first click applied",
);
assert.equal(
p.channels.shared.updatedAt,
FUTURE,
"timestamp stays fixed",
"updatedAt held at observed high-water",
);
assert.equal(p.channels.shared.rev, 2, "rev advanced 1→2");
await act(async () => hook.result.current[trueAction]("shared"));
assert.equal(p.channels.shared.rev, 8, "rev = maxRevSeen(7) + 1");
// Second same-second click: rev must strictly advance.
await act(async () => hook.result.current[falseAction]("shared"));
p = readStore(pubkey, "wss://r");
assert.equal(
p.channels.shared[entryValueField],
true,
"latest click wins",
false,
"second click applied",
);
assert.equal(
p.channels.shared.updatedAt,
FUTURE,
"timestamp still fixed",
"updatedAt still fixed",
);
assert.equal(p.channels.shared.rev, 3, "rev advanced 2→3");
assert.equal(p.channels.shared.rev, 9, "rev advanced 8→9");
hook.unmount();
} finally {
cleanup();
@@ -301,115 +210,13 @@ export function runMergeLaneHookSuite({
}
});
test(`${label}: empty-store click survives a later higher-rev head with an older updatedAt`, async () => {
const { act, cleanup, renderHook } = await import("@testing-library/react");
const { relayClient } = await import("@/shared/api/relayClient");
const live = {};
const restore = stubRelay(relayClient, { live });
const origTauri = window.__TAURI_INTERNALS__;
const origDateNow = Date.now;
Date.now = () => 1000 * 1_000;
window.__TAURI_INTERNALS__ = {
invoke: (cmd) => {
if (cmd === "nip44_decrypt_from_self")
return Promise.resolve(
makePayload({
shared: { [entryValueField]: false, updatedAt: 500, rev: 99 },
}),
);
return Promise.reject(new Error(`unmocked ${cmd}`));
},
};
const pubkey = `pk-${label}-empty`;
let hook = null;
try {
await act(async () => {
hook = renderHook(() => useHook(pubkey, "wss://r"));
for (let i = 0; i < 20; i++) await Promise.resolve();
});
await act(async () => hook.result.current[trueAction]("shared"));
await act(async () => {
live.cb({
id: "older-higher-rev",
pubkey,
created_at: 500,
content: "cipher",
kind: 30078,
tags: [["d", dTag]],
sig: "s",
});
for (let i = 0; i < 20; i++) await Promise.resolve();
});
assert.equal(
hook.result.current[idsField].has("shared"),
true,
"click at newer updatedAt survives an older higher-rev head",
);
hook.unmount();
} finally {
cleanup();
Date.now = origDateNow;
window.__TAURI_INTERNALS__ = origTauri;
restore();
}
});
test(`${label}: unobserved future head wins over an empty-high-water click on primary updatedAt`, async () => {
const { act, cleanup, renderHook } = await import("@testing-library/react");
const { relayClient } = await import("@/shared/api/relayClient");
const live = {};
const restore = stubRelay(relayClient, { live });
const origTauri = window.__TAURI_INTERNALS__;
const origDateNow = Date.now;
Date.now = () => 1000 * 1_000;
window.__TAURI_INTERNALS__ = {
invoke: (cmd) => {
if (cmd === "nip44_decrypt_from_self")
return Promise.resolve(
makePayload({
shared: { [entryValueField]: false, updatedAt: 1300, rev: 1 },
}),
);
return Promise.reject(new Error(`unmocked ${cmd}`));
},
};
const pubkey = `pk-${label}-unobserved-future`;
let hook = null;
try {
await act(async () => {
hook = renderHook(() => useHook(pubkey, "wss://r"));
for (let i = 0; i < 20; i++) await Promise.resolve();
});
await act(async () => hook.result.current[trueAction]("shared"));
await act(async () => {
live.cb({
id: "unobserved-future",
pubkey,
created_at: 1300,
content: "cipher",
kind: 30078,
tags: [["d", dTag]],
sig: "s",
});
for (let i = 0; i < 20; i++) await Promise.resolve();
});
assert.equal(
hook.result.current[idsField].has("shared"),
false,
"unobserved future head wins on primary updatedAt (accepted residual)",
);
hook.unmount();
} finally {
cleanup();
Date.now = origDateNow;
window.__TAURI_INTERNALS__ = origTauri;
restore();
}
});
test(`${label}: cross-window storage event is observed and max-merged`, async () => {
// Cross-window storage merge + outbox resume in one compact scenario.
// (a) A cross-window storage event is observed and max-merged into this window.
// (b) A click after the merge mints above the observed peer high-water.
// (c) A persisted outbox record is retained until the publish completes.
// Mutations: (a) removing the storageEvent listener; (b) not tracking peer
// maxRevSeen; (c) dropping writeOwnOutbox in the bootstrap path.
test(`${label}: cross-window merge, subsequent click mints above peer high-water, outbox retained`, async () => {
const { act, cleanup, renderHook } = await import("@testing-library/react");
const { relayClient } = await import("@/shared/api/relayClient");
@@ -417,12 +224,27 @@ export function runMergeLaneHookSuite({
const origDateNow = Date.now;
Date.now = () => 100 * 1_000;
const pubkey = `pk-${label}-xwin`;
const outboxKey = `${outboxKeyPrefix}:${pubkey}:${encodeURIComponent("wss://r")}`;
// Pre-seed a persisted outbox record (simulates resumed intent).
window.localStorage.setItem(
outboxKey,
makePayload({
resumed: { [entryValueField]: true, updatedAt: 90, rev: 2 },
}),
);
let hook = null;
try {
await act(async () => {
hook = renderHook(() => useHook(pubkey, "wss://r"));
for (let i = 0; i < 20; i++) await Promise.resolve();
for (let i = 0; i < 40; i++) await Promise.resolve();
});
// (c) Outbox retained until publish completes.
assert.ok(
window.localStorage.getItem(outboxKey) !== null,
"outbox retained during bootstrap",
);
// (a) Peer window writes a store with rev 12 and fires a storage event.
window.localStorage.setItem(
storageKey(pubkey, "wss://r"),
makePayload({
@@ -442,6 +264,8 @@ export function runMergeLaneHookSuite({
true,
"peer write merged into this window",
);
// (b) Click after merge mints above peer high-water (updatedAt 900, rev 12).
await act(async () => hook.result.current[falseAction]("shared"));
const p = readStore(pubkey, "wss://r");
assert.equal(p.channels.shared[entryValueField], false, "click applied");
@@ -454,38 +278,4 @@ export function runMergeLaneHookSuite({
restore();
}
});
test(`${label}: bootstrap resumes a persisted outbox edit`, async () => {
const { act, cleanup, renderHook } = await import("@testing-library/react");
const { relayClient } = await import("@/shared/api/relayClient");
const restore = stubRelay(relayClient);
const origDateNow = Date.now;
Date.now = () => 100 * 1_000;
const pubkey = `pk-${label}-outbox`;
const relayUrl = `wss://r.${label}-outbox`;
const outboxKey = `${outboxKeyPrefix}:${pubkey}:${encodeURIComponent(relayUrl)}`;
window.localStorage.setItem(
outboxKey,
makePayload({
resumed: { [entryValueField]: true, updatedAt: 90, rev: 2 },
}),
);
let hook = null;
try {
await act(async () => {
hook = renderHook(() => useHook(pubkey, relayUrl));
for (let i = 0; i < 40; i++) await Promise.resolve();
});
assert.ok(
window.localStorage.getItem(outboxKey) !== null,
"outbox retained until the resumed publish completes",
);
hook.unmount();
} finally {
cleanup();
Date.now = origDateNow;
restore();
}
});
}

View File

@@ -178,157 +178,105 @@ export function runMergeLaneSyncSuite({
}
});
test(`${label}: destroy: is safe to call with no pending publish`, () => {
const fw = makeFakeWindow();
const restore = installFakeWindow(fw);
try {
const manager = new Manager("pk-no-pending", RELAY);
assert.doesNotThrow(() => manager.destroy());
} finally {
restore();
}
});
// ─── Generation CAS: A-in-flight → B-click → A-completes (both variants) ────
// Finding 2 (A succeeds): an older in-flight publish that completes after a
// newer edit is queued must NOT clear the newer edit's pending store/outbox,
// and B must reach the relay via the completion re-drive. Mutation: dropping
// the generation CAS in discardPending lets A's success null out B's
// pending+outbox.
test(`${label}: A-in-flight → B-click → A-succeeds: B stays pending and B publishes`, async () => {
let releaseFirst = null;
let publishCount = 0;
let storedHead = [];
mock.method(relayClient, "fetchEvents", () => Promise.resolve(storedHead));
mock.method(relayClient, "publishEvent", (event) => {
publishCount++;
if (publishCount === 1) {
// A reaches the relay; hold its ACK open until the test releases it,
// then record it as the retained head.
return new Promise((res) => {
releaseFirst = () => {
storedHead = [event];
res();
};
});
// Finding 2: an older in-flight publish that completes (succeeds or fails) after
// a newer edit is queued must NOT clear B, and B must reach the relay via the
// serialized re-drive / completion re-drive. Mutation: dropping the generation
// CAS in discardPending lets A's outcome null out B's pending+outbox.
for (const { name, resolveFirst } of [
{
name: "A-succeeds",
resolveFirst: (event, storedHead, res) => {
storedHead.push(event);
res();
},
},
{
name: "A-fails",
resolveFirst: (_event, _storedHead, _res, rej) =>
rej(new Error("socket error")),
},
]) {
test(`${label}: A-in-flight → B-click → ${name}: B stays pending and B publishes`, async () => {
let releaseFirst = null;
let publishCount = 0;
const storedHead = [];
mock.method(relayClient, "fetchEvents", () =>
Promise.resolve([...storedHead]),
);
mock.method(relayClient, "publishEvent", (event) => {
publishCount++;
if (publishCount === 1) {
return new Promise((res, rej) => {
releaseFirst = () => resolveFirst(event, storedHead, res, rej);
});
}
storedHead.splice(0, storedHead.length, event);
return Promise.resolve();
});
const t = makeMultiTimerWindow();
const restore = installFakeWindow(t.win);
const tauri = installEchoTauri(`pk-ab-${name}`);
try {
const manager = new Manager(`pk-ab-${name}`, RELAY);
const storeA = makeStore({ a: makeEntry(true, 100, 1) });
const storeB = makeStore({ b: makeEntry(true, 101, 1) });
publish(manager, storeA);
await t.fireDelay(2000); // doPublish(A) awaits publishEvent
while (releaseFirst === null) await Promise.resolve();
// B arrives while A is in flight.
publish(manager, storeB);
assert.deepEqual(
Object.keys(getPending(manager).channels),
["b"],
"B is now pending",
);
assert.ok(readOutbox(`pk-ab-${name}`, RELAY), "outbox holds B");
// A completes (success or failure) — must NOT clear B.
releaseFirst();
for (let i = 0; i < 50; i++) await Promise.resolve();
assert.deepEqual(
Object.keys(getPending(manager)?.channels ?? {}),
["b"],
`older A completion (${name}) leaves B pending`,
);
assert.ok(
readOutbox(`pk-ab-${name}`, RELAY),
`older A completion (${name}) leaves B outbox`,
);
// B's own debounce fires and B reaches the relay; the retained-head fetch
// confirms subsumption, so B's outbox clears.
const capturedBefore = tauri.capturedPlaintext();
await t.fireDelay(2000);
for (let i = 0; i < 50; i++) await Promise.resolve();
const captured = tauri.capturedPlaintext();
assert.ok(
captured && captured !== capturedBefore && captured.includes('"b"'),
"B is published to the relay",
);
assert.equal(
getPending(manager),
null,
"B cleared after confirmed publish",
);
assert.equal(
readOutbox(`pk-ab-${name}`, RELAY),
null,
"B outbox cleared",
);
manager.destroy();
} finally {
tauri.restore();
restore();
mock.reset();
}
storedHead = [event];
return Promise.resolve();
});
const t = makeMultiTimerWindow();
const restore = installFakeWindow(t.win);
const tauri = installEchoTauri("pk-ab");
try {
const manager = new Manager("pk-ab", RELAY);
const storeA = makeStore({ a: makeEntry(true, 100, 1) });
const storeB = makeStore({ b: makeEntry(true, 101, 1) });
publish(manager, storeA);
await t.fireDelay(2000); // doPublish(A) awaits publishEvent
while (releaseFirst === null) await Promise.resolve();
// B arrives while A is in flight.
publish(manager, storeB);
assert.deepEqual(
Object.keys(getPending(manager).channels),
["b"],
"B is now pending",
);
assert.ok(readOutbox("pk-ab", RELAY), "outbox holds B");
// A completes — must NOT clear B.
releaseFirst();
for (let i = 0; i < 50; i++) await Promise.resolve();
assert.deepEqual(
Object.keys(getPending(manager)?.channels ?? {}),
["b"],
"older A completion leaves B pending",
);
assert.ok(
readOutbox("pk-ab", RELAY),
"older A completion leaves B outbox",
);
// B's own debounce fires and B reaches the relay with no kick; the
// post-publish retained-head fetch reads B's own write back and confirms
// subsumption, so B's outbox clears.
const capturedBefore = tauri.capturedPlaintext();
await t.fireDelay(2000);
for (let i = 0; i < 50; i++) await Promise.resolve();
const captured = tauri.capturedPlaintext();
assert.ok(
captured && captured !== capturedBefore && captured.includes('"b"'),
"B is published to the relay",
);
assert.equal(
getPending(manager),
null,
"B cleared after confirmed publish",
);
assert.equal(readOutbox("pk-ab", RELAY), null, "B outbox cleared");
manager.destroy();
} finally {
tauri.restore();
restore();
mock.reset();
}
});
// Finding 2 (A fails): A's publish rejects after B is queued. B must remain
// pending and be published by the serialized re-drive / retry — no manual kick.
test(`${label}: A-in-flight → B-click → A-fails: B remains pending and B publishes`, async () => {
let rejectFirst = null;
let publishCount = 0;
let storedHead = [];
mock.method(relayClient, "fetchEvents", () => Promise.resolve(storedHead));
mock.method(relayClient, "publishEvent", (event) => {
publishCount++;
if (publishCount === 1) {
return new Promise((_res, rej) => {
rejectFirst = () => rej(new Error("socket error"));
});
}
storedHead = [event];
return Promise.resolve();
});
const t = makeMultiTimerWindow();
const restore = installFakeWindow(t.win);
const tauri = installEchoTauri("pk-abfail");
try {
const manager = new Manager("pk-abfail", RELAY);
publish(manager, makeStore({ a: makeEntry(true, 100, 1) }));
await t.fireDelay(2000);
while (rejectFirst === null) await Promise.resolve();
publish(manager, makeStore({ b: makeEntry(true, 101, 1) }));
rejectFirst(); // A fails
for (let i = 0; i < 50; i++) await Promise.resolve();
assert.deepEqual(
Object.keys(getPending(manager)?.channels ?? {}),
["b"],
"B still pending after A's failure",
);
assert.ok(
readOutbox("pk-abfail", RELAY),
"B outbox intact after A's failure",
);
// B's debounce fires and B publishes successfully; the retained-head fetch
// confirms B's own write and clears it.
await t.fireDelay(2000);
for (let i = 0; i < 50; i++) await Promise.resolve();
const captured = tauri.capturedPlaintext();
assert.ok(captured?.includes('"b"'), "B published");
assert.equal(getPending(manager), null, "B cleared");
manager.destroy();
} finally {
tauri.restore();
restore();
mock.reset();
}
});
}
// ─── Bounded-backoff retry: failed publish on a healthy socket, no later edit
@@ -450,92 +398,83 @@ export function runMergeLaneSyncSuite({
// ─── Boot seed-publish guard (the revert-fix regression suite) ───────────────
test(`${label}: revert-fix: fetch failed (error) does not trigger seed-publish via bootstrap`, async () => {
mock.method(relayClient, "fetchEvents", () =>
Promise.reject(new Error("relay timeout")),
);
mock.method(relayClient, "publishEvent", () => Promise.resolve());
const fw = makeFakeWindow();
const restore = installFakeWindow(fw);
try {
const manager = new Manager("pk-fail", RELAY);
const result = await manager.bootstrap(
makeStore({ ch1: makeEntry(true, 1, 0) }),
);
assert.equal(result.action, "hold");
assert.equal(getPending(manager), null);
} finally {
restore();
mock.reset();
}
});
test(`${label}: revert-fix: absent fetch with prior watermark blocks seed-publish via bootstrap`, async () => {
mock.method(relayClient, "fetchEvents", () => Promise.resolve([]));
mock.method(relayClient, "publishEvent", () => Promise.resolve());
const fw = makeFakeWindow();
fw.localStorage.setItem(
`buzz-sync-watermark.v1:${watermarkKind}:pk-stale:${RELAY_KEY}`,
"1700000000",
);
const restore = installFakeWindow(fw);
try {
const manager = new Manager("pk-stale", RELAY);
const result = await manager.bootstrap(
makeStore({ ch1: makeEntry(true, 1, 0) }),
);
assert.equal(result.action, "hold");
assert.equal(getPending(manager), null);
} finally {
restore();
mock.reset();
}
});
test(`${label}: revert-fix: absent fetch with zero watermark seeds via bootstrap (first-sync preserved)`, async () => {
mock.method(relayClient, "fetchEvents", () => Promise.resolve([]));
mock.method(relayClient, "publishEvent", () => Promise.resolve());
const fw = makeFakeWindow();
const restore = installFakeWindow(fw);
try {
const manager = new Manager("pk-fresh", RELAY);
const result = await manager.bootstrap(
makeStore({ ch1: makeEntry(true, 1, 0) }),
);
assert.equal(result.action, "hold");
assert.ok(getPending(manager) !== null);
} finally {
restore();
mock.reset();
}
});
test(`${label}: revert-fix: relay-A watermark does not suppress first-sync seed on relay-B`, async () => {
const relayA = "wss://a.relay.test";
const relayB = "wss://b.relay.test";
mock.method(relayClient, "fetchEvents", () => Promise.resolve([]));
mock.method(relayClient, "publishEvent", () => Promise.resolve());
const fw = makeFakeWindow();
fw.localStorage.setItem(
`buzz-sync-watermark.v1:${watermarkKind}:pk-iso:${encodeURIComponent(relayA)}`,
"1700000100",
);
const restore = installFakeWindow(fw);
try {
const managerB = new Manager("pk-iso", relayB);
const result = await managerB.bootstrap(
makeStore({ ch1: makeEntry(true, 1, 0) }),
);
assert.equal(result.action, "hold");
assert.ok(
getPending(managerB) !== null,
`first-sync seed on relay B must not be blocked by relay A watermark`,
);
} finally {
restore();
mock.reset();
}
});
for (const {
title,
setupFetch,
setupWatermark,
relayOverride,
pubkey,
assertPending,
} of [
{
title: "fetch failed (error) does not trigger seed-publish via bootstrap",
setupFetch: () =>
mock.method(relayClient, "fetchEvents", () =>
Promise.reject(new Error("relay timeout")),
),
assertPending: (m) => assert.equal(getPending(m), null),
},
{
title:
"absent fetch with prior watermark blocks seed-publish via bootstrap",
setupFetch: () =>
mock.method(relayClient, "fetchEvents", () => Promise.resolve([])),
setupWatermark: (fw) =>
fw.localStorage.setItem(
`buzz-sync-watermark.v1:${watermarkKind}:pk-stale:${RELAY_KEY}`,
"1700000000",
),
pubkey: "pk-stale",
assertPending: (m) => assert.equal(getPending(m), null),
},
{
title:
"absent fetch with zero watermark seeds via bootstrap (first-sync preserved)",
setupFetch: () =>
mock.method(relayClient, "fetchEvents", () => Promise.resolve([])),
pubkey: "pk-fresh",
assertPending: (m) => assert.ok(getPending(m) !== null),
},
{
title: "relay-A watermark does not suppress first-sync seed on relay-B",
setupFetch: () =>
mock.method(relayClient, "fetchEvents", () => Promise.resolve([])),
setupWatermark: (fw) =>
fw.localStorage.setItem(
`buzz-sync-watermark.v1:${watermarkKind}:pk-iso:${encodeURIComponent("wss://a.relay.test")}`,
"1700000100",
),
relayOverride: "wss://b.relay.test",
pubkey: "pk-iso",
assertPending: (m) =>
assert.ok(
getPending(m) !== null,
"first-sync seed on relay B must not be blocked by relay A watermark",
),
},
]) {
test(`${label}: revert-fix: ${title}`, async () => {
setupFetch();
mock.method(relayClient, "publishEvent", () => Promise.resolve());
const fw = makeFakeWindow();
setupWatermark?.(fw);
const restore = installFakeWindow(fw);
try {
const manager = new Manager(
pubkey ?? "pk-fail",
relayOverride ?? RELAY,
);
const result = await manager.bootstrap(
makeStore({ ch1: makeEntry(true, 1, 0) }),
);
assert.equal(result.action, "hold");
assertPending(manager);
} finally {
restore();
mock.reset();
}
});
}
// ─── Failed pre-publish fetch: retain, never publish (Carl P1) ───────────────

View File

@@ -90,7 +90,6 @@ function withStorage(fn) {
const { normalizeRelayUrl } = await import("@/shared/lib/normalizeRelayUrl");
const { outboxWindowNonce } = await import("./sidebarSyncWatermark.ts");
const stars = await import("./channelStarsStorage.ts");
const mutes = await import("./channelMutesStorage.ts");
const sort = await import("./channelSortPreference.ts");
const sections = await import("./channelSectionsStorage.ts");
@@ -100,7 +99,6 @@ const SCOPE = `${PK}:${encodeURIComponent(normalizeRelayUrl(RELAY))}`;
const PREFIX = {
stars: "buzz-channel-stars-outbox.v1",
mutes: "buzz-channel-mutes-outbox.v1",
sort: "buzz-channel-sort-outbox.v1",
sections: "buzz-channel-sections-outbox.v1",
};
@@ -118,8 +116,6 @@ const writeAt = (ls, key, store, queuedAt) =>
const starStore = (channels) => ({ version: 1, channels });
const starEntry = (starred, updatedAt, rev) => ({ starred, updatedAt, rev });
const muteStore = (channels) => ({ version: 1, channels });
const muteEntry = (muted, updatedAt, rev) => ({ muted, updatedAt, rev });
const sortStore = (groups) => ({ version: 1, groups });
const sectionStore = (secs, assignments = {}) => ({
version: 1,
@@ -218,26 +214,6 @@ test("(ii) stars (merge): both windows' distinct-channel edits resume", () => {
});
});
test("(ii) mutes (merge): both windows' distinct-channel edits resume", () => {
withStorage((ls) => {
writeAt(
ls,
foreignKey("mutes", "A", 0),
muteStore({ a: muteEntry(true, 100, 1) }),
100,
);
writeAt(
ls,
foreignKey("mutes", "B", 0),
muteStore({ b: muteEntry(true, 200, 1) }),
200,
);
const resumed = mutes.readChannelMutesOutbox(PK, RELAY);
assert.deepEqual(resumed.channels.a, muteEntry(true, 100, 1));
assert.deepEqual(resumed.channels.b, muteEntry(true, 200, 1));
});
});
test("(ii) sort (whole-blob): the newest queued window resumes; older is LWW-superseded", () => {
withStorage((ls) => {
writeAt(
@@ -259,27 +235,6 @@ test("(ii) sort (whole-blob): the newest queued window resumes; older is LWW-sup
});
});
test("(ii) sections (whole-blob): the newest queued window resumes; older is LWW-superseded", () => {
withStorage((ls) => {
writeAt(
ls,
foreignKey("sections", "A", 0),
sectionStore([{ id: "s1", name: "One", order: 0 }]),
100,
);
writeAt(
ls,
foreignKey("sections", "B", 0),
sectionStore([{ id: "s2", name: "Two", order: 0 }]),
200,
);
assert.deepEqual(
sections.readChannelSectionsOutbox(PK, RELAY).store.sections,
[{ id: "s2", name: "Two", order: 0 }],
);
});
});
// ── (iii) Same-second whole-blob: head at t keeps a record queued at t ─────────
//
// One-second clock granularity means a record queued in the same second as the

View File

@@ -18,6 +18,69 @@ before(() => {
after(() => dom.window.close());
// ── Shared test helpers ───────────────────────────────────────────────────────
/** Install relay no-ops and return a restore function. */
function stubRelay(relayClient, { live, reconnect, publishCalls } = {}) {
const orig = {
fetchEvents: relayClient.fetchEvents,
subscribeLive: relayClient.subscribeLive,
subscribeToReconnects: relayClient.subscribeToReconnects,
publishEvent: relayClient.publishEvent,
};
relayClient.fetchEvents = async () => [];
relayClient.subscribeLive = async (_f, cb) => {
if (live) live.cb = cb;
return async () => {};
};
relayClient.subscribeToReconnects = (cb) => {
if (reconnect) reconnect.cb = cb;
return () => {};
};
relayClient.publishEvent = async (...args) => {
if (publishCalls) publishCalls.push(args);
};
return () => Object.assign(relayClient, orig);
}
/**
* Install a Tauri mock that:
* - encrypt returns "ct"
* - sign returns a minimal event with the given pubkey
* - decrypt returns `decryptPayload` (a JSON string or callback)
*/
function stubTauri(pubkey, decryptPayload) {
const orig = window.__TAURI_INTERNALS__;
window.__TAURI_INTERNALS__ = {
invoke: (cmd, args) => {
if (cmd === "nip44_decrypt_from_self") {
const payload =
typeof decryptPayload === "function"
? decryptPayload(args)
: decryptPayload;
return Promise.resolve(payload);
}
if (cmd === "nip44_encrypt_to_self") return Promise.resolve("ct");
if (cmd === "sign_event")
return Promise.resolve(
JSON.stringify({
id: "signed",
pubkey,
content: "ct",
created_at: 0,
kind: 30078,
tags: [],
sig: "s",
}),
);
return Promise.reject(new Error(`unmocked ${cmd}`));
},
};
return () => {
window.__TAURI_INTERNALS__ = orig;
};
}
test("assignChannel refreshes an existing assignment before the next eviction", async () => {
const { act, cleanup, renderHook } = await import("@testing-library/react");
const { relayClient } = await import("@/shared/api/relayClient");
@@ -26,12 +89,7 @@ test("assignChannel refreshes an existing assignment before the next eviction",
);
const { useChannelSections } = await import("./useChannelSections.ts");
const originalFetchEvents = relayClient.fetchEvents;
const originalSubscribeLive = relayClient.subscribeLive;
const originalSubscribeToReconnects = relayClient.subscribeToReconnects;
relayClient.fetchEvents = async () => [];
relayClient.subscribeLive = async () => async () => {};
relayClient.subscribeToReconnects = () => () => {};
const restoreRelay = stubRelay(relayClient);
const pubkey = "pk-at-capacity";
const relayUrl = "wss://relay.example";
@@ -71,9 +129,7 @@ test("assignChannel refreshes an existing assignment before the next eviction",
unmount();
} finally {
cleanup();
relayClient.fetchEvents = originalFetchEvents;
relayClient.subscribeLive = originalSubscribeLive;
relayClient.subscribeToReconnects = originalSubscribeToReconnects;
restoreRelay();
}
});
@@ -100,35 +156,8 @@ test("storage event while a local edit is pending is deferred, not applied", asy
const { useChannelSections } = await import("./useChannelSections.ts");
const { storageKey } = await import("./channelSectionsStorage.ts");
const origFetch = relayClient.fetchEvents;
const origLive = relayClient.subscribeLive;
const origReconnect = relayClient.subscribeToReconnects;
const origPublish = relayClient.publishEvent;
const origTauri = window.__TAURI_INTERNALS__;
// Block all publishes so the debounce never fires during the test.
relayClient.fetchEvents = async () => [];
relayClient.subscribeLive = async () => async () => {};
relayClient.subscribeToReconnects = () => () => {};
relayClient.publishEvent = async () => {};
window.__TAURI_INTERNALS__ = {
invoke: (cmd) => {
if (cmd === "nip44_encrypt_to_self") return Promise.resolve("ct");
if (cmd === "sign_event")
return Promise.resolve(
JSON.stringify({
id: "signed",
pubkey: "pk-storage-guard",
content: "ct",
created_at: 0,
kind: 30078,
tags: [],
sig: "s",
}),
);
return Promise.reject(new Error(`unmocked ${cmd}`));
},
};
const restoreRelay = stubRelay(relayClient);
const restoreTauri = stubTauri("pk-storage-guard", null);
const pubkey = "pk-storage-guard";
const relayUrl = "wss://r.storage-guard";
@@ -150,8 +179,7 @@ test("storage event while a local edit is pending is deferred, not applied", asy
"A1 section created",
);
// Window B writes B1 to the shared scoped cache key (simulates a peer
// window's edit landing in localStorage and firing the storage event).
// Window B writes B1 to the shared scoped cache key.
const b1Store = JSON.stringify({
version: 1,
sections: [{ id: "b1-section", name: "B1", order: 0 }],
@@ -184,9 +212,7 @@ test("storage event while a local edit is pending is deferred, not applied", asy
"optimistic state must be unchanged after the deferred storage event",
);
// A2: user makes a second edit inside the debounce window. It is derived
// from A1's state (since B1 was deferred), so the result contains A1's
// section plus A2's.
// A2: derived from A1's state (since B1 was deferred).
await act(async () => {
hook.result.current.createSection("A2-Section");
});
@@ -199,11 +225,8 @@ test("storage event while a local edit is pending is deferred, not applied", asy
hook.unmount();
} finally {
cleanup();
relayClient.fetchEvents = origFetch;
relayClient.subscribeLive = origLive;
relayClient.subscribeToReconnects = origReconnect;
relayClient.publishEvent = origPublish;
window.__TAURI_INTERNALS__ = origTauri;
restoreRelay();
restoreTauri();
}
});
@@ -220,46 +243,15 @@ test("live remote while a local edit is pending defers to the pending edit", asy
"./channelSectionsStorage.ts"
);
const origFetch = relayClient.fetchEvents;
const origLive = relayClient.subscribeLive;
const origReconnect = relayClient.subscribeToReconnects;
const origPublish = relayClient.publishEvent;
const origTauri = window.__TAURI_INTERNALS__;
let live = null;
relayClient.fetchEvents = async () => [];
relayClient.subscribeLive = async (_f, cb) => {
live = cb;
return async () => {};
};
relayClient.subscribeToReconnects = () => () => {};
relayClient.publishEvent = async () => {};
window.__TAURI_INTERNALS__ = {
invoke: (cmd) => {
if (cmd === "nip44_decrypt_from_self")
return Promise.resolve(
JSON.stringify({
version: 1,
sections: [{ id: "remote", name: "Remote", order: 0 }],
assignments: {},
}),
);
if (cmd === "nip44_encrypt_to_self") return Promise.resolve("ct");
if (cmd === "sign_event")
return Promise.resolve(
JSON.stringify({
id: "signed",
pubkey: "pk-live-pending",
content: "ct",
created_at: 0,
kind: 30078,
tags: [],
sig: "s",
}),
);
return Promise.reject(new Error(`unmocked ${cmd}`));
},
};
const live = {};
const restoreRelay = stubRelay(relayClient, { live });
const restoreTauri = stubTauri("pk-live-pending", () =>
JSON.stringify({
version: 1,
sections: [{ id: "remote", name: "Remote", order: 0 }],
assignments: {},
}),
);
const pubkey = "pk-live-pending";
const relayUrl = "wss://r.live";
@@ -272,7 +264,7 @@ test("live remote while a local edit is pending defers to the pending edit", asy
await Promise.resolve();
await Promise.resolve();
});
assert.ok(live, "live subscription installed");
assert.ok(live.cb, "live subscription installed");
// Make a local edit — it becomes the pending store and persists to outbox.
await act(async () => {
@@ -286,7 +278,7 @@ test("live remote while a local edit is pending defers to the pending edit", asy
// A remote live event arrives while the edit is still pending.
await act(async () => {
live({
live.cb({
id: "remote-event",
pubkey,
created_at: 500,
@@ -311,11 +303,8 @@ test("live remote while a local edit is pending defers to the pending edit", asy
hook.unmount();
} finally {
cleanup();
relayClient.fetchEvents = origFetch;
relayClient.subscribeLive = origLive;
relayClient.subscribeToReconnects = origReconnect;
relayClient.publishEvent = origPublish;
window.__TAURI_INTERNALS__ = origTauri;
restoreRelay();
restoreTauri();
}
});
@@ -328,35 +317,17 @@ test("equal-timestamp tie-break applies the lower event id (relay canonical winn
const { relayClient } = await import("@/shared/api/relayClient");
const { useChannelSections } = await import("./useChannelSections.ts");
const origFetch = relayClient.fetchEvents;
const origLive = relayClient.subscribeLive;
const origReconnect = relayClient.subscribeToReconnects;
const origTauri = window.__TAURI_INTERNALS__;
let live = null;
relayClient.fetchEvents = async () => [];
relayClient.subscribeLive = async (_f, cb) => {
live = cb;
return async () => {};
};
relayClient.subscribeToReconnects = () => () => {};
// Decrypt payload keyed off the event id embedded in the ciphertext so each
// delivered event yields a distinct store we can assert on.
window.__TAURI_INTERNALS__ = {
invoke: (cmd, args) => {
if (cmd === "nip44_decrypt_from_self") {
const id = args?.ciphertext ?? "";
return Promise.resolve(
JSON.stringify({
version: 1,
sections: [{ id, name: id, order: 0 }],
assignments: {},
}),
);
}
return Promise.reject(new Error(`unmocked ${cmd}`));
},
};
const live = {};
const restoreRelay = stubRelay(relayClient, { live });
// Decrypt payload keyed off the event id so each event yields a distinct store.
const restoreTauri = stubTauri("pk-tie", (args) => {
const id = args?.ciphertext ?? "";
return JSON.stringify({
version: 1,
sections: [{ id, name: id, order: 0 }],
assignments: {},
});
});
const pubkey = "pk-tie";
const relayUrl = "wss://r.tie";
@@ -368,11 +339,11 @@ test("equal-timestamp tie-break applies the lower event id (relay canonical winn
await Promise.resolve();
await Promise.resolve();
});
assert.ok(live, "live subscription installed");
assert.ok(live.cb, "live subscription installed");
const deliver = async (id) => {
await act(async () => {
live({
live.cb({
id,
pubkey,
created_at: 1000,
@@ -386,10 +357,8 @@ test("equal-timestamp tie-break applies the lower event id (relay canonical winn
});
};
// Larger id first (would win under the old <= comparator)...
await deliver("bbbb");
// ...then the lower id at the same timestamp — the relay's canonical winner.
await deliver("aaaa");
await deliver("bbbb"); // larger id first
await deliver("aaaa"); // lower id — relay canonical winner
assert.deepEqual(
hook.result.current.sections.map((s) => s.id),
@@ -399,10 +368,8 @@ test("equal-timestamp tie-break applies the lower event id (relay canonical winn
hook.unmount();
} finally {
cleanup();
relayClient.fetchEvents = origFetch;
relayClient.subscribeLive = origLive;
relayClient.subscribeToReconnects = origReconnect;
window.__TAURI_INTERNALS__ = origTauri;
restoreRelay();
restoreTauri();
}
});
@@ -420,58 +387,26 @@ test("reconnect adopts a remote that advanced while the edit was pending, never
const { relayClient } = await import("@/shared/api/relayClient");
const { useChannelSections } = await import("./useChannelSections.ts");
const origFetch = relayClient.fetchEvents;
const origLive = relayClient.subscribeLive;
const origReconnect = relayClient.subscribeToReconnects;
const origPublish = relayClient.publishEvent;
const origTauri = window.__TAURI_INTERNALS__;
let reconnect = null;
// A single mutable head; bumped to created_at 200 (a remote that won LWW)
// right before the reconnect fires.
const reconnect = {};
const publishCalls = [];
// A mutable head: bumped to 200 right before reconnect fires.
let head = {
pubkey: "pk-sec-recon",
content: "remote-cipher",
created_at: 100,
id: "evt-100",
};
const publishCalls = [];
const origFetch = relayClient.fetchEvents;
relayClient.fetchEvents = async () => [head];
relayClient.subscribeLive = async () => async () => {};
relayClient.subscribeToReconnects = (cb) => {
reconnect = cb;
return () => {};
};
relayClient.publishEvent = async (...args) => {
publishCalls.push(args);
};
window.__TAURI_INTERNALS__ = {
invoke: (cmd) => {
// The head decrypts to a remote store with a single "remote" section.
if (cmd === "nip44_decrypt_from_self")
return Promise.resolve(
JSON.stringify({
version: 1,
sections: [{ id: "remote", name: "Remote", order: 0 }],
assignments: {},
}),
);
if (cmd === "nip44_encrypt_to_self") return Promise.resolve("ct");
if (cmd === "sign_event")
return Promise.resolve(
JSON.stringify({
id: "signed",
pubkey: "pk-sec-recon",
content: "ct",
created_at: 0,
kind: 30078,
tags: [],
sig: "s",
}),
);
return Promise.reject(new Error(`unmocked ${cmd}`));
},
};
const restoreRelay = stubRelay(relayClient, { reconnect, publishCalls });
relayClient.fetchEvents = async () => [head]; // override stubRelay's fetchEvents
const restoreTauri = stubTauri("pk-sec-recon", () =>
JSON.stringify({
version: 1,
sections: [{ id: "remote", name: "Remote", order: 0 }],
assignments: {},
}),
);
const pubkey = "pk-sec-recon";
const relayUrl = "wss://r.recon";
@@ -483,10 +418,8 @@ test("reconnect adopts a remote that advanced while the edit was pending, never
await Promise.resolve();
await Promise.resolve();
});
assert.ok(reconnect, "reconnect handler installed");
assert.ok(reconnect.cb, "reconnect handler installed");
// Local edit while the head stands at created_at 100 — the baseline the
// edit is frozen against.
await act(async () => {
hook.result.current.createSection("Local");
});
@@ -495,8 +428,7 @@ test("reconnect adopts a remote that advanced while the edit was pending, never
"optimistic local section applied",
);
// The remote advances to created_at 200 (a peer won LWW), then reconnect
// fires: the hook re-fetches and wakes the existing generation.
// Remote advances to 200 (peer won LWW), then reconnect fires.
head = {
pubkey: "pk-sec-recon",
content: "remote-cipher",
@@ -504,7 +436,7 @@ test("reconnect adopts a remote that advanced while the edit was pending, never
id: "evt-200",
};
await act(async () => {
reconnect();
reconnect.cb();
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
@@ -514,11 +446,8 @@ test("reconnect adopts a remote that advanced while the edit was pending, never
assert.equal(
publishCalls.length,
0,
"must adopt the advanced remote on reconnect, never publish the stale edit over it",
"must adopt the advanced remote on reconnect, never publish",
);
// On adopt the whole-blob store is replaced by the remote (only a "remote"
// section), so the losing local edit is dropped. Under the mutation
// (re-queue) the pending edit is never adopted away and "Local" survives.
assert.deepEqual(
hook.result.current.sections.map((s) => s.id),
["remote"],
@@ -528,10 +457,8 @@ test("reconnect adopts a remote that advanced while the edit was pending, never
} finally {
cleanup();
relayClient.fetchEvents = origFetch;
relayClient.subscribeLive = origLive;
relayClient.subscribeToReconnects = origReconnect;
relayClient.publishEvent = origPublish;
window.__TAURI_INTERNALS__ = origTauri;
restoreRelay();
restoreTauri();
}
});
@@ -551,18 +478,13 @@ test("legacy replay whose v2 transfer fails (quota) does not write the consumed
);
const { normalizeRelayUrl } = await import("@/shared/lib/normalizeRelayUrl");
const origFetch = relayClient.fetchEvents;
const origLive = relayClient.subscribeLive;
const origReconnect = relayClient.subscribeToReconnects;
const origPublish = relayClient.publishEvent;
const origTauri = window.__TAURI_INTERNALS__;
const origLocalStorage = window.localStorage;
const pubkey = "pk-sec-quota";
const relayUrl = "wss://r.sec-quota";
const scope = `${pubkey}:${encodeURIComponent(normalizeRelayUrl(relayUrl))}`;
const legacyKey = `buzz-channel-sections-outbox.v1:${scope}`;
const v2Prefix = `buzz-channel-sections-outbox.v1:${scope}:`; // nonce/seq suffix
const v2Prefix = `buzz-channel-sections-outbox.v1:${scope}:`;
const legacyRaw = JSON.stringify({
store: {
version: 1,
@@ -572,9 +494,7 @@ test("legacy replay whose v2 transfer fails (quota) does not write the consumed
queuedAt: 0,
});
// A backing localStorage that throws on any v2 outbox write (the quota
// failure), while allowing the legacy read, the marker write, and applied
// state through — so the ONLY failure is the durability transfer.
// localStorage that throws on any v2 outbox write, allowing everything else.
const map = new Map([[legacyKey, legacyRaw]]);
const throwingStorage = {
getItem: (k) => (map.has(k) ? map.get(k) : null),
@@ -590,54 +510,26 @@ test("legacy replay whose v2 transfer fails (quota) does not write the consumed
key: (i) => [...map.keys()][i] ?? null,
};
const head = null;
relayClient.fetchEvents = async () => (head ? [head] : []);
relayClient.subscribeLive = async () => async () => {};
relayClient.subscribeToReconnects = () => () => {};
relayClient.publishEvent = async () => {};
window.__TAURI_INTERNALS__ = {
invoke: (cmd) => {
if (cmd === "nip44_encrypt_to_self") return Promise.resolve("ct");
if (cmd === "sign_event")
return Promise.resolve(
JSON.stringify({
id: "signed",
pubkey,
content: "ct",
created_at: 0,
kind: 30078,
tags: [],
sig: "s",
}),
);
return Promise.reject(new Error(`unmocked ${cmd}`));
},
};
const restoreRelay = stubRelay(relayClient);
const restoreTauri = stubTauri(pubkey, null);
let hook = null;
try {
// Seed a prior watermark so bootstrap holds (no first-sync seed) and the
// legacy blob is the sole outbox record resumed.
Object.defineProperty(window, "localStorage", {
value: throwingStorage,
configurable: true,
});
// Seed watermark so bootstrap holds (no first-sync seed).
window.localStorage.setItem(
`buzz-sync-watermark.v1:channel-sections:${pubkey}:${encodeURIComponent(
normalizeRelayUrl(relayUrl),
)}`,
`buzz-sync-watermark.v1:channel-sections:${pubkey}:${encodeURIComponent(normalizeRelayUrl(relayUrl))}`,
"1700000000",
);
await act(async () => {
hook = renderHook(() => useChannelSections(pubkey, relayUrl));
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
for (let i = 0; i < 4; i++) await Promise.resolve();
});
// The marker must NOT have been written: the v2 transfer threw.
const markerWritten = [...map.keys()].some((k) =>
k.includes("-legacy-consumed:"),
);
@@ -646,17 +538,17 @@ test("legacy replay whose v2 transfer fails (quota) does not write the consumed
false,
"consumed marker must not be written after a failed v2 transfer",
);
// The legacy blob is still enumerated as replayable — not silently lost.
const resumed = readChannelSectionsOutbox(pubkey, relayUrl);
assert.ok(resumed !== null, "legacy blob must remain replayable");
assert.equal(
resumed.store.sections[0]?.id,
"legacy",
"the exact legacy intent survives for a later boot",
"exact legacy intent survives",
);
assert.ok(
resumed.legacyRawToConsume !== null,
"legacy record still reports itself for consumption on a later boot",
"legacy record still reports itself for consumption",
);
hook.unmount();
} finally {
@@ -665,10 +557,7 @@ test("legacy replay whose v2 transfer fails (quota) does not write the consumed
value: origLocalStorage,
configurable: true,
});
relayClient.fetchEvents = origFetch;
relayClient.subscribeLive = origLive;
relayClient.subscribeToReconnects = origReconnect;
relayClient.publishEvent = origPublish;
window.__TAURI_INTERNALS__ = origTauri;
restoreRelay();
restoreTauri();
}
});

View File

@@ -18,16 +18,70 @@ before(() => {
after(() => dom.window.close());
// ── Shared test helpers ───────────────────────────────────────────────────────
/** Install relay no-ops and return a restore function. */
function stubRelay(relayClient, { live, reconnect, publishCalls } = {}) {
const orig = {
fetchEvents: relayClient.fetchEvents,
subscribeLive: relayClient.subscribeLive,
subscribeToReconnects: relayClient.subscribeToReconnects,
publishEvent: relayClient.publishEvent,
};
relayClient.fetchEvents = async () => [];
relayClient.subscribeLive = async (_f, cb) => {
if (live) live.cb = cb;
return async () => {};
};
relayClient.subscribeToReconnects = (cb) => {
if (reconnect) reconnect.cb = cb;
return () => {};
};
relayClient.publishEvent = async (...args) => {
if (publishCalls) publishCalls.push(args);
};
return () => Object.assign(relayClient, orig);
}
/**
* Install a Tauri mock for sort tests.
* - encrypt returns "ct" / sign returns a minimal event
* - decrypt returns `decryptPayload` (JSON string or callback)
*/
function stubTauri(pubkey, decryptPayload) {
const orig = window.__TAURI_INTERNALS__;
window.__TAURI_INTERNALS__ = {
invoke: (cmd, args) => {
if (cmd === "nip44_decrypt_from_self") {
const payload =
typeof decryptPayload === "function"
? decryptPayload(args)
: decryptPayload;
return Promise.resolve(payload);
}
if (cmd === "nip44_encrypt_to_self") return Promise.resolve("ct");
if (cmd === "sign_event")
return Promise.resolve(
JSON.stringify({
id: "signed",
pubkey,
content: "ct",
created_at: 0,
kind: 30078,
tags: [],
sig: "s",
}),
);
return Promise.reject(new Error(`unmocked ${cmd}`));
},
};
return () => {
window.__TAURI_INTERNALS__ = orig;
};
}
// Carl P1.1 regression (sort): a peer-cache storage event arriving while a
// local whole-blob sort edit is pending must NOT clobber the optimistic edit.
// The storage handler must defer to the pending edit just as `applyRemote` does
// for relay arrivals. Peer writes arrive immediately when nothing is pending.
//
// Scenario: window A sets sort mode for "channels" (pending). Window B writes
// B1 (sort mode "recent" for "channels") to the shared cache. A's storage event
// fires. A2 is then set inside the debounce window. The resulting sort mode for
// A must reflect A2, not B1's value — B1's write was ignored while A1 was live.
//
// Mutation: removing the `hasPendingEdit()` guard in the storage handler lets
// B1 apply, so A2's `setStore` updater derives from B1, and the visible mode
// reverts to B1's value.
@@ -39,34 +93,8 @@ test("storage event while a local sort edit is pending is deferred, not applied"
);
const { storageKey } = await import("./channelSortPreference.ts");
const origFetch = relayClient.fetchEvents;
const origLive = relayClient.subscribeLive;
const origReconnect = relayClient.subscribeToReconnects;
const origPublish = relayClient.publishEvent;
const origTauri = window.__TAURI_INTERNALS__;
relayClient.fetchEvents = async () => [];
relayClient.subscribeLive = async () => async () => {};
relayClient.subscribeToReconnects = () => () => {};
relayClient.publishEvent = async () => {};
window.__TAURI_INTERNALS__ = {
invoke: (cmd) => {
if (cmd === "nip44_encrypt_to_self") return Promise.resolve("ct");
if (cmd === "sign_event")
return Promise.resolve(
JSON.stringify({
id: "signed",
pubkey: "pk-sort-sg",
content: "ct",
created_at: 0,
kind: 30078,
tags: [],
sig: "s",
}),
);
return Promise.reject(new Error(`unmocked ${cmd}`));
},
};
const restoreRelay = stubRelay(relayClient);
const restoreTauri = stubTauri("pk-sort-sg", null);
const pubkey = "pk-sort-sg";
const relayUrl = "wss://r.sort-sg";
@@ -88,8 +116,7 @@ test("storage event while a local sort edit is pending is deferred, not applied"
"A1: channels sort set to alpha",
);
// Window B writes "recent" for "channels" to the shared cache and fires a
// storage event.
// Window B writes "recent" for "channels" to the shared cache.
const b1Store = JSON.stringify({
version: 1,
groups: { channels: "recent" },
@@ -107,40 +134,33 @@ test("storage event while a local sort edit is pending is deferred, not applied"
await Promise.resolve();
});
// B1's "recent" must NOT have replaced A1's "alpha".
assert.equal(
hook.result.current.sortModeFor("channels"),
"alpha",
"storage event while pending must not apply B1 (recent) over A1 (alpha)",
);
// A2: set "starred" to "recent" — derived from A1 state (channels stays alpha).
// A2: derived from A1 state (channels stays alpha).
await act(async () => {
hook.result.current.setSortModeFor("starred", "recent");
});
// channels must still reflect A1 (alpha), not B1 (recent).
assert.equal(
hook.result.current.sortModeFor("channels"),
"alpha",
"A2 must be derived from A1 state — channels must remain alpha, not revert to B1 recent",
"A2 must be derived from A1 state — channels must remain alpha",
);
hook.unmount();
} finally {
cleanup();
relayClient.fetchEvents = origFetch;
relayClient.subscribeLive = origLive;
relayClient.subscribeToReconnects = origReconnect;
relayClient.publishEvent = origPublish;
window.__TAURI_INTERNALS__ = origTauri;
restoreRelay();
restoreTauri();
}
});
// Carl P1-sort regression: a live remote arriving while a local sort edit is
// still pending must NOT overwrite the optimistic edit or strand its durable
// outbox. Reverting applyRemote's `hasPendingEdit()` guard (or restoring the
// old `cancelPendingPublish()` on remote arrival) lets the remote clobber the
// pending edit and drop it.
// Carl P1-sort regression: a live remote while a local sort edit is pending
// must NOT overwrite the optimistic edit or strand its durable outbox.
// Mutation: reverting applyRemote's hasPendingEdit() guard lets the remote clobber.
test("live remote while a local edit is pending defers to the pending edit", async () => {
const { act, cleanup, renderHook } = await import("@testing-library/react");
const { relayClient } = await import("@/shared/api/relayClient");
@@ -149,49 +169,14 @@ test("live remote while a local edit is pending defers to the pending edit", asy
);
const { readChannelSortOutbox } = await import("./channelSortPreference.ts");
const origFetch = relayClient.fetchEvents;
const origLive = relayClient.subscribeLive;
const origReconnect = relayClient.subscribeToReconnects;
const origPublish = relayClient.publishEvent;
const origTauri = window.__TAURI_INTERNALS__;
let live = null;
relayClient.fetchEvents = async () => [];
relayClient.subscribeLive = async (_f, cb) => {
live = cb;
return async () => {};
};
relayClient.subscribeToReconnects = () => () => {};
relayClient.publishEvent = async () => {};
window.__TAURI_INTERNALS__ = {
invoke: (cmd) => {
if (cmd === "nip44_decrypt_from_self")
return Promise.resolve(
JSON.stringify({
version: 1,
groups: { "remote-group": "recent" },
}),
);
if (cmd === "nip44_encrypt_to_self") return Promise.resolve("ct");
if (cmd === "sign_event")
return Promise.resolve(
JSON.stringify({
id: "signed",
pubkey: "pk-sort-pending",
content: "ct",
created_at: 0,
kind: 30078,
tags: [],
sig: "s",
}),
);
return Promise.reject(new Error(`unmocked ${cmd}`));
},
};
const live = {};
const restoreRelay = stubRelay(relayClient, { live });
const restoreTauri = stubTauri("pk-sort-pending", () =>
JSON.stringify({ version: 1, groups: { "remote-group": "recent" } }),
);
const pubkey = "pk-sort-pending";
const relayUrl = "wss://r.live";
let hook = null;
try {
await act(async () => {
@@ -200,9 +185,8 @@ test("live remote while a local edit is pending defers to the pending edit", asy
await Promise.resolve();
await Promise.resolve();
});
assert.ok(live, "live subscription installed");
assert.ok(live.cb, "live subscription installed");
// Make a local edit — it becomes the pending store and persists to outbox.
await act(async () => {
hook.result.current.setSortModeFor("channels", "recent");
});
@@ -216,9 +200,8 @@ test("live remote while a local edit is pending defers to the pending edit", asy
"optimistic local edit applied",
);
// A remote live event arrives while the edit is still pending.
await act(async () => {
live({
live.cb({
id: "remote-event",
pubkey,
created_at: 500,
@@ -234,7 +217,7 @@ test("live remote while a local edit is pending defers to the pending edit", asy
assert.equal(
hook.result.current.sortModeFor("channels"),
"recent",
"pending local edit must NOT be overwritten by the live remote",
"pending local edit must NOT be overwritten",
);
assert.equal(
hook.result.current.sortModeFor("remote-group"),
@@ -248,19 +231,13 @@ test("live remote while a local edit is pending defers to the pending edit", asy
hook.unmount();
} finally {
cleanup();
relayClient.fetchEvents = origFetch;
relayClient.subscribeLive = origLive;
relayClient.subscribeToReconnects = origReconnect;
relayClient.publishEvent = origPublish;
window.__TAURI_INTERNALS__ = origTauri;
restoreRelay();
restoreTauri();
}
});
// Equal-timestamp tie-break must match the relay's canonical winner
// (`created_at DESC, id ASC` → LOWEST id wins). Deliver the larger id first,
// then the lower id at the same timestamp; the lower id is the stored winner
// and its whole-blob store must replace the applied state. Reverting
// applyRemote's `>=` back to `<=` wrongly ignores the lower id (the relay winner).
// Equal-timestamp tie-break must match the relay's canonical winner (lowest id wins).
// Mutation: reverting applyRemote's `>=` back to `<=` wrongly ignores the lower id.
test("equal-timestamp tie-break applies the lower event id (relay canonical winner)", async () => {
const { act, cleanup, renderHook } = await import("@testing-library/react");
const { relayClient } = await import("@/shared/api/relayClient");
@@ -268,31 +245,13 @@ test("equal-timestamp tie-break applies the lower event id (relay canonical winn
"./useChannelSortPreference.ts"
);
const origFetch = relayClient.fetchEvents;
const origLive = relayClient.subscribeLive;
const origReconnect = relayClient.subscribeToReconnects;
const origTauri = window.__TAURI_INTERNALS__;
let live = null;
relayClient.fetchEvents = async () => [];
relayClient.subscribeLive = async (_f, cb) => {
live = cb;
return async () => {};
};
relayClient.subscribeToReconnects = () => () => {};
// Decrypt payload keyed off the event id embedded in the ciphertext so each
// delivered event yields a store setting a distinct group's mode to "recent".
window.__TAURI_INTERNALS__ = {
invoke: (cmd, args) => {
if (cmd === "nip44_decrypt_from_self") {
const id = args?.ciphertext ?? "";
return Promise.resolve(
JSON.stringify({ version: 1, groups: { [id]: "recent" } }),
);
}
return Promise.reject(new Error(`unmocked ${cmd}`));
},
};
const live = {};
const restoreRelay = stubRelay(relayClient, { live });
// Decrypt echoes ciphertext as the group key so each event yields a distinct store.
const restoreTauri = stubTauri("pk-sort-tie", (args) => {
const id = args?.ciphertext ?? "";
return JSON.stringify({ version: 1, groups: { [id]: "recent" } });
});
const pubkey = "pk-sort-tie";
const relayUrl = "wss://r.tie";
@@ -304,15 +263,15 @@ test("equal-timestamp tie-break applies the lower event id (relay canonical winn
await Promise.resolve();
await Promise.resolve();
});
assert.ok(live, "live subscription installed");
assert.ok(live.cb, "live subscription installed");
const deliver = async (id) => {
await act(async () => {
live({
live.cb({
id,
pubkey,
created_at: 1000,
content: id, // decrypt echoes this into the group key
content: id,
kind: 30078,
tags: [["d", "channel-sort"]],
sig: "s",
@@ -322,15 +281,13 @@ test("equal-timestamp tie-break applies the lower event id (relay canonical winn
});
};
// Larger id first (applied), then the lower id at the same timestamp — the
// relay's canonical winner, whose whole-blob store must replace the state.
await deliver("bbbb");
await deliver("aaaa");
assert.equal(
hook.result.current.sortModeFor("aaaa"),
"recent",
"lower event id (relay canonical winner) must be applied, not rejected",
"lower event id (relay canonical winner) must be applied",
);
assert.equal(
hook.result.current.sortModeFor("bbbb"),
@@ -340,21 +297,15 @@ test("equal-timestamp tie-break applies the lower event id (relay canonical winn
hook.unmount();
} finally {
cleanup();
relayClient.fetchEvents = origFetch;
relayClient.subscribeLive = origLive;
relayClient.subscribeToReconnects = origReconnect;
window.__TAURI_INTERNALS__ = origTauri;
restoreRelay();
restoreTauri();
}
});
// Carl P1-sort regression: on reconnect the hook must wake the EXISTING pending
// edit (retryPendingPublish) rather than re-queue it via publishSortPrefs().
// A re-queue bumps the generation and resets the frozen publishBaseline to the
// just-fetched head, so a remote that won whole-blob LWW while the edit was
// pending would be published over instead of adopted. Here the head advances
// (100 → 200) while a local edit is pending; on reconnect the advanced remote
// must be adopted and NOTHING published. Reverting the reconnect handler to
// publishSortPrefs(pending) resets the baseline and publishes the stale edit.
// Mutation: reverting the reconnect handler to publishSortPrefs(pending) resets
// the baseline and publishes the stale edit over the advanced remote.
test("reconnect adopts a remote that advanced while the edit was pending, never publishing over it", async () => {
const { act, cleanup, renderHook } = await import("@testing-library/react");
const { relayClient } = await import("@/shared/api/relayClient");
@@ -362,54 +313,21 @@ test("reconnect adopts a remote that advanced while the edit was pending, never
"./useChannelSortPreference.ts"
);
const origFetch = relayClient.fetchEvents;
const origLive = relayClient.subscribeLive;
const origReconnect = relayClient.subscribeToReconnects;
const origPublish = relayClient.publishEvent;
const origTauri = window.__TAURI_INTERNALS__;
let reconnect = null;
// A single mutable head the fetch returns; bumped to created_at 200 (a remote
// that won LWW) right before the reconnect fires.
const reconnect = {};
const publishCalls = [];
let head = {
pubkey: "pk-sort-recon",
content: "remote-cipher",
created_at: 100,
id: "evt-100",
};
const publishCalls = [];
const origFetch = relayClient.fetchEvents;
relayClient.fetchEvents = async () => [head];
relayClient.subscribeLive = async () => async () => {};
relayClient.subscribeToReconnects = (cb) => {
reconnect = cb;
return () => {};
};
relayClient.publishEvent = async (...args) => {
publishCalls.push(args);
};
window.__TAURI_INTERNALS__ = {
invoke: (cmd) => {
// The head decrypts to a remote store setting `remote-group`.
if (cmd === "nip44_decrypt_from_self")
return Promise.resolve(
JSON.stringify({ version: 1, groups: { "remote-group": "recent" } }),
);
if (cmd === "nip44_encrypt_to_self") return Promise.resolve("ct");
if (cmd === "sign_event")
return Promise.resolve(
JSON.stringify({
id: "signed",
pubkey: "pk-sort-recon",
content: "ct",
created_at: 0,
kind: 30078,
tags: [],
sig: "s",
}),
);
return Promise.reject(new Error(`unmocked ${cmd}`));
},
};
const restoreRelay = stubRelay(relayClient, { reconnect, publishCalls });
relayClient.fetchEvents = async () => [head]; // override stubRelay's fetchEvents
const restoreTauri = stubTauri("pk-sort-recon", () =>
JSON.stringify({ version: 1, groups: { "remote-group": "recent" } }),
);
const pubkey = "pk-sort-recon";
const relayUrl = "wss://r.recon";
@@ -421,16 +339,12 @@ test("reconnect adopts a remote that advanced while the edit was pending, never
await Promise.resolve();
await Promise.resolve();
});
assert.ok(reconnect, "reconnect handler installed");
assert.ok(reconnect.cb, "reconnect handler installed");
// Local edit while the head stands at created_at 100 — the baseline the
// edit is frozen against.
await act(async () => {
hook.result.current.setSortModeFor("channels", "recent");
});
// The remote advances to created_at 200 (a peer won LWW), then reconnect
// fires: the hook re-fetches and wakes the existing generation.
head = {
pubkey: "pk-sort-recon",
content: "remote-cipher",
@@ -438,7 +352,7 @@ test("reconnect adopts a remote that advanced while the edit was pending, never
id: "evt-200",
};
await act(async () => {
reconnect();
reconnect.cb();
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
@@ -448,16 +362,12 @@ test("reconnect adopts a remote that advanced while the edit was pending, never
assert.equal(
publishCalls.length,
0,
"must adopt the advanced remote on reconnect, never publish the stale edit over it",
"must adopt the advanced remote on reconnect, never publish",
);
// On adopt the whole-blob store is replaced by the remote (which has no
// `channels` entry), so the losing local edit is dropped and `channels`
// reverts to the default. Under the mutation (re-queue) the pending edit is
// never adopted away and `channels` stays "recent".
assert.equal(
hook.result.current.sortModeFor("channels"),
"alpha",
"the losing local edit must be adopted away by the advanced remote",
"the losing local edit must be adopted away",
);
assert.equal(
hook.result.current.sortModeFor("remote-group"),
@@ -468,21 +378,14 @@ test("reconnect adopts a remote that advanced while the edit was pending, never
} finally {
cleanup();
relayClient.fetchEvents = origFetch;
relayClient.subscribeLive = origLive;
relayClient.subscribeToReconnects = origReconnect;
relayClient.publishEvent = origPublish;
window.__TAURI_INTERNALS__ = origTauri;
restoreRelay();
restoreTauri();
}
});
// Thufir pass-3 blocker (Carl P1 durability): a legacy (old-build) blob is
// replayed by copying it into this window's own v2 outbox key, then writing a
// "consumed" marker. writeOwnOutbox swallows setItem failures; the hook must
// therefore write the marker ONLY when the v2 transfer actually succeeded.
// Here the v2 outbox setItem throws (quota), so the transfer failed: the marker
// must NOT be written, leaving the legacy blob replayable on a later boot rather
// than silently suppressed. Mutation: writing the marker unconditionally (the
// pre-fix code) consumes the legacy blob after a failed transfer and it is lost.
// Thufir pass-3 blocker: a legacy blob whose v2 transfer fails (quota) must NOT
// write the consumed marker. Mutation: writing the marker unconditionally
// consumes the legacy blob after a failed transfer and it is lost.
test("legacy replay whose v2 transfer fails (quota) does not write the consumed marker", async () => {
const { act, cleanup, renderHook } = await import("@testing-library/react");
const { relayClient } = await import("@/shared/api/relayClient");
@@ -492,26 +395,18 @@ test("legacy replay whose v2 transfer fails (quota) does not write the consumed
const { readChannelSortOutbox } = await import("./channelSortPreference.ts");
const { normalizeRelayUrl } = await import("@/shared/lib/normalizeRelayUrl");
const origFetch = relayClient.fetchEvents;
const origLive = relayClient.subscribeLive;
const origReconnect = relayClient.subscribeToReconnects;
const origPublish = relayClient.publishEvent;
const origTauri = window.__TAURI_INTERNALS__;
const origLocalStorage = window.localStorage;
const pubkey = "pk-quota";
const relayUrl = "wss://r.quota";
const scope = `${pubkey}:${encodeURIComponent(normalizeRelayUrl(relayUrl))}`;
const legacyKey = `buzz-channel-sort-outbox.v1:${scope}`;
const v2Prefix = `buzz-channel-sort-outbox.v1:${scope}:`; // nonce/seq suffix
const v2Prefix = `buzz-channel-sort-outbox.v1:${scope}:`;
const legacyRaw = JSON.stringify({
store: { version: 1, groups: { "legacy-group": "recent" } },
queuedAt: 0,
});
// A backing localStorage that throws on any v2 outbox write (the quota
// failure), while allowing the legacy read, the marker write, and applied
// state through — so the ONLY failure is the durability transfer.
const map = new Map([[legacyKey, legacyRaw]]);
const throwingStorage = {
getItem: (k) => (map.has(k) ? map.get(k) : null),
@@ -527,42 +422,17 @@ test("legacy replay whose v2 transfer fails (quota) does not write the consumed
key: (i) => [...map.keys()][i] ?? null,
};
const head = null;
relayClient.fetchEvents = async () => (head ? [head] : []);
relayClient.subscribeLive = async () => async () => {};
relayClient.subscribeToReconnects = () => () => {};
relayClient.publishEvent = async () => {};
window.__TAURI_INTERNALS__ = {
invoke: (cmd) => {
if (cmd === "nip44_encrypt_to_self") return Promise.resolve("ct");
if (cmd === "sign_event")
return Promise.resolve(
JSON.stringify({
id: "signed",
pubkey,
content: "ct",
created_at: 0,
kind: 30078,
tags: [],
sig: "s",
}),
);
return Promise.reject(new Error(`unmocked ${cmd}`));
},
};
const restoreRelay = stubRelay(relayClient);
const restoreTauri = stubTauri(pubkey, null);
let hook = null;
try {
// Seed a prior watermark so bootstrap holds (no first-sync seed) and the
// legacy blob is the sole outbox record resumed.
Object.defineProperty(window, "localStorage", {
value: throwingStorage,
configurable: true,
});
window.localStorage.setItem(
`buzz-sync-watermark.v1:channel-sort:${pubkey}:${encodeURIComponent(
normalizeRelayUrl(relayUrl),
)}`,
`buzz-sync-watermark.v1:channel-sort:${pubkey}:${encodeURIComponent(normalizeRelayUrl(relayUrl))}`,
"1700000000",
);
@@ -574,7 +444,6 @@ test("legacy replay whose v2 transfer fails (quota) does not write the consumed
await Promise.resolve();
});
// The marker must NOT have been written: the v2 transfer threw.
const markerWritten = [...map.keys()].some((k) =>
k.includes("-legacy-consumed:"),
);
@@ -583,17 +452,16 @@ test("legacy replay whose v2 transfer fails (quota) does not write the consumed
false,
"consumed marker must not be written after a failed v2 transfer",
);
// The legacy blob is still enumerated as replayable — not silently lost.
const resumed = readChannelSortOutbox(pubkey, relayUrl);
assert.ok(resumed !== null, "legacy blob must remain replayable");
assert.equal(
resumed.store.groups["legacy-group"],
"recent",
"the exact legacy intent survives for a later boot",
"the exact legacy intent survives",
);
assert.ok(
resumed.legacyRawToConsume !== null,
"legacy record still reports itself for consumption on a later boot",
"legacy record still reports itself for consumption",
);
hook.unmount();
} finally {
@@ -602,10 +470,7 @@ test("legacy replay whose v2 transfer fails (quota) does not write the consumed
value: origLocalStorage,
configurable: true,
});
relayClient.fetchEvents = origFetch;
relayClient.subscribeLive = origLive;
relayClient.subscribeToReconnects = origReconnect;
relayClient.publishEvent = origPublish;
window.__TAURI_INTERNALS__ = origTauri;
restoreRelay();
restoreTauri();
}
});

File diff suppressed because it is too large Load Diff