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 mockNormalizeFilingTaxonomySnapshotPayload = mock((input: unknown) => { return input as Record; }); const mockUpsertFilingTaxonomySnapshot = mock(async () => {}); const mockValidateMetricsWithPdfLlm = mock(async () => ({ validation_result: { status: "matched" as const, checks: [], validatedAt: "2026-03-09T00:00:00.000Z", }, metric_validations: [], })); 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 mockGenerateIssuerOverlayForTicker = mock(async () => ({ published: false, activeRevisionId: null, sampledSnapshotIds: [1], })); const mockRecordIssuerOverlayBuildFailure = mock(async () => {}); const mockGetActiveIssuerOverlayDefinition = mock(async () => null); const mockGetIssuerOverlay = mock(async () => null); 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: [], }, faithful_rows: { income: [], balance: [], cash_flow: [], equity: [], comprehensive_income: [], }, surface_rows: { income: [], balance: [], cash_flow: [], equity: [], comprehensive_income: [], }, detail_rows: { income: {}, balance: {}, cash_flow: {}, equity: {}, comprehensive_income: {}, }, kpi_rows: [], computed_definitions: [], 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: [], xbrl_validation: { status: "passed", }, }), ); 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, normalizeFilingTaxonomySnapshotPayload: mockNormalizeFilingTaxonomySnapshotPayload, 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/issuer-overlays", () => ({ generateIssuerOverlayForTicker: mockGenerateIssuerOverlayForTicker, recordIssuerOverlayBuildFailure: mockRecordIssuerOverlayBuildFailure, })); mock.module("@/lib/server/repos/issuer-overlays", () => ({ getActiveIssuerOverlayDefinition: mockGetActiveIssuerOverlayDefinition, getIssuerOverlay: mockGetIssuerOverlay, })); mock.module("@/lib/server/taxonomy/engine", () => ({ hydrateFilingTaxonomySnapshot: mockHydrateFilingTaxonomySnapshot, })); mock.module("@/lib/server/taxonomy/pdf-validation", () => ({ validateMetricsWithPdfLlm: mockValidateMetricsWithPdfLlm, })); 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(); mockValidateMetricsWithPdfLlm.mockClear(); mockGenerateIssuerOverlayForTicker.mockClear(); mockRecordIssuerOverlayBuildFailure.mockClear(); mockGetActiveIssuerOverlayDefinition.mockClear(); mockGetIssuerOverlay.mockClear(); mockNormalizeFilingTaxonomySnapshotPayload.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); expect(mockValidateMetricsWithPdfLlm).toHaveBeenCalled(); expect(mockUpsertFilingTaxonomySnapshot).toHaveBeenCalled(); }); 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/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(); }); });