Files
bitvid/tests/watchHistoryFingerprintEmit.test.mjs
thePR0M3TH3ANandClaude Opus 4.8 96da135042 test(watch-history): regression test for fingerprint emit-on-change
Locks in the fix for the infinite feed-refresh loop: asserts "fingerprint" is
emitted only when the value actually changes (unchanged re-evaluation must not
emit, or the For You feed would refresh -> reload -> emit forever). Verified to
fail when the change-guard is removed.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-13 08:00:28 -04:00

74 lines
3.1 KiB
JavaScript

// Regression test for the infinite feed-refresh loop that pegged the CPU.
//
// Scenario (SCN-fingerprint-emit-on-change-only):
// Given the For You feed re-runs whenever watch history emits "fingerprint",
// When watch history is re-evaluated but its fingerprint is UNCHANGED,
// Then no "fingerprint" event is emitted — so the feed does not refresh,
// which would re-evaluate watch history, re-emit, and loop forever.
// And when the fingerprint genuinely changes, exactly one event is emitted.
//
// The bug: getFingerprint/updateFingerprintCache emitted "fingerprint" on every
// evaluation regardless of change, so the feed's refresh -> reload -> emit chain
// never settled (a full feed rebuild ~2.5x/second).
import "./test-helpers/setup-localstorage.mjs";
import { test, mock } from "node:test";
import assert from "node:assert/strict";
const { nostrClient } = await import("../js/nostrClientFacade.js");
const { watchHistoryService } = await import("../js/watchHistoryService.js");
const { WATCH_HISTORY_CACHE_TTL_MS } = await import("../js/config.js");
const ACTOR = "a".repeat(64);
test("emits 'fingerprint' only when it actually changes", async () => {
const originalGet = nostrClient.getWatchHistoryFingerprint;
const originalPubkey = nostrClient.pubkey;
const originalSession = nostrClient.sessionActor;
// Logged-in, non-session actor so the feature is enabled for ACTOR.
nostrClient.pubkey = ACTOR;
nostrClient.sessionActor = null;
watchHistoryService.resetProgress(ACTOR); // start from a clean fingerprint cache
let currentFingerprint = "fp-1";
nostrClient.getWatchHistoryFingerprint = async () => currentFingerprint;
let emitCount = 0;
const unsubscribe = watchHistoryService.subscribe("fingerprint", () => {
emitCount += 1;
});
// Fake the clock so we can expire the (24h) fingerprint cache between calls
// and thus force a genuine re-evaluation each time (otherwise the fresh-cache
// early-return would mask whether the change-guard works).
mock.timers.enable({ apis: ["Date"] });
try {
// 1) First evaluation: undefined -> "fp-1" is a change => one emit.
await watchHistoryService.getFingerprint(ACTOR);
assert.equal(emitCount, 1, "first fingerprint should emit once");
// 2) Re-evaluate with the SAME fingerprint after the cache expires.
mock.timers.tick(WATCH_HISTORY_CACHE_TTL_MS + 1000);
await watchHistoryService.getFingerprint(ACTOR);
assert.equal(
emitCount,
1,
"unchanged fingerprint must NOT emit (this is what broke the loop)",
);
// 3) Re-evaluate after a real change => exactly one more emit.
mock.timers.tick(WATCH_HISTORY_CACHE_TTL_MS + 1000);
currentFingerprint = "fp-2";
await watchHistoryService.getFingerprint(ACTOR);
assert.equal(emitCount, 2, "a changed fingerprint should emit once more");
} finally {
mock.timers.reset();
unsubscribe();
nostrClient.getWatchHistoryFingerprint = originalGet;
nostrClient.pubkey = originalPubkey;
nostrClient.sessionActor = originalSession;
watchHistoryService.resetProgress(ACTOR);
}
});