Move src/ → apps/server/ and packages/web/ → apps/web/ to adopt standard monorepo conventions (apps/ for runnable apps, packages/ for reusable libraries). Update all config files, shared package imports, test fixtures, and documentation to reflect new paths. Key fixes: - Update workspace config to ["apps/*", "packages/*"] - Update tsconfig.json rootDir/include for apps/server/ - Add apps/web/** to vitest exclude list - Update drizzle.config.ts schema path - Fix ensure-schema.ts migration path detection (3 levels up in dev, 2 levels up in dist) - Fix tests/integration/cli-server.test.ts import paths - Update packages/shared imports to apps/server/ paths - Update all docs/ files with new paths
286 lines
9.5 KiB
TypeScript
286 lines
9.5 KiB
TypeScript
/**
|
|
* E2E Tests for Architect Workflow
|
|
*
|
|
* Tests the complete architect workflow from discussion through phase creation:
|
|
* - Discuss mode: Gather context, answer questions, capture decisions
|
|
* - Plan mode: Break initiative into phases
|
|
* - Full workflow: Discuss -> Plan -> Phase persistence
|
|
*
|
|
* 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 } from '../../events/types.js';
|
|
|
|
describe('Architect Workflow E2E', () => {
|
|
let harness: TestHarness;
|
|
|
|
beforeEach(() => {
|
|
harness = createTestHarness();
|
|
});
|
|
|
|
afterEach(() => {
|
|
harness.cleanup();
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
describe('discuss mode', () => {
|
|
it('should spawn architect in discuss mode and complete with decisions', async () => {
|
|
vi.useFakeTimers();
|
|
|
|
// Create initiative
|
|
const initiative = await harness.createInitiative('Auth System');
|
|
|
|
// Set up discuss completion scenario
|
|
harness.setArchitectDiscussComplete('auth-discuss', [
|
|
{ topic: 'Auth Method', decision: 'JWT', reason: 'Stateless, scalable' },
|
|
{ topic: 'Token Storage', decision: 'httpOnly cookie', reason: 'XSS protection' },
|
|
], 'Auth approach decided');
|
|
|
|
// Spawn architect in discuss mode
|
|
const agent = await harness.caller.spawnArchitectDiscuss({
|
|
name: 'auth-discuss',
|
|
initiativeId: initiative.id,
|
|
});
|
|
|
|
expect(agent.mode).toBe('discuss');
|
|
|
|
// Wait for completion
|
|
await harness.advanceTimers();
|
|
|
|
// Verify agent stopped with context_complete
|
|
const events = harness.getEmittedEvents('agent:stopped') as AgentStoppedEvent[];
|
|
expect(events).toHaveLength(1);
|
|
expect(events[0].payload.reason).toBe('context_complete');
|
|
});
|
|
|
|
it('should pause on questions and resume with answers', async () => {
|
|
vi.useFakeTimers();
|
|
|
|
const initiative = await harness.createInitiative('Auth System');
|
|
|
|
// First, agent asks questions
|
|
harness.setArchitectDiscussQuestions('auth-discuss', [
|
|
{ id: 'q1', question: 'JWT or Session?', options: [{ label: 'JWT' }, { label: 'Session' }] },
|
|
{ id: 'q2', question: 'OAuth providers?' },
|
|
]);
|
|
|
|
const agent = await harness.caller.spawnArchitectDiscuss({
|
|
name: 'auth-discuss',
|
|
initiativeId: initiative.id,
|
|
});
|
|
|
|
await harness.advanceTimers();
|
|
|
|
// Agent should be waiting
|
|
const waitingAgent = await harness.caller.getAgent({ name: 'auth-discuss' });
|
|
expect(waitingAgent?.status).toBe('waiting_for_input');
|
|
|
|
// Get pending questions
|
|
const pending = await harness.mockAgentManager.getPendingQuestions(agent.id);
|
|
expect(pending?.questions).toHaveLength(2);
|
|
|
|
// Now set up completion scenario for after resume
|
|
harness.setArchitectDiscussComplete('auth-discuss', [
|
|
{ topic: 'Auth', decision: 'JWT', reason: 'User chose' },
|
|
], 'Complete');
|
|
|
|
// Resume with answers
|
|
await harness.caller.resumeAgent({
|
|
name: 'auth-discuss',
|
|
answers: { q1: 'JWT', q2: 'Google, GitHub' },
|
|
});
|
|
|
|
await harness.advanceTimers();
|
|
|
|
// Should complete
|
|
const finalAgent = await harness.caller.getAgent({ name: 'auth-discuss' });
|
|
expect(finalAgent?.status).toBe('idle');
|
|
});
|
|
});
|
|
|
|
describe('plan mode', () => {
|
|
it('should spawn architect in plan mode and create phases', async () => {
|
|
vi.useFakeTimers();
|
|
|
|
const initiative = await harness.createInitiative('Auth System');
|
|
|
|
// Set up plan completion
|
|
harness.setArchitectPlanComplete('auth-plan', [
|
|
{ number: 1, name: 'Database Setup', description: 'User table and auth schema', dependencies: [] },
|
|
{ number: 2, name: 'JWT Implementation', description: 'Token generation and validation', dependencies: [1] },
|
|
{ number: 3, name: 'Protected Routes', description: 'Middleware and route guards', dependencies: [2] },
|
|
]);
|
|
|
|
const agent = await harness.caller.spawnArchitectPlan({
|
|
name: 'auth-plan',
|
|
initiativeId: initiative.id,
|
|
});
|
|
|
|
expect(agent.mode).toBe('plan');
|
|
|
|
await harness.advanceTimers();
|
|
|
|
// Verify stopped with plan_complete
|
|
const events = harness.getEmittedEvents('agent:stopped') as AgentStoppedEvent[];
|
|
expect(events).toHaveLength(1);
|
|
expect(events[0].payload.reason).toBe('plan_complete');
|
|
});
|
|
|
|
it('should persist phases from plan output', async () => {
|
|
const initiative = await harness.createInitiative('Auth System');
|
|
|
|
const phasesData = [
|
|
{ name: 'Foundation' },
|
|
{ name: 'Features' },
|
|
];
|
|
|
|
// Persist phases (simulating what would happen after plan)
|
|
const created = await harness.createPhasesFromPlan(initiative.id, phasesData);
|
|
|
|
expect(created).toHaveLength(2);
|
|
|
|
// Verify retrieval
|
|
const phases = await harness.getPhases(initiative.id);
|
|
expect(phases).toHaveLength(2);
|
|
expect(phases[0].name).toBe('Foundation');
|
|
expect(phases[1].name).toBe('Features');
|
|
});
|
|
});
|
|
|
|
describe('plan conflict detection', () => {
|
|
it('should reject if a plan agent is already running', async () => {
|
|
vi.useFakeTimers();
|
|
|
|
const initiative = await harness.createInitiative('Auth System');
|
|
|
|
// Set up a long-running plan agent (never completes during this test)
|
|
harness.setArchitectPlanComplete('first-plan', [
|
|
{ number: 1, name: 'Phase 1', description: 'First', dependencies: [] },
|
|
]);
|
|
// Use a delay so it stays running
|
|
harness.setAgentScenario('first-plan', { status: 'done', delay: 999999 });
|
|
|
|
await harness.caller.spawnArchitectPlan({
|
|
name: 'first-plan',
|
|
initiativeId: initiative.id,
|
|
});
|
|
|
|
// Agent should be running
|
|
const agents = await harness.caller.listAgents();
|
|
expect(agents.find(a => a.name === 'first-plan')?.status).toBe('running');
|
|
|
|
// Second plan should be rejected
|
|
await expect(
|
|
harness.caller.spawnArchitectPlan({
|
|
name: 'second-plan',
|
|
initiativeId: initiative.id,
|
|
}),
|
|
).rejects.toThrow(/already running/);
|
|
});
|
|
|
|
it('should auto-dismiss stale plan agents before checking', async () => {
|
|
vi.useFakeTimers();
|
|
|
|
const initiative = await harness.createInitiative('Auth System');
|
|
|
|
// Set up a plan agent that crashes immediately
|
|
harness.setAgentScenario('stale-plan', { status: 'error', error: 'crashed' });
|
|
|
|
await harness.caller.spawnArchitectPlan({
|
|
name: 'stale-plan',
|
|
initiativeId: initiative.id,
|
|
});
|
|
await harness.advanceTimers();
|
|
|
|
// Should be crashed
|
|
const agents = await harness.caller.listAgents();
|
|
expect(agents.find(a => a.name === 'stale-plan')?.status).toBe('crashed');
|
|
|
|
// New plan should succeed (stale one gets auto-dismissed)
|
|
harness.setArchitectPlanComplete('new-plan', [
|
|
{ number: 1, name: 'Phase 1', description: 'First', dependencies: [] },
|
|
]);
|
|
|
|
const agent = await harness.caller.spawnArchitectPlan({
|
|
name: 'new-plan',
|
|
initiativeId: initiative.id,
|
|
});
|
|
expect(agent.mode).toBe('plan');
|
|
});
|
|
|
|
it('should allow plan for different initiatives', async () => {
|
|
vi.useFakeTimers();
|
|
|
|
const init1 = await harness.createInitiative('Initiative 1');
|
|
const init2 = await harness.createInitiative('Initiative 2');
|
|
|
|
// Long-running agent on initiative 1
|
|
harness.setAgentScenario('plan-1', { status: 'done', delay: 999999 });
|
|
await harness.caller.spawnArchitectPlan({
|
|
name: 'plan-1',
|
|
initiativeId: init1.id,
|
|
});
|
|
|
|
// Plan on initiative 2 should succeed
|
|
harness.setArchitectPlanComplete('plan-2', [
|
|
{ number: 1, name: 'Phase 1', description: 'First', dependencies: [] },
|
|
]);
|
|
|
|
const agent = await harness.caller.spawnArchitectPlan({
|
|
name: 'plan-2',
|
|
initiativeId: init2.id,
|
|
});
|
|
expect(agent.mode).toBe('plan');
|
|
});
|
|
});
|
|
|
|
describe('full workflow', () => {
|
|
it('should complete discuss -> plan -> phases workflow', async () => {
|
|
vi.useFakeTimers();
|
|
|
|
// 1. Create initiative
|
|
const initiative = await harness.createInitiative('Full Workflow Test');
|
|
|
|
// 2. Discuss phase
|
|
harness.setArchitectDiscussComplete('discuss-agent', [
|
|
{ topic: 'Scope', decision: 'MVP only', reason: 'Time constraint' },
|
|
], 'Scope defined');
|
|
|
|
await harness.caller.spawnArchitectDiscuss({
|
|
name: 'discuss-agent',
|
|
initiativeId: initiative.id,
|
|
});
|
|
await harness.advanceTimers();
|
|
|
|
// 3. Plan phase
|
|
harness.setArchitectPlanComplete('plan-agent', [
|
|
{ number: 1, name: 'Core', description: 'Core functionality', dependencies: [] },
|
|
{ number: 2, name: 'Polish', description: 'UI and UX', dependencies: [1] },
|
|
]);
|
|
|
|
await harness.caller.spawnArchitectPlan({
|
|
name: 'plan-agent',
|
|
initiativeId: initiative.id,
|
|
contextSummary: 'MVP scope defined',
|
|
});
|
|
await harness.advanceTimers();
|
|
|
|
// 4. Persist phases
|
|
await harness.createPhasesFromPlan(initiative.id, [
|
|
{ name: 'Core' },
|
|
{ name: 'Polish' },
|
|
]);
|
|
|
|
// 5. Verify final state
|
|
const phases = await harness.getPhases(initiative.id);
|
|
expect(phases).toHaveLength(2);
|
|
|
|
// Both agents should be idle
|
|
const agents = await harness.caller.listAgents();
|
|
expect(agents.filter(a => a.status === 'idle')).toHaveLength(2);
|
|
});
|
|
});
|
|
});
|