mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Root cause of "watch history delete does not work at all": the removal paths call watchHistoryService.snapshot(remaining, …), but snapshot published via updateWatchHistoryList WITHOUT replace:true. The default merges incoming items with the existing cached list, so the just-removed item was merged straight back and the deletion silently had no effect. Removing the LAST item was worse: an empty list fell through to republishing the pending queue instead of clearing. - snapshot now honors options.replace: forwards it to updateWatchHistoryList, treats an explicit empty replace as a clear (publishes empty instead of short-circuiting or falling back to the queue), and skips the cached-items merge when replacing. Adds (the default) keep merge semantics. - Both removal call sites (watchHistoryController.handleWatchHistoryRemoval and historyView.defaultRemoveHandler) now pass replace:true. Tests (snapshot at the updateWatchHistoryList boundary): removing an item publishes the remaining list with replace:true; removing the last item publishes an empty replace (not a queue republish / no-op); a normal watch-tracking snapshot still merges (replace not forced). Mutation-verified — dropping the replace forwarding fails 2 cases and reverting the empty-clear handling fails the clear case. Build + full lint green. Co-Authored-By: Claude Opus 4.8 <[email protected]>
99 lines
3.2 KiB
JavaScript
99 lines
3.2 KiB
JavaScript
// Watch-history removal must publish the reduced list with REPLACE semantics.
|
|
//
|
|
// Bug (SCN-watch-history-remove-replaces): the removal path calls
|
|
// watchHistoryService.snapshot(remaining, …) but snapshot published via
|
|
// updateWatchHistoryList WITHOUT replace:true — the default merges incoming
|
|
// items with the existing cached list, so the just-removed item was merged
|
|
// straight back and the deletion had no effect ("watch history delete does not
|
|
// work at all"). Removing the LAST item was even worse: an empty list fell
|
|
// through to republishing the pending queue instead of clearing.
|
|
//
|
|
// Observable outcome asserted at the boundary: the options passed to
|
|
// nostrClient.updateWatchHistoryList (the relay-publish entry point).
|
|
|
|
import "./test-helpers/setup-localstorage.mjs";
|
|
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import { watchHistoryService } from "../js/watchHistoryService.js";
|
|
import { nostrClient } from "../js/nostrClientFacade.js";
|
|
|
|
// snapshot calls these as property lookups on the shared singleton, so stubbing
|
|
// the properties is sufficient to virtualize the relay boundary.
|
|
function installStub() {
|
|
const calls = [];
|
|
nostrClient.updateWatchHistoryList = async (items, opts) => {
|
|
calls.push({ items: items.slice(), opts });
|
|
return { ok: true, items };
|
|
};
|
|
nostrClient.getWatchHistoryFingerprint = async () => `fp-${calls.length}`;
|
|
return calls;
|
|
}
|
|
|
|
test("removing an item snapshots the remaining list with replace:true", async () => {
|
|
const calls = installStub();
|
|
const actor = "1".repeat(64);
|
|
const remaining = [
|
|
{ type: "e", value: "keep-1" },
|
|
{ type: "e", value: "keep-2" },
|
|
];
|
|
|
|
await watchHistoryService.snapshot(remaining, {
|
|
actor,
|
|
reason: "remove-item",
|
|
replace: true,
|
|
});
|
|
|
|
assert.equal(calls.length, 1, "must publish exactly once");
|
|
assert.equal(
|
|
calls[0].opts.replace,
|
|
true,
|
|
"snapshot must forward replace:true so the removed item is not merged back",
|
|
);
|
|
assert.deepEqual(
|
|
calls[0].items.map((p) => p.value).sort(),
|
|
["keep-1", "keep-2"],
|
|
"must publish the remaining items",
|
|
);
|
|
});
|
|
|
|
test("removing the LAST item publishes an empty replace (clears, not queue republish)", async () => {
|
|
const calls = installStub();
|
|
const actor = "2".repeat(64);
|
|
|
|
const result = await watchHistoryService.snapshot([], {
|
|
actor,
|
|
reason: "remove-item",
|
|
replace: true,
|
|
});
|
|
|
|
assert.notEqual(
|
|
result?.empty,
|
|
true,
|
|
"an empty replace must NOT short-circuit as a no-op",
|
|
);
|
|
assert.equal(calls.length, 1, "empty replace must still publish to clear relays");
|
|
assert.equal(calls[0].opts.replace, true);
|
|
assert.equal(
|
|
calls[0].items.length,
|
|
0,
|
|
"must publish an empty list, never fall back to the pending queue",
|
|
);
|
|
});
|
|
|
|
test("adds (no replace) keep merge semantics — replace is not forced", async () => {
|
|
const calls = installStub();
|
|
const actor = "3".repeat(64);
|
|
|
|
await watchHistoryService.snapshot([{ type: "e", value: "new-watch" }], {
|
|
actor,
|
|
reason: "watch",
|
|
});
|
|
|
|
assert.equal(calls.length, 1);
|
|
assert.notEqual(
|
|
calls[0].opts.replace,
|
|
true,
|
|
"a normal watch-tracking snapshot must NOT replace (it appends)",
|
|
);
|
|
});
|