#!/usr/bin/env node import { spawnSync } from 'node:child_process'; import { createHash } from 'node:crypto'; import { mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync, } from 'node:fs'; import { tmpdir } from 'node:os'; import path from 'node:path'; const AUDIT_SCRIPT = 'scripts/ops/pingora-cutover-evidence-audit.mjs'; const failures = []; const tmpRoot = mkdtempSync( path.join(tmpdir(), 'genarrative-pingora-cutover-evidence-audit-'), ); try { main(); } finally { rmSync(tmpRoot, { recursive: true, force: true }); } if (failures.length > 0) { console.error('[check:pingora-cutover-evidence-audit] FAILED'); for (const failure of failures) { console.error(`- ${failure}`); } process.exit(1); } console.log('[check:pingora-cutover-evidence-audit] OK'); function main() { assertScriptShape(); assertRequiredPhasesSucceed(); assertRequirePhaseDirectLiveAccessLogSucceeds(); assertRequirePhaseDirectLiveAccessLogFailsWhenMissing(); assertRequirePhaseDirectLiveAccessLogFailsWhenPhaseMissing(); assertRequirePhaseDirectLiveStaticHeadersSucceeds(); assertRequirePhaseDirectLiveStaticHeadersFailsWhenMissing(); assertRequirePhasePingoraEnvShadowSucceeds(); assertRequirePhasePingoraEnvShadowFailsWhenMissing(); assertRequirePhasePingoraEnvShadowFailsWhenDirectLowPortsRemain(); assertRequiredCommandsSucceed(); assertRequiredCommandExecutableImplicitlyRequiresCommand(); assertRequiredCommandArgImplicitlyRequiresCommand(); assertRequiredCommandExecutableMismatchFails(); assertInvalidTopLevelExpectedExecutableFails(); assertInvalidEmbeddedExpectedExecutableFails(); assertManifestCommandExecutableMismatchExpectedFails(); assertRelativeCommandExecutableFails(); assertMissingCommandExecutableFails(); assertRequiredCommandArgMissingFails(); assertCommandRecordMismatchFails(); assertCommandRecordTimelineMismatchFails(); assertCommandRecordOutputPathMismatchFails(); assertCommandRecordArgsMismatchFails(); assertCommandRecordArgsControlCharacterFails(); assertCommandNameMismatchFails(); assertInvalidTopLevelCommandNameFails(); assertInvalidEmbeddedCommandNameFails(); assertFullCutoverTimelineSucceed(); assertFullCutoverTimelineFailsWhenPostEnableAccessLogMissing(); assertFullCutoverTimelineFailsWhenPostEnableStaticHeadersMissing(); assertFullCutoverTimelineWithRunIdSucceed(); assertLongCutoverTimelineFails(); assertTimelineMaxSpanOverrideSucceed(); assertTimelineNonOkItemFails(); assertMixedTimelineCutoverRunIdFails(); assertPartialTimelineCutoverRunIdFails(); assertEmptyCutoverRunIdFails(); assertCutoverRunIdMismatchFails(); assertMissingCutoverRunIdFailsWhenRequired(); assertMissingGeneratedAtFails(); assertInvalidCommandGeneratedAtFails(); assertNonCanonicalGeneratedAtFails(); assertNonCanonicalCommandRecordTimesFail(); assertAmbiguousLatestPhaseFails(); assertAmbiguousLatestCommandFails(); assertMissingManifestSchemaVersionFails(); assertCommandRecordSchemaVersionMismatchFails(); assertCanAuditSinglePhase(); assertMissingPhaseFails(); assertCommandBundleCannotSatisfyPhaseRequirement(); assertMissingCommandFails(); assertOutOfOrderCutoverTimelineFails(); assertCriticalPhaseSummaryFails(); assertFailedCommandSummaryFails(); assertLatestBrokenBundleFails(); assertLatestBrokenCommandBundleFails(); assertManifestDiagnosticsFailAudit(); assertExtraRootFileFailsByDefault(); assertExtraRootDirectoryFailsByDefault(); assertExtraRootSymlinkFailsByDefault(); assertAllowExtraRootEntriesSucceeds(); assertRejectsUnsafeInputs(); assertManifestFileNameControlCharacterFails(); assertRejectsInvalidTimelineMaxSpan(); } function assertScriptShape() { const content = readFileSync(AUDIT_SCRIPT, 'utf8'); assertIncludes( content, '不会修改证据目录', '证据根审计脚本 usage 必须说明只读边界。', ); assertIncludes( content, 'execFile', '证据根审计脚本必须复用 verifier 逐包验真。', ); assertIncludes( content, "'--require-summary-ok'", '证据根审计脚本调用 verifier 时必须使用 strict summary 模式。', ); assertIncludes( content, 'manifest.phase', '证据根审计脚本必须按 manifest.phase 分组。', ); assertIncludes( content, 'manifest.commandName', '证据根审计脚本必须能按 manifest.commandName 锁定命令证据。', ); assertIncludes( content, 'manifest.commandName 与 manifest.command.name 必须一致', '证据根审计脚本必须拒绝 manifest.commandName 与 manifest.command.name 漂移。', ); assertIncludes( content, 'candidate.commandName === null', '证据根审计脚本必须防止命令证据冒充阶段证据。', ); assertIncludes( content, '--require-command :', '证据根审计脚本 usage 必须公开命令证据强制审计参数。', ); assertIncludes( content, '--require-phase-direct-live-access-log', '证据根审计脚本 usage 必须公开 direct live access log 摘要强制审计参数。', ); assertIncludes( content, '--require-phase-direct-live-static-headers', '证据根审计脚本 usage 必须公开 direct live 静态响应头摘要强制审计参数。', ); assertIncludes( content, '--require-phase-pingora-env-shadow', '证据根审计脚本 usage 必须公开 Pingora env shadow 摘要强制审计参数。', ); assertIncludes( content, 'manifest.summary.pingoraEnvShadow', '证据根审计脚本必须能校验 Pingora env shadow 摘要。', ); assertIncludes( content, '--require-command-executable ::', '证据根审计脚本 usage 必须公开命令证据真实脚本身份强制参数。', ); assertIncludes( content, '--require-command-arg ::', '证据根审计脚本 usage 必须公开命令证据必需参数强制参数。', ); assertIncludes( content, 'manifest.expectedExecutable 必须是', '证据根审计脚本必须能校验命令证据 expectedExecutable。', ); assertIncludes( content, 'readCommandRecord', '证据根审计脚本必须读取独立命令记录。', ); assertIncludes( content, 'compareCommandRecords', '证据根审计脚本必须阻断 manifest.command 与 command-record.json 语义漂移。', ); assertIncludes( content, 'checkCommandRecordTimes', '证据根审计脚本必须校验命令记录时间字段。', ); assertIncludes( content, 'stdoutPath', '证据根审计脚本必须校验命令 stdout 引用一致性。', ); assertIncludes( content, 'stderrPath', '证据根审计脚本必须校验命令 stderr 引用一致性。', ); assertIncludes( content, 'jsonValuesEqual', '证据根审计脚本必须校验独立命令记录参数一致性。', ); assertIncludes( content, "'args'", '证据根审计脚本必须把 args 纳入命令记录一致性比较。', ); assertIncludes( content, '最新证据目录 manifest 验真失败', '证据根审计脚本必须在最新证据损坏时给出明确诊断。', ); assertIncludes( content, 'manifest.summary.status 必须是 OK', '证据根审计脚本必须检查 manifest 业务结论。', ); assertIncludes( content, 'pre-cutover', '证据根审计脚本必须内置标准切换时间线。', ); assertIncludes( content, '疑似混入不同切换窗口证据', '证据根审计脚本必须在标准切换时间线倒序时给出明确诊断。', ); assertIncludes( content, '--timeline-max-span-ms', '证据根审计脚本必须公开标准切换时间线最大跨度配置。', ); assertIncludes( content, '--require-cutover-run-id', '证据根审计脚本必须公开切换批次 ID 强制参数。', ); assertIncludes( content, 'manifest.cutoverRunId', '证据根审计脚本必须读取并校验 manifest.cutoverRunId。', ); assertIncludes( content, 'manifest.generatedAt 必须是合法 ISO 时间', '证据根审计脚本必须拒绝缺少合法 generatedAt 的证据。', ); assertIncludes( content, 'manifest.schemaVersion 必须是 1', '证据根审计脚本必须拒绝未知 manifest schemaVersion。', ); assertIncludes( content, "['schemaVersion', 1]", '证据根审计脚本必须校验命令记录 schemaVersion。', ); assertIncludes( content, '不能依赖目录 mtime', '证据根审计脚本不能用目录 mtime 兜底选择最新证据。', ); assertIncludes( content, '不能按目录名打平选择', '证据根审计脚本必须拒绝同一阶段或命令的重复最新 generatedAt。', ); assertIncludes( content, 'GENARRATIVE_PINGORA_CUTOVER_EVIDENCE_TIMELINE_MAX_SPAN_MS', '证据根审计脚本必须支持用 env 覆盖标准切换时间线最大跨度。', ); assertIncludes( content, '时间跨度', '证据根审计脚本必须在标准切换时间线跨度过大时给出明确诊断。', ); if (content.includes('writeFile') || content.includes('chmod(')) { failures.push('证据根审计脚本必须保持只读,不能写文件或改权限。'); } if (content.includes('mtimeMs')) { failures.push('证据根审计脚本不能依赖目录 mtimeMs 选择最新证据。'); } } function assertRequiredPhasesSucceed() { const root = path.join(tmpRoot, 'required-ok'); prepareBundle(root, '20260617T010000Z-pre-cutover', 'pre-cutover'); prepareBundle(root, '20260617T020000Z-post-enable', 'post-enable', { directLiveSummary: true, }); prepareBundle(root, '20260617T030000Z-post-rollback', 'post-rollback'); const result = runAudit([ '--evidence-root', root, '--require-phase', 'pre-cutover', '--require-phase', 'post-enable', '--require-phase', 'post-rollback', ]); assertStatus(result, 0, '三阶段证据齐全且 manifest 验真通过时应成功。'); if (result.status !== 0) { return; } const output = parseJson(result.stdout, '三阶段证据审计 stdout'); assertEqual(output.ok, true, '三阶段证据审计 stdout 必须 ok=true。'); assertEqual(output.checkedCount, 3, '三阶段证据审计必须检查三项 phase。'); assertEqual(output.failedCount, 0, '三阶段证据审计不能有失败项。'); assertEqual( output.phases?.every((phase) => phase.status === 'OK'), true, '三阶段证据审计每个 phase 都应 OK。', ); assertEqual( output.phases?.every((phase) => phase.verify?.requireSummaryOk === true), true, '三阶段证据审计必须让 verifier 启用 --require-summary-ok。', ); const postEnablePhase = output.phases?.find( (phase) => phase.phase === 'post-enable', ); assertEqual( postEnablePhase?.directLiveStaticHeaders?.normal?.cacheControl, 'no-cache', '证据根审计输出必须提升 post-enable direct live 普通静态 Cache-Control 摘要。', ); assertEqual( postEnablePhase?.directLiveStaticHeaders?.fingerprinted?.cacheControl, 'public, max-age=31536000, immutable', '证据根审计输出必须提升 post-enable direct live 指纹静态 Cache-Control 摘要。', ); assertEqual( postEnablePhase?.directLiveAccessLog?.matchedCount, 20, '证据根审计输出必须提升 post-enable direct live access log 摘要。', ); } function assertRequirePhaseDirectLiveAccessLogSucceeds() { const root = path.join(tmpRoot, 'required-access-log-ok'); prepareBundle(root, '20260617T010000Z-pre-cutover', 'pre-cutover'); prepareBundle(root, '20260617T020000Z-post-enable', 'post-enable', { directLiveSummary: true, directLiveStaticHeaders: false, }); const result = runAudit([ '--evidence-root', root, '--require-phase', 'post-enable', '--require-phase-direct-live-access-log', 'post-enable', ]); assertStatus( result, 0, '要求 post-enable access log 摘要且 manifest 已包含完整 directLiveAccessLog 时应成功。', ); if (result.status !== 0) { return; } const output = parseJson(result.stdout, 'access log 摘要强制审计 stdout'); assertEqual( output.phases?.[0]?.requiredDirectLiveAccessLog, true, 'access log 摘要强制审计输出必须标记该 phase 已要求 direct live access log 摘要。', ); assertEqual( output.phases?.[0]?.manifestCheck?.directLiveAccessLogCheck?.ok, true, 'access log 摘要强制审计必须输出 directLiveAccessLogCheck.ok=true。', ); assertEqual( output.summary?.directLiveEvidence?.[0]?.staticHeaders?.ok, null, '只要求 access log 且没有静态头摘要时 operator summary 不应把 staticHeaders 标成 false。', ); } function assertRequirePhaseDirectLiveAccessLogFailsWhenMissing() { const root = path.join(tmpRoot, 'required-access-log-missing'); prepareBundle(root, '20260617T020000Z-post-enable', 'post-enable'); const result = runAudit([ '--evidence-root', root, '--require-phase', 'post-enable', '--require-phase-direct-live-access-log', 'post-enable', ]); assertStatus( result, 1, '要求 post-enable access log 摘要但 manifest 缺少 directLiveAccessLog 时必须失败。', ); assertIncludes( result.stdout, 'manifest.summary.directLiveAccessLog', '缺少 access log 摘要时必须给出 directLiveAccessLog 诊断。', ); } function assertRequirePhaseDirectLiveAccessLogFailsWhenPhaseMissing() { const root = path.join(tmpRoot, 'required-access-log-phase-missing'); prepareBundle(root, '20260617T010000Z-pre-cutover', 'pre-cutover'); const result = runAudit([ '--evidence-root', root, '--require-phase', 'post-enable', '--require-phase-direct-live-access-log', 'post-enable', ]); assertStatus( result, 1, '要求 post-enable access log 摘要但 post-enable 阶段证据缺失时必须失败。', ); const output = parseJson(result.stdout, '缺 post-enable 阶段 access log 审计 stdout'); assertEqual( output.summary?.directLiveEvidence?.[0]?.phase, 'post-enable', '缺 post-enable 阶段时 operator summary 仍必须列出 direct live 证据要求。', ); assertEqual( output.summary?.directLiveEvidence?.[0]?.accessLog?.ok, false, '缺 post-enable 阶段时 operator summary accessLog.ok 必须是 false。', ); assertIncludes( output.summary?.directLiveEvidence?.[0]?.accessLog?.reason || '', '没有找到该阶段的证据 manifest', '缺 post-enable 阶段时 operator summary 必须给出缺阶段 reason。', ); } function assertRequirePhaseDirectLiveStaticHeadersSucceeds() { const root = path.join(tmpRoot, 'required-static-headers-ok'); prepareBundle(root, '20260617T010000Z-pre-cutover', 'pre-cutover'); prepareBundle(root, '20260617T020000Z-post-enable', 'post-enable', { directLiveSummary: true, }); const result = runAudit([ '--evidence-root', root, '--require-phase', 'post-enable', '--require-phase-direct-live-static-headers', 'post-enable', ]); assertStatus( result, 0, '要求 post-enable 静态头摘要且 manifest 已包含完整 directLiveStaticHeaders 时应成功。', ); if (result.status !== 0) { return; } const output = parseJson(result.stdout, '静态头摘要强制审计 stdout'); assertEqual( output.phases?.[0]?.requiredDirectLiveStaticHeaders, true, '静态头摘要强制审计输出必须标记该 phase 已要求 direct live 静态头摘要。', ); assertEqual( output.phases?.[0]?.manifestCheck?.directLiveStaticHeadersCheck?.ok, true, '静态头摘要强制审计必须输出 directLiveStaticHeadersCheck.ok=true。', ); } function assertRequirePhaseDirectLiveStaticHeadersFailsWhenMissing() { const root = path.join(tmpRoot, 'required-static-headers-missing'); prepareBundle(root, '20260617T020000Z-post-enable', 'post-enable'); const result = runAudit([ '--evidence-root', root, '--require-phase', 'post-enable', '--require-phase-direct-live-static-headers', 'post-enable', ]); assertStatus( result, 1, '要求 post-enable 静态头摘要但 manifest 缺少 directLiveStaticHeaders 时必须失败。', ); assertIncludes( result.stdout, 'manifest.summary.directLiveStaticHeaders', '缺少静态头摘要时必须给出 directLiveStaticHeaders 诊断。', ); } function assertRequirePhasePingoraEnvShadowSucceeds() { const root = path.join(tmpRoot, 'required-pingora-env-shadow-ok'); prepareBundle(root, '20260617T030000Z-post-rollback', 'post-rollback', { pingoraEnvShadow: true, }); const result = runAudit([ '--evidence-root', root, '--require-phase', 'post-rollback', '--require-phase-pingora-env-shadow', 'post-rollback', ]); assertStatus( result, 0, '要求 post-rollback Pingora env shadow 摘要且 manifest 已包含完整摘要时应成功。', ); if (result.status !== 0) { return; } const output = parseJson(result.stdout, 'Pingora env shadow 强制审计 stdout'); assertEqual( output.phases?.[0]?.requiredPingoraEnvShadow, true, 'Pingora env shadow 强制审计输出必须标记该 phase 已要求 shadow 摘要。', ); assertEqual( output.phases?.[0]?.manifestCheck?.pingoraEnvShadowCheck?.ok, true, 'Pingora env shadow 强制审计必须输出 pingoraEnvShadowCheck.ok=true。', ); assertEqual( output.summary?.pingoraEnvShadowEvidence?.[0]?.shadow?.ok, true, 'operator summary 必须标记 Pingora env shadow 摘要通过。', ); } function assertRequirePhasePingoraEnvShadowFailsWhenMissing() { const root = path.join(tmpRoot, 'required-pingora-env-shadow-missing'); prepareBundle(root, '20260617T030000Z-post-rollback', 'post-rollback', { pingoraEnvShadow: false, }); const result = runAudit([ '--evidence-root', root, '--require-phase', 'post-rollback', '--require-phase-pingora-env-shadow', 'post-rollback', ]); assertStatus( result, 1, '要求 post-rollback Pingora env shadow 摘要但 manifest 缺少 pingoraEnvShadow 时必须失败。', ); assertIncludes( result.stdout, 'manifest.summary.pingoraEnvShadow', '缺少 Pingora env shadow 摘要时必须给出 pingoraEnvShadow 诊断。', ); const output = parseJson(result.stdout, '缺 Pingora env shadow 摘要审计 stdout'); assertEqual( output.summary?.pingoraEnvShadowEvidence?.[0]?.shadow?.ok, false, '缺 Pingora env shadow 摘要时 operator summary 必须标记 shadow.ok=false。', ); } function assertRequirePhasePingoraEnvShadowFailsWhenDirectLowPortsRemain() { const root = path.join(tmpRoot, 'required-pingora-env-shadow-low-ports'); prepareBundle(root, '20260617T030000Z-post-rollback', 'post-rollback', { pingoraEnvShadow: { present: true, 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', mode: 'direct', shadowReady: false, ok: false, diagnostics: [ 'tlsListen 应为空,实际 0.0.0.0:443', 'httpRedirectListen 应为空,实际 0.0.0.0:80', 'tlsCertFile 应为空,实际 /etc/genarrative/pingora-tls/example/fullchain.pem', ], }, }); const result = runAudit([ '--evidence-root', root, '--require-phase', 'post-rollback', '--require-phase-pingora-env-shadow', 'post-rollback', ]); assertStatus( result, 1, '要求 post-rollback Pingora env shadow 摘要但低端口监听仍残留时必须失败。', ); assertIncludes( result.stdout, 'tlsListen 必须为空', '低端口 TLS 残留时必须给出 tlsListen 诊断。', ); assertIncludes( result.stdout, 'httpRedirectListen 必须为空', '低端口 HTTP redirect 残留时必须给出 httpRedirectListen 诊断。', ); assertIncludes( result.stdout, 'tlsCertFile 必须为空', 'TLS 证书路径残留时必须给出 tlsCertFile 诊断。', ); assertIncludes( result.stdout, 'shadowReady 必须为 true', 'snapshot 姿态不是 shadow 时必须给出 shadowReady 诊断。', ); } function assertRequiredCommandsSucceed() { const root = path.join(tmpRoot, 'required-command-ok'); prepareBundle(root, '20260617T010000Z-pre-cutover', 'pre-cutover'); prepareCommandBundle( root, '20260617T020000Z-enable-apply-pingora-direct-enable-apply', 'enable-apply', 'pingora-direct-enable-apply', ); prepareCommandBundle( root, '20260617T030000Z-rollback-apply-pingora-direct-rollback-apply', 'rollback-apply', 'pingora-direct-rollback-apply', ); const result = runAudit([ '--evidence-root', root, '--require-command', 'enable-apply:pingora-direct-enable-apply', '--require-command', 'rollback-apply:pingora-direct-rollback-apply', '--require-command-executable', 'enable-apply:pingora-direct-enable-apply:/opt/genarrative/current/scripts/deploy/pingora-direct-enable.sh', '--require-command-executable', 'rollback-apply:pingora-direct-rollback-apply:/opt/genarrative/current/scripts/deploy/pingora-direct-rollback.sh', ]); assertStatus(result, 0, '要求的 enable / rollback 命令证据齐全时应成功。'); if (result.status !== 0) { return; } const output = parseJson(result.stdout, '命令证据审计 stdout'); assertEqual(output.ok, true, '命令证据审计 stdout 必须 ok=true。'); assertEqual(output.checkedCount, 2, '命令证据审计必须检查两项 command。'); assertEqual(output.phases?.length, 0, '只要求 command 时不应隐式检查 phase。'); assertEqual( output.commands?.every((command) => command.status === 'OK'), true, '命令证据审计每个 command 都应 OK。', ); assertEqual( output.commands?.every( (command) => command.verify?.requireSummaryOk === true, ), true, '命令证据审计必须让 verifier 启用 --require-summary-ok。', ); assertEqual( output.commands?.[0]?.commandName, 'pingora-direct-enable-apply', '命令证据审计必须输出 commandName。', ); assertEqual( output.commands?.[0]?.requiredExecutable, '/opt/genarrative/current/scripts/deploy/pingora-direct-enable.sh', '命令证据审计必须输出 requiredExecutable。', ); assertEqual( Array.isArray(output.commands?.[0]?.requiredArgs), true, '命令证据审计必须输出 requiredArgs。', ); } function assertRequiredCommandExecutableImplicitlyRequiresCommand() { const root = path.join(tmpRoot, 'required-command-executable-only-ok'); prepareCommandBundle( root, '20260617T020000Z-enable-apply-pingora-direct-enable-apply', 'enable-apply', 'pingora-direct-enable-apply', ); const result = runAudit([ '--evidence-root', root, '--require-command-executable', 'enable-apply:pingora-direct-enable-apply:/opt/genarrative/current/scripts/deploy/pingora-direct-enable.sh', ]); assertStatus( result, 0, '--require-command-executable 必须隐式要求对应命令证据并通过身份校验。', ); if (result.status !== 0) { return; } const output = parseJson(result.stdout, '仅要求命令真实脚本身份审计 stdout'); assertEqual( output.checkedCount, 1, '--require-command-executable 必须让总审计检查对应 command。', ); assertEqual( output.commands?.[0]?.requiredExecutable, '/opt/genarrative/current/scripts/deploy/pingora-direct-enable.sh', '--require-command-executable 隐式命令审计必须输出 requiredExecutable。', ); } function assertRequiredCommandArgImplicitlyRequiresCommand() { const root = path.join(tmpRoot, 'required-command-arg-only-ok'); prepareCommandBundle( root, '20260617T020000Z-enable-apply-pingora-direct-enable-apply', 'enable-apply', 'pingora-direct-enable-apply', ); const result = runAudit([ '--evidence-root', root, '--require-command-arg', 'enable-apply:pingora-direct-enable-apply:--apply', ]); assertStatus( result, 0, '--require-command-arg 必须隐式要求对应命令证据并通过 args 校验。', ); if (result.status !== 0) { return; } const output = parseJson(result.stdout, '仅要求命令必需参数审计 stdout'); assertEqual( output.checkedCount, 1, '--require-command-arg 必须让总审计检查对应 command。', ); assertEqual( output.commands?.[0]?.requiredArgs?.includes('--apply'), true, '--require-command-arg 隐式命令审计必须输出 requiredArgs。', ); } function assertRequiredCommandExecutableMismatchFails() { const root = path.join(tmpRoot, 'required-command-executable-mismatch'); prepareCommandBundle( root, '20260617T020000Z-enable-apply-pingora-direct-enable-apply', 'enable-apply', 'pingora-direct-enable-apply', ); const result = runAudit([ '--evidence-root', root, '--require-command', 'enable-apply:pingora-direct-enable-apply', '--require-command-executable', 'enable-apply:pingora-direct-enable-apply:/opt/genarrative/current/scripts/deploy/pingora-direct-rollback.sh', ]); assertStatus(result, 1, '命令证据真实脚本与要求不一致时必须失败。'); assertIncludes( result.stdout, 'manifest.expectedExecutable 必须是', 'expectedExecutable 不匹配必须给出明确诊断。', ); assertIncludes( result.stdout, 'command.executable 必须是', 'command.executable 不匹配必须给出明确诊断。', ); } function assertInvalidTopLevelExpectedExecutableFails() { const root = path.join(tmpRoot, 'invalid-top-level-expected-executable'); const bundleDir = prepareCommandBundle( root, '20260617T020000Z-enable-apply-pingora-direct-enable-apply', 'enable-apply', 'pingora-direct-enable-apply', ); const manifestPath = path.join(bundleDir, 'manifest.json'); const manifest = readJson(manifestPath, '命令证据 manifest'); manifest.expectedExecutable = ''; writeJson(manifestPath, manifest); const result = runAudit([ '--evidence-root', root, '--require-command', 'enable-apply:pingora-direct-enable-apply', ]); assertStatus( result, 1, '顶层 manifest.expectedExecutable 为空时必须作为坏 manifest 失败,不能退化成未声明字段。', ); assertIncludes( result.stdout, 'manifest.expectedExecutable 必须是绝对路径', '顶层 expectedExecutable 为空必须给出字段诊断。', ); } function assertInvalidEmbeddedExpectedExecutableFails() { const root = path.join(tmpRoot, 'invalid-embedded-expected-executable'); const bundleDir = prepareCommandBundle( root, '20260617T020000Z-enable-apply-pingora-direct-enable-apply', 'enable-apply', 'pingora-direct-enable-apply', ); const manifestPath = path.join(bundleDir, 'manifest.json'); const manifest = readJson(manifestPath, '命令证据 manifest'); delete manifest.expectedExecutable; manifest.command = { ...manifest.command, expectedExecutable: 'scripts/deploy/pingora-direct-enable.sh', }; writeJson(manifestPath, manifest); const result = runAudit([ '--evidence-root', root, '--require-command', 'enable-apply:pingora-direct-enable-apply', ]); assertStatus( result, 1, '内嵌 manifest.command.expectedExecutable 为相对路径时必须作为坏 manifest 失败。', ); assertIncludes( result.stdout, 'manifest.command.expectedExecutable 必须是绝对路径', '内嵌 expectedExecutable 相对路径必须给出字段诊断。', ); } function assertManifestCommandExecutableMismatchExpectedFails() { const root = path.join(tmpRoot, 'command-executable-mismatch-expected'); const bundleDir = prepareCommandBundle( root, '20260617T020000Z-enable-apply-pingora-direct-enable-apply', 'enable-apply', 'pingora-direct-enable-apply', ); const manifestPath = path.join(bundleDir, 'manifest.json'); const manifest = readJson(manifestPath, '命令证据 manifest'); const commandRecordPath = path.join(bundleDir, 'command-record.json'); const commandRecord = readJson(commandRecordPath, '命令记录 JSON'); const rollbackExecutable = '/opt/genarrative/current/scripts/deploy/pingora-direct-rollback.sh'; const driftedRecord = { ...commandRecord, executable: rollbackExecutable, command: `${rollbackExecutable} --apply`, }; writeJson(commandRecordPath, driftedRecord); manifest.command = driftedRecord; manifest.files.commandRecord = metadataFor(commandRecordPath); writeJson(manifestPath, manifest); const result = runAudit([ '--evidence-root', root, '--require-command', 'enable-apply:pingora-direct-enable-apply', ]); assertStatus( result, 1, '命令证据声明 expectedExecutable 后,真实 executable 与之不一致时必须失败。', ); assertIncludes( result.stdout, 'manifest.command.executable 必须是', 'manifest.command executable 与 expectedExecutable 漂移必须给出明确诊断。', ); assertIncludes( result.stdout, 'command-record.json.executable 必须是', 'command-record executable 与 expectedExecutable 漂移必须给出明确诊断。', ); } function assertRelativeCommandExecutableFails() { const root = path.join(tmpRoot, 'relative-command-executable'); const bundleDir = prepareCommandBundle( root, '20260617T020000Z-enable-apply-pingora-direct-enable-apply', 'enable-apply', 'pingora-direct-enable-apply', ); const manifestPath = path.join(bundleDir, 'manifest.json'); const manifest = readJson(manifestPath, '命令证据 manifest'); const commandRecordPath = path.join(bundleDir, 'command-record.json'); const commandRecord = readJson(commandRecordPath, '命令记录 JSON'); const relativeExecutable = 'scripts/deploy/pingora-direct-enable.sh'; const driftedRecord = { ...commandRecord, executable: relativeExecutable, command: `${relativeExecutable} --apply`, }; writeJson(commandRecordPath, driftedRecord); manifest.command = driftedRecord; manifest.files.commandRecord = metadataFor(commandRecordPath); writeJson(manifestPath, manifest); const result = runAudit([ '--evidence-root', root, '--require-command', 'enable-apply:pingora-direct-enable-apply', ]); assertStatus( result, 1, '命令证据真实 executable 为相对路径时必须失败。', ); assertIncludes( result.stdout, 'manifest.command.executable 必须是绝对路径', 'manifest.command executable 相对路径必须给出字段诊断。', ); assertIncludes( result.stdout, 'command-record.json.executable 必须是绝对路径', 'command-record executable 相对路径必须给出字段诊断。', ); } function assertMissingCommandExecutableFails() { const root = path.join(tmpRoot, 'missing-command-executable'); const bundleDir = prepareCommandBundle( root, '20260617T020000Z-enable-apply-pingora-direct-enable-apply', 'enable-apply', 'pingora-direct-enable-apply', ); const manifestPath = path.join(bundleDir, 'manifest.json'); const manifest = readJson(manifestPath, '命令证据 manifest'); const commandRecordPath = path.join(bundleDir, 'command-record.json'); const commandRecord = readJson(commandRecordPath, '命令记录 JSON'); delete commandRecord.expectedExecutable; delete commandRecord.executable; commandRecord.command = '/opt/genarrative/current/scripts/deploy/pingora-direct-enable.sh --apply'; writeJson(commandRecordPath, commandRecord); delete manifest.expectedExecutable; manifest.command = { ...commandRecord }; manifest.files.commandRecord = metadataFor(commandRecordPath); writeJson(manifestPath, manifest); const result = runAudit([ '--evidence-root', root, '--require-command', 'enable-apply:pingora-direct-enable-apply', ]); assertStatus( result, 1, '命令证据缺少真实 executable 时必须失败,即使 command 字符串存在。', ); assertIncludes( result.stdout, 'manifest.command.executable 必须是绝对路径', 'manifest.command 缺少 executable 必须给出字段诊断。', ); assertIncludes( result.stdout, 'command-record.json.executable 必须是绝对路径', 'command-record 缺少 executable 必须给出字段诊断。', ); } function assertRequiredCommandArgMissingFails() { const root = path.join(tmpRoot, 'required-command-arg-missing'); const bundleDir = prepareCommandBundle( root, '20260617T020000Z-enable-apply-pingora-direct-enable-apply', 'enable-apply', 'pingora-direct-enable-apply', ); const manifestPath = path.join(bundleDir, 'manifest.json'); const manifest = readJson(manifestPath, '命令证据 manifest'); const commandRecordPath = path.join(bundleDir, 'command-record.json'); const commandRecord = readJson(commandRecordPath, '命令记录 JSON'); const dryRunRecord = { ...commandRecord, args: ['--dry-run'], command: `${commandRecord.executable} --dry-run`, }; writeJson(commandRecordPath, dryRunRecord); manifest.command = dryRunRecord; manifest.files.commandRecord = metadataFor(commandRecordPath); writeJson(manifestPath, manifest); const result = runAudit([ '--evidence-root', root, '--require-command-arg', 'enable-apply:pingora-direct-enable-apply:--apply', ]); assertStatus(result, 1, '命令证据缺少必需 --apply 参数时必须失败。'); assertIncludes( result.stdout, 'manifest.command.args 必须包含 \\"--apply\\"', 'manifest.command 缺少必需参数必须给出明确诊断。', ); assertIncludes( result.stdout, 'command-record.json.args 必须包含 \\"--apply\\"', 'command-record 缺少必需参数必须给出明确诊断。', ); } function assertCommandRecordMismatchFails() { const root = path.join(tmpRoot, 'command-record-mismatch'); const bundleDir = prepareCommandBundle( root, '20260617T020000Z-enable-apply-pingora-direct-enable-apply', 'enable-apply', 'pingora-direct-enable-apply', ); const manifestPath = path.join(bundleDir, 'manifest.json'); const manifest = readJson(manifestPath, '命令证据 manifest'); const driftedRecord = { ...manifest.command, expectedExecutable: '/opt/genarrative/current/scripts/deploy/pingora-direct-rollback.sh', executable: '/opt/genarrative/current/scripts/deploy/pingora-direct-rollback.sh', command: '/opt/genarrative/current/scripts/deploy/pingora-direct-rollback.sh --apply', }; writeJson(path.join(bundleDir, 'command-record.json'), driftedRecord); manifest.files.commandRecord = metadataFor( path.join(bundleDir, 'command-record.json'), ); writeJson(manifestPath, manifest); const result = runAudit([ '--evidence-root', root, '--require-command', 'enable-apply:pingora-direct-enable-apply', '--require-command-executable', 'enable-apply:pingora-direct-enable-apply:/opt/genarrative/current/scripts/deploy/pingora-direct-enable.sh', ]); assertStatus( result, 1, 'command-record.json 与 manifest.command 语义漂移时必须失败。', ); assertIncludes( result.stdout, 'command-record.json.executable 必须是', 'command-record executable 漂移必须给出明确诊断。', ); assertIncludes( result.stdout, 'manifest.command.executable 与 command-record.json.executable 必须一致', 'manifest.command 与 command-record 不一致必须给出明确诊断。', ); } function assertCommandRecordTimelineMismatchFails() { const root = path.join(tmpRoot, 'command-record-timeline-mismatch'); prepareCommandBundle( root, '20260617T020000Z-enable-apply-pingora-direct-enable-apply', 'enable-apply', 'pingora-direct-enable-apply', { generatedAt: '2026-06-17T01:59:59.000Z', startedAt: '2026-06-17T02:00:00.000Z', finishedAt: '2026-06-17T02:00:03.000Z', }, ); const result = runAudit([ '--evidence-root', root, '--require-command', 'enable-apply:pingora-direct-enable-apply', ]); assertStatus( result, 1, 'manifest.generatedAt 早于命令 finishedAt 时必须失败。', ); assertIncludes( result.stdout, 'manifest.generatedAt 不能早于 manifest.command.finishedAt', 'manifest.command finishedAt 晚于 generatedAt 必须给出明确诊断。', ); assertIncludes( result.stdout, 'manifest.generatedAt 不能早于 command-record.json.finishedAt', 'command-record finishedAt 晚于 generatedAt 必须给出明确诊断。', ); } function assertCommandRecordOutputPathMismatchFails() { const root = path.join(tmpRoot, 'command-record-output-path-mismatch'); const bundleDir = prepareCommandBundle( root, '20260617T020000Z-enable-apply-pingora-direct-enable-apply', 'enable-apply', 'pingora-direct-enable-apply', ); const commandRecordPath = path.join(bundleDir, 'command-record.json'); const commandRecord = readJson(commandRecordPath, '命令记录 JSON'); writeJson(commandRecordPath, { ...commandRecord, stdoutPath: 'manual.stdout.txt', }); const manifestPath = path.join(bundleDir, 'manifest.json'); const manifest = readJson(manifestPath, '命令证据 manifest'); manifest.files.commandRecord = metadataFor(commandRecordPath); writeJson(manifestPath, manifest); const result = runAudit([ '--evidence-root', root, '--require-command', 'enable-apply:pingora-direct-enable-apply', ]); assertStatus( result, 1, 'command-record.json 的 stdoutPath 与 manifest.files.stdout.path 漂移时必须失败。', ); assertIncludes( result.stdout, 'command-record.json.stdoutPath 必须是', 'command-record stdoutPath 漂移必须给出明确诊断。', ); assertIncludes( result.stdout, 'manifest.command.stdoutPath 与 command-record.json.stdoutPath 必须一致', 'manifest.command 与 command-record stdoutPath 不一致必须给出明确诊断。', ); } function assertCommandRecordArgsMismatchFails() { const root = path.join(tmpRoot, 'command-record-args-mismatch'); const bundleDir = prepareCommandBundle( root, '20260617T020000Z-enable-apply-pingora-direct-enable-apply', 'enable-apply', 'pingora-direct-enable-apply', ); const commandRecordPath = path.join(bundleDir, 'command-record.json'); const commandRecord = readJson(commandRecordPath, '命令记录 JSON'); writeJson(commandRecordPath, { ...commandRecord, args: ['--dry-run'], command: `${commandRecord.executable} --dry-run`, }); const manifestPath = path.join(bundleDir, 'manifest.json'); const manifest = readJson(manifestPath, '命令证据 manifest'); manifest.files.commandRecord = metadataFor(commandRecordPath); writeJson(manifestPath, manifest); const result = runAudit([ '--evidence-root', root, '--require-command', 'enable-apply:pingora-direct-enable-apply', ]); assertStatus( result, 1, 'command-record.json 的 args / command 与 manifest.command 漂移时必须失败。', ); assertIncludes( result.stdout, 'manifest.command.args 与 command-record.json.args 必须一致', 'command-record args 漂移必须给出明确诊断。', ); assertIncludes( result.stdout, 'manifest.command.command 与 command-record.json.command 必须一致', 'command-record command 漂移必须给出明确诊断。', ); } function assertCommandRecordArgsControlCharacterFails() { const root = path.join(tmpRoot, 'command-record-args-control-character'); const bundleDir = prepareCommandBundle( root, '20260617T020000Z-enable-apply-pingora-direct-enable-apply', 'enable-apply', 'pingora-direct-enable-apply', ); const manifestPath = path.join(bundleDir, 'manifest.json'); const manifest = readJson(manifestPath, '命令证据 manifest'); const commandRecordPath = path.join(bundleDir, 'command-record.json'); const commandRecord = readJson(commandRecordPath, '命令记录 JSON'); const taintedRecord = { ...commandRecord, args: ['--apply', '--note=line1\nline2'], command: `${commandRecord.executable} --apply --note=line1`, }; writeJson(commandRecordPath, taintedRecord); manifest.command = taintedRecord; manifest.files.commandRecord = metadataFor(commandRecordPath); writeJson(manifestPath, manifest); const result = runAudit([ '--evidence-root', root, '--require-command', 'enable-apply:pingora-direct-enable-apply', ]); assertStatus( result, 1, '命令证据 args 含换行控制字符时必须失败。', ); assertIncludes( result.stdout, 'manifest.command.args 不能包含换行或 NUL 字符', 'manifest.command args 控制字符必须给出明确诊断。', ); assertIncludes( result.stdout, 'command-record.json.args 不能包含换行或 NUL 字符', 'command-record args 控制字符必须给出明确诊断。', ); } function assertCommandNameMismatchFails() { const root = path.join(tmpRoot, 'command-name-mismatch'); const bundleDir = prepareCommandBundle( root, '20260617T020000Z-enable-apply-pingora-direct-enable-apply', 'enable-apply', 'pingora-direct-enable-apply', ); const manifestPath = path.join(bundleDir, 'manifest.json'); const manifest = readJson(manifestPath, '命令证据 manifest'); manifest.command = { ...manifest.command, name: 'pingora-direct-rollback-apply', }; writeJson(manifestPath, manifest); const result = runAudit([ '--evidence-root', root, '--require-command', 'enable-apply:pingora-direct-enable-apply', ]); assertStatus( result, 1, 'manifest.commandName 与 manifest.command.name 漂移时必须失败。', ); assertIncludes( result.stdout, 'manifest.commandName 与 manifest.command.name 必须一致', '命令名双字段漂移必须给出明确诊断。', ); assertIncludes( result.stdout, 'pingora-direct-enable-apply', '命令名双字段漂移诊断必须包含顶层 commandName。', ); assertIncludes( result.stdout, 'pingora-direct-rollback-apply', '命令名双字段漂移诊断必须包含内嵌 command.name。', ); } function assertInvalidTopLevelCommandNameFails() { const root = path.join(tmpRoot, 'invalid-top-level-command-name'); const bundleDir = prepareCommandBundle( root, '20260617T020000Z-enable-apply-pingora-direct-enable-apply', 'enable-apply', 'pingora-direct-enable-apply', ); const manifestPath = path.join(bundleDir, 'manifest.json'); const manifest = readJson(manifestPath, '命令证据 manifest'); manifest.commandName = ''; delete manifest.command.name; writeJson(manifestPath, manifest); const result = runAudit([ '--evidence-root', root, '--require-phase', 'enable-apply', ]); assertStatus( result, 1, '顶层 manifest.commandName 为空时必须作为坏 manifest 失败,不能退化成阶段证据。', ); assertIncludes( result.stdout, 'manifest.commandName 只能包含 ASCII', '顶层 commandName 为空必须给出字段诊断。', ); } function assertInvalidEmbeddedCommandNameFails() { const root = path.join(tmpRoot, 'invalid-embedded-command-name'); const bundleDir = prepareCommandBundle( root, '20260617T020000Z-enable-apply-pingora-direct-enable-apply', 'enable-apply', 'pingora-direct-enable-apply', ); const manifestPath = path.join(bundleDir, 'manifest.json'); const manifest = readJson(manifestPath, '命令证据 manifest'); delete manifest.commandName; manifest.command = { ...manifest.command, name: '', }; writeJson(manifestPath, manifest); const result = runAudit([ '--evidence-root', root, '--require-command', 'enable-apply:pingora-direct-enable-apply', ]); assertStatus( result, 1, '内嵌 manifest.command.name 为空时必须作为坏 manifest 失败。', ); assertIncludes( result.stdout, 'manifest.command.name 只能包含 ASCII', '内嵌 command.name 为空必须给出字段诊断。', ); } function assertFullCutoverTimelineSucceed() { const root = path.join(tmpRoot, 'full-timeline-ok'); prepareFullCutoverTimeline(root, { preCutover: '2026-06-17T01:00:00.000Z', enableApply: '2026-06-17T02:00:00.000Z', healthPatrolDirect: '2026-06-17T02:10:00.000Z', postEnable: '2026-06-17T03:00:00.000Z', gatewayShadow: '2026-06-17T03:30:00.000Z', healthPatrolNginx: '2026-06-17T03:40:00.000Z', rollbackApply: '2026-06-17T04:00:00.000Z', postRollback: '2026-06-17T05:00:00.000Z', }); const result = runFullCutoverAudit(root); assertStatus(result, 0, '完整标准切换时间线顺序正确时应成功。'); if (result.status !== 0) { return; } const output = parseJson(result.stdout, '完整切换时间线审计 stdout'); assertEqual(output.timeline?.checked, true, '完整切换时间线必须被检查。'); assertEqual(output.timeline?.ok, true, '完整切换时间线顺序正确时 timeline.ok 必须为 true。'); assertEqual( output.timeline?.maxSpanMs, 86400000, '完整切换时间线默认最大跨度必须是 24 小时。', ); assertEqual( output.timeline?.spanMs, 14400000, '完整切换时间线必须输出实际跨度。', ); assertEqual(output.checkedCount, 8, '完整切换时间线总审计必须检查三阶段和五条命令。'); assertEqual( output.summary?.status, 'OK', '完整切换时间线总审计通过时 operator summary.status 必须是 OK。', ); assertEqual( output.summary?.directLiveEvidence?.[0]?.phase, 'post-enable', '完整切换时间线 operator summary 必须突出 post-enable direct live 证据。', ); assertEqual( output.summary?.directLiveEvidence?.[0]?.accessLog?.ok, true, '完整切换时间线 operator summary 必须标记 access log 摘要通过。', ); assertEqual( output.summary?.directLiveEvidence?.[0]?.staticHeaders?.ok, true, '完整切换时间线 operator summary 必须标记静态头摘要通过。', ); const postEnablePhase = output.phases?.find( (phase) => phase.phase === 'post-enable', ); assertEqual( postEnablePhase?.requiredDirectLiveStaticHeaders, true, '完整切换时间线总审计必须默认要求 post-enable direct live 静态头摘要。', ); assertEqual( postEnablePhase?.requiredDirectLiveAccessLog, true, '完整切换时间线总审计必须默认要求 post-enable direct live access log 摘要。', ); const postRollbackPhase = output.phases?.find( (phase) => phase.phase === 'post-rollback', ); assertEqual( postRollbackPhase?.requiredPingoraEnvShadow, true, '完整切换时间线总审计必须默认要求 post-rollback Pingora env shadow 摘要。', ); assertEqual( output.summary?.pingoraEnvShadowEvidence?.[0]?.shadow?.ok, true, '完整切换时间线 operator summary 必须标记 post-rollback Pingora env shadow 摘要通过。', ); } function assertFullCutoverTimelineFailsWhenPostEnableAccessLogMissing() { const root = path.join(tmpRoot, 'full-timeline-missing-access-log'); prepareFullCutoverTimeline( root, { preCutover: '2026-06-17T01:00:00.000Z', enableApply: '2026-06-17T02:00:00.000Z', healthPatrolDirect: '2026-06-17T02:10:00.000Z', postEnable: '2026-06-17T03:00:00.000Z', gatewayShadow: '2026-06-17T03:30:00.000Z', healthPatrolNginx: '2026-06-17T03:40:00.000Z', rollbackApply: '2026-06-17T04:00:00.000Z', postRollback: '2026-06-17T05:00:00.000Z', }, { postEnableDirectLiveAccessLog: false }, ); const result = runFullCutoverAudit(root); assertStatus( result, 1, '正式 runbook 风格总审计遇到缺 directLiveAccessLog 的 post-enable 旧证据包必须失败。', ); assertIncludes( result.stdout, 'manifest.summary.directLiveAccessLog', '完整时间线缺 post-enable access log 摘要时必须给出 directLiveAccessLog 诊断。', ); const output = parseJson(result.stdout, '缺 access log 完整时间线审计 stdout'); assertEqual( output.summary?.status, 'CRITICAL', '缺 access log 摘要时 operator summary.status 必须是 CRITICAL。', ); assertEqual( output.summary?.failedItems?.[0]?.phase, 'post-enable', '缺 access log 摘要时 operator summary.failedItems 必须指出 post-enable。', ); assertEqual( output.summary?.directLiveEvidence?.[0]?.accessLog?.ok, false, '缺 access log 摘要时 operator summary.directLiveEvidence 必须标记 accessLog.ok=false。', ); assertIncludes( output.summary?.directLiveEvidence?.[0]?.accessLog?.reason || '', '字段缺失或不是对象', '缺 access log 摘要时 operator summary 必须给出短 reason。', ); } function assertFullCutoverTimelineFailsWhenPostEnableStaticHeadersMissing() { const root = path.join(tmpRoot, 'full-timeline-missing-static-headers'); prepareFullCutoverTimeline( root, { preCutover: '2026-06-17T01:00:00.000Z', enableApply: '2026-06-17T02:00:00.000Z', healthPatrolDirect: '2026-06-17T02:10:00.000Z', postEnable: '2026-06-17T03:00:00.000Z', gatewayShadow: '2026-06-17T03:30:00.000Z', healthPatrolNginx: '2026-06-17T03:40:00.000Z', rollbackApply: '2026-06-17T04:00:00.000Z', postRollback: '2026-06-17T05:00:00.000Z', }, { postEnableDirectLiveSummary: false }, ); const result = runFullCutoverAudit(root); assertStatus( result, 1, '正式 runbook 风格总审计遇到缺 directLiveStaticHeaders 的 post-enable 旧证据包必须失败。', ); assertIncludes( result.stdout, 'manifest.summary.directLiveStaticHeaders', '完整时间线缺 post-enable 静态头摘要时必须给出 directLiveStaticHeaders 诊断。', ); const output = parseJson(result.stdout, '缺静态头完整时间线审计 stdout'); assertEqual( output.summary?.status, 'CRITICAL', '缺静态头摘要时 operator summary.status 必须是 CRITICAL。', ); assertEqual( output.summary?.failedItems?.[0]?.phase, 'post-enable', '缺静态头摘要时 operator summary.failedItems 必须指出 post-enable。', ); assertEqual( output.summary?.directLiveEvidence?.[0]?.staticHeaders?.ok, false, '缺静态头摘要时 operator summary.directLiveEvidence 必须标记 staticHeaders.ok=false。', ); assertIncludes( output.summary?.directLiveEvidence?.[0]?.staticHeaders?.reason || '', '字段缺失或不是对象', '缺静态头摘要时 operator summary 必须给出短 reason。', ); } function assertFullCutoverTimelineWithRunIdSucceed() { const root = path.join(tmpRoot, 'full-timeline-run-id-ok'); prepareFullCutoverTimeline( root, { preCutover: '2026-06-17T01:00:00.000Z', enableApply: '2026-06-17T02:00:00.000Z', healthPatrolDirect: '2026-06-17T02:10:00.000Z', postEnable: '2026-06-17T03:00:00.000Z', gatewayShadow: '2026-06-17T03:30:00.000Z', healthPatrolNginx: '2026-06-17T03:40:00.000Z', rollbackApply: '2026-06-17T04:00:00.000Z', postRollback: '2026-06-17T05:00:00.000Z', }, { cutoverRunId: 'cutover-20260617T010000Z' }, ); const result = runFullCutoverAudit(root, [ '--require-cutover-run-id', 'cutover-20260617T010000Z', ]); assertStatus(result, 0, '完整标准切换时间线同一 cutoverRunId 时应成功。'); if (result.status !== 0) { return; } const output = parseJson(result.stdout, '同批次完整切换时间线审计 stdout'); assertEqual( output.requiredCutoverRunId, 'cutover-20260617T010000Z', '审计 stdout 必须记录要求的 cutoverRunId。', ); assertEqual( output.phases?.every( (phase) => phase.cutoverRunId === 'cutover-20260617T010000Z', ), true, '每个 phase 输出都必须记录匹配的 cutoverRunId。', ); assertEqual( output.commands?.every( (command) => command.cutoverRunId === 'cutover-20260617T010000Z', ), true, '每个 command 输出都必须记录匹配的 cutoverRunId。', ); assertEqual( output.timeline?.items?.every( (item) => item.cutoverRunId === 'cutover-20260617T010000Z', ), true, '标准切换时间线条目必须输出匹配的 cutoverRunId。', ); } function assertLongCutoverTimelineFails() { const root = path.join(tmpRoot, 'long-timeline'); prepareFullCutoverTimeline(root, { preCutover: '2026-06-17T01:00:00.000Z', enableApply: '2026-06-17T02:00:00.000Z', healthPatrolDirect: '2026-06-17T02:10:00.000Z', postEnable: '2026-06-17T03:00:00.000Z', gatewayShadow: '2026-06-17T03:30:00.000Z', healthPatrolNginx: '2026-06-17T03:40:00.000Z', rollbackApply: '2026-06-18T04:00:00.000Z', postRollback: '2026-06-18T05:00:00.000Z', }); const result = runFullCutoverAudit(root); assertStatus(result, 1, '完整标准切换时间线超过默认 24 小时时必须失败。'); assertIncludes( result.stdout, '时间跨度', '完整标准切换时间线跨度过大必须给出明确诊断。', ); assertIncludes( result.stdout, '"maxSpanMs": 86400000', '完整标准切换时间线跨度过大必须输出默认最大跨度。', ); assertIncludes( result.stdout, '"spanMs": 100800000', '完整标准切换时间线跨度过大必须输出实际跨度。', ); } function assertTimelineMaxSpanOverrideSucceed() { const root = path.join(tmpRoot, 'long-timeline-override'); prepareFullCutoverTimeline(root, { preCutover: '2026-06-17T01:00:00.000Z', enableApply: '2026-06-17T02:00:00.000Z', healthPatrolDirect: '2026-06-17T02:10:00.000Z', postEnable: '2026-06-17T03:00:00.000Z', gatewayShadow: '2026-06-17T03:30:00.000Z', healthPatrolNginx: '2026-06-17T03:40:00.000Z', rollbackApply: '2026-06-18T04:00:00.000Z', postRollback: '2026-06-18T05:00:00.000Z', }); const result = runFullCutoverAudit(root, ['--timeline-max-span-ms', '172800000']); assertStatus(result, 0, '显式放宽切换时间线最大跨度后应允许长窗口证据。'); if (result.status !== 0) { return; } const output = parseJson(result.stdout, '放宽跨度切换时间线审计 stdout'); assertEqual(output.timeline?.ok, true, '放宽跨度后 timeline.ok 必须为 true。'); assertEqual( output.timeline?.maxSpanMs, 172800000, '放宽跨度后 timeline.maxSpanMs 必须反映 CLI 参数。', ); assertEqual( output.timeline?.spanMs, 100800000, '放宽跨度后 timeline.spanMs 仍必须输出实际跨度。', ); } function assertTimelineNonOkItemFails() { const root = path.join(tmpRoot, 'timeline-non-ok-item'); prepareFullCutoverTimeline(root, { preCutover: '2026-06-17T01:00:00.000Z', enableApply: '2026-06-17T02:00:00.000Z', healthPatrolDirect: '2026-06-17T02:10:00.000Z', postEnable: '2026-06-17T03:00:00.000Z', gatewayShadow: '2026-06-17T03:30:00.000Z', healthPatrolNginx: '2026-06-17T03:40:00.000Z', rollbackApply: '2026-06-17T04:00:00.000Z', postRollback: '2026-06-17T05:00:00.000Z', }); const preCutoverManifestPath = path.join( root, '20260617T010000Z-pre-cutover', 'manifest.json', ); const preCutoverManifest = readJson( preCutoverManifestPath, '切换前证据 manifest', ); preCutoverManifest.summary.status = 'CRITICAL'; writeJson(preCutoverManifestPath, preCutoverManifest); const postEnableManifestPath = path.join( root, '20260617T030000Z-post-enable', 'manifest.json', ); const postEnableManifest = readJson( postEnableManifestPath, '启用后证据 manifest', ); postEnableManifest.summary.status = 'CRITICAL'; writeJson(postEnableManifestPath, postEnableManifest); const result = runFullCutoverAudit(root); assertStatus(result, 1, '标准八段时间线包含非 OK 条目时 timeline 必须失败。'); assertIncludes( result.stdout, '标准切换时间线包含非 OK 证据', 'timeline 非 OK 条目必须给出明确诊断。', ); assertIncludes( result.stdout, 'post-enable=MANIFEST_FAILED', 'timeline 非 OK 条目诊断必须指出具体阶段状态。', ); assertIncludes( result.stdout, '"ok": false', 'timeline 非 OK 条目必须让输出中可见 ok=false。', ); const output = parseJson(result.stdout, '非 OK 时间线审计 stdout'); assertEqual( output.timeline?.failedCount, 2, 'timeline.failedCount 必须按非 OK 条目数量计数,不能只记一个笼统失败。', ); assertEqual( output.timeline?.failureBreakdown?.nonOkItems, 2, 'timeline.failureBreakdown 必须输出非 OK 条目数量,便于现场定位。', ); } function assertMixedTimelineCutoverRunIdFails() { const root = path.join(tmpRoot, 'mixed-timeline-cutover-run-id'); prepareFullCutoverTimeline( root, { preCutover: '2026-06-17T01:00:00.000Z', enableApply: '2026-06-17T02:00:00.000Z', healthPatrolDirect: '2026-06-17T02:10:00.000Z', postEnable: '2026-06-17T03:00:00.000Z', gatewayShadow: '2026-06-17T03:30:00.000Z', healthPatrolNginx: '2026-06-17T03:40:00.000Z', rollbackApply: '2026-06-17T04:00:00.000Z', postRollback: '2026-06-17T05:00:00.000Z', }, { cutoverRunId: 'cutover-window-a' }, ); const postRollbackManifestPath = path.join( root, '20260617T050000Z-post-rollback', 'manifest.json', ); const postRollbackManifest = readJson( postRollbackManifestPath, '回退后证据 manifest', ); postRollbackManifest.cutoverRunId = 'cutover-window-b'; writeJson(postRollbackManifestPath, postRollbackManifest); const result = runFullCutoverAudit(root); assertStatus(result, 1, '完整标准时间线混入不同 cutoverRunId 时必须失败。'); assertIncludes( result.stdout, '不同 cutoverRunId', '完整标准时间线混入不同 cutoverRunId 必须给出明确诊断。', ); assertIncludes( result.stdout, 'post-rollback=\\"cutover-window-b\\"', '完整标准时间线 cutoverRunId 漂移必须指出具体阶段。', ); assertIncludes( result.stdout, 'cutover-window-a', '完整标准时间线 cutoverRunId 漂移必须输出原批次。', ); } function assertPartialTimelineCutoverRunIdFails() { const root = path.join(tmpRoot, 'partial-timeline-cutover-run-id'); prepareFullCutoverTimeline( root, { preCutover: '2026-06-17T01:00:00.000Z', enableApply: '2026-06-17T02:00:00.000Z', healthPatrolDirect: '2026-06-17T02:10:00.000Z', postEnable: '2026-06-17T03:00:00.000Z', gatewayShadow: '2026-06-17T03:30:00.000Z', healthPatrolNginx: '2026-06-17T03:40:00.000Z', rollbackApply: '2026-06-17T04:00:00.000Z', postRollback: '2026-06-17T05:00:00.000Z', }, { cutoverRunId: 'cutover-window-a' }, ); const enableManifestPath = path.join( root, '20260617T020000Z-enable-apply-pingora-direct-enable-apply', 'manifest.json', ); const enableManifest = readJson(enableManifestPath, '启用命令证据 manifest'); delete enableManifest.cutoverRunId; delete enableManifest.command.cutoverRunId; writeJson(enableManifestPath, enableManifest); const result = runFullCutoverAudit(root); assertStatus(result, 1, '完整标准时间线部分缺少 cutoverRunId 时必须失败。'); assertIncludes( result.stdout, '缺少 cutoverRunId', '完整标准时间线部分缺少 cutoverRunId 必须给出明确诊断。', ); assertIncludes( result.stdout, 'enable-apply:pingora-direct-enable-apply=', '完整标准时间线部分缺少 cutoverRunId 必须指出具体命令证据。', ); } function assertEmptyCutoverRunIdFails() { const root = path.join(tmpRoot, 'empty-cutover-run-id'); const bundleDir = prepareBundle(root, '20260617T010000Z-pre-cutover', 'pre-cutover'); const manifestPath = path.join(bundleDir, 'manifest.json'); const manifest = readJson(manifestPath, '阶段证据 manifest'); manifest.cutoverRunId = ''; writeJson(manifestPath, manifest); const result = runAudit([ '--evidence-root', root, '--require-phase', 'pre-cutover', ]); assertStatus( result, 1, 'manifest.cutoverRunId 字段存在但为空时必须失败,不能当作缺省字段。', ); assertIncludes( result.stdout, 'manifest.cutoverRunId 只能包含 ASCII', '空 cutoverRunId 必须给出字段诊断。', ); } function assertCutoverRunIdMismatchFails() { const root = path.join(tmpRoot, 'cutover-run-id-mismatch'); prepareFullCutoverTimeline( root, { preCutover: '2026-06-17T01:00:00.000Z', enableApply: '2026-06-17T02:00:00.000Z', healthPatrolDirect: '2026-06-17T02:10:00.000Z', postEnable: '2026-06-17T03:00:00.000Z', gatewayShadow: '2026-06-17T03:30:00.000Z', healthPatrolNginx: '2026-06-17T03:40:00.000Z', rollbackApply: '2026-06-17T04:00:00.000Z', postRollback: '2026-06-17T05:00:00.000Z', }, { cutoverRunId: 'other-cutover-window' }, ); const result = runFullCutoverAudit(root, [ '--require-cutover-run-id', 'cutover-20260617T010000Z', ]); assertStatus(result, 1, '要求 cutoverRunId 时不应接受其它切换批次证据。'); assertIncludes( result.stdout, 'cutoverRunId=cutover-20260617T010000Z', 'cutoverRunId 不匹配时必须在诊断中写出要求的批次 ID。', ); assertIncludes( result.stdout, '其它切换批次', 'cutoverRunId 不匹配时必须说明已有证据属于其它切换批次。', ); } function assertMissingCutoverRunIdFailsWhenRequired() { const root = path.join(tmpRoot, 'missing-cutover-run-id'); prepareFullCutoverTimeline(root, { preCutover: '2026-06-17T01:00:00.000Z', enableApply: '2026-06-17T02:00:00.000Z', healthPatrolDirect: '2026-06-17T02:10:00.000Z', postEnable: '2026-06-17T03:00:00.000Z', gatewayShadow: '2026-06-17T03:30:00.000Z', healthPatrolNginx: '2026-06-17T03:40:00.000Z', rollbackApply: '2026-06-17T04:00:00.000Z', postRollback: '2026-06-17T05:00:00.000Z', }); const result = runFullCutoverAudit(root, [ '--require-cutover-run-id', 'cutover-20260617T010000Z', ]); assertStatus(result, 1, '要求 cutoverRunId 时缺少批次 ID 的旧证据必须失败。'); assertIncludes( result.stdout, '缺少 cutoverRunId', '缺少 cutoverRunId 时必须给出明确诊断。', ); } function assertMissingGeneratedAtFails() { const root = path.join(tmpRoot, 'missing-generated-at'); const bundleDir = prepareBundle(root, '20260617T010000Z-pre-cutover', 'pre-cutover'); const manifestPath = path.join(bundleDir, 'manifest.json'); const manifest = readJson(manifestPath, '阶段证据 manifest'); delete manifest.generatedAt; writeJson(manifestPath, manifest); const result = runAudit([ '--evidence-root', root, '--require-phase', 'pre-cutover', ]); assertStatus(result, 1, '阶段证据缺少 manifest.generatedAt 时必须失败。'); assertIncludes( result.stdout, 'manifest.generatedAt 必须是合法 ISO 时间', '阶段证据缺少 generatedAt 必须给出明确诊断。', ); assertIncludes( result.stdout, '不能依赖目录 mtime', '阶段证据缺少 generatedAt 时必须说明不能用目录 mtime 兜底。', ); } function assertInvalidCommandGeneratedAtFails() { const root = path.join(tmpRoot, 'invalid-command-generated-at'); const bundleDir = prepareCommandBundle( root, '20260617T020000Z-enable-apply-pingora-direct-enable-apply', 'enable-apply', 'pingora-direct-enable-apply', ); const manifestPath = path.join(bundleDir, 'manifest.json'); const manifest = readJson(manifestPath, '命令证据 manifest'); manifest.generatedAt = 'not-a-time'; writeJson(manifestPath, manifest); const result = runAudit([ '--evidence-root', root, '--require-command', 'enable-apply:pingora-direct-enable-apply', ]); assertStatus(result, 1, '命令证据 manifest.generatedAt 非法时必须失败。'); assertIncludes( result.stdout, 'manifest.generatedAt 必须是合法 ISO 时间', '命令证据 generatedAt 非法必须给出明确诊断。', ); } function assertNonCanonicalGeneratedAtFails() { const root = path.join(tmpRoot, 'non-canonical-generated-at'); const bundleDir = prepareBundle(root, '20260617T010000Z-pre-cutover', 'pre-cutover'); const manifestPath = path.join(bundleDir, 'manifest.json'); const manifest = readJson(manifestPath, '阶段证据 manifest'); manifest.generatedAt = '2026-06-17T01:00:00Z'; writeJson(manifestPath, manifest); const result = runAudit([ '--evidence-root', root, '--require-phase', 'pre-cutover', ]); assertStatus( result, 1, '阶段证据 manifest.generatedAt 缺少毫秒时必须失败。', ); assertIncludes( result.stdout, 'manifest.generatedAt 必须是合法 ISO 时间', '非规范 generatedAt 必须给出明确诊断。', ); } function assertNonCanonicalCommandRecordTimesFail() { const root = path.join(tmpRoot, 'non-canonical-command-times'); prepareCommandBundle( root, '20260617T020000Z-enable-apply-pingora-direct-enable-apply', 'enable-apply', 'pingora-direct-enable-apply', { startedAt: '2026-06-17T02:00:00Z', finishedAt: '2026-06-17T02:00:03Z', }, ); const result = runAudit([ '--evidence-root', root, '--require-command', 'enable-apply:pingora-direct-enable-apply', ]); assertStatus( result, 1, '命令记录时间缺少毫秒时必须失败。', ); assertIncludes( result.stdout, 'manifest.command.startedAt 必须是合法 ISO 时间', 'manifest.command 非规范 startedAt 必须给出明确诊断。', ); assertIncludes( result.stdout, 'command-record.json.finishedAt 必须是合法 ISO 时间', 'command-record 非规范 finishedAt 必须给出明确诊断。', ); } function assertAmbiguousLatestPhaseFails() { const root = path.join(tmpRoot, 'ambiguous-latest-phase'); prepareBundle(root, '20260617T010000Z-post-enable-a', 'post-enable', { generatedAt: '2026-06-17T02:00:00.000Z', }); prepareBundle(root, '20260617T010000Z-post-enable-b', 'post-enable', { generatedAt: '2026-06-17T02:00:00.000Z', }); const result = runAudit([ '--evidence-root', root, '--require-phase', 'post-enable', ]); assertStatus(result, 1, '同阶段最新证据 generatedAt 重复时必须失败。'); assertIncludes( result.stdout, 'AMBIGUOUS_LATEST', '同阶段最新证据 generatedAt 重复必须写入 AMBIGUOUS_LATEST。', ); assertIncludes( result.stdout, 'manifest.generatedAt=2026-06-17T02:00:00.000Z', '同阶段最新证据 generatedAt 重复必须写出重复时间。', ); assertIncludes( result.stdout, '20260617T010000Z-post-enable-a', '同阶段最新证据 generatedAt 重复必须列出第一个证据目录。', ); assertIncludes( result.stdout, '20260617T010000Z-post-enable-b', '同阶段最新证据 generatedAt 重复必须列出第二个证据目录。', ); assertIncludes( result.stdout, '不能按目录名打平选择', '同阶段最新证据 generatedAt 重复必须说明不能靠目录名打平。', ); } function assertAmbiguousLatestCommandFails() { const root = path.join(tmpRoot, 'ambiguous-latest-command'); prepareCommandBundle( root, '20260617T020000Z-enable-apply-a', 'enable-apply', 'pingora-direct-enable-apply', { generatedAt: '2026-06-17T02:00:00.000Z', }, ); prepareCommandBundle( root, '20260617T020000Z-enable-apply-b', 'enable-apply', 'pingora-direct-enable-apply', { generatedAt: '2026-06-17T02:00:00.000Z', }, ); const result = runAudit([ '--evidence-root', root, '--require-command', 'enable-apply:pingora-direct-enable-apply', ]); assertStatus(result, 1, '同命令最新证据 generatedAt 重复时必须失败。'); assertIncludes( result.stdout, 'AMBIGUOUS_LATEST', '同命令最新证据 generatedAt 重复必须写入 AMBIGUOUS_LATEST。', ); assertIncludes( result.stdout, 'manifest.generatedAt=2026-06-17T02:00:00.000Z', '同命令最新证据 generatedAt 重复必须写出重复时间。', ); assertIncludes( result.stdout, '20260617T020000Z-enable-apply-a', '同命令最新证据 generatedAt 重复必须列出第一个证据目录。', ); assertIncludes( result.stdout, '20260617T020000Z-enable-apply-b', '同命令最新证据 generatedAt 重复必须列出第二个证据目录。', ); assertIncludes( result.stdout, '不能按目录名打平选择', '同命令最新证据 generatedAt 重复必须说明不能靠目录名打平。', ); } function assertMissingManifestSchemaVersionFails() { const root = path.join(tmpRoot, 'missing-manifest-schema-version'); const bundleDir = prepareBundle(root, '20260617T010000Z-pre-cutover', 'pre-cutover'); const manifestPath = path.join(bundleDir, 'manifest.json'); const manifest = readJson(manifestPath, '阶段证据 manifest'); delete manifest.schemaVersion; writeJson(manifestPath, manifest); const result = runAudit([ '--evidence-root', root, '--require-phase', 'pre-cutover', ]); assertStatus(result, 1, '阶段证据 manifest 缺少 schemaVersion 时必须失败。'); assertIncludes( result.stdout, 'manifest.schemaVersion 必须是 1', '阶段证据缺少 manifest schemaVersion 必须给出明确诊断。', ); } function assertCommandRecordSchemaVersionMismatchFails() { const root = path.join(tmpRoot, 'command-record-schema-version-mismatch'); const bundleDir = prepareCommandBundle( root, '20260617T020000Z-enable-apply-pingora-direct-enable-apply', 'enable-apply', 'pingora-direct-enable-apply', ); const commandRecordPath = path.join(bundleDir, 'command-record.json'); const commandRecord = readJson(commandRecordPath, '命令记录 JSON'); writeJson(commandRecordPath, { ...commandRecord, schemaVersion: 2, }); const manifestPath = path.join(bundleDir, 'manifest.json'); const manifest = readJson(manifestPath, '命令证据 manifest'); manifest.files.commandRecord = metadataFor(commandRecordPath); writeJson(manifestPath, manifest); const result = runAudit([ '--evidence-root', root, '--require-command', 'enable-apply:pingora-direct-enable-apply', ]); assertStatus(result, 1, 'command-record.json schemaVersion 非 1 时必须失败。'); assertIncludes( result.stdout, 'command-record.json.schemaVersion 必须是 1', '命令记录 schemaVersion 漂移必须给出明确诊断。', ); assertIncludes( result.stdout, 'manifest.command.schemaVersion 与 command-record.json.schemaVersion 必须一致', 'manifest.command 与 command-record schemaVersion 不一致必须给出明确诊断。', ); } function assertCanAuditSinglePhase() { const root = path.join(tmpRoot, 'single-phase'); prepareBundle(root, '20260617T010000Z-pre-cutover', 'pre-cutover'); const result = runAudit([ '--evidence-root', root, '--phase', 'pre-cutover', ]); assertStatus(result, 0, '只要求单个已有阶段时应成功。'); if (result.status !== 0) { return; } const output = parseJson(result.stdout, '单阶段证据审计 stdout'); assertEqual(output.checkedCount, 1, '单阶段证据审计只应检查一个 phase。'); assertEqual( output.phases?.[0]?.latestBundleDir?.endsWith('pre-cutover'), true, '单阶段证据审计必须输出最新 bundleDir。', ); } function assertMissingPhaseFails() { const root = path.join(tmpRoot, 'missing-phase'); prepareBundle(root, '20260617T010000Z-pre-cutover', 'pre-cutover'); const result = runAudit([ '--evidence-root', root, '--require-phase', 'post-enable', ]); assertStatus(result, 1, '要求的 phase 缺失时必须失败。'); assertIncludes( result.stdout, 'MISSING', 'phase 缺失失败必须写入 JSON status。', ); } function assertCommandBundleCannotSatisfyPhaseRequirement() { const root = path.join(tmpRoot, 'command-bundle-not-phase'); prepareCommandBundle( root, '20260617T010000Z-post-enable-command', 'post-enable', 'pingora-direct-enable-apply', ); const result = runAudit([ '--evidence-root', root, '--require-phase', 'post-enable', ]); assertStatus( result, 1, '命令证据不能满足同名 phase 的阶段证据要求。', ); assertIncludes( result.stdout, 'MISSING', '命令证据冒充阶段证据时必须写入 MISSING。', ); assertIncludes( result.stdout, '没有找到该阶段的证据 manifest', '命令证据冒充阶段证据时必须给出缺少阶段证据的诊断。', ); } function assertMissingCommandFails() { const root = path.join(tmpRoot, 'missing-command'); prepareBundle(root, '20260617T010000Z-pre-cutover', 'pre-cutover'); const result = runAudit([ '--evidence-root', root, '--require-command', 'enable-apply:pingora-direct-enable-apply', ]); assertStatus(result, 1, '要求的 command 证据缺失时必须失败。'); assertIncludes( result.stdout, 'MISSING', 'command 缺失失败必须写入 JSON status。', ); assertIncludes( result.stdout, 'pingora-direct-enable-apply', 'command 缺失失败必须写入 commandName。', ); } function assertOutOfOrderCutoverTimelineFails() { const root = path.join(tmpRoot, 'out-of-order-timeline'); prepareFullCutoverTimeline(root, { preCutover: '2026-06-17T01:00:00.000Z', enableApply: '2026-06-17T03:00:00.000Z', healthPatrolDirect: '2026-06-17T03:10:00.000Z', postEnable: '2026-06-17T02:00:00.000Z', gatewayShadow: '2026-06-17T03:30:00.000Z', healthPatrolNginx: '2026-06-17T03:40:00.000Z', rollbackApply: '2026-06-17T04:00:00.000Z', postRollback: '2026-06-17T05:00:00.000Z', }); const result = runFullCutoverAudit(root); assertStatus(result, 1, '完整标准切换时间线倒序时必须失败。'); assertIncludes( result.stdout, '疑似混入不同切换窗口证据', '完整标准切换时间线倒序必须给出明确诊断。', ); assertIncludes( result.stdout, '"checked": true', '完整标准切换时间线倒序时必须显示 timeline 已检查。', ); } function assertCriticalPhaseSummaryFails() { const root = path.join(tmpRoot, 'critical-phase-summary'); prepareBundle(root, '20260617T010000Z-post-enable', 'post-enable', { summaryStatus: 'CRITICAL', }); const result = runAudit([ '--evidence-root', root, '--require-phase', 'post-enable', ]); assertStatus(result, 1, '最新阶段证据 manifest summary 非 OK 时必须失败。'); assertIncludes( result.stdout, 'MANIFEST_FAILED', '阶段证据 summary 非 OK 必须写入 MANIFEST_FAILED。', ); assertIncludes( result.stdout, 'manifest.summary.status 必须是 OK', '阶段证据 summary 非 OK 必须给出明确诊断。', ); } function assertFailedCommandSummaryFails() { const root = path.join(tmpRoot, 'failed-command-summary'); prepareCommandBundle( root, '20260617T010000Z-enable-apply-pingora-direct-enable-apply', 'enable-apply', 'pingora-direct-enable-apply', { summaryStatus: 'FAILED', exitCode: 1, }, ); const result = runAudit([ '--evidence-root', root, '--require-command', 'enable-apply:pingora-direct-enable-apply', ]); assertStatus(result, 1, '最新命令证据 manifest summary 失败时必须失败。'); assertIncludes( result.stdout, 'MANIFEST_FAILED', '命令证据 summary 失败必须写入 MANIFEST_FAILED。', ); assertIncludes( result.stdout, 'manifest.summary.exitCode 必须是 0', '命令证据 exitCode 非 0 必须给出明确诊断。', ); } function assertLatestBrokenBundleFails() { const root = path.join(tmpRoot, 'latest-broken'); prepareBundle(root, '20260617T010000Z-post-enable', 'post-enable', { generatedAt: '2026-06-17T01:00:00.000Z', }); const latest = prepareBundle(root, '20260617T020000Z-post-enable', 'post-enable', { generatedAt: '2026-06-17T02:00:00.000Z', }); writeFileSync(path.join(latest, 'snapshot.json'), '{"ok":false}\n', 'utf8'); const result = runAudit([ '--evidence-root', root, '--require-phase', 'post-enable', ]); assertStatus(result, 1, '同阶段最新证据损坏时必须失败。'); assertIncludes( result.stdout, 'VERIFY_FAILED', '最新证据损坏必须写入 VERIFY_FAILED。', ); assertIncludes( result.stdout, 'latest-broken/20260617T020000Z-post-enable', '失败输出必须指向最新 bundleDir。', ); } function assertLatestBrokenCommandBundleFails() { const root = path.join(tmpRoot, 'latest-broken-command'); prepareCommandBundle( root, '20260617T010000Z-enable-apply-pingora-direct-enable-apply', 'enable-apply', 'pingora-direct-enable-apply', { generatedAt: '2026-06-17T01:00:00.000Z', }, ); const latest = prepareCommandBundle( root, '20260617T020000Z-enable-apply-pingora-direct-enable-apply', 'enable-apply', 'pingora-direct-enable-apply', { generatedAt: '2026-06-17T02:00:00.000Z', }, ); writeFileSync(path.join(latest, 'command.stdout.txt'), 'drifted\n', 'utf8'); const result = runAudit([ '--evidence-root', root, '--require-command', 'enable-apply:pingora-direct-enable-apply', ]); assertStatus(result, 1, '同命令最新证据损坏时必须失败。'); assertIncludes( result.stdout, 'VERIFY_FAILED', '最新命令证据损坏必须写入 VERIFY_FAILED。', ); assertIncludes( result.stdout, '最新命令证据目录 manifest 验真失败', '最新命令证据损坏必须给出 command 诊断。', ); } function assertManifestDiagnosticsFailAudit() { const root = path.join(tmpRoot, 'diagnostic'); prepareBundle(root, '20260617T010000Z-pre-cutover', 'pre-cutover'); const badDir = path.join(root, '20260617T020000Z-bad'); mkdirSync(badDir, { recursive: true }); writeFileSync(path.join(badDir, 'manifest.json'), '{ bad json\n', 'utf8'); const result = runAudit([ '--evidence-root', root, '--require-phase', 'pre-cutover', ]); assertStatus(result, 1, '证据根目录存在坏 manifest 诊断时必须失败。'); assertIncludes( result.stdout, 'manifest 不是合法 JSON', '坏 manifest 必须出现在 diagnostics 中。', ); } function assertExtraRootFileFailsByDefault() { const root = path.join(tmpRoot, 'extra-root-file'); prepareBundle(root, '20260617T010000Z-pre-cutover', 'pre-cutover'); writeFileSync(path.join(root, 'operator-note.txt'), 'manual note\n', 'utf8'); const result = runAudit([ '--evidence-root', root, '--require-phase', 'pre-cutover', ]); assertStatus(result, 1, '证据根目录混入普通文件时默认必须失败。'); assertIncludes( result.stdout, '证据根目录只能包含证据目录,发现普通文件', '证据根目录普通文件失败必须写入 diagnostics。', ); } function assertExtraRootDirectoryFailsByDefault() { const root = path.join(tmpRoot, 'extra-root-directory'); prepareBundle(root, '20260617T010000Z-pre-cutover', 'pre-cutover'); mkdirSync(path.join(root, 'manual-notes'), { recursive: true }); const result = runAudit([ '--evidence-root', root, '--require-phase', 'pre-cutover', ]); assertStatus(result, 1, '证据根目录混入无 manifest 目录时默认必须失败。'); assertIncludes( result.stdout, '证据根目录只能包含带 manifest.json 的证据目录', '证据根目录无 manifest 目录失败必须写入 diagnostics。', ); } function assertExtraRootSymlinkFailsByDefault() { const root = path.join(tmpRoot, 'extra-root-symlink'); prepareBundle(root, '20260617T010000Z-pre-cutover', 'pre-cutover'); symlinkSync( path.join(root, '20260617T010000Z-pre-cutover'), path.join(root, 'pre-cutover-link'), ); const result = runAudit([ '--evidence-root', root, '--require-phase', 'pre-cutover', ]); assertStatus(result, 1, '证据根目录混入符号链接时默认必须失败。'); assertIncludes( result.stdout, '证据根目录下不能包含符号链接条目', '证据根目录符号链接失败必须写入 diagnostics。', ); } function assertAllowExtraRootEntriesSucceeds() { const root = path.join(tmpRoot, 'allow-extra-root-entries'); prepareBundle(root, '20260617T010000Z-pre-cutover', 'pre-cutover'); writeFileSync(path.join(root, 'operator-note.txt'), 'manual note\n', 'utf8'); mkdirSync(path.join(root, 'manual-notes'), { recursive: true }); symlinkSync( path.join(root, '20260617T010000Z-pre-cutover'), path.join(root, 'pre-cutover-link'), ); const result = runAudit([ '--evidence-root', root, '--require-phase', 'pre-cutover', '--allow-extra-root-entries', ]); assertStatus(result, 0, '显式允许额外根目录条目时审计应通过。'); if (result.status !== 0) { return; } const output = parseJson(result.stdout, '允许额外根目录条目审计 stdout'); assertEqual( output.allowExtraRootEntries, true, '允许额外根目录条目时 stdout 必须记录 allowExtraRootEntries=true。', ); assertEqual( output.diagnostics?.length, 0, '允许额外根目录条目时不应输出根目录额外条目诊断。', ); } function assertRejectsUnsafeInputs() { const root = path.join(tmpRoot, 'unsafe-target'); prepareBundle(root, '20260617T010000Z-pre-cutover', 'pre-cutover'); const linkPath = path.join(tmpRoot, 'unsafe-link'); symlinkSync(root, linkPath); const verifyLink = path.join(tmpRoot, 'verify-link.mjs'); symlinkSync(path.resolve('scripts/ops/pingora-cutover-evidence-verify.mjs'), verifyLink); const cases = [ { args: ['--evidence-root', 'relative-evidence'], expected: '--evidence-root 必须是绝对路径', reason: '证据根目录相对路径必须失败。', }, { args: ['--evidence-root', path.parse(process.cwd()).root], expected: '--evidence-root 不能是文件系统根目录', reason: '证据根目录为文件系统根目录必须失败。', }, { args: ['--evidence-root', `${root}\n--allow-extra-root-entries`], expected: '--evidence-root 不能包含换行或 NUL 字符', reason: '证据根目录带控制字符必须失败。', }, { args: ['--evidence-root', linkPath], expected: '--evidence-root 不能是符号链接', reason: '证据根目录为符号链接必须失败。', }, { args: ['--evidence-root', root, '--verify-script', 'relative.mjs'], expected: '--verify-script 必须是绝对路径', reason: 'verifier 相对路径必须失败。', }, { args: [ '--evidence-root', root, '--verify-script', `${path.resolve('scripts/ops/pingora-cutover-evidence-verify.mjs')}\n--allow-extra-files`, ], expected: '--verify-script 不能包含换行或 NUL 字符', reason: 'verifier 路径带控制字符必须失败。', }, { args: ['--evidence-root', root, '--verify-script', verifyLink], expected: '--verify-script 不能是符号链接', reason: 'verifier 符号链接必须失败。', }, { args: ['--evidence-root', root, '--require-phase', '../bad'], expected: '--require-phase 只能包含 ASCII', reason: 'phase 名称不安全必须失败。', }, { args: ['--evidence-root', root, '--require-command', 'enable-apply'], expected: '--require-command 必须使用 : 格式', reason: 'command 证据要求缺少 commandName 必须失败。', }, { args: [ '--evidence-root', root, '--require-command', 'enable-apply:../bad', ], expected: '--require-command commandName 只能包含 ASCII', reason: 'command 名称不安全必须失败。', }, { args: [ '--evidence-root', root, '--require-command-executable', 'enable-apply', ], expected: '--require-command-executable 必须使用 :: 格式', reason: 'command executable 要求缺少字段必须失败。', }, { args: [ '--evidence-root', root, '--require-command-executable', 'enable-apply:../bad:/opt/genarrative/current/scripts/deploy/pingora-direct-enable.sh', ], expected: '--require-command-executable commandName 只能包含 ASCII', reason: 'command executable 名称不安全必须失败。', }, { args: [ '--evidence-root', root, '--require-command-executable', 'enable-apply:pingora-direct-enable-apply:relative.sh', ], expected: '--require-command-executable executable 必须是绝对路径', reason: 'command executable 相对路径必须失败。', }, { args: [ '--evidence-root', root, '--require-command-executable', 'enable-apply:pingora-direct-enable-apply:/opt/genarrative/current/scripts/deploy/pingora-direct-enable.sh\n--apply', ], expected: '--require-command-executable executable 不能包含换行或 NUL 字符', reason: 'command executable 带控制字符必须失败。', }, { args: [ '--evidence-root', root, '--require-command-executable', 'enable-apply:pingora-direct-enable-apply:/opt/genarrative/current/scripts/deploy/pingora-direct-enable.sh', '--require-command-executable', 'enable-apply:pingora-direct-enable-apply:/opt/genarrative/current/scripts/deploy/pingora-direct-rollback.sh', ], expected: '--require-command-executable 不能为同一个 : 指定多个不同路径', reason: '同一 command executable 重复绑定不同路径必须失败。', }, { args: [ '--evidence-root', root, '--require-command-arg', 'enable-apply', ], expected: '--require-command-arg 必须使用 :: 格式', reason: 'command arg 要求缺少字段必须失败。', }, { args: [ '--evidence-root', root, '--require-command-arg', 'enable-apply:../bad:--apply', ], expected: '--require-command-arg commandName 只能包含 ASCII', reason: 'command arg 名称不安全必须失败。', }, { args: [ '--evidence-root', root, '--require-command-arg', 'enable-apply:pingora-direct-enable-apply:', ], expected: '--require-command-arg arg 必须是非空且不包含换行的字符串', reason: 'command arg 为空必须失败。', }, { args: ['--evidence-root', root, '--require-cutover-run-id', 'cutover 1'], expected: '--require-cutover-run-id 只能包含 ASCII', reason: 'cutoverRunId 不安全必须失败。', }, ]; for (const testCase of cases) { const result = runAudit(testCase.args); assertStatus(result, 1, testCase.reason); assertIncludes( `${result.stdout}\n${result.stderr}`, testCase.expected, `${testCase.reason} 必须给出明确错误。`, ); } } function assertManifestFileNameControlCharacterFails() { const root = path.join(tmpRoot, 'manifest-file-name-control-character'); const bundleDir = prepareCommandBundle( root, '20260617T010000Z-enable-apply-pingora-direct-enable-apply', 'enable-apply', 'pingora-direct-enable-apply', ); const manifestPath = path.join(bundleDir, 'manifest.json'); const manifest = readJson(manifestPath, '文件名控制字符 manifest'); manifest.files.commandRecord.path = 'command-record.json\noperator-note.txt'; writeJson(manifestPath, manifest); const result = runAudit([ '--evidence-root', root, '--require-command', 'enable-apply:pingora-direct-enable-apply', ]); assertStatus(result, 1, '命令证据 manifest 文件名带控制字符时总审计必须失败。'); assertIncludes( result.stdout, 'manifest.files.commandRecord.path 只能是证据目录内的普通文件名', '命令证据文件名控制字符必须给出明确诊断。', ); } function assertRejectsInvalidTimelineMaxSpan() { const root = path.join(tmpRoot, 'invalid-timeline-max-span'); prepareBundle(root, '20260617T010000Z-pre-cutover', 'pre-cutover'); const cases = [ { args: ['--evidence-root', root, '--timeline-max-span-ms', '0'], env: {}, expected: '--timeline-max-span-ms 必须是正整数', reason: 'timeline 最大跨度为 0 必须失败。', }, { args: ['--evidence-root', root], env: { GENARRATIVE_PINGORA_CUTOVER_EVIDENCE_TIMELINE_MAX_SPAN_MS: 'oops', }, expected: 'GENARRATIVE_PINGORA_CUTOVER_EVIDENCE_TIMELINE_MAX_SPAN_MS 必须是正整数', reason: 'timeline 最大跨度 env 非正整数必须失败。', }, ]; for (const testCase of cases) { const result = runAudit(testCase.args, testCase.env); assertStatus(result, 1, testCase.reason); assertIncludes( `${result.stdout}\n${result.stderr}`, testCase.expected, `${testCase.reason} 必须给出明确错误。`, ); } } function prepareFullCutoverTimeline(root, times, options = {}) { const timelineTimes = { preCutover: '2026-06-17T01:00:00.000Z', enableApply: '2026-06-17T02:00:00.000Z', healthPatrolDirect: '2026-06-17T02:10:00.000Z', postEnable: '2026-06-17T03:00:00.000Z', gatewayShadow: '2026-06-17T03:30:00.000Z', healthPatrolNginx: '2026-06-17T03:40:00.000Z', rollbackApply: '2026-06-17T04:00:00.000Z', postRollback: '2026-06-17T05:00:00.000Z', ...times, }; prepareBundle(root, '20260617T010000Z-pre-cutover', 'pre-cutover', { generatedAt: timelineTimes.preCutover, cutoverRunId: options.cutoverRunId, }); prepareCommandBundle( root, '20260617T020000Z-enable-apply-pingora-direct-enable-apply', 'enable-apply', 'pingora-direct-enable-apply', { generatedAt: timelineTimes.enableApply, cutoverRunId: options.cutoverRunId, }, ); prepareCommandBundle( root, `${directoryTimestamp(timelineTimes.healthPatrolDirect)}-post-enable-pingora-health-patrol-direct-env-switch`, 'post-enable', 'pingora-health-patrol-direct-env-switch', { generatedAt: timelineTimes.healthPatrolDirect, cutoverRunId: options.cutoverRunId, }, ); prepareBundle(root, '20260617T030000Z-post-enable', 'post-enable', { generatedAt: timelineTimes.postEnable, cutoverRunId: options.cutoverRunId, directLiveSummary: true, directLiveAccessLog: options.postEnableDirectLiveAccessLog !== false, directLiveStaticHeaders: options.postEnableDirectLiveSummary !== false, }); prepareCommandBundle( root, `${directoryTimestamp(timelineTimes.gatewayShadow)}-rollback-prep-pingora-gateway-shadow-env-switch`, 'rollback-prep', 'pingora-gateway-shadow-env-switch', { generatedAt: timelineTimes.gatewayShadow, cutoverRunId: options.cutoverRunId, }, ); prepareCommandBundle( root, `${directoryTimestamp(timelineTimes.healthPatrolNginx)}-rollback-prep-pingora-health-patrol-nginx-env-switch`, 'rollback-prep', 'pingora-health-patrol-nginx-env-switch', { generatedAt: timelineTimes.healthPatrolNginx, cutoverRunId: options.cutoverRunId, }, ); prepareCommandBundle( root, `${directoryTimestamp(timelineTimes.rollbackApply)}-rollback-apply-pingora-direct-rollback-apply`, 'rollback-apply', 'pingora-direct-rollback-apply', { generatedAt: timelineTimes.rollbackApply, cutoverRunId: options.cutoverRunId, }, ); prepareBundle( root, `${directoryTimestamp(timelineTimes.postRollback)}-post-rollback`, 'post-rollback', { generatedAt: timelineTimes.postRollback, cutoverRunId: options.cutoverRunId, }, ); } function directoryTimestamp(isoTime) { return isoTime .replace(/[-:]/gu, '') .replace(/\.\d{3}Z$/u, 'Z') .replace('T', 'T'); } function prepareBundle(root, dirName, phase, options = {}) { const dir = path.join(root, dirName); const directLiveAccessLog = options.directLiveSummary && options.directLiveAccessLog !== false ? createDirectLiveAccessLogSummary() : null; const directLiveStaticHeaders = options.directLiveSummary && options.directLiveStaticHeaders !== false ? createDirectLiveStaticHeadersSummary() : null; const pingoraEnvShadow = options.pingoraEnvShadow === false ? null : options.pingoraEnvShadow && typeof options.pingoraEnvShadow === 'object' ? options.pingoraEnvShadow : createPingoraEnvShadowSummary(); mkdirSync(dir, { recursive: true }); writeFileSync(path.join(dir, 'snapshot.json'), '{"ok":true}\n', 'utf8'); writeFileSync(path.join(dir, 'snapshot.stdout.txt'), '{"ok":true}\n', 'utf8'); writeFileSync(path.join(dir, 'snapshot.stderr.txt'), '', 'utf8'); writeFileSync( path.join(dir, 'snapshot-command.json'), '{"executable":"node"}\n', 'utf8', ); writeJson(path.join(dir, 'manifest.json'), { schemaVersion: 1, generatedAt: options.generatedAt || '2026-06-17T00:00:00.000Z', phase, ...(options.cutoverRunId ? { cutoverRunId: options.cutoverRunId } : {}), summary: { status: options.summaryStatus || 'OK', ...(directLiveAccessLog ? { directLiveAccessLog } : {}), ...(directLiveStaticHeaders ? { directLiveStaticHeaders } : {}), ...(pingoraEnvShadow ? { pingoraEnvShadow } : {}), }, files: { manifest: 'manifest.json', snapshot: metadataFor(path.join(dir, 'snapshot.json')), snapshotParseError: null, snapshotStdout: metadataFor(path.join(dir, 'snapshot.stdout.txt')), snapshotStderr: metadataFor(path.join(dir, 'snapshot.stderr.txt')), snapshotCommand: metadataFor(path.join(dir, 'snapshot-command.json')), directLive: null, directLiveParseError: null, directLiveStdout: null, directLiveStderr: null, directLiveCommand: null, }, }); return dir; } function createDirectLiveAccessLogSummary() { return { present: true, checked: 20, matchedCount: 20, missingCount: 0, mismatchCount: 0, }; } function createDirectLiveStaticHeadersSummary() { return { present: true, normal: { ok: true, present: true, cacheControl: 'no-cache', etag: 'W/"1b-6a327f90"', lastModified: 'Wed, 17 Jun 2026 11:05:52 GMT', acceptRanges: 'bytes', contentLength: '27', rangeStatusCode: 206, rangeContentRange: 'bytes 0-0/27', etag304StatusCode: 304, lastModified304StatusCode: 304, }, fingerprinted: { ok: true, present: true, cacheControl: 'public, max-age=31536000, immutable', etag: 'W/"29-6a327f90"', lastModified: 'Wed, 17 Jun 2026 11:05:52 GMT', acceptRanges: 'bytes', contentLength: '41', rangeStatusCode: 206, rangeContentRange: 'bytes 0-0/41', etag304StatusCode: 304, lastModified304StatusCode: 304, }, diagnostics: [], }; } function createPingoraEnvShadowSummary() { return { present: true, listen: '127.0.0.1:18081', tlsListen: '', httpRedirectListen: '', tlsCertFile: '', tlsKeyFile: '', mode: 'shadow', shadowReady: true, directReady: false, ok: true, diagnostics: [], }; } function prepareCommandBundle(root, dirName, phase, commandName, options = {}) { const dir = path.join(root, dirName); const executable = options.executable || defaultCommandExecutable(commandName); const args = options.args || defaultCommandArgs(commandName); const generatedAt = options.generatedAt || '2026-06-17T00:00:00.000Z'; const startedAt = options.startedAt || generatedAt; const finishedAt = options.finishedAt || generatedAt; const durationMs = Date.parse(finishedAt) - Date.parse(startedAt); mkdirSync(dir, { recursive: true }); writeFileSync(path.join(dir, 'command.stdout.txt'), 'ok\n', 'utf8'); writeFileSync(path.join(dir, 'command.stderr.txt'), '', 'utf8'); const commandRecord = { schemaVersion: 1, name: commandName, phase, ...(options.cutoverRunId ? { cutoverRunId: options.cutoverRunId } : {}), expectedExecutable: executable, executable, args, command: [executable, ...args].join(' '), cwd: process.cwd(), exitCode: options.exitCode ?? 0, signal: options.signal ?? null, error: null, startedAt, finishedAt, durationMs, stdoutPath: 'command.stdout.txt', stderrPath: 'command.stderr.txt', }; writeJson(path.join(dir, 'command-record.json'), commandRecord); writeJson(path.join(dir, 'manifest.json'), { schemaVersion: 1, generatedAt, phase, commandName, ...(options.cutoverRunId ? { cutoverRunId: options.cutoverRunId } : {}), expectedExecutable: executable, summary: { status: options.summaryStatus || 'OK', exitCode: options.exitCode ?? 0, signal: options.signal ?? null, }, command: commandRecord, files: { manifest: 'manifest.json', stdout: metadataFor(path.join(dir, 'command.stdout.txt')), stderr: metadataFor(path.join(dir, 'command.stderr.txt')), commandRecord: metadataFor(path.join(dir, 'command-record.json')), }, }); return dir; } function defaultCommandExecutable(commandName) { switch (commandName) { case 'pingora-direct-rollback-apply': return '/opt/genarrative/current/scripts/deploy/pingora-direct-rollback.sh'; case 'pingora-health-patrol-direct-env-switch': case 'pingora-health-patrol-nginx-env-switch': return '/opt/genarrative/current/scripts/deploy/pingora-health-patrol-env-switch.mjs'; case 'pingora-gateway-shadow-env-switch': return '/opt/genarrative/current/scripts/deploy/pingora-gateway-env-shadow-switch.mjs'; case 'pingora-direct-enable-apply': default: return '/opt/genarrative/current/scripts/deploy/pingora-direct-enable.sh'; } } function defaultCommandArgs(commandName) { switch (commandName) { case 'pingora-health-patrol-direct-env-switch': return ['--apply', 'pingora-direct']; case 'pingora-health-patrol-nginx-env-switch': return ['--apply', 'nginx']; case 'pingora-direct-enable-apply': case 'pingora-gateway-shadow-env-switch': case 'pingora-direct-rollback-apply': default: return ['--apply']; } } function metadataFor(filePath) { const content = readFileSync(filePath); return { path: path.basename(filePath), sizeBytes: content.length, sha256: createHash('sha256').update(content).digest('hex'), }; } function runAudit(args, extraEnv = {}) { return spawnSync('node', [AUDIT_SCRIPT, ...args], { cwd: process.cwd(), encoding: 'utf8', env: { ...process.env, GENARRATIVE_PINGORA_CUTOVER_EVIDENCE_ROOT: '', GENARRATIVE_PINGORA_CUTOVER_EVIDENCE_VERIFY_SCRIPT: '', GENARRATIVE_PINGORA_CUTOVER_EVIDENCE_TIMELINE_MAX_SPAN_MS: '', ...extraEnv, }, }); } function runFullCutoverAudit(root, extraArgs = []) { return runAudit([ '--evidence-root', root, '--require-phase', 'pre-cutover', '--require-phase', 'post-enable', '--require-phase', 'post-rollback', '--require-phase-direct-live-access-log', 'post-enable', '--require-phase-direct-live-static-headers', 'post-enable', '--require-phase-pingora-env-shadow', 'post-rollback', '--require-command', 'enable-apply:pingora-direct-enable-apply', '--require-command', 'post-enable:pingora-health-patrol-direct-env-switch', '--require-command', 'rollback-prep:pingora-gateway-shadow-env-switch', '--require-command', 'rollback-prep:pingora-health-patrol-nginx-env-switch', '--require-command', 'rollback-apply:pingora-direct-rollback-apply', '--require-command-executable', 'enable-apply:pingora-direct-enable-apply:/opt/genarrative/current/scripts/deploy/pingora-direct-enable.sh', '--require-command-executable', 'post-enable:pingora-health-patrol-direct-env-switch:/opt/genarrative/current/scripts/deploy/pingora-health-patrol-env-switch.mjs', '--require-command-executable', 'rollback-prep:pingora-gateway-shadow-env-switch:/opt/genarrative/current/scripts/deploy/pingora-gateway-env-shadow-switch.mjs', '--require-command-executable', 'rollback-prep:pingora-health-patrol-nginx-env-switch:/opt/genarrative/current/scripts/deploy/pingora-health-patrol-env-switch.mjs', '--require-command-executable', 'rollback-apply:pingora-direct-rollback-apply:/opt/genarrative/current/scripts/deploy/pingora-direct-rollback.sh', '--require-command-arg', 'enable-apply:pingora-direct-enable-apply:--apply', '--require-command-arg', 'post-enable:pingora-health-patrol-direct-env-switch:--apply', '--require-command-arg', 'post-enable:pingora-health-patrol-direct-env-switch:pingora-direct', '--require-command-arg', 'rollback-prep:pingora-gateway-shadow-env-switch:--apply', '--require-command-arg', 'rollback-prep:pingora-health-patrol-nginx-env-switch:--apply', '--require-command-arg', 'rollback-prep:pingora-health-patrol-nginx-env-switch:nginx', '--require-command-arg', 'rollback-apply:pingora-direct-rollback-apply:--apply', ...extraArgs, ]); } function writeJson(filePath, value) { writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8'); } function readJson(filePath, label) { try { return JSON.parse(readFileSync(filePath, 'utf8')); } catch (error) { failures.push(`${label} 不是合法 JSON: ${error.message}`); return {}; } } function parseJson(raw, label) { try { return JSON.parse(raw); } catch (error) { failures.push(`${label} 不是合法 JSON: ${error.message}\n${raw}`); return {}; } } function assertStatus(result, expected, reason) { if ((result.status ?? 0) !== expected) { failures.push( `${reason} 期望退出码 ${expected},实际 ${result.status}。\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`, ); } } function assertIncludes(value, expected, reason) { if (!value.includes(expected)) { failures.push(`${reason} 缺少 ${expected}`); } } function assertEqual(actual, expected, reason) { if (actual !== expected) { failures.push(`${reason} 期望 ${expected},实际 ${actual}`); } }