Files
Neon-Desk/lib/server/task-processors.outcomes.test.ts

414 lines
12 KiB
TypeScript

import {
beforeEach,
describe,
expect,
it,
mock
} from 'bun:test';
import type { Filing, Holding, Task } from '@/lib/types';
const stageUpdates: Array<{
taskId: string;
stage: string;
detail: string | null;
context: Record<string, unknown> | null;
}> = [];
const mockRunAiAnalysis = mock(async (_prompt: string, _instruction: string, options?: { workload?: string }) => {
if (options?.workload === 'extraction') {
return {
provider: 'zhipu',
model: 'glm-extract',
text: JSON.stringify({
summary: 'Revenue growth remained resilient despite FX pressure.',
keyPoints: ['Revenue up year-over-year'],
redFlags: ['Debt service burden is rising'],
followUpQuestions: ['Is margin guidance sustainable?'],
portfolioSignals: ['Monitor leverage trend'],
segmentSpecificData: ['Services segment outgrew hardware segment.'],
geographicRevenueBreakdown: ['EMEA revenue grew faster than Americas.'],
companySpecificData: ['Same-store sales increased 4.2%.'],
secApiCrossChecks: ['Revenue from SEC API aligns with filing narrative.'],
confidence: 0.72
})
};
}
return {
provider: 'zhipu',
model: options?.workload === 'report' ? 'glm-report' : 'glm-generic',
text: 'Structured output'
};
});
const mockBuildPortfolioSummary = mock((_holdings: Holding[]) => ({
positions: 14,
total_value: '100000',
total_gain_loss: '1000',
total_cost_basis: '99000',
avg_return_pct: '0.01'
}));
const mockGetQuote = mock(async (ticker: string) => {
return ticker === 'MSFT' ? 410 : 205;
});
const mockIndexSearchDocuments = mock(async (input: {
onStage?: (stage: 'collect' | 'fetch' | 'chunk' | 'embed' | 'persist', detail: string, context?: Record<string, unknown> | null) => Promise<void> | void;
}) => {
await input.onStage?.('collect', 'Collected 12 source records for search indexing', {
counters: {
sourcesCollected: 12,
deleted: 3
}
});
await input.onStage?.('fetch', 'Preparing filing_brief 0000320193-26-000001', {
progress: {
current: 1,
total: 12,
unit: 'sources'
},
subject: {
ticker: 'AAPL',
accessionNumber: '0000320193-26-000001'
}
});
await input.onStage?.('embed', 'Embedding 248 chunks for 0000320193-26-000001', {
progress: {
current: 1,
total: 12,
unit: 'sources'
},
counters: {
chunksEmbedded: 248
}
});
return {
sourcesCollected: 12,
indexed: 12,
skipped: 1,
deleted: 3,
chunksEmbedded: 248
};
});
const sampleFiling = (): Filing => ({
id: 1,
ticker: 'AAPL',
filing_type: '10-Q',
filing_date: '2026-01-30',
accession_number: '0000320193-26-000001',
cik: '0000320193',
company_name: 'Apple Inc.',
filing_url: 'https://www.sec.gov/Archives/edgar/data/320193/000032019326000001/a10q.htm',
submission_url: 'https://data.sec.gov/submissions/CIK0000320193.json',
primary_document: 'a10q.htm',
metrics: {
revenue: 120_000_000_000,
netIncome: 25_000_000_000,
totalAssets: 410_000_000_000,
cash: 70_000_000_000,
debt: 98_000_000_000
},
analysis: null,
created_at: '2026-01-30T00:00:00.000Z',
updated_at: '2026-01-30T00:00:00.000Z'
});
const mockGetFilingByAccession = mock(async () => sampleFiling());
const mockListFilingsRecords = mock(async () => [sampleFiling(), {
...sampleFiling(),
id: 2,
accession_number: '0000320193-26-000002',
filing_date: '2026-02-28'
}]);
const mockSaveFilingAnalysis = mock(async () => {});
const mockUpdateFilingMetricsById = mock(async () => {});
const mockUpsertFilingsRecords = mock(async () => ({
inserted: 2,
updated: 0
}));
const mockDeleteCompanyFinancialBundlesForTicker = mock(async () => {});
const mockGetFilingTaxonomySnapshotByFilingId = mock(async () => null);
const mockUpsertFilingTaxonomySnapshot = mock(async () => {});
const mockApplyRefreshedPrices = mock(async () => 24);
const mockListHoldingsForPriceRefresh = mock(async () => [
{
id: 1,
user_id: 'user-1',
ticker: 'AAPL',
company_name: 'Apple Inc.',
shares: '10',
avg_cost: '150',
current_price: '200',
market_value: '2000',
gain_loss: '500',
gain_loss_pct: '0.33',
last_price_at: null,
created_at: '2026-03-09T00:00:00.000Z',
updated_at: '2026-03-09T00:00:00.000Z'
},
{
id: 2,
user_id: 'user-1',
ticker: 'MSFT',
company_name: 'Microsoft Corporation',
shares: '4',
avg_cost: '300',
current_price: '400',
market_value: '1600',
gain_loss: '400',
gain_loss_pct: '0.25',
last_price_at: null,
created_at: '2026-03-09T00:00:00.000Z',
updated_at: '2026-03-09T00:00:00.000Z'
}
]);
const mockListUserHoldings = mock(async () => await mockListHoldingsForPriceRefresh());
const mockCreatePortfolioInsight = mock(async () => {});
const mockUpdateTaskStage = mock(async (taskId: string, stage: string, detail: string | null, context?: Record<string, unknown> | null) => {
stageUpdates.push({
taskId,
stage,
detail,
context: context ?? null
});
});
const mockFetchPrimaryFilingText = mock(async () => ({
text: 'Revenue accelerated in services and margins improved.',
source: 'primary_document' as const
}));
const mockFetchRecentFilings = mock(async () => ([
{
ticker: 'AAPL',
filingType: '10-Q',
filingDate: '2026-01-30',
accessionNumber: '0000320193-26-000001',
cik: '0000320193',
companyName: 'Apple Inc.',
filingUrl: 'https://www.sec.gov/Archives/edgar/data/320193/000032019326000001/a10q.htm',
submissionUrl: 'https://data.sec.gov/submissions/CIK0000320193.json',
primaryDocument: 'a10q.htm'
},
{
ticker: 'AAPL',
filingType: '10-K',
filingDate: '2025-10-30',
accessionNumber: '0000320193-25-000001',
cik: '0000320193',
companyName: 'Apple Inc.',
filingUrl: 'https://www.sec.gov/Archives/edgar/data/320193/000032019325000001/a10k.htm',
submissionUrl: 'https://data.sec.gov/submissions/CIK0000320193.json',
primaryDocument: 'a10k.htm'
}
]));
const mockEnqueueTask = mock(async () => ({
id: 'search-task-1'
}));
const mockHydrateFilingTaxonomySnapshot = mock(async (input: { filingId: number }) => ({
filing_id: input.filingId,
ticker: 'AAPL',
filing_date: '2026-01-30',
filing_type: '10-Q',
parse_status: 'ready',
parse_error: null,
source: 'xbrl_instance',
periods: [],
statement_rows: {
income: [],
balance: [],
cash_flow: [],
equity: [],
comprehensive_income: []
},
derived_metrics: {
revenue: 120_000_000_000
},
validation_result: {
status: 'matched',
checks: [],
validatedAt: '2026-03-09T00:00:00.000Z'
},
facts_count: 1,
concepts_count: 1,
dimensions_count: 0,
assets: [],
concepts: [],
facts: [],
metric_validations: []
}));
mock.module('@/lib/server/ai', () => ({
runAiAnalysis: mockRunAiAnalysis
}));
mock.module('@/lib/server/portfolio', () => ({
buildPortfolioSummary: mockBuildPortfolioSummary
}));
mock.module('@/lib/server/prices', () => ({
getQuote: mockGetQuote
}));
mock.module('@/lib/server/search', () => ({
indexSearchDocuments: mockIndexSearchDocuments
}));
mock.module('@/lib/server/repos/filings', () => ({
getFilingByAccession: mockGetFilingByAccession,
listFilingsRecords: mockListFilingsRecords,
saveFilingAnalysis: mockSaveFilingAnalysis,
updateFilingMetricsById: mockUpdateFilingMetricsById,
upsertFilingsRecords: mockUpsertFilingsRecords
}));
mock.module('@/lib/server/repos/company-financial-bundles', () => ({
deleteCompanyFinancialBundlesForTicker: mockDeleteCompanyFinancialBundlesForTicker
}));
mock.module('@/lib/server/repos/filing-taxonomy', () => ({
getFilingTaxonomySnapshotByFilingId: mockGetFilingTaxonomySnapshotByFilingId,
upsertFilingTaxonomySnapshot: mockUpsertFilingTaxonomySnapshot
}));
mock.module('@/lib/server/repos/holdings', () => ({
applyRefreshedPrices: mockApplyRefreshedPrices,
listHoldingsForPriceRefresh: mockListHoldingsForPriceRefresh,
listUserHoldings: mockListUserHoldings
}));
mock.module('@/lib/server/repos/insights', () => ({
createPortfolioInsight: mockCreatePortfolioInsight
}));
mock.module('@/lib/server/repos/tasks', () => ({
updateTaskStage: mockUpdateTaskStage
}));
mock.module('@/lib/server/sec', () => ({
fetchPrimaryFilingText: mockFetchPrimaryFilingText,
fetchRecentFilings: mockFetchRecentFilings
}));
mock.module('@/lib/server/tasks', () => ({
enqueueTask: mockEnqueueTask
}));
mock.module('@/lib/server/taxonomy/engine', () => ({
hydrateFilingTaxonomySnapshot: mockHydrateFilingTaxonomySnapshot
}));
const { runTaskProcessor } = await import('./task-processors');
function taskFactory(overrides: Partial<Task> = {}): Task {
return {
id: 'task-1',
user_id: 'user-1',
task_type: 'sync_filings',
status: 'running',
stage: 'running',
stage_detail: 'Running',
stage_context: null,
resource_key: null,
notification_read_at: null,
notification_silenced_at: null,
priority: 50,
payload: {},
result: null,
error: null,
attempts: 1,
max_attempts: 3,
workflow_run_id: 'run-1',
created_at: '2026-03-09T00:00:00.000Z',
updated_at: '2026-03-09T00:00:00.000Z',
finished_at: null,
notification: {
title: 'Task',
statusLine: 'Running',
detailLine: null,
tone: 'info',
progress: null,
stats: [],
actions: []
},
...overrides
};
}
describe('task processor outcomes', () => {
beforeEach(() => {
stageUpdates.length = 0;
mockRunAiAnalysis.mockClear();
mockGetQuote.mockClear();
mockIndexSearchDocuments.mockClear();
mockSaveFilingAnalysis.mockClear();
mockCreatePortfolioInsight.mockClear();
mockUpdateTaskStage.mockClear();
mockEnqueueTask.mockClear();
});
it('returns sync filing completion detail and progress context', async () => {
const outcome = await runTaskProcessor(taskFactory({
task_type: 'sync_filings',
payload: {
ticker: 'AAPL',
limit: 2
}
}));
expect(outcome.completionDetail).toContain('Synced 2 filings for AAPL');
expect(outcome.result.fetched).toBe(2);
expect(outcome.result.searchTaskId).toBe('search-task-1');
expect(outcome.completionContext?.counters?.hydrated).toBe(2);
expect(stageUpdates.some((entry) => entry.stage === 'sync.extract_taxonomy' && entry.context?.subject)).toBe(true);
});
it('returns refresh price completion detail with live quote progress', async () => {
const outcome = await runTaskProcessor(taskFactory({
task_type: 'refresh_prices'
}));
expect(outcome.completionDetail).toBe('Refreshed prices for 2 tickers across 2 holdings.');
expect(outcome.result.updatedCount).toBe(24);
expect(stageUpdates.filter((entry) => entry.stage === 'refresh.fetch_quotes')).toHaveLength(3);
expect(stageUpdates.at(-1)?.context?.counters).toBeDefined();
});
it('returns analyze filing completion detail with report metadata', async () => {
const outcome = await runTaskProcessor(taskFactory({
task_type: 'analyze_filing',
payload: {
accessionNumber: '0000320193-26-000001'
}
}));
expect(outcome.completionDetail).toBe('Analysis report generated for AAPL 10-Q 0000320193-26-000001.');
expect(outcome.result.ticker).toBe('AAPL');
expect(outcome.result.filingType).toBe('10-Q');
expect(outcome.result.model).toBe('glm-report');
expect(mockSaveFilingAnalysis).toHaveBeenCalled();
});
it('returns index search completion detail and counters', async () => {
const outcome = await runTaskProcessor(taskFactory({
task_type: 'index_search',
payload: {
ticker: 'AAPL',
sourceKinds: ['filing_brief']
}
}));
expect(outcome.completionDetail).toBe('Indexed 12 sources, embedded 248 chunks, skipped 1, deleted 3 stale documents.');
expect(outcome.result.indexed).toBe(12);
expect(outcome.completionContext?.counters?.chunksEmbedded).toBe(248);
expect(stageUpdates.some((entry) => entry.stage === 'search.embed')).toBe(true);
});
it('returns portfolio insight completion detail and summary payload', async () => {
const outcome = await runTaskProcessor(taskFactory({
task_type: 'portfolio_insights'
}));
expect(outcome.completionDetail).toBe('Generated portfolio insight for 14 holdings.');
expect(outcome.result.provider).toBe('zhipu');
expect(outcome.result.summary).toEqual({
positions: 14,
total_value: '100000',
total_gain_loss: '1000',
total_cost_basis: '99000',
avg_return_pct: '0.01'
});
expect(mockCreatePortfolioInsight).toHaveBeenCalled();
});
});