import { describe, expect, it } from "vitest";
import { groupTimeline } from "#lib/components/timeline/group.ts";
import type * as OrgTangledFeedGetTimeline from "#lib/api/lexicons/types/org/tangled/feed/getTimeline.ts";
type TimelineItem = OrgTangledFeedGetTimeline.$output["feed"][number];
const alice = { did: "did:plc:alice", handle: "alice.test" };
const bob = { did: "did:plc:bob", handle: "bob.test" };
const charlie = { did: "did:plc:charlie", handle: "charlie.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: "org.tangled.feed.getTimeline#followEvent",
uri: `at://${actor.did}/org.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: "org.tangled.feed.getTimeline#starEvent",
uri: `at://${actor.did}/org.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: "org.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: "org.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(charlie, "core", "2026-01-01T09:00:00Z")
])
).toEqual([
["stargazers", "core", "alice.test", "bob.test", "charlie.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(charlie, "core", "2026-01-01T08:00:00Z")
])
).toEqual([
["star", "alice.test", "a", "b"],
["stargazers", "core", "alice.test", "bob.test", "charlie.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(charlie, "hub", "2026-01-01T11:56:00Z"),
fork(charlie, "core", "2026-01-01T11:55:00Z")
])
).toEqual([
["followers", "hub", "alice.test", "bob.test", "charlie.test"],
["forkers", "core", "alice.test", "bob.test", "charlie.test"]
]);
});
});