Files
maestro/test/checks.test.ts
T
wangjia 2d127698d1 feat(agent): ② approve 前硬闸——分项检查(lint/typecheck/build) + diff 体量闸
- 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>
2026-06-24 22:53:56 +08:00

97 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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('diffSizeGategit 出错 → 不拦截(测不出体量)', async () => {
const r = await diffSizeGate('/nonexistent-repo-xyz', '/tmp', 'a', 'b');
assert.equal(r.ok, true);
});