#!/usr/bin/env node import { spawnSync } from 'node:child_process'; import { chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync, } from 'node:fs'; import { tmpdir } from 'node:os'; import path from 'node:path'; const SWITCH_SCRIPT = 'scripts/deploy/pingora-health-patrol-env-switch.mjs'; const ENV_CHECK_SCRIPT = path.join( process.cwd(), 'scripts/check-production-health-patrol-env.mjs', ); const failures = []; const tmpRoot = mkdtempSync( path.join(tmpdir(), 'genarrative-pingora-health-patrol-switch-'), ); try { main(); } finally { rmSync(tmpRoot, { recursive: true, force: true }); } if (failures.length > 0) { console.error('[check:pingora-health-patrol-env-switch] FAILED'); for (const failure of failures) { console.error(`- ${failure}`); } process.exit(1); } console.log('[check:pingora-health-patrol-env-switch] OK'); function main() { assertScriptShape(); assertDryRunDoesNotModifyEnv(); assertDryRunEnvFileModeIsPrivate(); assertApplyPreservesEnvFileModeAndOwnership(); assertApplySwitchesToPingoraDirectAndPreservesOtherKeys(); assertApplySwitchesBackToNginxAndClearsHost(); assertApplyDoesNotWriteWhenVerificationFails(); assertApplyRejectsSymlinkEnvFileBeforeWrite(); assertMissingManagedKeysAreAppended(); assertRejectsDirectLoopbackWithoutHost(); assertRejectsInvalidAndConflictingHostOptions(); assertRejectsRelativePaths(); assertRejectsFilesystemRootPaths(); assertRejectsControlCharacterInputsBeforeWrite(); assertRejectsInvalidBoolEnv(); assertReleaseLayoutUsesBundledCheckScript(); } function assertScriptShape() { const content = readFileSync(SWITCH_SCRIPT, 'utf8'); assertIncludes(content, '--apply', '切换脚本必须显式要求 --apply 才写 env。'); assertIncludes( content, '当前是 dry-run', '切换脚本必须在 dry-run 中明确不会写 env。', ); assertIncludes( content, 'check-production-health-patrol-env.mjs', '切换脚本必须复用生产 health patrol env 复核脚本。', ); assertIncludes( content, 'Pingora direct 模式使用本机 public base URL 时必须提供 --public-host', '切换脚本必须阻断 direct loopback 缺少正式 Host。', ); assertIncludes( content, 'health patrol env 中存在重复配置', '切换脚本必须拒绝重复目标键,避免 env 解析歧义。', ); assertIncludes( content, 'runHealthPatrolEnvCheck(checkFile);', '切换脚本必须先用临时目标 env 复核。', ); assertIncludes( content, 'writeEnvFileAtomically(config.envFile, nextText);', '切换脚本通过复核后才可原子写入真实 env。', ); assertIncludes( content, '--env-file 不能是符号链接', 'apply 写入真实 env 前必须拒绝符号链接目标。', ); assertIncludes( content, 'DRY_RUN_ENV_FILE_MODE = 0o600', '临时复核 env 文件权限必须固定为 0600。', ); assertIncludes( content, 'chownSync(tempFile, currentStat.uid, currentStat.gid);', '真实 env 原子替换必须保留原文件 owner/group。', ); } function assertDryRunDoesNotModifyEnv() { const envFile = writeEnv('dry-run.env', { GENARRATIVE_HEALTH_PATROL_API_BASE_URL: 'http://127.0.0.1:8082', GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL: 'http://127.0.0.1', GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE: 'nginx', GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST: '', }); const before = readFileSync(envFile, 'utf8'); const result = runSwitch([ '--env-file', envFile, '--gateway-mode', 'pingora-direct', '--public-base-url', 'https://127.0.0.1', '--public-host', 'example.com', ]); assertStatus(result, 0, 'dry-run 应成功并通过临时 env 复核。'); assertIncludes(result.stdout, 'apply=false', 'dry-run 输出必须显示 apply=false。'); assertIncludes( result.stdout, '[check:production-health-patrol-env] OK', 'dry-run 必须用临时 env 跑复核脚本。', ); const after = readFileSync(envFile, 'utf8'); if (after !== before) { failures.push('dry-run 不应修改 health patrol env 文件。'); } } function assertDryRunEnvFileModeIsPrivate() { const envFile = writeEnv('dry-run-mode.env', { GENARRATIVE_HEALTH_PATROL_API_BASE_URL: 'http://127.0.0.1:8082', GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL: 'http://127.0.0.1', GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE: 'nginx', GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST: '', }); const modeCheckScript = path.join(tmpRoot, 'check-dry-run-env-mode.mjs'); writeFileSync( modeCheckScript, [ '#!/usr/bin/env node', "import { statSync } from 'node:fs';", "const envFileIndex = process.argv.indexOf('--env-file');", 'const envFile = process.argv[envFileIndex + 1];', 'const mode = statSync(envFile).mode & 0o777;', 'if (mode !== 0o600) {', ' console.error(`dry-run env mode must be 0600, got ${mode.toString(8)}`);', ' process.exit(1);', '}', 'console.log(`[fake-health-patrol-env-check] dry-run env mode ${mode.toString(8)}`);', '', ].join('\n'), 'utf8', ); chmodSync(modeCheckScript, 0o755); const result = runSwitch([ '--env-file', envFile, '--check-script', modeCheckScript, '--gateway-mode', 'pingora-direct', '--public-base-url', 'https://127.0.0.1', '--public-host', 'example.com', ]); assertStatus(result, 0, 'dry-run 临时复核 env 文件权限应为 0600。'); assertIncludes( result.stdout, 'dry-run env mode 600', 'dry-run 必须把 0600 临时 env 交给复核脚本。', ); } function assertApplyPreservesEnvFileModeAndOwnership() { const envFile = writeEnv('apply-mode-owner.env', { GENARRATIVE_HEALTH_PATROL_API_BASE_URL: 'http://127.0.0.1:8082', GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL: 'http://127.0.0.1', GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE: 'nginx', GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST: '', }); chmodSync(envFile, 0o640); const beforeStat = statSync(envFile); const result = runSwitch([ '--env-file', envFile, '--gateway-mode', 'pingora-direct', '--public-base-url', 'https://127.0.0.1', '--public-host', 'example.com', '--apply', ]); assertStatus(result, 0, 'apply 原子替换应成功。'); const afterStat = statSync(envFile); const beforeMode = beforeStat.mode & 0o777; const afterMode = afterStat.mode & 0o777; if (afterMode !== beforeMode) { failures.push( `apply 原子替换必须保留 env 文件权限,实际 ${afterMode.toString(8)},预期 ${beforeMode.toString(8)}。`, ); } if (afterStat.uid !== beforeStat.uid || afterStat.gid !== beforeStat.gid) { failures.push('apply 原子替换必须保留 env 文件 owner/group。'); } } function assertApplySwitchesToPingoraDirectAndPreservesOtherKeys() { const envFile = writeEnv('direct.env', { GENARRATIVE_HEALTH_PATROL_API_BASE_URL: 'http://127.0.0.1:8082', GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL: 'http://127.0.0.1', GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE: 'nginx', GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST: '', GENARRATIVE_HEALTH_PATROL_TIMEOUT_MS: '5000', }); const result = runSwitch([ '--env-file', envFile, '--gateway-mode', 'pingora-direct', '--public-base-url', 'https://127.0.0.1', '--public-host', 'example.com', '--apply', ]); assertStatus(result, 0, 'apply 切到 pingora-direct 应成功。'); const content = readFileSync(envFile, 'utf8'); assertIncludes( content, 'GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE=pingora-direct', 'apply 必须写入 pingora-direct gateway mode。', ); assertIncludes( content, 'GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL=https://127.0.0.1', 'apply 必须写入直连 public base URL。', ); assertIncludes( content, 'GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST=example.com', 'apply 必须写入正式 public Host。', ); assertIncludes( content, 'GENARRATIVE_HEALTH_PATROL_TIMEOUT_MS=5000', 'apply 必须保留其它 health patrol env 项。', ); } function assertApplySwitchesBackToNginxAndClearsHost() { const envFile = writeEnv('nginx.env', { GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL: 'https://127.0.0.1', GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE: 'pingora-direct', GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST: 'example.com', }); const result = runSwitch([ '--env-file', envFile, '--gateway-mode', 'nginx', '--public-base-url', 'http://127.0.0.1', '--clear-public-host', '--apply', ]); assertStatus(result, 0, 'apply 切回 nginx 并清空 Host 应成功。'); const content = readFileSync(envFile, 'utf8'); assertIncludes( content, 'GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE=nginx', '回退 apply 必须写回 nginx gateway mode。', ); assertIncludes( content, 'GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL=http://127.0.0.1', '回退 apply 必须恢复 Nginx public base URL。', ); assertIncludes( content, 'GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST=', '回退 apply 必须清空 public Host。', ); } function assertApplyDoesNotWriteWhenVerificationFails() { const envFile = writeEnv('apply-verify-fails.env', { GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL: 'http://127.0.0.1', GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE: 'nginx', GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST: '', }); const before = readFileSync(envFile, 'utf8'); const failingCheckScript = path.join( tmpRoot, 'failing-health-patrol-env-check.mjs', ); writeFileSync( failingCheckScript, [ '#!/usr/bin/env node', 'console.error("[fake-health-patrol-env-check] forced failure");', 'process.exit(1);', '', ].join('\n'), 'utf8', ); const result = runSwitch([ '--env-file', envFile, '--check-script', failingCheckScript, '--gateway-mode', 'pingora-direct', '--public-base-url', 'https://127.0.0.1', '--public-host', 'example.com', '--apply', ]); assertStatus(result, 1, '复核失败时 apply 必须失败。'); assertIncludes( result.stderr, '[fake-health-patrol-env-check] forced failure', '复核失败时必须透出复核脚本错误。', ); const after = readFileSync(envFile, 'utf8'); if (after !== before) { failures.push('复核失败时 apply 不应写入真实 health patrol env 文件。'); } } function assertApplyRejectsSymlinkEnvFileBeforeWrite() { const targetEnvFile = writeEnv('symlink-target.env', { GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL: 'http://127.0.0.1', GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE: 'nginx', GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST: '', }); const symlinkEnvFile = path.join(tmpRoot, 'symlink-health-patrol.env'); symlinkSync(targetEnvFile, symlinkEnvFile); const before = readFileSync(targetEnvFile, 'utf8'); const result = runSwitch([ '--env-file', symlinkEnvFile, '--gateway-mode', 'pingora-direct', '--public-base-url', 'https://127.0.0.1', '--public-host', 'example.com', '--apply', ]); assertStatus(result, 1, 'apply 传入符号链接 env 文件时必须失败。'); assertIncludes( result.stderr, '--env-file 不能是符号链接', '符号链接 env 文件失败时必须给出明确错误。', ); const after = readFileSync(targetEnvFile, 'utf8'); if (after !== before) { failures.push('符号链接 env 文件被拒绝时不应写入真实目标文件。'); } const linkText = readFileSync(symlinkEnvFile, 'utf8'); if (linkText !== before) { failures.push('符号链接 env 文件被拒绝时不应替换或改写链接内容。'); } } function assertMissingManagedKeysAreAppended() { const envFile = path.join(tmpRoot, 'append.env'); writeFileSync( envFile, [ '# existing env', 'GENARRATIVE_HEALTH_PATROL_TIMEOUT_MS=5000', '', ].join('\n'), 'utf8', ); const result = runSwitch([ '--env-file', envFile, '--gateway-mode', 'nginx', '--public-base-url', 'http://127.0.0.1', '--clear-public-host', '--apply', ]); assertStatus(result, 0, '缺少目标键时 apply 应追加并通过复核。'); const content = readFileSync(envFile, 'utf8'); assertIncludes( content, 'GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE=nginx', '缺少 gateway mode 时必须追加。', ); assertIncludes( content, 'GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL=http://127.0.0.1', '缺少 public base URL 时必须追加。', ); assertIncludes( content, 'GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST=', '缺少 public Host 时必须追加。', ); } function assertRejectsDirectLoopbackWithoutHost() { const envFile = writeEnv('direct-missing-host.env', { GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL: 'http://127.0.0.1', GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE: 'nginx', GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST: '', }); const result = runSwitch([ '--env-file', envFile, '--gateway-mode', 'pingora-direct', '--public-base-url', 'https://127.0.0.1', '--clear-public-host', ]); assertStatus(result, 1, 'direct loopback 缺少 Host 必须失败。'); assertIncludes( result.stderr, 'Pingora direct 模式使用本机 public base URL 时必须提供 --public-host', 'direct loopback 缺少 Host 必须给出明确错误。', ); } function assertRejectsInvalidAndConflictingHostOptions() { const envFile = writeEnv('invalid-host.env', { GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL: 'http://127.0.0.1', GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE: 'nginx', GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST: '', }); const invalidHost = runSwitch([ '--env-file', envFile, '--gateway-mode', 'nginx', '--public-base-url', 'http://127.0.0.1', '--public-host', 'https://example.com', ]); assertStatus(invalidHost, 1, '非法 Host 必须失败。'); assertIncludes( invalidHost.stderr, '--public-host 只能是 host 或 host:port', '非法 Host 必须给出明确错误。', ); const conflictingHost = runSwitch([ '--env-file', envFile, '--gateway-mode', 'nginx', '--public-base-url', 'http://127.0.0.1', '--public-host', 'example.com', '--clear-public-host', ]); assertStatus(conflictingHost, 1, 'Host 与清空 Host 同时传入必须失败。'); assertIncludes( conflictingHost.stderr, '--public-host 和 --clear-public-host 不能同时使用', 'Host 选项冲突必须给出明确错误。', ); } function assertRejectsRelativePaths() { const relativeEnv = runSwitch([ '--env-file', 'relative.env', '--gateway-mode', 'nginx', '--public-base-url', 'http://127.0.0.1', '--clear-public-host', ]); assertStatus(relativeEnv, 1, '相对 env 路径必须失败。'); assertIncludes( relativeEnv.stderr, '--env-file 必须是绝对路径', '相对 env 路径必须给出明确错误。', ); const envFile = writeEnv('relative-check.env', { GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL: 'http://127.0.0.1', GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE: 'nginx', GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST: '', }); const relativeCheck = runSwitch([ '--env-file', envFile, '--check-script', 'scripts/check-production-health-patrol-env.mjs', '--gateway-mode', 'nginx', '--public-base-url', 'http://127.0.0.1', '--clear-public-host', ]); assertStatus(relativeCheck, 1, '相对 check script 路径必须失败。'); assertIncludes( relativeCheck.stderr, '--check-script 必须是绝对路径', '相对 check script 路径必须给出明确错误。', ); } function assertRejectsFilesystemRootPaths() { const rootEnv = runSwitch([ '--env-file', '/', '--gateway-mode', 'nginx', '--public-base-url', 'http://127.0.0.1', '--clear-public-host', ]); assertStatus(rootEnv, 1, '文件系统根目录 env 路径必须失败。'); assertIncludes( rootEnv.stderr, '--env-file 不能是文件系统根目录', '文件系统根目录 env 路径必须给出明确错误。', ); const envFile = writeEnv('root-check-script.env', { GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL: 'http://127.0.0.1', GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE: 'nginx', GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST: '', }); const rootCheckScript = runSwitch([ '--env-file', envFile, '--check-script', '/', '--gateway-mode', 'nginx', '--public-base-url', 'http://127.0.0.1', '--clear-public-host', ]); assertStatus(rootCheckScript, 1, '文件系统根目录 check script 路径必须失败。'); assertIncludes( rootCheckScript.stderr, '--check-script 不能是文件系统根目录', '文件系统根目录 check script 路径必须给出明确错误。', ); } function assertRejectsControlCharacterInputsBeforeWrite() { const envFile = writeEnv('control-character.env', { GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL: 'http://127.0.0.1', GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE: 'nginx', GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST: '', }); const before = readFileSync(envFile, 'utf8'); const envFileResult = runSwitch([ '--env-file', `${envFile}\nspoofed`, '--gateway-mode', 'nginx', '--public-base-url', 'http://127.0.0.1', '--clear-public-host', '--apply', ]); assertStatus(envFileResult, 1, '带控制字符的 env 文件路径必须失败。'); assertIncludes( envFileResult.stderr, '--env-file 不能包含换行或 NUL 字符', '带控制字符的 env 文件路径必须给出明确错误。', ); const checkScriptResult = runSwitch([ '--env-file', envFile, '--check-script', `${ENV_CHECK_SCRIPT}\nspoofed`, '--gateway-mode', 'nginx', '--public-base-url', 'http://127.0.0.1', '--clear-public-host', '--apply', ]); assertStatus(checkScriptResult, 1, '带控制字符的 check script 路径必须失败。'); assertIncludes( checkScriptResult.stderr, '--check-script 不能包含换行或 NUL 字符', '带控制字符的 check script 路径必须给出明确错误。', ); const baseUrlResult = runSwitch([ '--env-file', envFile, '--gateway-mode', 'nginx', '--public-base-url', 'http://127.0.0.1\nspoofed', '--clear-public-host', '--apply', ]); assertStatus(baseUrlResult, 1, '带控制字符的 public base URL 必须失败。'); assertIncludes( baseUrlResult.stderr, '--public-base-url 不能包含换行或 NUL 字符', '带控制字符的 public base URL 必须给出明确错误。', ); const hostResult = runSwitch([ '--env-file', envFile, '--gateway-mode', 'pingora-direct', '--public-base-url', 'https://127.0.0.1', '--public-host', 'example.com\nspoofed', '--apply', ]); assertStatus(hostResult, 1, '带控制字符的 public Host 必须失败。'); assertIncludes( hostResult.stderr, '--public-host 不能包含换行或 NUL 字符', '带控制字符的 public Host 必须给出明确错误。', ); const after = readFileSync(envFile, 'utf8'); if (after !== before) { failures.push('带控制字符的切换参数被拒绝时不应写入真实 env 文件。'); } } function assertRejectsInvalidBoolEnv() { const envFile = writeEnv('invalid-bool-env.env', { GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL: 'http://127.0.0.1', GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE: 'nginx', GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST: '', }); const result = runSwitch( [ '--env-file', envFile, '--gateway-mode', 'nginx', '--public-base-url', 'http://127.0.0.1', ], { GENARRATIVE_HEALTH_PATROL_CLEAR_PUBLIC_HOST: 'maybe', }, ); assertStatus(result, 1, 'health patrol env 切换脚本必须拒绝非法布尔 env。'); assertIncludes( result.stderr, 'GENARRATIVE_HEALTH_PATROL_CLEAR_PUBLIC_HOST 必须是布尔值', 'health patrol env 切换脚本非法布尔 env 必须给出明确错误。', ); } function assertReleaseLayoutUsesBundledCheckScript() { const releaseRoot = path.join(tmpRoot, 'current-release'); const releaseScript = path.join( releaseRoot, 'scripts/deploy/pingora-health-patrol-env-switch.mjs', ); const releaseCheckScript = path.join( releaseRoot, 'scripts/check-production-health-patrol-env.mjs', ); const envFile = writeEnv('release-layout.env', { GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL: 'http://127.0.0.1', GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE: 'nginx', GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST: '', }); mkdirSync(path.dirname(releaseScript), { recursive: true }); mkdirSync(path.dirname(releaseCheckScript), { recursive: true }); copyFileSync(SWITCH_SCRIPT, releaseScript); copyFileSync('scripts/check-production-health-patrol-env.mjs', releaseCheckScript); chmodSync(releaseScript, 0o755); const result = spawnSync( 'node', [ '--', releaseScript, '--env-file', envFile, '--gateway-mode', 'pingora-direct', '--public-base-url', 'https://127.0.0.1', '--public-host', 'example.com', ], { cwd: tmpRoot, encoding: 'utf8', env: cleanEnv() }, ); assertStatus( result, 0, 'current release 中的切换脚本 dry-run 应能使用随包 env 复核脚本。', ); assertIncludes( result.stdout, realpathSync(releaseCheckScript), 'current release 中的切换脚本必须默认调用随包 env 复核脚本。', ); if (!existsSync(releaseCheckScript)) { failures.push('release layout 测试必须保留随包 env 复核脚本。'); } } function writeEnv(name, values) { const filePath = path.join(tmpRoot, name); const body = Object.entries(values) .map(([key, value]) => `${key}=${value}`) .join('\n'); writeFileSync(filePath, `${body}\n`, 'utf8'); return filePath; } function runSwitch(args, extraEnv = {}) { return spawnSync( 'node', ['--', SWITCH_SCRIPT, '--check-script', ENV_CHECK_SCRIPT, ...args], { cwd: process.cwd(), encoding: 'utf8', env: { ...cleanEnv(), ...extraEnv, }, }, ); } function cleanEnv() { return { ...process.env, GENARRATIVE_HEALTH_PATROL_ENV_FILE: '', GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE: '', GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL: '', GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST: '', GENARRATIVE_HEALTH_PATROL_CLEAR_PUBLIC_HOST: '', GENARRATIVE_HEALTH_PATROL_ENV_CHECK_SCRIPT: '', }; } 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 assertIncludes(content, needle, reason) { if (!content.includes(needle)) { failures.push(`${reason} 缺少: ${needle}`); } }