test(12-08): add E2E tests for decompose workflow
- Add tests for spawn decompose agent with completion - Add tests for Q&A flow with questions and resume - Add tests for multiple questions handling - Add tests for task persistence from decomposition - Add tests for all task types (auto, checkpoint variants) - Add tests for task dependencies - Add full workflow test: initiative -> phase -> plan -> decompose -> tasks
This commit is contained in:
301
src/test/e2e/decompose-workflow.test.ts
Normal file
301
src/test/e2e/decompose-workflow.test.ts
Normal file
@@ -0,0 +1,301 @@
|
||||
/**
|
||||
* E2E Tests for Decompose Workflow
|
||||
*
|
||||
* Tests the complete decomposition workflow from plan creation through task creation:
|
||||
* - Decompose mode: Break plan into executable tasks
|
||||
* - Q&A flow: Handle clarifying questions during decomposition
|
||||
* - Task persistence: Save tasks from decomposition output
|
||||
*
|
||||
* Uses TestHarness from src/test/ for full system wiring.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { createTestHarness, type TestHarness } from '../index.js';
|
||||
import type { AgentStoppedEvent, AgentWaitingEvent } from '../../events/types.js';
|
||||
|
||||
describe('Decompose Workflow E2E', () => {
|
||||
let harness: TestHarness;
|
||||
|
||||
beforeEach(() => {
|
||||
harness = createTestHarness();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
harness.cleanup();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe('spawn decompose agent', () => {
|
||||
it('should spawn agent in decompose mode and complete with tasks', async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
// Setup: Create initiative -> phase -> plan
|
||||
const initiative = await harness.createInitiative('Test Project', 'Test project description');
|
||||
const phases = await harness.createPhasesFromBreakdown(initiative.id, [
|
||||
{ number: 1, name: 'Phase 1', description: 'First phase' },
|
||||
]);
|
||||
const plan = await harness.createPlan(phases[0].id, 'Auth Plan', 'Implement authentication');
|
||||
|
||||
// Set decompose scenario
|
||||
harness.setArchitectDecomposeComplete('decomposer', [
|
||||
{ number: 1, name: 'Create schema', description: 'User table', type: 'auto', dependencies: [] },
|
||||
{ number: 2, name: 'Create endpoint', description: 'Login API', type: 'auto', dependencies: [1] },
|
||||
]);
|
||||
|
||||
// Spawn decompose agent
|
||||
const agent = await harness.caller.spawnArchitectDecompose({
|
||||
name: 'decomposer',
|
||||
planId: plan.id,
|
||||
});
|
||||
|
||||
expect(agent.mode).toBe('decompose');
|
||||
|
||||
// Advance timers for async completion
|
||||
await harness.advanceTimers();
|
||||
|
||||
// Verify agent completed
|
||||
const events = harness.getEmittedEvents('agent:stopped') as AgentStoppedEvent[];
|
||||
expect(events).toHaveLength(1);
|
||||
expect(events[0].payload.name).toBe('decomposer');
|
||||
expect(events[0].payload.reason).toBe('decompose_complete');
|
||||
});
|
||||
|
||||
it('should pause on questions and resume', async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const initiative = await harness.createInitiative('Test Project');
|
||||
const phases = await harness.createPhasesFromBreakdown(initiative.id, [
|
||||
{ number: 1, name: 'Phase 1', description: 'First phase' },
|
||||
]);
|
||||
const plan = await harness.createPlan(phases[0].id, 'Complex Plan');
|
||||
|
||||
// Set questions scenario
|
||||
harness.setArchitectDecomposeQuestions('decomposer', [
|
||||
{ id: 'q1', question: 'How granular should tasks be?' },
|
||||
]);
|
||||
|
||||
const agent = await harness.caller.spawnArchitectDecompose({
|
||||
name: 'decomposer',
|
||||
planId: plan.id,
|
||||
});
|
||||
|
||||
await harness.advanceTimers();
|
||||
|
||||
// Verify agent is waiting for input
|
||||
const waitingAgent = await harness.caller.getAgent({ name: 'decomposer' });
|
||||
expect(waitingAgent?.status).toBe('waiting_for_input');
|
||||
|
||||
// Verify paused on questions (emits agent:waiting, not agent:stopped)
|
||||
const waitingEvents = harness.getEmittedEvents('agent:waiting') as AgentWaitingEvent[];
|
||||
expect(waitingEvents).toHaveLength(1);
|
||||
expect(waitingEvents[0].payload.questions).toHaveLength(1);
|
||||
|
||||
// Get pending questions
|
||||
const pending = await harness.mockAgentManager.getPendingQuestions(agent.id);
|
||||
expect(pending?.questions).toHaveLength(1);
|
||||
expect(pending?.questions[0].question).toBe('How granular should tasks be?');
|
||||
|
||||
// Set completion scenario for resume
|
||||
harness.setArchitectDecomposeComplete('decomposer', [
|
||||
{ number: 1, name: 'Task 1', description: 'Single task', type: 'auto', dependencies: [] },
|
||||
]);
|
||||
|
||||
// Resume with answer
|
||||
await harness.caller.resumeAgent({
|
||||
name: 'decomposer',
|
||||
answers: { q1: 'Very granular' },
|
||||
});
|
||||
await harness.advanceTimers();
|
||||
|
||||
// Verify completed after resume
|
||||
const finalAgent = await harness.caller.getAgent({ name: 'decomposer' });
|
||||
expect(finalAgent?.status).toBe('idle');
|
||||
});
|
||||
|
||||
it('should handle multiple questions', async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const initiative = await harness.createInitiative('Multi-Q Project');
|
||||
const phases = await harness.createPhasesFromBreakdown(initiative.id, [
|
||||
{ number: 1, name: 'Phase 1', description: 'First phase' },
|
||||
]);
|
||||
const plan = await harness.createPlan(phases[0].id, 'Complex Plan');
|
||||
|
||||
// Set multiple questions scenario
|
||||
harness.setArchitectDecomposeQuestions('decomposer', [
|
||||
{ id: 'q1', question: 'What task granularity?', options: [{ label: 'Fine' }, { label: 'Coarse' }] },
|
||||
{ id: 'q2', question: 'Include checkpoints?' },
|
||||
{ id: 'q3', question: 'Any blocking dependencies?' },
|
||||
]);
|
||||
|
||||
const agent = await harness.caller.spawnArchitectDecompose({
|
||||
name: 'decomposer',
|
||||
planId: plan.id,
|
||||
});
|
||||
|
||||
await harness.advanceTimers();
|
||||
|
||||
// Verify all questions received
|
||||
const pending = await harness.mockAgentManager.getPendingQuestions(agent.id);
|
||||
expect(pending?.questions).toHaveLength(3);
|
||||
|
||||
// Set completion scenario for resume
|
||||
harness.setArchitectDecomposeComplete('decomposer', [
|
||||
{ number: 1, name: 'Task 1', description: 'First task', type: 'auto', dependencies: [] },
|
||||
{ number: 2, name: 'Task 2', description: 'Second task', type: 'auto', dependencies: [1] },
|
||||
{ number: 3, name: 'Verify', description: 'Verify all', type: 'checkpoint:human-verify', dependencies: [2] },
|
||||
]);
|
||||
|
||||
// Resume with all answers
|
||||
await harness.caller.resumeAgent({
|
||||
name: 'decomposer',
|
||||
answers: {
|
||||
q1: 'Fine',
|
||||
q2: 'Yes, add human verification',
|
||||
q3: 'Tasks 1 and 2 are sequential',
|
||||
},
|
||||
});
|
||||
await harness.advanceTimers();
|
||||
|
||||
// Verify completed
|
||||
const finalAgent = await harness.caller.getAgent({ name: 'decomposer' });
|
||||
expect(finalAgent?.status).toBe('idle');
|
||||
});
|
||||
});
|
||||
|
||||
describe('task persistence', () => {
|
||||
it('should create tasks from decomposition output', async () => {
|
||||
const initiative = await harness.createInitiative('Test Project');
|
||||
const phases = await harness.createPhasesFromBreakdown(initiative.id, [
|
||||
{ number: 1, name: 'Phase 1', description: 'First phase' },
|
||||
]);
|
||||
const plan = await harness.createPlan(phases[0].id, 'Auth Plan');
|
||||
|
||||
// Create tasks from decomposition
|
||||
await harness.caller.createTasksFromDecomposition({
|
||||
planId: plan.id,
|
||||
tasks: [
|
||||
{ number: 1, name: 'Schema', description: 'Create tables', type: 'auto', dependencies: [] },
|
||||
{ number: 2, name: 'API', description: 'Create endpoints', type: 'auto', dependencies: [1] },
|
||||
{ number: 3, name: 'Verify', description: 'Test flow', type: 'checkpoint:human-verify', dependencies: [2] },
|
||||
],
|
||||
});
|
||||
|
||||
// Verify tasks created
|
||||
const tasks = await harness.getTasksForPlan(plan.id);
|
||||
expect(tasks).toHaveLength(3);
|
||||
expect(tasks[0].name).toBe('Schema');
|
||||
expect(tasks[1].name).toBe('API');
|
||||
expect(tasks[2].name).toBe('Verify');
|
||||
expect(tasks[2].type).toBe('checkpoint:human-verify');
|
||||
});
|
||||
|
||||
it('should handle all task types', async () => {
|
||||
const initiative = await harness.createInitiative('Task Types Test');
|
||||
const phases = await harness.createPhasesFromBreakdown(initiative.id, [
|
||||
{ number: 1, name: 'Phase 1', description: 'First phase' },
|
||||
]);
|
||||
const plan = await harness.createPlan(phases[0].id, 'Mixed Tasks');
|
||||
|
||||
// Create tasks with all types
|
||||
await harness.caller.createTasksFromDecomposition({
|
||||
planId: plan.id,
|
||||
tasks: [
|
||||
{ number: 1, name: 'Auto Task', description: 'Automated work', type: 'auto' },
|
||||
{ number: 2, name: 'Human Verify', description: 'Visual check', type: 'checkpoint:human-verify', dependencies: [1] },
|
||||
{ number: 3, name: 'Decision', description: 'Choose approach', type: 'checkpoint:decision', dependencies: [2] },
|
||||
{ number: 4, name: 'Human Action', description: 'Manual step', type: 'checkpoint:human-action', dependencies: [3] },
|
||||
],
|
||||
});
|
||||
|
||||
const tasks = await harness.getTasksForPlan(plan.id);
|
||||
expect(tasks).toHaveLength(4);
|
||||
expect(tasks[0].type).toBe('auto');
|
||||
expect(tasks[1].type).toBe('checkpoint:human-verify');
|
||||
expect(tasks[2].type).toBe('checkpoint:decision');
|
||||
expect(tasks[3].type).toBe('checkpoint:human-action');
|
||||
});
|
||||
|
||||
it('should create task dependencies', async () => {
|
||||
const initiative = await harness.createInitiative('Dependencies Test');
|
||||
const phases = await harness.createPhasesFromBreakdown(initiative.id, [
|
||||
{ number: 1, name: 'Phase 1', description: 'First phase' },
|
||||
]);
|
||||
const plan = await harness.createPlan(phases[0].id, 'Dependent Tasks');
|
||||
|
||||
// Create tasks with complex dependencies
|
||||
await harness.caller.createTasksFromDecomposition({
|
||||
planId: plan.id,
|
||||
tasks: [
|
||||
{ number: 1, name: 'Task A', description: 'No deps', type: 'auto' },
|
||||
{ number: 2, name: 'Task B', description: 'Depends on A', type: 'auto', dependencies: [1] },
|
||||
{ number: 3, name: 'Task C', description: 'Depends on A', type: 'auto', dependencies: [1] },
|
||||
{ number: 4, name: 'Task D', description: 'Depends on B and C', type: 'auto', dependencies: [2, 3] },
|
||||
],
|
||||
});
|
||||
|
||||
const tasks = await harness.getTasksForPlan(plan.id);
|
||||
expect(tasks).toHaveLength(4);
|
||||
|
||||
// All tasks should be created with correct names
|
||||
expect(tasks.map(t => t.name)).toEqual(['Task A', 'Task B', 'Task C', 'Task D']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('full decompose workflow', () => {
|
||||
it('should complete initiative -> phase -> plan -> decompose -> tasks workflow', async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
// 1. Create initiative
|
||||
const initiative = await harness.createInitiative('Full Workflow Test', 'Complete workflow');
|
||||
|
||||
// 2. Create phase
|
||||
const phases = await harness.createPhasesFromBreakdown(initiative.id, [
|
||||
{ number: 1, name: 'Auth Phase', description: 'Authentication implementation' },
|
||||
]);
|
||||
|
||||
// 3. Create plan
|
||||
const plan = await harness.createPlan(phases[0].id, 'Auth Plan', 'Implement JWT auth');
|
||||
|
||||
// 4. Spawn decompose agent
|
||||
harness.setArchitectDecomposeComplete('decomposer', [
|
||||
{ number: 1, name: 'Create user schema', description: 'Define User model', type: 'auto', dependencies: [] },
|
||||
{ number: 2, name: 'Implement JWT', description: 'Token generation', type: 'auto', dependencies: [1] },
|
||||
{ number: 3, name: 'Protected routes', description: 'Middleware', type: 'auto', dependencies: [2] },
|
||||
{ number: 4, name: 'Verify auth', description: 'Test login flow', type: 'checkpoint:human-verify', dependencies: [3] },
|
||||
]);
|
||||
|
||||
await harness.caller.spawnArchitectDecompose({
|
||||
name: 'decomposer',
|
||||
planId: plan.id,
|
||||
});
|
||||
await harness.advanceTimers();
|
||||
|
||||
// 5. Verify agent completed
|
||||
const events = harness.getEmittedEvents('agent:stopped') as AgentStoppedEvent[];
|
||||
expect(events).toHaveLength(1);
|
||||
expect(events[0].payload.reason).toBe('decompose_complete');
|
||||
|
||||
// 6. Persist tasks (simulating what orchestrator would do after decompose)
|
||||
await harness.caller.createTasksFromDecomposition({
|
||||
planId: plan.id,
|
||||
tasks: [
|
||||
{ number: 1, name: 'Create user schema', description: 'Define User model', type: 'auto', dependencies: [] },
|
||||
{ number: 2, name: 'Implement JWT', description: 'Token generation', type: 'auto', dependencies: [1] },
|
||||
{ number: 3, name: 'Protected routes', description: 'Middleware', type: 'auto', dependencies: [2] },
|
||||
{ number: 4, name: 'Verify auth', description: 'Test login flow', type: 'checkpoint:human-verify', dependencies: [3] },
|
||||
],
|
||||
});
|
||||
|
||||
// 7. Verify final state
|
||||
const tasks = await harness.getTasksForPlan(plan.id);
|
||||
expect(tasks).toHaveLength(4);
|
||||
expect(tasks[0].name).toBe('Create user schema');
|
||||
expect(tasks[3].type).toBe('checkpoint:human-verify');
|
||||
|
||||
// Agent should be idle
|
||||
const finalAgent = await harness.caller.getAgent({ name: 'decomposer' });
|
||||
expect(finalAgent?.status).toBe('idle');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user