feat: PR 审核流程 + 自动合并 + macOS 通知 + 订阅额度透传 + 归档详情
PR 流程(开发→提PR→CodeReview→安全审计→审核→通过即合并): - reviewer 拆分为 code review 与安全审计两个独立 CC run(kind=reviewer/security), TaskResult 四字段(summary/verdict + securitySummary/securityVerdict),闸上两节报告两枚结论徽章 - executor/merge.ts: 通过即合并——临时 worktree 内 merge --no-ff,绝不碰用户工作区/不 push; 冲突安全拒绝(报冲突文件);重复合并幂等;合并后回收执行 worktree + 删分支 - decide 路由 merge:false 逃生口 + 看板「仅通过」按钮(目标分支被工作区检出时用) 通知(daemon/notify.ts): - macOS 原生通知: 进审核闸(复审建议拒绝标⚠)/连续失败需人工/合并完成 - 同任务同类型 60s 抑制、osascript 转义截断、MAESTRO_NOTIFY=0 关闭 订阅额度透传(daemon/usage.ts): - OAuth usage API(与 Claude Code/claude-hud 同源),凭证 keychain→内存零泄漏 - 60s 成败双缓存+并发去重+5s 超时,失败降级 null - GET /api/agents 顶层 usage 字段;Agent 面板显示 5h/周用量条+重置倒计时(>80%琥珀/>95%红) 看板与生命周期: - 归档区: 深度1整树完成沉底,时间倒序分页(尺寸 chip 10/20/50/100 置底) - 归档详情对话框: 全属性/执行历史与时长/审批记录/状态流转时间线(GET /api/tasks/:id/events) - 容器收口: 已拆解 Hard 子任务全 done 自动 done(afterDone 逐级向上) - 同步按钮收进配置面板;执行白名单扩测试命令(npm/go/shellcheck/make/pytest) - Agent 面板显示调度模式与各复杂度模型;被依赖阻塞→被阻塞 测试: 74/74(新增 merge 6/notify 10/usage 7/容器收口/双复审适配) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+62
-11
@@ -23,6 +23,13 @@ const okReview: ReviewResult = {
|
||||
sessionId: 'sess-review-1',
|
||||
};
|
||||
|
||||
const okSecurity: ReviewResult = {
|
||||
summary: '## 安全审计\nmock 审计通过',
|
||||
verdict: 'approve',
|
||||
transcriptRef: '/tmp/fake-security.jsonl',
|
||||
sessionId: 'sess-security-1',
|
||||
};
|
||||
|
||||
/** 全 mock 依赖(不真起 CC、不动 git):可按用例覆盖 */
|
||||
function mockDeps(overrides: Partial<OrchestratorDeps> = {}): OrchestratorDeps {
|
||||
return {
|
||||
@@ -30,7 +37,8 @@ function mockDeps(overrides: Partial<OrchestratorDeps> = {}): OrchestratorDeps {
|
||||
worktreeDiff: async () => ({ diffSummary: ' README.md | 1 +', commits: ['abc1234 hello maestro'] }),
|
||||
verify: async () => ({ ok: true, exitCode: 0, logRef: null }),
|
||||
runner: async () => okRun,
|
||||
reviewer: async () => okReview,
|
||||
reviewCode: async () => okReview,
|
||||
reviewSecurity: async () => okSecurity,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
@@ -140,7 +148,7 @@ test('concurrency=1:同项目同轮只领 1 个,跑完下一轮再领', asyn
|
||||
store.close();
|
||||
});
|
||||
|
||||
test('成功路径:状态流转 + setResult(含复审 summary/verdict) + 双 run succeeded', async () => {
|
||||
test('成功路径:状态流转 + setResult(四字段) + executor/reviewer/security 三 run succeeded', async () => {
|
||||
const { store, projectId } = setup('auto-easy');
|
||||
const t = store.createTask({ projectId, title: 'tweak', complexity: 'easy' });
|
||||
store.setOperations(t.id, '在 README.md 追加一行');
|
||||
@@ -148,12 +156,17 @@ test('成功路径:状态流转 + setResult(含复审 summary/verdict) + 双 r
|
||||
const seen: string[] = [];
|
||||
store.subscribe((e) => { if (e.type === 'status.changed' && e.taskId === t.id) seen.push(String(e.payload.to)); });
|
||||
|
||||
let reportSeen: string | null = null;
|
||||
let codeReportSeen: string | null = null;
|
||||
let secReportSeen: string | null = null;
|
||||
const orch = createOrchestrator(store, noopLog, mockDeps({
|
||||
reviewer: async (_task, _project, _wt, _runId, executorReport) => {
|
||||
reportSeen = executorReport;
|
||||
reviewCode: async (_task, _project, _wt, _runId, executorReport) => {
|
||||
codeReportSeen = executorReport;
|
||||
return okReview;
|
||||
},
|
||||
reviewSecurity: async (_task, _project, _wt, _runId, executorReport) => {
|
||||
secReportSeen = executorReport;
|
||||
return okSecurity;
|
||||
},
|
||||
}));
|
||||
orch.tick();
|
||||
await orch.drain();
|
||||
@@ -169,11 +182,14 @@ test('成功路径:状态流转 + setResult(含复审 summary/verdict) + 双 r
|
||||
prUrl: null,
|
||||
summary: '## 做了什么\nmock 复审通过',
|
||||
verdict: 'approve',
|
||||
securitySummary: '## 安全审计\nmock 审计通过',
|
||||
securityVerdict: 'approve',
|
||||
});
|
||||
assert.equal(reportSeen, '执行自述:改了 README'); // runner finalText 传给 reviewer 作执行者自述
|
||||
assert.equal(codeReportSeen, '执行自述:改了 README'); // runner finalText 传给两个复审作执行者自述
|
||||
assert.equal(secReportSeen, '执行自述:改了 README');
|
||||
|
||||
const runs = store.listRuns(t.id);
|
||||
assert.equal(runs.length, 2);
|
||||
assert.equal(runs.length, 3);
|
||||
const executor = runs.find((r) => r.kind === 'executor')!;
|
||||
assert.equal(executor.status, 'succeeded');
|
||||
assert.equal(executor.branch, `maestro/${t.id}`);
|
||||
@@ -183,14 +199,19 @@ test('成功路径:状态流转 + setResult(含复审 summary/verdict) + 双 r
|
||||
assert.equal(reviewer.status, 'succeeded');
|
||||
assert.equal(reviewer.transcriptRef, '/tmp/fake-review.jsonl');
|
||||
assert.equal(reviewer.claudeSessionId, 'sess-review-1');
|
||||
const security = runs.find((r) => r.kind === 'security')!;
|
||||
assert.equal(security.status, 'succeeded');
|
||||
assert.equal(security.transcriptRef, '/tmp/fake-security.jsonl');
|
||||
assert.equal(security.claudeSessionId, 'sess-security-1');
|
||||
store.close();
|
||||
});
|
||||
|
||||
test('reviewer verdict=reject 也照常落进 result(最终裁决仍归用户)', async () => {
|
||||
test('任一 verdict=reject 也照常落进 result(最终裁决仍归用户)', async () => {
|
||||
const { store, projectId } = setup('auto-easy');
|
||||
const t = store.createTask({ projectId, title: 'risky', complexity: 'easy' });
|
||||
const orch = createOrchestrator(store, noopLog, mockDeps({
|
||||
reviewer: async () => ({ ...okReview, summary: '发现问题', verdict: 'reject' as const }),
|
||||
reviewCode: async () => ({ ...okReview, summary: '发现问题', verdict: 'reject' as const }),
|
||||
reviewSecurity: async () => ({ ...okSecurity, summary: '发现密钥泄露', verdict: 'reject' as const }),
|
||||
}));
|
||||
orch.tick();
|
||||
await orch.drain();
|
||||
@@ -198,14 +219,16 @@ test('reviewer verdict=reject 也照常落进 result(最终裁决仍归用户
|
||||
assert.equal(done.status, 'exec_review');
|
||||
assert.equal(done.result!.verdict, 'reject');
|
||||
assert.equal(done.result!.summary, '发现问题');
|
||||
assert.equal(done.result!.securityVerdict, 'reject');
|
||||
assert.equal(done.result!.securitySummary, '发现密钥泄露');
|
||||
store.close();
|
||||
});
|
||||
|
||||
test('复审失败不挡任务:照常进 exec_review,summary 记失败原因、verdict=null', async () => {
|
||||
test('code review 失败不挡任务:summary 记失败原因、verdict=null,安全审计照常跑', async () => {
|
||||
const { store, projectId } = setup('auto-easy');
|
||||
const t = store.createTask({ projectId, title: 'review-broken', complexity: 'easy' });
|
||||
const orch = createOrchestrator(store, noopLog, mockDeps({
|
||||
reviewer: async () => { throw new Error('复审 CC 崩了'); },
|
||||
reviewCode: async () => { throw new Error('复审 CC 崩了'); },
|
||||
}));
|
||||
orch.tick();
|
||||
await orch.drain();
|
||||
@@ -214,12 +237,40 @@ test('复审失败不挡任务:照常进 exec_review,summary 记失败原因
|
||||
assert.equal(done.status, 'exec_review'); // 不挡结果闸
|
||||
assert.equal(done.result!.verdict, null);
|
||||
assert.equal(done.result!.summary, '自动复审失败:复审 CC 崩了');
|
||||
assert.equal(done.result!.securityVerdict, 'approve'); // 另一个复审不受影响
|
||||
assert.equal(done.result!.securitySummary, '## 安全审计\nmock 审计通过');
|
||||
|
||||
const runs = store.listRuns(t.id);
|
||||
assert.equal(runs.find((r) => r.kind === 'executor')!.status, 'succeeded');
|
||||
const reviewer = runs.find((r) => r.kind === 'reviewer')!;
|
||||
assert.equal(reviewer.status, 'failed');
|
||||
assert.match(reviewer.error ?? '', /复审 CC 崩了/);
|
||||
assert.equal(runs.find((r) => r.kind === 'security')!.status, 'succeeded');
|
||||
store.close();
|
||||
});
|
||||
|
||||
test('安全审计失败不挡任务:securitySummary 记失败原因、securityVerdict=null,code review 不受影响', async () => {
|
||||
const { store, projectId } = setup('auto-easy');
|
||||
const t = store.createTask({ projectId, title: 'security-broken', complexity: 'easy' });
|
||||
const orch = createOrchestrator(store, noopLog, mockDeps({
|
||||
reviewSecurity: async () => { throw new Error('审计 CC 崩了'); },
|
||||
}));
|
||||
orch.tick();
|
||||
await orch.drain();
|
||||
|
||||
const done = store.getTask(t.id)!;
|
||||
assert.equal(done.status, 'exec_review');
|
||||
assert.equal(done.result!.verdict, 'approve');
|
||||
assert.equal(done.result!.summary, '## 做了什么\nmock 复审通过');
|
||||
assert.equal(done.result!.securityVerdict, null);
|
||||
assert.equal(done.result!.securitySummary, '自动复审失败:审计 CC 崩了');
|
||||
|
||||
const runs = store.listRuns(t.id);
|
||||
assert.equal(runs.find((r) => r.kind === 'executor')!.status, 'succeeded');
|
||||
assert.equal(runs.find((r) => r.kind === 'reviewer')!.status, 'succeeded');
|
||||
const security = runs.find((r) => r.kind === 'security')!;
|
||||
assert.equal(security.status, 'failed');
|
||||
assert.match(security.error ?? '', /审计 CC 崩了/);
|
||||
store.close();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user