Integrate crabrl parser into taxonomy hydration
This commit is contained in:
@@ -1,11 +1,5 @@
|
||||
import {
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
mock
|
||||
} from 'bun:test';
|
||||
import type { Filing, Holding, Task } from '@/lib/types';
|
||||
import { beforeEach, describe, expect, it, mock } from "bun:test";
|
||||
import type { Filing, Holding, Task } from "@/lib/types";
|
||||
|
||||
const stageUpdates: Array<{
|
||||
taskId: string;
|
||||
@@ -14,290 +8,373 @@ const stageUpdates: Array<{
|
||||
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
|
||||
})
|
||||
};
|
||||
}
|
||||
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'
|
||||
};
|
||||
});
|
||||
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'
|
||||
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;
|
||||
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: {
|
||||
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,
|
||||
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
|
||||
};
|
||||
});
|
||||
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',
|
||||
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
|
||||
debt: 98_000_000_000,
|
||||
},
|
||||
analysis: null,
|
||||
created_at: '2026-01-30T00:00:00.000Z',
|
||||
updated_at: '2026-01-30T00:00:00.000Z'
|
||||
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 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
|
||||
updated: 0,
|
||||
}));
|
||||
|
||||
const mockDeleteCompanyFinancialBundlesForTicker = mock(async () => {});
|
||||
const mockGetFilingTaxonomySnapshotByFilingId = mock(async () => null);
|
||||
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',
|
||||
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'
|
||||
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',
|
||||
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'
|
||||
}
|
||||
created_at: "2026-03-09T00:00:00.000Z",
|
||||
updated_at: "2026-03-09T00:00:00.000Z",
|
||||
},
|
||||
]);
|
||||
const mockListUserHoldings = mock(async () => await mockListHoldingsForPriceRefresh());
|
||||
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 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
|
||||
text: "Revenue accelerated in services and margins improved.",
|
||||
source: "primary_document" as const,
|
||||
}));
|
||||
const mockFetchRecentFilings = mock(async () => ([
|
||||
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-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'
|
||||
}
|
||||
]));
|
||||
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: []
|
||||
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: [],
|
||||
},
|
||||
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/ai", () => ({
|
||||
runAiAnalysis: mockRunAiAnalysis,
|
||||
}));
|
||||
mock.module('@/lib/server/portfolio', () => ({
|
||||
buildPortfolioSummary: mockBuildPortfolioSummary
|
||||
mock.module("@/lib/server/portfolio", () => ({
|
||||
buildPortfolioSummary: mockBuildPortfolioSummary,
|
||||
}));
|
||||
mock.module('@/lib/server/prices', () => ({
|
||||
getQuote: mockGetQuote
|
||||
mock.module("@/lib/server/prices", () => ({
|
||||
getQuote: mockGetQuote,
|
||||
}));
|
||||
mock.module('@/lib/server/search', () => ({
|
||||
indexSearchDocuments: mockIndexSearchDocuments
|
||||
mock.module("@/lib/server/search", () => ({
|
||||
indexSearchDocuments: mockIndexSearchDocuments,
|
||||
}));
|
||||
mock.module('@/lib/server/repos/filings', () => ({
|
||||
mock.module("@/lib/server/repos/filings", () => ({
|
||||
getFilingByAccession: mockGetFilingByAccession,
|
||||
listFilingsRecords: mockListFilingsRecords,
|
||||
saveFilingAnalysis: mockSaveFilingAnalysis,
|
||||
updateFilingMetricsById: mockUpdateFilingMetricsById,
|
||||
upsertFilingsRecords: mockUpsertFilingsRecords
|
||||
upsertFilingsRecords: mockUpsertFilingsRecords,
|
||||
}));
|
||||
mock.module('@/lib/server/repos/company-financial-bundles', () => ({
|
||||
deleteCompanyFinancialBundlesForTicker: mockDeleteCompanyFinancialBundlesForTicker
|
||||
mock.module("@/lib/server/repos/company-financial-bundles", () => ({
|
||||
deleteCompanyFinancialBundlesForTicker:
|
||||
mockDeleteCompanyFinancialBundlesForTicker,
|
||||
}));
|
||||
mock.module('@/lib/server/repos/filing-taxonomy', () => ({
|
||||
mock.module("@/lib/server/repos/filing-taxonomy", () => ({
|
||||
getFilingTaxonomySnapshotByFilingId: mockGetFilingTaxonomySnapshotByFilingId,
|
||||
upsertFilingTaxonomySnapshot: mockUpsertFilingTaxonomySnapshot
|
||||
upsertFilingTaxonomySnapshot: mockUpsertFilingTaxonomySnapshot,
|
||||
}));
|
||||
mock.module('@/lib/server/repos/holdings', () => ({
|
||||
mock.module("@/lib/server/repos/holdings", () => ({
|
||||
applyRefreshedPrices: mockApplyRefreshedPrices,
|
||||
listHoldingsForPriceRefresh: mockListHoldingsForPriceRefresh,
|
||||
listUserHoldings: mockListUserHoldings
|
||||
listUserHoldings: mockListUserHoldings,
|
||||
}));
|
||||
mock.module('@/lib/server/repos/insights', () => ({
|
||||
createPortfolioInsight: mockCreatePortfolioInsight
|
||||
mock.module("@/lib/server/repos/insights", () => ({
|
||||
createPortfolioInsight: mockCreatePortfolioInsight,
|
||||
}));
|
||||
mock.module('@/lib/server/repos/tasks', () => ({
|
||||
updateTaskStage: mockUpdateTaskStage
|
||||
mock.module("@/lib/server/repos/tasks", () => ({
|
||||
updateTaskStage: mockUpdateTaskStage,
|
||||
}));
|
||||
mock.module('@/lib/server/sec', () => ({
|
||||
mock.module("@/lib/server/sec", () => ({
|
||||
fetchPrimaryFilingText: mockFetchPrimaryFilingText,
|
||||
fetchRecentFilings: mockFetchRecentFilings
|
||||
fetchRecentFilings: mockFetchRecentFilings,
|
||||
}));
|
||||
mock.module('@/lib/server/tasks', () => ({
|
||||
enqueueTask: mockEnqueueTask
|
||||
mock.module("@/lib/server/tasks", () => ({
|
||||
enqueueTask: mockEnqueueTask,
|
||||
}));
|
||||
mock.module('@/lib/server/taxonomy/engine', () => ({
|
||||
hydrateFilingTaxonomySnapshot: mockHydrateFilingTaxonomySnapshot
|
||||
mock.module("@/lib/server/taxonomy/engine", () => ({
|
||||
hydrateFilingTaxonomySnapshot: mockHydrateFilingTaxonomySnapshot,
|
||||
}));
|
||||
mock.module("@/lib/server/taxonomy/pdf-validation", () => ({
|
||||
validateMetricsWithPdfLlm: mockValidateMetricsWithPdfLlm,
|
||||
}));
|
||||
|
||||
const { runTaskProcessor } = await import('./task-processors');
|
||||
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',
|
||||
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,
|
||||
@@ -308,24 +385,24 @@ function taskFactory(overrides: Partial<Task> = {}): Task {
|
||||
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',
|
||||
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',
|
||||
title: "Task",
|
||||
statusLine: "Running",
|
||||
detailLine: null,
|
||||
tone: 'info',
|
||||
tone: "info",
|
||||
progress: null,
|
||||
stats: [],
|
||||
actions: []
|
||||
actions: [],
|
||||
},
|
||||
...overrides
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('task processor outcomes', () => {
|
||||
describe("task processor outcomes", () => {
|
||||
beforeEach(() => {
|
||||
stageUpdates.length = 0;
|
||||
mockRunAiAnalysis.mockClear();
|
||||
@@ -335,78 +412,108 @@ describe('task processor outcomes', () => {
|
||||
mockCreatePortfolioInsight.mockClear();
|
||||
mockUpdateTaskStage.mockClear();
|
||||
mockEnqueueTask.mockClear();
|
||||
mockValidateMetricsWithPdfLlm.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
|
||||
}
|
||||
}));
|
||||
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.completionDetail).toContain("Synced 2 filings for AAPL");
|
||||
expect(outcome.result.fetched).toBe(2);
|
||||
expect(outcome.result.searchTaskId).toBe('search-task-1');
|
||||
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(
|
||||
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'
|
||||
}));
|
||||
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.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.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'
|
||||
}
|
||||
}));
|
||||
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(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']
|
||||
}
|
||||
}));
|
||||
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.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);
|
||||
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'
|
||||
}));
|
||||
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.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'
|
||||
total_value: "100000",
|
||||
total_gain_loss: "1000",
|
||||
total_cost_basis: "99000",
|
||||
avg_return_pct: "0.01",
|
||||
});
|
||||
expect(mockCreatePortfolioInsight).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user