Files
maestro/test/exec-merge.test.ts
T
wangjia 1f318c2667 feat(agent): ② exec 自动放行(autoApproveExec)+ 抽出共享合并流程
- 新增 exec-merge.acceptAndMerge:封装 exec_review 通过并合并的完整流程
  (merge→冲突建补救任务/成功 decide+收口+回收),API decide 与 ingest 复用同一逻辑
- server.ts decide 改调 acceptAndMerge(去重 ~40 行内联逻辑)
- ingest result:auto-approved + autoApproveExec + 双复审 approve → fire-and-forget
  自动合并(git 异步不阻塞 tick);冲突则补救任务 + 留 exec_review 人审
- 测试:acceptAndMerge 可合并→done、冲突→补救+留审核闸(真实 git repo)

至此 plan ① ② ③ 三类优化 + 通用规则 + L1-L4 记忆注入代码侧全部落地

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 22:58:37 +08:00

92 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 { execFileSync } from 'node:child_process';
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { Store } from '../src/store/index.js';
import { acceptAndMerge } from '../src/executor/exec-merge.js';
import type { Task } from '../src/model/types.js';
const noopLog = { info() {}, error() {} };
function git(cwd: string, args: string[]): string {
return execFileSync('git', args, { cwd, encoding: 'utf8' });
}
/** 独立 dataDir + 真实 git repomain 含 1 commit),t.after 收尾。 */
function setup(t: { after: (fn: () => void) => void }): string {
const dataDir = mkdtempSync(join(tmpdir(), 'maestro-em-data-'));
const prev = process.env.MAESTRO_DATA_DIR;
process.env.MAESTRO_DATA_DIR = dataDir;
const repo = mkdtempSync(join(tmpdir(), 'maestro-em-repo-'));
git(repo, ['init', '-b', 'main']);
git(repo, ['config', 'user.name', 'em']); git(repo, ['config', 'user.email', 'em@t']);
writeFileSync(join(repo, 'README.md'), '# demo\n');
git(repo, ['add', '-A']); git(repo, ['commit', '-qm', 'init']);
t.after(() => {
if (prev === undefined) delete process.env.MAESTRO_DATA_DIR; else process.env.MAESTRO_DATA_DIR = prev;
rmSync(dataDir, { recursive: true, force: true });
rmSync(repo, { recursive: true, force: true });
});
return repo;
}
/** 在 store 里把任务推到 exec_review 且带 result(双 verdict=approve)。 */
function taskAtExecReview(s: Store, projectId: string, branch: string): Task {
const t = s.createTask({ projectId, title: 'feat', complexity: 'easy' });
s.setOperations(t.id, 'do');
s.transition(t.id, 'queued');
s.transition(t.id, 'executing');
s.setResult(t.id, {
branch, worktree: null, diffSummary: '', commits: [], prUrl: null,
summary: 'ok', verdict: 'approve', securitySummary: 'ok', securityVerdict: 'approve', mergeTaskId: null,
});
s.transition(t.id, 'exec_review');
return s.getTask(t.id)!;
}
test('acceptAndMerge:可合并 → 任务 done + result.prUrl=merged:<commit>', async (t) => {
const repo = setup(t);
const s = new Store(':memory:');
const p = s.createProject({ name: 'em', repoPath: repo });
const branch = 'maestro/tsk_ok';
git(repo, ['checkout', '-q', '-b', branch]);
writeFileSync(join(repo, 'feat.txt'), 'F\n');
git(repo, ['add', '-A']); git(repo, ['commit', '-qm', 'maestro(tsk_ok): feat']);
git(repo, ['switch', '-q', '--detach']); // 释放 main,让合并走临时 worktree
const task = taskAtExecReview(s, p.id, branch);
const outcome = await acceptAndMerge(s, noopLog, task, 'test');
assert.equal(outcome.ok, true, outcome.error);
assert.ok(outcome.mergeCommit);
const after = s.getTask(task.id)!;
assert.equal(after.status, 'done', '合并成功 → done');
assert.match(after.result!.prUrl!, /^merged:/);
s.close();
});
test('acceptAndMerge:冲突 → ok:false + 建补救任务 + 原任务留 exec_review', async (t) => {
const repo = setup(t);
const s = new Store(':memory:');
const p = s.createProject({ name: 'em', repoPath: repo });
// main 与 branch 改同一文件同一行 → 冲突
writeFileSync(join(repo, 'shared.txt'), 'base\n');
git(repo, ['add', '-A']); git(repo, ['commit', '-qm', 'base shared']);
const branch = 'maestro/tsk_conf';
git(repo, ['checkout', '-q', '-b', branch]);
writeFileSync(join(repo, 'shared.txt'), 'BRANCH\n');
git(repo, ['add', '-A']); git(repo, ['commit', '-qm', 'maestro(tsk_conf): branch side']);
git(repo, ['checkout', '-q', 'main']);
writeFileSync(join(repo, 'shared.txt'), 'MAIN\n');
git(repo, ['add', '-A']); git(repo, ['commit', '-qm', 'main side']);
git(repo, ['switch', '-q', '--detach']);
const task = taskAtExecReview(s, p.id, branch);
const outcome = await acceptAndMerge(s, noopLog, task, 'test');
assert.equal(outcome.ok, false, '冲突应失败');
assert.ok(outcome.remediationTaskId, '应建补救任务');
assert.equal(s.getTask(task.id)!.status, 'exec_review', '原任务留审核闸(不 decide');
s.close();
});