Files
bird/tests/commands.search.test.ts
yuta 81921226f0
CI / test (push) Successful in 10m17s
feat: support ranked tweet search
2026-07-13 09:18:42 +09:00

142 lines
5.3 KiB
TypeScript

import { Command } from 'commander';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { CliContext } from '../src/cli/shared.js';
import { registerSearchCommands } from '../src/commands/search.js';
import { TwitterClient } from '../src/lib/twitter-client.js';
describe('search command', () => {
let program: Command;
let mockContext: Partial<CliContext>;
beforeEach(() => {
program = new Command();
mockContext = {
resolveTimeoutFromOptions: () => undefined,
resolveQuoteDepthFromOptions: () => undefined,
resolveRelayBaseUrlFromOptions: () => 'https://relay.example',
resolveProfileNameFromOptions: () => undefined,
p: () => '',
printTweetsResult: vi.fn(),
};
});
it('requires --all or --cursor when --max-pages is provided', async () => {
registerSearchCommands(program, mockContext as CliContext);
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', 'search', 'cats', '--max-pages', '2'])).rejects.toThrow(
'exit 1',
);
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('--max-pages requires --all or --cursor'));
} finally {
exitSpy.mockRestore();
errorSpy.mockRestore();
}
});
it('requires positive --count when not paging', async () => {
registerSearchCommands(program, mockContext as CliContext);
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', 'search', 'cats', '--count', '0'])).rejects.toThrow('exit 1');
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('Invalid --count. Expected a positive integer.'));
} finally {
exitSpy.mockRestore();
errorSpy.mockRestore();
}
});
it('requires positive --max-pages when paging', async () => {
registerSearchCommands(program, mockContext as CliContext);
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', 'search', 'cats', '--all', '--max-pages', '0'])).rejects.toThrow(
'exit 1',
);
expect(errorSpy).toHaveBeenCalledWith(
expect.stringContaining('Invalid --max-pages. Expected a positive integer.'),
);
} finally {
exitSpy.mockRestore();
errorSpy.mockRestore();
}
});
it('uses paged search when --all is set', async () => {
registerSearchCommands(program, mockContext as CliContext);
const getAllSpy = vi
.spyOn(TwitterClient.prototype, 'getAllSearchResults')
.mockResolvedValue({ success: true, tweets: [] });
const searchSpy = vi.spyOn(TwitterClient.prototype, 'search').mockResolvedValue({ success: true, tweets: [] });
try {
await program.parseAsync(['node', 'bird', 'search', 'cats', '--all', '--json', '--product', 'Top']);
expect(getAllSpy).toHaveBeenCalledWith('cats', expect.objectContaining({ includeRaw: false, product: 'Top' }));
expect(searchSpy).not.toHaveBeenCalled();
expect(mockContext.printTweetsResult).toHaveBeenCalledWith(expect.objectContaining({ tweets: [] }), {
json: true,
usePagination: true,
emptyMessage: 'No tweets found.',
});
} finally {
getAllSpy.mockRestore();
searchSpy.mockRestore();
}
});
it('passes --product to non-paged search', async () => {
registerSearchCommands(program, mockContext as CliContext);
const searchSpy = vi.spyOn(TwitterClient.prototype, 'search').mockResolvedValue({ success: true, tweets: [] });
try {
await program.parseAsync(['node', 'bird', 'search', 'cats', '--product', 'Top']);
expect(searchSpy).toHaveBeenCalledWith('cats', 10, {
includeRaw: false,
product: 'Top',
});
} finally {
searchSpy.mockRestore();
}
});
it('rejects unsupported search products', async () => {
program.exitOverride();
registerSearchCommands(program, mockContext as CliContext);
await expect(program.parseAsync(['node', 'bird', 'search', 'cats', '--product', 'Media'])).rejects.toMatchObject({
code: 'commander.invalidArgument',
});
});
it('uses cursor pagination when --cursor is set', async () => {
registerSearchCommands(program, mockContext as CliContext);
const getAllSpy = vi
.spyOn(TwitterClient.prototype, 'getAllSearchResults')
.mockResolvedValue({ success: true, tweets: [] });
try {
await program.parseAsync(['node', 'bird', 'search', 'cats', '--cursor', 'cursor-1']);
expect(getAllSpy).toHaveBeenCalledWith('cats', expect.objectContaining({ cursor: 'cursor-1' }));
expect(mockContext.printTweetsResult).toHaveBeenCalledWith(expect.objectContaining({ tweets: [] }), {
json: false,
usePagination: true,
emptyMessage: 'No tweets found.',
});
} finally {
getAllSpy.mockRestore();
}
});
});