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