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

547 lines
15 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 mockNormalizeFilingTaxonomySnapshotPayload = mock((input: unknown) => {
return input as Record<string, unknown>;
});
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<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 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> = {}): 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();
});
});