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.
338 lines
25 KiB
TypeScript
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")));
|
|
});
|