文件
hyb-oa/scripts/qa/check-live-smoke-env.mjs

219 行
8.4 KiB
JavaScript
可执行文件

#!/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);
});