feat: Phase2 完整管线——score 调度 + 自动复审 + 模型分级 + 归档与详情
调度: - model/scoring.ts: score = 自身分(P0=3/P1=2/P2=1) + 已完成依赖分(链条惯性) + 等待解锁的 blocked 任务分(解锁加权),编排器与 nextExecutable 同一打分 - createTask 校验 deps 存在且同项目(依赖图天然无环) - daemon 重启中断自愈: executing 任务标 failed run 后重新入队(reconcileInterrupted) 执行管线: - executor/cc.ts: 公共 headless CC 执行器(转录/超时/模型回退重试) - executor/reviewer.ts: 执行后自动复审(只读 CC 审 diff),固定模板 summary (做了什么/怎么做/测试/CodeReview/安全Review/结论) + VERDICT 解析 - executor/models.ts: 按复杂度选模型(easy→sonnet/medium→opus/hard→fable5), env 可覆盖、project.model 最优先、不可用自动回退链 - runner: 测试/构建命令白名单(npm/go/shellcheck/make/pytest),prompt 要求实跑测试 - TaskResult 加 summary/verdict; RunKind 加 reviewer - 容器收口: 已拆解 Hard 子任务全 done → 容器自动 done(afterDone 逐级向上) 看板: - 五徽章组(待审批/待执行/执行中/被阻塞/总量,hover 展开,均不含已完成) - 归档区: 深度1整树完成沉底,时间倒序分页(10/20/50/100 chip 选择) - 归档详情对话框: 全属性/执行历史与时长/审批记录/状态流转时间线(含相关人或事) - Agent 面板显示调度模式 + 各复杂度实际模型 - 结果闸展示复审 summary + 建议通过/拒绝徽章 - 筛选修复(组选与单选分离、已拆解移出进行中)、同步按钮收进配置面板、 保存配置自动收起、预览全宽、被依赖阻塞→被阻塞 - API: GET /api/tasks/:id/events(任务级事件时间线)、/api/agents 带 scheduling/models 测试: 49/49(新增 scoring/复审/模型/容器收口/deps 校验/中断恢复) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { createWorktree, worktreeDiff, removeWorktree, worktreeDirFor, branchFor } from '../src/executor/worktree.js';
|
||||
|
||||
function gitSync(cwd: string, args: string[]): string {
|
||||
return execFileSync('git', args, { cwd, encoding: 'utf8' });
|
||||
}
|
||||
|
||||
/** /tmp 下建一个带 1 个 commit 的真实 git repo(main 分支) */
|
||||
function makeRepo(): string {
|
||||
const repo = mkdtempSync(join(tmpdir(), 'maestro-wt-repo-'));
|
||||
gitSync(repo, ['init', '-b', 'main']);
|
||||
gitSync(repo, ['config', 'user.name', 'maestro-test']);
|
||||
gitSync(repo, ['config', 'user.email', 'test@maestro.local']);
|
||||
writeFileSync(join(repo, 'README.md'), '# demo\n');
|
||||
gitSync(repo, ['add', '-A']);
|
||||
gitSync(repo, ['commit', '-m', 'init']);
|
||||
return repo;
|
||||
}
|
||||
|
||||
test('worktree:create / diff / remove 在真实 git repo 上工作', async (t) => {
|
||||
// worktree 根目录隔离到临时 MAESTRO_DATA_DIR,不碰 ~/.maestro
|
||||
const dataDir = mkdtempSync(join(tmpdir(), 'maestro-wt-data-'));
|
||||
const prevDataDir = process.env.MAESTRO_DATA_DIR;
|
||||
process.env.MAESTRO_DATA_DIR = dataDir;
|
||||
const repo = makeRepo();
|
||||
t.after(() => {
|
||||
if (prevDataDir === undefined) delete process.env.MAESTRO_DATA_DIR;
|
||||
else process.env.MAESTRO_DATA_DIR = prevDataDir;
|
||||
rmSync(dataDir, { recursive: true, force: true });
|
||||
rmSync(repo, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
const taskId = 'tsk_test123';
|
||||
|
||||
// 1) 创建:目录与分支符合约定
|
||||
const wt = await createWorktree(repo, taskId, 'main');
|
||||
assert.equal(wt.branch, branchFor(taskId));
|
||||
assert.equal(wt.dir, worktreeDirFor(repo, taskId));
|
||||
assert.ok(existsSync(join(wt.dir, 'README.md')), 'worktree 应检出 README.md');
|
||||
const head = gitSync(wt.dir, ['rev-parse', '--abbrev-ref', 'HEAD']).trim();
|
||||
assert.equal(head, wt.branch);
|
||||
|
||||
// 2) 空分支 diff:无 commit、无摘要
|
||||
const empty = await worktreeDiff(repo, wt.dir, wt.branch, 'main');
|
||||
assert.equal(empty.commits.length, 0);
|
||||
assert.equal(empty.diffSummary, '');
|
||||
|
||||
// 3) 在 worktree 提交一笔改动 → diff 能看到 commit 与 --stat 摘要
|
||||
writeFileSync(join(wt.dir, 'README.md'), '# demo\nhello maestro\n');
|
||||
gitSync(wt.dir, ['add', '-A']);
|
||||
gitSync(wt.dir, ['commit', '-m', `maestro(${taskId}): tweak readme`]);
|
||||
const diff = await worktreeDiff(repo, wt.dir, wt.branch, 'main');
|
||||
assert.equal(diff.commits.length, 1);
|
||||
assert.ok(diff.commits[0].includes(taskId), `commit 行应含任务 id:${diff.commits[0]}`);
|
||||
assert.ok(diff.diffSummary.includes('README.md'), `diffSummary 应含文件名:${diff.diffSummary}`);
|
||||
|
||||
// 4) 已存在同名 worktree/分支时重建(重试场景):从 main 干净重来
|
||||
const wt2 = await createWorktree(repo, taskId, 'main');
|
||||
assert.equal(wt2.dir, wt.dir);
|
||||
const diff2 = await worktreeDiff(repo, wt2.dir, wt2.branch, 'main');
|
||||
assert.equal(diff2.commits.length, 0, '重建后分支应回到 baseBranch,无旧 commit');
|
||||
|
||||
// 5) remove:目录与注册项移除,主 repo 不受影响
|
||||
await removeWorktree(repo, wt2.dir);
|
||||
assert.ok(!existsSync(wt2.dir), 'worktree 目录应被移除');
|
||||
const list = gitSync(repo, ['worktree', 'list']);
|
||||
assert.ok(!list.includes(taskId), 'git worktree list 不应再含该任务');
|
||||
assert.ok(existsSync(join(repo, 'README.md')), '主 repo 不受影响');
|
||||
});
|
||||
|
||||
test('worktree:错误透传(不存在的 baseBranch)', async (t) => {
|
||||
const dataDir = mkdtempSync(join(tmpdir(), 'maestro-wt-data2-'));
|
||||
const prevDataDir = process.env.MAESTRO_DATA_DIR;
|
||||
process.env.MAESTRO_DATA_DIR = dataDir;
|
||||
const repo = makeRepo();
|
||||
t.after(() => {
|
||||
if (prevDataDir === undefined) delete process.env.MAESTRO_DATA_DIR;
|
||||
else process.env.MAESTRO_DATA_DIR = prevDataDir;
|
||||
rmSync(dataDir, { recursive: true, force: true });
|
||||
rmSync(repo, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() => createWorktree(repo, 'tsk_badbase', 'no-such-branch'),
|
||||
/git worktree add/,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user