419 lines
22 KiB
TypeScript
419 lines
22 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { access, lstat, mkdir, mkdtemp, readFile, stat, symlink, utimes, writeFile } from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import test from "node:test";
|
|
import { buildOcrIdentity, OcrClient, OcrClientError, type OcrResult } from "../../src/modules/ocr/client.js";
|
|
import {
|
|
persistComposedCandidateArtifact,
|
|
persistOcrResultArtifact,
|
|
persistReviewImageArtifacts,
|
|
readComposedCandidateArtifact,
|
|
readOcrResultArtifact,
|
|
resolveArtifactPath,
|
|
stageOcrArtifacts,
|
|
sweepOrphanArtifacts,
|
|
transferReviewImageArtifacts
|
|
} 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";
|
|
const identity = buildOcrIdentity({ documentSha256, pages: [1, 3] });
|
|
|
|
function jsonResponse(status: number, body: unknown): Response {
|
|
return new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
}
|
|
|
|
function ack(overrides: Record<string, unknown> = {}): Record<string, unknown> {
|
|
return {
|
|
jobId,
|
|
status: "queued",
|
|
...identity,
|
|
createdAt: "2026-09-14T10:00:00.000Z",
|
|
...overrides
|
|
};
|
|
}
|
|
|
|
function result(overrides: Record<string, unknown> = {}): Record<string, unknown> {
|
|
const pages = Array.isArray(overrides.pages) ? overrides.pages.map((page) => (page as { page: number }).page) : [1, 3];
|
|
const resultIdentity = buildOcrIdentity({ documentSha256, pages, idempotencyKey: typeof overrides.idempotencyKey === "string" ? overrides.idempotencyKey : undefined });
|
|
return {
|
|
schemaVersion: "1",
|
|
jobId,
|
|
...resultIdentity,
|
|
engine: {
|
|
name: "paddleocr",
|
|
version: "3.4.0",
|
|
runtime: "paddlepaddle-3.2.2",
|
|
device: "cpu",
|
|
configVersion: "ocr-v2",
|
|
dpi: 200
|
|
},
|
|
pages: [1, 3].map((page) => ({
|
|
page,
|
|
width: 1700,
|
|
height: 2200,
|
|
processingMs: 25,
|
|
text: `page ${page}`,
|
|
metrics: {
|
|
lineCount: 1,
|
|
nonWhitespaceCharacters: 5,
|
|
inkCoverage: 0.3,
|
|
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<Response | Error> = [
|
|
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, identity);
|
|
|
|
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-v2:${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 () => {
|
|
const malformedResult = result();
|
|
malformedResult.pages = [result().pages];
|
|
for (const response of [
|
|
jsonResponse(202, ack({ requestedPages: [3, 1] })),
|
|
jsonResponse(202, ack({ status: "unknown" })),
|
|
jsonResponse(200, result({ schemaVersion: "2" })),
|
|
jsonResponse(200, malformedResult)
|
|
]) {
|
|
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, identity);
|
|
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(),
|
|
...identity,
|
|
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, identity)).status, "succeeded");
|
|
assert.deepEqual(delays, [2_000, 4_000]);
|
|
});
|
|
|
|
test("authenticated delete accepts an empty idempotent response", async () => {
|
|
let request: { url: string; method: string; authorization: string | null } | undefined;
|
|
const client = new OcrClient({
|
|
baseUrl: "http://ocr.internal:8000",
|
|
token: "delete-token",
|
|
fetch: (async (input, init) => {
|
|
request = { url: String(input), method: init?.method ?? "GET", authorization: new Headers(init?.headers).get("Authorization") };
|
|
return new Response(null, { status: 204 });
|
|
}) as typeof fetch
|
|
});
|
|
|
|
await client.delete("job / 1");
|
|
assert.deepEqual(request, { url: "http://ocr.internal:8000/v1/jobs/job%20%2F%201", method: "DELETE", authorization: "Bearer delete-token" });
|
|
});
|
|
|
|
test("review image transfer validates authentication, identity, content type, and hash", async () => {
|
|
const png = Buffer.from("89504e470d0a1a0a0102", "hex");
|
|
const client = new OcrClient({
|
|
baseUrl: "http://ocr.internal:8000",
|
|
token: "image-token",
|
|
fetch: (async (_input, init) => new Response(png, { status: 200, headers: {
|
|
"content-type": "image/png", "content-length": String(png.length), "x-document-sha256": documentSha256,
|
|
"x-ocr-job-id": jobId, "x-ocr-identity-sha256": identity.requestIdentitySha256, "x-ocr-config-version": "ocr-v2",
|
|
"x-content-sha256": sha256Hex(png), "x-page-number": "1",
|
|
"x-seen-authorization": new Headers(init?.headers).get("authorization") ?? ""
|
|
} })) as typeof fetch
|
|
});
|
|
|
|
assert.deepEqual(await client.getReviewImage(jobId, 1, identity), { bytes: png, sha256: sha256Hex(png) });
|
|
await assert.rejects(client.getReviewImage(jobId, 2, identity), /identity is invalid/);
|
|
});
|
|
|
|
test("review image transfer treats size and HTTP 500 as terminal while retrying transient failures", async () => {
|
|
const png = Buffer.from("89504e470d0a1a0a0102", "hex");
|
|
for (const [status, expectedAttempts, retryable] of [[500, 1, false], [503, 3, true]] as const) {
|
|
let attempts = 0;
|
|
const client = new OcrClient({ baseUrl: "http://ocr.internal:8000", token: "token",
|
|
fetch: (async () => { attempts += 1; return new Response(null, { status }); }) as typeof fetch,
|
|
sleep: async () => undefined });
|
|
await assert.rejects(client.getReviewImage(jobId, 1, identity),
|
|
(error: unknown) => error instanceof OcrClientError && error.status === status && error.retryable === retryable);
|
|
assert.equal(attempts, expectedAttempts);
|
|
}
|
|
const invalidSize = new OcrClient({ baseUrl: "http://ocr.internal:8000", token: "token",
|
|
fetch: (async () => new Response(png, { headers: { "content-type": "image/png", "content-length": String(png.length + 1),
|
|
"x-ocr-job-id": jobId, "x-ocr-identity-sha256": identity.requestIdentitySha256, "x-ocr-config-version": "ocr-v2",
|
|
"x-document-sha256": documentSha256, "x-content-sha256": sha256Hex(png), "x-page-number": "1" } })) as typeof fetch });
|
|
await assert.rejects(invalidSize.getReviewImage(jobId, 1, identity), /integrity validation failed/);
|
|
});
|
|
|
|
test("review image transfer is sequential and resumes from durable per-page checkpoints", async (context) => {
|
|
const rootDirectory = await mkdtemp(path.join(os.tmpdir(), "rag-ocr-image-transfer-"));
|
|
context.after(() => import("node:fs/promises").then(({ rm }) => rm(rootDirectory, { recursive: true, force: true })));
|
|
const versionId = "12121212-1212-4121-8121-121212121212";
|
|
const documentId = "doc:image-transfer";
|
|
await stageOcrArtifacts({ rootDirectory, versionId, createdAt: "2026-09-21T10:00:00.000Z", documents: [{
|
|
documentId, documentKey: "scan.pdf", bytes: document, requestedPages: [1, 2, 3], pages: [1, 2, 3].map((page) => ({
|
|
page, text: "", rasterCoverage: 1, textSha256: sha256Hex("")
|
|
}))
|
|
}] });
|
|
const calls: number[] = [];
|
|
let active = 0;
|
|
let maxActive = 0;
|
|
let interrupted = true;
|
|
const loadImage = async (page: number) => {
|
|
calls.push(page);
|
|
active += 1;
|
|
maxActive = Math.max(maxActive, active);
|
|
await Promise.resolve();
|
|
active -= 1;
|
|
if (page === 2 && interrupted) throw new Error("transfer interrupted");
|
|
const bytes = Buffer.from(`89504e470d0a1a0a${String(page).padStart(4, "0")}`, "hex");
|
|
return { bytes, sha256: sha256Hex(bytes) };
|
|
};
|
|
|
|
await assert.rejects(transferReviewImageArtifacts({ rootDirectory, versionId, documentId, loadImage }), /transfer interrupted/);
|
|
assert.deepEqual(calls, [1, 2]);
|
|
interrupted = false;
|
|
const transferred = await transferReviewImageArtifacts({ rootDirectory, versionId, documentId, loadImage });
|
|
|
|
assert.deepEqual(calls, [1, 2, 2, 3]);
|
|
assert.equal(maxActive, 1);
|
|
assert.deepEqual(transferred.images.map(({ page }) => page), [1, 2, 3]);
|
|
assert.deepEqual(await transferReviewImageArtifacts({ rootDirectory, versionId, documentId, loadImage }), transferred);
|
|
assert.deepEqual(calls, [1, 2, 2, 3]);
|
|
await writeFile(transferred.images[0]!.artifactPath, "corrupt", { mode: 0o600 });
|
|
await assert.rejects(transferReviewImageArtifacts({ rootDirectory, versionId, documentId, loadImage }), /checkpoint integrity validation failed/);
|
|
assert.deepEqual(calls, [1, 2, 2, 3]);
|
|
});
|
|
|
|
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("durable OCR result survives simulated remote deletion with exact restart readback", async (context) => {
|
|
const rootDirectory = await mkdtemp(path.join(os.tmpdir(), "rag-ocr-result-"));
|
|
context.after(() => import("node:fs/promises").then(({ rm }) => rm(rootDirectory, { recursive: true, force: true })));
|
|
const versionId = "66666666-6666-4666-8666-666666666666";
|
|
const documentId = "doc:durable-result";
|
|
await stageOcrArtifacts({
|
|
rootDirectory,
|
|
versionId,
|
|
createdAt: "2026-09-16T10:00:00.000Z",
|
|
documents: [{ documentId, documentKey: "scan.pdf", bytes: document }]
|
|
});
|
|
let remoteResult: OcrResult | undefined = result() as unknown as OcrResult;
|
|
|
|
const persisted = await persistOcrResultArtifact({ rootDirectory, versionId, documentId, result: remoteResult });
|
|
remoteResult = undefined;
|
|
const restarted = await readOcrResultArtifact({
|
|
rootDirectory,
|
|
versionId,
|
|
documentId,
|
|
jobId,
|
|
documentSha256,
|
|
pages: [1, 3],
|
|
artifactSha256: persisted.artifactSha256
|
|
});
|
|
const repeated = await persistOcrResultArtifact({ rootDirectory, versionId, documentId, result: restarted });
|
|
|
|
assert.equal(remoteResult, undefined);
|
|
assert.deepEqual(restarted, result());
|
|
assert.equal(repeated.artifactPath, persisted.artifactPath);
|
|
assert.equal(repeated.artifactSha256, persisted.artifactSha256);
|
|
assert.equal((await stat(persisted.artifactPath)).mode & 0o777, 0o600);
|
|
});
|
|
|
|
test("OCR result readback fails closed on corruption and artifact identity mismatch", async (context) => {
|
|
const rootDirectory = await mkdtemp(path.join(os.tmpdir(), "rag-ocr-corrupt-"));
|
|
context.after(() => import("node:fs/promises").then(({ rm }) => rm(rootDirectory, { recursive: true, force: true })));
|
|
const versionId = "77777777-7777-4777-8777-777777777777";
|
|
const documentId = "doc:corruption";
|
|
await stageOcrArtifacts({ rootDirectory, versionId, createdAt: "2026-09-16T10:00:00.000Z", documents: [{ documentId, documentKey: "scan.pdf", bytes: document }] });
|
|
const persisted = await persistOcrResultArtifact({ rootDirectory, versionId, documentId, result: result() as unknown as OcrResult });
|
|
const envelope = JSON.parse(await readFile(persisted.artifactPath, "utf8")) as Record<string, unknown>;
|
|
|
|
await writeFile(persisted.artifactPath, canonicalJson({ ...envelope, documentId: "doc:other" }), { mode: 0o600 });
|
|
await assert.rejects(readOcrResultArtifact({ rootDirectory, versionId, documentId, jobId, documentSha256, pages: [1, 3] }), /identity validation failed/);
|
|
await writeFile(persisted.artifactPath, "{corrupt", { mode: 0o600 });
|
|
await assert.rejects(readOcrResultArtifact({ rootDirectory, versionId, documentId, jobId, documentSha256, pages: [1, 3] }), /integrity validation failed/);
|
|
});
|
|
|
|
test("restart-safe candidate composition persists exact native, OCR, and blank page evidence", async (context) => {
|
|
const rootDirectory = await mkdtemp(path.join(os.tmpdir(), "rag-ocr-candidate-"));
|
|
context.after(() => import("node:fs/promises").then(({ rm }) => rm(rootDirectory, { recursive: true, force: true })));
|
|
const versionId = "88888888-8888-4888-8888-888888888888";
|
|
const documentId = "doc:candidate";
|
|
await stageOcrArtifacts({ rootDirectory, versionId, createdAt: "2026-09-16T10:00:00.000Z", documents: [{
|
|
documentId, documentKey: "mixed.pdf", bytes: document, requestedPages: [2, 3], pages: [
|
|
{ page: 1, text: "Native page", rasterCoverage: 0, textSha256: sha256Hex("Native page") },
|
|
{ page: 2, text: "weak native", rasterCoverage: 0.8, textSha256: sha256Hex("weak native") },
|
|
{ page: 3, text: "", rasterCoverage: 0, textSha256: sha256Hex("") }
|
|
]
|
|
}] });
|
|
const ocrText = "codigo CBGO4a has enough OCR characters for the quality gate";
|
|
await persistOcrResultArtifact({ rootDirectory, versionId, documentId, result: result({ documentSha256, pages: [
|
|
{ page: 2, width: 1700, height: 2200, processingMs: 10, text: ocrText,
|
|
metrics: { lineCount: 1, nonWhitespaceCharacters: 50, inkCoverage: 0.4, medianConfidence: 0.95, p10Confidence: 0.95, lowConfidenceLineRatio: 0 },
|
|
lines: [{ lineId: "p2-l1", text: ocrText, confidence: 0.95, bbox: [1, 2, 3, 4] }] },
|
|
{ page: 3, width: 1700, height: 2200, processingMs: 10, text: "",
|
|
metrics: { lineCount: 0, nonWhitespaceCharacters: 0, inkCoverage: 0.001, medianConfidence: 0, p10Confidence: 0, lowConfidenceLineRatio: 0 }, lines: [] }
|
|
] }) as unknown as OcrResult });
|
|
await persistReviewImageArtifacts({ rootDirectory, versionId, documentId, images: [2, 3].map((page) => {
|
|
const bytes = Buffer.from(`89504e470d0a1a0a${String(page).padStart(4, "0")}`, "hex");
|
|
return { page, bytes, sha256: sha256Hex(bytes) };
|
|
}) });
|
|
|
|
const persisted = await persistComposedCandidateArtifact({ rootDirectory, versionId, jobs: [{ documentId, remoteJobId: jobId, requestedPages: [2, 3], state: "succeeded" }] });
|
|
const restarted = await readComposedCandidateArtifact({ rootDirectory, versionId, artifactSha256: persisted.artifactSha256 });
|
|
|
|
assert.deepEqual(restarted.documents[0]?.pages.map(({ method, candidateText }) => [method, candidateText]), [
|
|
["native", "Native page"], ["ocr", ocrText], ["blank", ""]
|
|
]);
|
|
assert.equal(restarted.documents[0]?.pages[1]?.risks[0], "CBGO4a");
|
|
assert.equal(restarted.candidateSha256, persisted.candidate.candidateSha256);
|
|
assert.equal((await stat(persisted.artifactPath)).mode & 0o777, 0o600);
|
|
await writeFile(persisted.artifactPath, "{corrupt", { mode: 0o600 });
|
|
await assert.rejects(readComposedCandidateArtifact({ rootDirectory, versionId }), /candidate artifact integrity/);
|
|
await assert.rejects(persistComposedCandidateArtifact({ rootDirectory, versionId, jobs: [{ documentId, remoteJobId: jobId, requestedPages: [2, 3], state: "succeeded" }] }), /conflicts with durable content/);
|
|
await writeFile(persisted.artifactPath, canonicalJson(restarted), { mode: 0o600 });
|
|
const manifest = JSON.parse(await readFile(path.join(rootDirectory, versionId, "manifest.json"), "utf8")) as { documents: Array<{ nativePagesPath: string }> };
|
|
await import("node:fs/promises").then(({ rm }) => rm(path.join(path.dirname(resolveArtifactPath(path.join(rootDirectory, versionId), manifest.documents[0]!.nativePagesPath)), "ocr-result.json")));
|
|
await assert.rejects(persistComposedCandidateArtifact({ rootDirectory, versionId, jobs: [{ documentId, remoteJobId: jobId, requestedPages: [2, 3], state: "succeeded" }] }), /result artifact integrity/);
|
|
});
|
|
|
|
test("candidate composition fails closed on quality failure and corrupt native evidence", async (context) => {
|
|
const rootDirectory = await mkdtemp(path.join(os.tmpdir(), "rag-ocr-candidate-fail-"));
|
|
context.after(() => import("node:fs/promises").then(({ rm }) => rm(rootDirectory, { recursive: true, force: true })));
|
|
const versionId = "99999999-9999-4999-8999-999999999999";
|
|
const documentId = "doc:blocked";
|
|
const staged = await stageOcrArtifacts({ rootDirectory, versionId, createdAt: "2026-09-16T10:00:00.000Z", documents: [{
|
|
documentId, documentKey: "scan.pdf", bytes: document, requestedPages: [1],
|
|
pages: [{ page: 1, text: "", rasterCoverage: 1, textSha256: sha256Hex("") }]
|
|
}] });
|
|
await persistOcrResultArtifact({ rootDirectory, versionId, documentId, result: result({ pages: [{
|
|
page: 1, width: 1700, height: 2200, processingMs: 10, text: "", lines: [],
|
|
metrics: { lineCount: 0, nonWhitespaceCharacters: 0, inkCoverage: 0.5, medianConfidence: 0, p10Confidence: 0, lowConfidenceLineRatio: 0 }
|
|
}] }) as unknown as OcrResult });
|
|
const blockedPng = Buffer.from("89504e470d0a1a0a0001", "hex");
|
|
await persistReviewImageArtifacts({ rootDirectory, versionId, documentId, images: [{ page: 1, bytes: blockedPng, sha256: sha256Hex(blockedPng) }] });
|
|
const jobs = [{ documentId, remoteJobId: jobId, requestedPages: [1], state: "succeeded" as const }];
|
|
|
|
assert.equal((await persistComposedCandidateArtifact({ rootDirectory, versionId, jobs })).artifactPath, null);
|
|
const manifest = JSON.parse(await readFile(staged.manifestPath, "utf8")) as { documents: Array<{ nativePagesPath: string }> };
|
|
await writeFile(resolveArtifactPath(staged.versionDirectory, manifest.documents[0]!.nativePagesPath), "{corrupt", { mode: 0o600 });
|
|
await assert.rejects(persistComposedCandidateArtifact({ rootDirectory, versionId, jobs }), /native page artifact integrity/);
|
|
});
|
|
|
|
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);
|
|
});
|