Files
bitvid/tests/watch-history-hydration-cache.test.mjs
T
thePR0M3TH3ANandClaude Opus 4.8 ebacf223c8 fix(watch-history): resolve freshly-watched videos from cache (default video kind in address lookup)
Watch-history cards rendered "Untitled video" / "Unknown" for newly watched
videos. nostrClient.allEvents stores CONVERTED video objects
(convertEventToVideo), which carry tags/pubkey but no `kind` field — only raw
relay events do. resolveEventAddress() did Number(event.kind) → NaN and bailed
with "", so the address (cache-by-"a"-pointer) scan matched nothing. Every video
pointer (all are "a" addresses) was forced to a relay round-trip, and when that
returned nothing the card showed Unknown — even though the just-watched video
was sitting right there in allEvents.

Fix: default to the video kind (30078) when a cached object has no `kind`, so the
local cache scan resolves freshly-watched "a" pointers without a relay round-trip.

Adds tests/watch-history-hydration-cache.test.mjs: relays return nothing, so a
pass can only come from the cache scan (mutation-verified against the NaN-reject
regression). Exports createWatchHistoryHydrationStage for boundary testing.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-21 14:10:21 -04:00

91 lines
2.9 KiB
JavaScript

// A freshly-watched video is already in nostrClient.allEvents (you just played
// it). The watch-history feed stores an "a" (address) pointer for it and must
// resolve the title/thumbnail from that local cache WITHOUT a relay round-trip.
//
// Bug: allEvents holds *converted* video objects (convertEventToVideo), which
// carry tags/pubkey but NO `kind` field. resolveEventAddress did Number(kind) →
// NaN → returned "" for every cached video, so the address cache-scan never
// matched. Every video pointer fell through to a relay fetch, and when that came
// up empty the card rendered "Untitled video" / "Unknown".
//
// Correct behavior: the cache-scan resolves an "a" pointer to the cached video
// using the video kind by default, so freshly-watched videos show their real
// metadata immediately — even when relays return nothing.
import "./test-helpers/setup-localstorage.mjs";
import assert from "node:assert/strict";
import test from "node:test";
import { createWatchHistoryHydrationStage } from "../js/feedEngine/watchHistoryFeed.js";
import { nostrClient } from "../js/nostrClientFacade.js";
const PUBKEY = "c".repeat(64);
const DTAG = "my-video-root-id";
const ADDRESS = `30078:${PUBKEY}:${DTAG}`;
// Mirrors convertEventToVideo output: tags + pubkey, but NO `kind`.
function cachedVideoWithoutKind() {
return {
id: "e".repeat(64),
pubkey: PUBKEY,
title: "My Real Video Title",
thumbnail: "https://example.com/thumb.jpg",
tags: [["d", DTAG]],
created_at: 1750000000,
invalid: false,
};
}
test("an 'a'-pointer resolves from the local cache (no relay needed) for a converted video lacking `kind`", async () => {
const prevAllEvents = nostrClient.allEvents;
const prevPool = nostrClient.pool;
const prevRead = nostrClient.readRelays;
nostrClient.allEvents = new Map([
[cachedVideoWithoutKind().id, cachedVideoWithoutKind()],
]);
// Relays return nothing — so a pass can ONLY come from the cache scan.
let relayHits = 0;
nostrClient.pool = {
list: async () => {
relayHits += 1;
return [];
},
};
nostrClient.readRelays = ["wss://relay.example"];
try {
const stage = createWatchHistoryHydrationStage();
const items = [
{
pointer: { type: "a", value: ADDRESS },
pointerKey: `a:${ADDRESS}`,
watchedAt: 1750000001,
video: null,
metadata: {},
},
];
const result = await stage(items, {});
assert.equal(result.length, 1);
assert.ok(
result[0].video,
"freshly-watched video must resolve from the local cache",
);
assert.equal(
result[0].video.title,
"My Real Video Title",
"resolved video must carry the real title (not Unknown)",
);
assert.equal(
relayHits,
0,
"cache hit must avoid a relay round-trip entirely",
);
} finally {
nostrClient.allEvents = prevAllEvents;
nostrClient.pool = prevPool;
nostrClient.readRelays = prevRead;
}
});