chore: add route smoke verification script
This commit is contained in:
parent
593b7e3df0
commit
e5224ff089
@ -54,6 +54,15 @@ curl -I --max-time 30 "http://127.0.0.1:3001/api/featured?page=1&pageSize=24"
|
||||
|
||||
## 路由回归烟测
|
||||
|
||||
推荐使用脚本化检查:
|
||||
|
||||
```bash
|
||||
AUTH_TRUST_HOST=true PORT=3001 pnpm dev
|
||||
SMOKE_BASE_URL=http://127.0.0.1:3001 pnpm smoke:routes
|
||||
```
|
||||
|
||||
手动检查等价命令:
|
||||
|
||||
```bash
|
||||
curl -I http://127.0.0.1:3001/
|
||||
curl -I http://127.0.0.1:3001/classic
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
"start": "next start",
|
||||
"lint": "npx ultracite@latest check",
|
||||
"check:logs": "node scripts/check-log-hygiene.mjs && tsx scripts/check-log-redaction.ts",
|
||||
"smoke:routes": "node scripts/smoke-routes.mjs",
|
||||
"format": "npx ultracite@latest fix",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:migrate": "npx tsx lib/db/migrate.ts",
|
||||
|
||||
124
scripts/smoke-routes.mjs
Normal file
124
scripts/smoke-routes.mjs
Normal file
@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const DEFAULT_BASE_URL = `http://127.0.0.1:${process.env.PORT || "3001"}`;
|
||||
const BASE_URL = stripTrailingSlash(
|
||||
process.env.SMOKE_BASE_URL || process.argv[2] || DEFAULT_BASE_URL
|
||||
);
|
||||
const REQUEST_TIMEOUT_MS = Number(process.env.SMOKE_TIMEOUT_MS || 20_000);
|
||||
|
||||
const checks = [
|
||||
{
|
||||
path: "/",
|
||||
method: "HEAD",
|
||||
expect: { status: [200] },
|
||||
},
|
||||
{
|
||||
path: "/classic",
|
||||
method: "HEAD",
|
||||
expect: { status: [307, 308], location: "/" },
|
||||
},
|
||||
{
|
||||
path: "/chat/test-id",
|
||||
method: "HEAD",
|
||||
expect: { status: [307, 308], location: "/" },
|
||||
},
|
||||
{
|
||||
path: "/settings",
|
||||
method: "HEAD",
|
||||
expect: {
|
||||
status: [307, 308],
|
||||
location: "/login?callbackUrl=%2Fsettings",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
let hasFailure = false;
|
||||
|
||||
for (const check of checks) {
|
||||
const result = await runCheck(check);
|
||||
if (!result.ok) {
|
||||
hasFailure = true;
|
||||
console.error(`FAIL ${check.method} ${check.path} - ${result.message}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`PASS ${check.method} ${check.path} - ${result.message}`);
|
||||
}
|
||||
|
||||
if (hasFailure) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
async function runCheck(check) {
|
||||
const startedAt = Date.now();
|
||||
const url = `${BASE_URL}${check.path}`;
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: check.method,
|
||||
redirect: "manual",
|
||||
headers: {
|
||||
"user-agent": "Codex Route Smoke Check",
|
||||
},
|
||||
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
||||
});
|
||||
const durationMs = Date.now() - startedAt;
|
||||
const location = response.headers.get("location");
|
||||
|
||||
if (!check.expect.status.includes(response.status)) {
|
||||
return {
|
||||
ok: false,
|
||||
message: `expected status ${check.expect.status.join(
|
||||
"/"
|
||||
)}, got ${response.status} in ${durationMs}ms`,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
check.expect.location &&
|
||||
!matchesLocation(location, check.expect.location)
|
||||
) {
|
||||
return {
|
||||
ok: false,
|
||||
message: `expected location ${check.expect.location}, got ${
|
||||
location || "<empty>"
|
||||
} in ${durationMs}ms`,
|
||||
};
|
||||
}
|
||||
|
||||
const redirectMessage = check.expect.location
|
||||
? `, location ${check.expect.location}`
|
||||
: "";
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
message: `status ${response.status}${redirectMessage} in ${durationMs}ms`,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function matchesLocation(actual, expected) {
|
||||
if (!actual) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (actual === expected) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
const actualUrl = new URL(actual, BASE_URL);
|
||||
return `${actualUrl.pathname}${actualUrl.search}` === expected;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function stripTrailingSlash(value) {
|
||||
return value.endsWith("/") ? value.slice(0, -1) : value;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user