Files
Genarrative/scripts/check-pingora-cutover-evidence-bundle.mjs
T
kdletters e9c3dc1120 退役旧创作模板业务
保留 SpacetimeDB 历史表、迁移白名单与旧业务源码
切换前端 active 入口并解除旧创作页面和路由编译链
移除旧后端路由、worker 与纯业务 crate 依赖
收敛 SpacetimeDB 模块为历史数据壳
同步 Nginx、Pingora、验证门禁与架构文档
2026-07-17 22:07:52 +08:00

2013 lines
62 KiB
JavaScript

#!/usr/bin/env node
import { spawnSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import {
chmodSync,
existsSync,
mkdirSync,
mkdtempSync,
readdirSync,
readFileSync,
rmSync,
statSync,
symlinkSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
const BUNDLE_SCRIPT = 'scripts/ops/pingora-cutover-evidence-bundle.mjs';
const failures = [];
const tmpRoot = mkdtempSync(
path.join(tmpdir(), 'genarrative-pingora-cutover-evidence-'),
);
try {
main();
} finally {
rmSync(tmpRoot, { recursive: true, force: true });
}
if (failures.length > 0) {
console.error('[check:pingora-cutover-evidence-bundle] FAILED');
for (const failure of failures) {
console.error(`- ${failure}`);
}
process.exit(1);
}
console.log('[check:pingora-cutover-evidence-bundle] OK');
function main() {
assertScriptShape();
assertBundleSucceedsAndWritesManifest();
assertBundleSummarizesPingoraEnvShadow();
assertBundleSummarizesDirectPingoraEnvAsNotShadow();
assertBundleWritesCutoverRunId();
assertBundleCanArchiveDirectLiveEvidence();
assertDirectLiveFailureStillWritesEvidenceAndFails();
assertDirectLiveMissingAccessLogSummaryFails();
assertDirectLiveMissingStaticHeadersSummaryFails();
assertDirectLiveStaticHeaderDiagnosticsFails();
assertDirectLiveParseFailureWritesParseErrorEvidence();
assertBundleRedactsProbeTokensFromArtifacts();
assertCriticalSnapshotStillWritesEvidenceAndFails();
assertSnapshotParseFailureWritesParseErrorEvidence();
assertRejectsUnsafePhase();
assertRejectsUnsafeCutoverRunId();
assertRejectsSnapshotArgsWithControlCharacters();
assertRejectsDirectLiveArgsWithControlCharacters();
assertRejectsRelativePaths();
assertRejectsFilesystemRootPaths();
assertRejectsSymlinkOutputRootBeforeSnapshot();
assertRejectsSymlinkOutputRootParentBeforeSnapshot();
assertRejectsFileOutputRootBeforeSnapshot();
assertRejectsInvalidTimeout();
assertRejectsInvalidBoolEnv();
}
function assertScriptShape() {
const content = readFileSync(BUNDLE_SCRIPT, 'utf8');
assertIncludes(
content,
'只写 --output-root 下的新证据目录',
'证据包脚本 usage 必须说明只写证据目录。',
);
assertIncludes(
content,
'BUNDLE_DIR_MODE = 0o750',
'证据包脚本必须显式固定新证据目录权限。',
);
assertIncludes(
content,
'EVIDENCE_FILE_MODE = 0o640',
'证据包脚本必须显式固定证据文件权限。',
);
assertIncludes(
content,
"flag: 'wx'",
'证据包脚本写证据文件时必须避免覆盖既有文件。',
);
assertIncludes(
content,
'validatePhase(config.phase);',
'证据包脚本必须校验 phase,避免目录名和 manifest 阶段漂移。',
);
assertIncludes(
content,
'cutoverRunId',
'证据包脚本必须支持写入切换批次 ID。',
);
assertIncludes(
content,
'--cutover-run-id',
'证据包脚本 usage 必须公开切换批次 ID 参数。',
);
assertIncludes(
content,
'validateOutputRootForWriting(outputRoot);',
'证据包脚本写入前必须校验 output-root 路径安全。',
);
assertIncludes(
content,
'--output-root 不能是符号链接',
'证据包脚本必须拒绝符号链接 output-root。',
);
assertIncludes(
content,
'--output-root 已存在但不是目录',
'证据包脚本必须拒绝非目录 output-root。',
);
assertIncludes(
content,
'snapshot.stdout.txt',
'证据包必须保存快照 stdout。',
);
assertIncludes(
content,
'direct-live.stdout.txt',
'证据包必须能保存 direct live stdout。',
);
assertIncludes(
content,
'parseDirectLiveJson',
'证据包必须能从 direct live stdout 中提取 JSON 结果。',
);
assertIncludes(
content,
'pingoraEnvShadow',
'证据包 manifest 必须提升 Pingora env shadow 摘要。',
);
assertIncludes(
content,
'--run-direct-live',
'证据包必须暴露 direct live 归档开关。',
);
assertIncludes(
content,
'manifest.json',
'证据包必须写 manifest。',
);
assertIncludes(
content,
'SECRET_VALUE_FLAGS',
'证据包命令记录必须集中维护敏感参数脱敏列表。',
);
assertIncludes(
content,
'args: redactSecretArgs',
'证据包命令记录必须保存结构化脱敏参数数组。',
);
assertIncludes(
content,
'<redacted>',
'证据包命令记录必须用 <redacted> 隐藏敏感参数值。',
);
if (
content.includes("execFile('systemctl'") ||
content.includes('execFile("systemctl"') ||
content.includes('nginx -s reload')
) {
failures.push('证据包脚本不应直接操作 systemd 或 Nginx。');
}
}
function assertBundleSucceedsAndWritesManifest() {
const fixture = prepareFixture('ok');
const result = runBundle(fixture, {
status: 'OK',
extraArgs: [
'--phase',
'post-enable',
'--expected-gateway-mode',
'pingora-direct',
'--expected-public-base-url',
'https://127.0.0.1',
'--expected-public-host',
'example.com',
'--run-health-patrol',
'--require-pingora-gateway',
'--fail-on-critical',
],
});
assertStatus(result, 0, 'OK 快照应生成证据包并成功退出。');
if (result.status !== 0) {
return;
}
const output = parseJson(result.stdout, 'OK 证据包 stdout');
assertEqual(output.phase, 'post-enable', '证据包 stdout 必须记录阶段。');
assertEqual(output.status, 'OK', '证据包 stdout 必须记录 OK 状态。');
assertFileExists(output.bundleDir, '证据包目录必须存在。');
const manifest = readJson(path.join(output.bundleDir, 'manifest.json'));
assertEqual(manifest.phase, 'post-enable', 'manifest 必须记录阶段。');
assertEqual(manifest.summary.status, 'OK', 'manifest 必须记录快照状态。');
assertEqual(
manifest.summary.pingoraEnvShadow?.ok,
true,
'manifest 必须记录 Pingora env 已处于 shadow 高端口。',
);
assertEqual(
manifest.commands?.[0]?.exitCode,
0,
'manifest 必须记录快照命令退出码。',
);
assertIncludes(
manifest.commands?.[0]?.command || '',
'--run-health-patrol',
'manifest 命令必须记录快照执行参数。',
);
assertIncludes(
manifest.commands?.[0]?.command || '',
'--require-pingora-gateway',
'manifest 命令必须记录 Pingora 发布物自审要求。',
);
assertEqual(
manifest.commands?.[0]?.executable,
'node',
'manifest 命令记录必须保存可执行文件。',
);
assertIncludes(
manifest.commands?.[0]?.args || [],
'--require-pingora-gateway',
'manifest 命令记录必须保存结构化参数数组。',
);
assertManifestFileMetadata(
output.bundleDir,
manifest.files?.snapshot,
'snapshot.json',
);
assertManifestFileMetadata(
output.bundleDir,
manifest.files?.snapshotStdout,
'snapshot.stdout.txt',
);
assertManifestFileMetadata(
output.bundleDir,
manifest.files?.snapshotStderr,
'snapshot.stderr.txt',
);
assertManifestFileMetadata(
output.bundleDir,
manifest.files?.snapshotCommand,
'snapshot-command.json',
);
assertFileExists(
path.join(output.bundleDir, 'snapshot.json'),
'证据包必须保存解析后的 snapshot.json。',
);
assertFileExists(
path.join(output.bundleDir, 'snapshot.stdout.txt'),
'证据包必须保存 snapshot stdout。',
);
assertFileExists(
path.join(output.bundleDir, 'snapshot.stderr.txt'),
'证据包必须保存 snapshot stderr。',
);
assertFileExists(
path.join(output.bundleDir, 'snapshot-command.json'),
'证据包必须保存快照命令记录。',
);
assertMode(output.bundleDir, 0o750, '证据包目录权限必须是 0750。');
for (const fileName of [
'manifest.json',
'snapshot.json',
'snapshot.stdout.txt',
'snapshot.stderr.txt',
'snapshot-command.json',
]) {
assertMode(
path.join(output.bundleDir, fileName),
0o640,
`${fileName} 权限必须是 0640。`,
);
}
const snapshot = readJson(path.join(output.bundleDir, 'snapshot.json'));
assertIncludes(
snapshot.args,
'--expected-gateway-mode',
'证据包必须把 expected gateway mode 透传给状态快照。',
);
assertIncludes(
snapshot.args,
'--require-pingora-gateway',
'证据包必须把 Pingora 发布物自审要求透传给状态快照。',
);
assertFileUnchanged(
fixture.healthEnvFile,
fixture.originalHealthEnvText,
'证据包不应改写 health patrol env。',
);
const commandRecord = readJson(path.join(output.bundleDir, 'snapshot-command.json'));
assertEqual(
commandRecord.executable,
'node',
'snapshot-command.json 必须保存可执行文件。',
);
assertIncludes(
commandRecord.args || [],
'--expected-gateway-mode',
'snapshot-command.json 必须保存结构化参数数组。',
);
}
function assertBundleSummarizesPingoraEnvShadow() {
const fixture = prepareFixture('pingora-env-shadow-summary');
const result = runBundle(fixture, {
status: 'OK',
extraArgs: ['--phase', 'post-rollback', '--fail-on-critical'],
});
assertStatus(result, 0, 'shadow env 快照应生成 OK 摘要。');
if (result.status !== 0) {
return;
}
const output = parseJson(result.stdout, 'shadow env 证据包 stdout');
const manifest = readJson(path.join(output.bundleDir, 'manifest.json'));
assertEqual(
manifest.summary.pingoraEnvShadow?.present,
true,
'manifest.summary.pingoraEnvShadow 必须标记 env 摘要存在。',
);
assertEqual(
manifest.summary.pingoraEnvShadow?.listen,
'127.0.0.1:18081',
'manifest.summary.pingoraEnvShadow 必须记录 shadow listen。',
);
assertEqual(
manifest.summary.pingoraEnvShadow?.tlsListen,
'',
'manifest.summary.pingoraEnvShadow 必须记录 TLS 低端口为空。',
);
assertEqual(
manifest.summary.pingoraEnvShadow?.httpRedirectListen,
'',
'manifest.summary.pingoraEnvShadow 必须记录 HTTP redirect 低端口为空。',
);
assertEqual(
manifest.summary.pingoraEnvShadow?.tlsCertFile,
'',
'manifest.summary.pingoraEnvShadow 必须记录 TLS cert 路径为空。',
);
assertEqual(
manifest.summary.pingoraEnvShadow?.tlsKeyFile,
'',
'manifest.summary.pingoraEnvShadow 必须记录 TLS key 路径为空。',
);
assertEqual(
manifest.summary.pingoraEnvShadow?.mode,
'shadow',
'manifest.summary.pingoraEnvShadow 必须记录 snapshot 判定的 shadow 姿态。',
);
assertEqual(
manifest.summary.pingoraEnvShadow?.shadowReady,
true,
'manifest.summary.pingoraEnvShadow 必须记录 shadowReady=true。',
);
assertEqual(
manifest.summary.pingoraEnvShadow?.ok,
true,
'manifest.summary.pingoraEnvShadow.ok 必须为 true。',
);
}
function assertBundleSummarizesDirectPingoraEnvAsNotShadow() {
const fixture = prepareFixture('pingora-env-direct-summary', {
snapshotPingoraEnvValues: {
listen: '127.0.0.1:18081',
tlsListen: '0.0.0.0:443',
httpRedirectListen: '0.0.0.0:80',
tlsCertFile: '/etc/genarrative/pingora-tls/example/fullchain.pem',
tlsKeyFile: '/etc/genarrative/pingora-tls/example/privkey.pem',
},
});
const result = runBundle(fixture, {
status: 'OK',
extraArgs: ['--phase', 'post-enable', '--fail-on-critical'],
});
assertStatus(
result,
0,
'direct env 快照默认只生成摘要,不应让证据包本身失败。',
);
if (result.status !== 0) {
return;
}
const output = parseJson(result.stdout, 'direct env 证据包 stdout');
const manifest = readJson(path.join(output.bundleDir, 'manifest.json'));
assertEqual(
manifest.summary.pingoraEnvShadow?.ok,
false,
'direct 低端口 env 摘要必须标记 ok=false。',
);
assertIncludes(
manifest.summary.pingoraEnvShadow?.diagnostics || [],
'tlsListen 应为空,实际 0.0.0.0:443',
'direct 低端口 env 摘要必须记录 TLS 低端口诊断。',
);
assertIncludes(
manifest.summary.pingoraEnvShadow?.diagnostics || [],
'httpRedirectListen 应为空,实际 0.0.0.0:80',
'direct 低端口 env 摘要必须记录 HTTP redirect 低端口诊断。',
);
assertIncludes(
manifest.summary.pingoraEnvShadow?.diagnostics || [],
'tlsCertFile 应为空,实际 /etc/genarrative/pingora-tls/example/fullchain.pem',
'direct 低端口 env 摘要必须记录 TLS cert 残留诊断。',
);
assertEqual(
manifest.summary.pingoraEnvShadow?.mode,
'direct',
'direct 低端口 env 摘要必须记录 snapshot 判定的 direct 姿态。',
);
}
function assertBundleWritesCutoverRunId() {
const fixture = prepareFixture('cutover-run-id');
const result = runBundle(fixture, {
status: 'OK',
extraArgs: [
'--phase',
'pre-cutover',
'--cutover-run-id',
'cutover-20260617T010000Z',
'--fail-on-critical',
],
});
assertStatus(result, 0, '提供 cutover run id 时证据包应成功退出。');
if (result.status !== 0) {
return;
}
const output = parseJson(result.stdout, 'cutover run id 证据包 stdout');
assertEqual(
output.cutoverRunId,
'cutover-20260617T010000Z',
'证据包 stdout 必须返回 cutoverRunId。',
);
const manifest = readJson(path.join(output.bundleDir, 'manifest.json'));
assertEqual(
manifest.cutoverRunId,
'cutover-20260617T010000Z',
'manifest 必须记录 cutoverRunId。',
);
}
function assertBundleCanArchiveDirectLiveEvidence() {
const fixture = prepareFixture('direct-live-ok', {
directLiveStatus: 'OK',
});
const result = runBundle(fixture, {
status: 'OK',
extraArgs: directLiveArgs(fixture, [
'--phase',
'post-enable',
'--expected-gateway-mode',
'pingora-direct',
'--expected-public-base-url',
'https://127.0.0.1',
'--expected-public-host',
'example.com',
'--run-health-patrol',
'--require-pingora-gateway',
'--fail-on-critical',
]),
});
assertStatus(result, 0, 'direct live 成功时证据包应成功退出。');
if (result.status !== 0) {
return;
}
const output = parseJson(result.stdout, 'direct live 证据包 stdout');
assertFileExists(output.directLivePath, '证据包 stdout 必须返回 direct live JSON 路径。');
const manifest = readJson(path.join(output.bundleDir, 'manifest.json'));
assertEqual(
manifest.summary.directLiveStatus,
'OK',
'manifest 必须记录 direct live 状态。',
);
assertEqual(
manifest.summary.directLiveAccessLog?.checked,
2,
'manifest 必须记录 direct live access log 检查请求数。',
);
assertEqual(
manifest.summary.directLiveAccessLog?.matchedCount,
2,
'manifest 必须记录 direct live access log 匹配数量。',
);
assertEqual(
manifest.summary.directLiveAccessLog?.missingCount,
0,
'manifest 必须记录 direct live access log 缺失数量。',
);
assertEqual(
manifest.summary.directLiveAccessLog?.mismatchCount,
0,
'manifest 必须记录 direct live access log 漂移数量。',
);
assertEqual(
manifest.summary.directLiveStaticHeaders?.normal?.cacheControl,
'no-cache',
'manifest 必须提升普通静态资源 Cache-Control 证据。',
);
assertEqual(
manifest.summary.directLiveStaticHeaders?.normal?.rangeContentRange,
'bytes 0-0/27',
'manifest 必须提升普通静态资源 Content-Range 证据。',
);
assertEqual(
manifest.summary.directLiveStaticHeaders?.normal?.etag304StatusCode,
304,
'manifest 必须提升普通静态资源 ETag 304 证据。',
);
assertEqual(
manifest.summary.directLiveStaticHeaders?.normal?.lastModified304StatusCode,
304,
'manifest 必须提升普通静态资源 Last-Modified 304 证据。',
);
assertEqual(
manifest.summary.directLiveStaticHeaders?.fingerprinted?.cacheControl,
'public, max-age=31536000, immutable',
'manifest 必须提升指纹静态资源 Cache-Control 证据。',
);
assertEqual(
manifest.summary.directLiveStaticHeaders?.fingerprinted?.rangeContentRange,
'bytes 0-0/41',
'manifest 必须提升指纹静态资源 Content-Range 证据。',
);
assertEqual(
manifest.summary.directLiveStaticHeaders?.fingerprinted?.etag304StatusCode,
304,
'manifest 必须提升指纹静态资源 ETag 304 证据。',
);
assertEqual(
manifest.summary.directLiveStaticHeaders?.fingerprinted
?.lastModified304StatusCode,
304,
'manifest 必须提升指纹静态资源 Last-Modified 304 证据。',
);
assertEqual(
manifest.commands?.[1]?.name,
'pingora-direct-live',
'manifest 必须记录 direct live 命令。',
);
assertIncludes(
manifest.commands?.[1]?.args || [],
'--pingora-access-log',
'direct live 命令记录必须保留 access log 参数。',
);
assertIncludes(
manifest.commands?.[1]?.command || '',
'--require-wss-upgrade',
'direct live 命令必须强制 WSS 101。',
);
assertManifestFileMetadata(
output.bundleDir,
manifest.files?.directLive,
'direct-live.json',
);
assertManifestFileMetadata(
output.bundleDir,
manifest.files?.directLiveStdout,
'direct-live.stdout.txt',
);
assertManifestFileMetadata(
output.bundleDir,
manifest.files?.directLiveStderr,
'direct-live.stderr.txt',
);
assertManifestFileMetadata(
output.bundleDir,
manifest.files?.directLiveCommand,
'direct-live-command.json',
);
assertFileExists(
path.join(output.bundleDir, 'direct-live.json'),
'证据包必须保存 direct-live.json。',
);
assertFileExists(
path.join(output.bundleDir, 'direct-live.stdout.txt'),
'证据包必须保存 direct live stdout。',
);
assertFileExists(
path.join(output.bundleDir, 'direct-live.stderr.txt'),
'证据包必须保存 direct live stderr。',
);
assertFileExists(
path.join(output.bundleDir, 'direct-live-command.json'),
'证据包必须保存 direct live 命令记录。',
);
const directLive = readJson(path.join(output.bundleDir, 'direct-live.json'));
assertEqual(directLive.ok, true, 'direct-live.json 必须记录 ok=true。');
const staticAssetCheck = directLive.results?.find(
(check) => check.name === 'https-static-asset',
);
if (!staticAssetCheck) {
failures.push('direct-live.json 必须保留静态资源检查结果。');
} else {
assertEqual(
staticAssetCheck.headers?.['cache-control'],
'no-cache',
'direct-live.json 必须保留普通静态资源 Cache-Control 证据。',
);
assertEqual(
staticAssetCheck.range?.headers?.['content-range'],
'bytes 0-0/27',
'direct-live.json 必须保留普通静态资源 Content-Range 证据。',
);
assertEqual(
staticAssetCheck.notModified?.etag?.statusCode,
304,
'direct-live.json 必须保留普通静态资源 ETag 304 证据。',
);
assertEqual(
staticAssetCheck.notModified?.lastModified?.statusCode,
304,
'direct-live.json 必须保留普通静态资源 Last-Modified 304 证据。',
);
assertEqual(
staticAssetCheck.fingerprinted?.headers?.['cache-control'],
'public, max-age=31536000, immutable',
'direct-live.json 必须保留指纹静态资源 Cache-Control 证据。',
);
assertEqual(
staticAssetCheck.fingerprinted?.range?.headers?.['content-range'],
'bytes 0-0/41',
'direct-live.json 必须保留指纹静态资源 Content-Range 证据。',
);
assertEqual(
staticAssetCheck.fingerprinted?.notModified?.etag?.statusCode,
304,
'direct-live.json 必须保留指纹静态资源 ETag 304 证据。',
);
assertEqual(
staticAssetCheck.fingerprinted?.notModified?.lastModified?.statusCode,
304,
'direct-live.json 必须保留指纹静态资源 Last-Modified 304 证据。',
);
}
const accessLogCheck = directLive.results?.find(
(check) => check.name === 'direct-access-log',
);
if (!accessLogCheck) {
failures.push('direct-live.json 必须保留 direct-access-log 检查结果。');
} else {
assertEqual(
accessLogCheck.scannedLineCount,
1,
'direct-access-log 检查必须记录扫描日志行数。',
);
assertEqual(
accessLogCheck.matchedCount,
2,
'direct-access-log 检查必须记录 matchedCount。',
);
assertEqual(
accessLogCheck.missingCount,
0,
'direct-access-log 检查必须记录 missingCount=0。',
);
assertEqual(
accessLogCheck.mismatchCount,
0,
'direct-access-log 检查必须记录 mismatchCount=0。',
);
assertEqual(
accessLogCheck.matched?.[0]?.expectedMethod,
'GET',
'direct-access-log 检查必须保留已匹配 request_id 的预期方法。',
);
assertEqual(
accessLogCheck.matched?.[0]?.actualMethod,
'GET',
'direct-access-log 检查必须保留已匹配 request_id 的实际方法。',
);
assertEqual(
accessLogCheck.matched?.[0]?.expectedStatusCode,
200,
'direct-access-log 检查必须保留已匹配 request_id 的预期状态码。',
);
assertEqual(
accessLogCheck.matched?.[0]?.actualStatusCode,
200,
'direct-access-log 检查必须保留已匹配 request_id 的实际状态码。',
);
assertEqual(
Array.isArray(accessLogCheck.missing),
true,
'direct-access-log 检查必须保留 missing 明细数组。',
);
assertEqual(
Array.isArray(accessLogCheck.mismatches),
true,
'direct-access-log 检查必须保留 mismatches 明细数组。',
);
}
}
function assertDirectLiveFailureStillWritesEvidenceAndFails() {
const fixture = prepareFixture('direct-live-critical', {
directLiveStatus: 'CRITICAL',
});
const result = runBundle(fixture, {
status: 'OK',
extraArgs: directLiveArgs(fixture, [
'--phase',
'post-enable',
'--expected-gateway-mode',
'pingora-direct',
'--expected-public-base-url',
'https://127.0.0.1',
'--expected-public-host',
'example.com',
'--fail-on-critical',
]),
});
assertStatus(result, 1, 'direct live 失败时证据包应失败。');
const output = parseJson(result.stdout, 'direct live 失败证据包 stdout');
assertEqual(output.status, 'CRITICAL', 'direct live 失败时 stdout 必须记录 CRITICAL。');
assertFileExists(output.bundleDir, 'direct live 失败证据包目录必须存在。');
if (!output.bundleDir) {
return;
}
const manifest = readJson(path.join(output.bundleDir, 'manifest.json'));
assertEqual(
manifest.summary.directLiveStatus,
'CRITICAL',
'direct live 失败时 manifest 必须记录 CRITICAL。',
);
assertEqual(
manifest.summary.directLiveExitCode,
1,
'direct live 失败时 manifest 必须记录退出码。',
);
assertFileExists(
path.join(output.bundleDir, 'direct-live.stdout.txt'),
'direct live 失败时仍必须保存 stdout。',
);
assertFileExists(
path.join(output.bundleDir, 'direct-live.stderr.txt'),
'direct live 失败时仍必须保存 stderr。',
);
assertFileExists(
path.join(output.bundleDir, 'direct-live-command.json'),
'direct live 失败时仍必须保存命令记录。',
);
}
function assertDirectLiveMissingAccessLogSummaryFails() {
const fixture = prepareFixture('direct-live-missing-access-log', {
directLiveOutputMode: 'without-access-log',
});
const result = runBundle(fixture, {
status: 'OK',
extraArgs: directLiveArgs(fixture, [
'--phase',
'post-enable',
'--expected-gateway-mode',
'pingora-direct',
'--expected-public-base-url',
'https://127.0.0.1',
'--expected-public-host',
'example.com',
'--fail-on-critical',
]),
});
assertStatus(
result,
1,
'direct live JSON 缺少 direct-access-log 时证据包应失败。',
);
const output = parseJson(result.stdout, 'direct live 缺 access log 证据包 stdout');
assertEqual(
output.status,
'CRITICAL',
'direct live 缺 access log 时 stdout 必须记录 CRITICAL。',
);
assertFileExists(output.bundleDir, 'direct live 缺 access log 证据包目录必须存在。');
if (!output.bundleDir) {
return;
}
const manifest = readJson(path.join(output.bundleDir, 'manifest.json'));
assertEqual(
manifest.summary.directLiveStatus,
'CRITICAL',
'direct live 缺 access log 时 manifest 必须记录 CRITICAL。',
);
assertEqual(
manifest.summary.directLiveAccessLog?.present,
false,
'direct live 缺 access log 时 manifest 必须记录缺失原因。',
);
}
function assertDirectLiveMissingStaticHeadersSummaryFails() {
const fixture = prepareFixture('direct-live-missing-static-headers', {
directLiveOutputMode: 'without-static-headers',
});
const result = runBundle(fixture, {
status: 'OK',
extraArgs: directLiveArgs(fixture, [
'--phase',
'post-enable',
'--expected-gateway-mode',
'pingora-direct',
'--expected-public-base-url',
'https://127.0.0.1',
'--expected-public-host',
'example.com',
'--fail-on-critical',
]),
});
assertStatus(
result,
1,
'direct live JSON 缺少静态响应头证据时证据包应失败。',
);
const output = parseJson(result.stdout, 'direct live 缺静态头证据包 stdout');
assertEqual(
output.status,
'CRITICAL',
'direct live 缺静态头时 stdout 必须记录 CRITICAL。',
);
assertFileExists(output.bundleDir, 'direct live 缺静态头证据包目录必须存在。');
if (!output.bundleDir) {
return;
}
const manifest = readJson(path.join(output.bundleDir, 'manifest.json'));
assertEqual(
manifest.summary.directLiveStatus,
'CRITICAL',
'direct live 缺静态头时 manifest 必须记录 CRITICAL。',
);
assertEqual(
manifest.summary.directLiveStaticHeaders?.present,
false,
'direct live 缺静态头时 manifest 必须记录缺失原因。',
);
}
function assertDirectLiveStaticHeaderDiagnosticsFails() {
const fixture = prepareFixture('direct-live-bad-static-headers', {
directLiveOutputMode: 'bad-static-headers',
});
const result = runBundle(fixture, {
status: 'OK',
extraArgs: directLiveArgs(fixture, [
'--phase',
'post-enable',
'--expected-gateway-mode',
'pingora-direct',
'--expected-public-base-url',
'https://127.0.0.1',
'--expected-public-host',
'example.com',
'--fail-on-critical',
]),
});
assertStatus(
result,
1,
'direct live 静态响应头摘要缺少 Range / 304 证据时证据包应失败。',
);
const output = parseJson(result.stdout, 'direct live 静态头漂移证据包 stdout');
assertEqual(
output.status,
'CRITICAL',
'direct live 静态头漂移时 stdout 必须记录 CRITICAL。',
);
if (!output.bundleDir) {
return;
}
const manifest = readJson(path.join(output.bundleDir, 'manifest.json'));
assertEqual(
manifest.summary.directLiveStatus,
'CRITICAL',
'direct live 静态头漂移时 manifest 必须记录 CRITICAL。',
);
assertEqual(
manifest.summary.directLiveStaticHeaders?.normal?.ok,
false,
'direct live 静态头漂移时 manifest 必须标记普通静态资源摘要失败。',
);
assertIncludes(
manifest.summary.directLiveStaticHeaders?.diagnostics || [],
'normal: Range statusCode 应为 206,实际 -',
'direct live 静态头漂移时 manifest 必须记录 Range 诊断。',
);
assertIncludes(
manifest.summary.directLiveStaticHeaders?.diagnostics || [],
'normal: ETag 304 statusCode 应为 304,实际 -',
'direct live 静态头漂移时 manifest 必须记录 ETag 304 诊断。',
);
}
function assertDirectLiveParseFailureWritesParseErrorEvidence() {
const fixture = prepareFixture('direct-live-parse-error', {
directLiveOutputMode: 'log-only',
});
const result = runBundle(fixture, {
status: 'OK',
extraArgs: directLiveArgs(fixture, [
'--phase',
'post-enable',
'--expected-gateway-mode',
'pingora-direct',
'--expected-public-base-url',
'https://127.0.0.1',
'--expected-public-host',
'example.com',
'--fail-on-critical',
]),
});
assertStatus(result, 1, 'direct live 未输出 JSON 时证据包应失败。');
const output = parseJson(result.stdout, 'direct live 解析失败证据包 stdout');
assertEqual(
output.status,
'CRITICAL',
'direct live 解析失败时 stdout 必须记录 CRITICAL。',
);
assertFileExists(
output.directLiveParseErrorPath,
'证据包 stdout 必须返回 direct live parse error 路径。',
);
assertFileExists(output.bundleDir, 'direct live 解析失败证据包目录必须存在。');
if (!output.bundleDir) {
return;
}
const manifest = readJson(path.join(output.bundleDir, 'manifest.json'));
assertEqual(
manifest.files?.directLive,
null,
'direct live 解析失败时 manifest 不应指向 direct-live.json。',
);
assertEqual(
manifest.files?.directLiveParseError?.path,
'direct-live-parse-error.txt',
'direct live 解析失败时 manifest 必须指向 parse error 文件。',
);
assertManifestFileMetadata(
output.bundleDir,
manifest.files?.directLiveParseError,
'direct-live-parse-error.txt',
);
assertEqual(
manifest.summary.directLiveStatus,
'CRITICAL',
'direct live 解析失败时 manifest 必须记录 CRITICAL。',
);
assertFileExists(
path.join(output.bundleDir, 'direct-live.stdout.txt'),
'direct live 解析失败时仍必须保存 stdout。',
);
assertFileExists(
path.join(output.bundleDir, 'direct-live.stderr.txt'),
'direct live 解析失败时仍必须保存 stderr。',
);
}
function assertCriticalSnapshotStillWritesEvidenceAndFails() {
const fixture = prepareFixture('critical');
const result = runBundle(fixture, {
status: 'CRITICAL',
extraArgs: [
'--phase',
'post-enable',
'--expected-gateway-mode',
'pingora-direct',
'--fail-on-critical',
],
});
assertStatus(result, 1, 'CRITICAL 快照应保留证据并返回失败。');
const output = parseJson(result.stdout, 'CRITICAL 证据包 stdout');
assertEqual(output.status, 'CRITICAL', '失败证据包 stdout 必须记录 CRITICAL。');
assertFileExists(output.bundleDir, '失败证据包目录也必须存在。');
if (!output.bundleDir) {
return;
}
const manifest = readJson(path.join(output.bundleDir, 'manifest.json'));
assertEqual(
manifest.summary.status,
'CRITICAL',
'失败证据包 manifest 必须记录 CRITICAL。',
);
assertEqual(
manifest.summary.snapshotExitCode,
1,
'失败证据包 manifest 必须记录快照退出码。',
);
assertFileExists(
path.join(output.bundleDir, 'snapshot.json'),
'失败证据包仍必须保存 snapshot.json。',
);
assertMode(output.bundleDir, 0o750, '失败证据包目录权限必须是 0750。');
assertMode(
path.join(output.bundleDir, 'manifest.json'),
0o640,
'失败证据包 manifest 权限必须是 0640。',
);
assertFileUnchanged(
fixture.healthEnvFile,
fixture.originalHealthEnvText,
'失败证据包不应改写 health patrol env。',
);
}
function assertSnapshotParseFailureWritesParseErrorEvidence() {
const fixture = prepareFixture('snapshot-parse-error', {
snapshotOutputMode: 'log-only',
});
const result = runBundle(fixture, {
status: 'OK',
extraArgs: [
'--phase',
'post-enable',
'--expected-gateway-mode',
'pingora-direct',
'--fail-on-critical',
],
});
assertStatus(result, 1, 'snapshot 未输出 JSON 时证据包应失败。');
const output = parseJson(result.stdout, 'snapshot 解析失败证据包 stdout');
assertEqual(
output.status,
'CRITICAL',
'snapshot 解析失败时 stdout 必须记录 CRITICAL。',
);
assertFileExists(
output.snapshotParseErrorPath,
'证据包 stdout 必须返回 snapshot parse error 路径。',
);
assertFileExists(output.bundleDir, 'snapshot 解析失败证据包目录必须存在。');
if (!output.bundleDir) {
return;
}
const manifest = readJson(path.join(output.bundleDir, 'manifest.json'));
assertEqual(
manifest.files?.snapshot,
null,
'snapshot 解析失败时 manifest 不应指向 snapshot.json。',
);
assertEqual(
manifest.files?.snapshotParseError?.path,
'snapshot-parse-error.txt',
'snapshot 解析失败时 manifest 必须指向 parse error 文件。',
);
assertManifestFileMetadata(
output.bundleDir,
manifest.files?.snapshotParseError,
'snapshot-parse-error.txt',
);
assertEqual(
manifest.summary.snapshotStatus,
'CRITICAL',
'snapshot 解析失败时 manifest 必须记录 snapshot CRITICAL。',
);
assertFileExists(
path.join(output.bundleDir, 'snapshot.stdout.txt'),
'snapshot 解析失败时仍必须保存 stdout。',
);
assertFileExists(
path.join(output.bundleDir, 'snapshot-parse-error.txt'),
'snapshot 解析失败时必须保存 parse error 文件。',
);
}
function assertBundleRedactsProbeTokensFromArtifacts() {
const fixture = prepareFixture('redact-probe-token', {
snapshotIncludesProbeFlags: true,
});
const result = runBundle(fixture, {
status: 'OK',
extraArgs: directLiveArgs(fixture, [
'--phase',
'post-enable',
'--expected-gateway-mode',
'pingora-direct',
'--run-health-patrol',
'--fail-on-critical',
]),
});
assertStatus(result, 0, '带 probe token 的证据包应成功生成。');
if (result.status !== 0) {
return;
}
const output = parseJson(result.stdout, 'probe token 证据包 stdout');
const filesToCheck = [
'manifest.json',
'snapshot.json',
'snapshot.stdout.txt',
'snapshot-command.json',
'direct-live-command.json',
];
for (const fileName of filesToCheck) {
const content = readTextFile(
path.join(output.bundleDir, fileName),
`证据包 ${fileName}`,
);
assertNotIncludes(
content,
'health-patrol-secret-token',
`${fileName} 不能泄露 health patrol probe token 原文。`,
);
assertNotIncludes(
content,
'pingora-gateway-secret-token',
`${fileName} 不能泄露 Pingora probe token 原文。`,
);
assertNotIncludes(
content,
'direct-live-secret-token',
`${fileName} 不能泄露 direct live probe token 原文。`,
);
}
const commandRecord = readJson(path.join(output.bundleDir, 'snapshot-command.json'));
assertIncludes(
commandRecord.args || [],
'--health-patrol-env-file',
'snapshot-command.json 结构化参数数组必须保留快照参数。',
);
const snapshot = readJson(path.join(output.bundleDir, 'snapshot.json'));
assertEqual(
snapshot.healthPatrolEnv?.values?.hasPingoraProbeToken,
true,
'证据包 snapshot.json 可以记录 health patrol probe token 是否存在。',
);
assertEqual(
snapshot.pingoraEnv?.values?.hasProbeToken,
true,
'证据包 snapshot.json 可以记录 Pingora probe token 是否存在。',
);
const healthPatrolCheck = snapshot.checks?.find(
(check) => check.name === 'production-health-patrol',
);
if (!healthPatrolCheck) {
failures.push('证据包 snapshot.json 必须保留生产巡检子检查结果。');
} else {
assertIncludes(
`${healthPatrolCheck.stdout}\n${healthPatrolCheck.stderr}`,
'<redacted>',
'证据包 snapshot.json 中的子检查 stdout/stderr 必须保留脱敏占位符。',
);
}
}
function readTextFile(filePath, label) {
try {
return readFileSync(filePath, 'utf8');
} catch (error) {
failures.push(`无法读取 ${label}: ${error.message}`);
return '';
}
}
function assertManifestFileMetadata(bundleDir, metadata, expectedPath) {
if (!metadata || typeof metadata !== 'object') {
failures.push(`${expectedPath} 必须在 manifest.files 中记录结构化元数据。`);
return;
}
assertEqual(
metadata.path,
expectedPath,
`${expectedPath} manifest 元数据必须记录文件名。`,
);
const filePath = path.join(bundleDir, expectedPath);
const content = readFileSync(filePath);
assertEqual(
metadata.sizeBytes,
content.length,
`${expectedPath} manifest 元数据必须记录文件大小。`,
);
assertEqual(
metadata.sha256,
createHash('sha256').update(content).digest('hex'),
`${expectedPath} manifest 元数据必须记录 sha256。`,
);
}
function directLiveArgs(fixture, prefixArgs = []) {
return [
...prefixArgs,
'--direct-live-script',
fixture.directLiveScript,
'--run-direct-live',
'--direct-https-base-url',
'https://127.0.0.1',
'--direct-http-base-url',
'http://127.0.0.1',
'--direct-host',
'example.com',
'--direct-redirect-host',
'example.com',
'--direct-probe-token',
'direct-live-secret-token',
'--direct-spacetime-database',
'genarrative-prod',
'--direct-pingora-access-log',
fixture.directAccessLog,
'--direct-access-log-since-lines',
'99',
];
}
function assertRejectsRelativePaths() {
const fixture = prepareFixture('relative-path');
for (const [flag, value, expected] of [
['--output-root', 'pingora-evidence', '--output-root 必须是绝对路径'],
['--release-root', 'current', '--release-root 必须是绝对路径'],
['--snapshot-script', 'snapshot.mjs', '--snapshot-script 必须是绝对路径'],
]) {
const result = spawnSync(
'node',
[
BUNDLE_SCRIPT,
'--release-root',
fixture.releaseRoot,
'--output-root',
fixture.outputRoot,
'--health-patrol-env-file',
fixture.healthEnvFile,
'--pingora-env-file',
fixture.pingoraEnvFile,
'--snapshot-script',
fixture.snapshotScript,
flag,
value,
],
{
cwd: process.cwd(),
encoding: 'utf8',
},
);
if ((result.status ?? 0) === 0) {
failures.push(`${flag} 使用相对路径时必须失败。`);
}
assertIncludes(
`${result.stdout}\n${result.stderr}`,
expected,
`${flag} 相对路径必须给出明确错误。`,
);
}
}
function assertRejectsFilesystemRootPaths() {
const fixture = prepareFixture('filesystem-root-path');
for (const [flag, expected] of [
['--release-root', '--release-root 不能是文件系统根目录'],
['--output-root', '--output-root 不能是文件系统根目录'],
['--health-patrol-env-file', '--health-patrol-env-file 不能是文件系统根目录'],
['--pingora-env-file', '--pingora-env-file 不能是文件系统根目录'],
['--snapshot-script', '--snapshot-script 不能是文件系统根目录'],
]) {
const result = spawnSync(
'node',
[
BUNDLE_SCRIPT,
'--release-root',
fixture.releaseRoot,
'--output-root',
fixture.outputRoot,
'--health-patrol-env-file',
fixture.healthEnvFile,
'--pingora-env-file',
fixture.pingoraEnvFile,
'--snapshot-script',
fixture.snapshotScript,
flag,
'/',
],
{
cwd: process.cwd(),
encoding: 'utf8',
},
);
if ((result.status ?? 0) === 0) {
failures.push(`${flag} 使用文件系统根目录时必须失败。`);
}
assertIncludes(
`${result.stdout}\n${result.stderr}`,
expected,
`${flag} 文件系统根目录必须给出明确错误。`,
);
assertNotIncludes(
`${result.stdout}\n${result.stderr}`,
'[fake-snapshot]',
`${flag} 文件系统根目录被拒绝时不应继续执行状态快照。`,
);
}
const directLogResult = runBundle(fixture, {
status: 'OK',
extraArgs: [
...directLiveArgs(fixture, [
'--phase',
'post-enable',
'--expected-gateway-mode',
'pingora-direct',
]),
'--direct-pingora-access-log',
'/',
],
});
if ((directLogResult.status ?? 0) === 0) {
failures.push('--direct-pingora-access-log 使用文件系统根目录时必须失败。');
}
assertIncludes(
`${directLogResult.stdout}\n${directLogResult.stderr}`,
'--direct-pingora-access-log 不能是文件系统根目录',
'direct live access log 指向文件系统根目录必须给出明确错误。',
);
assertNotIncludes(
`${directLogResult.stdout}\n${directLogResult.stderr}`,
'[fake-snapshot]',
'direct live access log 根目录被拒绝时不应执行状态快照。',
);
}
function assertRejectsUnsafePhase() {
const fixture = prepareFixture('unsafe-phase');
for (const phase of ['post enable', '../post-enable', 'post/enable', '回退']) {
const result = runBundle(fixture, {
status: 'OK',
extraArgs: ['--phase', phase],
});
if ((result.status ?? 0) === 0) {
failures.push(`--phase=${phase} 包含不安全字符时必须失败。`);
}
assertIncludes(
`${result.stdout}\n${result.stderr}`,
'--phase 只能包含 ASCII 字母、数字、点、下划线或短横线',
`--phase=${phase} 必须给出明确错误。`,
);
}
}
function assertRejectsUnsafeCutoverRunId() {
const fixture = prepareFixture('unsafe-cutover-run-id');
for (const runId of ['cutover 1', '../cutover', '切换']) {
const result = runBundle(fixture, {
status: 'OK',
extraArgs: ['--cutover-run-id', runId],
});
if ((result.status ?? 0) === 0) {
failures.push(`--cutover-run-id=${runId} 包含不安全字符时必须失败。`);
}
assertIncludes(
`${result.stdout}\n${result.stderr}`,
'--cutover-run-id 只能包含 ASCII 字母、数字、点、下划线或短横线',
`--cutover-run-id=${runId} 必须给出明确错误。`,
);
}
}
function assertRejectsSnapshotArgsWithControlCharacters() {
const fixture = prepareFixture('snapshot-control-character-arg');
const result = runBundle(fixture, {
status: 'OK',
extraArgs: [
'--phase',
'pre-cutover',
'--release-root',
`${fixture.releaseRoot}\n--fake-flag`,
],
});
if ((result.status ?? 0) === 0) {
failures.push('证据包必须拒绝带换行的状态快照命令参数。');
}
assertIncludes(
`${result.stdout}\n${result.stderr}`,
'状态快照命令参数不能包含换行或 NUL 字符',
'状态快照命令参数控制字符负例必须给出明确错误。',
);
assertNotIncludes(
`${result.stdout}\n${result.stderr}`,
'[fake-snapshot]',
'状态快照命令参数被拒绝时不应执行状态快照。',
);
}
function assertRejectsDirectLiveArgsWithControlCharacters() {
const fixture = prepareFixture('direct-live-control-character-arg', {
directLiveStatus: 'OK',
});
for (const [flag, value, expected] of [
[
'--direct-probe-token',
'direct-token\n--fake-flag',
'--direct-probe-token 不能包含换行或 NUL 字符',
],
[
'--direct-spacetime-database',
'genarrative\nprod',
'--direct-spacetime-database 不能包含换行或 NUL 字符',
],
[
'--direct-pingora-access-log',
`${fixture.directAccessLog}\n--fake-flag`,
'--direct-pingora-access-log 不能包含换行或 NUL 字符',
],
[
'--direct-access-log-since-lines',
'99\n1',
'--direct-access-log-since-lines 不能包含换行或 NUL 字符',
],
]) {
const result = runBundle(fixture, {
status: 'OK',
extraArgs: [
...directLiveArgs(fixture, [
'--phase',
'post-enable',
'--expected-gateway-mode',
'pingora-direct',
'--fail-on-critical',
]),
flag,
value,
],
});
if ((result.status ?? 0) === 0) {
failures.push(`${flag} 带换行时必须在执行 direct live 前失败。`);
}
assertIncludes(
`${result.stdout}\n${result.stderr}`,
expected,
`${flag} 控制字符负例必须给出明确错误。`,
);
assertNotIncludes(
`${result.stdout}\n${result.stderr}`,
'[fake-snapshot]',
`${flag} 被拒绝时不应执行状态快照。`,
);
assertNotIncludes(
`${result.stdout}\n${result.stderr}`,
'[fake-direct-live]',
`${flag} 被拒绝时不应执行 direct live。`,
);
}
}
function assertRejectsSymlinkOutputRootBeforeSnapshot() {
const fixture = prepareFixture('symlink-output-root');
const realTarget = path.join(tmpRoot, 'symlink-output-root-real-target');
rmSync(fixture.outputRoot, { recursive: true, force: true });
mkdirSync(realTarget, { recursive: true });
symlinkSync(realTarget, fixture.outputRoot, 'dir');
const result = runBundle(fixture, {
status: 'OK',
extraArgs: ['--phase', 'pre-cutover'],
});
if ((result.status ?? 0) === 0) {
failures.push('证据包必须拒绝符号链接 --output-root。');
}
assertIncludes(
`${result.stdout}\n${result.stderr}`,
'--output-root 不能是符号链接',
'符号链接 output-root 负例必须给出明确错误。',
);
assertNotIncludes(
`${result.stdout}\n${result.stderr}`,
'[fake-snapshot]',
'符号链接 output-root 被拒绝时不应继续执行状态快照。',
);
assertDirectoryEmpty(
realTarget,
'符号链接 output-root 被拒绝时不应写入真实目标目录。',
);
}
function assertRejectsSymlinkOutputRootParentBeforeSnapshot() {
const fixture = prepareFixture('symlink-output-root-parent');
const realTarget = path.join(
tmpRoot,
'symlink-output-root-parent-real-target',
);
const linkPath = path.join(tmpRoot, 'symlink-output-root-parent-link');
mkdirSync(realTarget, { recursive: true });
symlinkSync(realTarget, linkPath, 'dir');
const result = runBundle(
{
...fixture,
outputRoot: path.join(linkPath, 'evidence'),
},
{
status: 'OK',
extraArgs: ['--phase', 'pre-cutover'],
},
);
if ((result.status ?? 0) === 0) {
failures.push('证据包必须拒绝符号链接 --output-root 上级目录。');
}
assertIncludes(
`${result.stdout}\n${result.stderr}`,
'--output-root 上级目录不能是符号链接',
'符号链接 output-root 上级目录负例必须给出明确错误。',
);
assertNotIncludes(
`${result.stdout}\n${result.stderr}`,
'[fake-snapshot]',
'符号链接 output-root 上级目录被拒绝时不应继续执行状态快照。',
);
assertDirectoryEmpty(
realTarget,
'符号链接 output-root 上级目录被拒绝时不应写入真实目标目录。',
);
}
function assertRejectsFileOutputRootBeforeSnapshot() {
const fixture = prepareFixture('file-output-root');
rmSync(fixture.outputRoot, { recursive: true, force: true });
writeFileSync(fixture.outputRoot, 'not a directory\n', 'utf8');
const result = runBundle(fixture, {
status: 'OK',
extraArgs: ['--phase', 'pre-cutover'],
});
if ((result.status ?? 0) === 0) {
failures.push('证据包必须拒绝已存在但不是目录的 --output-root。');
}
assertIncludes(
`${result.stdout}\n${result.stderr}`,
'--output-root 已存在但不是目录',
'非目录 output-root 负例必须给出明确错误。',
);
assertNotIncludes(
`${result.stdout}\n${result.stderr}`,
'[fake-snapshot]',
'非目录 output-root 被拒绝时不应继续执行状态快照。',
);
assertEqual(
readFileSync(fixture.outputRoot, 'utf8'),
'not a directory\n',
'非目录 output-root 被拒绝时不应改写原文件。',
);
}
function assertRejectsInvalidTimeout() {
const fixture = prepareFixture('invalid-timeout');
const cliResult = runBundle(fixture, {
status: 'OK',
extraArgs: ['--timeout-ms', '0'],
});
if ((cliResult.status ?? 0) === 0) {
failures.push('证据包必须拒绝非正数 --timeout-ms。');
}
assertIncludes(
`${cliResult.stdout}\n${cliResult.stderr}`,
'--timeout-ms 必须是正整数',
'非法 --timeout-ms 必须给出明确错误。',
);
const envResult = runBundle(
fixture,
{
status: 'OK',
extraArgs: [],
},
{
GENARRATIVE_PINGORA_CUTOVER_SNAPSHOT_TIMEOUT_MS: 'abc',
},
);
if ((envResult.status ?? 0) === 0) {
failures.push('证据包必须拒绝非法 timeout env。');
}
assertIncludes(
`${envResult.stdout}\n${envResult.stderr}`,
'GENARRATIVE_PINGORA_CUTOVER_SNAPSHOT_TIMEOUT_MS 必须是正整数',
'非法 timeout env 必须给出明确错误。',
);
}
function assertRejectsInvalidBoolEnv() {
const fixture = prepareFixture('invalid-bool-env');
const cases = [
{
env: { GENARRATIVE_PINGORA_CUTOVER_SNAPSHOT_RUN_HEALTH_PATROL: 'ture' },
expected:
'GENARRATIVE_PINGORA_CUTOVER_SNAPSHOT_RUN_HEALTH_PATROL 必须是布尔值',
reason: '证据包必须拒绝拼写错误的 run health patrol env。',
},
{
env: { GENARRATIVE_PINGORA_CUTOVER_SNAPSHOT_REQUIRE_PINGORA_GATEWAY: 'maybe' },
expected:
'GENARRATIVE_PINGORA_CUTOVER_SNAPSHOT_REQUIRE_PINGORA_GATEWAY 必须是布尔值',
reason: '证据包必须拒绝非法 require pingora gateway env。',
},
{
env: { GENARRATIVE_PINGORA_CUTOVER_SNAPSHOT_FAIL_ON_CRITICAL: 'enabled' },
expected:
'GENARRATIVE_PINGORA_CUTOVER_SNAPSHOT_FAIL_ON_CRITICAL 必须是布尔值',
reason: '证据包必须拒绝非法 fail on critical env。',
},
];
for (const testCase of cases) {
const result = runBundle(
fixture,
{
status: 'OK',
extraArgs: [],
},
testCase.env,
);
if ((result.status ?? 0) === 0) {
failures.push(testCase.reason);
}
assertIncludes(
`${result.stdout}\n${result.stderr}`,
testCase.expected,
`${testCase.reason} 必须给出明确错误。`,
);
}
}
function prepareFixture(name, options = {}) {
const root = path.join(tmpRoot, name);
const releaseRoot = path.join(root, 'current');
const outputRoot = path.join(root, 'evidence');
const healthEnvFile = path.join(root, 'etc', 'health-patrol.env');
const pingoraEnvFile = path.join(root, 'etc', 'pingora-gateway.env');
const snapshotScript = path.join(
releaseRoot,
'scripts/ops/pingora-cutover-status-snapshot.mjs',
);
const directLiveScript = path.join(
releaseRoot,
'scripts/check-pingora-direct-live.mjs',
);
const directAccessLog = path.join(root, 'var/log/pingora-gateway.access.log');
mkdirSync(path.dirname(snapshotScript), { recursive: true });
mkdirSync(path.dirname(directLiveScript), { recursive: true });
mkdirSync(path.dirname(directAccessLog), { recursive: true });
mkdirSync(path.dirname(healthEnvFile), { recursive: true });
mkdirSync(outputRoot, { recursive: true });
const healthEnvText = [
'GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE=nginx',
'GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL=http://127.0.0.1',
'GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST=',
'GENARRATIVE_HEALTH_PATROL_PINGORA_PROBE_TOKEN=health-patrol-secret-token',
'',
].join('\n');
writeFileSync(healthEnvFile, healthEnvText, 'utf8');
writeFileSync(
pingoraEnvFile,
[
'GENARRATIVE_PINGORA_GATEWAY_LISTEN=127.0.0.1:18081',
'GENARRATIVE_PINGORA_GATEWAY_PROBE_TOKEN=pingora-gateway-secret-token',
'',
].join('\n'),
'utf8',
);
writeFileSync(
snapshotScript,
[
'#!/usr/bin/env node',
'const args = process.argv.slice(2);',
'const status = process.env.FAKE_SNAPSHOT_STATUS || "OK";',
`const outputMode = ${JSON.stringify(options.snapshotOutputMode || 'json')};`,
`const pingoraEnvValues = ${JSON.stringify(
options.snapshotPingoraEnvValues || {
listen: '127.0.0.1:18081',
tlsListen: '',
httpRedirectListen: '',
tlsCertFile: '',
tlsKeyFile: '',
},
)};`,
'const shadowReady = pingoraEnvValues.listen === "127.0.0.1:18081" && !pingoraEnvValues.tlsListen && !pingoraEnvValues.httpRedirectListen && !pingoraEnvValues.tlsCertFile && !pingoraEnvValues.tlsKeyFile;',
'const directReady = Boolean(pingoraEnvValues.tlsListen && pingoraEnvValues.httpRedirectListen && pingoraEnvValues.tlsCertFile && pingoraEnvValues.tlsKeyFile);',
'const pingoraEnvPosture = { mode: directReady ? "direct" : shadowReady ? "shadow" : "mixed", shadowReady, directReady };',
'const phaseIndex = args.indexOf("--phase");',
'console.error(`[fake-snapshot] ${status}`);',
'if (outputMode === "log-only") {',
' console.log("[fake-snapshot] finished without json");',
' process.exit(0);',
'}',
...(options.snapshotIncludesProbeFlags
? [
'import { readFileSync } from "node:fs";',
'function readEnv(file) {',
' return Object.fromEntries(readFileSync(file, "utf8").split(/\\r?\\n/).filter(Boolean).filter((line) => !line.startsWith("#")).map((line) => line.split("=")).map(([key, ...value]) => [key, value.join("=")]));',
'}',
'const healthEnv = readEnv(args[args.indexOf("--health-patrol-env-file") + 1]);',
'const pingoraEnv = readEnv(args[args.indexOf("--pingora-env-file") + 1]);',
]
: []),
'console.log(JSON.stringify({',
' schemaVersion: 1,',
' phase: phaseIndex >= 0 ? args[phaseIndex + 1] : "manual",',
' summary: {',
' status,',
' criticalCount: status === "CRITICAL" ? 1 : 0,',
' warningCount: 0,',
' },',
...(options.snapshotIncludesProbeFlags
? [
' healthPatrolEnv: { values: { hasPingoraProbeToken: Boolean(healthEnv.GENARRATIVE_HEALTH_PATROL_PINGORA_PROBE_TOKEN || healthEnv.GENARRATIVE_PINGORA_GATEWAY_PROBE_TOKEN) } },',
' pingoraEnv: { values: { ...pingoraEnvValues, hasProbeToken: Boolean(pingoraEnv.GENARRATIVE_PINGORA_GATEWAY_PROBE_TOKEN) }, posture: pingoraEnvPosture },',
' checks: [{ name: "production-health-patrol", stdout: "probe stdout <redacted>:<redacted>", stderr: "probe stderr <redacted>:<redacted>" }],',
]
: [' pingoraEnv: { values: pingoraEnvValues, posture: pingoraEnvPosture },']),
' args,',
'}, null, 2));',
'if (status === "CRITICAL" && args.includes("--fail-on-critical")) process.exit(1);',
'',
].join('\n'),
'utf8',
);
chmodSync(snapshotScript, 0o755);
writeFileSync(
directAccessLog,
'ts=2026-06-16T00:00:00Z\trequest_id=direct-test\tpath=/\n',
'utf8',
);
writeFileSync(
directLiveScript,
[
'#!/usr/bin/env node',
'const args = process.argv.slice(2);',
`const status = ${JSON.stringify(options.directLiveStatus || 'OK')};`,
`const outputMode = ${JSON.stringify(options.directLiveOutputMode || 'json')};`,
'const accessLogIndex = args.indexOf("--pingora-access-log");',
'const sinceLinesIndex = args.indexOf("--access-log-since-lines");',
'const databaseIndex = args.indexOf("--spacetime-database");',
'console.log(`[pingora-direct-live] fake ${status}`);',
'console.error(`[fake-direct-live] ${status}`);',
'if (outputMode === "log-only") {',
' console.log("[pingora-direct-live] finished without json");',
' process.exit(0);',
'}',
'const accessLogResults = outputMode === "without-access-log" ? [] : [',
' {',
' name: "direct-access-log",',
' logFile: accessLogIndex >= 0 ? args[accessLogIndex + 1] : "",',
' sinceLines: sinceLinesIndex >= 0 ? Number(args[sinceLinesIndex + 1]) : 0,',
' scannedLineCount: 1,',
' checked: 2,',
' matchedCount: status === "OK" ? 2 : 1,',
' missingCount: status === "OK" ? 0 : 1,',
' mismatchCount: 0,',
' matched: [{',
' name: "https-root",',
' requestId: "direct-live-root",',
' expectedMethod: "GET",',
' expectedPath: "/",',
' expectedStatusCode: 200,',
' actualMethod: "GET",',
' actualPath: "/",',
' actualStatusCode: 200,',
' }],',
' missing: status === "OK" ? [] : [{',
' name: "https-api-history",',
' requestId: "direct-live-api",',
' expectedMethod: "GET",',
' expectedPath: "/api/assets/history",',
' expectedStatusCode: 200,',
' }],',
' mismatches: [],',
' },',
'];',
'const staticAssetResult = {',
' name: "https-static-asset",',
' statusCode: 200,',
' requestId: "direct-live-static",',
' headers: {',
' "cache-control": "no-cache",',
' etag: "W/\\"1b-18bcfe56800\\"",',
' "last-modified": "Tue, 14 Nov 2023 22:13:20 GMT",',
' "accept-ranges": "bytes",',
' "content-length": "27",',
' },',
' range: {',
' name: "https-static-asset-range",',
' statusCode: 206,',
' headers: {',
' "content-range": "bytes 0-0/27",',
' "content-length": "1",',
' },',
' },',
' notModified: {',
' etag: {',
' name: "https-static-asset-etag-304",',
' statusCode: 304,',
' headers: {',
' "cache-control": "no-cache",',
' etag: "W/\\"1b-18bcfe56800\\"",',
' },',
' },',
' lastModified: {',
' name: "https-static-asset-last-modified-304",',
' statusCode: 304,',
' headers: {',
' "cache-control": "no-cache",',
' "last-modified": "Tue, 14 Nov 2023 22:13:20 GMT",',
' },',
' },',
' },',
' fingerprinted: {',
' name: "https-static-fingerprinted-asset",',
' statusCode: 200,',
' headers: {',
' "cache-control": "public, max-age=31536000, immutable",',
' etag: "W/\\"29-18bcfe56800\\"",',
' "last-modified": "Tue, 14 Nov 2023 22:13:20 GMT",',
' "accept-ranges": "bytes",',
' "content-length": "41",',
' },',
' range: {',
' name: "https-static-fingerprinted-asset-range",',
' statusCode: 206,',
' headers: {',
' "content-range": "bytes 0-0/41",',
' "content-length": "1",',
' },',
' },',
' notModified: {',
' etag: {',
' name: "https-static-fingerprinted-asset-etag-304",',
' statusCode: 304,',
' headers: {',
' "cache-control": "public, max-age=31536000, immutable",',
' etag: "W/\\"29-18bcfe56800\\"",',
' },',
' },',
' lastModified: {',
' name: "https-static-fingerprinted-asset-last-modified-304",',
' statusCode: 304,',
' headers: {',
' "cache-control": "public, max-age=31536000, immutable",',
' "last-modified": "Tue, 14 Nov 2023 22:13:20 GMT",',
' },',
' },',
' },',
' },',
'};',
'if (outputMode === "bad-static-headers") {',
' delete staticAssetResult.range;',
' delete staticAssetResult.notModified;',
'}',
'const staticAssetResults = outputMode === "without-static-headers" ? [] : [staticAssetResult];',
'console.log(JSON.stringify({',
' ok: status === "OK",',
' results: [',
' { name: "https-root", statusCode: 200, requestId: "direct-live-root" },',
' ...staticAssetResults,',
' ...accessLogResults,',
' { name: "wss-spacetime-subscribe", database: databaseIndex >= 0 ? args[databaseIndex + 1] : "" },',
' ],',
' args,',
'}, null, 2));',
'console.log(status === "OK" ? "[pingora-direct-live] OK" : "[pingora-direct-live] FAILED");',
'if (status !== "OK") process.exit(1);',
'',
].join('\n'),
'utf8',
);
chmodSync(directLiveScript, 0o755);
return {
releaseRoot,
outputRoot,
healthEnvFile,
pingoraEnvFile,
snapshotScript,
directLiveScript,
directAccessLog,
originalHealthEnvText: healthEnvText,
};
}
function runBundle(fixture, { status, extraArgs }, extraEnv = {}) {
return spawnSync(
'node',
[
BUNDLE_SCRIPT,
'--release-root',
fixture.releaseRoot,
'--output-root',
fixture.outputRoot,
'--health-patrol-env-file',
fixture.healthEnvFile,
'--pingora-env-file',
fixture.pingoraEnvFile,
'--snapshot-script',
fixture.snapshotScript,
...extraArgs,
],
{
cwd: process.cwd(),
encoding: 'utf8',
env: {
...process.env,
GENARRATIVE_PINGORA_CUTOVER_RUN_ID: '',
...extraEnv,
FAKE_SNAPSHOT_STATUS: status,
},
},
);
}
function parseJson(text, label) {
try {
return JSON.parse(text);
} catch (error) {
failures.push(`${label} 不是合法 JSON: ${error.message}`);
return {};
}
}
function readJson(filePath) {
try {
return JSON.parse(readFileSync(filePath, 'utf8'));
} catch (error) {
failures.push(`${filePath} 不是合法 JSON: ${error.message}`);
return {};
}
}
function assertFileExists(filePath, reason) {
if (!filePath || !existsSync(filePath)) {
failures.push(`${reason} 缺少: ${filePath}`);
}
}
function assertFileUnchanged(filePath, expected, reason) {
const actual = readFileSync(filePath, 'utf8');
if (actual !== expected) {
failures.push(reason);
}
}
function assertDirectoryEmpty(directoryPath, reason) {
try {
const entries = readdirSync(directoryPath);
if (entries.length > 0) {
failures.push(`${reason} 实际包含: ${entries.join(', ')}`);
}
} catch (error) {
failures.push(`无法读取 ${directoryPath} 目录内容: ${error.message}`);
}
}
function assertMode(filePath, expected, reason) {
try {
const actual = statSync(filePath).mode & 0o777;
if (actual !== expected) {
failures.push(
`${reason} 实际 ${actual.toString(8)},预期 ${expected.toString(8)}。`,
);
}
} catch (error) {
failures.push(`无法读取 ${filePath} 权限: ${error.message}`);
}
}
function assertStatus(result, expected, reason) {
if ((result.status ?? 0) !== expected) {
failures.push(
`${reason} 实际退出码 ${result.status}。\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`,
);
}
}
function assertEqual(actual, expected, reason) {
if (actual !== expected) {
failures.push(`${reason} 实际 ${actual},预期 ${expected}。`);
}
}
function assertIncludes(value, expected, reason) {
const haystack = Array.isArray(value) ? value.join('\n') : String(value);
if (!haystack.includes(expected)) {
failures.push(`${reason} 缺少: ${expected}`);
}
}
function assertNotIncludes(value, unexpected, reason) {
const haystack = Array.isArray(value) ? value.join('\n') : String(value);
if (haystack.includes(unexpected)) {
failures.push(`${reason} 不应包含: ${unexpected}`);
}
}