// Where a post's QR goes: deck.mjs's pure postQrUrl, the deck's `posts.links` // setting and the post keys it reads, and post-links.mjs's resolver (with a // stub archive -- nothing here touches the network). // // Run with: pnpm test:scripts import assert from "node:assert/strict"; import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import test from "node:test"; import { createJsonCache } from "./cues.mjs"; import { DECK_DEFAULTS, estimateSchedule, postArchiveUrl, postNativeId, postQrUrl, validateChrome, validatePosts, } from "./deck.mjs"; import { channelMatchesHandle, createPostChannelResolver, resolvePostLinks } from "./post-links.mjs"; const ORIGIN = "https://jasolyzer.pages.dev"; const PROV = { siteOrigin: ORIGIN, channelSlug: "piratesoftware" }; const BSKY = { id: "bs-3l6uxj6esfr2z", platform: "bluesky", handle: "piratesoftware.live", date: "2024-10-19T17:01:17.640Z", text: "words", url: "https://bsky.app/profile/piratesoftware.live/post/3l6uxj6esfr2z", }; const X = { id: "x-1", platform: "x", handle: "PirateSoftware", date: "2024-10-20", text: "words", url: "https://x.com/PirateSoftware/status/1847712345678901234", }; // ---- the pure half (deck.mjs) ------------------------------------------------ test("postNativeId: a bsky rkey, an X status id, an explicit postId, else null", () => { assert.equal(postNativeId(BSKY), "3l6uxj6esfr2z"); assert.equal(postNativeId(X), "1847712345678901234"); assert.equal(postNativeId({ url: "https://twitter.com/a/status/42/photo/1" }), "42"); assert.equal(postNativeId({ ...X, postId: "999" }), "999"); assert.equal(postNativeId({ url: "https://bsky.app/p/1" }), null); assert.equal(postNativeId({ url: "not a url" }), null); }); test("postArchiveUrl: the site's post modal for /", () => { assert.equal( postArchiveUrl(`${ORIGIN}/`, "piratesoftware-bsky", "3l6uxj6esfr2z"), "https://jasolyzer.pages.dev/?v=piratesoftware-bsky%2F3l6uxj6esfr2z&vm=post", ); }); test("postQrUrl: the archive page when the post names its channel, else its own url", () => { // bsky, with its archive channel: the archive page. assert.equal( postQrUrl({ ...BSKY, siteChannel: "piratesoftware-bsky" }, PROV), "https://jasolyzer.pages.dev/?v=piratesoftware-bsky%2F3l6uxj6esfr2z&vm=post", ); // X, the same. assert.equal( postQrUrl({ ...X, siteChannel: "piratesoftware-x" }, PROV), "https://jasolyzer.pages.dev/?v=piratesoftware-x%2F1847712345678901234&vm=post", ); // An explicit siteUrl wins over everything but links "original". assert.equal(postQrUrl({ ...BSKY, siteChannel: "c", siteUrl: "https://elsewhere.example/p" }, PROV), "https://elsewhere.example/p"); assert.equal(postQrUrl({ ...BSKY, siteUrl: "https://elsewhere.example/p" }, {}), "https://elsewhere.example/p"); // links "original": today's behaviour, whatever the post names. assert.equal(postQrUrl({ ...BSKY, siteChannel: "piratesoftware-bsky", siteUrl: "https://e/p" }, PROV, { links: "original" }), BSKY.url); // No siteOrigin, no channel, or no id: the post's own url. assert.equal(postQrUrl({ ...BSKY, siteChannel: "piratesoftware-bsky" }, {}), BSKY.url); assert.equal(postQrUrl(BSKY, PROV), BSKY.url); assert.equal(postQrUrl({ ...BSKY, url: "https://bsky.app/p/1", siteChannel: "c" }, PROV), "https://bsky.app/p/1"); // A postId supplies the id a url does not carry. assert.equal( postQrUrl({ ...BSKY, url: "https://bsky.app/p/1", siteChannel: "c", postId: "abc" }, PROV), "https://jasolyzer.pages.dev/?v=c%2Fabc&vm=post", ); }); test("the schedule's posts carry postQrUrl's link; links \"original\" keeps the post's own", () => { const render = { fps: 30, transition: 0.5, chrome: { engine: "hyperframes", layout: "deck", deck: { posts: { layout: "feed" } } } }; const timeline = [{ type: "clip", id: "c01", video: "v1", start: 0, end: 10, date: "2024-10-01" }]; const posts = [{ ...BSKY, siteChannel: "piratesoftware-bsky" }, X]; const s = estimateSchedule({ render, provenance: PROV, timeline, posts }); const by = Object.fromEntries(s.posts.map((p) => [p.id, p])); assert.equal(by[BSKY.id].qrUrl, "https://jasolyzer.pages.dev/?v=piratesoftware-bsky%2F3l6uxj6esfr2z&vm=post"); assert.equal(by[BSKY.id].url, BSKY.url, "the post's own link stays beside it"); assert.equal(by[X.id].qrUrl, X.url, "a post with no archive channel links its original"); const orig = { ...render, chrome: { ...render.chrome, deck: { posts: { layout: "feed", links: "original" } } } }; const o = estimateSchedule({ render: orig, provenance: PROV, timeline, posts }); assert.ok(o.posts.every((p) => p.qrUrl === p.url)); }); test("posts.links: archive by default; archive or original, nothing else", () => { assert.equal(DECK_DEFAULTS.posts.links, "archive"); assert.deepEqual(validateChrome({ engine: "hyperframes", layout: "deck", deck: { posts: { links: "original" } } }, { width: 1920, height: 1080 }), []); assert.deepEqual(validateChrome({ engine: "hyperframes", layout: "deck", deck: { posts: { links: "archive" } } }, { width: 1920, height: 1080 }), []); const bad = validateChrome({ engine: "hyperframes", layout: "deck", deck: { posts: { links: "platform" } } }, { width: 1920, height: 1080 }); assert.ok(bad.some((e) => /posts\.links must be "archive" or "original"/.test(e)), bad.join("; ")); }); test("validatePosts: siteChannel, siteUrl and postId are optional and checked", () => { assert.deepEqual(validatePosts([{ ...BSKY, siteChannel: "piratesoftware-bsky", siteUrl: "https://a.example/p", postId: "3l6uxj6esfr2z" }]), []); const errors = validatePosts([{ ...BSKY, siteChannel: "has space", siteUrl: "ftp://a/p", postId: "a/b" }]); assert.equal(errors.length, 3, errors.join("; ")); assert.ok(errors.some((e) => /siteChannel/.test(e))); assert.ok(errors.some((e) => /siteUrl must be an http\(s\) link/.test(e))); assert.ok(errors.some((e) => /postId/.test(e))); assert.ok(validatePosts([{ ...BSKY, siteChannel: 7 }]).some((e) => /siteChannel/.test(e))); }); // ---- the resolver (post-links.mjs) ------------------------------------------- /** A stub archive: corpus.json plus each channel's posts manifest, as getJson answers them. */ function archive(channels, { failing = new Set() } = {}) { const docs = new Map(); docs.set(`${ORIGIN}/corpus.json`, { channels: channels.map((c) => ({ slug: c.slug, name: c.name ?? c.slug, videoCount: 0, ...(c.ids ? { postCount: c.ids.length, manifests: { posts: `${ORIGIN}/posts/${c.slug}/manifest.json` } } : { manifests: {} }), })), }); for (const c of channels) { if (c.ids) docs.set(`${ORIGIN}/posts/${c.slug}/manifest.json`, { slugToPage: Object.fromEntries(c.ids.map((id) => [id, 0])) }); } const asked = []; const getJson = async (url, { refresh = false } = {}) => { asked.push({ url, refresh }); if (failing.has(url)) throw new Error(`GET ${url} -> 503`); if (!docs.has(url)) throw new Error(`GET ${url} -> 404`); return docs.get(url); }; return { getJson, asked, docs }; } const DECK = { chrome: { engine: "hyperframes", layout: "deck", deck: {} } }; test("resolver: finds the channel that keeps the post, and the QR is its archive page", async () => { const a = archive([ { slug: "piratesoftware" }, { slug: "piratesoftware-bsky", name: "piratesoftware.live (BlueSky)", ids: ["3l6uxj6esfr2z", "other"] }, ]); const resolver = createPostChannelResolver({ getJson: a.getJson }); const r = await resolvePostLinks({ posts: [BSKY], provenance: PROV, render: DECK, resolver }); assert.equal(r.posts[0].siteChannel, "piratesoftware-bsky"); assert.deepEqual(r.notes, [`${BSKY.id}: archive link via piratesoftware-bsky`]); assert.equal(postQrUrl(r.posts[0], PROV), "https://jasolyzer.pages.dev/?v=piratesoftware-bsky%2F3l6uxj6esfr2z&vm=post"); assert.equal(BSKY.siteChannel, undefined, "the manifest's post is not changed"); }); test("resolver: a post the archive does not have links its original, with a note", async () => { const a = archive([{ slug: "piratesoftware-bsky", ids: ["nope"] }]); const resolver = createPostChannelResolver({ getJson: a.getJson }); const r = await resolvePostLinks({ posts: [BSKY], provenance: PROV, render: DECK, resolver }); assert.equal(r.posts[0].siteChannel, undefined); assert.deepEqual(r.notes, [`${BSKY.id}: not in the archive at ${ORIGIN} -- QR links the original`]); // A miss is looked for again in a fresh copy, once. assert.deepEqual(a.asked.map((x) => x.refresh), [false, false, true, true]); }); test("resolver: an archive that does not answer links the original, says why, and is asked once", async () => { const a = archive([], { failing: new Set([`${ORIGIN}/corpus.json`]) }); const resolver = createPostChannelResolver({ getJson: a.getJson }); const r = await resolvePostLinks({ posts: [BSKY, { ...X }], provenance: PROV, render: DECK, resolver }); assert.ok(r.posts.every((p) => p.siteChannel === undefined)); assert.equal(r.notes.length, 2); assert.match(r.notes[0], /did not answer \(GET .*corpus\.json -> 503\) -- QR links the original/); // The failure is remembered: the second post asks nothing new without refresh. assert.equal(a.asked.filter((x) => !x.refresh).length, 1); }); test("resolver: several channels hold the id -- the one whose name carries the handle wins", async () => { const a = archive([ { slug: "mirror-bsky", name: "a mirror (BlueSky)", ids: ["3l6uxj6esfr2z"] }, { slug: "piratesoftware-bsky", name: "piratesoftware.live (BlueSky)", ids: ["3l6uxj6esfr2z"] }, ]); const resolver = createPostChannelResolver({ getJson: a.getJson }); assert.equal(await resolver.find(ORIGIN, BSKY), "piratesoftware-bsky"); // With no handle to go by, corpus order. assert.equal(await resolver.find(ORIGIN, { ...BSKY, handle: "" }), "mirror-bsky"); assert.equal(channelMatchesHandle({ slug: "piratesoftware-x", name: "PirateSoftware (X)" }, "PirateSoftware"), true); assert.equal(channelMatchesHandle({ slug: "other", name: "Other" }, "piratesoftware.live"), false); }); test("resolvePostLinks: pinned posts, links \"original\", no siteOrigin, no id", async () => { const a = archive([{ slug: "piratesoftware-bsky", ids: ["3l6uxj6esfr2z"] }]); const resolver = createPostChannelResolver({ getJson: a.getJson }); // Pinned: not looked up. const pinned = await resolvePostLinks({ posts: [{ ...BSKY, siteChannel: "pinned-bsky" }, { ...X, siteUrl: "https://e.example/x" }], provenance: PROV, render: DECK, resolver, }); assert.deepEqual(pinned.notes, [`${BSKY.id}: archive link via pinned-bsky (pinned)`, `${X.id}: QR links https://e.example/x (siteUrl)`]); assert.equal(a.asked.length, 0); // links "original": nothing to find, nothing said. const orig = await resolvePostLinks({ posts: [BSKY], provenance: PROV, render: { chrome: { ...DECK.chrome, deck: { posts: { links: "original" } } } }, resolver, }); assert.deepEqual(orig, { posts: [BSKY], notes: [] }); // No archive named: one note for the lot. const none = await resolvePostLinks({ posts: [BSKY], provenance: {}, render: DECK, resolver }); assert.equal(none.posts[0], BSKY); assert.equal(none.notes.length, 1); assert.match(none.notes[0], /^posts: no provenance\.siteOrigin names an archive -- every post's QR links the original \(set provenance\.siteOrigin/); // No id in the url. const noId = await resolvePostLinks({ posts: [{ ...BSKY, url: "https://bsky.app/p/1" }], provenance: PROV, render: DECK, resolver }); assert.deepEqual(noId.notes, [`${BSKY.id}: no post id in its url -- QR links the original`]); assert.equal(a.asked.length, 0); }); test("createJsonCache: a refresh refetches once per process, then reads what it fetched", async () => { const dir = await mkdtemp(path.join(tmpdir(), "post-links-cache-")); try { let n = 0; const fetchImpl = async () => ({ ok: true, status: 200, json: async () => ({ n: ++n }) }); const url = "https://archive.example/corpus.json"; // A first process fills the disk cache. assert.deepEqual(await createJsonCache({ cacheDir: dir, fetchImpl }).getJson(url), { n: 1 }); // A second reads it from disk; a refresh goes to the network once, and only once. const c = createJsonCache({ cacheDir: dir, fetchImpl }); assert.deepEqual(await c.getJson(url), { n: 1 }); assert.deepEqual(await c.getJson(url, { refresh: true }), { n: 2 }); assert.deepEqual(await c.getJson(url, { refresh: true }), { n: 2 }); assert.deepEqual(await c.getJson(url), { n: 2 }); // A window lets a long-lived reader refresh again once it has passed. const w = createJsonCache({ cacheDir: dir, fetchImpl, refreshAfterMs: 0 }); assert.deepEqual(await w.getJson(url, { refresh: true }), { n: 3 }); assert.deepEqual(await w.getJson(url, { refresh: true }), { n: 4 }); } finally { await rm(dir, { recursive: true, force: true }); } }); // ---- the failure memo, the deadline, what is not looked up ------------------- test("failure memo: an archive that hangs costs each URL one try per build, across both passes and every post", async () => { // corpus.json is cached (the cached pass answers); every fresh read and the // uncached posts manifest fail, as an archive that connects and stalls would. const a = archive([{ slug: "piratesoftware-bsky", name: "piratesoftware.live (BlueSky)", ids: ["other"] }]); const manifest = `${ORIGIN}/posts/piratesoftware-bsky/manifest.json`; const asked = []; const getJson = async (url, { refresh = false } = {}) => { asked.push({ url, refresh }); if (url.endsWith("/corpus.json") && !refresh) return a.docs.get(url); throw new Error(`GET ${url} -> timed out`); }; const resolver = createPostChannelResolver({ getJson }); const posts = [BSKY, { ...BSKY, id: "b2", url: "https://bsky.app/profile/piratesoftware.live/post/rk2" }, { ...BSKY, id: "b3", url: "https://bsky.app/profile/piratesoftware.live/post/rk3" }]; const r = await resolvePostLinks({ posts, provenance: PROV, render: DECK, resolver }); assert.ok(r.posts.every((p) => p.siteChannel === undefined)); assert.ok(r.notes.every((n) => /did not answer .* -- QR links the original/.test(n)), r.notes.join("\n")); // The manifest: asked once, by the first post's cached pass, never again. assert.deepEqual(asked.filter((x) => x.url === manifest), [{ url: manifest, refresh: false }]); // corpus.json fresh: asked once, by the first post's second pass. assert.equal(asked.filter((x) => x.url.endsWith("/corpus.json") && x.refresh).length, 1); }); test("failure memo: a server's resolver asks again once refreshAfterMs has passed, and a success re-arms it", async () => { let t = 0; let down = true; const a = archive([{ slug: "piratesoftware-bsky", name: "piratesoftware.live (BlueSky)", ids: ["3l6uxj6esfr2z"] }]); let asks = 0; const getJson = async (url, opts) => { asks += 1; if (down) throw new Error(`GET ${url} -> 503`); return a.getJson(url, opts); }; const resolver = createPostChannelResolver({ getJson, refreshAfterMs: 600_000, now: () => t }); await assert.rejects(resolver.find(ORIGIN, BSKY), /503/); assert.equal(asks, 1); // Within the window: refused from the memo, nothing asked, even after the archive is back. down = false; t = 599_999; await assert.rejects(resolver.find(ORIGIN, BSKY), /503/); assert.equal(asks, 1); // Past it: asked, found. t = 600_000; assert.equal(await resolver.find(ORIGIN, BSKY), "piratesoftware-bsky"); // The success cleared the memo; a new failure is remembered from its own time. down = true; const before = asks; await assert.rejects(resolver.find(ORIGIN, { ...BSKY, url: "https://bsky.app/profile/piratesoftware.live/post/zz" }), /503/); t = 600_001; await assert.rejects(resolver.find(ORIGIN, { ...BSKY, url: "https://bsky.app/profile/piratesoftware.live/post/zz" }), /503/); assert.equal(asks, before + 1, "one failed ask, then the memo"); }); test("resolvePostLinks: a deadline bounds the whole call; the rest keep their own links", async () => { const resolver = { find: () => new Promise(() => {}) }; // an archive that never answers const posts = [BSKY, { ...BSKY, id: "b2" }]; const started = Date.now(); const r = await resolvePostLinks({ posts, provenance: PROV, render: DECK, resolver, deadlineMs: 50 }); assert.ok(Date.now() - started < 1000); assert.ok(r.posts.every((p) => p.siteChannel === undefined)); assert.match(r.notes[0], /still being asked when time ran out -- QR links the original/); assert.match(r.notes[1], /no time left to ask the archive/); }); test("resolvePostLinks: hidden posts and a cut without the deck are not looked up", async () => { const asked = []; const resolver = { find: async (o, p) => (asked.push(p.id), "piratesoftware-bsky") }; const r = await resolvePostLinks({ posts: [{ ...BSKY, hide: true }, X], provenance: PROV, render: DECK, resolver }); assert.deepEqual(asked, [X.id]); assert.equal(r.posts[0].siteChannel, undefined); assert.equal(r.notes.length, 1); const off = await resolvePostLinks({ posts: [BSKY], provenance: PROV, render: {}, resolver }); assert.deepEqual(off, { posts: [BSKY], notes: [] }); assert.deepEqual(asked, [X.id]); }); test("validatePosts: null is unset for siteChannel, siteUrl and postId, as the README's example writes them", () => { assert.deepEqual(validatePosts([{ ...BSKY, attachTo: null, hide: false, siteChannel: null, siteUrl: null, postId: null }]), []); assert.equal(postQrUrl({ ...BSKY, siteChannel: null, siteUrl: null, postId: null }, PROV), BSKY.url); }); test("createJsonCache: callers asking for one URL at once share one fetch", async () => { let n = 0; let release; const gate = new Promise((r) => { release = r; }); const fetchImpl = async () => { n += 1; await gate; return { ok: true, status: 200, json: async () => ({ n }) }; }; const c = createJsonCache({ cacheDir: null, fetchImpl }); const both = Promise.all([c.getJson("https://a.example/x"), c.getJson("https://a.example/x", { refresh: true })]); release(); assert.deepEqual(await both, [{ n: 1 }, { n: 1 }]); assert.equal(n, 1); // Once it has landed, a refresh is a new fetch only when allowed (once per process here: not again). assert.deepEqual(await c.getJson("https://a.example/x", { refresh: true }), { n: 1 }); });