Skip to content

Commit 16aeab6

Browse files
Duncanwpfleger96
andcommitted
test(sidebar): make the two accepted-residual tests faithful to their contract
The hard-eviction-branch test asserted only the rev tuple outcome on two one-entry stores while its comment claimed the >500-entry eviction/remount setup. Build the real fixture: >500 equal-updatedAt entries so the bound evicts the target by the id tiebreak, then a remounted rev-1 click merged against the retained rev-100 remote, asserting the deterministic rev-100 outcome in both merge orders. Stars and mutes. The unobserved-future mixed-fleet residual was stated but never exercised directly: the fast-clock suites cover only the observed-future fix. Add a click with an empty high-water at t, then a genuinely unobserved opposite-value head at t+300 that wins on the primary updatedAt key. Both hook suites. Test-only; no production source changes. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
1 parent 439df98 commit 16aeab6

4 files changed

Lines changed: 199 additions & 18 deletions

File tree

desktop/src/features/sidebar/lib/channelMutesStorage.test.mjs

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -378,19 +378,47 @@ test("finding 3 easy branch: a fresh click beats an evicted high-rev entry at an
378378
// here as the hard branch (not disguised as safety).
379379
test("finding 3 hard branch: equal-second evicted click (rev 1) loses to observed remote (rev 100)", () => {
380380
const NOW = 777;
381-
// Remount click on the evicted channel: empty high-water → rev 1, updatedAt=NOW.
382-
const click = S(E(true, NOW, 1));
383-
// The previously observed remote for the same channel at the same second,
384-
// rev 100 (it may precede the remount — not genuinely concurrent).
385-
const remote = S(E(false, NOW, 100));
386-
const merged = mergeStores(click, remote);
381+
const TARGET = "aaa-target"; // lexicographically small → evicted by the id tiebreak
382+
383+
// >500 entries all at the CURRENT second (NOW). With MAX+1 equal-updatedAt
384+
// entries, boundMuteStore sorts ascending by (updatedAt, id) and keeps the
385+
// highest MAX, so the lowest id is evicted — TARGET, NOT because it is old.
386+
const channels = { [TARGET]: E(true, NOW, 7) };
387+
for (let i = 0; i < MAX_CHANNEL_MUTE_ENTRIES; i++) {
388+
channels[`z-${String(i).padStart(3, "0")}`] = E(true, NOW, 0);
389+
}
390+
const bounded = boundMuteStore({ version: 1, channels });
391+
assert.equal(
392+
Object.keys(bounded.channels).length,
393+
MAX_CHANNEL_MUTE_ENTRIES,
394+
"bound trims to the cap",
395+
);
396+
assert.equal(
397+
bounded.channels[TARGET],
398+
undefined,
399+
"TARGET evicted by the id tiebreak at equal updatedAt",
400+
);
401+
402+
// Remount in the same second: TARGET's rev high-water is gone with the entry,
403+
// so a fresh click mints rev 1 at updatedAt=NOW.
404+
const click = { version: 1, channels: { [TARGET]: E(true, NOW, 1) } };
405+
// The previously observed remote for TARGET at the same second, rev 100 (it
406+
// may precede the remount — not genuinely concurrent).
407+
const remote = { version: 1, channels: { [TARGET]: E(false, NOW, 100) } };
408+
409+
// equal updatedAt → rev decides → 100 > 1: the click LOSES. Documented
410+
// deterministic residual, proven here through real eviction+remount, not a
411+
// pre-shrunk tuple. Deterministic in both merge orders — a lost click, never
412+
// a divergence.
387413
assert.deepEqual(
388-
merged.channels.c,
414+
mergeStores(click, remote).channels[TARGET],
389415
E(false, NOW, 100),
390416
"equal updatedAt → higher rev wins deterministically (documented residual)",
391417
);
392-
// Deterministic either merge order — a lost click, never a divergence.
393-
assert.deepEqual(mergeStores(remote, click).channels.c, E(false, NOW, 100));
418+
assert.deepEqual(
419+
mergeStores(remote, click).channels[TARGET],
420+
E(false, NOW, 100),
421+
);
394422
});
395423

396424
// ── mutedChannelIdsFromStore ────────────────────────────────────────────────

desktop/src/features/sidebar/lib/channelStarsStorage.test.mjs

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -378,19 +378,47 @@ test("finding 3 easy branch: a fresh click beats an evicted high-rev entry at an
378378
// here as the hard branch (not disguised as safety).
379379
test("finding 3 hard branch: equal-second evicted click (rev 1) loses to observed remote (rev 100)", () => {
380380
const NOW = 777;
381-
// Remount click on the evicted channel: empty high-water → rev 1, updatedAt=NOW.
382-
const click = S(E(true, NOW, 1));
383-
// The previously observed remote for the same channel at the same second,
384-
// rev 100 (it may precede the remount — not genuinely concurrent).
385-
const remote = S(E(false, NOW, 100));
386-
const merged = mergeStores(click, remote);
381+
const TARGET = "aaa-target"; // lexicographically small → evicted by the id tiebreak
382+
383+
// >500 entries all at the CURRENT second (NOW). With MAX+1 equal-updatedAt
384+
// entries, boundStarStore sorts ascending by (updatedAt, id) and keeps the
385+
// highest MAX, so the lowest id is evicted — TARGET, NOT because it is old.
386+
const channels = { [TARGET]: E(true, NOW, 7) };
387+
for (let i = 0; i < MAX_CHANNEL_STAR_ENTRIES; i++) {
388+
channels[`z-${String(i).padStart(3, "0")}`] = E(true, NOW, 0);
389+
}
390+
const bounded = boundStarStore({ version: 1, channels });
391+
assert.equal(
392+
Object.keys(bounded.channels).length,
393+
MAX_CHANNEL_STAR_ENTRIES,
394+
"bound trims to the cap",
395+
);
396+
assert.equal(
397+
bounded.channels[TARGET],
398+
undefined,
399+
"TARGET evicted by the id tiebreak at equal updatedAt",
400+
);
401+
402+
// Remount in the same second: TARGET's rev high-water is gone with the entry,
403+
// so a fresh click mints rev 1 at updatedAt=NOW.
404+
const click = { version: 1, channels: { [TARGET]: E(true, NOW, 1) } };
405+
// The previously observed remote for TARGET at the same second, rev 100 (it
406+
// may precede the remount — not genuinely concurrent).
407+
const remote = { version: 1, channels: { [TARGET]: E(false, NOW, 100) } };
408+
409+
// equal updatedAt → rev decides → 100 > 1: the click LOSES. Documented
410+
// deterministic residual, proven here through real eviction+remount, not a
411+
// pre-shrunk tuple. Deterministic in both merge orders — a lost click, never
412+
// a divergence.
387413
assert.deepEqual(
388-
merged.channels.c,
414+
mergeStores(click, remote).channels[TARGET],
389415
E(false, NOW, 100),
390416
"equal updatedAt → higher rev wins deterministically (documented residual)",
391417
);
392-
// Deterministic either merge order — a lost click, never a divergence.
393-
assert.deepEqual(mergeStores(remote, click).channels.c, E(false, NOW, 100));
418+
assert.deepEqual(
419+
mergeStores(remote, click).channels[TARGET],
420+
E(false, NOW, 100),
421+
);
394422
});
395423

396424
// ── starredChannelIdsFromStore ────────────────────────────────────────────────

desktop/src/features/sidebar/lib/useChannelMutes.test.mjs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,69 @@ test("empty-store click survives a later higher-rev head with an older updatedAt
327327
}
328328
});
329329

330+
// Unobserved-future residual (accepted mixed-fleet case, MINOR-2 contrast to the
331+
// fast-clock fix): a click mints at wall-clock t with an empty high-water; a
332+
// genuinely UNOBSERVED opposite-value head then arrives at t+300 and wins on the
333+
// primary updatedAt key. The logical-monotonic stamp only defends against state
334+
// the replica already observed — a future head it never saw before clicking is
335+
// not covered, exactly as under today's shipped LWW.
336+
test("unobserved future head wins over an empty-high-water click on primary updatedAt", async () => {
337+
const { act, cleanup, renderHook } = await import("@testing-library/react");
338+
const { relayClient } = await import("@/shared/api/relayClient");
339+
const { useChannelMutes } = await import("./useChannelMutes.ts");
340+
341+
const live = {};
342+
const restore = stubRelay(relayClient, { live });
343+
const origTauri = window.__TAURI_INTERNALS__;
344+
const origDateNow = Date.now;
345+
Date.now = () => 1000 * 1_000; // click at updatedAt 1000 (t)
346+
window.__TAURI_INTERNALS__ = {
347+
invoke: (cmd) => {
348+
// fetchEvents is stubbed empty, so bootstrap/pre-publish never decrypt;
349+
// the only decrypt is the live head — the genuinely unobserved future
350+
// entry at updatedAt 1300, delivered after the empty-high-water click.
351+
if (cmd === "nip44_decrypt_from_self")
352+
return Promise.resolve(
353+
mutePayload({ shared: { muted: false, updatedAt: 1300, rev: 1 } }),
354+
);
355+
return Promise.reject(new Error(`unmocked ${cmd}`));
356+
},
357+
};
358+
const pubkey = "pk-unobserved-future";
359+
let hook = null;
360+
try {
361+
await act(async () => {
362+
hook = renderHook(() => useChannelMutes(pubkey, "wss://r"));
363+
for (let i = 0; i < 20; i++) await Promise.resolve();
364+
});
365+
await act(async () => hook.result.current.muteChannel("shared")); // empty store → rev 1 @ 1000
366+
await act(async () => {
367+
// Genuinely unobserved head at t+300 (updatedAt 1300), opposite value.
368+
live.cb({
369+
id: "unobserved-future",
370+
pubkey,
371+
created_at: 1300,
372+
content: "cipher",
373+
kind: 30078,
374+
tags: [["d", "channel-mutes"]],
375+
sig: "s",
376+
});
377+
for (let i = 0; i < 20; i++) await Promise.resolve();
378+
});
379+
assert.equal(
380+
hook.result.current.mutedChannelIds.has("shared"),
381+
false,
382+
"unobserved future head wins on primary updatedAt (accepted residual)",
383+
);
384+
hook.unmount();
385+
} finally {
386+
cleanup();
387+
Date.now = origDateNow;
388+
window.__TAURI_INTERNALS__ = origTauri;
389+
restore();
390+
}
391+
});
392+
330393
// Cross-window storage: a peer window's write is observed into the high-water
331394
// and max-merged, so a following click sees the peer's rev and no edit is lost.
332395
test("cross-window storage event is observed and max-merged", async () => {

desktop/src/features/sidebar/lib/useChannelStars.test.mjs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,68 @@ test("empty-store click survives a later higher-rev head with an older updatedAt
327327
}
328328
});
329329

330+
// Unobserved-future residual (accepted mixed-fleet case, MINOR-2 contrast to the
331+
// fast-clock fix): a click mints at wall-clock t with an empty high-water; a
332+
// genuinely UNOBSERVED opposite-value head then arrives at t+300 and wins on the
333+
// primary updatedAt key. The logical-monotonic stamp only defends against state
334+
// the replica already observed — a future head it never saw before clicking is
335+
// not covered, exactly as under today's shipped LWW.
336+
test("unobserved future head wins over an empty-high-water click on primary updatedAt", async () => {
337+
const { act, cleanup, renderHook } = await import("@testing-library/react");
338+
const { relayClient } = await import("@/shared/api/relayClient");
339+
const { useChannelStars } = await import("./useChannelStars.ts");
340+
341+
const live = {};
342+
const restore = stubRelay(relayClient, { live });
343+
const origTauri = window.__TAURI_INTERNALS__;
344+
const origDateNow = Date.now;
345+
Date.now = () => 1000 * 1_000; // click at updatedAt 1000 (t)
346+
window.__TAURI_INTERNALS__ = {
347+
invoke: (cmd) => {
348+
// fetchEvents is stubbed empty, so bootstrap/pre-publish never decrypt;
349+
// the only decrypt is the live head — the genuinely unobserved future
350+
// entry at updatedAt 1300, delivered after the empty-high-water click.
351+
if (cmd === "nip44_decrypt_from_self")
352+
return Promise.resolve(
353+
starPayload({ shared: { starred: false, updatedAt: 1300, rev: 1 } }),
354+
);
355+
return Promise.reject(new Error(`unmocked ${cmd}`));
356+
},
357+
};
358+
const pubkey = "pk-unobserved-future";
359+
let hook = null;
360+
try {
361+
await act(async () => {
362+
hook = renderHook(() => useChannelStars(pubkey, "wss://r"));
363+
for (let i = 0; i < 20; i++) await Promise.resolve();
364+
});
365+
await act(async () => hook.result.current.starChannel("shared")); // empty store → rev 1 @ 1000
366+
await act(async () => {
367+
// Genuinely unobserved head at t+300 (updatedAt 1300), opposite value.
368+
live.cb({
369+
id: "unobserved-future",
370+
pubkey,
371+
created_at: 1300,
372+
content: "cipher",
373+
kind: 30078,
374+
tags: [["d", "channel-stars"]],
375+
sig: "s",
376+
});
377+
for (let i = 0; i < 20; i++) await Promise.resolve();
378+
});
379+
assert.equal(
380+
hook.result.current.starredChannelIds.has("shared"),
381+
false,
382+
"unobserved future head wins on primary updatedAt (accepted residual)",
383+
);
384+
hook.unmount();
385+
} finally {
386+
cleanup();
387+
Date.now = origDateNow;
388+
window.__TAURI_INTERNALS__ = origTauri;
389+
restore();
390+
}
391+
});
330392
// Cross-window storage: a peer window's write is observed into the high-water
331393
// and max-merged, so a following click sees the peer's rev and no edit is lost.
332394
test("cross-window storage event is observed and max-merged", async () => {

0 commit comments

Comments
 (0)