3172718a94
- src/model: 复杂度分级、16 状态状态机、实体类型 - src/store: better-sqlite3 接 schema,transition 受 canTransition 守卫, decide 审批闸(reject 必带改进意见),事件订阅广播,nextExecutable - src/api: Fastify REST + ws 事件广播(/ws) - src/daemon: maestrod 入口(env 配置,默认 ~/.maestro :4517) - test: 9 个生命周期单测全过;typecheck/build 干净;REST+WS 端到端实跑验证 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
129 lines
5.5 KiB
TypeScript
129 lines
5.5 KiB
TypeScript
import { test } from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { Store, StoreError } from '../src/store/index.js';
|
||
|
||
function freshStore(): Store {
|
||
return new Store(':memory:');
|
||
}
|
||
|
||
test('project + task creation, complexity routes initial status', () => {
|
||
const s = freshStore();
|
||
const p = s.createProject({ name: 'demo', repoPath: '/tmp/demo-' + Math.random() });
|
||
assert.equal(p.status, 'active');
|
||
|
||
const hard = s.createTask({ projectId: p.id, title: 'big', complexity: 'hard' });
|
||
const medium = s.createTask({ projectId: p.id, title: 'mid', complexity: 'medium' });
|
||
const easy = s.createTask({ projectId: p.id, title: 'small', complexity: 'easy' });
|
||
assert.equal(hard.status, 'analyzing'); // Hard 强制分析
|
||
assert.equal(medium.status, 'speccing'); // Medium 写方案
|
||
assert.equal(easy.status, 'ready'); // Easy 直接可执行
|
||
s.close();
|
||
});
|
||
|
||
test('Easy 路径:写操作 → ready → nextExecutable 领取', () => {
|
||
const s = freshStore();
|
||
const p = s.createProject({ name: 'e', repoPath: '/tmp/e-' + Math.random() });
|
||
const t = s.createTask({ projectId: p.id, title: 'tweak', complexity: 'easy' });
|
||
s.setOperations(t.id, '改 README 一行');
|
||
const next = s.nextExecutable(p.id);
|
||
assert.equal(next?.id, t.id);
|
||
s.close();
|
||
});
|
||
|
||
test('Medium 闸:spec_review accept → ready;reject 必带意见', () => {
|
||
const s = freshStore();
|
||
const p = s.createProject({ name: 'm', repoPath: '/tmp/m-' + Math.random() });
|
||
const t = s.createTask({ projectId: p.id, title: 'feat', complexity: 'medium' });
|
||
s.setSpec(t.id, '加一个接口,因为前端需要');
|
||
const inReview = s.transition(t.id, 'spec_review');
|
||
assert.equal(inReview.status, 'spec_review');
|
||
|
||
// reject 无意见 → 报错
|
||
assert.throws(() => s.decide(t.id, 'reject', 'user'), StoreError);
|
||
// reject 带意见 → 回到 speccing
|
||
const back = s.decide(t.id, 'reject', 'user', '方案漏了鉴权');
|
||
assert.equal(back.status, 'speccing');
|
||
assert.equal(back.approvals.at(-1)?.reason, '方案漏了鉴权');
|
||
|
||
// 重新评审 → accept → ready
|
||
s.transition(t.id, 'spec_review');
|
||
const ok = s.decide(t.id, 'accept', 'user');
|
||
assert.equal(ok.status, 'ready');
|
||
s.close();
|
||
});
|
||
|
||
test('Hard 闸:plan_review accept → decomposed,子任务汇总', () => {
|
||
const s = freshStore();
|
||
const p = s.createProject({ name: 'h', repoPath: '/tmp/h-' + Math.random() });
|
||
const t = s.createTask({ projectId: p.id, title: 'epic', complexity: 'hard' });
|
||
s.setPlan(t.id, '分析…拆 2 个子任务');
|
||
s.transition(t.id, 'plan_review');
|
||
const decomposed = s.decide(t.id, 'accept', 'user');
|
||
assert.equal(decomposed.status, 'decomposed');
|
||
|
||
const c1 = s.createTask({ projectId: p.id, parentId: t.id, title: '子1', complexity: 'easy' });
|
||
assert.equal(c1.depth, 2);
|
||
// 拆解后容器不应被 nextExecutable 领取(它有子任务且非 ready)
|
||
s.setOperations(c1.id, '做子1');
|
||
const next = s.nextExecutable(p.id);
|
||
assert.equal(next?.id, c1.id); // 取到叶子,不是容器
|
||
s.close();
|
||
});
|
||
|
||
test('层级上限:medium 默认 3 层,第 4 层拒绝', () => {
|
||
const s = freshStore();
|
||
const p = s.createProject({ name: 'd', repoPath: '/tmp/d-' + Math.random() });
|
||
const l1 = s.createTask({ projectId: p.id, title: 'l1', complexity: 'medium' });
|
||
const l2 = s.createTask({ projectId: p.id, parentId: l1.id, title: 'l2', complexity: 'medium' });
|
||
const l3 = s.createTask({ projectId: p.id, parentId: l2.id, title: 'l3', complexity: 'medium' });
|
||
assert.equal(l3.depth, 3);
|
||
assert.throws(() => s.createTask({ projectId: p.id, parentId: l3.id, title: 'l4', complexity: 'medium' }), StoreError);
|
||
s.close();
|
||
});
|
||
|
||
test('非法状态流转被守卫拒绝', () => {
|
||
const s = freshStore();
|
||
const p = s.createProject({ name: 'g', repoPath: '/tmp/g-' + Math.random() });
|
||
const t = s.createTask({ projectId: p.id, title: 'x', complexity: 'easy' }); // ready
|
||
assert.throws(() => s.transition(t.id, 'done'), StoreError); // ready→done 非法
|
||
s.close();
|
||
});
|
||
|
||
test('exec_review 结果闸:accept → done', () => {
|
||
const s = freshStore();
|
||
const p = s.createProject({ name: 'x', repoPath: '/tmp/x-' + Math.random() });
|
||
const t = s.createTask({ projectId: p.id, title: 'run', complexity: 'easy' });
|
||
s.setOperations(t.id, 'op');
|
||
s.transition(t.id, 'queued');
|
||
s.transition(t.id, 'executing');
|
||
s.setResult(t.id, { branch: 'maestro/run', worktree: '/wt', diffSummary: '+1 -0', commits: ['abc'], prUrl: null });
|
||
s.transition(t.id, 'exec_review');
|
||
const done = s.decide(t.id, 'accept', 'user');
|
||
assert.equal(done.status, 'done');
|
||
s.close();
|
||
});
|
||
|
||
test('依赖未满足时不可领取', () => {
|
||
const s = freshStore();
|
||
const p = s.createProject({ name: 'dep', repoPath: '/tmp/dep-' + Math.random() });
|
||
const a = s.createTask({ projectId: p.id, title: 'A', complexity: 'easy' });
|
||
const b = s.createTask({ projectId: p.id, title: 'B', complexity: 'easy', deps: [a.id] });
|
||
s.setOperations(a.id, 'a'); s.setOperations(b.id, 'b');
|
||
// 优先级相同时 A 先建,但 B 依赖 A 未 done → 只能取到 A
|
||
const next = s.nextExecutable(p.id);
|
||
assert.equal(next?.id, a.id);
|
||
s.close();
|
||
});
|
||
|
||
test('事件订阅:状态变更广播', () => {
|
||
const s = freshStore();
|
||
const got: string[] = [];
|
||
s.subscribe((e) => got.push(e.type));
|
||
const p = s.createProject({ name: 'ev', repoPath: '/tmp/ev-' + Math.random() });
|
||
const t = s.createTask({ projectId: p.id, title: 't', complexity: 'easy' });
|
||
s.transition(t.id, 'queued');
|
||
assert.ok(got.includes('task.created'));
|
||
assert.ok(got.includes('status.changed'));
|
||
s.close();
|
||
});
|