Files
agents-marketplace/tests/unit/supplier-ocr-vision.test.ts
FilipBl4gojevic 069edcb076 feat(ocr): model-scoped OCR capability + gateway /v1/ocr/extract route
Adds the ocr.page.extract.chandra-ocr-2.v1 capability end to end: the
postOcrEscrow builder (locking the min-ada floor for the Submitted-state
datum, required since the min-ada fix), the gateway OCR route that selects
an OCR supplier by capability, posts a per-page escrow, calls the supplier,
verifies and settles, and returns content plus the signed receipt labelled
first-party-brokered. Supplier OCR handler with a hosted document-API
upstream and buyer-SDK OCR support included.

Squashed from the 2026-08-06 working subset; the deploy pack is
intentionally excluded (it lives in a private operator repo).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-10 16:40:00 +02:00

165 lines
6.1 KiB
TypeScript

/**
* supplier-ocr-vision.test.ts — coverage for the OpenAI-compatible VISION
* upstream client used by the `ocr` capability kind.
*
* Mirrors supplier-openai.test.ts: stub global fetch, pin the outbound wire
* shape (vision content parts, data-URL image), and the error taxonomy
* (ocr_failure / ocr_timeout / ocr_malformed).
*/
import { describe, it, expect, vi, afterEach } from "vitest";
import {
callOcrVision,
probeRevision,
defaultOcrInstruction,
OcrVisionError,
} from "../../supplier/src/ocrVision.js";
const IMG = Buffer.from("some page bytes").toString("base64");
function validParams() {
return {
baseUrl: "http://vllm.fake:8000",
model: "datalab-to/chandra-ocr-2",
request: { image_b64: IMG, mime: "image/png", output_format: "markdown" },
timeoutMs: 5_000,
};
}
function okBody(content = "# Page\ntext") {
return {
choices: [{ index: 0, message: { role: "assistant", content }, finish_reason: "stop" }],
usage: { prompt_tokens: 900, completion_tokens: 120, total_tokens: 1020 },
};
}
afterEach(() => {
vi.unstubAllGlobals();
});
describe("callOcrVision — happy path + wire shape", () => {
it("POSTs vision content parts with a data-URL image and returns the normalised result", async () => {
let captured: { url: string; init: RequestInit } | null = null;
vi.stubGlobal("fetch", async (url: string, init: RequestInit) => {
captured = { url, init };
return new Response(JSON.stringify(okBody()), {
status: 200,
headers: { "content-type": "application/json" },
});
});
const result = await callOcrVision(validParams());
expect(result.content).toBe("# Page\ntext");
expect(result.prompt_tokens).toBe(900);
expect(result.completion_tokens).toBe(120);
expect(result.wallclock_ms).toBeGreaterThanOrEqual(0);
expect(captured).not.toBeNull();
expect(captured!.url).toBe("http://vllm.fake:8000/v1/chat/completions");
const sent = JSON.parse(String(captured!.init.body));
expect(sent.model).toBe("datalab-to/chandra-ocr-2");
expect(sent.stream).toBe(false);
expect(Array.isArray(sent.messages)).toBe(true);
const content = sent.messages[0].content;
expect(content[0].type).toBe("image_url");
expect(content[0].image_url.url).toBe(`data:image/png;base64,${IMG}`);
expect(content[1].type).toBe("text");
expect(content[1].text).toBe(defaultOcrInstruction("markdown"));
});
it("uses promptOverride verbatim when provided", async () => {
let sentText = "";
vi.stubGlobal("fetch", async (_url: string, init: RequestInit) => {
sentText = JSON.parse(String(init.body)).messages[0].content[1].text;
return new Response(JSON.stringify(okBody()), { status: 200 });
});
await callOcrVision({ ...validParams(), promptOverride: "OCR EXACTLY AS CHANDRA WANTS" });
expect(sentText).toBe("OCR EXACTLY AS CHANDRA WANTS");
});
it("forwards max_tokens when positive and the bearer key when set", async () => {
let sent: Record<string, unknown> = {};
let auth = "";
vi.stubGlobal("fetch", async (_url: string, init: RequestInit) => {
sent = JSON.parse(String(init.body));
auth = (init.headers as Record<string, string>).authorization ?? "";
return new Response(JSON.stringify(okBody()), { status: 200 });
});
await callOcrVision({ ...validParams(), maxTokens: 4096, apiKey: "sk-test" });
expect(sent.max_tokens).toBe(4096);
expect(auth).toBe("Bearer sk-test");
});
});
describe("callOcrVision — error taxonomy", () => {
it("ocr_failure on non-2xx", async () => {
vi.stubGlobal("fetch", async () => new Response("boom", { status: 500 }));
await expect(callOcrVision(validParams())).rejects.toMatchObject({
name: "OcrVisionError",
reason: "ocr_failure",
});
});
it("ocr_malformed on missing content", async () => {
vi.stubGlobal("fetch", async () =>
new Response(JSON.stringify({ choices: [{ message: { role: "assistant", content: "" } }] }), { status: 200 }));
await expect(callOcrVision(validParams())).rejects.toMatchObject({
reason: "ocr_malformed",
});
});
it("ocr_timeout when the upstream exceeds timeoutMs", async () => {
vi.stubGlobal("fetch", (_url: string, init: RequestInit) =>
new Promise((_resolve, reject) => {
const signal = init.signal as AbortSignal;
signal.addEventListener("abort", () => {
const err = new Error("aborted");
err.name = "AbortError";
reject(err);
});
}));
await expect(callOcrVision({ ...validParams(), timeoutMs: 30 })).rejects.toMatchObject({
reason: "ocr_timeout",
});
});
it("exports the error class for instanceof checks", async () => {
vi.stubGlobal("fetch", async () => new Response("nope", { status: 502 }));
try {
await callOcrVision(validParams());
expect.unreachable("should have thrown");
} catch (err) {
expect(err).toBeInstanceOf(OcrVisionError);
}
});
});
describe("probeRevision — served-checkpoint probe", () => {
it("extracts a 40-hex sha from known runtime fields", async () => {
const sha = "a".repeat(40);
vi.stubGlobal("fetch", async () =>
new Response(JSON.stringify({ model_sha: sha }), { status: 200 }));
const result = await probeRevision({ probeUrl: "http://vllm.fake:8000/info", timeoutMs: 1000 });
expect(result.ok).toBe(true);
expect(result.servedSha).toBe(sha);
});
it("ok with empty sha when the runtime reports no revision field", async () => {
vi.stubGlobal("fetch", async () =>
new Response(JSON.stringify({ served_model: "chandra" }), { status: 200 }));
const result = await probeRevision({ probeUrl: "http://x/info", timeoutMs: 1000 });
expect(result.ok).toBe(true);
expect(result.servedSha).toBe("");
});
it("not ok on HTTP error / network failure", async () => {
vi.stubGlobal("fetch", async () => new Response("x", { status: 503 }));
expect((await probeRevision({ probeUrl: "http://x/info", timeoutMs: 1000 })).ok).toBe(false);
vi.stubGlobal("fetch", async () => { throw new Error("ECONNREFUSED"); });
expect((await probeRevision({ probeUrl: "http://x/info", timeoutMs: 1000 })).ok).toBe(false);
});
});