Files
aurora-admin/tools/verify-dev-server.mjs
T
aurora-admin eb25feedaf feat(S6-P21): 组件族参数化(13族/44成员/导航族15文件同源/79→48概念组件)
【本次核心 · S6-P21】
- 族层数据:families.json + 44 份契约注入 family/familyRole/familyParams;
  data.json / data.js / site/details 同步。13 族 / 44 成员 / 35 独立 → 概念组件 79→48。
- 79 个 slug 全保留、集合逐一不变(铁律 5 对外承诺未破);frameworks 仍 395 文件、薄壳仍 79。
- 归族判据为契约中可核对字段(semanticTypeCandidates 重叠 / anatomy 为同一骨架子集 /
  变体维度同构 / doNotInvent 显式从属声明),每族 mergeBasis 写明依据,不按名字猜。
- 实现层合并(导航族端到端切片):tools/gen-family-impl.mjs 从 5 端模板生成
  TopMenu / SideMenu / MixedNavigation 共 15 文件,参数 direction=top|side|mixed;
  三份 CSS md5 完全相同 = 一份样式表服务三个组件。
- 新增 tools/gen-families.mjs、tools/gen-family-impl.mjs、tools/verify-families.mjs、
  tools/lib/family-model.mjs、tools/lib/family-impl/nav-menu/*.tpl。

【同时清掉此前已完成但未提交的批次】
生成物(data.json / data.js / site/sources / site/components 薄壳 / sitemap.xml / tests 报告)
跨阶段交织,无法拆成互相自洽的多个提交,故按既有批量风格合并提交:
- Package:三端可 import(S5-P18)+ 发布到私有 npm 源
- Docs site:导航语言改下拉(S5-P19)、详情页代码块默认展开、中英切换完整性
- Security:生产部署链审计修复(2026-09-19)+ 线上部署
- Theme modes 日间/夜间/自动;S1-P4 data.js 瘦身;S2-P5 暗色;S2-P6 跨端一致性;
  S2-P7 行为断言;S2-P9 FAQ;S3-P8 RTL;S3-P9 契约缺口解释层;S4-P12 发布流程
- 补入 tools/pack-deploy.mjs、run-site-smoke.mjs、verify-*.mjs,.dockerignore、
  安全审计修复与待决策项.md

【验收】
- node tools/verify-families.mjs → OK: 族层端到端一致(13 族 / 44 成员 / 79 组件不变 / 395 文件不变)
- node tools/verify-cross-platform.mjs → 79/79 identical(HEAD 基线 high 44)
- node tools/run-regression.mjs → 100%(79/79 页,1017/1017 断言,N/A 34),连跑 8 次一致,0 超时
- 逐页实测:topmenu / sidemenu / mixednavigation 各 13/13,帧内 direction 参数正确,0 JS 错误
- 零运行时依赖 OK;build-site.ps1 ASCII-only OK

【未纳入】site/components/<slug>/ 平台薄壳 316 个 —— 历史从未跟踪且属构建产物,保持现状。
2026-09-20 03:32:31 +08:00

171 lines
7.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* verify-dev-server.mjs — 静态服务的健壮性验证(零依赖,只用 node 内置模块)。
*
* 背景(实测缺陷 P1):
* 修复前 `site/dev-server.js` 的请求回调直接调用 fs.readFile(filePath, cb),
* 而 decodeURIComponent('%00') 会解出 NUL 字符;fs.readFile 对含 NUL 的路径**同步**
* 抛 TypeError [ERR_INVALID_ARG_VALUE],异常逃逸出请求回调 → 整个进程退出。
* 即:单个畸形请求 = 一次 DoS。
*
* 反例断言(修复前必然失败,用来区分修复前后):
* [COUNTEREXAMPLE] MALFORMED_BURST_SERVER_ALIVE
* —— 发完 %00 / 穿越 / 双重编码 NUL 请求后,子进程仍存活(exitCode === null),
* 且再发一次正常请求仍为 200;stderr 不含 ERR_INVALID_ARG_VALUE。
* 修复前的真实表现(本机实测,修复前工作树副本):
* GET /site/%00 -> 客户端 ECONNRESET
* child.exitCode -> 1
* 其后 GET /site/ -> ECONNREFUSED
* stderr: TypeError [ERR_INVALID_ARG_VALUE]: The argument 'path' must be a string,
* Uint8Array, or URL without null bytes.
* at Object.readFile (node:fs:385:16)
* at Server.<anonymous> (.../site/dev-server.js:106:6)
*
* 端口:默认用 13311,避免与正在运行的 3311 实例抢端口;可用 AA_PORT 覆盖。
* 验证脚本把 AA_PORT 传给子进程(这也是 dev-server.js 新增的唯一开关)。
*
* 运行:node tools/verify-dev-server.mjs
* 退出码:0 = 全部通过;1 = 有断言失败 / 服务起不来。
*/
import { spawn } from 'node:child_process';
import { request } from 'node:http';
import { join } from 'node:path';
const ROOT = join(import.meta.dirname, '..');
const HOST = '127.0.0.1';
const PORT = Number(process.env.AA_PORT) || 13311;
const BASELINE_PORT = 13311;
let failed = 0;
let passed = 0;
const results = [];
function check(name, ok, detail = '') {
results.push({ name, ok, detail });
if (ok) passed++;
else failed++;
}
function get(path) {
return new Promise((resolve) => {
const req = request({ host: HOST, port: PORT, path, method: 'GET' }, (res) => {
let body = '';
res.setEncoding('utf8');
res.on('data', (chunk) => { body += chunk; });
res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body, error: null }));
});
req.on('error', (e) => resolve({ status: 0, headers: {}, body: '', error: e.code || e.message }));
req.setTimeout(5000, () => { req.destroy(); resolve({ status: 0, headers: {}, body: '', error: 'TIMEOUT' }); });
req.end();
});
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
console.log(`[dev-server] 启动子进程 node site/dev-server.js (AA_PORT=${PORT}, HOST=${HOST})`);
const child = spawn(process.execPath, [join('site', 'dev-server.js')], {
cwd: ROOT,
env: { ...process.env, AA_PORT: String(PORT) },
stdio: ['ignore', 'pipe', 'pipe']
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (d) => { stdout += d; });
child.stderr.on('data', (d) => { stderr += d; });
const watchdog = setTimeout(() => {
console.error('[dev-server] 看门狗超时(20s),强制收尾');
try { child.kill(); } catch { /* 已退出 */ }
process.exit(1);
}, 20000);
/* ---------- 1. 等待就绪 ---------- */
let ready = null;
for (let i = 0; i < 40; i++) {
if (child.exitCode !== null) break;
const r = await get('/site/');
if (r.status === 200) { ready = r; break; }
await sleep(250);
}
check('server starts and answers GET /site/ with 200', ready !== null,
ready ? `status=${ready.status}` : `子进程未就绪 exitCode=${child.exitCode} stderr=${JSON.stringify(stderr.slice(0, 200))}`);
check('AA_PORT honored in startup banner', stdout.includes(`:${PORT}/site/`),
`banner=${JSON.stringify(stdout.trim().split('\n')[0] || '')}`);
if (ready) {
check('GET /site/ serves the real site document', /<!DOCTYPE html>/i.test(ready.body) && ready.body.length > 200,
`bytes=${ready.body.length}`);
check('security headers kept (CSP + nosniff)', /default-src 'self'/.test(ready.headers['content-security-policy'] || '') &&
ready.headers['x-content-type-options'] === 'nosniff');
}
/* ---------- 2. 既有行为回归(不得被本次修复改动) ---------- */
const rootRedirect = await get('/');
check('GET / -> 302 to /site/', rootRedirect.status === 302 && rootRedirect.headers.location === '/site/',
`status=${rootRedirect.status} location=${rootRedirect.headers.location}`);
const indexHtml = await get('/site/index.html');
check('GET /site/index.html -> 200 text/html', indexHtml.status === 200 &&
/text\/html/.test(indexHtml.headers['content-type'] || ''), `status=${indexHtml.status}`);
const appJs = await get('/site/app.js');
check('GET /site/app.js -> 200 text/javascript', appJs.status === 200 &&
/text\/javascript/.test(appJs.headers['content-type'] || ''), `status=${appJs.status}`);
const missing = await get('/site/definitely-missing-xyz.html');
check('missing file -> 404 with unchanged wording', missing.status === 404 &&
missing.body === '404 Not Found: /site/definitely-missing-xyz.html', `status=${missing.status}`);
/* ---------- 3. 畸形请求矩阵(全部必须 4xx,且不得打挂进程) ---------- */
const malformed = [
['/site/%00', 'NUL 编码'],
['/site/%2500', '双重编码 NUL(%2500)'],
['/site/%00.html', 'NUL 编码 + 后缀'],
['/site/sub/%2500', '子目录下的双重编码 NUL'],
['/site/../package.json', '相对穿越'],
['/..%2fpackage.json', '编码穿越 %2f'],
['/site/%2e%2e/package.json', '编码穿越 %2e'],
['/site/.git/config', '敏感段 .git']
];
const malformedSeen = [];
for (const [path, label] of malformed) {
const r = await get(path);
malformedSeen.push(`${path}(${label}) -> ${r.status || r.error}`);
check(`malformed ${path} (${label}) -> 4xx`,
r.status >= 400 && r.status < 500, `status=${r.status || r.error}`);
}
/* ---------- 4. 反例断言:修复前必然失败 ---------- */
await sleep(300);
const aliveExitCode = child.exitCode;
const afterBurst = await get('/site/');
check('[COUNTEREXAMPLE] MALFORMED_BURST_SERVER_ALIVE — 畸形请求突发后进程仍存活',
aliveExitCode === null, `child.exitCode=${aliveExitCode}(修复前为 1)`);
check('[COUNTEREXAMPLE] MALFORMED_BURST_STILL_SERVING — 突发后正常请求仍 200',
afterBurst.status === 200, `status=${afterBurst.status || afterBurst.error}(修复前为 ECONNREFUSED)`);
check('[COUNTEREXAMPLE] NO_FATAL_STDERR — stderr 不含 ERR_INVALID_ARG_VALUE 崩溃栈',
!/ERR_INVALID_ARG_VALUE/.test(stderr), `stderr=${JSON.stringify(stderr.slice(0, 160))}`);
check('repeated NUL requests stay handled (10x)', await (async () => {
for (let i = 0; i < 10; i++) {
const r = await get('/site/%00');
if (!(r.status >= 400 && r.status < 500)) return false;
}
const last = await get('/site/');
return last.status === 200 && child.exitCode === null;
})(), '10 次 %00 后仍存活且仍 4xx');
/* ---------- 收尾 ---------- */
try { child.kill(); } catch { /* 已退出 */ }
clearTimeout(watchdog);
console.log('');
console.log(`[dev-server] 端口 ${PORT}${PORT === BASELINE_PORT ? '(默认验证端口,未占用 3311)' : '(AA_PORT 覆盖)'}`);
console.log(`[dev-server] 畸形请求实测:${malformedSeen.join(' | ')}`);
for (const r of results) {
console.log(`[dev-server] ${r.ok ? 'OK' : 'FAIL'} ${r.name}${r.detail ? ` — ${r.detail}` : ''}`);
}
if (failed) {
console.error(`\n[dev-server] ${failed} check(s) failed / ${passed} passed`);
process.exit(1);
}
console.log(`\n[dev-server] OK — ${passed} checks passed(含 3 条修复前必失败的反例断言)`);
process.exit(0);