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:'); } // ---------- patchProject ---------- test('patchProject:更新 autonomy/concurrency/verifyCmd/model/status', () => { const s = freshStore(); const p = s.createProject({ name: 'pp', repoPath: '/tmp/pp-' + Math.random() }); assert.equal(p.lastSyncAt, null); const updated = s.patchProject(p.id, { autonomy: 'auto-easy', concurrency: 3, verifyCmd: 'npm test', model: 'sonnet', status: 'paused', }); assert.equal(updated.autonomy, 'auto-easy'); assert.equal(updated.concurrency, 3); assert.equal(updated.verifyCmd, 'npm test'); assert.equal(updated.model, 'sonnet'); assert.equal(updated.status, 'paused'); // 部分字段更新不影响其余字段;null 清空 const again = s.patchProject(p.id, { verifyCmd: null, status: 'active' }); assert.equal(again.verifyCmd, null); assert.equal(again.autonomy, 'auto-easy'); assert.equal(again.status, 'active'); s.close(); }); test('patchProject:非法值被拒(autonomy/concurrency/status)', () => { const s = freshStore(); const p = s.createProject({ name: 'pv', repoPath: '/tmp/pv-' + Math.random() }); assert.throws(() => s.patchProject(p.id, { autonomy: 'yolo' as never }), StoreError); assert.throws(() => s.patchProject(p.id, { concurrency: 0 }), StoreError); assert.throws(() => s.patchProject(p.id, { concurrency: 1.5 }), StoreError); assert.throws(() => s.patchProject(p.id, { status: 'stopped' as never }), StoreError); assert.throws(() => s.patchProject('prj_nope', { concurrency: 1 }), StoreError); s.close(); }); // ---------- patchTask ---------- test('patchTask:title/priority 更新', () => { const s = freshStore(); const p = s.createProject({ name: 'pt', repoPath: '/tmp/pt-' + Math.random() }); const t = s.createTask({ projectId: p.id, title: '原标题', complexity: 'easy' }); assert.equal(t.priority, 1); // 默认 P1(中) const u = s.patchTask(t.id, { title: '新标题', priority: 0 }); assert.equal(u.title, '新标题'); assert.equal(u.priority, 0); // P0 最高 assert.equal(u.status, 'ready'); // 未动 complexity 不重置状态 assert.throws(() => s.patchTask(t.id, { title: ' ' }), StoreError); assert.throws(() => s.patchTask(t.id, { priority: 5 }), StoreError); // 超出 0..2 assert.throws(() => s.patchTask(t.id, { priority: -1 }), StoreError); s.close(); }); test('patchTask:complexity 修改 → status 重置为新初始态 + status.changed 事件', () => { const s = freshStore(); const events: Array<{ type: string; payload: Record }> = []; s.subscribe((e) => events.push({ type: e.type, payload: e.payload })); const p = s.createProject({ name: 'pc', repoPath: '/tmp/pc-' + Math.random() }); // easy(ready) → hard:重置为 analyzing const t = s.createTask({ projectId: p.id, title: 'x', complexity: 'easy' }); const u = s.patchTask(t.id, { complexity: 'hard' }); assert.equal(u.complexity, 'hard'); assert.equal(u.status, 'analyzing'); const sc = events.filter((e) => e.type === 'status.changed').at(-1); assert.equal(sc?.payload.from, 'ready'); assert.equal(sc?.payload.to, 'analyzing'); // hard(analyzing) → medium:重置为 speccing const u2 = s.patchTask(t.id, { complexity: 'medium' }); assert.equal(u2.status, 'speccing'); // 同值修改 = no-op,不重置 s.transition(t.id, 'spec_review'); const u3 = s.patchTask(t.id, { complexity: 'medium' }); assert.equal(u3.status, 'spec_review'); // spec_review 在允许列表内:可改 const u4 = s.patchTask(t.id, { complexity: 'easy' }); assert.equal(u4.status, 'ready'); s.close(); }); test('patchTask:执行链路状态下改 complexity 被拒(StoreError → API 400)', () => { const s = freshStore(); const p = s.createProject({ name: 'pr', repoPath: '/tmp/pr-' + Math.random() }); const t = s.createTask({ projectId: p.id, title: 'y', complexity: 'easy' }); // ready s.transition(t.id, 'queued'); assert.throws( () => s.patchTask(t.id, { complexity: 'hard' }), (e: unknown) => e instanceof StoreError && (e as Error).message.includes('不允许修改复杂度'), ); // queued 下 title 仍可改 assert.equal(s.patchTask(t.id, { title: 'z' }).title, 'z'); // done(终态)同样拒绝 s.transition(t.id, 'executing'); s.transition(t.id, 'exec_review'); s.decide(t.id, 'accept', 'user'); assert.throws(() => s.patchTask(t.id, { complexity: 'medium' }), StoreError); s.close(); });