alif-digest 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/publish.yml +33 -0
- package/.husky/pre-commit +1 -0
- package/.prettierrc +7 -0
- package/LICENSE +21 -0
- package/README.md +131 -0
- package/dist/cli/commands/init.d.ts +1 -0
- package/dist/cli/commands/init.js +88 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/run.d.ts +4 -0
- package/dist/cli/commands/run.js +46 -0
- package/dist/cli/commands/run.js.map +1 -0
- package/dist/cli/commands/schedule.d.ts +1 -0
- package/dist/cli/commands/schedule.js +94 -0
- package/dist/cli/commands/schedule.js.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +29 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/core/config-manager.d.ts +14 -0
- package/dist/core/config-manager.js +65 -0
- package/dist/core/config-manager.js.map +1 -0
- package/dist/core/config-schema.d.ts +40 -0
- package/dist/core/config-schema.js +24 -0
- package/dist/core/config-schema.js.map +1 -0
- package/dist/core/default-keywords.d.ts +1 -0
- package/dist/core/default-keywords.js +10 -0
- package/dist/core/default-keywords.js.map +1 -0
- package/dist/core/filters/deduplicator.d.ts +10 -0
- package/dist/core/filters/deduplicator.js +34 -0
- package/dist/core/filters/deduplicator.js.map +1 -0
- package/dist/core/filters/keywords.d.ts +6 -0
- package/dist/core/filters/keywords.js +17 -0
- package/dist/core/filters/keywords.js.map +1 -0
- package/dist/core/orchestrator.d.ts +6 -0
- package/dist/core/orchestrator.js +44 -0
- package/dist/core/orchestrator.js.map +1 -0
- package/dist/core/pipeline.d.ts +15 -0
- package/dist/core/pipeline.js +140 -0
- package/dist/core/pipeline.js.map +1 -0
- package/dist/core/scheduler.d.ts +9 -0
- package/dist/core/scheduler.js +64 -0
- package/dist/core/scheduler.js.map +1 -0
- package/dist/core/scraper-types.d.ts +27 -0
- package/dist/core/scraper-types.js +3 -0
- package/dist/core/scraper-types.js.map +1 -0
- package/dist/core/scrapers/api-scraper.d.ts +4 -0
- package/dist/core/scrapers/api-scraper.js +46 -0
- package/dist/core/scrapers/api-scraper.js.map +1 -0
- package/dist/core/scrapers/arxiv-scraper.d.ts +4 -0
- package/dist/core/scrapers/arxiv-scraper.js +34 -0
- package/dist/core/scrapers/arxiv-scraper.js.map +1 -0
- package/dist/core/scrapers/json-scraper.d.ts +4 -0
- package/dist/core/scrapers/json-scraper.js +56 -0
- package/dist/core/scrapers/json-scraper.js.map +1 -0
- package/dist/core/scrapers/rss-scraper.d.ts +6 -0
- package/dist/core/scrapers/rss-scraper.js +32 -0
- package/dist/core/scrapers/rss-scraper.js.map +1 -0
- package/dist/core/scrapers/scrape-scraper.d.ts +4 -0
- package/dist/core/scrapers/scrape-scraper.js +49 -0
- package/dist/core/scrapers/scrape-scraper.js.map +1 -0
- package/dist/db/article-store.d.ts +22 -0
- package/dist/db/article-store.js +43 -0
- package/dist/db/article-store.js.map +1 -0
- package/dist/db/connection.d.ts +2 -0
- package/dist/db/connection.js +15 -0
- package/dist/db/connection.js.map +1 -0
- package/dist/db/migrate.d.ts +2 -0
- package/dist/db/migrate.js +60 -0
- package/dist/db/migrate.js.map +1 -0
- package/dist/db/schedule-store.d.ts +17 -0
- package/dist/db/schedule-store.js +23 -0
- package/dist/db/schedule-store.js.map +1 -0
- package/dist/db/source-health-store.d.ts +16 -0
- package/dist/db/source-health-store.js +31 -0
- package/dist/db/source-health-store.js.map +1 -0
- package/dist/providers/delivery/index.d.ts +18 -0
- package/dist/providers/delivery/index.js +2 -0
- package/dist/providers/delivery/index.js.map +1 -0
- package/dist/providers/delivery/slack.d.ts +6 -0
- package/dist/providers/delivery/slack.js +52 -0
- package/dist/providers/delivery/slack.js.map +1 -0
- package/dist/providers/delivery/webhook.d.ts +6 -0
- package/dist/providers/delivery/webhook.js +16 -0
- package/dist/providers/delivery/webhook.js.map +1 -0
- package/dist/providers/factory.d.ts +7 -0
- package/dist/providers/factory.js +33 -0
- package/dist/providers/factory.js.map +1 -0
- package/dist/providers/llm/anthropic.d.ts +12 -0
- package/dist/providers/llm/anthropic.js +43 -0
- package/dist/providers/llm/anthropic.js.map +1 -0
- package/dist/providers/llm/index.d.ts +10 -0
- package/dist/providers/llm/index.js +2 -0
- package/dist/providers/llm/index.js.map +1 -0
- package/dist/providers/llm/ollama.d.ts +12 -0
- package/dist/providers/llm/ollama.js +42 -0
- package/dist/providers/llm/ollama.js.map +1 -0
- package/dist/providers/llm/openrouter.d.ts +13 -0
- package/dist/providers/llm/openrouter.js +53 -0
- package/dist/providers/llm/openrouter.js.map +1 -0
- package/dist/providers/llm/utils.d.ts +6 -0
- package/dist/providers/llm/utils.js +45 -0
- package/dist/providers/llm/utils.js.map +1 -0
- package/dist/resources/default-feeds.json +650 -0
- package/dist/resources/index.d.ts +2 -0
- package/dist/resources/index.js +3 -0
- package/dist/resources/index.js.map +1 -0
- package/eslint.config.mjs +29 -0
- package/package.json +66 -0
- package/src/cli/commands/init.ts +94 -0
- package/src/cli/commands/run.ts +52 -0
- package/src/cli/commands/schedule.ts +99 -0
- package/src/cli/index.ts +34 -0
- package/src/core/config-manager.ts +72 -0
- package/src/core/config-schema.ts +31 -0
- package/src/core/default-keywords.ts +9 -0
- package/src/core/filters/deduplicator.ts +39 -0
- package/src/core/filters/keywords.ts +18 -0
- package/src/core/orchestrator.ts +47 -0
- package/src/core/pipeline.ts +171 -0
- package/src/core/scheduler.ts +74 -0
- package/src/core/scraper-types.ts +30 -0
- package/src/core/scrapers/api-scraper.ts +45 -0
- package/src/core/scrapers/arxiv-scraper.ts +35 -0
- package/src/core/scrapers/json-scraper.ts +54 -0
- package/src/core/scrapers/rss-scraper.ts +34 -0
- package/src/core/scrapers/scrape-scraper.ts +50 -0
- package/src/db/article-store.ts +75 -0
- package/src/db/connection.ts +17 -0
- package/src/db/migrate.ts +68 -0
- package/src/db/schedule-store.ts +41 -0
- package/src/db/source-health-store.ts +42 -0
- package/src/providers/delivery/index.ts +19 -0
- package/src/providers/delivery/slack.ts +55 -0
- package/src/providers/delivery/webhook.ts +16 -0
- package/src/providers/factory.ts +37 -0
- package/src/providers/llm/anthropic.ts +48 -0
- package/src/providers/llm/index.ts +8 -0
- package/src/providers/llm/ollama.ts +44 -0
- package/src/providers/llm/openrouter.ts +56 -0
- package/src/providers/llm/utils.ts +54 -0
- package/src/resources/default-feeds.json +650 -0
- package/src/resources/index.ts +3 -0
- package/tests/config-manager.test.ts +70 -0
- package/tests/db-integration.test.ts +72 -0
- package/tests/filters.test.ts +53 -0
- package/tests/llm-provider.test.ts +115 -0
- package/tsconfig.json +18 -0
- package/vitest.config.ts +13 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import { ConfigManager } from '../src/core/config-manager.js';
|
|
6
|
+
import { Config } from '../src/core/config-schema.js';
|
|
7
|
+
|
|
8
|
+
vi.mock('fs');
|
|
9
|
+
vi.mock('os');
|
|
10
|
+
|
|
11
|
+
describe('ConfigManager', () => {
|
|
12
|
+
const mockConfigDir = '/mock/home/.config/alif';
|
|
13
|
+
const mockConfigFile = path.join(mockConfigDir, 'config.json');
|
|
14
|
+
const mockConfig: Config = {
|
|
15
|
+
llm: {
|
|
16
|
+
provider: 'ollama',
|
|
17
|
+
model: 'llama3',
|
|
18
|
+
baseUrl: 'http://localhost:11434',
|
|
19
|
+
},
|
|
20
|
+
delivery: [{ type: 'slack', webhookUrl: 'https://hooks.slack.com/services/test' }],
|
|
21
|
+
preferences: {
|
|
22
|
+
signalThreshold: 60,
|
|
23
|
+
maxItemsPerCategory: 5,
|
|
24
|
+
sourceCooldownMinutes: 5,
|
|
25
|
+
customKeywords: {},
|
|
26
|
+
},
|
|
27
|
+
dbPath: path.join(mockConfigDir, 'alif.db'),
|
|
28
|
+
feedsPath: path.join(mockConfigDir, 'feeds.json'),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
vi.clearAllMocks();
|
|
33
|
+
vi.mocked(os.homedir).mockReturnValue('/mock/home');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should return correct config paths', () => {
|
|
37
|
+
const manager = ConfigManager.getInstance();
|
|
38
|
+
expect(manager.getConfigDir()).toBe(mockConfigDir);
|
|
39
|
+
expect(manager.getConfigFile()).toBe(mockConfigFile);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should throw if loading non-existent config', () => {
|
|
43
|
+
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
44
|
+
const manager = ConfigManager.getInstance();
|
|
45
|
+
expect(() => manager.load()).toThrow(/Configuration file not found/);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should load and parse valid config', () => {
|
|
49
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
50
|
+
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(mockConfig));
|
|
51
|
+
|
|
52
|
+
const manager = ConfigManager.getInstance();
|
|
53
|
+
const loaded = manager.load();
|
|
54
|
+
expect(loaded).toEqual(mockConfig);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should save config and create directory if needed', () => {
|
|
58
|
+
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
59
|
+
|
|
60
|
+
const manager = ConfigManager.getInstance();
|
|
61
|
+
manager.save(mockConfig);
|
|
62
|
+
|
|
63
|
+
expect(fs.mkdirSync).toHaveBeenCalledWith(mockConfigDir, { recursive: true });
|
|
64
|
+
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
|
65
|
+
mockConfigFile,
|
|
66
|
+
expect.stringContaining('"provider": "ollama"'),
|
|
67
|
+
'utf-8',
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { createDatabase } from '../src/db/connection.js';
|
|
3
|
+
import { runMigrations } from '../src/db/migrate.js';
|
|
4
|
+
import { ArticleStore } from '../src/db/article-store.js';
|
|
5
|
+
import { SourceHealthStore } from '../src/db/source-health-store.js';
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
|
|
9
|
+
describe('Database Integration', () => {
|
|
10
|
+
const testDbPath = path.join('/tmp', `alif-test-${Date.now()}.db`);
|
|
11
|
+
let db: any;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
db = createDatabase(testDbPath);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
db.close();
|
|
19
|
+
if (fs.existsSync(testDbPath)) {
|
|
20
|
+
fs.unlinkSync(testDbPath);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should run migrations and create tables', () => {
|
|
25
|
+
runMigrations(db);
|
|
26
|
+
|
|
27
|
+
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all();
|
|
28
|
+
const tableNames = tables.map((t: any) => t.name);
|
|
29
|
+
|
|
30
|
+
expect(tableNames).toContain('articles');
|
|
31
|
+
expect(tableNames).toContain('source_health');
|
|
32
|
+
expect(tableNames).toContain('schedules');
|
|
33
|
+
expect(tableNames).toContain('migrations');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should upsert articles and retrieve latest timestamp', () => {
|
|
37
|
+
runMigrations(db);
|
|
38
|
+
const store = new ArticleStore(db);
|
|
39
|
+
|
|
40
|
+
const article = {
|
|
41
|
+
id: 'test-1',
|
|
42
|
+
title: 'Test Article',
|
|
43
|
+
url: 'https://example.com',
|
|
44
|
+
source: 'test',
|
|
45
|
+
digest_date: '2024-03-05',
|
|
46
|
+
published_at: '2024-03-05T10:00:00Z',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
store.upsert(article);
|
|
50
|
+
expect(store.getLatestTimestamp()).toBe('2024-03-05T10:00:00Z');
|
|
51
|
+
|
|
52
|
+
// Update
|
|
53
|
+
store.upsert({ ...article, title: 'Updated Title' });
|
|
54
|
+
const saved = db.prepare('SELECT title FROM articles WHERE id = ?').get('test-1');
|
|
55
|
+
expect(saved.title).toBe('Updated Title');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should record source health', () => {
|
|
59
|
+
runMigrations(db);
|
|
60
|
+
const healthStore = new SourceHealthStore(db);
|
|
61
|
+
|
|
62
|
+
healthStore.record({
|
|
63
|
+
source: 'hn',
|
|
64
|
+
status: 'ok',
|
|
65
|
+
items_found: 10,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const latest = healthStore.getLatest('hn') as any;
|
|
69
|
+
expect(latest.status).toBe('ok');
|
|
70
|
+
expect(latest.items_found).toBe(10);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { KeywordScorer } from '../src/core/filters/keywords.js';
|
|
3
|
+
import { Deduplicator } from '../src/core/filters/deduplicator.js';
|
|
4
|
+
|
|
5
|
+
describe('Filters', () => {
|
|
6
|
+
describe('KeywordScorer', () => {
|
|
7
|
+
it('should calculate score based on keywords', () => {
|
|
8
|
+
const scorer = new KeywordScorer({ 'gpt-4': 50, ai: 10 });
|
|
9
|
+
const article = {
|
|
10
|
+
id: '1',
|
|
11
|
+
title: 'New GPT-4 details',
|
|
12
|
+
content: 'AI is changing the world',
|
|
13
|
+
source: 'test',
|
|
14
|
+
url: 'http://example.com',
|
|
15
|
+
};
|
|
16
|
+
expect(scorer.score(article)).toBe(60);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should prioritize overridden keywords and include new ones', () => {
|
|
20
|
+
// Simulate merging mechanics from pipeline
|
|
21
|
+
const BASE = { 'gpt-4': 50, ai: 10 };
|
|
22
|
+
const CUSTOM = { 'gpt-4': 100, custom_tool: 30, ai: 0 };
|
|
23
|
+
const merged = { ...BASE, ...CUSTOM };
|
|
24
|
+
|
|
25
|
+
const scorer = new KeywordScorer(merged);
|
|
26
|
+
const article = {
|
|
27
|
+
id: '1',
|
|
28
|
+
title: 'New GPT-4 details using custom_tool',
|
|
29
|
+
content: 'AI is changing the world',
|
|
30
|
+
source: 'test',
|
|
31
|
+
url: 'http://example.com',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// GPT-4 (100) + custom_tool (30) + AI (0) = 130
|
|
35
|
+
expect(scorer.score(article)).toBe(130);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('Deduplicator', () => {
|
|
40
|
+
it('should remove duplicate URLs', () => {
|
|
41
|
+
const deduplicator = new Deduplicator();
|
|
42
|
+
const articles = [
|
|
43
|
+
{ id: '1', title: 'A', url: 'http://example.com/', source: 's1' },
|
|
44
|
+
{ id: '2', title: 'A proxy', url: 'http://example.com', source: 's2' },
|
|
45
|
+
{ id: '3', title: 'B', url: 'http://other.com', source: 's1' },
|
|
46
|
+
];
|
|
47
|
+
const result = deduplicator.process(articles);
|
|
48
|
+
expect(result).toHaveLength(2);
|
|
49
|
+
expect(result[0].id).toBe('1');
|
|
50
|
+
expect(result[1].id).toBe('3');
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
import { OllamaProvider } from '../src/providers/llm/ollama.js';
|
|
4
|
+
|
|
5
|
+
vi.mock('axios');
|
|
6
|
+
|
|
7
|
+
describe('OllamaProvider', () => {
|
|
8
|
+
const options = { baseUrl: 'http://localhost:11434', model: 'test-model' };
|
|
9
|
+
const provider = new OllamaProvider(options);
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
vi.clearAllMocks();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should return empty array if no articles provided', async () => {
|
|
16
|
+
const result = await provider.analyze([]);
|
|
17
|
+
expect(result).toEqual([]);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should parse valid JSON response from Ollama', async () => {
|
|
21
|
+
const mockResponse = {
|
|
22
|
+
data: {
|
|
23
|
+
response: JSON.stringify([
|
|
24
|
+
{ summary: 'AI reaches breakthrough.', category: 'Research' },
|
|
25
|
+
{ summary: 'New model released.', category: 'Model Release' },
|
|
26
|
+
]),
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
vi.mocked(axios.post).mockResolvedValue(mockResponse);
|
|
30
|
+
|
|
31
|
+
const articles = [{ title: 'Article 1' }, { title: 'Article 2' }];
|
|
32
|
+
const result = await provider.analyze(articles);
|
|
33
|
+
|
|
34
|
+
expect(result).toHaveLength(2);
|
|
35
|
+
expect(result[0]).toEqual({ summary: 'AI reaches breakthrough.', category: 'Research' });
|
|
36
|
+
expect(result[1]).toEqual({ summary: 'New model released.', category: 'Model Release' });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should handle single object response by wrapping in array', async () => {
|
|
40
|
+
const mockResponse = {
|
|
41
|
+
data: {
|
|
42
|
+
response: JSON.stringify({ summary: 'Single breakthrough.', category: 'Research' }),
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
vi.mocked(axios.post).mockResolvedValue(mockResponse);
|
|
46
|
+
|
|
47
|
+
const result = await provider.analyze([{ title: 'Article 1' }]);
|
|
48
|
+
expect(result).toHaveLength(1);
|
|
49
|
+
expect(result[0].summary).toBe('Single breakthrough.');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should return default objects on parse error', async () => {
|
|
53
|
+
const mockResponse = {
|
|
54
|
+
data: {
|
|
55
|
+
response: 'Invalid JSON',
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
vi.mocked(axios.post).mockResolvedValue(mockResponse);
|
|
59
|
+
|
|
60
|
+
const articles = [{ title: 'Article 1' }];
|
|
61
|
+
const result = await provider.analyze(articles);
|
|
62
|
+
|
|
63
|
+
expect(result).toHaveLength(1);
|
|
64
|
+
expect(result[0]).toEqual({ summary: null, category: 'Uncategorized' });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should handle empty response from Ollama', async () => {
|
|
68
|
+
const mockResponse = {
|
|
69
|
+
data: {
|
|
70
|
+
response: '',
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
vi.mocked(axios.post).mockResolvedValue(mockResponse);
|
|
74
|
+
|
|
75
|
+
const result = await provider.analyze([{ title: 'Article 1' }]);
|
|
76
|
+
expect(result[0]).toEqual({ summary: null, category: 'Uncategorized' });
|
|
77
|
+
});
|
|
78
|
+
it('should handle markdown-wrapped JSON response', async () => {
|
|
79
|
+
const mockResponse = {
|
|
80
|
+
data: {
|
|
81
|
+
response:
|
|
82
|
+
'Here is the analysis:\n```json\n[{"summary": "Markdown works.", "category": "Test"}]\n```',
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
vi.mocked(axios.post).mockResolvedValue(mockResponse);
|
|
86
|
+
|
|
87
|
+
const result = await provider.analyze([{ title: 'Article 1' }]);
|
|
88
|
+
expect(result[0].summary).toBe('Markdown works.');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should handle text-wrapped JSON response', async () => {
|
|
92
|
+
const mockResponse = {
|
|
93
|
+
data: {
|
|
94
|
+
response:
|
|
95
|
+
'The result is: [{"summary": "Text wrapped.", "category": "Test"}] end of message.',
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
vi.mocked(axios.post).mockResolvedValue(mockResponse);
|
|
99
|
+
|
|
100
|
+
const result = await provider.analyze([{ title: 'Article 1' }]);
|
|
101
|
+
expect(result[0].summary).toBe('Text wrapped.');
|
|
102
|
+
});
|
|
103
|
+
it('should fallback to thinking field if response is empty', async () => {
|
|
104
|
+
const mockResponse = {
|
|
105
|
+
data: {
|
|
106
|
+
response: '',
|
|
107
|
+
thinking: JSON.stringify([{ summary: 'Thoughtful insight.', category: 'Research' }]),
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
vi.mocked(axios.post).mockResolvedValue(mockResponse);
|
|
111
|
+
|
|
112
|
+
const result = await provider.analyze([{ title: 'Article 1' }]);
|
|
113
|
+
expect(result[0].summary).toBe('Thoughtful insight.');
|
|
114
|
+
});
|
|
115
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"sourceMap": true,
|
|
14
|
+
"resolveJsonModule": true
|
|
15
|
+
},
|
|
16
|
+
"include": ["src/**/*"],
|
|
17
|
+
"exclude": ["node_modules", "dist", "tests", "**/*.test.ts"]
|
|
18
|
+
}
|
package/vitest.config.ts
ADDED