fix: tighten news output and parsing (#39) (thanks @aavetis)

This commit is contained in:
Peter Steinberger
2026-01-12 05:37:44 +00:00
parent fb092f5cec
commit c96ce5238f
5 changed files with 146 additions and 25 deletions
+33
View File
@@ -2,6 +2,7 @@ import { Command } from 'commander';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { CliContext } from '../src/cli/shared.js';
import { registerNewsCommand } from '../src/commands/news.js';
import { TwitterClient } from '../src/lib/twitter-client.js';
describe('news command', () => {
let program: Command;
@@ -151,4 +152,36 @@ describe('news command', () => {
errorSpy.mockRestore();
}
});
it('limits related tweets to tweets-per-item in CLI output', async () => {
registerNewsCommand(program, mockContext as CliContext);
const getNewsSpy = vi.spyOn(TwitterClient.prototype, 'getNews').mockResolvedValue({
success: true,
items: [
{
id: 'item-1',
headline: 'News headline',
category: 'News',
tweets: [
{ id: 't1', text: 'first tweet', author: { username: 'a', name: 'A' } },
{ id: 't2', text: 'second tweet', author: { username: 'b', name: 'B' } },
{ id: 't3', text: 'third tweet', author: { username: 'c', name: 'C' } },
],
},
],
});
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
try {
await program.parseAsync(['node', 'bird', 'news', '--with-tweets', '--tweets-per-item', '2', '--count', '1']);
expect(getNewsSpy).toHaveBeenCalledWith(1, expect.objectContaining({ tweetsPerItem: 2, withTweets: true }));
const tweetLines = logSpy.mock.calls
.map((call) => String(call[0]))
.filter((line) => line.trimStart().startsWith('@'));
expect(tweetLines).toHaveLength(2);
} finally {
getNewsSpy.mockRestore();
logSpy.mockRestore();
}
});
});
+82 -4
View File
@@ -24,7 +24,7 @@ const makeResponse = (overrides: Partial<ResponseLike> = {}): ResponseLike => ({
});
// Helper to create GenericTimelineById response structure
const makeTimelineResponse = (items: any[]) => ({
const makeTimelineResponse = (items: unknown[]) => ({
data: {
timeline: {
timeline: {
@@ -50,6 +50,21 @@ const makeTimelineResponse = (items: any[]) => ({
},
});
const makeTimelineResponseWithInstruction = (type: string, entries: unknown[]) => ({
data: {
timeline: {
timeline: {
instructions: [
{
type,
entries,
},
],
},
},
},
});
describe('TwitterClient news API coverage', () => {
const originalFetch = global.fetch;
@@ -162,9 +177,7 @@ describe('TwitterClient news API coverage', () => {
});
it('returns error when no news items found', async () => {
const mockFetch = vi
.fn()
.mockResolvedValue(makeResponse({ json: async () => makeTimelineResponse([]) }));
const mockFetch = vi.fn().mockResolvedValue(makeResponse({ json: async () => makeTimelineResponse([]) }));
global.fetch = mockFetch as unknown as typeof fetch;
@@ -244,5 +257,70 @@ describe('TwitterClient news API coverage', () => {
expect(result.success).toBe(true);
expect(result.items?.length).toBe(2);
});
it('parses entries from non-TimelineAddEntries instructions', async () => {
const mockFetch = vi
.fn()
.mockResolvedValueOnce(
makeResponse({
json: async () =>
makeTimelineResponseWithInstruction('TimelineReplaceEntry', [
{
entryId: 'replace-entry-1',
content: {
itemContent: {
is_ai_trend: true,
name: 'Replacement headline',
},
},
},
]),
}),
)
.mockResolvedValue(makeResponse({ json: async () => makeTimelineResponse([]) }));
global.fetch = mockFetch as unknown as typeof fetch;
const client = new TwitterClient({ cookies: validCookies });
const result = await client.getNews(1);
expect(result.success).toBe(true);
expect(result.items?.[0].headline).toBe('Replacement headline');
});
it('assigns unique ids for module entries', async () => {
const mockFetch = vi
.fn()
.mockResolvedValueOnce(
makeResponse({
json: async () =>
makeTimelineResponseWithInstruction('TimelineAddEntries', [
{
entryId: 'module-entry',
content: {
items: [
{
item: { itemContent: { is_ai_trend: true, name: 'Headline A' } },
},
{
item: { itemContent: { is_ai_trend: true, name: 'Headline B' } },
},
],
},
},
]),
}),
)
.mockResolvedValue(makeResponse({ json: async () => makeTimelineResponse([]) }));
global.fetch = mockFetch as unknown as typeof fetch;
const client = new TwitterClient({ cookies: validCookies });
const result = await client.getNews(2);
expect(result.success).toBe(true);
const ids = result.items?.map((item) => item.id) ?? [];
expect(new Set(ids).size).toBe(ids.length);
});
});
});