Files
Genarrative/scripts/check-pingora-cutover-evidence-audit.mjs
T
kdletters e3258f6f1d 完善 Pingora 直连切换门禁
新增 Pingora shadow env 回切脚本与对应检查。

补齐直连证据包时间线和 cutoverRunId 审计门禁。

支持 Gitea Host 透传并更新直连多域名文档。

修复百分号编码静态图标路径并补 smoke 覆盖。

更新生产发布与运维护栏对 Pingora 发布包的校验。
2026-06-18 21:10:45 +08:00

3195 lines
102 KiB
JavaScript

#!/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 <phase>:<commandName>',
'证据根审计脚本 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 <phase>:<commandName>:<absolutePath>',
'证据根审计脚本 usage 必须公开命令证据真实脚本身份强制参数。',
);
assertIncludes(
content,
'--require-command-arg <phase>:<commandName>:<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=<missing>',
'完整标准时间线部分缺少 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 必须使用 <phase>:<commandName> 格式',
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 必须使用 <phase>:<commandName>:<absolutePath> 格式',
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 不能为同一个 <phase>:<commandName> 指定多个不同路径',
reason: '同一 command executable 重复绑定不同路径必须失败。',
},
{
args: [
'--evidence-root',
root,
'--require-command-arg',
'enable-apply',
],
expected:
'--require-command-arg 必须使用 <phase>:<commandName>:<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}`);
}
}