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>
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
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 repo(main 含 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();
|
||||
});
|
||||
Reference in New Issue
Block a user