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 | 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 | null) => Promise | 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 | 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 { 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(); }); });