Files
OmniRoute/tests/unit/ocr-route.test.ts
Diego Rodrigues de Sa e Souza c62ace5a49 feat(ocr): multi-provider /v1/ocr with transformation layer (Azure Document Intelligence) (#10283)
* feat(ocr): transformation layer on ocrRegistry (Mistral shape canonical)

* feat(ocr): Azure Document Intelligence provider (prebuilt-read, analyze+poll)

* feat(ocr): generic dispatch with per-provider transformation and DI poll loop

* test(ocr): align sanitized-500 assert with HR#12 error sanitization

The test's own title ("returns a sanitized 500") describes the new
behavior mandated by HR#12 (never leak err.message in a response body).
The old regex asserted the pre-sanitization leak (`OCR request failed:
socket closed`) as expected output, which contradicted its own title
and the sanitization this task intentionally introduced in
open-sse/handlers/ocr.ts. Scoped to this single assertion only.

* fix(ocr): fail fast on non-ok poll responses instead of misleading 504

pollOcrOperation now checks pollRes.ok and returns a sanitized 502
immediately (logging the upstream status via console.error) instead of
looping until the 30-attempt cap and surfacing a misleading timeout for
what was actually an auth/upstream error during polling.

* feat(ocr): route/docs for multi-provider /v1/ocr

- Route: map the connection's providerSpecificData.baseUrl onto
  credentials.baseUrl (resolveOcrCredentials) so azure-document-intelligence
  connections resolve their endpoint the same way every other custom-endpoint
  provider does (src/lib/providers/validation/*); previously handleOcr only
  saw a baseUrl when a caller set it directly, so the DB-backed Azure
  connection endpoint was never forwarded.
- v1OcrSchema.model is already a free-form string, no schema change needed.
- Docs: add the /v1/ocr provider table + example + Azure poll-flow note to
  API_REFERENCE.md, and describe the provider/model prefix + async poll
  behavior in openapi.yaml.
- Test: tests/unit/ocr-route-contract.test.ts covers getAllOcrModels/
  parseOcrModel for both providers and resolveOcrCredentials's mapping.

* chore(quality): rebaseline deadExports for the OCR/image-to-text series

* docs(skills): regenerate omni-inference skill for the multi-provider /v1/ocr

The generated agent skill mirrors docs/reference/API_REFERENCE.md; updating the
/v1/ocr section left it stale and tripped the merge-integrity gate.

---------

Co-authored-by: Xiangzhe <bakryun0718@proton.me>
2026-08-14 13:47:35 -03:00

190 lines
7.1 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
const { POST, OPTIONS } = await import("../../src/app/api/v1/ocr/route.ts");
const { handleOcr } = await import("../../open-sse/handlers/ocr.ts");
const { OCR_PROVIDERS, getOcrProvider, parseOcrModel, getAllOcrModels } =
await import("../../open-sse/config/ocrRegistry.ts");
const { v1OcrSchema } = await import("../../src/shared/validation/schemas/apiV1.ts");
const originalFetch = globalThis.fetch;
test.afterEach(() => {
globalThis.fetch = originalFetch;
});
function ocrRequest(body: string) {
return new Request("http://localhost/v1/ocr", {
method: "POST",
headers: { "Content-Type": "application/json" },
body,
});
}
// ── Registry ───────────────────────────────────────────────────────────────
test("OCR_PROVIDERS registers mistral with the Mistral OCR base URL", () => {
assert.equal(OCR_PROVIDERS.mistral.baseUrl, "https://api.mistral.ai/v1/ocr");
const provider = getOcrProvider("mistral");
assert.ok(provider);
assert.equal(provider.authHeader, "bearer");
assert.ok(provider.models.some((m: { id: string }) => m.id === "mistral-ocr-latest"));
});
test("parseOcrModel routes bare and prefixed mistral models to the mistral provider", () => {
assert.deepEqual(parseOcrModel("mistral-ocr-latest"), {
provider: "mistral",
model: "mistral-ocr-latest",
});
assert.deepEqual(parseOcrModel("mistral/mistral-ocr-latest"), {
provider: "mistral",
model: "mistral-ocr-latest",
});
// Unknown model → no provider resolved
assert.deepEqual(parseOcrModel("mystery-model"), {
provider: null,
model: "mystery-model",
});
});
test("getAllOcrModels exposes the mistral OCR model with a provider prefix", () => {
const models = getAllOcrModels();
assert.ok(models.some((m: { id: string }) => m.id === "mistral/mistral-ocr-latest"));
});
// ── Schema (Zod, Rule #7) ────────────────────────────────────────────────────
test("v1OcrSchema rejects a body without a document", () => {
const result = v1OcrSchema.safeParse({ model: "mistral-ocr-latest" });
assert.equal(result.success, false);
});
test("v1OcrSchema accepts a document_url document object", () => {
const result = v1OcrSchema.safeParse({
model: "mistral-ocr-latest",
document: { type: "document_url", document_url: "https://example.com/a.pdf" },
});
assert.equal(result.success, true);
});
test("v1OcrSchema accepts an image_url document object", () => {
const result = v1OcrSchema.safeParse({
document: { type: "image_url", image_url: "https://example.com/a.png" },
});
assert.equal(result.success, true);
});
// ── Route (public /v1/ocr entry point) ───────────────────────────────────────
test("POST /v1/ocr returns 400 for invalid JSON without leaking a stack trace", async () => {
const response = await POST(ocrRequest("not json at all"));
const body = (await response.json()) as any;
assert.equal(response.status, 400);
assert.equal(body.error.message, "Invalid JSON body");
// Rule #12 — error responses must never leak stack traces.
assert.ok(!body.error.message.includes("at /"));
});
test("OPTIONS /v1/ocr answers the CORS preflight", async () => {
const response = await OPTIONS();
assert.equal(response.status, 200);
assert.match(response.headers.get("access-control-allow-methods") || "", /OPTIONS/);
});
// ── Handler ──────────────────────────────────────────────────────────────────
test("handleOcr requires a document", async () => {
const response = await handleOcr({
body: { model: "mistral-ocr-latest" },
credentials: { apiKey: "sk-test" },
});
const payload = (await response.json()) as any;
assert.equal(response.status, 400);
assert.equal(payload.error.message, "document is required");
});
test("handleOcr rejects unknown OCR models", async () => {
const response = await handleOcr({
body: { model: "mystery/ocr", document: { document_url: "x" } },
credentials: { apiKey: "sk-test" },
});
const payload = (await response.json()) as any;
assert.equal(response.status, 400);
assert.match(payload.error.message, /No OCR provider found/);
});
test("handleOcr requires credentials for the resolved provider", async () => {
const response = await handleOcr({
body: { document: { document_url: "x" } },
credentials: null,
});
const payload = (await response.json()) as any;
assert.equal(response.status, 401);
assert.equal(payload.error.message, "No credentials for OCR provider: mistral");
});
test("handleOcr proxies a successful request to the mistral OCR endpoint", async () => {
let captured: any;
globalThis.fetch = async (url: any, options: any = {}) => {
captured = {
url: String(url),
headers: options.headers,
body: JSON.parse(String(options.body || "{}")),
};
return Response.json({ pages: [{ index: 0, markdown: "hello" }] });
};
const response = await handleOcr({
body: { document: { type: "document_url", document_url: "https://example.com/a.pdf" } },
credentials: { apiKey: "sk-mistral" },
});
assert.equal(captured.url, "https://api.mistral.ai/v1/ocr");
assert.equal(captured.headers.Authorization, "Bearer sk-mistral");
// model defaults to mistral-ocr-latest and the document is forwarded upstream.
assert.equal(captured.body.model, "mistral-ocr-latest");
assert.deepEqual(captured.body.document, {
type: "document_url",
document_url: "https://example.com/a.pdf",
});
assert.equal(response.status, 200);
assert.deepEqual(await response.json(), { pages: [{ index: 0, markdown: "hello" }] });
});
test("handleOcr passes upstream error payloads through with the upstream status", async () => {
globalThis.fetch = async () =>
new Response('{"error":"bad request"}', {
status: 422,
headers: { "content-type": "application/json" },
});
const response = await handleOcr({
body: { model: "mistral/mistral-ocr-latest", document: { document_url: "x" } },
credentials: { apiKey: "sk-test" },
});
assert.equal(response.status, 422);
assert.equal(await response.text(), '{"error":"bad request"}');
});
test("handleOcr returns a sanitized 500 when the upstream request throws", async () => {
globalThis.fetch = async () => {
throw new Error("socket closed");
};
const response = await handleOcr({
body: { model: "mistral-ocr-latest", document: { document_url: "x" } },
credentials: { apiKey: "sk-test" },
});
const payload = (await response.json()) as any;
assert.equal(response.status, 500);
assert.ok(payload.error.message.includes("OCR request failed"));
assert.ok(!payload.error.message.includes("socket closed"));
assert.ok(!payload.error.message.includes("at /"));
});