feat: 完成 A 群組 — 專案骨架與可重啟的服務
- packages/shared:TypeScript 專案參考建置鏈路、統一日誌模組(Asia/Taipei 格式) - apps/api:最小 NestJS 服務,GET /health - apps/web:最小 Next.js(App Router + Tailwind v4)首頁,套用品牌色票與亮暗模式 - scripts/dev-restart.mjs:讀 .dev-pids 以 process group 訊號安全停止舊行程、背景啟動 api/web 並輪詢健康檢查 - scripts/smoke:npm run smoke -- <群組代號> 冒煙測試框架,A 群組測試涵蓋 /health 與首頁內容 - 本機 port 3000 被既有服務佔用,web 開發埠改為 3100(已記錄於 todo.md) npm run restart && npm run smoke -- A 皆通過(A-V)。
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
const API_PORT = process.env.PORT_API ?? "3001";
|
||||
const WEB_PORT = process.env.PORT_WEB ?? "3100";
|
||||
|
||||
export default async function smokeA() {
|
||||
const healthRes = await fetch(`http://localhost:${API_PORT}/health`);
|
||||
if (healthRes.status !== 200) {
|
||||
throw new Error(`GET /health 回傳 ${healthRes.status},預期 200`);
|
||||
}
|
||||
const healthBody = await healthRes.json();
|
||||
if (healthBody.status !== "ok") {
|
||||
throw new Error("GET /health 回應內容缺少 status: ok");
|
||||
}
|
||||
|
||||
const homeRes = await fetch(`http://localhost:${WEB_PORT}/`);
|
||||
if (homeRes.status !== 200) {
|
||||
throw new Error(`web 首頁回傳 ${homeRes.status},預期 200`);
|
||||
}
|
||||
const homeHtml = await homeRes.text();
|
||||
if (!homeHtml.includes("心音")) {
|
||||
throw new Error("web 首頁內容缺少「心音」字樣");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env node
|
||||
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();
|
||||
}
|
||||
|
||||
async function runOne(file) {
|
||||
const group = file.replace(/\.mjs$/, "");
|
||||
const modUrl = pathToFileURL(path.join(SMOKE_DIR, file)).href;
|
||||
log("冒煙測試", "INF", `開始執行 ${group} 群組冒煙測試`);
|
||||
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}`);
|
||||
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();
|
||||
Reference in New Issue
Block a user