rag-service/tests/ocr/review.test.ts
Paco POR-CORREO 5fef85cfb2 feat(ocr): durable review indexing production wiring
Wire the complete OCR review and indexing production pipeline:
durable OCR result handoff before remote deletion (17a), native-page
evidence and quality-gated candidate composition (17b), review images
and restart-safe candidate loading (17c), transactional approval and
rejection decisions (17d), reviewed-artifact indexing store with exact
count verification (17e), and production approve-to-ready wiring with
fail-closed OCR_INDEXING_UNAVAILABLE (17f). Activation remains a
separately authorized operation; task 7.4 stays pending.
2026-09-16 18:35:18 +02:00

338 lines
25 KiB
TypeScript

import assert from "node:assert/strict";
import { access, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import { createApp } from "../../src/app.js";
import { env } from "../../src/config/env.js";
import { CatalogError } from "../../src/modules/catalog/errors.js";
import { OcrIndexingService, type ApprovedOcrCandidate, type OcrIndexingStore } from "../../src/modules/ocr/indexing.js";
import { OcrReviewService, PostgresOcrReviewStore, type OcrReviewCandidate } from "../../src/modules/ocr/review.js";
import { DurableOcrReviewReader } from "../../src/modules/ocr/review.js";
import { persistComposedCandidateArtifact, persistOcrResultArtifact, persistReviewImageArtifacts, readReviewedPagesArtifact, stageOcrArtifacts } from "../../src/modules/ocr/artifacts.js";
import { sha256Hex } from "../../src/shared/utils/ids.js";
function candidate(state: OcrReviewCandidate["state"] = "review_required"): OcrReviewCandidate {
const lines = ["CBGO4a", "FATo7"].map((text, index) => ({
lineId: `line-${index + 1}`, text, confidence: 0.7, bbox: [0, index * 10, 50, index * 10 + 8] as [number, number, number, number], lineSha256: sha256Hex(text)
}));
return {
versionId: "version-2", sourceId: "source-1", state, candidateSha256: "candidate-hash",
baseActiveVersionId: "version-1", currentActiveVersionId: "version-1", activateRequested: true,
processingFingerprint: "fingerprint", metadataHash: "metadata", documents: [{ documentId: "document-1", pages: [{
page: 1, imageUrl: "/private/page-1.png", nativeText: "", ocr: { text: "CBGO4a\nFATo7", lines },
candidateText: "CBGO4a\nFATo7", differences: ["native text is empty"], risks: ["CBGO4a", "FATo7"]
}] }]
};
}
test("review HTTP rejects unauthorized and premature access without exposing artifacts", async (context) => {
const previous = { token: env.lifecycleAdminToken, enabled: env.ocrIngestEnabled };
Object.assign(env, { lifecycleAdminToken: "review-token", ocrIngestEnabled: true });
context.after(() => Object.assign(env, { lifecycleAdminToken: previous.token, ocrIngestEnabled: previous.enabled }));
let value = candidate();
let reads = 0;
const review = new OcrReviewService({ async loadCandidate() { reads += 1; return value; }, async commitApproval() { throw new Error("not called"); } });
const server = createApp({ reviewService: review, startReconciler: false }).listen(0);
context.after(() => server.close());
const address = server.address();
assert.ok(address && typeof address === "object");
const url = `http://127.0.0.1:${address.port}/ingestions/version-2/review`;
const unauthorized = await fetch(url);
assert.equal(unauthorized.status, 401);
assert.equal((await fetch(url.replace("/review", "/approve"), { method: "POST" })).status, 401);
assert.equal(reads, 0);
value = candidate("indexing");
const premature = await fetch(url, { headers: { authorization: "Bearer review-token" } });
assert.equal(premature.status, 409);
assert.deepEqual(await premature.json(), { ok: false, error: "Version is not awaiting OCR review", code: "INVALID_VERSION_STATE" });
});
test("review exposes audit detail and commits current corrections as one immutable set", async () => {
const commits: unknown[] = [];
const service = new OcrReviewService({ async loadCandidate() { return candidate(); }, async commitApproval(value) { commits.push(value); } });
const review = await service.view("version-2");
assert.deepEqual(review.documents[0]?.pages[0]?.ocr.lines.map(({ text, confidence, bbox }) => [text, confidence, bbox]), [
["CBGO4a", 0.7, [0, 0, 50, 8]], ["FATo7", 0.7, [0, 10, 50, 18]]
]);
assert.deepEqual(review.documents[0]?.pages[0]?.risks, ["CBGO4a", "FATo7"]);
const approved = await service.approve("version-2", {
candidateSha256: "candidate-hash", expectedActiveVersionId: "version-1", reviewedBy: "admin",
corrections: [
{ documentId: "document-1", page: 1, lineId: "line-1", expectedLineSha256: sha256Hex("CBGO4a"), replacementText: "CBG04a" },
{ documentId: "document-1", page: 1, lineId: "line-2", expectedLineSha256: sha256Hex("FATo7"), replacementText: "FAT07" }
]
});
assert.equal(commits.length, 1);
assert.deepEqual((commits[0] as { corrections: Array<{ replacementText: string }> }).corrections.map(({ replacementText }) => replacementText), ["CBG04a", "FAT07"]);
assert.equal(approved.reviewedText, "CBG04a\nFAT07");
assert.equal(approved.reviewedTextSha256, sha256Hex("CBG04a\nFAT07"));
});
test("stale or duplicate corrections conflict before any correction or transition", async () => {
let commits = 0;
const service = new OcrReviewService({ async loadCandidate() { return candidate(); }, async commitApproval() { commits += 1; } });
const base = { candidateSha256: "stale", expectedActiveVersionId: "version-1", reviewedBy: "admin", corrections: [] };
await assert.rejects(service.approve("version-2", base), (error) => error instanceof CatalogError && error.statusCode === 409);
const duplicate = { ...base, candidateSha256: "candidate-hash", corrections: Array(2).fill({
documentId: "document-1", page: 1, lineId: "line-1", expectedLineSha256: sha256Hex("CBGO4a"), replacementText: "CBG04a"
}) };
await assert.rejects(service.approve("version-2", duplicate), (error) => error instanceof CatalogError && error.code === "CORRECTION_CONFLICT");
await assert.rejects(service.approve("version-2", { ...base, candidateSha256: "candidate-hash", expectedActiveVersionId: "version-old" }),
(error) => error instanceof CatalogError && error.code === "ACTIVE_VERSION_CHANGED");
await assert.rejects(service.approve("version-2", { ...base, candidateSha256: "candidate-hash", corrections: [{
documentId: "document-1", page: 1, lineId: "line-1", expectedLineSha256: sha256Hex("stale"), replacementText: "CBG04a"
}] }), (error) => error instanceof CatalogError && error.code === "CORRECTION_CONFLICT");
assert.equal(commits, 0);
});
function approved(activateRequested: boolean, state: ApprovedOcrCandidate["state"] = "indexing"): ApprovedOcrCandidate {
return { versionId: "version-2", sourceId: "source-1", state, activateRequested, expectedActiveVersionId: "version-1", reviewedText: "reviewed", reviewedTextSha256: sha256Hex("reviewed"), processingFingerprint: "fingerprint", metadataHash: "metadata" };
}
test("indexing activates only when requested and leaves a new candidate ready on an activation race", async () => {
const calls: string[] = [];
const store: OcrIndexingStore = {
async findReusableVersion() { return undefined; }, async indexReviewed() { calls.push("embed"); return 1; },
async markReady() { calls.push("ready"); }, async settleReusable() { throw new Error("not reusable"); },
async activateVersion() { calls.push("activate"); return "version-2"; }
};
const service = new OcrIndexingService(store);
assert.deepEqual(await service.index(approved(true)), { versionId: "version-2", state: "active", activated: true, activatedVersionId: "version-2" });
assert.deepEqual(calls.splice(0), ["embed", "ready", "activate"]);
assert.deepEqual(await service.index(approved(false)), { versionId: "version-2", state: "ready", activated: false });
assert.deepEqual(calls.splice(0), ["embed", "ready"]);
store.activateVersion = async () => { throw new CatalogError("race", 409, "ACTIVE_VERSION_PRECONDITION_FAILED"); };
await assert.rejects(service.index(approved(true)), (error) => error instanceof CatalogError && error.code === "ACTIVE_VERSION_CHANGED");
assert.deepEqual(calls, ["embed", "ready"]);
});
test("reusable and rejected candidates create no embeddings and reusable activation obeys CAS intent", async () => {
const calls: string[] = [];
const store: OcrIndexingStore = {
async findReusableVersion() { return { versionId: "version-existing" }; }, async indexReviewed() { calls.push("embed"); return 1; },
async markReady() { calls.push("ready"); }, async settleReusable(_candidate, _reusable, activate) { calls.push(`settle:${activate}`); return activate; },
async activateVersion() { throw new Error("new activation must not run"); }
};
const service = new OcrIndexingService(store);
assert.deepEqual(await service.index(approved(true)), { versionId: "version-2", state: "rejected", activated: true, activatedVersionId: "version-existing", errorCode: "DUPLICATE_REUSABLE_VERSION" });
assert.deepEqual(await service.index(approved(false)), { versionId: "version-2", state: "rejected", activated: false, errorCode: "DUPLICATE_REUSABLE_VERSION" });
store.settleReusable = async () => { throw new CatalogError("race", 409, "ACTIVE_VERSION_PRECONDITION_FAILED"); };
await assert.rejects(service.index(approved(true)), (error) => error instanceof CatalogError && error.code === "ACTIVE_VERSION_CHANGED");
await assert.rejects(service.index(approved(true, "rejected")), (error) => error instanceof CatalogError && error.code === "INVALID_VERSION_STATE");
assert.deepEqual(calls, ["settle:true", "settle:false"]);
});
test("authenticated rejection is durable, conflict-safe, and never indexes or activates", async (context) => {
const previous = { token: env.lifecycleAdminToken, enabled: env.ocrIngestEnabled };
Object.assign(env, { lifecycleAdminToken: "review-token", ocrIngestEnabled: true });
context.after(() => Object.assign(env, { lifecycleAdminToken: previous.token, ocrIngestEnabled: previous.enabled }));
let value = candidate();
let reads = 0;
const audits: Array<{ reviewedBy: string; reason: string }> = [];
const review = new OcrReviewService({
async loadCandidate() { reads += 1; return value; },
async commitApproval() { throw new Error("approval must not run"); },
async commitRejection({ reviewedBy, reason }) {
audits.push({ reviewedBy, reason });
value = { ...value, state: "rejected" };
}
});
let indexingCalls = 0;
const catalog = { async getIngestionStatus() {
return { versionId: value.versionId, state: value.state, phase: value.state, activated: false, error: value.state === "rejected" ? { code: "OCR_REJECTED", message: audits[0]?.reason, retryable: false } : null };
} };
const server = createApp({ catalog: catalog as never, reviewService: review, indexingService: { async index() { indexingCalls += 1; throw new Error("indexing must not run"); } }, startReconciler: false }).listen(0);
context.after(() => server.close());
const address = server.address();
assert.ok(address && typeof address === "object");
const url = `http://127.0.0.1:${address.port}/ingestions/version-2`;
const unauthorized = await fetch(`${url}/reject`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ candidateSha256: "candidate-hash", reviewedBy: "admin", reason: "Unreadable code" }) });
assert.equal(unauthorized.status, 401);
assert.equal(reads, 0);
const invalid = await fetch(`${url}/reject`, { method: "POST", headers: { authorization: "Bearer review-token", "content-type": "application/json" }, body: "{}" });
assert.equal(invalid.status, 400);
assert.deepEqual(await invalid.json(), { ok: false, error: "Candidate hash, reviewer, and rejection reason are required", code: "INVALID_REJECTION" });
assert.equal(reads, 0);
const stale = await fetch(`${url}/reject`, { method: "POST", headers: { authorization: "Bearer review-token", "content-type": "application/json" }, body: JSON.stringify({ candidateSha256: "stale", reviewedBy: "admin", reason: "Unreadable code" }) });
assert.equal(stale.status, 409);
assert.equal(audits.length, 0);
const rejected = await fetch(`${url}/reject`, { method: "POST", headers: { authorization: "Bearer review-token", "content-type": "application/json" }, body: JSON.stringify({ candidateSha256: "candidate-hash", reviewedBy: "admin", reason: "Unreadable code" }) });
assert.equal(rejected.status, 200);
assert.deepEqual(await rejected.json(), { versionId: "version-2", state: "rejected", activated: false });
assert.deepEqual(audits, [{ reviewedBy: "admin", reason: "Unreadable code" }]);
const status = await fetch(url, { headers: { authorization: "Bearer review-token" } });
assert.equal(status.status, 200);
assert.deepEqual(await status.json(), { versionId: "version-2", state: "rejected", phase: "rejected", activated: false, error: { code: "OCR_REJECTED", message: "Unreadable code", retryable: false } });
const repeated = await fetch(`${url}/reject`, { method: "POST", headers: { authorization: "Bearer review-token", "content-type": "application/json" }, body: JSON.stringify({ candidateSha256: "candidate-hash", reviewedBy: "admin", reason: "Again" }) });
assert.equal(repeated.status, 409);
assert.equal(indexingCalls, 0);
assert.equal(audits.length, 1);
});
test("playground serves the authenticated OCR review controls and audit fields", async (context) => {
const server = createApp({ startReconciler: false }).listen(0);
context.after(() => server.close());
const address = server.address();
assert.ok(address && typeof address === "object");
const base = `http://127.0.0.1:${address.port}`;
const [html, script, styles] = await Promise.all([
fetch(`${base}/playground`).then((response) => response.text()),
fetch(`${base}/playground/app.js`).then((response) => response.text()),
fetch(`${base}/playground/styles.css`).then((response) => response.text())
]);
for (const marker of ["data-tab=\"review\"", "reviewVersionId", "reviewToken", "loadReviewButton", "approveReviewButton", "rejectReviewButton", "reviewCandidate"]) assert.match(html, new RegExp(marker));
for (const marker of ["Authorization", "/review", "/approve", "/reject", "candidateSha256", "expectedLineSha256", "imageUrl", "nativeText", "confidence", "bbox", "differences", "risks"]) assert.match(script, new RegExp(marker));
assert.match(styles, /\.review-page/);
});
test("production review reader survives restart and fails closed on unauthorized or mismatched durable evidence", async (context) => {
const previous = { token: env.lifecycleAdminToken, enabled: env.ocrIngestEnabled, root: env.ocrArtifactRoot };
const rootDirectory = await mkdtemp(path.join(os.tmpdir(), "rag-review-loader-"));
const versionId = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa";
const documentId = "doc:review";
const original = Buffer.from("%PDF-review");
const text = "codigo CBGO4a has enough OCR characters for durable review";
Object.assign(env, { lifecycleAdminToken: "review-token", ocrIngestEnabled: true, ocrArtifactRoot: rootDirectory });
context.after(async () => { Object.assign(env, { lifecycleAdminToken: previous.token, ocrIngestEnabled: previous.enabled, ocrArtifactRoot: previous.root }); await rm(rootDirectory, { recursive: true, force: true }); });
await stageOcrArtifacts({ rootDirectory, versionId, createdAt: "2026-09-16T12:00:00.000Z", documents: [{
documentId, documentKey: "review.pdf", bytes: original, requestedPages: [1],
pages: [{ page: 1, text: "weak native", rasterCoverage: 1, textSha256: sha256Hex("weak native") }]
}] });
const result = { schemaVersion: "1" as const, jobId: "ocr-review", documentSha256: sha256Hex(original),
engine: { name: "paddleocr" as const, version: "3.4.0" as const, runtime: "paddlepaddle-3.2.2" as const, device: "cpu" as const, configVersion: "ocr-v1" as const, dpi: 200 as const },
pages: [{ page: 1, width: 100, height: 100, processingMs: 1, text,
metrics: { lineCount: 1, nonWhitespaceCharacters: 50, inkCoverage: 0.5, medianConfidence: 0.95, p10Confidence: 0.95, lowConfidenceLineRatio: 0 },
lines: [{ lineId: "p1-l1", text, confidence: 0.95, bbox: [1, 2, 30, 10] as [number, number, number, number] }] }] };
await persistOcrResultArtifact({ rootDirectory, versionId, documentId, result });
const png = Buffer.from("89504e470d0a1a0a0102", "hex");
const images = await persistReviewImageArtifacts({ rootDirectory, versionId, documentId, images: [{ page: 1, bytes: png, sha256: sha256Hex(png) }] });
const { candidate: durable } = await persistComposedCandidateArtifact({ rootDirectory, versionId, jobs: [{ documentId, remoteJobId: "ocr-review", requestedPages: [1], state: "succeeded" }] });
const page = durable.documents[0]!.pages[0]!;
let reads = 0;
const contextValue = { versionId, sourceId: "source-1", state: "review_required" as const, baseActiveVersionId: null, currentActiveVersionId: null,
activateRequested: false, processingFingerprint: "fingerprint", metadataHash: "metadata", pages: [{ documentId, page: 1,
nativeTextSha256: page.nativeTextSha256, ocrTextSha256: page.ocrTextSha256, candidateTextSha256: page.candidateTextSha256, metrics: page.metrics, risks: page.risks }] };
const catalog = { async loadOcrReviewContext() { reads += 1; return contextValue; } };
const restarted = new DurableOcrReviewReader(catalog, rootDirectory);
assert.equal((await restarted.view(versionId)).documents[0]!.pages[0]!.ocr.lines[0]!.lineSha256, sha256Hex(text));
assert.deepEqual((await restarted.image(versionId, documentId, 1)).bytes, png);
const server = createApp({ catalog: catalog as never, startReconciler: false }).listen(0);
context.after(() => server.close());
const address = server.address();
assert.ok(address && typeof address === "object");
const base = `http://127.0.0.1:${address.port}/ingestions/${versionId}`;
assert.equal((await fetch(`${base}/review`)).status, 401);
assert.equal(reads, 2);
assert.equal((await fetch(`${base}/review`, { headers: { authorization: "Bearer review-token" } })).status, 200);
const image = await fetch(`${base}/documents/${encodeURIComponent(documentId)}/pages/1/image`, { headers: { authorization: "Bearer review-token" } });
assert.equal(image.status, 200);
assert.deepEqual(Buffer.from(await image.arrayBuffer()), png);
assert.equal((await fetch(`${base}/approve`, { method: "POST", headers: { authorization: "Bearer review-token", "content-type": "application/json" }, body: "{}" })).status, 503);
assert.equal((await fetch(`${base}/reject`, { method: "POST", headers: { authorization: "Bearer review-token", "content-type": "application/json" }, body: "{}" })).status, 503);
contextValue.pages[0]!.candidateTextSha256 = "0".repeat(64);
assert.equal((await fetch(`${base}/review`, { headers: { authorization: "Bearer review-token" } })).status, 500);
contextValue.pages[0]!.candidateTextSha256 = page.candidateTextSha256;
await assert.rejects(restarted.image(versionId, "../escape", 1), /not found/i);
await writeFile(images.images[0]!.artifactPath, "corrupt", { mode: 0o600 });
await assert.rejects(restarted.image(versionId, documentId, 1), /integrity validation failed/);
});
function decisionPool(current: OcrReviewCandidate, state = "review_required", failReviewedPage = false) {
const sql: string[] = [];
const client = {
async query(statement: string) {
sql.push(statement);
if (/^(BEGIN|COMMIT|ROLLBACK|SET CONSTRAINTS)/u.test(statement.trim())) return { rowCount: 0, rows: [] };
if (statement.includes("FOR UPDATE OF v, s")) return { rowCount: 1, rows: [{
version_id: current.versionId, source_id: current.sourceId, state, base_active_version_id: current.baseActiveVersionId,
current_active_version_id: current.currentActiveVersionId, activate_requested: current.activateRequested,
processing_fingerprint: current.processingFingerprint, metadata_hash: current.metadataHash
}] };
if (statement.includes("FROM rag_document_pages") && statement.includes("FOR UPDATE")) return { rowCount: 1, rows: [{
document_id: current.documents[0]!.documentId, page_number: 1,
native_text_hash: sha256Hex(current.documents[0]!.pages[0]!.nativeText),
ocr_text_hash: sha256Hex(current.documents[0]!.pages[0]!.ocr.text),
candidate_text_hash: sha256Hex(current.documents[0]!.pages[0]!.candidateText), risk_tokens: current.documents[0]!.pages[0]!.risks
}] };
if (failReviewedPage && statement.includes("SET reviewed_text_hash")) return { rowCount: 0, rows: [] };
return { rowCount: 1, rows: [] };
},
release() {}
};
return { pool: { connect: async () => client } as never, sql };
}
test("production decision store commits corrected reviewed pages and the indexing transition in one locked transaction", async (context) => {
const rootDirectory = await mkdtemp(path.join(os.tmpdir(), "rag-review-decision-"));
const current = candidate();
current.versionId = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb";
await mkdir(path.join(rootDirectory, current.versionId));
context.after(() => rm(rootDirectory, { recursive: true, force: true }));
const { pool, sql } = decisionPool(current);
const store = new PostgresOcrReviewStore(pool, { async view() { return structuredClone(current); } }, rootDirectory);
const approved = await new OcrReviewService(store).approve(current.versionId, {
candidateSha256: current.candidateSha256, expectedActiveVersionId: current.baseActiveVersionId, reviewedBy: "admin",
corrections: [{ documentId: "document-1", page: 1, lineId: "line-1", expectedLineSha256: sha256Hex("CBGO4a"), replacementText: "CBG04a" }]
});
const reviewed = await readReviewedPagesArtifact({ rootDirectory, versionId: current.versionId,
candidateSha256: current.candidateSha256, reviewedTextSha256: approved.reviewedTextSha256 });
assert.equal(reviewed.documents[0]!.pages[0]!.candidateText, "CBG04a\nFATo7");
assert.match(sql.join("\n"), /FOR UPDATE OF v, s/u);
assert.match(sql.join("\n"), /INSERT INTO rag_review_corrections/u);
assert.match(sql.join("\n"), /reviewed_text_hash/u);
assert.match(sql.join("\n"), /SET state = 'indexing'/u);
assert.equal(sql.at(-1)?.trim(), "COMMIT");
});
test("production decisions reject stale, replayed, and path-corrupt writes without durable or lifecycle side effects", async (context) => {
const rootDirectory = await mkdtemp(path.join(os.tmpdir(), "rag-review-conflict-"));
const current = candidate();
current.versionId = "cccccccc-cccc-4ccc-8ccc-cccccccccccc";
context.after(() => rm(rootDirectory, { recursive: true, force: true }));
const input = { candidateSha256: current.candidateSha256, expectedActiveVersionId: current.baseActiveVersionId,
reviewedBy: "admin", corrections: [] };
for (const conflictState of ["indexing", "rejected"]) {
const { pool, sql } = decisionPool(current, conflictState);
const service = new OcrReviewService(new PostgresOcrReviewStore(pool, { async view() { return structuredClone(current); } }, rootDirectory));
await assert.rejects(service.approve(current.versionId, input), (error) => error instanceof CatalogError && error.statusCode === 409);
assert.doesNotMatch(sql.join("\n"), /INSERT INTO rag_review_corrections|SET state = 'indexing'/u);
}
const { pool, sql } = decisionPool(current);
const service = new OcrReviewService(new PostgresOcrReviewStore(pool, { async view() { return structuredClone(current); } }, rootDirectory));
await assert.rejects(service.approve(current.versionId, input), /ENOENT|artifact directory/u);
assert.doesNotMatch(sql.join("\n"), /INSERT INTO rag_review_corrections|SET state = 'indexing'/u);
await assert.rejects(access(path.join(rootDirectory, current.versionId, "reviewed-pages.json")));
await mkdir(path.join(rootDirectory, current.versionId));
const partial = decisionPool(current, "review_required", true);
const partialService = new OcrReviewService(new PostgresOcrReviewStore(partial.pool, { async view() { return structuredClone(current); } }, rootDirectory));
await assert.rejects(partialService.approve(current.versionId, input), /page evidence changed/u);
assert.equal(partial.sql.at(-1)?.trim(), "ROLLBACK");
await assert.rejects(access(path.join(rootDirectory, current.versionId, "reviewed-pages.json")));
});
test("production rejection records the bound reason without reviewed, correction, indexing, or activation artifacts", async (context) => {
const rootDirectory = await mkdtemp(path.join(os.tmpdir(), "rag-review-rejection-"));
const current = candidate();
current.versionId = "dddddddd-dddd-4ddd-8ddd-dddddddddddd";
await mkdir(path.join(rootDirectory, current.versionId));
context.after(() => rm(rootDirectory, { recursive: true, force: true }));
const { pool, sql } = decisionPool(current);
const service = new OcrReviewService(new PostgresOcrReviewStore(pool, { async view() { return structuredClone(current); } }, rootDirectory));
assert.deepEqual(await service.reject(current.versionId, { candidateSha256: current.candidateSha256, reviewedBy: " admin ", reason: " unreadable code " }),
{ versionId: current.versionId, state: "rejected", activated: false });
const statements = sql.join("\n");
assert.match(statements, /error_code = 'OCR_REJECTED'.*error_detail =/su);
assert.doesNotMatch(statements, /rag_review_corrections|reviewed_text_hash|state = 'indexing'/u);
await assert.rejects(access(path.join(rootDirectory, current.versionId, "reviewed-pages.json")));
});