Files
0pt/tests/p0/document-intelligence-adapter.test.ts
T

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/);
});
});