Use twitter safe relay
CI / test (pull_request) Has been cancelled

This commit is contained in:
2026-06-24 03:39:34 +09:00
parent b771e827aa
commit 1f6e228272
66 changed files with 597 additions and 1720 deletions
+51 -110
View File
@@ -7,8 +7,7 @@ type RunResult = { exitCode: number; stdout: string; stderr: string; signal: Nod
const LIVE = process.env.BIRD_LIVE === '1';
const authToken = (process.env.AUTH_TOKEN ?? process.env.TWITTER_AUTH_TOKEN ?? '').trim();
const ct0 = (process.env.CT0 ?? process.env.TWITTER_CT0 ?? '').trim();
const relayBaseUrl = (process.env.TWITTER_RELAY_BASE_URL ?? '').trim();
const CLI_PATH = path.resolve(process.cwd(), 'dist', 'cli.js');
@@ -27,8 +26,6 @@ function runBird(args: string[], options: { timeoutMs?: number } = {}): Promise<
stdio: ['ignore', 'pipe', 'pipe'],
env: {
...process.env,
AUTH_TOKEN: authToken,
CT0: ct0,
NODE_ENV: LIVE_NODE_ENV,
},
});
@@ -76,7 +73,6 @@ const d = LIVE ? describe : describe.skip;
d('live CLI (Twitter/X) all commands', () => {
const timeoutArg = (process.env.BIRD_LIVE_TIMEOUT_MS ?? '20000').trim();
const cookieTimeoutArg = (process.env.BIRD_LIVE_COOKIE_TIMEOUT_MS ?? '30000').trim();
const baseArgs = ['--plain', '--timeout', timeoutArg, '--quote-depth', '0'];
let whoamiStdout = '';
@@ -92,19 +88,16 @@ d('live CLI (Twitter/X) all commands', () => {
throw new Error(`Missing ${CLI_PATH}. Run: pnpm run build:dist`);
}
if (!authToken || !ct0) {
const check = await runBird([...baseArgs, '--cookie-timeout', cookieTimeoutArg, 'check'], { timeoutMs: 45_000 });
if (check.exitCode !== 0) {
throw new Error(
'Missing live credentials.\n' +
'- Option A: set AUTH_TOKEN + CT0 (or TWITTER_AUTH_TOKEN/TWITTER_CT0)\n' +
'- Option B: login to x.com in Safari/Chrome/Firefox for cookie extraction\n\n' +
`bird check output:\n${check.stdout}\n${check.stderr}`,
);
}
if (!relayBaseUrl) {
throw new Error('Missing TWITTER_RELAY_BASE_URL for live tests.');
}
const who = await runBird([...baseArgs, '--cookie-timeout', cookieTimeoutArg, 'whoami'], { timeoutMs: 45_000 });
const check = await runBird([...baseArgs, 'check'], { timeoutMs: 45_000 });
if (check.exitCode !== 0) {
throw new Error(`bird check failed:\n${check.stdout}\n${check.stderr}`);
}
const who = await runBird([...baseArgs, 'whoami'], { timeoutMs: 45_000 });
if (who.exitCode !== 0) {
throw new Error(`whoami failed (exit ${who.exitCode}, signal ${who.signal ?? 'none'}):\n${who.stderr}`);
}
@@ -132,12 +125,9 @@ d('live CLI (Twitter/X) all commands', () => {
const searchQuery = (
process.env.BIRD_LIVE_SEARCH_QUERY ?? `from:${handle} -filter:replies -filter:retweets`
).trim();
const search = await runBird(
[...baseArgs, '--cookie-timeout', cookieTimeoutArg, 'search', searchQuery, '-n', '25', '--json'],
{
timeoutMs: 45_000,
},
);
const search = await runBird([...baseArgs, 'search', searchQuery, '-n', '25', '--json'], {
timeoutMs: 45_000,
});
if (search.exitCode !== 0) {
throw new Error(
`search failed (exit ${search.exitCode}, signal ${search.signal ?? 'none'}):\n${search.stderr}`,
@@ -157,7 +147,7 @@ d('live CLI (Twitter/X) all commands', () => {
listId = (process.env.BIRD_LIVE_LIST_ID ?? '').trim();
if (!listId) {
const lists = await runBird([...baseArgs, '--cookie-timeout', cookieTimeoutArg, 'lists', '-n', '1', '--json'], {
const lists = await runBird([...baseArgs, 'lists', '-n', '1', '--json'], {
timeoutMs: 45_000,
});
if (lists.exitCode === 0) {
@@ -166,12 +156,9 @@ d('live CLI (Twitter/X) all commands', () => {
}
}
if (!listId) {
const lists = await runBird(
[...baseArgs, '--cookie-timeout', cookieTimeoutArg, 'lists', '--member-of', '-n', '1', '--json'],
{
timeoutMs: 45_000,
},
);
const lists = await runBird([...baseArgs, 'lists', '--member-of', '-n', '1', '--json'], {
timeoutMs: 45_000,
});
if (lists.exitCode === 0) {
const parsed = parseJson<Array<{ id?: string }>>(lists.stdout);
listId = String(parsed[0]?.id ?? '').trim();
@@ -186,7 +173,7 @@ d('live CLI (Twitter/X) all commands', () => {
});
it('check works', async () => {
const check = await runBird([...baseArgs, '--cookie-timeout', cookieTimeoutArg, 'check'], { timeoutMs: 45_000 });
const check = await runBird([...baseArgs, 'check'], { timeoutMs: 45_000 });
expect(check.exitCode).toBe(0);
expect(check.stdout.length).toBeGreaterThan(0);
});
@@ -199,17 +186,14 @@ d('live CLI (Twitter/X) all commands', () => {
it('search returns JSON array', async () => {
const search = searchStdout
? { exitCode: 0, stdout: searchStdout }
: await runBird(
[...baseArgs, '--cookie-timeout', cookieTimeoutArg, 'search', `from:${handle}`, '-n', '10', '--json'],
{ timeoutMs: 45_000 },
);
: await runBird([...baseArgs, 'search', `from:${handle}`, '-n', '10', '--json'], { timeoutMs: 45_000 });
expect(search.exitCode).toBe(0);
const tweets = parseJson<Array<{ id?: string }>>(search.stdout);
expect(Array.isArray(tweets)).toBe(true);
});
it('read returns tweet JSON', async () => {
const read = await runBird([...baseArgs, '--cookie-timeout', cookieTimeoutArg, 'read', tweetId, '--json'], {
const read = await runBird([...baseArgs, 'read', tweetId, '--json'], {
timeoutMs: 45_000,
});
expect(read.exitCode).toBe(0);
@@ -219,7 +203,7 @@ d('live CLI (Twitter/X) all commands', () => {
});
it('tweet-id shorthand returns tweet JSON', async () => {
const shorthand = await runBird([...baseArgs, '--cookie-timeout', cookieTimeoutArg, tweetId, '--json'], {
const shorthand = await runBird([...baseArgs, tweetId, '--json'], {
timeoutMs: 45_000,
});
expect(shorthand.exitCode).toBe(0);
@@ -228,7 +212,7 @@ d('live CLI (Twitter/X) all commands', () => {
});
it('replies returns JSON array', async () => {
const replies = await runBird([...baseArgs, '--cookie-timeout', cookieTimeoutArg, 'replies', tweetId, '--json'], {
const replies = await runBird([...baseArgs, 'replies', tweetId, '--json'], {
timeoutMs: 45_000,
});
expect(replies.exitCode).toBe(0);
@@ -237,7 +221,7 @@ d('live CLI (Twitter/X) all commands', () => {
});
it('thread returns JSON array', async () => {
const thread = await runBird([...baseArgs, '--cookie-timeout', cookieTimeoutArg, 'thread', tweetId, '--json'], {
const thread = await runBird([...baseArgs, 'thread', tweetId, '--json'], {
timeoutMs: 45_000,
});
expect(thread.exitCode).toBe(0);
@@ -246,31 +230,25 @@ d('live CLI (Twitter/X) all commands', () => {
});
it('mentions returns JSON array', async () => {
const mentions = await runBird(
[...baseArgs, '--cookie-timeout', cookieTimeoutArg, 'mentions', '-n', '10', '--json'],
{
timeoutMs: 45_000,
},
);
const mentions = await runBird([...baseArgs, 'mentions', '-n', '10', '--json'], {
timeoutMs: 45_000,
});
expect(mentions.exitCode).toBe(0);
const mentionTweets = parseJson<Array<{ id?: string }>>(mentions.stdout);
expect(Array.isArray(mentionTweets)).toBe(true);
});
it('bookmarks returns JSON array', async () => {
const bookmarks = await runBird(
[...baseArgs, '--cookie-timeout', cookieTimeoutArg, 'bookmarks', '-n', '10', '--json'],
{
timeoutMs: 45_000,
},
);
const bookmarks = await runBird([...baseArgs, 'bookmarks', '-n', '10', '--json'], {
timeoutMs: 45_000,
});
expect(bookmarks.exitCode).toBe(0);
const bookmarkTweets = parseJson<Array<{ id?: string }>>(bookmarks.stdout);
expect(Array.isArray(bookmarkTweets)).toBe(true);
});
it('likes returns JSON array', async () => {
const likes = await runBird([...baseArgs, '--cookie-timeout', cookieTimeoutArg, 'likes', '-n', '10', '--json'], {
const likes = await runBird([...baseArgs, 'likes', '-n', '10', '--json'], {
timeoutMs: 45_000,
});
expect(likes.exitCode).toBe(0);
@@ -279,24 +257,18 @@ d('live CLI (Twitter/X) all commands', () => {
});
it('following returns JSON array', async () => {
const following = await runBird(
[...baseArgs, '--cookie-timeout', cookieTimeoutArg, 'following', '--user', userId, '-n', '10', '--json'],
{
timeoutMs: 45_000,
},
);
const following = await runBird([...baseArgs, 'following', '--user', userId, '-n', '10', '--json'], {
timeoutMs: 45_000,
});
expect(following.exitCode).toBe(0);
const followingUsers = parseJson<Array<{ id?: string; username?: string }>>(following.stdout);
expect(Array.isArray(followingUsers)).toBe(true);
});
it('followers returns JSON array', async () => {
const followers = await runBird(
[...baseArgs, '--cookie-timeout', cookieTimeoutArg, 'followers', '--user', userId, '-n', '10', '--json'],
{
timeoutMs: 45_000,
},
);
const followers = await runBird([...baseArgs, 'followers', '--user', userId, '-n', '10', '--json'], {
timeoutMs: 45_000,
});
expect(followers.exitCode).toBe(0);
const followerUsers = parseJson<Array<{ id?: string; username?: string }>>(followers.stdout);
expect(Array.isArray(followerUsers)).toBe(true);
@@ -304,12 +276,9 @@ d('live CLI (Twitter/X) all commands', () => {
it('user-tweets returns JSON array', async () => {
const testHandle = process.env.BIRD_LIVE_USER_TWEETS_HANDLE || 'X';
const userTweets = await runBird(
[...baseArgs, '--cookie-timeout', cookieTimeoutArg, 'user-tweets', testHandle, '-n', '5', '--json'],
{
timeoutMs: 45_000,
},
);
const userTweets = await runBird([...baseArgs, 'user-tweets', testHandle, '-n', '5', '--json'], {
timeoutMs: 45_000,
});
expect(userTweets.exitCode).toBe(0);
const tweets = parseJson<Array<{ id?: string; author?: { username?: string } }>>(userTweets.stdout);
expect(Array.isArray(tweets)).toBe(true);
@@ -320,20 +289,7 @@ d('live CLI (Twitter/X) all commands', () => {
const testHandle =
process.env.BIRD_LIVE_USER_TWEETS_PAGED_HANDLE ?? process.env.BIRD_LIVE_USER_TWEETS_HANDLE ?? 'X';
const userTweets = await runBird(
[
...baseArgs,
'--cookie-timeout',
cookieTimeoutArg,
'user-tweets',
testHandle,
'-n',
'50',
'--max-pages',
'3',
'--delay',
'0',
'--json',
],
[...baseArgs, 'user-tweets', testHandle, '-n', '50', '--max-pages', '3', '--delay', '0', '--json'],
{
timeoutMs: 45_000,
},
@@ -344,7 +300,7 @@ d('live CLI (Twitter/X) all commands', () => {
});
it('lists returns JSON array', async () => {
const lists = await runBird([...baseArgs, '--cookie-timeout', cookieTimeoutArg, 'lists', '-n', '10', '--json'], {
const lists = await runBird([...baseArgs, 'lists', '-n', '10', '--json'], {
timeoutMs: 45_000,
});
expect(lists.exitCode).toBe(0);
@@ -356,17 +312,14 @@ d('live CLI (Twitter/X) all commands', () => {
if (!listTimelineAvailable) {
return;
}
const timeline = await runBird(
[...baseArgs, '--cookie-timeout', cookieTimeoutArg, 'list-timeline', listId, '-n', '5', '--json'],
{ timeoutMs: 45_000 },
);
const timeline = await runBird([...baseArgs, 'list-timeline', listId, '-n', '5', '--json'], { timeoutMs: 45_000 });
expect(timeline.exitCode).toBe(0);
const payload = parseJson<Array<{ id?: string }>>(timeline.stdout);
expect(Array.isArray(payload)).toBe(true);
});
it('home for-you returns JSON array', async () => {
const home = await runBird([...baseArgs, '--cookie-timeout', cookieTimeoutArg, 'home', '-n', '5', '--json'], {
const home = await runBird([...baseArgs, 'home', '-n', '5', '--json'], {
timeoutMs: 45_000,
});
expect(home.exitCode).toBe(0);
@@ -375,20 +328,14 @@ d('live CLI (Twitter/X) all commands', () => {
}, 60_000);
it('home following returns JSON array', async () => {
let home = await runBird(
[...baseArgs, '--cookie-timeout', cookieTimeoutArg, 'home', '--following', '-n', '5', '--json'],
{
timeoutMs: 45_000,
},
);
let home = await runBird([...baseArgs, 'home', '--following', '-n', '5', '--json'], {
timeoutMs: 45_000,
});
if (home.exitCode !== 0 && home.stderr.includes('OverCapacity')) {
await sleep(1500);
home = await runBird(
[...baseArgs, '--cookie-timeout', cookieTimeoutArg, 'home', '--following', '-n', '5', '--json'],
{
timeoutMs: 45_000,
},
);
home = await runBird([...baseArgs, 'home', '--following', '-n', '5', '--json'], {
timeoutMs: 45_000,
});
}
expect(home.exitCode).toBe(0);
const payload = parseJson<Array<{ id?: string }>>(home.stdout);
@@ -396,27 +343,21 @@ d('live CLI (Twitter/X) all commands', () => {
}, 60_000);
it('news returns JSON array', async () => {
const news = await runBird(
[...baseArgs, '--cookie-timeout', cookieTimeoutArg, 'news', '-n', '5', '--ai-only', '--json'],
{ timeoutMs: 45_000 },
);
const news = await runBird([...baseArgs, 'news', '-n', '5', '--ai-only', '--json'], { timeoutMs: 45_000 });
expect(news.exitCode).toBe(0);
const payload = parseJson<Array<{ id?: string }>>(news.stdout);
expect(Array.isArray(payload)).toBe(true);
});
it('trending returns JSON array', async () => {
const trending = await runBird(
[...baseArgs, '--cookie-timeout', cookieTimeoutArg, 'trending', '-n', '5', '--json'],
{ timeoutMs: 45_000 },
);
const trending = await runBird([...baseArgs, 'trending', '-n', '5', '--json'], { timeoutMs: 45_000 });
expect(trending.exitCode).toBe(0);
const payload = parseJson<Array<{ id?: string }>>(trending.stdout);
expect(Array.isArray(payload)).toBe(true);
});
it('query-ids returns JSON', async () => {
const queryIds = await runBird([...baseArgs, '--cookie-timeout', cookieTimeoutArg, 'query-ids', '--json'], {
const queryIds = await runBird([...baseArgs, 'query-ids', '--json'], {
timeoutMs: 60_000,
});
expect(queryIds.exitCode).toBe(0);
+38 -77
View File
@@ -7,8 +7,7 @@ type RunResult = { exitCode: number; stdout: string; stderr: string; signal: Nod
const LIVE = process.env.BIRD_LIVE === '1';
const authToken = (process.env.AUTH_TOKEN ?? process.env.TWITTER_AUTH_TOKEN ?? '').trim();
const ct0 = (process.env.CT0 ?? process.env.TWITTER_CT0 ?? '').trim();
const relayBaseUrl = (process.env.TWITTER_RELAY_BASE_URL ?? '').trim();
const CLI_PATH = path.resolve(process.cwd(), 'dist', 'cli.js');
@@ -27,8 +26,6 @@ function runBird(args: string[], options: { timeoutMs?: number } = {}): Promise<
stdio: ['ignore', 'pipe', 'pipe'],
env: {
...process.env,
AUTH_TOKEN: authToken,
CT0: ct0,
NODE_ENV: LIVE_NODE_ENV,
},
});
@@ -72,7 +69,6 @@ const d = LIVE ? describe : describe.skip;
d('live CLI (Twitter/X)', () => {
const timeoutArg = (process.env.BIRD_LIVE_TIMEOUT_MS ?? '20000').trim();
const cookieTimeoutArg = (process.env.BIRD_LIVE_COOKIE_TIMEOUT_MS ?? '30000').trim();
const baseArgs = ['--plain', '--timeout', timeoutArg, '--quote-depth', '0'];
let whoamiStdout = '';
@@ -85,19 +81,16 @@ d('live CLI (Twitter/X)', () => {
throw new Error(`Missing ${CLI_PATH}. Run: pnpm run build:dist`);
}
if (!authToken || !ct0) {
const check = await runBird([...baseArgs, '--cookie-timeout', cookieTimeoutArg, 'check'], { timeoutMs: 45_000 });
if (check.exitCode !== 0) {
throw new Error(
'Missing live credentials.\n' +
'- Option A: set AUTH_TOKEN + CT0 (or TWITTER_AUTH_TOKEN/TWITTER_CT0)\n' +
'- Option B: login to x.com in Safari/Chrome/Firefox for cookie extraction\n\n' +
`bird check output:\n${check.stdout}\n${check.stderr}`,
);
}
if (!relayBaseUrl) {
throw new Error('Missing TWITTER_RELAY_BASE_URL for live tests.');
}
const who = await runBird([...baseArgs, '--cookie-timeout', cookieTimeoutArg, 'whoami'], { timeoutMs: 45_000 });
const check = await runBird([...baseArgs, 'check'], { timeoutMs: 45_000 });
if (check.exitCode !== 0) {
throw new Error(`bird check failed:\n${check.stdout}\n${check.stderr}`);
}
const who = await runBird([...baseArgs, 'whoami'], { timeoutMs: 45_000 });
if (who.exitCode !== 0) {
throw new Error(`whoami failed (exit ${who.exitCode}, signal ${who.signal ?? 'none'}):\n${who.stderr}`);
}
@@ -127,12 +120,9 @@ d('live CLI (Twitter/X)', () => {
const searchQuery = (
process.env.BIRD_LIVE_SEARCH_QUERY ?? `from:${handle} -filter:replies -filter:retweets`
).trim();
const search = await runBird(
[...baseArgs, '--cookie-timeout', cookieTimeoutArg, 'search', searchQuery, '-n', '25', '--json'],
{
timeoutMs: 45_000,
},
);
const search = await runBird([...baseArgs, 'search', searchQuery, '-n', '25', '--json'], {
timeoutMs: 45_000,
});
if (search.exitCode !== 0) {
throw new Error(`search failed (exit ${search.exitCode}, signal ${search.signal ?? 'none'}):\n${search.stderr}`);
}
@@ -160,7 +150,7 @@ d('live CLI (Twitter/X)', () => {
});
it('read returns tweet JSON', async () => {
const read = await runBird([...baseArgs, '--cookie-timeout', cookieTimeoutArg, 'read', tweetId, '--json'], {
const read = await runBird([...baseArgs, 'read', tweetId, '--json'], {
timeoutMs: 45_000,
});
expect(read.exitCode).toBe(0);
@@ -170,7 +160,7 @@ d('live CLI (Twitter/X)', () => {
});
it('tweet-id shorthand returns tweet JSON', async () => {
const shorthand = await runBird([...baseArgs, '--cookie-timeout', cookieTimeoutArg, tweetId, '--json'], {
const shorthand = await runBird([...baseArgs, tweetId, '--json'], {
timeoutMs: 45_000,
});
expect(shorthand.exitCode).toBe(0);
@@ -179,7 +169,7 @@ d('live CLI (Twitter/X)', () => {
});
it('replies returns JSON array', async () => {
const replies = await runBird([...baseArgs, '--cookie-timeout', cookieTimeoutArg, 'replies', tweetId, '--json'], {
const replies = await runBird([...baseArgs, 'replies', tweetId, '--json'], {
timeoutMs: 45_000,
});
expect(replies.exitCode).toBe(0);
@@ -188,7 +178,7 @@ d('live CLI (Twitter/X)', () => {
});
it('thread returns JSON array', async () => {
const thread = await runBird([...baseArgs, '--cookie-timeout', cookieTimeoutArg, 'thread', tweetId, '--json'], {
const thread = await runBird([...baseArgs, 'thread', tweetId, '--json'], {
timeoutMs: 45_000,
});
expect(thread.exitCode).toBe(0);
@@ -198,24 +188,18 @@ d('live CLI (Twitter/X)', () => {
});
it('mentions returns JSON array', async () => {
const mentions = await runBird(
[...baseArgs, '--cookie-timeout', cookieTimeoutArg, 'mentions', '-n', '10', '--json'],
{
timeoutMs: 45_000,
},
);
const mentions = await runBird([...baseArgs, 'mentions', '-n', '10', '--json'], {
timeoutMs: 45_000,
});
expect(mentions.exitCode).toBe(0);
const mentionTweets = parseJson<Array<{ id?: string }>>(mentions.stdout);
expect(Array.isArray(mentionTweets)).toBe(true);
});
it('bookmarks returns JSON array', async () => {
const bookmarks = await runBird(
[...baseArgs, '--cookie-timeout', cookieTimeoutArg, 'bookmarks', '-n', '10', '--json'],
{
timeoutMs: 45_000,
},
);
const bookmarks = await runBird([...baseArgs, 'bookmarks', '-n', '10', '--json'], {
timeoutMs: 45_000,
});
expect(bookmarks.exitCode).toBe(0);
const bookmarkTweets = parseJson<Array<{ id?: string }>>(bookmarks.stdout);
expect(Array.isArray(bookmarkTweets)).toBe(true);
@@ -226,17 +210,16 @@ d('live CLI (Twitter/X)', () => {
if (!folderId) {
return;
}
const bookmarks = await runBird(
[...baseArgs, '--cookie-timeout', cookieTimeoutArg, 'bookmarks', '--folder-id', folderId, '-n', '10', '--json'],
{ timeoutMs: 45_000 },
);
const bookmarks = await runBird([...baseArgs, 'bookmarks', '--folder-id', folderId, '-n', '10', '--json'], {
timeoutMs: 45_000,
});
expect(bookmarks.exitCode).toBe(0);
const bookmarkTweets = parseJson<Array<{ id?: string }>>(bookmarks.stdout);
expect(Array.isArray(bookmarkTweets)).toBe(true);
});
it('likes returns JSON array', async () => {
const likes = await runBird([...baseArgs, '--cookie-timeout', cookieTimeoutArg, 'likes', '-n', '10', '--json'], {
const likes = await runBird([...baseArgs, 'likes', '-n', '10', '--json'], {
timeoutMs: 45_000,
});
expect(likes.exitCode).toBe(0);
@@ -245,24 +228,18 @@ d('live CLI (Twitter/X)', () => {
});
it('following returns JSON array', async () => {
const following = await runBird(
[...baseArgs, '--cookie-timeout', cookieTimeoutArg, 'following', '--user', userId, '-n', '10', '--json'],
{
timeoutMs: 45_000,
},
);
const following = await runBird([...baseArgs, 'following', '--user', userId, '-n', '10', '--json'], {
timeoutMs: 45_000,
});
expect(following.exitCode).toBe(0);
const followingUsers = parseJson<Array<{ id?: string; username?: string }>>(following.stdout);
expect(Array.isArray(followingUsers)).toBe(true);
});
it('followers returns JSON array', async () => {
const followers = await runBird(
[...baseArgs, '--cookie-timeout', cookieTimeoutArg, 'followers', '--user', userId, '-n', '10', '--json'],
{
timeoutMs: 45_000,
},
);
const followers = await runBird([...baseArgs, 'followers', '--user', userId, '-n', '10', '--json'], {
timeoutMs: 45_000,
});
expect(followers.exitCode).toBe(0);
const followerUsers = parseJson<Array<{ id?: string; username?: string }>>(followers.stdout);
expect(Array.isArray(followerUsers)).toBe(true);
@@ -271,12 +248,9 @@ d('live CLI (Twitter/X)', () => {
it('user-tweets returns JSON array', async () => {
// Use a known active account for reliable testing (authenticated user may have no tweets)
const testHandle = process.env.BIRD_LIVE_USER_TWEETS_HANDLE || 'X';
const userTweets = await runBird(
[...baseArgs, '--cookie-timeout', cookieTimeoutArg, 'user-tweets', testHandle, '-n', '5', '--json'],
{
timeoutMs: 45_000,
},
);
const userTweets = await runBird([...baseArgs, 'user-tweets', testHandle, '-n', '5', '--json'], {
timeoutMs: 45_000,
});
expect(userTweets.exitCode).toBe(0);
const tweets = parseJson<Array<{ id?: string; author?: { username?: string } }>>(userTweets.stdout);
expect(Array.isArray(tweets)).toBe(true);
@@ -289,20 +263,7 @@ d('live CLI (Twitter/X)', () => {
const testHandle =
process.env.BIRD_LIVE_USER_TWEETS_PAGED_HANDLE ?? process.env.BIRD_LIVE_USER_TWEETS_HANDLE ?? 'X';
const userTweets = await runBird(
[
...baseArgs,
'--cookie-timeout',
cookieTimeoutArg,
'user-tweets',
testHandle,
'-n',
'50',
'--max-pages',
'3',
'--delay',
'0',
'--json',
],
[...baseArgs, 'user-tweets', testHandle, '-n', '50', '--max-pages', '3', '--delay', '0', '--json'],
{
timeoutMs: 45_000,
},
@@ -315,7 +276,7 @@ d('live CLI (Twitter/X)', () => {
});
it('query-ids returns JSON', async () => {
const queryIds = await runBird([...baseArgs, '--cookie-timeout', cookieTimeoutArg, 'query-ids', '--json'], {
const queryIds = await runBird([...baseArgs, 'query-ids', '--json'], {
timeoutMs: 60_000,
});
expect(queryIds.exitCode).toBe(0);
@@ -340,7 +301,7 @@ d('live CLI (Twitter/X)', () => {
// Skip unless explicitly provided - long-form tweets may be deleted/unavailable
return;
}
const read = await runBird([...baseArgs, '--cookie-timeout', cookieTimeoutArg, 'read', longformTweetId, '--json'], {
const read = await runBird([...baseArgs, 'read', longformTweetId, '--json'], {
timeoutMs: 45_000,
});
expect(read.exitCode).toBe(0);