diff --git a/tests/live/live.test.ts b/tests/live/live.test.ts index 59b759f..ca46a6f 100644 --- a/tests/live/live.test.ts +++ b/tests/live/live.test.ts @@ -284,6 +284,35 @@ d('live CLI (Twitter/X)', () => { expect(tweets[0].author?.username?.toLowerCase()).toBe(testHandle.toLowerCase()); }); + it('user-tweets paged JSON returns { tweets, nextCursor }', async () => { + 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', + ], + { + timeoutMs: 45_000, + }, + ); + expect(userTweets.exitCode).toBe(0); + const payload = parseJson<{ tweets?: Array<{ id?: string }>; nextCursor?: string | null }>(userTweets.stdout); + expect(Array.isArray(payload.tweets)).toBe(true); + expect(payload.tweets?.length).toBeGreaterThan(0); + expect(payload.tweets?.[0]?.id?.length).toBeGreaterThan(0); + }); + it('query-ids returns JSON', async () => { const queryIds = await runBird([...baseArgs, '--cookie-timeout', cookieTimeoutArg, 'query-ids', '--json'], { timeoutMs: 60_000, diff --git a/tests/twitter-client.user-tweets.test.ts b/tests/twitter-client.user-tweets.test.ts index 6935e7d..a31ef15 100644 --- a/tests/twitter-client.user-tweets.test.ts +++ b/tests/twitter-client.user-tweets.test.ts @@ -394,6 +394,61 @@ describe('TwitterClient getUserTweetsPaged', () => { global.fetch = mockFetch as unknown as typeof fetch; }); + it('requests only the remaining count', async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => + makeTimelineResponse( + [makeTweetResult('1', 'Tweet 1'), makeTweetResult('2', 'Tweet 2'), makeTweetResult('3', 'Tweet 3')], + 'cursor-2', + ), + }); + + const client = new TwitterClient({ cookies: validCookies }); + const result = await client.getUserTweetsPaged('12345', 5, { pageDelayMs: 0 }); + + expect(result.success).toBe(true); + + const [url] = mockFetch.mock.calls[0]; + const variablesRaw = new URL(url).searchParams.get('variables'); + expect(variablesRaw).not.toBeNull(); + const variables = JSON.parse(String(variablesRaw)) as { count?: number }; + expect(variables.count).toBe(5); + }); + + it('enforces a hard 10 page cap by default', async () => { + for (let page = 1; page <= 10; page += 1) { + const startId = (page - 1) * 20 + 1; + const tweets = Array.from({ length: 20 }, (_v, index) => { + const id = String(startId + index); + return makeTweetResult(id, `Tweet ${id}`); + }); + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => makeTimelineResponse(tweets, `cursor-${page + 1}`), + }); + } + + const client = new TwitterClient({ cookies: validCookies }); + const result = await client.getUserTweetsPaged('12345', 9999, { pageDelayMs: 0 }); + + expect(result.success).toBe(true); + expect(mockFetch).toHaveBeenCalledTimes(10); + expect(result.tweets?.length).toBe(200); + expect(result.nextCursor).toBe('cursor-11'); + }); + + it('returns error for invalid limit', async () => { + const client = new TwitterClient({ cookies: validCookies }); + const result = await client.getUserTweetsPaged('12345', 0); + + expect(result.success).toBe(false); + expect(result.error).toContain('Invalid limit'); + expect(mockFetch).not.toHaveBeenCalled(); + }); + it('fetches multiple pages with cursor', async () => { mockFetch .mockResolvedValueOnce({