7993ecc6d6
CI / test (push) Has been cancelled
Phase 2 of PulseDatabase abstraction: - Create @uncaged/pulse-local with bun:sqlite implementation - Core @uncaged/pulse now exports types only for store (no createStore/createScopedStore) - Update pulse-workflows, upulse to import factories from @uncaged/pulse-local - All tests passing (267 core, 39 pulse-workflows)
494 lines
12 KiB
TypeScript
494 lines
12 KiB
TypeScript
/**
|
|
* 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();
|
|
}
|
|
});
|
|
});
|