From ad0705e711eb4e8b1ade0f465b369591f64c518c Mon Sep 17 00:00:00 2001 From: npub1g8493u0xfsjrvflg4n08ezd7vec99mnwzlv0qgwpr9d7gvjwhuzqx59rhw <41ea58f1e64c243627e8acde7c89be667052ee6e17d8f021c1195be4324ebf04@buzz.block.builderlab.xyz> Date: Fri, 31 Jul 2026 17:20:53 -0400 Subject: [PATCH] test(read-state): add overlay and mark-all partial-refusal witnesses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add two tests that were missing from the pass-1 fix commit: - applyOverrideUnread_refusal_caller_must_not_mutate_overlay: witnesses that applyOverrideUnread returns false on budget_exhausted and that the caller does not mutate its session overlay (covers the useChannelUnreadState.handleMarkUnread active-channel path). - applyOverrideRead_partial_refusal_pattern_clears_only_inactive: witnesses the per-channel gating pattern used by markAllChannelsRead — a channel whose C-bump resolves to inactive liveness is removed from the forced map; a channel whose C-bump refuses while liveness stays active is retained. Co-authored-by: Will Pfleger Signed-off-by: Will Pfleger --- .../channels/nip-rs-unread-ui.test.mjs | 93 +++++++++++++++++-- 1 file changed, 86 insertions(+), 7 deletions(-) diff --git a/desktop/src/features/channels/nip-rs-unread-ui.test.mjs b/desktop/src/features/channels/nip-rs-unread-ui.test.mjs index 4770663532..e53a34dee5 100644 --- a/desktop/src/features/channels/nip-rs-unread-ui.test.mjs +++ b/desktop/src/features/channels/nip-rs-unread-ui.test.mjs @@ -69,27 +69,27 @@ function makeIsolatedStorage() { test("overrideErrorMessage_budget_exhausted_unread_contains_budget_keyword", () => { const msg = overrideErrorMessage("unread", "budget_exhausted"); - assert.ok(msg !== null && msg.includes("budget exhausted"), `got: ${msg}`); + assert.ok(msg?.includes("budget exhausted"), `got: ${msg}`); }); test("overrideErrorMessage_uint32_overflow_unread_contains_counter_keyword", () => { const msg = overrideErrorMessage("unread", "uint32_overflow"); - assert.ok(msg !== null && msg.includes("counter limit"), `got: ${msg}`); + assert.ok(msg?.includes("counter limit"), `got: ${msg}`); }); test("overrideErrorMessage_uint32_overflow_read_contains_counter_keyword", () => { const msg = overrideErrorMessage("read", "uint32_overflow"); - assert.ok(msg !== null && msg.includes("counter limit"), `got: ${msg}`); + assert.ok(msg?.includes("counter limit"), `got: ${msg}`); }); test("overrideErrorMessage_load_incomplete_unread_contains_loading_keyword", () => { const msg = overrideErrorMessage("unread", "load_incomplete"); - assert.ok(msg !== null && msg.includes("still loading"), `got: ${msg}`); + assert.ok(msg?.includes("still loading"), `got: ${msg}`); }); test("overrideErrorMessage_load_incomplete_read_contains_loading_keyword", () => { const msg = overrideErrorMessage("read", "load_incomplete"); - assert.ok(msg !== null && msg.includes("still loading"), `got: ${msg}`); + assert.ok(msg?.includes("still loading"), `got: ${msg}`); }); test("overrideErrorMessage_already_inactive_returns_null_both_ops", () => { @@ -185,12 +185,12 @@ test("applyOverrideRead_active_liveness_success_then_inactive_returns_overrideCl test("applyOverrideRead_active_liveness_uint32_overflow_returns_overrideStillActive", () => { // Active → C-bump refuses → re-read still active → overrideStillActive - let callCount = 0; + let _callCount = 0; const result = applyOverrideRead("ch-1", { markChannelUnread: () => ({ success: true }), markChannelRead: () => ({ success: false, reason: "uint32_overflow" }), getOverrideLiveness: () => { - callCount++; + _callCount++; return { active: true, frontier: 50 }; }, }); @@ -341,3 +341,82 @@ test("forced_unread_store_write_and_read_round_trips", () => { restore(); } }); + +// ── Tests: refusal-does-not-mutate-overlay (active-channel path) ───────────── +// +// Witnesses the requirement that a refused mark-unread does NOT update the +// caller's local state. applyOverrideUnread returns false on any manager +// refusal; callers gate cache writes on that return value. This covers the +// active-channel session-overlay scenario (useChannelUnreadState.handleMarkUnread) +// where the overlay must NOT be set before or on a refused call. + +test("applyOverrideUnread_refusal_caller_must_not_mutate_overlay", () => { + const _toasts = []; + const apis = { + markChannelUnread: () => ({ success: false, reason: "budget_exhausted" }), + markChannelRead: () => ({ success: true }), + getOverrideLiveness: () => null, + }; + // Simulate caller logic: only update local state when applyOverrideUnread + // returns true. On false, the overlay map must remain unmodified. + const overlayMap = {}; + const result = applyOverrideUnread("ch-overlay", apis); + if (result) { + overlayMap["ch-overlay"] = true; // would be set on success + } + assert.equal(result, false, "refusal returns false"); + assert.equal( + Object.hasOwn(overlayMap, "ch-overlay"), + false, + "overlay not mutated on refusal", + ); +}); + +// ── Tests: markAllChannelsRead partial refusal pattern ──────────────────────── +// +// Witnesses that per-channel gating in markAllChannelsRead correctly clears +// only channels whose override is confirmed inactive. Channels where the +// C-bump refuses and liveness remains active must keep their local state. + +test("applyOverrideRead_partial_refusal_pattern_clears_only_inactive", () => { + // ch-cleared: liveness inactive after successful C-bump → cleared + // ch-stuck: liveness active and C-bump refuses → stays unread + const livenessMap = { + "ch-cleared": { active: true, frontier: 100 }, + "ch-stuck": { active: true, frontier: 100 }, + }; + let clearCallCount = 0; + const apis = { + markChannelUnread: () => ({ success: true }), + markChannelRead: (id) => { + clearCallCount++; + if (id === "ch-cleared") { + livenessMap["ch-cleared"] = { active: false, frontier: 101 }; + return { success: true }; + } + // ch-stuck: refuse with overflow; liveness stays active + return { success: false, reason: "uint32_overflow" }; + }, + getOverrideLiveness: (id) => livenessMap[id] ?? null, + }; + + // Simulate the per-channel loop in markAllChannelsRead + const forcedMap = { "ch-cleared": 99, "ch-stuck": 99 }; + for (const channelId of ["ch-cleared", "ch-stuck"]) { + if (applyOverrideRead(channelId, apis) === "overrideCleared") { + delete forcedMap[channelId]; + } + } + + assert.equal( + Object.hasOwn(forcedMap, "ch-cleared"), + false, + "cleared channel removed from map", + ); + assert.equal( + Object.hasOwn(forcedMap, "ch-stuck"), + true, + "stuck channel remains in map", + ); + assert.equal(clearCallCount, 2, "C-bump attempted for both channels"); +});