import assert from 'node:assert/strict'; import { spawnSync } from 'node:child_process'; import { mkdtempSync, mkdirSync, readFileSync, writeFileSync, existsSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import test from 'node:test'; import { inspectSourceLineEndings, installDependencies, npmEnvironment, validateEnvironment, validateFrontend, validateGoVersion, validateSourceLineEndings } from './toolchain.mjs'; const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm'; test('源码换行检查接受规范文本、无换行文件及二进制', () => { const records = [ 'i/lf w/lf attr/text=auto eol=lf \t页面 空格.vue', 'i/none w/none attr/text=auto eol=lf \tempty.js', 'i/-text w/-text attr/text=auto eol=lf \timage.png', 'i/lf w/lf attr/text=auto eol=lf \t换行\n文件.js', ].join('\0') + '\0'; assert.deepEqual(validateSourceLineEndings(records), { policy: 'lf', tracked_files: 4 }); }); for (const ending of ['crlf', 'mixed']) { test(`源码换行检查拒绝 ${ending} 而不是自动修改文件`, () => { assert.throws(() => validateSourceLineEndings(`i/lf w/${ending} attr/text=auto eol=lf \t页面.vue\0`), /非规范换行.*页面.vue/); }); } test('源码换行检查拒绝空、截断、格式错误或未知状态', () => { for (const output of ['', '\0', 'i/lf w/lf attr/text\tapp.js', 'invalid\0', 'i/lf w/unknown attr/text\tapp.js\0']) { assert.throws(() => validateSourceLineEndings(output)); } }); test('实际 Git 干净差异仍可检测工作树换行,且不改写源文件', (t) => { const directory = mkdtempSync(join(tmpdir(), 'scert-source-endings-')); t.after(() => rmSync(directory, { recursive: true, force: true })); const run = (...args) => { const result = spawnSync('git', args, { cwd: directory, encoding: 'utf8', windowsHide: true }); assert.equal(result.status, 0, `${result.error ?? ''}\n${result.stderr}`); return result.stdout; }; run('init', '--quiet'); writeFileSync(join(directory, '.gitattributes'), '* text=auto eol=lf\n'); const source = join(directory, '页面.vue'); writeFileSync(source, '\n'); writeFileSync(join(directory, 'image.png'), Buffer.from([0, 13, 10, 255])); run('add', '--', '.gitattributes', '页面.vue', 'image.png'); assert.equal(inspectSourceLineEndings(directory).tracked_files, 3); const changed = Buffer.from('\r\n'); writeFileSync(source, changed); assert.equal(run('diff', '--exit-code'), ''); assert.throws(() => inspectSourceLineEndings(directory), /非规范换行/); assert.deepEqual(readFileSync(source), changed); const result = spawnSync(process.execPath, [join(root, 'build', 'toolchain.mjs'), '--backend', directory, '--frontend', directory, '--install'], { cwd: directory, encoding: 'utf8', windowsHide: true, }); assert.equal(result.status, 1); assert.match(result.stderr, /非规范换行/); assert.equal(result.stdout, ''); assert.ok(!existsSync(join(directory, 'node_modules'))); assert.ok(!existsSync(join(directory, 'package-lock.json'))); assert.deepEqual(readFileSync(source), changed); }); test('非 Git 目录不能跳过源码换行检查', (t) => { const directory = mkdtempSync(join(tmpdir(), 'scert-source-no-git-')); t.after(() => rmSync(directory, { recursive: true, force: true })); assert.throws(() => inspectSourceLineEndings(directory), /git 执行失败/); }); function fixture() { const frontend = join(root, 'frontend'); const pkg = JSON.parse(readFileSync(join(frontend, 'package.json'), 'utf8')); return { pkg, lock: JSON.parse(readFileSync(join(frontend, 'package-lock.json'), 'utf8')), npmrc: readFileSync(join(frontend, '.npmrc'), 'utf8'), actual: { ...pkg.engines }, }; } function validate(value) { validateFrontend(value.pkg, value.lock, value.npmrc, value.actual); } test('仓库版本声明、锁文件和安装策略一致', () => { validate(fixture()); }); for (const [name, mutate] of Object.entries({ '拒绝错误 Node 版本': (f) => { f.actual.node = '24.0.0'; }, '拒绝错误 npm 版本': (f) => { f.actual.npm = '10.0.0'; }, '拒绝工具链范围版本': (f) => { f.pkg.engines.node = '^24.19.0'; }, '拒绝开发工具链警告降级': (f) => { f.pkg.devEngines.runtime.onFail = 'warn'; }, '拒绝包管理器声明漂移': (f) => { f.pkg.packageManager = 'npm@10.0.0'; }, '拒绝旧版锁文件': (f) => { f.lock.lockfileVersion = 2; }, '拒绝锁文件工具链漂移': (f) => { f.lock.packages[''].engines.node = '24.0.0'; }, '拒绝依赖范围版本': (f) => { f.pkg.dependencies.vue = '^3.5.41'; }, '拒绝依赖锁定漂移': (f) => { f.lock.packages['node_modules/vue'].version = '3.5.0'; }, '拒绝锁文件多余直接依赖': (f) => { f.lock.packages[''].dependencies.extra = '1.0.0'; }, '拒绝前端标识漂移': (f) => { f.lock.name = 'other-project'; }, '拒绝放行安装脚本': (f) => { f.npmrc = f.npmrc.replace('ignore-scripts=true', 'ignore-scripts=false'); }, '拒绝重复安装策略': (f) => { f.npmrc += '\nignore-scripts=false\n'; }, '拒绝关闭版本限制': (f) => { f.npmrc = f.npmrc.replace('engine-strict=true', 'engine-strict=false'); }, '拒绝额外 npm 配置': (f) => { f.npmrc += '\nforce=true\n'; }, '拒绝脚本白名单绕行': (f) => { f.pkg.allowScripts = { esbuild: true }; }, '拒绝全量脚本放行': (f) => { f.pkg.dangerouslyAllowAllScripts = true; }, })) { test(name, () => { const value = fixture(); mutate(value); assert.throws(() => validate(value)); }); } test('Go 工具链必须精确匹配模块基线', () => { assert.equal(validateGoVersion('module test\n\ngo 1.26.6\n', 'go1.26.6'), '1.26.6'); for (const version of ['go1.26.2', 'go1.26.7', 'devel go1.27', '']) { assert.throws(() => validateGoVersion('go 1.26.6\n', version)); } assert.throws(() => validateGoVersion('go 1.26\n', 'go1.26.6')); }); test('拒绝外部构建覆盖和未支持的构建机', () => { const host = { platform: 'win32', arch: 'x64' }; validateEnvironment({}, host); validateEnvironment({ GOWORK: 'off' }, { ...host, platform: 'linux' }); for (const env of [{ ESBUILD_BINARY_PATH: 'external' }, { GOWORK: 'go.work' }, { GOFLAGS: '-mod=mod' }]) { assert.throws(() => validateEnvironment(env, host)); } assert.throws(() => validateEnvironment({}, { ...host, platform: 'darwin' })); assert.throws(() => validateEnvironment({}, { ...host, arch: 'arm64' })); }); test('发布入口在创建目录前检查并安装依赖,构建后复核版本文件', () => { const script = readFileSync(join(root, 'build', 'build-release.ps1'), 'utf8'); const install = script.indexOf('--frontend $frontend --install'); assert.ok(install > 0 && install < script.indexOf('New-Item -ItemType Directory -Path $releaseDirectory')); assert.match(script, /--ignore-scripts run build/); assert.match(script, /go build -trimpath -mod=readonly/); assert.match(script, /build_environment = \$toolchain/); assert.match(script, /\$verifiedToolchain\.input_sha256/); }); test('工具链命令实际拒绝外部二进制覆盖', () => { const result = spawnSync(process.execPath, [join(root, 'build', 'toolchain.mjs'), '--install'], { cwd: root, encoding: 'utf8', windowsHide: true, env: { ...process.env, ESBUILD_BINARY_PATH: 'unreviewed-binary' }, }); assert.equal(result.status, 1); assert.match(result.stderr, /禁止使用锁文件之外的 esbuild/); assert.equal(result.stdout, ''); }); test('嵌套 npm 安装不继承项目安装禁止的用户脚本白名单', () => { const original = { npm_config_allow_scripts: 'fixture', NPM_CONFIG_ALLOW_SCRIPTS: 'fixture', PATH: 'retained' }; assert.deepEqual(npmEnvironment(original), { PATH: 'retained' }); assert.equal(original.npm_config_allow_scripts, 'fixture'); }); test('发布脚本实际在工具链拒绝后不创建产物', { skip: process.platform !== 'win32' }, (t) => { const directory = mkdtempSync(join(tmpdir(), 'scert-release-preflight-')); t.after(() => rmSync(directory, { recursive: true, force: true })); const output = join(directory, 'release'); const result = spawnSync('powershell.exe', [ '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', join(root, 'build', 'build-release.ps1'), '-OutputDirectory', output, ], { cwd: root, encoding: 'utf8', windowsHide: true, env: { ...process.env, ESBUILD_BINARY_PATH: 'unreviewed-binary' }, }); assert.equal(result.status, 1, `${result.error ?? ''}\n${result.stdout}\n${result.stderr}`); assert.match(result.stderr, /esbuild/); assert.ok(!existsSync(output)); }); test('真实离线安装禁止根项目及依赖脚本,命令行策略覆盖环境绕行', { timeout: 120000 }, (t) => { const directory = mkdtempSync(join(tmpdir(), 'scert-install-policy-')); t.after(() => rmSync(directory, { recursive: true, force: true })); const dependency = join(directory, 'dependency'); mkdirSync(dependency); const script = 'node -e "require(\'node:fs\').writeFileSync(\'lifecycle-ran\', \'yes\')"'; writeFileSync(join(dependency, 'package.json'), JSON.stringify({ name: 'scert-policy-fixture', version: '1.0.0', scripts: { postinstall: script }, })); writeFileSync(join(directory, 'package.json'), JSON.stringify({ name: 'scert-policy-test', version: '1.0.0', private: true, dependencies: { 'scert-policy-fixture': 'file:./dependency' }, scripts: { postinstall: script }, })); writeFileSync(join(directory, '.npmrc'), 'ignore-scripts=true\n'); const run = (args) => { const options = { cwd: directory, encoding: 'utf8', windowsHide: true, env: npmEnvironment() }; const result = process.platform === 'win32' ? spawnSync(`${npmCommand} ${args.join(' ')}`, { ...options, shell: true }) : spawnSync(npmCommand, args, options); assert.equal(result.status, 0, `${result.error ?? ''}\n${result.stdout}\n${result.stderr}`); }; run(['install', '--package-lock-only', '--ignore-scripts', '--offline', '--no-audit', '--no-fund']); const lock = readFileSync(join(directory, 'package-lock.json')); const envKeys = ['npm_config_ignore_scripts', 'npm_config_dangerously_allow_all_scripts', 'npm_config_offline']; const previous = envKeys.map((key) => process.env[key]); try { process.env.npm_config_ignore_scripts = 'false'; process.env.npm_config_dangerously_allow_all_scripts = 'true'; process.env.npm_config_offline = 'true'; installDependencies(directory); } finally { envKeys.forEach((key, i) => { if (previous[i] === undefined) delete process.env[key]; else process.env[key] = previous[i]; }); } assert.ok(existsSync(join(directory, 'node_modules', 'scert-policy-fixture', 'package.json'))); assert.ok(!existsSync(join(directory, 'lifecycle-ran'))); assert.ok(!existsSync(join(dependency, 'lifecycle-ran'))); assert.deepEqual(readFileSync(join(directory, 'package-lock.json')), lock); // 正向对照证明夹具确实会执行脚本,避免用未触发的脚本证明禁止策略。 run(['ci', '--ignore-scripts=false', '--dangerously-allow-all-scripts', '--offline', '--no-audit', '--no-fund']); assert.ok(existsSync(join(directory, 'lifecycle-ran'))); assert.ok(existsSync(join(dependency, 'lifecycle-ran'))); });