#!/usr/bin/env node import { existsSync, readdirSync, statSync } from "node:fs"; import { extname } from "node:path"; const baseUrl = (process.env.LIVE_SMOKE_BASE_URL || "https://oa.hyinda.com").trim().replace(/\/$/, ""); const expectedBuildShaInput = String(process.env.LIVE_SMOKE_EXPECTED_BUILD_SHA ?? "").trim(); function normalizeBuildSha(value) { const normalized = String(value ?? "").trim().toLowerCase(); if (normalized === "unknown") { return "unknown"; } return /^[0-9a-f]{7,64}$/.test(normalized) ? normalized.slice(0, 12) : null; } function buildShaMatches(expected, observed) { if (!expected || !observed || observed === "unknown") { return null; } return expected.startsWith(observed) || observed.startsWith(expected); } function present(key) { return Boolean(process.env[key]?.trim()); } function readinessMap(entries) { return Object.fromEntries(entries.map(([label, isReady]) => [label, Boolean(isReady)])); } function existingFileFrom(keys) { const values = keys.map((key) => process.env[key]?.trim()).filter(Boolean); return values.find((value) => existsSync(value)) || ""; } function isPdfFile(value) { if (!value || !existsSync(value)) { return false; } try { return statSync(value).isFile() && extname(value).toLowerCase() === ".pdf"; } catch { return false; } } function hasPdfFileInDir(value) { if (!value || !existsSync(value)) { return false; } try { return statSync(value).isDirectory() && readdirSync(value).some((entry) => extname(entry).toLowerCase() === ".pdf"); } catch { return false; } } async function fetchJson(url) { try { const response = await fetch(url, { signal: AbortSignal.timeout(10_000) }); const text = await response.text(); let json = null; try { json = text ? JSON.parse(text) : null; } catch { json = null; } return { ok: response.ok, status: response.status, json, text: text.slice(0, 300) }; } catch (error) { return { ok: false, status: 0, error: error instanceof Error ? error.message : String(error) }; } } function buildStaticChecks() { const adminCredentialReady = present("LIVE_SMOKE_ADMIN_IDENTIFIER") && present("LIVE_SMOKE_ADMIN_PASSWORD"); const employeeCredentialReady = present("LIVE_SMOKE_EMPLOYEE_IDENTIFIER") && present("LIVE_SMOKE_EMPLOYEE_PASSWORD"); const managerCredentialReady = present("LIVE_SMOKE_MANAGER_IDENTIFIER") && present("LIVE_SMOKE_MANAGER_PASSWORD"); const financeCredentialReady = present("LIVE_SMOKE_FINANCE_IDENTIFIER") && present("LIVE_SMOKE_FINANCE_PASSWORD"); const liveSmokeReady = adminCredentialReady && employeeCredentialReady; const ocrUserReady = ( (present("PLAYWRIGHT_OCR_LIVE_IDENTIFIER") && present("PLAYWRIGHT_OCR_LIVE_PASSWORD")) || (present("LIVE_SMOKE_EMPLOYEE_IDENTIFIER") && present("LIVE_SMOKE_EMPLOYEE_PASSWORD")) ); const invoiceFile = existingFileFrom(["PLAYWRIGHT_OCR_LIVE_FILE", "LIVE_OCR_INVOICE_FILE"]); const invoiceBatchDir = process.env.PLAYWRIGHT_OCR_LIVE_DIR?.trim() || ""; const invoiceFileReady = isPdfFile(invoiceFile); const invoiceBatchDirReady = hasPdfFileInDir(invoiceBatchDir); const invoiceSampleReady = invoiceFileReady || invoiceBatchDirReady; return { baseUrl, liveSmokeReady, liveSmoke: readinessMap([ ["adminCredentialReady", adminCredentialReady], ["employeeCredentialReady", employeeCredentialReady], ]), liveSmokeOptional: readinessMap([ ["expectedWecomStatusReady", present("LIVE_EXPECT_WECOM_STATUS")], ["managerCredentialReady", managerCredentialReady], ["financeCredentialReady", financeCredentialReady], ]), liveOcrReady: ocrUserReady && invoiceSampleReady, liveOcr: { userReady: ocrUserReady, invoiceSampleReady, invoiceFileProvided: Boolean(invoiceFile), invoiceFileReady, invoiceBatchDirProvided: Boolean(invoiceBatchDir), invoiceBatchDirReady, allowDuplicate: process.env.PLAYWRIGHT_OCR_ALLOW_DUPLICATE === "1", }, missing: [], }; } function addMissing(checks) { if (!checks.liveSmokeReady) { for (const [label, value] of Object.entries(checks.liveSmoke)) { if (!value) checks.missing.push(label); } } if (!checks.liveOcr.userReady) { checks.missing.push("ocrOrEmployeeCredentialReady"); } if (!checks.liveOcr.invoiceSampleReady) { checks.missing.push("invoiceSampleReady"); } } async function main() { const strict = process.argv.includes("--strict"); const jsonOnly = process.argv.includes("--json"); const checks = buildStaticChecks(); addMissing(checks); const ready = await fetchJson(`${baseUrl}/ready`); const wecomConfig = await fetchJson(`${baseUrl}/api/auth/wecom/config`); const authCapabilities = await fetchJson(`${baseUrl}/api/auth/capabilities`); const findings = checks.missing.map((code) => ({ code, severity: "manual_or_config" })); if (!ready.ok) findings.push({ code: "publicReady", severity: "blocker" }); const expectedBuildSha = normalizeBuildSha(expectedBuildShaInput); const observedBuildSha = normalizeBuildSha( ready.json?.data?.buildSha ?? ready.json?.error?.details?.buildSha, ); const buildMatches = buildShaMatches(expectedBuildSha, observedBuildSha); if (ready.ok && (!observedBuildSha || observedBuildSha === "unknown")) { findings.push({ code: "publicBuildShaUnavailable", severity: "manual_or_config" }); } else if (ready.ok && expectedBuildSha && buildMatches === false) { findings.push({ code: "publicBuildShaMismatch", severity: "manual_or_config" }); } if (expectedBuildShaInput && !expectedBuildSha) { findings.push({ code: "expectedBuildShaInvalid", severity: "manual_or_config" }); } if (!wecomConfig.ok) findings.push({ code: "wecomConfigReady", severity: "blocker" }); if (!authCapabilities.ok) findings.push({ code: "authCapabilitiesReady", severity: "blocker" }); const status = findings.some((finding) => finding.severity === "blocker") ? "FAIL" : findings.length ? "PASS_WITH_FINDINGS" : "PASS"; const result = { status, findingCount: findings.length, findings, checkedAt: new Date().toISOString(), ...checks, publicReady: { ok: ready.ok, status: ready.status, expectedBuildSha, observedBuildSha, buildShaMatches: buildMatches, }, wecomConfig: { ok: wecomConfig.ok, status: wecomConfig.status, statusText: wecomConfig.json?.data?.status ?? null, entryEnabled: wecomConfig.json?.data?.entryEnabled ?? null, oauthEnabled: wecomConfig.json?.data?.oauthEnabled ?? null, defaultReturnTo: wecomConfig.json?.data?.defaultReturnTo ?? null, loginUrlReady: Boolean(wecomConfig.json?.data?.loginUrl), }, authCapabilities: { ok: authCapabilities.ok, status: authCapabilities.status, emailDeliveryReady: authCapabilities.json?.data?.emailDelivery?.ready ?? null, emailDeliveryReasons: authCapabilities.json?.data?.emailDelivery?.statusReasons ?? null, emailLoginCodeEnabled: authCapabilities.json?.data?.emailLoginCode?.enabled ?? null, passwordResetEnabled: authCapabilities.json?.data?.passwordReset?.enabled ?? null, }, }; if (jsonOnly) { console.log(JSON.stringify(result, null, 2)); } else { console.log(`[live-smoke-env] status=${result.status} findings=${result.findingCount}`); console.log(`[live-smoke-env] base=${result.baseUrl}`); console.log(`[live-smoke-env] publicReady=${result.publicReady.ok} status=${result.publicReady.status}`); console.log(`[live-smoke-env] buildShaObserved=${result.publicReady.observedBuildSha ?? "unavailable"} match=${result.publicReady.buildShaMatches ?? "not_checked"}`); console.log(`[live-smoke-env] wecom=${result.wecomConfig.statusText ?? "unknown"} entry=${result.wecomConfig.entryEnabled} oauth=${result.wecomConfig.oauthEnabled}`); console.log(`[live-smoke-env] authCapabilities=${result.authCapabilities.ok} emailDelivery=${result.authCapabilities.emailDeliveryReady}`); console.log(`[live-smoke-env] liveSmokeReady=${result.liveSmokeReady}`); console.log(`[live-smoke-env] liveOcrReady=${result.liveOcrReady}`); if (result.missing.length) { console.log(`[live-smoke-env] missing=${result.missing.join(", ")}`); } } if (strict && result.status !== "PASS") { process.exit(1); } } main().catch((error) => { console.error(`[live-smoke-env][ERROR] ${error instanceof Error ? error.message : String(error)}`); process.exit(1); });