Files
baoyu-skills/packages/baoyu-fetch/src/__tests__/x-thread.test.ts
T

306 lines
8.4 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import { extractThreadDocumentFromPayloads, extractThreadTweetsFromPayloads } from "../adapters/x/thread";
function buildTweet(options: {
id: string;
text: string;
createdAt: string;
userId?: string;
screenName?: string;
name?: string;
conversationId?: string;
inReplyToStatusId?: string;
inReplyToUserId?: string;
quotedTweet?: unknown;
}) {
const userId = options.userId ?? "3178231";
const screenName = options.screenName ?? "dotey";
const name = options.name ?? "宝玉";
return {
__typename: "Tweet",
rest_id: options.id,
legacy: {
id_str: options.id,
full_text: options.text,
favorite_count: 0,
retweet_count: 0,
reply_count: 0,
created_at: options.createdAt,
user_id_str: userId,
conversation_id_str: options.conversationId ?? options.id,
in_reply_to_status_id_str: options.inReplyToStatusId,
in_reply_to_user_id_str: options.inReplyToUserId,
},
core: {
user_results: {
result: {
core: {
name,
screen_name: screenName,
},
legacy: {},
},
},
},
quoted_status_result: options.quotedTweet
? {
result: options.quotedTweet,
}
: undefined,
};
}
function tweetEntry(tweet: unknown) {
return {
content: {
itemContent: {
tweet_results: {
result: tweet,
},
},
},
};
}
function moduleTweetItem(tweet: unknown) {
return {
item: {
itemContent: {
tweet_results: {
result: tweet,
},
},
},
};
}
describe("x thread extraction", () => {
test("keeps only the continuous same-author reply chain", () => {
const rootId = "1996285439867556304";
const reply1Id = "1996285442275340783";
const reply2Id = "1996285444582146559";
const quotedId = "1993729800922341810";
const quotedInsideThreadId = "1993729800922341811";
const otherAuthorReplyId = "2000000000000000001";
const sameAuthorAfterOtherId = "2000000000000000002";
const root = buildTweet({
id: rootId,
text: "A thread for my nana banana pro prompts 🧵",
createdAt: "Wed Dec 03 18:28:32 +0000 2025",
conversationId: rootId,
});
const reply1 = buildTweet({
id: reply1Id,
text: "Prompt 1",
createdAt: "Wed Dec 03 18:28:33 +0000 2025",
conversationId: rootId,
inReplyToStatusId: rootId,
inReplyToUserId: "3178231",
quotedTweet: buildTweet({
id: quotedInsideThreadId,
text: "Quoted inside the thread body",
createdAt: "Tue Nov 25 18:28:35 +0000 2025",
screenName: "quoted_author",
name: "Quoted Author",
}),
});
const reply2 = buildTweet({
id: reply2Id,
text: "Prompt 2",
createdAt: "Wed Dec 03 18:28:34 +0000 2025",
conversationId: rootId,
inReplyToStatusId: reply1Id,
inReplyToUserId: "3178231",
});
const quotedSameAuthor = buildTweet({
id: quotedId,
text: "Quoted standalone tweet",
createdAt: "Tue Nov 25 18:28:34 +0000 2025",
});
const otherAuthorReply = buildTweet({
id: otherAuthorReplyId,
text: "Another author joined the conversation",
createdAt: "Wed Dec 03 18:28:35 +0000 2025",
userId: "42",
screenName: "someone_else",
name: "Someone Else",
conversationId: rootId,
inReplyToStatusId: reply2Id,
inReplyToUserId: "3178231",
});
const sameAuthorAfterOther = buildTweet({
id: sameAuthorAfterOtherId,
text: "This should not be part of the continuous author chain",
createdAt: "Wed Dec 03 18:28:36 +0000 2025",
conversationId: rootId,
inReplyToStatusId: otherAuthorReplyId,
inReplyToUserId: "42",
});
const payloads = [
{
data: {
threaded_conversation_with_injections_v2: {
instructions: [
{
type: "TimelineAddEntries",
entries: [
tweetEntry(root),
tweetEntry(reply1),
tweetEntry(quotedSameAuthor),
{
content: {
items: [moduleTweetItem(reply2)],
},
},
],
},
{
type: "TimelineAddToModule",
moduleItems: [moduleTweetItem(otherAuthorReply), moduleTweetItem(sameAuthorAfterOther)],
},
],
},
},
},
];
const tweets = extractThreadTweetsFromPayloads(
payloads,
rootId,
"https://x.com/dotey/status/1996285439867556304",
);
expect(tweets.map((tweet) => tweet.id)).toEqual([rootId, reply1Id, reply2Id]);
const document = extractThreadDocumentFromPayloads(
payloads,
rootId,
"https://x.com/dotey/status/1996285439867556304",
);
expect(document).not.toBeNull();
expect(document?.metadata?.tweetCount).toBe(3);
expect(document?.metadata?.lastTweetId).toBe(reply2Id);
const content = document?.content[0];
expect(content?.type).toBe("markdown");
if (!content || content.type !== "markdown") {
throw new Error("Expected markdown content");
}
expect(content.markdown).toContain("Prompt 1");
expect(content.markdown).toContain("Prompt 2");
expect(content.markdown).toContain("Quoted inside the thread body");
expect(content.markdown).not.toContain("Quoted standalone tweet");
expect(content.markdown).not.toContain("This should not be part of the continuous author chain");
});
test("returns null when there is no same-author reply chain", () => {
const rootId = "1996285439867556304";
const root = buildTweet({
id: rootId,
text: "Root tweet",
createdAt: "Wed Dec 03 18:28:32 +0000 2025",
conversationId: rootId,
});
const quotedSameAuthor = buildTweet({
id: "1993729800922341810",
text: "Quoted standalone tweet",
createdAt: "Tue Nov 25 18:28:34 +0000 2025",
});
const payloads = [
{
data: {
threaded_conversation_with_injections_v2: {
instructions: [
{
type: "TimelineAddEntries",
entries: [tweetEntry(root), tweetEntry(quotedSameAuthor)],
},
],
},
},
},
];
expect(
extractThreadDocumentFromPayloads(
payloads,
rootId,
"https://x.com/dotey/status/1996285439867556304",
),
).toBeNull();
});
test("restores ancestors when the requested tweet is in the middle of a thread", () => {
const rootId = "1996285439867556304";
const reply1Id = "1996285442275340783";
const reply2Id = "1996285444582146559";
const root = buildTweet({
id: rootId,
text: "Root tweet",
createdAt: "Wed Dec 03 18:28:32 +0000 2025",
conversationId: rootId,
});
const reply1 = buildTweet({
id: reply1Id,
text: "Middle tweet",
createdAt: "Wed Dec 03 18:28:33 +0000 2025",
conversationId: rootId,
inReplyToStatusId: rootId,
inReplyToUserId: "3178231",
});
const reply2 = buildTweet({
id: reply2Id,
text: "Last tweet",
createdAt: "Wed Dec 03 18:28:34 +0000 2025",
conversationId: rootId,
inReplyToStatusId: reply1Id,
inReplyToUserId: "3178231",
});
const payloads = [
{
data: {
threaded_conversation_with_injections_v2: {
instructions: [
{
type: "TimelineAddEntries",
entries: [
tweetEntry(root),
tweetEntry(reply1),
tweetEntry(reply2),
],
},
],
},
},
},
];
const tweets = extractThreadTweetsFromPayloads(
payloads,
reply1Id,
"https://x.com/dotey/status/1996285442275340783",
);
expect(tweets.map((tweet) => tweet.id)).toEqual([rootId, reply1Id, reply2Id]);
const document = extractThreadDocumentFromPayloads(
payloads,
reply1Id,
"https://x.com/dotey/status/1996285442275340783",
);
expect(document).not.toBeNull();
expect(document?.metadata?.tweetId).toBe(rootId);
expect(document?.metadata?.lastTweetId).toBe(reply2Id);
expect(document?.metadata?.tweetCount).toBe(3);
});
});