#!/usr/bin/env node import { spawnSync } from 'node:child_process'; import { createHash } from 'node:crypto'; import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, symlinkSync, writeFileSync, } from 'node:fs'; import { tmpdir } from 'node:os'; import path from 'node:path'; const COMMAND_EVIDENCE_SCRIPT = 'scripts/ops/pingora-cutover-command-evidence.mjs'; const failures = []; const tmpRoot = mkdtempSync( path.join(tmpdir(), 'genarrative-pingora-cutover-command-evidence-'), ); try { main(); } finally { rmSync(tmpRoot, { recursive: true, force: true }); } if (failures.length > 0) { console.error('[check:pingora-cutover-command-evidence] FAILED'); for (const failure of failures) { console.error(`- ${failure}`); } process.exit(1); } console.log('[check:pingora-cutover-command-evidence] OK'); function main() { assertScriptShape(); assertCommandEvidenceSucceedsAndWritesArtifacts(); assertCommandEvidenceWritesCutoverRunId(); assertCommandFailureStillWritesEvidenceAndFails(); assertCommandEvidenceRedactsProbeTokensFromArtifacts(); assertRejectsUnsafeNames(); assertRejectsRelativeOutputRoot(); assertRejectsFilesystemRootOutputRoot(); assertRejectsOutputRootWithControlCharacters(); assertRejectsSymlinkOutputRootBeforeCommand(); assertRejectsFileOutputRootBeforeCommand(); assertRejectsRelativeCommandPath(); assertRejectsBareCommandName(); assertRejectsFilesystemRootCommand(); assertRejectsCommandWithControlCharacters(); assertRejectsCommandArgWithControlCharacters(); assertRejectsRelativeExpectedExecutable(); assertRejectsFilesystemRootExpectedExecutable(); assertRejectsExpectedExecutableWithControlCharacters(); assertRejectsMismatchedExpectedExecutableBeforeCommand(); assertRejectsMissingRequiredArgBeforeCommand(); } function assertScriptShape() { const content = readFileSync(COMMAND_EVIDENCE_SCRIPT, 'utf8'); assertIncludes( content, 'command.stdout.txt', '命令证据脚本必须保存真实命令 stdout。', ); assertIncludes( content, 'command.stderr.txt', '命令证据脚本必须保存真实命令 stderr。', ); assertIncludes( content, 'command-record.json', '命令证据脚本必须保存结构化命令记录。', ); assertIncludes( content, 'manifest.json', '命令证据脚本必须写 manifest。', ); assertIncludes( content, 'cutoverRunId', '命令证据脚本必须支持写入切换批次 ID。', ); assertIncludes( content, '--cutover-run-id', '命令证据脚本 usage 必须公开切换批次 ID 参数。', ); assertIncludes( content, '--expected-executable', '命令证据脚本 usage 必须公开预期真实命令参数。', ); assertIncludes( content, '--require-arg', '命令证据脚本 usage 必须公开真实命令必需参数。', ); assertIncludes( content, 'SECRET_VALUE_FLAGS', '命令证据脚本必须集中维护敏感参数脱敏列表。', ); assertIncludes( content, 'redactSecrets(text', '命令证据脚本必须脱敏 stdout/stderr 中的敏感值。', ); assertIncludes( content, "flag: 'wx'", '命令证据脚本写证据文件时必须避免覆盖既有文件。', ); assertIncludes( content, 'BUNDLE_DIR_MODE = 0o750', '命令证据目录必须显式固定权限。', ); assertIncludes( content, 'EVIDENCE_FILE_MODE = 0o640', '命令证据文件必须显式固定权限。', ); } function assertCommandEvidenceSucceedsAndWritesArtifacts() { const fixture = prepareFixture('success'); const result = runCommandEvidence(fixture, [ '--phase', 'enable-apply', '--command-name', 'pingora-direct-enable-apply', '--output-root', fixture.outputRoot, '--expected-executable', fixture.commandScript, '--', fixture.commandScript, '--message', 'hello-command', ]); assertStatus(result, 0, '真实命令成功时命令证据脚本应成功退出。'); assertIncludes( result.stdout, 'hello-command', '命令证据脚本必须把真实命令 stdout 透传到终端。', ); const output = parseLastJsonObject(result.stdout, '成功命令证据 stdout'); assertEqual(output.status, 'OK', 'stdout JSON 必须记录 OK 状态。'); assertFileExists(output.bundleDir, '命令证据目录必须存在。'); const manifest = readJson(path.join(output.bundleDir, 'manifest.json')); assertEqual(manifest.summary.status, 'OK', 'manifest 必须记录 OK 状态。'); assertEqual( manifest.command?.name, 'pingora-direct-enable-apply', 'manifest 必须记录命令名。', ); assertEqual( manifest.command?.exitCode, 0, 'manifest 必须记录真实命令退出码。', ); assertEqual( manifest.expectedExecutable, fixture.commandScript, 'manifest 必须记录预期真实命令路径。', ); assertEqual( manifest.command?.expectedExecutable, fixture.commandScript, 'command-record 必须记录预期真实命令路径。', ); assertEqual( manifest.command?.executable, fixture.commandScript, 'manifest.command 必须记录真实命令绝对路径。', ); const commandRecord = readJson(path.join(output.bundleDir, 'command-record.json')); assertEqual( commandRecord.executable, fixture.commandScript, 'command-record.json 必须记录真实命令绝对路径。', ); assertEqual( output.expectedExecutable, fixture.commandScript, 'stdout JSON 必须返回预期真实命令路径。', ); assertIncludes( manifest.command?.args || [], '--message', 'manifest 必须记录结构化命令参数。', ); assertManifestFileMetadata( output.bundleDir, manifest.files?.stdout, 'command.stdout.txt', ); assertManifestFileMetadata( output.bundleDir, manifest.files?.stderr, 'command.stderr.txt', ); assertManifestFileMetadata( output.bundleDir, manifest.files?.commandRecord, 'command-record.json', ); assertFileExists( path.join(output.bundleDir, 'command.stdout.txt'), '命令证据必须保存 stdout。', ); assertFileExists( path.join(output.bundleDir, 'command.stderr.txt'), '命令证据必须保存 stderr。', ); assertFileExists( path.join(output.bundleDir, 'command-record.json'), '命令证据必须保存 command-record.json。', ); assertMode(output.bundleDir, 0o750, '命令证据目录权限必须是 0750。'); for (const fileName of [ 'manifest.json', 'command.stdout.txt', 'command.stderr.txt', 'command-record.json', ]) { assertMode( path.join(output.bundleDir, fileName), 0o640, `${fileName} 权限必须是 0640。`, ); } } function assertCommandEvidenceWritesCutoverRunId() { const fixture = prepareFixture('cutover-run-id'); const result = runCommandEvidence(fixture, [ '--phase', 'enable-apply', '--command-name', 'pingora-direct-enable-apply', '--cutover-run-id', 'cutover-20260617T020000Z', '--output-root', fixture.outputRoot, '--', fixture.commandScript, '--message', 'hello-cutover-run', ]); assertStatus(result, 0, '提供 cutover run id 时命令证据脚本应成功退出。'); if (result.status !== 0) { return; } const output = parseLastJsonObject(result.stdout, 'cutover run id 命令证据 stdout'); assertEqual( output.cutoverRunId, 'cutover-20260617T020000Z', '命令证据 stdout 必须返回 cutoverRunId。', ); const manifest = readJson(path.join(output.bundleDir, 'manifest.json')); assertEqual( manifest.cutoverRunId, 'cutover-20260617T020000Z', '命令证据 manifest 必须记录 cutoverRunId。', ); assertEqual( manifest.command?.cutoverRunId, 'cutover-20260617T020000Z', 'command-record 必须同步记录 cutoverRunId。', ); } function assertCommandFailureStillWritesEvidenceAndFails() { const fixture = prepareFixture('failure'); const result = runCommandEvidence(fixture, [ '--phase', 'rollback-apply', '--command-name', 'pingora-direct-rollback-apply', '--output-root', fixture.outputRoot, '--', fixture.commandScript, '--message', 'rollback-output', '--fail', ]); assertStatus(result, 37, '真实命令失败时命令证据脚本应返回真实退出码。'); const output = parseLastJsonObject(result.stdout, '失败命令证据 stdout'); assertEqual(output.status, 'FAILED', '失败 stdout JSON 必须记录 FAILED。'); assertFileExists(output.bundleDir, '失败时仍必须保留命令证据目录。'); const manifest = readJson(path.join(output.bundleDir, 'manifest.json')); assertEqual( manifest.summary.exitCode, 37, '失败 manifest 必须记录真实退出码。', ); assertFileExists( path.join(output.bundleDir, 'command.stdout.txt'), '失败时仍必须保存 stdout。', ); assertFileExists( path.join(output.bundleDir, 'command.stderr.txt'), '失败时仍必须保存 stderr。', ); } function assertCommandEvidenceRedactsProbeTokensFromArtifacts() { const fixture = prepareFixture('redact-token'); const result = runCommandEvidence(fixture, [ '--phase', 'rollback-apply', '--command-name', 'pingora-direct-rollback-apply', '--output-root', fixture.outputRoot, '--', fixture.commandScript, '--message', 'probe-token=secret-shadow-token', '--pingora-shadow-probe-token', 'secret-shadow-token', '--direct-probe-token=secret-inline-token', '--echo-env-token', ]); assertStatus(result, 0, '带 token 的命令证据脚本应成功退出。'); for (const secret of [ 'secret-shadow-token', 'secret-inline-token', 'secret-env-token', ]) { assertNotIncludes( result.stdout, secret, '命令证据脚本透传 stdout 时不能泄露 token。', ); } const output = parseLastJsonObject(result.stdout, '脱敏命令证据 stdout'); for (const fileName of [ 'manifest.json', 'command.stdout.txt', 'command.stderr.txt', 'command-record.json', ]) { const content = readFileSync(path.join(output.bundleDir, fileName), 'utf8'); for (const secret of [ 'secret-shadow-token', 'secret-inline-token', 'secret-env-token', ]) { assertNotIncludes( content, secret, `${fileName} 不能泄露 token 原文。`, ); } assertIncludes(content, '', `${fileName} 必须保留脱敏占位符。`); } } function assertRejectsUnsafeNames() { const fixture = prepareFixture('unsafe-name'); for (const [flag, value, expected] of [ ['--phase', '../rollback', '--phase 只能包含 ASCII'], ['--command-name', '回退', '--command-name 只能包含 ASCII'], ['--cutover-run-id', 'cutover 1', '--cutover-run-id 只能包含 ASCII'], ]) { const result = runCommandEvidence(fixture, [ flag, value, '--output-root', fixture.outputRoot, '--', fixture.commandScript, ]); if ((result.status ?? 0) === 0) { failures.push(`${flag} 使用不安全值时必须失败。`); } assertIncludes( `${result.stdout}\n${result.stderr}`, expected, `${flag} 不安全值必须给出明确错误。`, ); assertNotIncludes( `${result.stdout}\n${result.stderr}`, '[fake-command]', `${flag} 不安全值被拒绝时不应执行真实命令。`, ); } } function assertRejectsRelativeOutputRoot() { const fixture = prepareFixture('relative-output-root'); const result = runCommandEvidence(fixture, [ '--output-root', 'pingora-evidence', '--', fixture.commandScript, ]); if ((result.status ?? 0) === 0) { failures.push('命令证据脚本必须拒绝相对 --output-root。'); } assertIncludes( `${result.stdout}\n${result.stderr}`, '--output-root 必须是绝对路径', '相对 output-root 必须给出明确错误。', ); } function assertRejectsFilesystemRootOutputRoot() { const fixture = prepareFixture('root-output-root'); const result = runCommandEvidence(fixture, [ '--output-root', '/', '--', fixture.commandScript, ]); if ((result.status ?? 0) === 0) { failures.push('命令证据脚本必须拒绝文件系统根目录 output-root。'); } assertIncludes( `${result.stdout}\n${result.stderr}`, '--output-root 不能是文件系统根目录', '根目录 output-root 必须给出明确错误。', ); } function assertRejectsOutputRootWithControlCharacters() { const fixture = prepareFixture('output-root-control-characters'); const result = runCommandEvidence(fixture, [ '--output-root', `${fixture.outputRoot}\n--phase rollback-apply`, '--', fixture.commandScript, ]); if ((result.status ?? 0) === 0) { failures.push('命令证据脚本必须拒绝带换行的 --output-root。'); } assertIncludes( `${result.stdout}\n${result.stderr}`, '--output-root 不能包含换行或 NUL 字符', '带换行 output-root 必须给出明确错误。', ); assertNotIncludes( `${result.stdout}\n${result.stderr}`, '[fake-command]', '带换行 output-root 被拒绝时不应执行真实命令。', ); } function assertRejectsSymlinkOutputRootBeforeCommand() { const fixture = prepareFixture('symlink-output-root'); const realTarget = path.join(tmpRoot, 'symlink-output-root-real-target'); rmSync(fixture.outputRoot, { recursive: true, force: true }); mkdirSync(realTarget, { recursive: true }); symlinkSync(realTarget, fixture.outputRoot, 'dir'); const result = runCommandEvidence(fixture, [ '--output-root', fixture.outputRoot, '--', fixture.commandScript, ]); if ((result.status ?? 0) === 0) { failures.push('命令证据脚本必须拒绝符号链接 output-root。'); } assertIncludes( `${result.stdout}\n${result.stderr}`, '--output-root 不能是符号链接', '符号链接 output-root 必须给出明确错误。', ); assertNotIncludes( `${result.stdout}\n${result.stderr}`, '[fake-command]', '符号链接 output-root 被拒绝时不应执行真实命令。', ); } function assertRejectsFileOutputRootBeforeCommand() { const fixture = prepareFixture('file-output-root'); rmSync(fixture.outputRoot, { recursive: true, force: true }); writeFileSync(fixture.outputRoot, 'not a directory\n', 'utf8'); const result = runCommandEvidence(fixture, [ '--output-root', fixture.outputRoot, '--', fixture.commandScript, ]); if ((result.status ?? 0) === 0) { failures.push('命令证据脚本必须拒绝非目录 output-root。'); } assertIncludes( `${result.stdout}\n${result.stderr}`, '--output-root 已存在但不是目录', '非目录 output-root 必须给出明确错误。', ); assertNotIncludes( `${result.stdout}\n${result.stderr}`, '[fake-command]', '非目录 output-root 被拒绝时不应执行真实命令。', ); } function assertRejectsRelativeCommandPath() { const fixture = prepareFixture('relative-command-path'); const result = runCommandEvidence(fixture, [ '--output-root', fixture.outputRoot, '--', 'scripts/deploy/pingora-direct-rollback.sh', ]); if ((result.status ?? 0) === 0) { failures.push('命令证据脚本必须拒绝带路径的相对命令。'); } assertIncludes( `${result.stdout}\n${result.stderr}`, '真实命令必须是绝对路径', '相对命令路径必须给出明确错误。', ); } function assertRejectsBareCommandName() { const fixture = prepareFixture('bare-command-name'); const result = runCommandEvidence(fixture, [ '--output-root', fixture.outputRoot, '--', 'echo', 'SHOULD_NOT_RUN', ]); if ((result.status ?? 0) === 0) { failures.push('命令证据脚本必须拒绝 PATH 裸命令名。'); } assertIncludes( `${result.stdout}\n${result.stderr}`, '真实命令必须是绝对路径', 'PATH 裸命令名必须给出明确错误。', ); assertNotIncludes( `${result.stdout}\n${result.stderr}`, 'SHOULD_NOT_RUN', 'PATH 裸命令名被拒绝时不应执行真实命令。', ); } function assertRejectsFilesystemRootCommand() { const fixture = prepareFixture('filesystem-root-command'); const result = runCommandEvidence(fixture, [ '--output-root', fixture.outputRoot, '--', path.parse(process.cwd()).root, ]); if ((result.status ?? 0) === 0) { failures.push('命令证据脚本必须拒绝文件系统根目录真实命令。'); } assertIncludes( `${result.stdout}\n${result.stderr}`, '真实命令不能是文件系统根目录', '文件系统根目录真实命令必须给出明确错误。', ); } function assertRejectsCommandWithControlCharacters() { const fixture = prepareFixture('command-control-characters'); const result = runCommandEvidence(fixture, [ '--output-root', fixture.outputRoot, '--', `${fixture.commandScript}\n`, ]); if ((result.status ?? 0) === 0) { failures.push('命令证据脚本必须拒绝带换行的真实命令。'); } assertIncludes( `${result.stdout}\n${result.stderr}`, '真实命令不能包含换行或 NUL 字符', '带换行的真实命令必须给出明确错误。', ); assertNotIncludes( `${result.stdout}\n${result.stderr}`, '[fake-command]', '带换行的真实命令被拒绝时不应执行真实命令。', ); } function assertRejectsCommandArgWithControlCharacters() { const fixture = prepareFixture('command-arg-control-characters'); const result = runCommandEvidence(fixture, [ '--output-root', fixture.outputRoot, '--', fixture.commandScript, '--message', 'line1\nline2', ]); if ((result.status ?? 0) === 0) { failures.push('命令证据脚本必须拒绝带换行的真实命令参数。'); } assertIncludes( `${result.stdout}\n${result.stderr}`, '真实命令参数不能包含换行或 NUL 字符', '带换行的真实命令参数必须给出明确错误。', ); assertNotIncludes( `${result.stdout}\n${result.stderr}`, '[fake-command]', '带换行的真实命令参数被拒绝时不应执行真实命令。', ); } function assertRejectsRelativeExpectedExecutable() { const fixture = prepareFixture('relative-expected-executable'); const result = runCommandEvidence(fixture, [ '--output-root', fixture.outputRoot, '--expected-executable', 'scripts/deploy/pingora-direct-enable.sh', '--', fixture.commandScript, ]); if ((result.status ?? 0) === 0) { failures.push('命令证据脚本必须拒绝相对 --expected-executable。'); } assertIncludes( `${result.stdout}\n${result.stderr}`, '--expected-executable 必须是绝对路径', '相对 expected executable 必须给出明确错误。', ); assertNotIncludes( `${result.stdout}\n${result.stderr}`, '[fake-command]', '相对 expected executable 被拒绝时不应执行真实命令。', ); } function assertRejectsFilesystemRootExpectedExecutable() { const fixture = prepareFixture('filesystem-root-expected-executable'); const result = runCommandEvidence(fixture, [ '--output-root', fixture.outputRoot, '--expected-executable', path.parse(process.cwd()).root, '--', fixture.commandScript, ]); if ((result.status ?? 0) === 0) { failures.push('命令证据脚本必须拒绝文件系统根目录 --expected-executable。'); } assertIncludes( `${result.stdout}\n${result.stderr}`, '--expected-executable 不能是文件系统根目录', '文件系统根目录 expected executable 必须给出明确错误。', ); assertNotIncludes( `${result.stdout}\n${result.stderr}`, '[fake-command]', '文件系统根目录 expected executable 被拒绝时不应执行真实命令。', ); } function assertRejectsExpectedExecutableWithControlCharacters() { const fixture = prepareFixture('expected-executable-control-characters'); const result = runCommandEvidence(fixture, [ '--output-root', fixture.outputRoot, '--expected-executable', `${fixture.commandScript}\n`, '--', fixture.commandScript, ]); if ((result.status ?? 0) === 0) { failures.push('命令证据脚本必须拒绝带换行的 --expected-executable。'); } assertIncludes( `${result.stdout}\n${result.stderr}`, '--expected-executable 不能包含换行或 NUL 字符', '带换行的 expected executable 必须给出明确错误。', ); assertNotIncludes( `${result.stdout}\n${result.stderr}`, '[fake-command]', '带换行 expected executable 被拒绝时不应执行真实命令。', ); } function assertRejectsMismatchedExpectedExecutableBeforeCommand() { const fixture = prepareFixture('mismatched-expected-executable'); const expected = path.join(fixture.root, 'expected-enable.sh'); const result = runCommandEvidence(fixture, [ '--output-root', fixture.outputRoot, '--expected-executable', expected, '--', fixture.commandScript, ]); if ((result.status ?? 0) === 0) { failures.push('命令证据脚本必须拒绝与 --expected-executable 不一致的真实命令。'); } assertIncludes( `${result.stdout}\n${result.stderr}`, '真实命令与 --expected-executable 不一致', '真实命令与 expected executable 不一致时必须给出明确错误。', ); assertNotIncludes( `${result.stdout}\n${result.stderr}`, '[fake-command]', '真实命令与 expected executable 不一致时不应执行真实命令。', ); } function assertRejectsMissingRequiredArgBeforeCommand() { const fixture = prepareFixture('missing-required-arg'); const result = runCommandEvidence(fixture, [ '--output-root', fixture.outputRoot, '--require-arg', '--apply', '--', fixture.commandScript, '--message', 'dry-run-only', ]); if ((result.status ?? 0) === 0) { failures.push('命令证据脚本必须拒绝缺少 --require-arg 的真实命令。'); } assertIncludes( `${result.stdout}\n${result.stderr}`, '真实命令参数缺少 --require-arg 要求的 --apply', '真实命令缺少 required arg 时必须给出明确错误。', ); assertNotIncludes( `${result.stdout}\n${result.stderr}`, '[fake-command]', '真实命令缺少 required arg 时不应执行真实命令。', ); } function prepareFixture(name) { const root = path.join(tmpRoot, name); const outputRoot = path.join(root, 'evidence'); const commandScript = path.join(root, 'fake-command.sh'); mkdirSync(outputRoot, { recursive: true }); writeFileSync( commandScript, [ '#!/usr/bin/env bash', 'set -euo pipefail', 'message="ok"', 'exit_code=0', 'while [[ $# -gt 0 ]]; do', ' case "$1" in', ' --message)', ' message="$2"', ' shift 2', ' ;;', ' --fail)', ' exit_code=37', ' shift', ' ;;', ' --echo-env-token)', ' echo "[fake-command] env=${GENARRATIVE_PINGORA_DIRECT_PROBE_TOKEN:-}"', ' shift', ' ;;', ' --pingora-shadow-probe-token)', ' echo "[fake-command] token=$2" >&2', ' shift 2', ' ;;', ' *)', ' shift', ' ;;', ' esac', 'done', 'echo "[fake-command] stdout ${message}"', 'echo "[fake-command] stderr ${message}" >&2', 'exit "${exit_code}"', '', ].join('\n'), 'utf8', ); spawnSync('chmod', ['0755', commandScript], { encoding: 'utf8' }); return { root, outputRoot, commandScript }; } function runCommandEvidence(fixture, args) { return spawnSync('node', [COMMAND_EVIDENCE_SCRIPT, ...args], { cwd: process.cwd(), encoding: 'utf8', env: { ...process.env, GENARRATIVE_PINGORA_CUTOVER_RUN_ID: '', GENARRATIVE_PINGORA_CUTOVER_EVIDENCE_ROOT: fixture.outputRoot, GENARRATIVE_PINGORA_DIRECT_PROBE_TOKEN: 'secret-env-token', }, }); } function parseLastJsonObject(text, label) { const end = text.lastIndexOf('}'); if (end < 0) { failures.push(`${label} 未包含 JSON 结果。`); return {}; } for (let start = text.lastIndexOf('{', end); start >= 0; start = text.lastIndexOf('{', start - 1)) { try { return JSON.parse(text.slice(start, end + 1)); } catch { // 继续向前找外层 JSON 起点。 } } failures.push(`${label} 未包含合法 JSON 结果。`); return {}; } function readJson(filePath) { try { return JSON.parse(readFileSync(filePath, 'utf8')); } catch (error) { failures.push(`${filePath} 不是合法 JSON: ${error.message}`); return {}; } } function assertManifestFileMetadata(bundleDir, metadata, expectedPath) { if (!metadata || typeof metadata !== 'object') { failures.push(`${expectedPath} 必须在 manifest.files 中记录结构化元数据。`); return; } assertEqual( metadata.path, expectedPath, `${expectedPath} manifest 元数据必须记录文件名。`, ); const filePath = path.join(bundleDir, expectedPath); const content = readFileSync(filePath); assertEqual( metadata.sizeBytes, content.length, `${expectedPath} manifest 元数据必须记录文件大小。`, ); assertEqual( metadata.sha256, createHash('sha256').update(content).digest('hex'), `${expectedPath} manifest 元数据必须记录 sha256。`, ); } function assertFileExists(filePath, reason) { if (!existsSync(filePath)) { failures.push(`${reason} 缺少: ${filePath}`); } } function assertMode(filePath, expected, reason) { if (!existsSync(filePath)) { return; } const actual = statSync(filePath).mode & 0o777; if (actual !== expected) { failures.push( `${reason} 实际 ${actual.toString(8)},预期 ${expected.toString(8)}。`, ); } } function assertStatus(result, expected, reason) { const actual = result.status ?? 0; if (actual !== expected) { failures.push( `${reason} 预期退出码 ${expected},实际 ${actual}。\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`, ); } } function assertEqual(actual, expected, reason) { if (actual !== expected) { failures.push(`${reason} 实际 ${JSON.stringify(actual)},预期 ${JSON.stringify(expected)}。`); } } function assertIncludes(value, expected, reason) { if (!String(value).includes(expected)) { failures.push(`${reason} 缺少: ${expected}`); } } function assertNotIncludes(value, unexpected, reason) { if (String(value).includes(unexpected)) { failures.push(`${reason} 不应包含: ${unexpected}`); } }