import assert from "node:assert/strict"; import { access, lstat, mkdir, mkdtemp, readFile, stat, symlink, utimes } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import test from "node:test"; import { OcrClient, OcrClientError } from "../../src/modules/ocr/client.js"; import { resolveArtifactPath, stageOcrArtifacts, sweepOrphanArtifacts } from "../../src/modules/ocr/artifacts.js"; import { canonicalJson, hashOrderedPairs, sha256Hex } from "../../src/shared/utils/ids.js"; const document = Buffer.from("%PDF-1.4\nunit-7\n%%EOF\n"); const documentSha256 = sha256Hex(document); const jobId = "ocr_job-7"; function jsonResponse(status: number, body: unknown): Response { return new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } }); } function ack(overrides: Record = {}): Record { return { jobId, status: "queued", documentSha256, requestedPages: [1, 3], configVersion: "ocr-v1", createdAt: "2026-09-14T10:00:00.000Z", ...overrides }; } function result(overrides: Record = {}): Record { return { schemaVersion: "1", jobId, documentSha256, engine: { name: "paddleocr", version: "3.4.0", runtime: "paddlepaddle-3.2.2", device: "cpu", configVersion: "ocr-v1", dpi: 200 }, pages: [1, 3].map((page) => ({ page, width: 1700, height: 2200, processingMs: 25, text: `page ${page}`, metrics: { lineCount: 1, nonWhitespaceCharacters: 5, medianConfidence: 0.95, p10Confidence: 0.95, lowConfidenceLineRatio: 0 }, lines: [{ lineId: `p${page}-l1`, text: `page ${page}`, confidence: 0.95, bbox: [1, 2, 3, 4] }] })), ...overrides }; } test("runtime retry stub preserves identity, exact attempts, backoff, and result integrity", async () => { const calls: Array<{ url: string; key: string | null }> = []; const delays: number[] = []; const responses: Array = [ new TypeError("connection reset"), jsonResponse(503, { detail: { code: "ENGINE_UNAVAILABLE" } }), jsonResponse(202, ack()), jsonResponse(200, result()) ]; const client = new OcrClient({ baseUrl: "http://ocr.internal:8000/", token: "internal-test-token", fetch: (async (input, init) => { calls.push({ url: String(input), key: new Headers(init?.headers).get("Idempotency-Key") }); const response = responses.shift(); if (response instanceof Error) throw response; return response as Response; }) as typeof fetch, sleep: async (milliseconds) => { delays.push(milliseconds); } }); const submitted = await client.submit(document, { documentSha256, pages: [1, 3] }); const completed = await client.getResult(jobId, { documentSha256, pages: [1, 3] }); assert.equal(submitted.jobId, jobId); assert.deepEqual(delays, [2_000, 4_000]); assert.equal(calls.filter(({ url }) => url.endsWith("/v1/jobs")).length, 3); assert.equal(new Set(calls.slice(0, 3).map(({ key }) => key)).size, 1); assert.equal(calls[0]?.key, `${documentSha256}:ocr-v1:${sha256Hex("[1,3]")}`); assert.deepEqual(completed.pages.map(({ page }) => page), [1, 3]); }); test("queue pressure and deterministic failures are surfaced without retries", async () => { for (const [status, retryable] of [[429, true], [422, false]] as const) { let attempts = 0; const client = new OcrClient({ baseUrl: "http://ocr.internal:8000", token: "token", fetch: (async () => { attempts += 1; return jsonResponse(status, { detail: { code: status === 429 ? "QUEUE_FULL" : "UNSUPPORTED_PDF" } }); }) as typeof fetch, sleep: async () => { throw new Error("must not sleep"); } }); await assert.rejects( client.submit(document, { documentSha256, pages: [1, 3] }), (error: unknown) => error instanceof OcrClientError && error.status === status && error.retryable === retryable ); assert.equal(attempts, 1); } }); test("strict validation rejects mismatched acknowledgements and result contracts", async () => { for (const response of [ jsonResponse(202, ack({ requestedPages: [3, 1] })), jsonResponse(200, result({ schemaVersion: "2" })), jsonResponse(200, result({ pages: [result().pages as unknown] })) ]) { const client = new OcrClient({ baseUrl: "http://ocr.internal:8000", token: "token", fetch: (async () => response) as typeof fetch, sleep: async () => undefined }); const operation = response.status === 202 ? client.submit(document, { documentSha256, pages: [1, 3] }) : client.getResult(jobId, { documentSha256, pages: [1, 3] }); await assert.rejects(operation, /OCR response integrity validation failed/); } }); test("polling uses bounded exponential backoff until a terminal status", async () => { const delays: number[] = []; const states = ["queued", "running", "succeeded"] as const; const client = new OcrClient({ baseUrl: "http://ocr.internal:8000", token: "token", fetch: (async () => jsonResponse(200, { jobId, status: states.shift(), completedPages: states.length === 0 ? 2 : 0, totalPages: 2, error: null })) as typeof fetch, sleep: async (milliseconds) => { delays.push(milliseconds); } }); assert.equal((await client.pollUntilTerminal(jobId)).status, "succeeded"); assert.deepEqual(delays, [2_000, 4_000]); }); test("artifact staging writes private originals and a verifiable canonical manifest", async (context) => { const rootDirectory = await mkdtemp(path.join(os.tmpdir(), "rag-ocr-artifacts-")); context.after(async () => { await import("node:fs/promises").then(({ rm }) => rm(rootDirectory, { recursive: true, force: true })); }); const versionId = "11111111-1111-4111-8111-111111111111"; const staged = await stageOcrArtifacts({ rootDirectory, versionId, createdAt: "2026-09-14T10:00:00.000Z", documents: [ { documentId: "doc:source:b", documentKey: "b.pdf", bytes: Buffer.from("%PDF-b") }, { documentId: "doc:source:a", documentKey: "a.pdf", bytes: Buffer.from("%PDF-a") } ] }); const persisted = JSON.parse(await readFile(staged.manifestPath, "utf8")); assert.equal(staged.originalManifestHash, hashOrderedPairs([["b.pdf", sha256Hex("%PDF-b")], ["a.pdf", sha256Hex("%PDF-a")]])); assert.equal(staged.manifestSha256, sha256Hex(canonicalJson(persisted))); assert.deepEqual(persisted.documents.map((entry: { documentKey: string }) => entry.documentKey), ["a.pdf", "b.pdf"]); for (const entry of persisted.documents) { const originalPath = resolveArtifactPath(staged.versionDirectory, entry.originalPath); assert.equal((await stat(originalPath)).mode & 0o777, 0o600); assert.equal(sha256Hex(await readFile(originalPath)), entry.originalSha256); } assert.equal((await stat(staged.manifestPath)).mode & 0o777, 0o600); }); test("safe resolution blocks traversal and orphan sweep preserves retained, recent, and non-directory entries", async (context) => { const rootDirectory = await mkdtemp(path.join(os.tmpdir(), "rag-ocr-sweep-")); context.after(async () => { await import("node:fs/promises").then(({ rm }) => rm(rootDirectory, { recursive: true, force: true })); }); const retained = "22222222-2222-4222-8222-222222222222"; const orphan = "33333333-3333-4333-8333-333333333333"; const recent = "44444444-4444-4444-8444-444444444444"; await Promise.all([retained, orphan, recent].map((id) => mkdir(path.join(rootDirectory, id)))); await utimes(path.join(rootDirectory, orphan), new Date(0), new Date(0)); await symlink(path.join(rootDirectory, orphan), path.join(rootDirectory, "55555555-5555-4555-8555-555555555555")); assert.throws(() => resolveArtifactPath(path.join(rootDirectory, retained), "../manifest.json"), /escapes version directory/); assert.deepEqual(await sweepOrphanArtifacts({ rootDirectory, retainedVersionIds: new Set([retained]), olderThan: new Date("2026-09-14T09:00:00.000Z") }), [orphan]); await assert.rejects(access(path.join(rootDirectory, orphan))); await Promise.all([retained, recent].map((id) => access(path.join(rootDirectory, id)))); assert.equal((await lstat(path.join(rootDirectory, "55555555-5555-4555-8555-555555555555"))).isSymbolicLink(), true); });