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:
Lukas May
2026-02-01 11:56:55 +01:00
parent 7c9200d755
commit 9b370a2617

View 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');
});
});
});