Files
0pt/tests/p0/document-intelligence-adapter.test.ts
T
adminandClaude Sonnet 5 c89731115c feat: add 0pt.query_source, 0pt.document_ocr, 0pt.write_external_record capabilities
Three new, tenant-agnostic P0 capabilities:

- 0pt.query_source: bounded, timeout/row-capped ad-hoc SQL query execution
  against registered database sources, via a Fabric/OneLake adapter
  (Azure AD credential-based mssql connection), with AUD0 audit events
  and an app/api/0pt/sources/[sourceId]/query endpoint.

- 0pt.document_ocr (document capture + extraction): mailbox-based document
  intake via Microsoft Graph, OCR/field extraction via Azure Document
  Intelligence, with capture/extraction API endpoints and services that
  never fabricate extracted values.

- 0pt.write_external_record (external writeback): a generic writeback-adapter
  contract plus a concrete Odoo bills adapter that matches partners via
  Fabric, backing an external-writeback-service for pushing records to
  downstream systems of record.

Known interim gaps:
- src/adapters/local-blob-store.ts is an interim ST0RE substitute pending
  the real ST0RE artifact-store integration.
- mssql is used by the Fabric/OneLake adapter but deliberately not yet
  added to package.json (pending `pnpm add mssql` once the concurrent
  session's package.json/dependency changes settle).

A separate, concurrently-active session has unrelated in-flight changes
in this same working tree (data-platform catalog/lineage/feature-store,
dataset quality, lakehouse/Iceberg docs, deploy scaffolding, package.json/
pnpm-lock.yaml updates, etc.) — none of that is touched or included here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-08 19:32:43 +02:00

137 lines
6.1 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
import { DocumentIntelligenceAdapter } from "@/src/adapters/document-intelligence-adapter";
function jsonResponse(body: unknown, opts: { status?: number; headers?: Record<string, string> } = {}) {
return {
ok: (opts.status ?? 200) < 400,
status: opts.status ?? 200,
headers: { get: (k: string) => (opts.headers ?? {})[k] ?? null },
json: async () => body,
text: async () => JSON.stringify(body),
} as unknown as Response;
}
describe("DocumentIntelligenceAdapter", () => {
beforeEach(() => {
vi.restoreAllMocks();
});
it("isConfigured requires endpoint + apiKey", () => {
expect(DocumentIntelligenceAdapter.isConfigured({ endpoint: "https://x", apiKey: "k" })).toBe(true);
expect(DocumentIntelligenceAdapter.isConfigured({ endpoint: "https://x" })).toBe(false);
expect(DocumentIntelligenceAdapter.isConfigured({})).toBe(false);
});
it("normalizes structured + array fields and never fabricates a missing value", async () => {
const fetchMock = vi.fn(async (url: string) => {
if (url.includes(":analyze")) {
return jsonResponse({}, { status: 202, headers: { "operation-location": "https://di.example.com/op/123" } });
}
if (url.includes("/op/123")) {
return jsonResponse({
status: "succeeded",
analyzeResult: {
content: "raw extracted text",
pages: [{}, {}],
documents: [
{
confidence: 0.93,
fields: {
VendorName: { type: "string", valueString: "Acme AB", confidence: 0.98 },
InvoiceTotal: { type: "currency", valueCurrency: { amount: 1234.5, currencyCode: "SEK" }, confidence: 0.91 },
// Field region detected but no value could be read — must stay blank, not fabricated.
DueDate: { type: "date", confidence: 0.4 },
Items: {
type: "array",
valueArray: [
{
type: "object",
valueObject: {
Description: { type: "string", valueString: "Widget", confidence: 0.9 },
Amount: { type: "currency", valueCurrency: { amount: 100 }, confidence: 0.85 },
Unit: { type: "string", confidence: 0.2 }, // present but empty -> found:false
},
},
],
},
},
},
],
},
});
}
throw new Error(`unexpected fetch: ${url}`);
});
vi.stubGlobal("fetch", fetchMock);
const adapter = new DocumentIntelligenceAdapter({ endpoint: "https://di.example.com", apiKey: "key123", pollIntervalMs: 1 });
const result = await adapter.analyzeDocument(Buffer.from("%PDF-1.4"), "prebuilt-invoice");
expect(result.modelId).toBe("prebuilt-invoice");
expect(result.content).toBe("raw extracted text");
expect(result.documentConfidence).toBe(0.93);
expect(result.pageCount).toBe(2);
expect(result.fields.VendorName).toEqual({ value: "Acme AB", confidence: 0.98, found: true });
expect(result.fields.InvoiceTotal).toEqual({ value: 1234.5, confidence: 0.91, found: true });
// Not fabricated: value stays null, but DI's own (low) confidence is still recorded.
expect(result.fields.DueDate).toEqual({ value: null, confidence: 0.4, found: false });
expect(result.arrayFields.Items).toHaveLength(1);
expect(result.arrayFields.Items[0].Description).toEqual({ value: "Widget", confidence: 0.9, found: true });
expect(result.arrayFields.Items[0].Amount).toEqual({ value: 100, confidence: 0.85, found: true });
expect(result.arrayFields.Items[0].Unit).toEqual({ value: null, confidence: 0.2, found: false });
});
it("retries once on 429 then proceeds", async () => {
let analyzeAttempts = 0;
const fetchMock = vi.fn(async (url: string) => {
if (url.includes(":analyze")) {
analyzeAttempts++;
if (analyzeAttempts === 1) return jsonResponse({}, { status: 429 });
return jsonResponse({}, { status: 202, headers: { "operation-location": "https://di.example.com/op/retry" } });
}
if (url.includes("/op/retry")) {
return jsonResponse({ status: "succeeded", analyzeResult: { documents: [{ fields: {} }] } });
}
throw new Error(`unexpected fetch: ${url}`);
});
vi.stubGlobal("fetch", fetchMock);
const adapter = new DocumentIntelligenceAdapter({
endpoint: "https://di.example.com",
apiKey: "key123",
pollIntervalMs: 1,
rateLimitRetryMs: 1,
});
const result = await adapter.analyzeDocument(Buffer.from("x"), "prebuilt-receipt");
expect(analyzeAttempts).toBe(2);
expect(result.fields).toEqual({});
});
it("throws (never returns a fabricated result) when analyze status is failed", async () => {
const fetchMock = vi.fn(async (url: string) => {
if (url.includes(":analyze")) {
return jsonResponse({}, { status: 202, headers: { "operation-location": "https://di.example.com/op/999" } });
}
if (url.includes("/op/999")) {
return jsonResponse({ status: "failed", error: { code: "InvalidDocument" } });
}
throw new Error("unexpected");
});
vi.stubGlobal("fetch", fetchMock);
const adapter = new DocumentIntelligenceAdapter({ endpoint: "https://di.example.com", apiKey: "key123", pollIntervalMs: 1 });
await expect(adapter.analyzeDocument(Buffer.from("x"), "prebuilt-invoice")).rejects.toThrow(/failed/i);
});
it("throws when the analyze request itself errors (5xx)", async () => {
const fetchMock = vi.fn(async (url: string) => {
if (url.includes(":analyze")) return jsonResponse({}, { status: 500 });
throw new Error("unexpected");
});
vi.stubGlobal("fetch", fetchMock);
const adapter = new DocumentIntelligenceAdapter({ endpoint: "https://di.example.com", apiKey: "key123" });
await expect(adapter.analyzeDocument(Buffer.from("x"), "prebuilt-invoice")).rejects.toThrow(/500/);
});
});