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.
81 lines
No EOL
4.5 KiB
TypeScript
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");
|
|
}); |