Files
Neon-Desk/lib/server/taxonomy/parser-client.test.ts
francy51 14a7773504 Add consolidated disclosure statement type
Create unified disclosure statement to organize footnote disclosures
separate from primary financial statements. Disclosures are now grouped
by type (tax, debt, securities, derivatives, leases, intangibles, ma,
revenue, cash_flow) in a dedicated statement type for cleaner UI
presentation.
2026-03-16 18:54:23 -04:00

291 lines
7.4 KiB
TypeScript

import { beforeEach, describe, expect, it, mock } from "bun:test";
import type {
TaxonomyHydrationInput,
TaxonomyHydrationResult,
} from "@/lib/server/taxonomy/types";
import { __parserClientInternals } from "@/lib/server/taxonomy/parser-client";
function streamFromText(text: string) {
const encoded = new TextEncoder().encode(text);
return new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(encoded);
controller.close();
},
});
}
function sampleHydrationResult(): TaxonomyHydrationResult {
return {
filing_id: 1,
ticker: "AAPL",
filing_date: "2026-01-30",
filing_type: "10-Q",
parse_status: "ready",
parse_error: null,
source: "xbrl_instance",
parser_engine: "fiscal-xbrl",
parser_version: "0.1.0",
taxonomy_regime: "us-gaap",
fiscal_pack: "core",
periods: [],
faithful_rows: {
income: [],
balance: [],
cash_flow: [],
disclosure: [],
equity: [],
comprehensive_income: [],
},
statement_rows: {
income: [],
balance: [],
cash_flow: [],
disclosure: [],
equity: [],
comprehensive_income: [],
},
surface_rows: {
income: [],
balance: [],
cash_flow: [],
disclosure: [],
equity: [],
comprehensive_income: [],
},
detail_rows: {
income: {},
balance: {},
cash_flow: {},
disclosure: {},
equity: {},
comprehensive_income: {},
},
kpi_rows: [],
computed_definitions: [],
contexts: [],
derived_metrics: null,
validation_result: null,
facts_count: 0,
concepts_count: 0,
dimensions_count: 0,
assets: [],
concepts: [],
facts: [],
metric_validations: [],
normalization_summary: {
surface_row_count: 0,
detail_row_count: 0,
kpi_row_count: 0,
unmapped_row_count: 0,
material_unmapped_row_count: 0,
warnings: [],
},
xbrl_validation: {
status: "passed",
},
};
}
function sampleInput(): TaxonomyHydrationInput {
return {
filingId: 1,
ticker: "AAPL",
cik: "0000320193",
accessionNumber: "0000320193-26-000001",
filingDate: "2026-01-30",
filingType: "10-Q",
filingUrl:
"https://www.sec.gov/Archives/edgar/data/320193/000032019326000001/",
primaryDocument: "a10q.htm",
};
}
const passThroughTimeout = ((handler: TimerHandler, timeout?: number) =>
globalThis.setTimeout(
handler,
timeout,
)) as unknown as typeof globalThis.setTimeout;
const immediateTimeout = ((handler: TimerHandler) => {
if (typeof handler === "function") {
handler();
}
return 1 as unknown as ReturnType<typeof globalThis.setTimeout>;
}) as unknown as typeof globalThis.setTimeout;
describe("parser client", () => {
beforeEach(() => {
delete process.env.FISCAL_XBRL_BIN;
delete process.env.XBRL_ENGINE_TIMEOUT_MS;
});
it("throws when the sidecar binary cannot be resolved", () => {
expect(() =>
__parserClientInternals.resolveFiscalXbrlBinary({
existsSync: () => false,
}),
).toThrow(/Rust XBRL sidecar binary is required/);
});
it("returns parsed sidecar JSON on success", async () => {
const stdinWrite = mock(() => {});
const stdinEnd = mock(() => {});
const result = await __parserClientInternals.hydrateFromSidecarImpl(
sampleInput(),
{
existsSync: () => true,
spawn: mock(() => ({
stdin: {
write: stdinWrite,
end: stdinEnd,
},
stdout: streamFromText(JSON.stringify(sampleHydrationResult())),
stderr: streamFromText(""),
exited: Promise.resolve(0),
kill: mock(() => {}),
})) as never,
setTimeout: passThroughTimeout,
clearTimeout,
},
);
expect(result.parser_engine).toBe("fiscal-xbrl");
expect(stdinWrite).toHaveBeenCalledTimes(1);
expect(stdinEnd).toHaveBeenCalledTimes(1);
});
it("throws when the sidecar exits non-zero", async () => {
await expect(
__parserClientInternals.hydrateFromSidecarImpl(sampleInput(), {
existsSync: () => true,
spawn: mock(() => ({
stdin: {
write: () => {},
end: () => {},
},
stdout: streamFromText(""),
stderr: streamFromText("fatal parse error"),
exited: Promise.resolve(3),
kill: mock(() => {}),
})) as never,
setTimeout: passThroughTimeout,
clearTimeout,
}),
).rejects.toThrow(/exit code 3/);
});
it("throws on invalid JSON stdout", async () => {
await expect(
__parserClientInternals.hydrateFromSidecarImpl(sampleInput(), {
existsSync: () => true,
spawn: mock(() => ({
stdin: {
write: () => {},
end: () => {},
},
stdout: streamFromText("{not json"),
stderr: streamFromText(""),
exited: Promise.resolve(0),
kill: mock(() => {}),
})) as never,
setTimeout: passThroughTimeout,
clearTimeout,
}),
).rejects.toThrow();
});
it("kills the sidecar when the timeout fires", async () => {
const kill = mock(() => {});
await expect(
__parserClientInternals.hydrateFromSidecarImpl(sampleInput(), {
existsSync: () => true,
spawn: mock(() => ({
stdin: {
write: () => {},
end: () => {},
},
stdout: streamFromText(""),
stderr: streamFromText("killed"),
exited: Promise.resolve(137),
kill,
})) as never,
setTimeout: immediateTimeout,
clearTimeout: () => {},
}),
).rejects.toThrow(/exit code 137/);
expect(kill).toHaveBeenCalledTimes(1);
});
it("retries retryable sidecar failures but not invalid requests", async () => {
let attempts = 0;
const spawn = mock(() => {
attempts += 1;
const exitCode = attempts < 3 ? 1 : 0;
const stdout =
exitCode === 0 ? JSON.stringify(sampleHydrationResult()) : "";
const stderr = exitCode === 0 ? "" : "process killed";
return {
stdin: {
write: () => {},
end: () => {},
},
stdout: streamFromText(stdout),
stderr: streamFromText(stderr),
exited: Promise.resolve(exitCode),
kill: mock(() => {}),
};
});
const result =
await __parserClientInternals.hydrateFilingTaxonomySnapshotFromSidecarWithDeps(
sampleInput(),
{
existsSync: () => true,
spawn: spawn as never,
setTimeout: passThroughTimeout,
clearTimeout,
},
);
expect(result.parser_version).toBe("0.1.0");
expect(attempts).toBe(3);
attempts = 0;
const invalidRequestSpawn = mock(() => {
attempts += 1;
return {
stdin: {
write: () => {},
end: () => {},
},
stdout: streamFromText(""),
stderr: streamFromText("invalid request: bad command"),
exited: Promise.resolve(6),
kill: mock(() => {}),
};
});
await expect(
__parserClientInternals.hydrateFilingTaxonomySnapshotFromSidecarWithDeps(
sampleInput(),
{
existsSync: () => true,
spawn: invalidRequestSpawn as never,
setTimeout: passThroughTimeout,
clearTimeout,
},
),
).rejects.toThrow(/invalid request/);
expect(attempts).toBe(1);
});
});