#!/usr/bin/env node import "dotenv/config"; import path from "node:path"; import { readdir } from "node:fs/promises"; import { existsSync } from "node:fs"; import { fileURLToPath, pathToFileURL } from "node:url"; import { log } from "../../packages/shared/dist/index.js"; const SMOKE_DIR = path.dirname(fileURLToPath(import.meta.url)); async function listGroupFiles() { const files = await readdir(SMOKE_DIR); return files.filter((f) => f.endsWith(".mjs") && f !== "run.mjs").sort(); } // 連續跑多個群組時,偶爾會在某個群組剛結束、下一個群組第一個請求時遇到暫時性的連線錯誤 // (observed:長時間跑(如 T 群組的重試/全域暫停測試)結束後,緊接著的下一個群組第一個 fetch // 偶發 "fetch failed"/連線被對方關閉——與程式邏輯無關,是連線在群組交接瞬間的暫時性狀態)。 // 每個群組檔案的既有慣例是「一進來就先 cleanup() 清掉自己上一輪可能留下的資料」,因此整個 // default() 重跑一次是安全、可重複的,不會因為半途失敗留下的殘留資料造成誤判。 const TRANSIENT_ERROR_PATTERN = /fetch failed|ECONNRESET|ECONNREFUSED|socket|other side closed|network/i; async function runOne(file, attempt = 1) { const group = file.replace(/\.mjs$/, ""); const modUrl = pathToFileURL(path.join(SMOKE_DIR, file)).href; log("冒煙測試", "INF", `開始執行 ${group} 群組冒煙測試${attempt > 1 ? `(第 ${attempt} 次嘗試)` : ""}`); try { const mod = await import(modUrl); if (typeof mod.default !== "function") { throw new Error(`${file} 未匯出預設測試函式`); } await mod.default(); log("冒煙測試", "INF", `${group} 群組冒煙測試全綠`); return true; } catch (err) { log("冒煙測試", "ERR", `${group} 群組冒煙測試失敗:${err.message}`); if (attempt === 1 && TRANSIENT_ERROR_PATTERN.test(err.message)) { log("冒煙測試", "WRN", `${group} 疑似暫時性連線錯誤,重試一次`); return runOne(file, attempt + 1); } return false; } } async function main() { const arg = process.argv[2]; if (!arg) { log("冒煙測試", "ERR", "請指定群組代號,例如:npm run smoke -- A(或 all 執行全部群組)"); process.exit(1); } let files; if (arg === "all") { files = await listGroupFiles(); if (files.length === 0) { log("冒煙測試", "WRN", "尚無任何冒煙測試檔案"); } } else { const file = `${arg}.mjs`; if (!existsSync(path.join(SMOKE_DIR, file))) { log("冒煙測試", "ERR", `找不到群組 ${arg} 的冒煙測試檔案(scripts/smoke/${file})`); process.exit(1); } files = [file]; } let allOk = true; for (const file of files) { const ok = await runOne(file); allOk = allOk && ok; } process.exit(allOk ? 0 : 1); } main();