// Inbound NIP-71 ingest service. Scenario-style with a virtualized nostrClient // (in-memory activeMap/allEvents + a fake subscription manager) and a fake // accessControl. Asserts observable outcomes: foreign videos land in the active // store and trigger a refresh; native videos are never clobbered; the relay // subscription is scoped to the whitelist; and FEATURE_NIP71_INGEST off makes // the service inert. import assert from "node:assert/strict"; import test from "node:test"; import { createNip71IngestService } from "../js/services/nip71IngestService.js"; const KINDS = [21, 22, 34235, 34236]; function activeKey(video) { return video.videoRootId ? `ROOT:${video.videoRootId}` : `LEGACY:${video.id}`; } function makeHarness({ whitelistOn = false, whitelist = [] } = {}) { const activeMap = new Map(); const allEvents = new Map(); let capturedFilters = null; let onEvent = null; let onEose = null; let closed = false; let whitelistListener = null; const manager = { subscribe(opts) { capturedFilters = opts.filters; onEvent = opts.onEvent; onEose = opts.onEose; return { close() { closed = true; }, update() {}, }; }, }; const emitted = []; const nostrClient = { activeMap, allEvents, getActiveKey: activeKey, getSubscriptionManager: () => manager, }; const feedListeners = new Set(); const cached = []; const nostrService = { emit(name, detail) { emitted.push({ name, detail }); }, on(name, fn) { if (name === "videos:updated") { feedListeners.add(fn); } return () => feedListeners.delete(fn); }, getFilteredActiveVideos: () => Array.from(activeMap.values()), cacheVideos: (videos) => cached.push(...videos), }; const accessControl = { whitelistMode: () => whitelistOn, getWhitelistPubkeys: () => whitelist, onWhitelistChange: (fn) => { whitelistListener = fn; return () => { whitelistListener = null; }; }, }; return { activeMap, allEvents, emitted, cached, get capturedFilters() { return capturedFilters; }, get closed() { return closed; }, fireEvent: (e) => onEvent?.(e), triggerEose: () => onEose?.(), triggerWhitelistChange: () => whitelistListener?.(), triggerFeedReady: () => feedListeners.forEach((fn) => fn({})), nostrClient, nostrService, accessControl, }; } function foreignEvent(overrides = {}) { return { id: overrides.id || "evt-1", pubkey: overrides.pubkey || "author-1", kind: overrides.kind || 21, created_at: overrides.created_at ?? 1000, content: "desc", tags: overrides.tags || [ ["title", overrides.title || "Foreign"], ["imeta", `url ${overrides.url || "https://e/v.mp4"}`, "m video/mp4"], ], }; } function makeService(harness, extra = {}) { return createNip71IngestService({ nostrClient: harness.nostrClient, nostrService: harness.nostrService, accessControl: harness.accessControl, featureEnabled: true, flushDelayMs: 0, // Disable the hydration retry by default so each test is deterministic and // leaves no dangling timers; the retry behavior is covered explicitly below. maxOpenAttempts: 0, // Emit refreshes synchronously (no throttle timer) for deterministic tests. refreshThrottleMs: 0, ...extra, }); } test("ingests a foreign NIP-71 video into the active store and signals a refresh", () => { const h = makeHarness(); const svc = makeService(h); assert.equal(svc.start(), true); h.fireEvent(foreignEvent({ id: "e1", title: "Hello" })); const injected = svc.flush(); assert.equal(injected, 1); const stored = h.activeMap.get("ROOT:e1"); assert.ok(stored, "video present in activeMap"); assert.equal(stored.title, "Hello"); assert.equal(stored.source, "nip71-ingest"); assert.ok(h.allEvents.has("e1"), "raw video cached in allEvents"); const refresh = h.emitted.find((e) => e.name === "videos:updated"); assert.ok(refresh, "emits videos:updated"); assert.equal(refresh.detail.reason, "nip71-ingest"); }); test("caches injected videos into nostrService (persisted/author-feed paths)", () => { const h = makeHarness(); const svc = makeService(h); svc.start(); h.fireEvent(foreignEvent({ id: "e1", title: "Hello" })); svc.flush(); assert.equal(h.cached.length, 1, "injected video forwarded to cacheVideos"); assert.equal(h.cached[0].id, "e1"); }); test("never clobbers a native bitvid video sharing the same root", () => { const h = makeHarness(); const native = { id: "native", videoRootId: "shared", source: "nostr", created_at: 500, title: "Native" }; h.activeMap.set("ROOT:shared", native); const svc = makeService(h); svc.start(); h.fireEvent(foreignEvent({ id: "foreign", created_at: 9999, tags: [["d", "shared"], ["title", "Foreign"], ["imeta", "url https://e/v.mp4", "m video/mp4"], ], kind: 34235 })); const injected = svc.flush(); assert.equal(injected, 0, "native video is not overwritten even by a newer foreign event"); assert.equal(h.activeMap.get("ROOT:shared"), native); }); test("among ingested videos for one root, newest wins", () => { const h = makeHarness(); const svc = makeService(h); svc.start(); h.fireEvent(foreignEvent({ id: "old", kind: 34235, created_at: 100, tags: [["d", "r"], ["title", "Old"], ["imeta", "url https://e/v1.mp4", "m video/mp4"]] })); svc.flush(); h.fireEvent(foreignEvent({ id: "new", kind: 34235, created_at: 200, tags: [["d", "r"], ["title", "New"], ["imeta", "url https://e/v2.mp4", "m video/mp4"]] })); svc.flush(); assert.equal(h.activeMap.get("ROOT:r").title, "New"); // An older event for the same root must not resurrect. h.fireEvent(foreignEvent({ id: "older", kind: 34235, created_at: 50, tags: [["d", "r"], ["title", "Older"], ["imeta", "url https://e/v3.mp4", "m video/mp4"]] })); svc.flush(); assert.equal(h.activeMap.get("ROOT:r").title, "New", "older event does not override newer"); }); test("scopes the relay subscription to whitelisted authors when whitelist mode is on", () => { const h = makeHarness({ whitelistOn: true, whitelist: ["aa", "bb"] }); const svc = makeService(h); assert.equal(svc.start(), true); const filters = h.capturedFilters; assert.ok(Array.isArray(filters) && filters.length === 1); assert.deepEqual(filters[0].kinds.slice().sort((a, b) => a - b), KINDS); assert.deepEqual(filters[0].authors, ["aa", "bb"], "subscribes only to whitelisted authors"); }); test("with whitelist on but no authors, does not open a subscription (nothing to ingest)", () => { const h = makeHarness({ whitelistOn: true, whitelist: [] }); const svc = makeService(h); assert.equal(svc.start(), false, "no subscription opened"); assert.equal(h.capturedFilters, null); }); test("opens an unscoped (capped) subscription when whitelist mode is off", () => { const h = makeHarness({ whitelistOn: false }); const svc = makeService(h); svc.start(); const filters = h.capturedFilters; assert.ok(filters[0].authors === undefined, "no author scope when whitelist disabled"); assert.ok(Number.isFinite(filters[0].limit), "still capped by a limit"); }); test("retries until the whitelist hydrates, then subscribes (no missed-emit race)", async () => { // Simulates the real startup race: whitelist mode is on but the hex author // set isn't populated yet at start(), and no change event is emitted. const h = makeHarness({ whitelistOn: true, whitelist: [] }); const svc = makeService(h, { maxOpenAttempts: 5, openRetryDelayMs: 5 }); assert.equal(svc.start(), false, "cannot open yet — authors not hydrated"); assert.equal(h.capturedFilters, null); // Authors hydrate a moment later, WITHOUT firing onWhitelistChange. h.accessControl.getWhitelistPubkeys = () => ["aa", "bb"]; // Wait past a retry tick. await new Promise((resolve) => setTimeout(resolve, 20)); assert.ok(h.capturedFilters, "retry opened the subscription after hydration"); assert.deepEqual(h.capturedFilters[0].authors, ["aa", "bb"]); svc.stop(); }); test("re-subscribes when the whitelist changes", () => { const h = makeHarness({ whitelistOn: true, whitelist: ["aa"] }); const svc = makeService(h); svc.start(); assert.deepEqual(h.capturedFilters[0].authors, ["aa"]); // Whitelist grows; the access-control change fires. h.accessControl.getWhitelistPubkeys = () => ["aa", "cc"]; h.triggerWhitelistChange(); assert.deepEqual(h.capturedFilters[0].authors, ["aa", "cc"], "subscription re-scoped after change"); }); test("startWhenFeedReady defers subscription until the native feed renders", () => { const h = makeHarness({ whitelistOn: true, whitelist: ["aa"] }); const svc = makeService(h, { feedReadyFallbackMs: 100000 }); assert.equal(svc.startWhenFeedReady(), true); assert.equal(h.capturedFilters, null, "does not subscribe during cold-start"); // Native feed renders -> first videos:updated -> ingest starts now. h.triggerFeedReady(); assert.ok(h.capturedFilters, "subscribes once the feed is ready"); assert.deepEqual(h.capturedFilters[0].authors, ["aa"]); svc.stop(); }); test("startWhenFeedReady falls back to starting if the feed never emits", async () => { const h = makeHarness({ whitelistOn: true, whitelist: ["aa"] }); const svc = makeService(h, { feedReadyFallbackMs: 5 }); svc.startWhenFeedReady(); assert.equal(h.capturedFilters, null); await new Promise((resolve) => setTimeout(resolve, 20)); assert.ok(h.capturedFilters, "fallback timer started ingest"); svc.stop(); }); test("initial NIP-71 sync keeps every event but emits one consolidated feed update", () => { const h = makeHarness(); const svc = makeService(h, { feedReadyFallbackMs: 100000, initialSyncFallbackMs: 100000 }); svc.startWhenFeedReady(); h.triggerFeedReady(); h.fireEvent(foreignEvent({ id: "a", kind: 34235, tags: [["d", "ra"], ["title", "A"], ["imeta", "url https://e/a.mp4", "m video/mp4"]] })); h.fireEvent(foreignEvent({ id: "b", kind: 34235, tags: [["d", "rb"], ["title", "B"], ["imeta", "url https://e/b.mp4", "m video/mp4"]] })); svc.flush(); assert.ok(h.activeMap.has("ROOT:ra") && h.activeMap.has("ROOT:rb"), "all initial NIP-71 events are retained"); assert.equal(h.emitted.filter((e) => e.name === "videos:updated").length, 0, "no partial-startup repaint"); h.triggerEose(); const refreshes = h.emitted.filter((e) => e.name === "videos:updated"); assert.equal(refreshes.length, 1, "one consolidated update at initial sync completion"); assert.equal(refreshes[0].detail.reason, "nip71-ingest"); svc.stop(); }); test("coalesces refresh signals so an event burst can't storm the feed", () => { const h = makeHarness(); const svc = makeService(h, { refreshThrottleMs: 10000 }); svc.start(); h.fireEvent(foreignEvent({ id: "a", kind: 34235, tags: [["d", "ra"], ["title", "A"], ["imeta", "url https://e/a.mp4", "m video/mp4"]] })); svc.flush(); h.fireEvent(foreignEvent({ id: "b", kind: 34235, tags: [["d", "rb"], ["title", "B"], ["imeta", "url https://e/b.mp4", "m video/mp4"]] })); svc.flush(); const refreshes = h.emitted.filter((e) => e.name === "videos:updated"); assert.equal(refreshes.length, 1, "only one immediate refresh within the throttle window"); // Both videos still landed in the store regardless of the throttled signal. assert.ok(h.activeMap.has("ROOT:ra") && h.activeMap.has("ROOT:rb")); svc.stop(); }); test("FEATURE_NIP71_INGEST off makes the service completely inert", () => { const h = makeHarness(); const svc = createNip71IngestService({ nostrClient: h.nostrClient, nostrService: h.nostrService, accessControl: h.accessControl, featureEnabled: false, flushDelayMs: 0, }); assert.equal(svc.isAvailable(), false); assert.equal(svc.start(), false); assert.equal(h.capturedFilters, null, "no relay subscription when ingest is disabled"); }); test("stop() closes the subscription and unhooks the whitelist listener", () => { const h = makeHarness({ whitelistOn: true, whitelist: ["aa"] }); const svc = makeService(h); svc.start(); svc.stop(); assert.equal(h.closed, true, "subscription closed"); // After stop, a whitelist change must not reopen anything. h.triggerWhitelistChange(); });