rag-service/tests/ocr/approval-indexing-wiring.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

81 lines
No EOL
4.5 KiB
TypeScript

import assert from "node:assert/strict";
import test from "node:test";
import { createApp } from "../../src/app.js";
import { env } from "../../src/config/env.js";
import { OcrReadyIndexingService } from "../../src/modules/ocr/indexing.js";
import { OcrReviewService } from "../../src/modules/ocr/review.js";
import type { OcrReviewCandidate } from "../../src/modules/ocr/review.js";
const token = "admin-token";
function candidate(state: string): OcrReviewCandidate {
return {
versionId: "11111111-2222-4333-8444-555555555555", sourceId: "src:scan.pdf", state: state as OcrReviewCandidate["state"],
baseActiveVersionId: "active-1", currentActiveVersionId: "active-1", activateRequested: true, processingFingerprint: "fp", metadataHash: "mh",
candidateSha256: "candidate-hash",
documents: [{ documentId: "doc:scan", pages: [{ page: 1, imageUrl: "/img", nativeText: "", ocr: { text: "CBGO4a", lines: [{ lineId: "line-1", text: "CBGO4a", confidence: 0.9, bbox: [0, 0, 1, 1], lineSha256: "hash" }] }, candidateText: "CBGO4a", differences: [], risks: [] }] }]
} as OcrReviewCandidate;
}
test("production approval triggers durable indexing and reports ready", async (context) => {
const previous = { lifecycle: env.knowledgeLifecycleEnforced, enabled: env.ocrIngestEnabled, admin: env.lifecycleAdminToken };
Object.assign(env, { knowledgeLifecycleEnforced: true, ocrIngestEnabled: true, lifecycleAdminToken: token });
context.after(() => Object.assign(env, previous));
let reviewed: string | undefined;
let indexedCandidate: string | undefined;
const review = new OcrReviewService({
async loadCandidate() { return candidate(reviewed ? "indexing" : "review_required"); },
async commitApproval(input) { reviewed = input.reviewedText; },
async commitRejection() { throw new Error("rejection is out of scope"); }
});
const indexing = new OcrReadyIndexingService({
async findReusableVersion() { return undefined; },
async indexReviewed(value) { indexedCandidate = value.reviewedText; return 1; },
async markReady() { /* ready marker */ },
async settleReusable() { throw new Error("no reusable path"); },
async activateVersion() { throw new Error("activation is a separate unit"); }
} as never);
const server = createApp({ reviewService: review, indexingService: indexing, 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 versionId = "11111111-2222-4333-8444-555555555555";
const approval = await fetch(`${base}/ingestions/${versionId}/approve`, {
method: "POST",
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
body: JSON.stringify({ candidateSha256: "candidate-hash", expectedActiveVersionId: "active-1", reviewedBy: "reviewer", corrections: [] })
});
assert.equal(approval.status, 200);
const body = await approval.json() as { state: string; activated: boolean };
assert.equal(body.state, "ready");
assert.equal(body.activated, false);
assert.equal(reviewed, "CBGO4a");
assert.equal(indexedCandidate, "CBGO4a");
});
test("approval without a configured indexing service stays at the indexing boundary with 503", async (context) => {
const previous = { lifecycle: env.knowledgeLifecycleEnforced, enabled: env.ocrIngestEnabled, admin: env.lifecycleAdminToken };
Object.assign(env, { knowledgeLifecycleEnforced: true, ocrIngestEnabled: true, lifecycleAdminToken: token });
context.after(() => Object.assign(env, previous));
const review = new OcrReviewService({
async loadCandidate() { return candidate("review_required"); },
async commitApproval() { /* recorded */ },
async commitRejection() { throw new Error("out of scope"); }
});
const server = createApp({ reviewService: review, 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 versionId = "11111111-2222-4333-8444-555555555555";
const approval = await fetch(`${base}/ingestions/${versionId}/approve`, {
method: "POST",
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
body: JSON.stringify({ candidateSha256: "candidate-hash", expectedActiveVersionId: "active-1", reviewedBy: "reviewer", corrections: [] })
});
assert.equal(approval.status, 503);
assert.equal(((await approval.json()) as { code?: string }).code, "OCR_INDEXING_UNAVAILABLE");
});