Files
maestro/test/store.test.ts
T
wangjia 3172718a94 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>
2026-06-12 20:44:45 +08:00

129 lines
5.5 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 { 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 → readyreject 必带意见', () => {
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();
});