const path = require("node:path"); const { pathToFileURL } = require("node:url"); const { spawnSync } = require("node:child_process"); const ROOT_DIR = path.resolve(__dirname, "../../../.."); const SCRIPT_PATH = path.join(ROOT_DIR, "scripts/qa/check-ocr-provider-samples.mjs"); const OCR_PDF_MAX_BYTES = 7 * 1024 * 1024; function syntheticPdf(body = "synthetic") { return Buffer.from(`%PDF-1.7\n${body}\n%%EOF\n`, "ascii"); } function providerConfig() { return { ocrProvider: "tencent", tencentSecretId: "synthetic-id", tencentSecretKey: "synthetic-key", tencentOcrRegion: "ap-beijing", ocrTimeoutMs: 5000, }; } function normalizedResult(overrides = {}) { return { documentSubtype: "VatElectronicCommonInvoice", documentCategory: "vat_invoice", documentCount: 1, invoiceCode: "synthetic-code", invoiceNumber: "synthetic-number", invoiceDate: "2026-01-01", amount: "1.00", taxAmount: "0.01", totalAmount: "1.01", sellerName: "synthetic-seller", buyerName: "synthetic-buyer", transportMode: "pdf_native", ...overrides, }; } describe("check-ocr-provider-samples", () => { let liveRunner; beforeAll(async () => { liveRunner = await import(pathToFileURL(SCRIPT_PATH).href); }); it.each([ ["vat_invoice", normalizedResult()], [ "full_vat_invoice", normalizedResult({ documentSubtype: "VatElectronicInvoiceFull", documentCategory: "full_vat_invoice", invoiceCode: null, }), ], [ "electronic_train_ticket", normalizedResult({ documentSubtype: "ElectronicTrainTicketFull", documentCategory: "electronic_train_ticket", invoiceCode: null, sellerName: null, }), ], ])("accepts category-aware required fields for %s", async (_category, result) => { const runOcr = vi.fn().mockResolvedValue(result); const summary = await liveRunner.runLiveSampleCheck({ alias: "SAMPLE-PDF-TEST", buffer: syntheticPdf(), config: providerConfig(), runOcr, now: vi.fn().mockReturnValueOnce(100).mockReturnValueOnce(125), }); expect(summary.status).toBe("PASS"); expect(summary.elapsedMs).toBe(25); expect(summary.documentCategory).toBe(result.documentCategory); expect(summary.missingRequiredFields).toEqual([]); expect(runOcr).toHaveBeenCalledTimes(1); expect(runOcr.mock.calls[0][0]).toMatchObject({ ocrType: "vat", sourceMimeType: "application/pdf", }); }); it("holds a railway ticket when its buyer field is missing", async () => { const summary = await liveRunner.runLiveSampleCheck({ alias: "SAMPLE-PDF-TRAIN", buffer: syntheticPdf(), config: providerConfig(), runOcr: vi.fn().mockResolvedValue( normalizedResult({ documentSubtype: "ElectronicTrainTicketFull", documentCategory: "electronic_train_ticket", invoiceCode: null, sellerName: null, buyerName: null, }), ), }); expect(summary.status).toBe("HOLD"); expect(summary.missingRequiredFields).toEqual(["buyerName"]); }); it("never exposes provider raw fields or normalized business values", async () => { const summary = await liveRunner.runLiveSampleCheck({ alias: "SAMPLE-PDF-PRIVACY", buffer: syntheticPdf(), config: providerConfig(), runOcr: vi.fn().mockResolvedValue( normalizedResult({ raw: { passengerIdentity: "forbidden-identity-marker", response: "forbidden-response-marker", }, }), ), }); const serialized = JSON.stringify(summary); expect(serialized).not.toContain("forbidden-identity-marker"); expect(serialized).not.toContain("forbidden-response-marker"); expect(serialized).not.toContain("synthetic-number"); expect(serialized).not.toContain("synthetic-seller"); expect(summary.fieldHits.invoiceNumber).toBe(true); expect(summary.fieldHits.sellerName).toBe(true); }); it.each([ ["non-pdf", Buffer.from("not a pdf", "ascii"), "sample_pdf_invalid"], ["truncated", Buffer.from("%PDF-1.7\nmissing eof", "ascii"), "sample_pdf_invalid"], ["oversized", Buffer.alloc(OCR_PDF_MAX_BYTES + 1, 0x20), "sample_too_large"], ])("rejects %s before provider invocation", async (_caseName, buffer, errorCode) => { const runOcr = vi.fn(); await expect( liveRunner.runLiveSampleCheck({ alias: "SAMPLE-PDF-TEST", buffer, config: providerConfig(), runOcr, }), ).rejects.toMatchObject({ code: errorCode }); expect(runOcr).not.toHaveBeenCalled(); }); it("consumes one stdin PDF and returns only a safe config error without credentials", () => { const result = spawnSync(process.execPath, [SCRIPT_PATH, "--alias=SAMPLE-PDF-STDIN"], { cwd: ROOT_DIR, env: { ...process.env, TENCENT_SECRET_ID: "", TENCENT_SECRET_KEY: "", }, input: syntheticPdf(), encoding: "utf8", }); const summary = JSON.parse(result.stdout.trim()); expect(result.status).toBe(1); expect(result.stderr).toBe(""); expect(summary).toEqual({ alias: "SAMPLE-PDF-STDIN", provider: "tencent", status: "HOLD", transportMode: "pdf_native", errorCode: "ocr_credentials_missing", }); }); });