/** * coding-tdd WorkflowType tests — TDD pipeline + rejection loops. * * 小橘 🍊 (NEKO Team) */ import { describe, expect, it } from 'bun:test'; import { mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { createStore, type PulseStore } from '@uncaged/pulse-local'; import { createTddCodingWorkflow } from './coding-tdd.js'; import { createWorkflowRule } from '@uncaged/pulse'; import { END, START } from '@uncaged/pulse'; describe('coding-tdd WorkflowType', () => { let store: PulseStore; let tmpDir: string; function setup() { tmpDir = mkdtempSync(join(tmpdir(), 'coding-tdd-')); store = createStore({ eventsDbPath: join(tmpDir, 'test.db'), objectsDir: join(tmpDir, 'objects'), }); } async function cleanup() { try { await store?.close(); } catch {} if (tmpDir) rmSync(tmpDir, { recursive: true, force: true }); } async function trigger(topicId: string, title: string, description: string) { const hash = await store.putObject(description); await store.appendEvent({ occurredAt: Date.now(), kind: 'coding-tdd.__start__', key: topicId, hash, meta: JSON.stringify({ title, repoDir: '/tmp/r' }), }); } const ruleOpts = { maxTicksPerTopic: 200, maxTicksPerWindow: 500, cooldownMs: 0, } as const; it('limits.maxRounds is 25', () => { const wf = createTddCodingWorkflow(); expect(wf.limits?.maxRounds).toBe(25); }); it('exports createTddCodingWorkflow', () => { expect(typeof createTddCodingWorkflow).toBe('function'); }); it('full happy path via adapter: all roles until END', async () => { setup(); try { const wf = createTddCodingWorkflow(); const rule = createWorkflowRule(wf, store, undefined, ruleOpts); await trigger('t1', 'Feature', 'desc'); const order: string[] = []; for (let i = 0; i < 30; i++) { const r = await rule.tick(); if (r.executed.length === 0) break; order.push(...r.executed.map((x) => x.role)); } expect(order).toEqual([ 'test-planner', 'test-reviewer', 'test-coder', 'coder', 'auto-tester', 'manual-tester', 'reviewer', ]); const r = await rule.tick(); expect(r.executed).toEqual([]); } finally { await cleanup(); } }); it('moderator: START → test-planner', () => { const wf = createTddCodingWorkflow(); expect(wf.moderator({ role: START, meta: null }, 'x')).toBe('test-planner'); }); it('moderator: test-planner → test-reviewer', () => { const wf = createTddCodingWorkflow(); expect( wf.moderator( { role: 'test-planner', meta: { testPlan: '# p', scenarios: ['a'], }, }, 'x', ), ).toBe('test-reviewer'); }); it('moderator: test-reviewer approved → test-coder', () => { const wf = createTddCodingWorkflow(); expect( wf.moderator( { role: 'test-reviewer', meta: { verdict: 'approved', feedback: '' } }, 'x', ), ).toBe('test-coder'); }); it('moderator: test-reviewer rejected → test-planner', () => { const wf = createTddCodingWorkflow(); expect( wf.moderator( { role: 'test-reviewer', meta: { verdict: 'rejected', feedback: 'no' }, }, 'x', 10, ), ).toBe('test-planner'); }); it('moderator: test-reviewer rejected + emergency → END', () => { const wf = createTddCodingWorkflow(); expect( wf.moderator( { role: 'test-reviewer', meta: { verdict: 'rejected', feedback: 'no' }, }, 'x', 1, ), ).toBe(END); }); it('moderator: test-coder → coder', () => { const wf = createTddCodingWorkflow(); expect( wf.moderator( { role: 'test-coder', meta: { testFiles: ['a.test.ts'], testCount: 1 }, }, 'x', ), ).toBe('coder'); }); it('moderator: coder → auto-tester', () => { const wf = createTddCodingWorkflow(); expect( wf.moderator( { role: 'coder', meta: { filesChanged: ['src/foo.ts'], deploymentGuide: 'bun test', }, }, 'x', ), ).toBe('auto-tester'); }); it('moderator: auto-tester pass → manual-tester', () => { const wf = createTddCodingWorkflow(); expect( wf.moderator( { role: 'auto-tester', meta: { pass: true, failedTests: [], output: 'ok' }, }, 'x', ), ).toBe('manual-tester'); }); it('moderator: auto-tester fail → coder', () => { const wf = createTddCodingWorkflow(); expect( wf.moderator( { role: 'auto-tester', meta: { pass: false, failedTests: ['x'], output: 'fail' }, }, 'x', 10, ), ).toBe('coder'); }); it('moderator: auto-tester fail + emergency → END', () => { const wf = createTddCodingWorkflow(); expect( wf.moderator( { role: 'auto-tester', meta: { pass: false, failedTests: ['x'], output: 'fail' }, }, 'x', 1, ), ).toBe(END); }); it('moderator: manual-tester pass → reviewer', () => { const wf = createTddCodingWorkflow(); expect( wf.moderator( { role: 'manual-tester', meta: { pass: true, issues: [] } }, 'x', ), ).toBe('reviewer'); }); it('moderator: manual-tester fail → coder', () => { const wf = createTddCodingWorkflow(); expect( wf.moderator( { role: 'manual-tester', meta: { pass: false, issues: ['ui broken'] }, }, 'x', 10, ), ).toBe('coder'); }); it('moderator: manual-tester fail + emergency → END', () => { const wf = createTddCodingWorkflow(); expect( wf.moderator( { role: 'manual-tester', meta: { pass: false, issues: ['ui broken'] }, }, 'x', 1, ), ).toBe(END); }); it('moderator: reviewer approved → END', () => { const wf = createTddCodingWorkflow(); expect( wf.moderator( { role: 'reviewer', meta: { verdict: 'approved', comments: '', codeQuality: 'a', testQuality: 'a', }, }, 'x', ), ).toBe(END); }); it('moderator: reviewer approved + emergency → END', () => { const wf = createTddCodingWorkflow(); expect( wf.moderator( { role: 'reviewer', meta: { verdict: 'approved', comments: '', codeQuality: 'a', testQuality: 'a', }, }, 'x', 1, ), ).toBe(END); }); it('moderator: reviewer rejected → coder', () => { const wf = createTddCodingWorkflow(); expect( wf.moderator( { role: 'reviewer', meta: { verdict: 'rejected', comments: 'fix', codeQuality: 'c', testQuality: 'b', }, }, 'x', 10, ), ).toBe('coder'); }); it('moderator: reviewer rejected + emergency → END', () => { const wf = createTddCodingWorkflow(); expect( wf.moderator( { role: 'reviewer', meta: { verdict: 'rejected', comments: 'fix', codeQuality: 'c', testQuality: 'b', }, }, 'x', 1, ), ).toBe(END); }); it('loop: test-reviewer rejects once then approves', async () => { setup(); try { let trRound = 0; const wf = createTddCodingWorkflow({ testReviewerFn: async () => { trRound++; return { content: `review ${trRound}`, meta: trRound === 1 ? { verdict: 'rejected' as const, feedback: 'add cases' } : { verdict: 'approved' as const, feedback: 'ok' }, }; }, }); const rule = createWorkflowRule(wf, store, undefined, ruleOpts); await trigger('loop-tr', 'L', 'd'); const roles: string[] = []; for (let i = 0; i < 40; i++) { const r = await rule.tick(); if (r.executed.length === 0) break; roles.push(...r.executed.map((x) => x.role)); } expect(roles.slice(0, 4)).toEqual([ 'test-planner', 'test-reviewer', 'test-planner', 'test-reviewer', ]); expect(roles).toContain('test-coder'); } finally { await cleanup(); } }); it('loop: auto-tester fails once then passes', async () => { setup(); try { let autoRound = 0; const wf = createTddCodingWorkflow({ autoTesterFn: async () => { autoRound++; return { content: 'out', meta: autoRound === 1 ? { pass: false, failedTests: ['t1'], output: 'fail', } : { pass: true, failedTests: [], output: 'ok', }, }; }, }); const rule = createWorkflowRule(wf, store, undefined, ruleOpts); await trigger('loop-auto', 'L', 'd'); const roles: string[] = []; for (let i = 0; i < 40; i++) { const r = await rule.tick(); if (r.executed.length === 0) break; roles.push(...r.executed.map((x) => x.role)); } const firstCoder = roles.indexOf('coder'); const firstAuto = roles.indexOf('auto-tester'); const secondCoder = roles.indexOf('coder', firstCoder + 1); expect(firstAuto).toBeGreaterThan(firstCoder); expect(secondCoder).toBeGreaterThan(firstAuto); } finally { await cleanup(); } }); it('loop: manual-tester fails once then passes', async () => { setup(); try { let manRound = 0; const wf = createTddCodingWorkflow({ manualTesterFn: async () => { manRound++; return { content: 'notes', meta: manRound === 1 ? { pass: false, issues: ['bug'] } : { pass: true, issues: [] }, }; }, }); const rule = createWorkflowRule(wf, store, undefined, ruleOpts); await trigger('loop-man', 'L', 'd'); const roles: string[] = []; for (let i = 0; i < 40; i++) { const r = await rule.tick(); if (r.executed.length === 0) break; roles.push(...r.executed.map((x) => x.role)); } const idxReviewer1 = roles.indexOf('reviewer'); expect(idxReviewer1).toBeGreaterThan(-1); expect(roles.lastIndexOf('coder')).toBeGreaterThan( roles.indexOf('manual-tester'), ); } finally { await cleanup(); } }); it('loop: reviewer rejects once then approves', async () => { setup(); try { let revRound = 0; const wf = createTddCodingWorkflow({ reviewerFn: async () => { revRound++; return { content: `rev ${revRound}`, meta: revRound === 1 ? { verdict: 'rejected' as const, comments: 'fix', codeQuality: 'c', testQuality: 'c', } : { verdict: 'approved' as const, comments: 'ok', codeQuality: 'a', testQuality: 'a', }, }; }, }); const rule = createWorkflowRule(wf, store, undefined, ruleOpts); await trigger('loop-rev', 'L', 'd'); const roles: string[] = []; for (let i = 0; i < 40; i++) { const r = await rule.tick(); if (r.executed.length === 0) break; roles.push(...r.executed.map((x) => x.role)); } expect(roles.filter((r) => r === 'reviewer').length).toBe(2); } finally { await cleanup(); } }); });