import { test } from "node:test"; import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { didFromAtUri, normalizeBlueskyItem, rkeyFromAtUri, } from "./blueskyFetcher"; import { monthShardFromCreatedAt, parsePost, uploadDateFromCreatedAt, } from "../lib/posts"; // A recorded getAuthorFeed response — no network in tests. Captured live from // public.api.bsky.app and trimmed to one repost, one reply and two plain posts. const fixtureDir = path.join( path.dirname(fileURLToPath(import.meta.url)), "__fixtures__", ); const feed = JSON.parse( readFileSync(path.join(fixtureDir, "bluesky-author-feed.json"), "utf8"), ) as { feed: unknown[]; cursor?: string }; test("at-uri parsing pulls the rkey and did", () => { const uri = "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3mqc36slinc2m"; assert.equal(rkeyFromAtUri(uri), "3mqc36slinc2m"); assert.equal(didFromAtUri(uri), "did:plc:z72i7hdynmk6r22z27h6tvur"); assert.equal(rkeyFromAtUri(undefined), null); assert.equal(didFromAtUri("not-an-at-uri"), null); }); test("normalizes every fixture item into a valid Post", () => { const posts = feed.feed.map((item) => normalizeBlueskyItem(item as never, "testchannel"), ); assert.equal(posts.length, 4); for (const post of posts) { assert.ok(post, "every fixture item should normalize"); assert.equal(post!.platform, "bluesky"); assert.equal(post!.channelSlug, "testchannel"); assert.equal(post!.slug, `testchannel/${post!.id}`); // A normalized post must survive the on-disk round trip unchanged. assert.deepEqual(parsePost(JSON.parse(JSON.stringify(post))), post); } }); test("derives uploadDate and the permalink from the record", () => { const post = normalizeBlueskyItem(feed.feed[0] as never, "testchannel")!; assert.equal(post.uploadDate, uploadDateFromCreatedAt(post.createdAt)); assert.match(post.uploadDate, /^\d{8}$/); assert.equal( post.url, `https://bsky.app/profile/${post.author}/post/${post.id}`, ); }); test("marks the repost item and points repostOf at the original", () => { const posts = feed.feed.map((i) => normalizeBlueskyItem(i as never, "c")!); const repost = posts.find((p) => p.isRepost); assert.ok(repost, "fixture contains a repost"); assert.equal(repost!.repostOf?.platform, "bluesky"); assert.equal(repost!.repostOf?.id, repost!.id); }); test("marks the reply item and records its thread root", () => { const posts = feed.feed.map((i) => normalizeBlueskyItem(i as never, "c")!); const reply = posts.find((p) => p.isReply); assert.ok(reply, "fixture contains a reply"); assert.ok(reply!.replyTo?.id, "a reply carries a parent ref"); assert.ok(reply!.threadId, "a reply carries a thread id"); assert.notEqual(reply!.threadId, reply!.id); }); test("a root post threads under its own id", () => { const posts = feed.feed.map((i) => normalizeBlueskyItem(i as never, "c")!); const root = posts.find((p) => !p.isReply); assert.ok(root); assert.equal(root!.threadId, root!.id); }); test("rejects items with no resolvable id or timestamp", () => { assert.equal(normalizeBlueskyItem({} as never, "c"), null); assert.equal( normalizeBlueskyItem({ post: { uri: "at://did/x/abc" } } as never, "c"), null, "no createdAt -> rejected", ); }); test("createdAt drives both the date filter field and the month shard", () => { assert.equal(uploadDateFromCreatedAt("2026-07-10T11:46:11.858Z"), "20260710"); assert.equal(monthShardFromCreatedAt("2026-07-10T11:46:11.858Z"), "2026-07"); // Unparseable input degrades rather than poisoning the lexicographic bounds. assert.equal(uploadDateFromCreatedAt("garbage"), ""); assert.equal(monthShardFromCreatedAt("garbage"), "unknown"); });