Files
maestro/test/reviewer.test.ts
T
wangjia 740d2c2637 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>
2026-06-13 03:22:30 +08:00

77 lines
3.9 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 { parseVerdict, buildReviewPrompt } from '../src/executor/reviewer.js';
import type { Project, Task } from '../src/model/types.js';
test('parseVerdictapprove', () => {
const { summary, verdict } = parseVerdict('## 做了什么\n改了 README\n\n## 结论\n建议通过\nVERDICT: approve');
assert.equal(verdict, 'approve');
assert.equal(summary, '## 做了什么\n改了 README\n\n## 结论\n建议通过'); // VERDICT 行已剔除
});
test('parseVerdictreject(大小写/前后空白宽容)', () => {
const { summary, verdict } = parseVerdict('问题很多\n verdict: REJECT \n');
assert.equal(verdict, 'reject');
assert.equal(summary, '问题很多');
});
test('parseVerdict:解析不到 = nullsummary 原样保留', () => {
const { summary, verdict } = parseVerdict('## 结论\n建议通过,但忘了写 VERDICT 行');
assert.equal(verdict, null);
assert.equal(summary, '## 结论\n建议通过,但忘了写 VERDICT 行');
});
test('parseVerdict:多个 VERDICT 行取最后一个,全部从 summary 剔除', () => {
const { summary, verdict } = parseVerdict('VERDICT: approve\n复查后改判\nVERDICT: reject');
assert.equal(verdict, 'reject');
assert.equal(summary, '复查后改判');
});
test('parseVerdictVERDICT 行带多余内容不算(如模板原文 approve|reject', () => {
const { verdict } = parseVerdict('VERDICT: approve|reject');
assert.equal(verdict, null);
});
test('buildReviewPrompt(code):含任务说明、执行者自述、diff 指令与 code review 模板', () => {
const task = { id: 'task_1', title: '修复登录', operations: '改 auth.ts', spec: null, plan: null } as Task;
const project = { defaultBranch: 'main' } as Project;
const wt = { dir: '/tmp/wt', branch: 'maestro/task_1' };
const prompt = buildReviewPrompt('code', task, project, wt, '我改了 auth.ts 并加了测试');
assert.match(prompt, /修复登录/);
assert.match(prompt, /改 auth\.ts/);
assert.match(prompt, /我改了 auth\.ts 并加了测试/);
assert.match(prompt, /git diff main\.\.\.maestro\/task_1/);
for (const section of ['## 做了什么', '## 怎么做的', '## 测试情况', '## Code Review', '## 结论']) {
assert.ok(prompt.includes(section), `模板缺少 ${section}`);
}
assert.ok(!prompt.includes('## 安全审计'), 'code review 模板不应含安全审计节');
assert.match(prompt, /VERDICT: approve\|reject/);
assert.match(prompt, /只读权限/);
});
test('buildReviewPrompt(security):安全审计模板(凭证/注入/权限/文案红线/部署风险)', () => {
const task = { id: 'task_1', title: '修复登录', operations: '改 auth.ts', spec: null, plan: null } as Task;
const project = { defaultBranch: 'main' } as Project;
const wt = { dir: '/tmp/wt', branch: 'maestro/task_1' };
const prompt = buildReviewPrompt('security', task, project, wt, '我改了 auth.ts');
assert.match(prompt, /安全审计/);
for (const section of ['凭证与密钥泄露', '注入与危险命令', '权限与边界', 'UI 文案红线词', '生产部署风险', '## 结论']) {
assert.ok(prompt.includes(section), `安全模板缺少 ${section}`);
}
assert.ok(!prompt.includes('## 做了什么'), '安全审计模板不应含 code review 节');
assert.match(prompt, /git diff main\.\.\.maestro\/task_1/);
assert.match(prompt, /VERDICT: approve\|reject/);
assert.match(prompt, /只读权限/);
});
test('buildReviewPrompt:执行者无自述时给占位说明(两种角色)', () => {
const task = { id: 'task_2', title: 't', operations: null, spec: null, plan: null } as Task;
const project = { defaultBranch: 'main' } as Project;
for (const role of ['code', 'security'] as const) {
const prompt = buildReviewPrompt(role, task, project, { dir: '/x', branch: 'maestro/task_2' }, '');
assert.match(prompt, /执行者未留下自述/);
}
});