pulse/modules/formatter/formatter.test.ts
Edo Limburg 78a2b27f6d feat: implement orchestrator module with feed source management
Add FeedOrchestrator that coordinates fetch→parse→dedup→store pipeline:
- FeedSource type for managing RSS/Atom feed configurations
- Feed source CRUD operations in IStorage interface
- Database schema migration for feed_sources table
- Exponential backoff retry with configurable delays
- Per-feed poll intervals with health tracking
- Concurrency-limited parallel feed processing
- ProcessResult and FeedHealth interfaces for status monitoring

Files added:
- orchestrator/orchestrator.ts - main orchestrator class
- orchestrator/scheduler.ts - backoff calculation utilities
- orchestrator/index.ts - module exports
- orchestrator/orchestrator.test.ts - comprehensive test suite

Files modified:
- interfaces/feed.types.ts - add FeedSource type
- interfaces/storage.interface.ts - extend with feed source methods
- infrastructure/db/database.ts - add FeedSourceTable interface
- infrastructure/db/schema.ts - add feed_sources table migration
- modules/storage/storage.ts - implement feed source CRUD
- modules/storage/storage.test.ts - add feed source tests
2026-05-05 22:17:16 +02:00

208 lines
6.8 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { Formatter } from './formatter.js';
import { TerminalFormatter } from './terminal.formatter.js';
import { JsonFormatter } from './json.formatter.js';
import type { FeedItem } from '../../interfaces/feed.types.js';
describe('Formatter', () => {
const formatter = new Formatter();
const createMockItem = (id: string, overrides: Partial<FeedItem> = {}): FeedItem => ({
id,
source: 'https://example.com/feed',
title: 'Test Article',
url: 'https://example.com/article/1',
publishedAt: new Date('2026-05-05T10:30:00Z'),
content: undefined,
summary: undefined,
...overrides
});
describe('format with terminal format', () => {
it('should format empty array', async () => {
const result = await formatter.format([], 'terminal');
expect(result).toContain('No items to display');
});
it('should format single item', async () => {
const item = createMockItem('item-1', {
title: 'My Article',
source: 'https://myblog.com/feed'
});
const result = await formatter.format([item], 'terminal');
expect(result).toContain('1.');
expect(result).toContain('My Article');
expect(result).toContain('https://myblog.com/feed');
expect(result).toContain('https://example.com/article/1');
});
it('should format multiple items', async () => {
const items = [
createMockItem('item-1', { title: 'Article 1' }),
createMockItem('item-2', { title: 'Article 2' })
];
const result = await formatter.format(items, 'terminal');
expect(result).toContain('Found 2 items');
expect(result).toContain('1.');
expect(result).toContain('2.');
expect(result).toContain('Article 1');
expect(result).toContain('Article 2');
});
it('should include summary when present', async () => {
const item = createMockItem('item-1', {
summary: 'This is a test summary that might be truncated'
});
const result = await formatter.format([item], 'terminal');
expect(result).toContain('This is a test summary');
});
});
describe('format with json format', () => {
it('should format empty array', async () => {
const result = await formatter.format([], 'json');
expect(JSON.parse(result)).toEqual([]);
});
it('should format single item', async () => {
const item = createMockItem('item-1', {
title: 'My Article'
});
const result = await formatter.format([item], 'json');
const parsed = JSON.parse(result);
expect(parsed).toHaveLength(1);
expect(parsed[0].id).toBe('item-1');
expect(parsed[0].title).toBe('My Article');
expect(parsed[0].publishedAt).toBe('2026-05-05T10:30:00.000Z');
});
it('should format multiple items', async () => {
const items = [
createMockItem('item-1', { title: 'Article 1' }),
createMockItem('item-2', { title: 'Article 2' })
];
const result = await formatter.format(items, 'json');
const parsed = JSON.parse(result);
expect(parsed).toHaveLength(2);
expect(parsed[0].title).toBe('Article 1');
expect(parsed[1].title).toBe('Article 2');
});
it('should include optional content when present', async () => {
const item = createMockItem('item-1', {
content: 'Full article content here'
});
const result = await formatter.format([item], 'json');
const parsed = JSON.parse(result);
expect(parsed[0].content).toBe('Full article content here');
});
it('should include optional summary when present', async () => {
const item = createMockItem('item-1', {
summary: 'Article summary'
});
const result = await formatter.format([item], 'json');
const parsed = JSON.parse(result);
expect(parsed[0].summary).toBe('Article summary');
});
it('should not include undefined fields', async () => {
const item = createMockItem('item-1');
const result = await formatter.format([item], 'json');
const parsed = JSON.parse(result);
expect(parsed[0]).not.toHaveProperty('content');
expect(parsed[0]).not.toHaveProperty('summary');
});
it('should use ISO 8601 date format', async () => {
const item = createMockItem('item-1', {
publishedAt: new Date('2026-12-25T15:30:45.123Z')
});
const result = await formatter.format([item], 'json');
const parsed = JSON.parse(result);
expect(parsed[0].publishedAt).toBe('2026-12-25T15:30:45.123Z');
});
});
describe('error handling', () => {
it('should throw FormatterError for HTML format', async () => {
await expect(formatter.format([], 'html')).rejects.toMatchObject({
code: 'UNKNOWN',
message: 'HTML format not implemented'
});
});
it('should throw FormatterError for unknown format', async () => {
await expect(formatter.format([], 'unknown-format' as any)).rejects.toMatchObject({
code: 'SERIALIZE_ERROR'
});
});
});
});
describe('TerminalFormatter', () => {
const terminalFormatter = new TerminalFormatter();
it('should truncate long summary text', () => {
const longSummary = 'a'.repeat(100);
const item: FeedItem = {
id: 'item-1',
source: 'https://example.com',
title: 'Test',
url: 'https://example.com/article',
publishedAt: new Date(),
summary: longSummary
};
const result = terminalFormatter.format([item]);
expect(result).toContain('...');
expect(result).not.toContain(longSummary);
});
it('should format date in readable format', () => {
const item: FeedItem = {
id: 'item-1',
source: 'https://example.com',
title: 'Test',
url: 'https://example.com/article',
publishedAt: new Date('2026-05-05T10:30:00Z')
};
const result = terminalFormatter.format([item]);
expect(result).toContain('May');
expect(result).toContain('2026');
});
});
describe('JsonFormatter', () => {
const jsonFormatter = new JsonFormatter();
it('should produce valid JSON', () => {
const items: FeedItem[] = [
{
id: 'test-1',
source: 'https://example.com',
title: 'Test Article',
url: 'https://example.com/1',
publishedAt: new Date()
}
];
const result = jsonFormatter.format(items);
expect(() => JSON.parse(result)).not.toThrow();
});
it('should convert Date to ISO string', () => {
const date = new Date('2026-01-15T08:00:00Z');
const items: FeedItem[] = [
{
id: 'test-1',
source: 'https://example.com',
title: 'Test',
url: 'https://example.com/1',
publishedAt: date
}
];
const result = jsonFormatter.format(items);
const parsed = JSON.parse(result);
expect(parsed[0].publishedAt).toBe('2026-01-15T08:00:00.000Z');
});
});