import { test, expect, describe, beforeEach, afterEach } from "bun:test"; import { scoreFreshness, scoreEngagement, scoreRecency, combineScores, formatOutput, getProfile, getAllFollows, getAllPosts, getAllFollowRecords, resolvePds, setFetchImpl, resetFetchImpl, SCORE_DIRECT_REPLY, SCORE_THREAD_REPLY, SCORE_FRESHNESS_MAX, SCORE_RECENCY_PENALTY_PER_DAY, type Follow, type FeedItem, type ScoredEntry, } from "./index"; describe("scoreFreshness", () => { test("returns 0 for follows without a date", () => { const follows = new Map([ ["did:plc:abc", { did: "did:plc:abc", handle: "alice.bsky.social" }], ]); const followDates = new Map(); const scores = scoreFreshness(followDates, follows); expect(scores.get("did:plc:abc")).toBe(0); }); test("returns max score for follows from today", () => { const follows = new Map([ ["did:plc:abc", { did: "did:plc:abc", handle: "alice.bsky.social" }], ]); const followDates = new Map([ ["did:plc:abc", new Date()], ]); const scores = scoreFreshness(followDates, follows); expect(scores.get("did:plc:abc")).toBe(SCORE_FRESHNESS_MAX); }); test("decays score by 2 points per day", () => { const follows = new Map([ ["did:plc:abc", { did: "did:plc:abc", handle: "alice.bsky.social" }], ]); const fiveDaysAgo = new Date(Date.now() - 5 * 24 * 60 * 60 * 1000); const followDates = new Map([ ["did:plc:abc", fiveDaysAgo], ]); const scores = scoreFreshness(followDates, follows); expect(scores.get("did:plc:abc")).toBe(SCORE_FRESHNESS_MAX - 10); }); test("returns 0 for very old follows", () => { const follows = new Map([ ["did:plc:abc", { did: "did:plc:abc", handle: "alice.bsky.social" }], ]); const longAgo = new Date(Date.now() - 100 * 24 * 60 * 60 * 1000); const followDates = new Map([ ["did:plc:abc", longAgo], ]); const scores = scoreFreshness(followDates, follows); expect(scores.get("did:plc:abc")).toBe(0); }); }); describe("scoreEngagement", () => { const myDid = "did:plc:me"; test("returns 0 for follows with no replies", () => { const follows = new Map([ ["did:plc:alice", { did: "did:plc:alice", handle: "alice.bsky.social" }], ]); const posts: FeedItem[] = []; const scores = scoreEngagement(posts, follows, myDid); expect(scores.get("did:plc:alice")).toBe(0); }); test("scores direct replies with SCORE_DIRECT_REPLY points", () => { const follows = new Map([ ["did:plc:alice", { did: "did:plc:alice", handle: "alice.bsky.social" }], ]); const posts: FeedItem[] = [ { post: { uri: "at://did:plc:me/app.bsky.feed.post/1", author: { did: myDid, handle: "me.bsky.social" }, record: { reply: { parent: { uri: "at://did:plc:alice/app.bsky.feed.post/1", cid: "cid1" }, root: { uri: "at://did:plc:alice/app.bsky.feed.post/1", cid: "cid1" }, }, }, }, reply: { parent: { author: { did: "did:plc:alice", handle: "alice.bsky.social" } }, root: { author: { did: "did:plc:alice", handle: "alice.bsky.social" } }, }, }, ]; const scores = scoreEngagement(posts, follows, myDid); expect(scores.get("did:plc:alice")).toBe(SCORE_DIRECT_REPLY); }); test("scores thread replies with SCORE_THREAD_REPLY points", () => { const follows = new Map([ ["did:plc:alice", { did: "did:plc:alice", handle: "alice.bsky.social" }], ["did:plc:bob", { did: "did:plc:bob", handle: "bob.bsky.social" }], ]); const posts: FeedItem[] = [ { post: { uri: "at://did:plc:me/app.bsky.feed.post/1", author: { did: myDid, handle: "me.bsky.social" }, record: { reply: { parent: { uri: "at://did:plc:bob/app.bsky.feed.post/2", cid: "cid2" }, root: { uri: "at://did:plc:alice/app.bsky.feed.post/1", cid: "cid1" }, }, }, }, reply: { parent: { author: { did: "did:plc:bob", handle: "bob.bsky.social" } }, root: { author: { did: "did:plc:alice", handle: "alice.bsky.social" } }, }, }, ]; const scores = scoreEngagement(posts, follows, myDid); expect(scores.get("did:plc:alice")).toBe(SCORE_THREAD_REPLY); expect(scores.get("did:plc:bob")).toBe(SCORE_DIRECT_REPLY); }); test("accumulates scores for multiple replies", () => { const follows = new Map([ ["did:plc:alice", { did: "did:plc:alice", handle: "alice.bsky.social" }], ]); const posts: FeedItem[] = [ { post: { uri: "at://did:plc:me/app.bsky.feed.post/1", author: { did: myDid, handle: "me.bsky.social" }, record: { reply: { parent: { uri: "at://did:plc:alice/app.bsky.feed.post/1", cid: "cid1" }, root: { uri: "at://did:plc:alice/app.bsky.feed.post/1", cid: "cid1" }, }, }, }, reply: { parent: { author: { did: "did:plc:alice", handle: "alice.bsky.social" } }, root: { author: { did: "did:plc:alice", handle: "alice.bsky.social" } }, }, }, { post: { uri: "at://did:plc:me/app.bsky.feed.post/2", author: { did: myDid, handle: "me.bsky.social" }, record: { reply: { parent: { uri: "at://did:plc:alice/app.bsky.feed.post/2", cid: "cid2" }, root: { uri: "at://did:plc:alice/app.bsky.feed.post/2", cid: "cid2" }, }, }, }, reply: { parent: { author: { did: "did:plc:alice", handle: "alice.bsky.social" } }, root: { author: { did: "did:plc:alice", handle: "alice.bsky.social" } }, }, }, ]; const scores = scoreEngagement(posts, follows, myDid); expect(scores.get("did:plc:alice")).toBe(SCORE_DIRECT_REPLY * 2); }); test("ignores replies to self", () => { const follows = new Map([ [myDid, { did: myDid, handle: "me.bsky.social" }], ]); const posts: FeedItem[] = [ { post: { uri: "at://did:plc:me/app.bsky.feed.post/2", author: { did: myDid, handle: "me.bsky.social" }, record: { reply: { parent: { uri: "at://did:plc:me/app.bsky.feed.post/1", cid: "cid1" }, root: { uri: "at://did:plc:me/app.bsky.feed.post/1", cid: "cid1" }, }, }, }, reply: { parent: { author: { did: myDid, handle: "me.bsky.social" } }, root: { author: { did: myDid, handle: "me.bsky.social" } }, }, }, ]; const scores = scoreEngagement(posts, follows, myDid); expect(scores.get(myDid)).toBe(0); }); test("ignores replies to accounts not followed", () => { const follows = new Map([ ["did:plc:alice", { did: "did:plc:alice", handle: "alice.bsky.social" }], ]); const posts: FeedItem[] = [ { post: { uri: "at://did:plc:me/app.bsky.feed.post/1", author: { did: myDid, handle: "me.bsky.social" }, record: { reply: { parent: { uri: "at://did:plc:stranger/app.bsky.feed.post/1", cid: "cid1" }, root: { uri: "at://did:plc:stranger/app.bsky.feed.post/1", cid: "cid1" }, }, }, }, reply: { parent: { author: { did: "did:plc:stranger", handle: "stranger.bsky.social" } }, root: { author: { did: "did:plc:stranger", handle: "stranger.bsky.social" } }, }, }, ]; const scores = scoreEngagement(posts, follows, myDid); expect(scores.get("did:plc:alice")).toBe(0); }); test("skips posts without replies", () => { const follows = new Map([ ["did:plc:alice", { did: "did:plc:alice", handle: "alice.bsky.social" }], ]); const posts: FeedItem[] = [ { post: { uri: "at://did:plc:me/app.bsky.feed.post/1", author: { did: myDid, handle: "me.bsky.social" }, record: {}, }, }, ]; const scores = scoreEngagement(posts, follows, myDid); expect(scores.get("did:plc:alice")).toBe(0); }); }); describe("scoreRecency", () => { const myDid = "did:plc:me"; const now = new Date("2025-01-15T12:00:00Z"); test("penalizes follows with no engagement based on follow age", () => { const follows = new Map([ ["did:plc:alice", { did: "did:plc:alice", handle: "alice.bsky.social" }], ]); const followDates = new Map([ ["did:plc:alice", new Date("2025-01-05T12:00:00Z")], // 10 days ago ]); const posts: FeedItem[] = []; const scores = scoreRecency(posts, follows, myDid, followDates, now); expect(scores.get("did:plc:alice")).toBe(-10 * SCORE_RECENCY_PENALTY_PER_DAY); }); test("returns 0 for follows with no engagement and no follow date", () => { const follows = new Map([ ["did:plc:alice", { did: "did:plc:alice", handle: "alice.bsky.social" }], ]); const followDates = new Map(); const posts: FeedItem[] = []; const scores = scoreRecency(posts, follows, myDid, followDates, now); expect(scores.get("did:plc:alice")).toBe(0); }); test("returns 0 penalty for engagement from today", () => { const follows = new Map([ ["did:plc:alice", { did: "did:plc:alice", handle: "alice.bsky.social" }], ]); const followDates = new Map(); const posts: FeedItem[] = [ { post: { uri: "at://did:plc:me/app.bsky.feed.post/1", author: { did: myDid, handle: "me.bsky.social" }, record: { createdAt: "2025-01-15T10:00:00Z", reply: { parent: { uri: "at://did:plc:alice/app.bsky.feed.post/1", cid: "cid1" }, root: { uri: "at://did:plc:alice/app.bsky.feed.post/1", cid: "cid1" }, }, }, }, reply: { parent: { author: { did: "did:plc:alice", handle: "alice.bsky.social" } }, root: { author: { did: "did:plc:alice", handle: "alice.bsky.social" } }, }, }, ]; const scores = scoreRecency(posts, follows, myDid, followDates, now); expect(scores.get("did:plc:alice")).toBe(0); }); test("subtracts 1 point per day since last engagement", () => { const follows = new Map([ ["did:plc:alice", { did: "did:plc:alice", handle: "alice.bsky.social" }], ]); const followDates = new Map(); // Post from 10 days ago const posts: FeedItem[] = [ { post: { uri: "at://did:plc:me/app.bsky.feed.post/1", author: { did: myDid, handle: "me.bsky.social" }, record: { createdAt: "2025-01-05T10:00:00Z", reply: { parent: { uri: "at://did:plc:alice/app.bsky.feed.post/1", cid: "cid1" }, root: { uri: "at://did:plc:alice/app.bsky.feed.post/1", cid: "cid1" }, }, }, }, reply: { parent: { author: { did: "did:plc:alice", handle: "alice.bsky.social" } }, root: { author: { did: "did:plc:alice", handle: "alice.bsky.social" } }, }, }, ]; const scores = scoreRecency(posts, follows, myDid, followDates, now); expect(scores.get("did:plc:alice")).toBe(-10 * SCORE_RECENCY_PENALTY_PER_DAY); }); test("uses most recent engagement when multiple replies exist", () => { const follows = new Map([ ["did:plc:alice", { did: "did:plc:alice", handle: "alice.bsky.social" }], ]); const followDates = new Map(); const posts: FeedItem[] = [ // Older post from 20 days ago { post: { uri: "at://did:plc:me/app.bsky.feed.post/1", author: { did: myDid, handle: "me.bsky.social" }, record: { createdAt: "2024-12-26T10:00:00Z", reply: { parent: { uri: "at://did:plc:alice/app.bsky.feed.post/1", cid: "cid1" }, root: { uri: "at://did:plc:alice/app.bsky.feed.post/1", cid: "cid1" }, }, }, }, reply: { parent: { author: { did: "did:plc:alice", handle: "alice.bsky.social" } }, root: { author: { did: "did:plc:alice", handle: "alice.bsky.social" } }, }, }, // More recent post from 5 days ago { post: { uri: "at://did:plc:me/app.bsky.feed.post/2", author: { did: myDid, handle: "me.bsky.social" }, record: { createdAt: "2025-01-10T10:00:00Z", reply: { parent: { uri: "at://did:plc:alice/app.bsky.feed.post/2", cid: "cid2" }, root: { uri: "at://did:plc:alice/app.bsky.feed.post/2", cid: "cid2" }, }, }, }, reply: { parent: { author: { did: "did:plc:alice", handle: "alice.bsky.social" } }, root: { author: { did: "did:plc:alice", handle: "alice.bsky.social" } }, }, }, ]; const scores = scoreRecency(posts, follows, myDid, followDates, now); // Should use the 5-day-old engagement, not the 20-day-old one expect(scores.get("did:plc:alice")).toBe(-5 * SCORE_RECENCY_PENALTY_PER_DAY); }); test("tracks thread participation for recency", () => { const follows = new Map([ ["did:plc:alice", { did: "did:plc:alice", handle: "alice.bsky.social" }], ["did:plc:bob", { did: "did:plc:bob", handle: "bob.bsky.social" }], ]); const followDates = new Map(); // Reply to bob in alice's thread from 7 days ago const posts: FeedItem[] = [ { post: { uri: "at://did:plc:me/app.bsky.feed.post/1", author: { did: myDid, handle: "me.bsky.social" }, record: { createdAt: "2025-01-08T10:00:00Z", reply: { parent: { uri: "at://did:plc:bob/app.bsky.feed.post/2", cid: "cid2" }, root: { uri: "at://did:plc:alice/app.bsky.feed.post/1", cid: "cid1" }, }, }, }, reply: { parent: { author: { did: "did:plc:bob", handle: "bob.bsky.social" } }, root: { author: { did: "did:plc:alice", handle: "alice.bsky.social" } }, }, }, ]; const scores = scoreRecency(posts, follows, myDid, followDates, now); // Both should have recency tracked expect(scores.get("did:plc:alice")).toBe(-7 * SCORE_RECENCY_PENALTY_PER_DAY); expect(scores.get("did:plc:bob")).toBe(-7 * SCORE_RECENCY_PENALTY_PER_DAY); }); test("ignores replies to self", () => { const follows = new Map([ [myDid, { did: myDid, handle: "me.bsky.social" }], ]); const followDates = new Map(); const posts: FeedItem[] = [ { post: { uri: "at://did:plc:me/app.bsky.feed.post/2", author: { did: myDid, handle: "me.bsky.social" }, record: { createdAt: "2025-01-10T10:00:00Z", reply: { parent: { uri: "at://did:plc:me/app.bsky.feed.post/1", cid: "cid1" }, root: { uri: "at://did:plc:me/app.bsky.feed.post/1", cid: "cid1" }, }, }, }, reply: { parent: { author: { did: myDid, handle: "me.bsky.social" } }, root: { author: { did: myDid, handle: "me.bsky.social" } }, }, }, ]; const scores = scoreRecency(posts, follows, myDid, followDates, now); expect(scores.get(myDid)).toBe(0); }); test("ignores posts without createdAt and uses follow date for penalty", () => { const follows = new Map([ ["did:plc:alice", { did: "did:plc:alice", handle: "alice.bsky.social" }], ]); const followDates = new Map([ ["did:plc:alice", new Date("2025-01-10T12:00:00Z")], // 5 days ago ]); const posts: FeedItem[] = [ { post: { uri: "at://did:plc:me/app.bsky.feed.post/1", author: { did: myDid, handle: "me.bsky.social" }, record: { // No createdAt reply: { parent: { uri: "at://did:plc:alice/app.bsky.feed.post/1", cid: "cid1" }, root: { uri: "at://did:plc:alice/app.bsky.feed.post/1", cid: "cid1" }, }, }, }, reply: { parent: { author: { did: "did:plc:alice", handle: "alice.bsky.social" } }, root: { author: { did: "did:plc:alice", handle: "alice.bsky.social" } }, }, }, ]; const scores = scoreRecency(posts, follows, myDid, followDates, now); // No valid engagement due to missing createdAt, so penalty based on follow date (5 days) expect(scores.get("did:plc:alice")).toBe(-5 * SCORE_RECENCY_PENALTY_PER_DAY); }); }); describe("combineScores", () => { test("skips recency penalty when freshness is greater than zero", () => { const follows = new Map([ ["did:plc:fresh", { did: "did:plc:fresh", handle: "fresh.bsky.social" }], ["did:plc:stale", { did: "did:plc:stale", handle: "stale.bsky.social" }], ]); const engagementScores = new Map([ ["did:plc:fresh", 20], ["did:plc:stale", 20], ]); const freshnessScores = new Map([ ["did:plc:fresh", 30], // Fresh follow (freshness > 0) ["did:plc:stale", 0], // Stale follow (freshness = 0) ]); const recencyScores = new Map([ ["did:plc:fresh", -15], // Would be penalized, but freshness > 0 ["did:plc:stale", -15], // Should be penalized ]); const results = combineScores(follows, engagementScores, freshnessScores, recencyScores); const freshEntry = results.find(e => e.handle === "fresh.bsky.social")!; const staleEntry = results.find(e => e.handle === "stale.bsky.social")!; // Fresh follow: recency should be 0 (skipped), score = 20 + 30 + 0 = 50 expect(freshEntry.recency).toBe(0); expect(freshEntry.score).toBe(50); // Stale follow: recency should be applied, score = 20 + 0 + (-15) = 5 expect(staleEntry.recency).toBe(-15); expect(staleEntry.score).toBe(5); }); test("applies recency penalty when freshness is zero", () => { const follows = new Map([ ["did:plc:alice", { did: "did:plc:alice", handle: "alice.bsky.social" }], ]); const engagementScores = new Map([["did:plc:alice", 10]]); const freshnessScores = new Map([["did:plc:alice", 0]]); const recencyScores = new Map([["did:plc:alice", -5]]); const results = combineScores(follows, engagementScores, freshnessScores, recencyScores); expect(results[0].recency).toBe(-5); expect(results[0].score).toBe(5); // 10 + 0 + (-5) }); test("sorts results by score descending", () => { const follows = new Map([ ["did:plc:low", { did: "did:plc:low", handle: "low.bsky.social" }], ["did:plc:high", { did: "did:plc:high", handle: "high.bsky.social" }], ["did:plc:mid", { did: "did:plc:mid", handle: "mid.bsky.social" }], ]); const engagementScores = new Map([ ["did:plc:low", 5], ["did:plc:high", 50], ["did:plc:mid", 25], ]); const freshnessScores = new Map(); const recencyScores = new Map(); const results = combineScores(follows, engagementScores, freshnessScores, recencyScores); expect(results[0].handle).toBe("high.bsky.social"); expect(results[1].handle).toBe("mid.bsky.social"); expect(results[2].handle).toBe("low.bsky.social"); }); }); describe("formatOutput", () => { test("outputs header as first line", () => { const entries: ScoredEntry[] = []; const output = formatOutput(entries); expect(output).toBe("handle score engagement freshness recency"); }); test("formats single entry correctly", () => { const entries: ScoredEntry[] = [ { handle: "alice.bsky.social", score: 50, engagement: 30, freshness: 40, recency: -20 }, ]; const output = formatOutput(entries); const lines = output.split("\n"); expect(lines).toHaveLength(2); expect(lines[0]).toBe("handle score engagement freshness recency"); expect(lines[1]).toBe("alice.bsky.social 50 30 40 -20"); }); test("formats multiple entries in order", () => { const entries: ScoredEntry[] = [ { handle: "alice.bsky.social", score: 50, engagement: 30, freshness: 40, recency: -20 }, { handle: "bob.bsky.social", score: 25, engagement: 10, freshness: 20, recency: -5 }, { handle: "carol.bsky.social", score: 0, engagement: 0, freshness: 0, recency: 0 }, ]; const output = formatOutput(entries); const lines = output.split("\n"); expect(lines).toHaveLength(4); expect(lines[0]).toBe("handle score engagement freshness recency"); expect(lines[1]).toBe("alice.bsky.social 50 30 40 -20"); expect(lines[2]).toBe("bob.bsky.social 25 10 20 -5"); expect(lines[3]).toBe("carol.bsky.social 0 0 0 0"); }); test("handles negative scores correctly", () => { const entries: ScoredEntry[] = [ { handle: "alice.bsky.social", score: -15, engagement: 10, freshness: 5, recency: -30 }, ]; const output = formatOutput(entries); const lines = output.split("\n"); expect(lines[1]).toBe("alice.bsky.social -15 10 5 -30"); }); test("output is space-separated and parseable", () => { const entries: ScoredEntry[] = [ { handle: "alice.bsky.social", score: 100, engagement: 50, freshness: 50, recency: 0 }, ]; const output = formatOutput(entries); const lines = output.split("\n"); const headerFields = lines[0].split(" "); const dataFields = lines[1].split(" "); expect(headerFields).toEqual(["handle", "score", "engagement", "freshness", "recency"]); expect(dataFields).toHaveLength(5); expect(dataFields[0]).toBe("alice.bsky.social"); expect(parseInt(dataFields[1])).toBe(100); expect(parseInt(dataFields[2])).toBe(50); expect(parseInt(dataFields[3])).toBe(50); expect(parseInt(dataFields[4])).toBe(0); }); }); // Integration tests with mock fetch describe("API functions with mock fetch", () => { afterEach(() => { resetFetchImpl(); }); test("getProfile returns profile data", async () => { const mockProfile = { did: "did:plc:alice", handle: "alice.bsky.social", displayName: "Alice", }; setFetchImpl(async () => new Response(JSON.stringify(mockProfile))); const profile = await getProfile("alice.bsky.social"); expect(profile.did).toBe("did:plc:alice"); expect(profile.handle).toBe("alice.bsky.social"); }); test("resolvePds extracts PDS URL from DID document", async () => { const mockDidDoc = { id: "did:plc:alice", service: [ { id: "#atproto_pds", type: "AtprotoPersonalDataServer", serviceEndpoint: "https://pds.example.com", }, ], }; setFetchImpl(async () => new Response(JSON.stringify(mockDidDoc))); const pdsUrl = await resolvePds("did:plc:alice"); expect(pdsUrl).toBe("https://pds.example.com"); }); test("getAllFollows paginates through all follows", async () => { let callCount = 0; setFetchImpl(async () => { callCount++; if (callCount === 1) { return new Response(JSON.stringify({ follows: [ { did: "did:plc:bob", handle: "bob.bsky.social" }, { did: "did:plc:carol", handle: "carol.bsky.social" }, ], cursor: "page2", })); } return new Response(JSON.stringify({ follows: [ { did: "did:plc:dave", handle: "dave.bsky.social" }, ], })); }); const follows = await getAllFollows("did:plc:alice"); expect(follows.size).toBe(3); expect(follows.has("did:plc:bob")).toBe(true); expect(follows.has("did:plc:carol")).toBe(true); expect(follows.has("did:plc:dave")).toBe(true); expect(callCount).toBe(2); }); test("getAllPosts paginates through all posts", async () => { let callCount = 0; setFetchImpl(async () => { callCount++; if (callCount === 1) { return new Response(JSON.stringify({ feed: [ { post: { uri: "at://did:plc:alice/post/1", author: { did: "did:plc:alice", handle: "alice.bsky.social" }, record: {} } }, ], cursor: "page2", })); } return new Response(JSON.stringify({ feed: [ { post: { uri: "at://did:plc:alice/post/2", author: { did: "did:plc:alice", handle: "alice.bsky.social" }, record: {} } }, ], })); }); const posts = await getAllPosts("did:plc:alice"); expect(posts.length).toBe(2); expect(callCount).toBe(2); }); }); // Live integration tests against the real Bluesky API describe("live integration with alice.bsky.social", () => { test("fetches profile for alice.bsky.social", async () => { const profile = await getProfile("alice.bsky.social"); expect(profile.did).toStartWith("did:plc:"); expect(profile.handle).toBe("alice.bsky.social"); }); test("resolves PDS for alice.bsky.social", async () => { const profile = await getProfile("alice.bsky.social"); const pdsUrl = await resolvePds(profile.did); expect(pdsUrl).toStartWith("https://"); }); test("fetches follows for alice.bsky.social", async () => { const profile = await getProfile("alice.bsky.social"); const follows = await getAllFollows(profile.did); // Alice follows people, so we should get some results expect(follows.size).toBeGreaterThan(0); // Verify the structure of a follow entry const firstFollow = follows.values().next().value; expect(firstFollow.did).toStartWith("did:"); expect(typeof firstFollow.handle).toBe("string"); }); test("fetches posts for alice.bsky.social", async () => { const profile = await getProfile("alice.bsky.social"); const posts = await getAllPosts(profile.did); // Alice has posted, so we should get some results expect(posts.length).toBeGreaterThan(0); // Verify the structure of a post entry const firstPost = posts[0]; expect(firstPost.post.uri).toStartWith("at://"); expect(firstPost.post.author.did).toBe(profile.did); }); test("fetches follow records with dates for alice.bsky.social", async () => { const profile = await getProfile("alice.bsky.social"); const pdsUrl = await resolvePds(profile.did); const followDates = await getAllFollowRecords(pdsUrl, profile.did); // Should have timestamps for follows expect(followDates.size).toBeGreaterThan(0); // Verify dates are valid const firstDate = followDates.values().next().value; expect(firstDate).toBeInstanceOf(Date); expect(firstDate.getTime()).toBeGreaterThan(0); }); test("runs full scoring pipeline for alice.bsky.social", async () => { const profile = await getProfile("alice.bsky.social"); const pdsUrl = await resolvePds(profile.did); const follows = await getAllFollows(profile.did); const followDates = await getAllFollowRecords(pdsUrl, profile.did); const posts = await getAllPosts(profile.did); // Run scoring const engagementScores = scoreEngagement(posts, follows, profile.did); const freshnessScores = scoreFreshness(followDates, follows); const recencyScores = scoreRecency(posts, follows, profile.did, followDates); // Combine and format const results = combineScores(follows, engagementScores, freshnessScores, recencyScores); const output = formatOutput(results); // Verify output structure const lines = output.split("\n"); expect(lines[0]).toBe("handle score engagement freshness recency"); expect(lines.length).toBeGreaterThan(1); // Verify each data line has 5 fields for (let i = 1; i < lines.length; i++) { const fields = lines[i].split(" "); expect(fields.length).toBe(5); expect(fields[0]).toContain("."); // handle has a dot expect(Number.isInteger(parseInt(fields[1]))).toBe(true); // score is a number } }); });