2d127698d1
- checks.ts:runChecks 按 lint→typecheck→build 逐项跑(worktree 内,各 10min, 任一非0=硬失败报哪项挂);parseChecks 解析 project.checks JSON - diffSizeGate:git diff --numstat 统计文件数+增删行数,超阈值=硬失败(提示拆解); 阈值 env 可配 MAESTRO_DIFF_MAX_FILES/LINES(默认 60/3000,0=不限);git 出错不拦截 - pipeline:verify 后插 checks+diff 闸,任一失败 emit failed 不进复审; conflict 管线只跑 checks(diff 含整条原分支,体量闸会误伤) - 注:diff「声明外文件」闸未做——task 无声明文件范围字段 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
97 lines
4.1 KiB
TypeScript
97 lines
4.1 KiB
TypeScript
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> | void): Promise<void> | 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);
|
||
});
|