#!/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, '', '证据包命令记录必须用 隐藏敏感参数值。', ); 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}`, '', '证据包 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 :", stderr: "probe stderr :" }],', ] : [' 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}`); } }