feat: Phase1 核心——模型/状态机 + SQLite Store(守卫+审批闸) + REST/WS API + daemon
- 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>
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
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();
|
||||
});
|
||||
Reference in New Issue
Block a user