diff --git a/src/agent/mock-manager.test.ts b/src/agent/mock-manager.test.ts index fb48acd..42eaacb 100644 --- a/src/agent/mock-manager.test.ts +++ b/src/agent/mock-manager.test.ts @@ -129,9 +129,9 @@ describe('MockAgentManager', () => { describe('spawn with configured delay', () => { it('should not complete before delay expires', async () => { manager.setScenario('delayed-agent', { - outcome: 'success', + status: 'done', delay: 100, - message: 'Delayed completion', + result: 'Delayed completion', }); const agent = await manager.spawn({ @@ -149,9 +149,9 @@ describe('MockAgentManager', () => { it('should complete after delay expires', async () => { manager.setScenario('delayed-agent', { - outcome: 'success', + status: 'done', delay: 100, - message: 'Delayed completion', + result: 'Delayed completion', }); const agent = await manager.spawn({ @@ -175,12 +175,12 @@ describe('MockAgentManager', () => { // spawn() with crash scenario // =========================================================================== - describe('spawn with crash scenario', () => { + describe('spawn with unrecoverable_error scenario', () => { it('should emit agent:crashed and set result.success=false', async () => { manager.setScenario('crash-agent', { - outcome: 'crash', + status: 'unrecoverable_error', delay: 0, - message: 'Something went terribly wrong', + error: 'Something went terribly wrong', }); const agent = await manager.spawn({ @@ -208,13 +208,13 @@ describe('MockAgentManager', () => { }); // =========================================================================== - // spawn() with waiting_for_input scenario + // spawn() with question scenario // =========================================================================== - describe('spawn with waiting_for_input scenario', () => { + describe('spawn with question scenario', () => { it('should emit agent:waiting and set status to waiting_for_input', async () => { manager.setScenario('waiting-agent', { - outcome: 'waiting_for_input', + status: 'question', delay: 0, question: 'Should I continue?', }); @@ -242,10 +242,10 @@ describe('MockAgentManager', () => { // resume() after waiting_for_input // =========================================================================== - describe('resume after waiting_for_input', () => { + describe('resume after question', () => { it('should emit agent:resumed and continue with scenario', async () => { manager.setScenario('resume-agent', { - outcome: 'waiting_for_input', + status: 'question', delay: 0, question: 'Need your input', }); @@ -311,9 +311,9 @@ describe('MockAgentManager', () => { describe('stop', () => { it('should cancel scheduled completion and emit agent:stopped', async () => { manager.setScenario('stoppable-agent', { - outcome: 'success', + status: 'done', delay: 1000, - message: 'Should not see this', + result: 'Should not see this', }); const agent = await manager.spawn({ @@ -411,11 +411,11 @@ describe('MockAgentManager', () => { describe('setScenario overrides', () => { it('should use scenario override for specific agent name', async () => { - // Set crash scenario for one agent + // Set unrecoverable_error scenario for one agent manager.setScenario('crasher', { - outcome: 'crash', + status: 'unrecoverable_error', delay: 0, - message: 'Intentional crash', + error: 'Intentional crash', }); // Spawn two agents - one with override, one with default @@ -443,8 +443,9 @@ describe('MockAgentManager', () => { it('should allow clearing scenario override', async () => { manager.setScenario('flip-flop', { - outcome: 'crash', + status: 'unrecoverable_error', delay: 0, + error: 'Crash for test', }); // First spawn crashes @@ -489,7 +490,7 @@ describe('MockAgentManager', () => { }); it('should emit spawned before crashed', async () => { - manager.setScenario('crash-order', { outcome: 'crash', delay: 0 }); + manager.setScenario('crash-order', { status: 'unrecoverable_error', delay: 0, error: 'Crash' }); await manager.spawn({ name: 'crash-order', taskId: 't1', prompt: 'p1' }); await vi.advanceTimersByTimeAsync(0); @@ -503,8 +504,9 @@ describe('MockAgentManager', () => { it('should emit spawned before waiting', async () => { manager.setScenario('wait-order', { - outcome: 'waiting_for_input', + status: 'question', delay: 0, + question: 'Test question', }); await manager.spawn({ name: 'wait-order', taskId: 't1', prompt: 'p1' }); await vi.advanceTimersByTimeAsync(0); @@ -551,9 +553,9 @@ describe('MockAgentManager', () => { it('should use provided default scenario', async () => { const customDefault: MockAgentScenario = { - outcome: 'crash', + status: 'unrecoverable_error', delay: 0, - message: 'Default crash', + error: 'Default crash', }; const customManager = new MockAgentManager({ @@ -580,7 +582,7 @@ describe('MockAgentManager', () => { describe('clear', () => { it('should remove all agents and cancel pending timers', async () => { - manager.setScenario('pending', { outcome: 'success', delay: 1000 }); + manager.setScenario('pending', { status: 'done', delay: 1000 }); await manager.spawn({ name: 'pending', taskId: 't1', prompt: 'p1' }); await manager.spawn({ name: 'another', taskId: 't2', prompt: 'p2' }); @@ -592,4 +594,83 @@ describe('MockAgentManager', () => { expect((await manager.list()).length).toBe(0); }); }); + + // =========================================================================== + // Structured question data (new schema tests) + // =========================================================================== + + describe('structured question data', () => { + it('emits agent:waiting with structured question data', async () => { + manager.setScenario('test-agent', { + status: 'question', + question: 'Which database?', + options: [ + { label: 'PostgreSQL', description: 'Full-featured' }, + { label: 'SQLite', description: 'Lightweight' }, + ], + multiSelect: false, + }); + + await manager.spawn({ name: 'test-agent', taskId: 'task-1', prompt: 'test' }); + await vi.runAllTimersAsync(); + + const events = eventBus.emittedEvents.filter((e) => e.type === 'agent:waiting'); + expect(events.length).toBe(1); + expect((events[0] as any).payload.options).toHaveLength(2); + expect((events[0] as any).payload.options[0].label).toBe('PostgreSQL'); + expect((events[0] as any).payload.multiSelect).toBe(false); + }); + + it('stores pending question for retrieval', async () => { + manager.setScenario('test-agent', { + status: 'question', + question: 'Which database?', + options: [{ label: 'PostgreSQL' }], + }); + + const agent = await manager.spawn({ name: 'test-agent', taskId: 'task-1', prompt: 'test' }); + await vi.runAllTimersAsync(); + + const pending = await manager.getPendingQuestion(agent.id); + expect(pending?.question).toBe('Which database?'); + expect(pending?.options).toHaveLength(1); + expect(pending?.options?.[0].label).toBe('PostgreSQL'); + }); + + it('clears pending question after resume', async () => { + manager.setScenario('resume-test', { + status: 'question', + question: 'Need your input', + options: [{ label: 'Option A' }, { label: 'Option B' }], + }); + + const agent = await manager.spawn({ name: 'resume-test', taskId: 'task-1', prompt: 'test' }); + await vi.runAllTimersAsync(); + + // Verify question is pending + const pendingBefore = await manager.getPendingQuestion(agent.id); + expect(pendingBefore).not.toBeNull(); + expect(pendingBefore?.question).toBe('Need your input'); + + // Resume the agent + await manager.resume(agent.id, 'Option A'); + + // Pending question should be cleared + const pendingAfter = await manager.getPendingQuestion(agent.id); + expect(pendingAfter).toBeNull(); + }); + + it('returns null for non-existent agent pending question', async () => { + const pending = await manager.getPendingQuestion('non-existent-id'); + expect(pending).toBeNull(); + }); + + it('returns null for agent not in waiting state', async () => { + const agent = await manager.spawn({ name: 'running-agent', taskId: 'task-1', prompt: 'test' }); + + // Agent is running, not waiting + const pending = await manager.getPendingQuestion(agent.id); + expect(pending).toBeNull(); + }); + }); }); diff --git a/src/test/e2e/edge-cases.test.ts b/src/test/e2e/edge-cases.test.ts index e7f1b6f..0667ca9 100644 --- a/src/test/e2e/edge-cases.test.ts +++ b/src/test/e2e/edge-cases.test.ts @@ -52,10 +52,10 @@ describe('E2E Edge Cases', () => { }); await vi.runAllTimersAsync(); - // Set crash scenario BEFORE dispatch + // Set unrecoverable_error scenario BEFORE dispatch harness.setAgentScenario(`agent-${taskAId.slice(0, 6)}`, { - outcome: 'crash', - message: 'Token limit exceeded', + status: 'unrecoverable_error', + error: 'Token limit exceeded', }); await harness.dispatchManager.queue(taskAId); @@ -91,10 +91,10 @@ describe('E2E Edge Cases', () => { }); await vi.runAllTimersAsync(); - // Set crash scenario + // Set unrecoverable_error scenario harness.setAgentScenario(`agent-${taskAId.slice(0, 6)}`, { - outcome: 'crash', - message: 'Token limit exceeded', + status: 'unrecoverable_error', + error: 'Token limit exceeded', }); await harness.dispatchManager.queue(taskAId); @@ -119,10 +119,10 @@ describe('E2E Edge Cases', () => { }); await vi.runAllTimersAsync(); - // Set crash scenario + // Set unrecoverable_error scenario harness.setAgentScenario(`agent-${taskAId.slice(0, 6)}`, { - outcome: 'crash', - message: 'Out of memory', + status: 'unrecoverable_error', + error: 'Out of memory', }); await harness.dispatchManager.queue(taskAId); @@ -151,9 +151,9 @@ describe('E2E Edge Cases', () => { }); await vi.runAllTimersAsync(); - // Set waiting scenario + // Set question scenario harness.setAgentScenario(`agent-${taskAId.slice(0, 6)}`, { - outcome: 'waiting_for_input', + status: 'question', question: 'Which database should I use?', }); @@ -184,9 +184,9 @@ describe('E2E Edge Cases', () => { }); await vi.runAllTimersAsync(); - // Set waiting scenario + // Set question scenario harness.setAgentScenario(`agent-${taskAId.slice(0, 6)}`, { - outcome: 'waiting_for_input', + status: 'question', question: 'Which database should I use?', }); @@ -234,9 +234,9 @@ describe('E2E Edge Cases', () => { }); await vi.runAllTimersAsync(); - // Set waiting scenario + // Set question scenario harness.setAgentScenario(`agent-${taskAId.slice(0, 6)}`, { - outcome: 'waiting_for_input', + status: 'question', question: 'Which database should I use?', }); diff --git a/src/test/harness.test.ts b/src/test/harness.test.ts index 77e4653..fb270af 100644 --- a/src/test/harness.test.ts +++ b/src/test/harness.test.ts @@ -267,11 +267,11 @@ describe('TestHarness', () => { await vi.runAllTimersAsync(); harness.clearEvents(); - // Set crash scenario for the agent that will be spawned + // Set unrecoverable_error scenario for the agent that will be spawned harness.setAgentScenario(`agent-${taskAId.slice(0, 6)}`, { - outcome: 'crash', + status: 'unrecoverable_error', delay: 0, - message: 'Test crash', + error: 'Test crash', }); // Queue and dispatch