const { execFile } = require("node:child_process"); const { mkdtemp, readFile, rm } = require("node:fs/promises"); const http = require("node:http"); const os = require("node:os"); const path = require("node:path"); const { promisify } = require("node:util"); const { pathToFileURL } = require("node:url"); const execFileAsync = promisify(execFile); const ROOT_DIR = path.resolve(__dirname, "../../../.."); let readinessReport; function passSteps() { return Object.fromEntries( readinessReport.READINESS_STEP_NAMES.map((name) => [name, { status: "PASS", kind: "required", findingCount: 0, logRef: `artifacts/qa-flow/test/${name}.log`, }]), ); } function listen(server) { return new Promise((resolve, reject) => { server.once("error", reject); server.listen(0, "127.0.0.1", () => { server.off("error", reject); resolve(); }); }); } function close(server) { return new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve()))); } const EXPECTED_BUILD_SHA = "abcdef123456"; describe("live smoke readiness aggregation", () => { beforeAll(async () => { const scriptPath = path.join(ROOT_DIR, "scripts/qa/live-smoke-readiness-report.mjs"); readinessReport = await import(pathToFileURL(scriptPath).href); }); it.each([ ["missing database", "pendingOcr"], ["missing login credentials", "liveSmoke"], ["missing OCR sample", "liveOcr"], ])("propagates %s as PASS_WITH_FINDINGS", (_label, stepName) => { const steps = passSteps(); steps[stepName] = { ...steps[stepName], status: "SKIP", findingCount: 1, }; const result = readinessReport.evaluateReadinessSteps(steps); expect(result.status).toBe("PASS_WITH_FINDINGS"); expect(result.findingCount).toBeGreaterThan(0); expect(result.findings).toEqual(expect.arrayContaining([ expect.objectContaining({ source: stepName, severity: "manual_or_config" }), ])); }); it("reports clean PASS only when every prerequisite and step is ready", () => { expect(readinessReport.evaluateReadinessSteps(passSteps())).toMatchObject({ status: "PASS", findingCount: 0, findings: [], }); }); it("treats a required command failure as a blocker", () => { const steps = passSteps(); steps.publicReady = { ...steps.publicReady, status: "FAIL", findingCount: 1 }; expect(readinessReport.evaluateReadinessSteps(steps)).toMatchObject({ status: "FAIL", findingCount: 1, }); }); it("runs the shell parent against a local public stub and writes a redacted non-clean summary", async () => { const reportDir = await mkdtemp(path.join(os.tmpdir(), "hyb-oa-readiness-test-")); const server = http.createServer((request, response) => { response.setHeader("content-type", "application/json; charset=utf-8"); if (request.url === "/ready") { response.end(JSON.stringify({ data: { ok: true, buildSha: EXPECTED_BUILD_SHA } })); return; } if (request.url === "/api/auth/wecom/config") { response.end(JSON.stringify({ data: { status: "ready", entryEnabled: true, oauthEnabled: true, defaultReturnTo: "/portal", loginUrl: "/api/auth/wecom/login", }, })); return; } if (request.url === "/api/auth/capabilities") { response.end(JSON.stringify({ data: { emailDelivery: { ready: true, statusReasons: [] }, emailLoginCode: { enabled: true }, passwordReset: { enabled: true }, }, })); return; } if (request.url === "/") { response.setHeader("content-type", "text/html; charset=utf-8"); response.end('OA'); return; } if (request.url === "/assets/index-test.js") { response.setHeader("content-type", "application/javascript; charset=utf-8"); response.end(`document.documentElement.dataset.buildSha="${EXPECTED_BUILD_SHA}";`); return; } response.statusCode = 404; response.end(JSON.stringify({ error: "not_found" })); }); try { await listen(server); const address = server.address(); const env = { ...process.env }; for (const key of [ "DATABASE_URL", "LIVE_SMOKE_ADMIN_IDENTIFIER", "LIVE_SMOKE_ADMIN_PASSWORD", "LIVE_SMOKE_EMPLOYEE_IDENTIFIER", "LIVE_SMOKE_EMPLOYEE_PASSWORD", "PLAYWRIGHT_OCR_LIVE_IDENTIFIER", "PLAYWRIGHT_OCR_LIVE_PASSWORD", "PLAYWRIGHT_OCR_LIVE_FILE", "PLAYWRIGHT_OCR_LIVE_DIR", "LIVE_OCR_INVOICE_FILE", ]) { delete env[key]; } env.LIVE_SMOKE_BASE_URL = `http://127.0.0.1:${address.port}`; env.LIVE_SMOKE_READINESS_DIR = reportDir; env.LIVE_SMOKE_EXPECTED_BUILD_SHA = EXPECTED_BUILD_SHA; env.QA_ISSUE_ID = "READINESS-TEST"; const result = await execFileAsync("bash", [path.join(ROOT_DIR, "scripts/qa/run-live-smoke-readiness.sh")], { cwd: ROOT_DIR, env, maxBuffer: 2 * 1024 * 1024, }); const summaryText = await readFile(path.join(reportDir, "summary.json"), "utf8"); const summary = JSON.parse(summaryText); expect(result.stdout).toContain("[live-readiness][PASS_WITH_FINDINGS]"); expect(summary.status).toBe("PASS_WITH_FINDINGS"); expect(summary.findingCount).toBeGreaterThan(0); expect(summary.steps.pendingOcr.status).toBe("SKIP"); expect(summary.steps.liveSmoke.status).toBe("SKIP"); expect(summary.steps.liveOcr.status).toBe("SKIP"); expect(summary.steps.privacy.status).toBe("PASS"); expect(summary.expectedBuildSha).toBe(EXPECTED_BUILD_SHA); expect(summary.observedBuildSha).toBe(EXPECTED_BUILD_SHA); expect(summary.buildShaMatches).toBe(true); expect(summary.childReports.pendingOcr.findingCount).toBeGreaterThan(0); expect(summaryText).not.toContain(ROOT_DIR); expect(summaryText).not.toMatch(/DATABASE_URL|LIVE_SMOKE_ADMIN_PASSWORD|PLAYWRIGHT_OCR_LIVE_FILE/); } finally { await close(server); await rm(reportDir, { recursive: true, force: true }); } }, 30_000); });