import { test } from 'node:test'; import assert from 'node:assert/strict'; import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { execFileSync } from 'node:child_process'; import { parseChecks, runChecks, diffSizeGate } from '../src/executor/checks.js'; import type { Project } from '../src/model/types.js'; function withTmpData(cb: (dir: string) => Promise | void): Promise | void { const dir = mkdtempSync(join(tmpdir(), 'maestro-checks-')); const prev = process.env.MAESTRO_DATA_DIR; process.env.MAESTRO_DATA_DIR = dir; const done = () => { if (prev === undefined) delete process.env.MAESTRO_DATA_DIR; else process.env.MAESTRO_DATA_DIR = prev; rmSync(dir, { recursive: true, force: true }); }; try { const r = cb(dir); if (r instanceof Promise) return r.finally(done); done(); } catch (e) { done(); throw e; } } function proj(checks: string | null): Project { return { checks, repoPath: '/tmp/x', defaultBranch: 'main' } as Project; } test('parseChecks:合法 JSON 取 lint/typecheck/build,非法/空 → {}', () => { assert.deepEqual(parseChecks('{"lint":"npm run lint","build":"npm run build"}'), { lint: 'npm run lint', build: 'npm run build' }); assert.deepEqual(parseChecks('{"other":"x","typecheck":" tsc "}'), { typecheck: 'tsc' }); assert.deepEqual(parseChecks(null), {}); assert.deepEqual(parseChecks('not json'), {}); assert.deepEqual(parseChecks('{"lint":""}'), {}); // 空命令跳过 }); test('runChecks:无 checks → 通过', async () => { await withTmpData(async (dir) => { const r = await runChecks(proj(null), dir, 'run_a'); assert.equal(r.ok, true); assert.equal(r.logRef, null); }); }); test('runChecks:各项 exit0 → 通过;任一非0 → 失败并报哪项挂', async () => { await withTmpData(async (dir) => { const pass = await runChecks(proj('{"lint":"true","typecheck":"true"}'), dir, 'run_b'); assert.equal(pass.ok, true); assert.equal(pass.failed, null); const fail = await runChecks(proj('{"lint":"true","typecheck":"false","build":"true"}'), dir, 'run_c'); assert.equal(fail.ok, false); assert.equal(fail.failed, 'typecheck'); assert.match(fail.error!, /typecheck 检查失败/); }); }); test('diffSizeGate:超文件/行阈值 → 拦截;阈值内 → 通过', async () => { const repo = mkdtempSync(join(tmpdir(), 'maestro-gitdiff-')); const g = (args: string[]) => execFileSync('git', args, { cwd: repo }).toString(); g(['init', '-q', '-b', 'main']); g(['config', 'user.email', 't@t']); g(['config', 'user.name', 't']); writeFileSync(join(repo, 'base.txt'), 'base\n'); g(['add', '-A']); g(['commit', '-qm', 'base']); g(['checkout', '-q', '-b', 'feature']); // 改动:3 个文件、若干行 for (const f of ['a.txt', 'b.txt', 'c.txt']) writeFileSync(join(repo, f), 'x\ny\nz\n'); g(['add', '-A']); g(['commit', '-qm', 'feat']); const prevF = process.env.MAESTRO_DIFF_MAX_FILES; const prevL = process.env.MAESTRO_DIFF_MAX_LINES; try { process.env.MAESTRO_DIFF_MAX_FILES = '2'; // 3 文件 > 2 → 拦截 process.env.MAESTRO_DIFF_MAX_LINES = '0'; const over = await diffSizeGate(repo, repo, 'feature', 'main'); assert.equal(over.ok, false); assert.equal(over.files, 3); assert.match(over.error!, /文件数 3 > 2/); process.env.MAESTRO_DIFF_MAX_FILES = '0'; // 0=不限 process.env.MAESTRO_DIFF_MAX_LINES = '0'; const under = await diffSizeGate(repo, repo, 'feature', 'main'); assert.equal(under.ok, true); assert.equal(under.files, 3); } finally { if (prevF === undefined) delete process.env.MAESTRO_DIFF_MAX_FILES; else process.env.MAESTRO_DIFF_MAX_FILES = prevF; if (prevL === undefined) delete process.env.MAESTRO_DIFF_MAX_LINES; else process.env.MAESTRO_DIFF_MAX_LINES = prevL; rmSync(repo, { recursive: true, force: true }); } }); test('diffSizeGate:git 出错 → 不拦截(测不出体量)', async () => { const r = await diffSizeGate('/nonexistent-repo-xyz', '/tmp', 'a', 'b'); assert.equal(r.ok, true); });