Files
Neon-Desk/lib/server/taxonomy/parser-client.test.ts

353 lines
9.3 KiB
TypeScript

import { beforeEach, describe, expect, it, mock } from "bun:test";
import type {
TaxonomyHydrationInput,
TaxonomyHydrationResult,
} from "@/lib/server/taxonomy/types";
import type { IssuerOverlayDefinition } from "@/lib/server/db/schema";
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,
residual_primary_count: 0,
residual_disclosure_count: 0,
unsupported_concept_count: 0,
issuer_overlay_match_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",
};
}
function sampleOverlay(): IssuerOverlayDefinition {
return {
version: "fiscal-v1",
ticker: "AAPL",
pack: "core",
mappings: [
{
surface_key: "revenue",
statement: "income",
allowed_source_concepts: ["aapl:ServicesRevenue"],
allowed_authoritative_concepts: [],
},
],
};
}
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);
const [firstCall] = stdinWrite.mock.calls as unknown[][];
const firstWrite = firstCall?.[0];
expect(firstWrite).toBeDefined();
const payload = JSON.parse(
new TextDecoder().decode(firstWrite as unknown as Uint8Array),
) as { issuerOverlay?: IssuerOverlayDefinition | null };
expect(payload.issuerOverlay).toBeNull();
});
it("includes issuer overlay payload when provided", async () => {
const stdinWrite = mock(() => {});
await __parserClientInternals.hydrateFromSidecarImpl(
{
...sampleInput(),
issuerOverlay: sampleOverlay(),
},
{
existsSync: () => true,
spawn: mock(() => ({
stdin: {
write: stdinWrite,
end: () => {},
},
stdout: streamFromText(JSON.stringify(sampleHydrationResult())),
stderr: streamFromText(""),
exited: Promise.resolve(0),
kill: mock(() => {}),
})) as never,
setTimeout: passThroughTimeout,
clearTimeout,
},
);
const [firstCall] = stdinWrite.mock.calls as unknown[][];
const firstWrite = firstCall?.[0];
expect(firstWrite).toBeDefined();
const payload = JSON.parse(
new TextDecoder().decode(firstWrite as unknown as Uint8Array),
) as { issuerOverlay?: IssuerOverlayDefinition | null };
expect(payload.issuerOverlay).toEqual(sampleOverlay());
});
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);
});
});