Files
bird/tests/commands.read.test.ts
yuta 3f370cc4b6
CI / test (push) Successful in 10m2s
feat: support relay profile selection
2026-06-24 18:30:24 +09:00

184 lines
6.5 KiB
TypeScript

import { Command } from 'commander';
import { describe, expect, it, vi } from 'vitest';
import type { CliContext } from '../src/cli/shared.js';
import { registerReadCommands } from '../src/commands/read.js';
import { TwitterClient } from '../src/lib/twitter-client.js';
describe('replies command', () => {
const createMockContext = () =>
({
resolveTimeoutFromOptions: () => undefined,
resolveQuoteDepthFromOptions: () => 1,
resolveProfileNameFromOptions: () => undefined,
extractTweetId: (input: string) => input,
resolveRelayBaseUrlFromOptions: () => 'https://relay.example',
p: () => '',
printTweets: () => undefined,
printTweetsResult: () => undefined,
}) as unknown as CliContext;
it('uses pagination when --max-pages is provided', async () => {
const program = new Command();
registerReadCommands(program, createMockContext());
const pagedSpy = vi
.spyOn(TwitterClient.prototype, 'getRepliesPaged')
.mockResolvedValue({ success: true, tweets: [], nextCursor: undefined });
const unpagedSpy = vi.spyOn(TwitterClient.prototype, 'getReplies').mockResolvedValue({ success: true, tweets: [] });
try {
await program.parseAsync(['node', 'bird', 'replies', '123', '--max-pages', '2', '--json']);
expect(pagedSpy).toHaveBeenCalledTimes(1);
expect(unpagedSpy).toHaveBeenCalledTimes(0);
} finally {
pagedSpy.mockRestore();
unpagedSpy.mockRestore();
}
});
it('validates --max-pages is a positive integer', async () => {
const program = new Command();
registerReadCommands(program, createMockContext());
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(((code?: number) => {
throw new Error(`exit ${code}`);
}) as never);
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
try {
await expect(
program.parseAsync(['node', 'bird', 'replies', '123', '--all', '--max-pages', '-1']),
).rejects.toThrow('exit 1');
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('Invalid --max-pages'));
} finally {
exitSpy.mockRestore();
errorSpy.mockRestore();
}
});
it('validates --delay is a non-negative integer', async () => {
const program = new Command();
registerReadCommands(program, createMockContext());
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(((code?: number) => {
throw new Error(`exit ${code}`);
}) as never);
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
try {
await expect(program.parseAsync(['node', 'bird', 'replies', '123', '--all', '--delay', '-100'])).rejects.toThrow(
'exit 1',
);
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('Invalid --delay'));
} finally {
exitSpy.mockRestore();
errorSpy.mockRestore();
}
});
it('passes profileName to TwitterClient', async () => {
const program = new Command();
registerReadCommands(program, {
...createMockContext(),
resolveProfileNameFromOptions: () => 'work',
});
const getTweetSpy = vi.spyOn(TwitterClient.prototype, 'getTweet').mockImplementation(async function () {
const headers = (this as unknown as { getHeaders: () => Record<string, string> }).getHeaders();
expect(headers).toEqual({ 'x-profile-name': 'work' });
return {
success: true,
tweet: {
id: '123',
text: 'hello',
author: { username: 'tester', name: 'Tester' },
createdAt: '2024-01-01T00:00:00Z',
url: 'https://x.com/tester/status/123',
},
};
});
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
try {
await program.parseAsync(['node', 'bird', 'read', '123', '--json']);
expect(getTweetSpy).toHaveBeenCalledTimes(1);
} finally {
getTweetSpy.mockRestore();
logSpy.mockRestore();
}
});
});
describe('thread command', () => {
const createMockContext = () =>
({
resolveTimeoutFromOptions: () => undefined,
resolveQuoteDepthFromOptions: () => 1,
resolveProfileNameFromOptions: () => undefined,
extractTweetId: (input: string) => input,
resolveRelayBaseUrlFromOptions: () => 'https://relay.example',
p: () => '',
printTweets: () => undefined,
printTweetsResult: () => undefined,
}) as unknown as CliContext;
it('uses pagination when --max-pages is provided', async () => {
const program = new Command();
registerReadCommands(program, createMockContext());
const pagedSpy = vi
.spyOn(TwitterClient.prototype, 'getThreadPaged')
.mockResolvedValue({ success: true, tweets: [], nextCursor: undefined });
const unpagedSpy = vi.spyOn(TwitterClient.prototype, 'getThread').mockResolvedValue({ success: true, tweets: [] });
try {
await program.parseAsync(['node', 'bird', 'thread', '123', '--max-pages', '2', '--json']);
expect(pagedSpy).toHaveBeenCalledTimes(1);
expect(unpagedSpy).toHaveBeenCalledTimes(0);
} finally {
pagedSpy.mockRestore();
unpagedSpy.mockRestore();
}
});
it('validates --max-pages is a positive integer', async () => {
const program = new Command();
registerReadCommands(program, createMockContext());
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(((code?: number) => {
throw new Error(`exit ${code}`);
}) as never);
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
try {
await expect(program.parseAsync(['node', 'bird', 'thread', '123', '--all', '--max-pages', '0'])).rejects.toThrow(
'exit 1',
);
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('Invalid --max-pages'));
} finally {
exitSpy.mockRestore();
errorSpy.mockRestore();
}
});
it('validates --delay is a non-negative integer', async () => {
const program = new Command();
registerReadCommands(program, createMockContext());
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(((code?: number) => {
throw new Error(`exit ${code}`);
}) as never);
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
try {
await expect(program.parseAsync(['node', 'bird', 'thread', '123', '--all', '--delay', 'abc'])).rejects.toThrow(
'exit 1',
);
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('Invalid --delay'));
} finally {
exitSpy.mockRestore();
errorSpy.mockRestore();
}
});
});