import { describe, expect, it } from "vitest"; import { groupTimeline } from "$lib/components/timeline/group"; import type * as ShTangledFeedGetTimeline from "$lib/api/lexicons/types/sh/tangled/feed/getTimeline"; type TimelineItem = ShTangledFeedGetTimeline.$output["feed"][number]; const alice = { did: "did:plc:alice", handle: "alice.test" }; const bob = { did: "did:plc:bob", handle: "bob.test" }; const carol = { did: "did:plc:carol", handle: "carol.test" }; const dan = { did: "did:plc:dan", handle: "dan.test" }; let rkey = 0; const repo = (owner: typeof alice, slug: string) => ({ did: owner.did, slug, owner }); const follow = (actor: typeof alice, subject: string, eventAt: string) => ({ event: { $type: "sh.tangled.feed.getTimeline#followEvent", uri: `at://${actor.did}/sh.tangled.graph.follow/${rkey++}`, actor, subject: { did: `did:plc:${subject}`, handle: subject } }, eventAt }) as unknown as TimelineItem; const star = (actor: typeof alice, slug: string, eventAt: string) => ({ event: { $type: "sh.tangled.feed.getTimeline#starEvent", uri: `at://${actor.did}/sh.tangled.feed.star/${rkey++}`, actor, repo: repo(bob, slug) }, eventAt }) as unknown as TimelineItem; const create = (owner: typeof alice, slug: string, eventAt: string) => ({ event: { $type: "sh.tangled.feed.getTimeline#repoEvent", uri: `at://${owner.did}/sh.tangled.repo/${rkey++}`, repo: repo(owner, slug) }, eventAt }) as unknown as TimelineItem; const fork = (owner: typeof alice, slug: string, eventAt: string) => ({ event: { $type: "sh.tangled.feed.getTimeline#repoEvent", uri: `at://${owner.did}/sh.tangled.repo/${rkey++}`, repo: repo(owner, slug), source: repo(bob, slug) }, eventAt }) as unknown as TimelineItem; const label = (item: TimelineItem) => { const event = item.event as { actor?: { handle: string }; subject?: { handle: string }; repo: { slug: string; owner: { handle: string } }; }; return event.subject?.handle ?? event.repo.slug; }; const actorLabel = (item: TimelineItem) => { const event = item.event as { actor?: { handle: string }; repo: { owner: { handle: string } } }; return event.actor?.handle ?? event.repo.owner.handle; }; const shape = (feed: TimelineItem[]) => groupTimeline(feed).map((entry) => "actor" in entry ? [entry.kind, entry.actor.handle, ...entry.items.map((i) => label(i as TimelineItem))] : [ entry.kind, label(entry.items[0] as TimelineItem), ...entry.items.map((i) => actorLabel(i as TimelineItem)) ] ); describe("groupTimeline", () => { it("merges consecutive follows by the same actor", () => { expect( shape([ follow(alice, "a", "2026-01-01T12:00:00Z"), follow(alice, "b", "2026-01-01T11:30:00Z"), follow(alice, "c", "2026-01-01T11:00:01Z") ]) ).toEqual([["follow", "alice.test", "a", "b", "c"]]); }); it("merges stars, creations and forks the same way", () => { expect( shape([ star(alice, "a", "2026-01-01T12:00:00Z"), star(alice, "b", "2026-01-01T11:59:00Z"), create(alice, "c", "2026-01-01T11:58:00Z"), create(alice, "d", "2026-01-01T11:57:00Z"), fork(alice, "e", "2026-01-01T11:56:00Z"), fork(alice, "f", "2026-01-01T11:55:00Z") ]) ).toEqual([ ["star", "alice.test", "a", "b"], ["create", "alice.test", "c", "d"], ["fork", "alice.test", "e", "f"] ]); }); it("attributes repo events to the repo owner", () => { expect( shape([ create(alice, "a", "2026-01-01T12:00:00Z"), create(bob, "b", "2026-01-01T11:59:00Z") ]) ).toEqual([ ["create", "alice.test", "a"], ["create", "bob.test", "b"] ]); }); it("merges an uninterrupted run by one actor regardless of time", () => { expect( shape([ star(alice, "a", "2026-01-01T12:00:00Z"), star(alice, "b", "2026-01-01T08:00:00Z") ]) ).toEqual([["star", "alice.test", "a", "b"]]); }); it("merges across another actor's event within an hour", () => { expect( shape([ star(alice, "a", "2026-01-01T12:00:00Z"), star(bob, "b", "2026-01-01T11:50:00Z"), star(alice, "c", "2026-01-01T11:30:00Z") ]) ).toEqual([ ["star", "alice.test", "a", "c"], ["star", "bob.test", "b"] ]); }); it("splits across another actor's event after an hour or more", () => { expect( shape([ star(alice, "a", "2026-01-01T12:00:00Z"), star(bob, "b", "2026-01-01T11:30:00Z"), star(alice, "c", "2026-01-01T11:00:00Z") ]) ).toEqual([ ["star", "alice.test", "a"], ["star", "bob.test", "b"], ["star", "alice.test", "c"] ]); }); it("merges across the same actor's events of another kind", () => { expect( shape([ follow(alice, "a", "2026-01-01T12:00:00Z"), star(alice, "b", "2026-01-01T09:00:00Z"), follow(alice, "c", "2026-01-01T08:00:00Z"), star(alice, "d", "2026-01-01T07:00:00Z"), star(alice, "e", "2026-01-01T06:00:00Z") ]) ).toEqual([ ["follow", "alice.test", "a", "c"], ["star", "alice.test", "b", "d", "e"] ]); }); it("measures the window between neighbours, not from the first event", () => { expect( shape([ follow(alice, "a", "2026-01-01T12:00:00Z"), follow(alice, "b", "2026-01-01T11:20:00Z"), follow(alice, "c", "2026-01-01T10:40:00Z") ]) ).toEqual([["follow", "alice.test", "a", "b", "c"]]); }); it("splits on a different actor", () => { expect( shape([ follow(alice, "a", "2026-01-01T12:00:00Z"), follow(bob, "b", "2026-01-01T11:59:00Z") ]) ).toEqual([ ["follow", "alice.test", "a"], ["follow", "bob.test", "b"] ]); }); it("keys each entry by its first event uri", () => { const feed = [ follow(alice, "a", "2026-01-01T12:00:00Z"), follow(alice, "b", "2026-01-01T11:59:00Z") ]; expect(groupTimeline(feed).map((e) => e.key)).toEqual([feed[0].event.uri]); }); it("groups three or more actors on the same repo into one stargazers card", () => { expect( shape([ star(alice, "core", "2026-01-01T12:00:00Z"), create(dan, "x", "2026-01-01T11:00:00Z"), star(bob, "core", "2026-01-01T10:00:00Z"), star(carol, "core", "2026-01-01T09:00:00Z") ]) ).toEqual([ ["stargazers", "core", "alice.test", "bob.test", "carol.test"], ["create", "dan.test", "x"] ]); }); it("leaves a repo starred by fewer than three actors to actor grouping", () => { expect( shape([ star(alice, "core", "2026-01-01T12:00:00Z"), star(bob, "core", "2026-01-01T11:59:00Z") ]) ).toEqual([ ["star", "alice.test", "core"], ["star", "bob.test", "core"] ]); }); it("counts distinct actors towards the threshold", () => { expect( shape([ star(alice, "core", "2026-01-01T12:00:00Z"), star(alice, "core", "2026-01-01T11:59:00Z"), star(bob, "core", "2026-01-01T11:58:00Z") ]) ).toEqual([ ["star", "alice.test", "core", "core"], ["star", "bob.test", "core"] ]); }); it("takes a popular target out of an actor burst without breaking it", () => { expect( shape([ star(alice, "a", "2026-01-01T12:00:00Z"), star(alice, "core", "2026-01-01T11:59:00Z"), star(bob, "core", "2026-01-01T11:00:00Z"), star(alice, "b", "2026-01-01T09:00:00Z"), star(carol, "core", "2026-01-01T08:00:00Z") ]) ).toEqual([ ["star", "alice.test", "a", "b"], ["stargazers", "core", "alice.test", "bob.test", "carol.test"] ]); }); it("groups popular follow and fork targets", () => { expect( shape([ follow(alice, "hub", "2026-01-01T12:00:00Z"), fork(alice, "core", "2026-01-01T11:59:00Z"), follow(bob, "hub", "2026-01-01T11:58:00Z"), fork(bob, "core", "2026-01-01T11:57:00Z"), follow(carol, "hub", "2026-01-01T11:56:00Z"), fork(carol, "core", "2026-01-01T11:55:00Z") ]) ).toEqual([ ["followers", "hub", "alice.test", "bob.test", "carol.test"], ["forkers", "core", "alice.test", "bob.test", "carol.test"] ]); }); });