refactor: Standardize fake timer usage across E2E tests

- Add withFakeTimers(fn) helper to TestHarness for scoped timer control
- Replace all vi.runAllTimersAsync() with harness.advanceTimers() in E2E
  and harness tests (37 call sites across 5 files)
- Keep vi.useFakeTimers() per-test activation pattern (intentional)
This commit is contained in:
Lukas May
2026-03-02 12:08:24 +09:00
parent dcb855ede1
commit a1366efe4d
6 changed files with 77 additions and 61 deletions

View File

@@ -50,7 +50,7 @@ describe('E2E Edge Cases', () => {
taskId: 'placeholder',
prompt: 'placeholder',
});
await vi.runAllTimersAsync();
await harness.advanceTimers();
// Set error scenario BEFORE dispatch
harness.setAgentScenario(`agent-${taskAId.slice(0, 6)}`, {
@@ -62,7 +62,7 @@ describe('E2E Edge Cases', () => {
harness.clearEvents();
await harness.dispatchManager.dispatchNext();
await vi.runAllTimersAsync();
await harness.advanceTimers();
// Verify: agent:spawned event emitted
const spawnedEvents = harness.getEventsByType('agent:spawned');
@@ -89,7 +89,7 @@ describe('E2E Edge Cases', () => {
taskId: 'placeholder',
prompt: 'placeholder',
});
await vi.runAllTimersAsync();
await harness.advanceTimers();
// Set error scenario
harness.setAgentScenario(`agent-${taskAId.slice(0, 6)}`, {
@@ -99,7 +99,7 @@ describe('E2E Edge Cases', () => {
await harness.dispatchManager.queue(taskAId);
await harness.dispatchManager.dispatchNext();
await vi.runAllTimersAsync();
await harness.advanceTimers();
// Task status should be 'in_progress' (not 'completed')
const task = await harness.taskRepository.findById(taskAId);
@@ -117,7 +117,7 @@ describe('E2E Edge Cases', () => {
taskId: 'placeholder',
prompt: 'placeholder',
});
await vi.runAllTimersAsync();
await harness.advanceTimers();
// Set error scenario
harness.setAgentScenario(`agent-${taskAId.slice(0, 6)}`, {
@@ -127,7 +127,7 @@ describe('E2E Edge Cases', () => {
await harness.dispatchManager.queue(taskAId);
const dispatchResult = await harness.dispatchManager.dispatchNext();
await vi.runAllTimersAsync();
await harness.advanceTimers();
// Get agent result - should have error
const agentResult = await harness.agentManager.getResult(dispatchResult.agentId!);
@@ -149,7 +149,7 @@ describe('E2E Edge Cases', () => {
taskId: 'placeholder',
prompt: 'placeholder',
});
await vi.runAllTimersAsync();
await harness.advanceTimers();
// Set questions scenario
harness.setAgentScenario(`agent-${taskAId.slice(0, 6)}`, {
@@ -161,7 +161,7 @@ describe('E2E Edge Cases', () => {
harness.clearEvents();
await harness.dispatchManager.dispatchNext();
await vi.runAllTimersAsync();
await harness.advanceTimers();
// Verify: agent:waiting event emitted
const waitingEvents = harness.getEventsByType('agent:waiting');
@@ -182,7 +182,7 @@ describe('E2E Edge Cases', () => {
taskId: 'placeholder',
prompt: 'placeholder',
});
await vi.runAllTimersAsync();
await harness.advanceTimers();
// Set questions scenario
harness.setAgentScenario(`agent-${taskAId.slice(0, 6)}`, {
@@ -194,7 +194,7 @@ describe('E2E Edge Cases', () => {
harness.clearEvents();
const dispatchResult = await harness.dispatchManager.dispatchNext();
await vi.runAllTimersAsync();
await harness.advanceTimers();
// Verify agent is in waiting_for_input status
const agent = await harness.agentManager.get(dispatchResult.agentId!);
@@ -205,7 +205,7 @@ describe('E2E Edge Cases', () => {
// Resume agent with answers map
await harness.agentManager.resume(dispatchResult.agentId!, { q1: 'PostgreSQL' });
await vi.runAllTimersAsync();
await harness.advanceTimers();
// Verify: agent:resumed event emitted
const resumedEvents = harness.getEventsByType('agent:resumed');
@@ -232,7 +232,7 @@ describe('E2E Edge Cases', () => {
taskId: 'placeholder',
prompt: 'placeholder',
});
await vi.runAllTimersAsync();
await harness.advanceTimers();
// Set questions scenario
harness.setAgentScenario(`agent-${taskAId.slice(0, 6)}`, {
@@ -247,7 +247,7 @@ describe('E2E Edge Cases', () => {
let agent = await harness.agentManager.get(dispatchResult.agentId!);
expect(agent?.status).toBe('running');
await vi.runAllTimersAsync();
await harness.advanceTimers();
// After scenario completes: waiting_for_input
agent = await harness.agentManager.get(dispatchResult.agentId!);
@@ -260,7 +260,7 @@ describe('E2E Edge Cases', () => {
agent = await harness.agentManager.get(dispatchResult.agentId!);
expect(agent?.status).toBe('running');
await vi.runAllTimersAsync();
await harness.advanceTimers();
// After completion: idle
agent = await harness.agentManager.get(dispatchResult.agentId!);
@@ -311,7 +311,7 @@ describe('E2E Edge Cases', () => {
taskId: 'placeholder',
prompt: 'placeholder',
});
await vi.runAllTimersAsync();
await harness.advanceTimers();
// Queue Task A and block it
await harness.dispatchManager.queue(taskAId);

View File

@@ -304,7 +304,7 @@ describe('E2E Extended Scenarios', () => {
taskId: 'placeholder-3',
prompt: 'placeholder',
});
await vi.runAllTimersAsync();
await harness.advanceTimers();
harness.clearEvents();
// Queue all 4 tasks
@@ -327,8 +327,8 @@ describe('E2E Extended Scenarios', () => {
const dispatchedIds = [result1.agentId, result2.agentId, result3.agentId];
expect(new Set(dispatchedIds).size).toBe(3);
// Use vi.runAllTimersAsync() to complete all 3 agents
await vi.runAllTimersAsync();
// Advance timers to complete all 3 agents
await harness.advanceTimers();
// Verify: 3 agent:stopped events
const stoppedEvents = harness.getEventsByType('agent:stopped');
@@ -343,7 +343,7 @@ describe('E2E Extended Scenarios', () => {
const result4 = await harness.dispatchManager.dispatchNext();
expect(result4.success).toBe(true);
await vi.runAllTimersAsync();
await harness.advanceTimers();
await harness.dispatchManager.completeTask(result4.taskId!);
// Verify: all 4 tasks completed in database
@@ -483,7 +483,7 @@ describe('E2E Extended Scenarios', () => {
taskId: 'placeholder-2',
prompt: 'placeholder',
});
await vi.runAllTimersAsync();
await harness.advanceTimers();
// Set Task X to succeed, Task Y to crash
harness.setAgentDone(`agent-${taskXId.slice(0, 6)}`, 'Task X completed');
@@ -503,7 +503,7 @@ describe('E2E Extended Scenarios', () => {
expect(result2.success).toBe(true);
// Run timers to complete agents
await vi.runAllTimersAsync();
await harness.advanceTimers();
// Verify: one agent:stopped, one agent:crashed
const stoppedEvents = harness.getEventsByType('agent:stopped');

View File

@@ -42,7 +42,7 @@ describe('E2E Happy Path', () => {
taskId: 'placeholder',
prompt: 'placeholder',
});
await vi.runAllTimersAsync();
await harness.advanceTimers();
harness.clearEvents();
// Step 1: Queue task
@@ -69,7 +69,7 @@ describe('E2E Happy Path', () => {
expect(spawnedEvents.length).toBe(1);
// Step 3: Wait for agent completion
await vi.runAllTimersAsync();
await harness.advanceTimers();
// Verify agent:stopped event
const stoppedEvents = harness.getEventsByType('agent:stopped');
@@ -102,7 +102,7 @@ describe('E2E Happy Path', () => {
taskId: 'placeholder',
prompt: 'placeholder',
});
await vi.runAllTimersAsync();
await harness.advanceTimers();
harness.clearEvents();
// Queue all three tasks
@@ -130,7 +130,7 @@ describe('E2E Happy Path', () => {
expect(dispatchResult.taskId).toBe(taskAId);
// Wait for agent completion
await vi.runAllTimersAsync();
await harness.advanceTimers();
// Complete Task A
await harness.dispatchManager.completeTask(taskAId);
@@ -172,7 +172,7 @@ describe('E2E Happy Path', () => {
taskId: 'placeholder-2',
prompt: 'placeholder',
});
await vi.runAllTimersAsync();
await harness.advanceTimers();
harness.clearEvents();
// Queue all 4 tasks
@@ -220,7 +220,7 @@ describe('E2E Happy Path', () => {
taskId: 'placeholder',
prompt: 'placeholder',
});
await vi.runAllTimersAsync();
await harness.advanceTimers();
harness.clearEvents();
// Queue and dispatch task
@@ -229,7 +229,7 @@ describe('E2E Happy Path', () => {
expect(dispatchResult.success).toBe(true);
// Wait for agent completion
await vi.runAllTimersAsync();
await harness.advanceTimers();
// Complete task
await harness.dispatchManager.completeTask(taskAId);
@@ -289,7 +289,7 @@ describe('E2E Happy Path', () => {
taskId: 'placeholder',
prompt: 'placeholder',
});
await vi.runAllTimersAsync();
await harness.advanceTimers();
harness.clearEvents();
// Queue all 5 tasks
@@ -318,7 +318,7 @@ describe('E2E Happy Path', () => {
expect(result1.taskId).toBe(task1AId);
// Wait for agent completion
await vi.runAllTimersAsync();
await harness.advanceTimers();
// Complete Task 1A
await harness.dispatchManager.completeTask(task1AId);
@@ -335,7 +335,7 @@ describe('E2E Happy Path', () => {
// Task 1B (high priority among remaining)
const result2 = await harness.dispatchManager.dispatchNext();
expect(result2.success).toBe(true);
await vi.runAllTimersAsync();
await harness.advanceTimers();
await harness.dispatchManager.completeTask(result2.taskId!);
// 3 tasks remain
@@ -345,17 +345,17 @@ describe('E2E Happy Path', () => {
// Continue dispatching remaining tasks
const result3 = await harness.dispatchManager.dispatchNext();
expect(result3.success).toBe(true);
await vi.runAllTimersAsync();
await harness.advanceTimers();
await harness.dispatchManager.completeTask(result3.taskId!);
const result4 = await harness.dispatchManager.dispatchNext();
expect(result4.success).toBe(true);
await vi.runAllTimersAsync();
await harness.advanceTimers();
await harness.dispatchManager.completeTask(result4.taskId!);
const result5 = await harness.dispatchManager.dispatchNext();
expect(result5.success).toBe(true);
await vi.runAllTimersAsync();
await harness.advanceTimers();
await harness.dispatchManager.completeTask(result5.taskId!);
// All tasks completed

View File

@@ -82,7 +82,7 @@ describe('E2E Recovery Scenarios', () => {
taskId: 'placeholder',
prompt: 'placeholder',
});
await vi.runAllTimersAsync();
await harness.advanceTimers();
// Set crash scenario
harness.setAgentError(`agent-${taskAId.slice(0, 6)}`, 'Token limit exceeded');
@@ -90,7 +90,7 @@ describe('E2E Recovery Scenarios', () => {
// Queue and dispatch
await harness.dispatchManager.queue(taskAId);
await harness.dispatchManager.dispatchNext();
await vi.runAllTimersAsync();
await harness.advanceTimers();
// Verify task status is 'in_progress' (not completed, not lost)
let task = await harness.taskRepository.findById(taskAId);
@@ -104,7 +104,7 @@ describe('E2E Recovery Scenarios', () => {
taskId: 'placeholder',
prompt: 'placeholder',
});
await vi.runAllTimersAsync();
await harness.advanceTimers();
// Re-queue the task (it's still in_progress but we can retry)
await harness.dispatchManager.queue(taskAId);
@@ -115,7 +115,7 @@ describe('E2E Recovery Scenarios', () => {
// Clear events and dispatch again
harness.clearEvents();
const dispatchResult = await harness.dispatchManager.dispatchNext();
await vi.runAllTimersAsync();
await harness.advanceTimers();
// Verify: agent completed successfully
expect(dispatchResult.agentId).toBeDefined();
@@ -217,7 +217,7 @@ describe('E2E Recovery Scenarios', () => {
taskId: 'placeholder',
prompt: 'placeholder',
});
await vi.runAllTimersAsync();
await harness.advanceTimers();
// Set questions scenario with options
harness.setAgentQuestions(`agent-${taskAId.slice(0, 6)}`, [
@@ -236,7 +236,7 @@ describe('E2E Recovery Scenarios', () => {
harness.clearEvents();
const dispatchResult = await harness.dispatchManager.dispatchNext();
await vi.runAllTimersAsync();
await harness.advanceTimers();
// Verify: agent:waiting event emitted
const waitingEvents = harness.getEventsByType('agent:waiting');
@@ -248,7 +248,7 @@ describe('E2E Recovery Scenarios', () => {
// Clear and resume with answers map
harness.clearEvents();
await harness.agentManager.resume(dispatchResult.agentId!, { q1: 'PostgreSQL' });
await vi.runAllTimersAsync();
await harness.advanceTimers();
// Verify: resumed and stopped events
const resumedEvents = harness.getEventsByType('agent:resumed');
@@ -273,7 +273,7 @@ describe('E2E Recovery Scenarios', () => {
taskId: 'placeholder',
prompt: 'placeholder',
});
await vi.runAllTimersAsync();
await harness.advanceTimers();
// Set questions scenario with options
harness.setAgentQuestions(`agent-${taskAId.slice(0, 6)}`, [
@@ -291,7 +291,7 @@ describe('E2E Recovery Scenarios', () => {
// Queue and dispatch
await harness.dispatchManager.queue(taskAId);
const dispatchResult = await harness.dispatchManager.dispatchNext();
await vi.runAllTimersAsync();
await harness.advanceTimers();
// Verify: agent:waiting event has questions
const waitingEvents = harness.getEventsByType('agent:waiting');
@@ -326,7 +326,7 @@ describe('E2E Recovery Scenarios', () => {
taskId: 'placeholder',
prompt: 'placeholder',
});
await vi.runAllTimersAsync();
await harness.advanceTimers();
// Set questions scenario
harness.setAgentQuestions(`agent-${taskAId.slice(0, 6)}`, [
@@ -336,7 +336,7 @@ describe('E2E Recovery Scenarios', () => {
// Queue and dispatch
await harness.dispatchManager.queue(taskAId);
const dispatchResult = await harness.dispatchManager.dispatchNext();
await vi.runAllTimersAsync();
await harness.advanceTimers();
// Verify agent is waiting
const agent = await harness.agentManager.get(dispatchResult.agentId!);
@@ -344,7 +344,7 @@ describe('E2E Recovery Scenarios', () => {
// Resume with answers map
await harness.agentManager.resume(dispatchResult.agentId!, { q1: 'PostgreSQL' });
await vi.runAllTimersAsync();
await harness.advanceTimers();
// Verify: agent completed successfully
const agentResult = await harness.agentManager.getResult(dispatchResult.agentId!);
@@ -368,7 +368,7 @@ describe('E2E Recovery Scenarios', () => {
taskId: 'placeholder',
prompt: 'placeholder',
});
await vi.runAllTimersAsync();
await harness.advanceTimers();
// Set questions scenario
harness.setAgentQuestions(`agent-${taskAId.slice(0, 6)}`, [
@@ -383,7 +383,7 @@ describe('E2E Recovery Scenarios', () => {
let agent = await harness.agentManager.get(dispatchResult.agentId!);
expect(agent?.status).toBe('running');
await vi.runAllTimersAsync();
await harness.advanceTimers();
// Phase 2: After scenario completes, waiting_for_input
agent = await harness.agentManager.get(dispatchResult.agentId!);
@@ -400,7 +400,7 @@ describe('E2E Recovery Scenarios', () => {
agent = await harness.agentManager.get(dispatchResult.agentId!);
expect(agent?.status).toBe('running');
await vi.runAllTimersAsync();
await harness.advanceTimers();
// Phase 4: After completion, idle
agent = await harness.agentManager.get(dispatchResult.agentId!);
@@ -422,7 +422,7 @@ describe('E2E Recovery Scenarios', () => {
taskId: 'placeholder',
prompt: 'placeholder',
});
await vi.runAllTimersAsync();
await harness.advanceTimers();
// Setup: agent asks two questions
harness.setAgentQuestions(`agent-${taskAId.slice(0, 6)}`, [
@@ -443,7 +443,7 @@ describe('E2E Recovery Scenarios', () => {
harness.clearEvents();
const dispatchResult = await harness.dispatchManager.dispatchNext();
await vi.runAllTimersAsync();
await harness.advanceTimers();
// Verify: agent:waiting event emitted
const waitingEvents = harness.getEventsByType('agent:waiting');
@@ -465,7 +465,7 @@ describe('E2E Recovery Scenarios', () => {
q1: 'SQLite',
q2: 'Yes',
});
await vi.runAllTimersAsync();
await harness.advanceTimers();
// Verify: agent:resumed event emitted
const resumedEvents = harness.getEventsByType('agent:resumed');

View File

@@ -68,7 +68,7 @@ describe('TestHarness', () => {
prompt: 'test',
});
await vi.runAllTimersAsync();
await harness.advanceTimers();
// Verify questions array format
const pending = await harness.getPendingQuestions(agent.id);
@@ -207,7 +207,7 @@ describe('TestHarness', () => {
prompt: 'placeholder',
});
// Wait for agent to complete and become idle
await vi.runAllTimersAsync();
await harness.advanceTimers();
// Queue the task
await harness.dispatchManager.queue(taskAId);
@@ -219,7 +219,7 @@ describe('TestHarness', () => {
const result = await harness.dispatchManager.dispatchNext();
// Advance timers to trigger mock agent completion
await vi.runAllTimersAsync();
await harness.advanceTimers();
expect(result.success).toBe(true);
expect(result.taskId).toBe(taskAId);
@@ -264,7 +264,7 @@ describe('TestHarness', () => {
taskId: 'placeholder',
prompt: 'placeholder',
});
await vi.runAllTimersAsync();
await harness.advanceTimers();
harness.clearEvents();
// Queue and dispatch
@@ -277,7 +277,7 @@ describe('TestHarness', () => {
expect(spawnedEvents.length).toBe(1);
// Advance timers to trigger completion
await vi.runAllTimersAsync();
await harness.advanceTimers();
// Should have agent:stopped
const stoppedEvents = harness.getEventsByType('agent:stopped');
@@ -295,7 +295,7 @@ describe('TestHarness', () => {
taskId: 'placeholder',
prompt: 'placeholder',
});
await vi.runAllTimersAsync();
await harness.advanceTimers();
harness.clearEvents();
// Set error scenario for the agent that will be spawned
@@ -311,7 +311,7 @@ describe('TestHarness', () => {
await harness.dispatchManager.dispatchNext();
// Advance timers
await vi.runAllTimersAsync();
await harness.advanceTimers();
// Should have agent:crashed
const crashedEvents = harness.getEventsByType('agent:crashed');
@@ -331,7 +331,7 @@ describe('TestHarness', () => {
taskId: 'placeholder',
prompt: 'placeholder',
});
await vi.runAllTimersAsync();
await harness.advanceTimers();
harness.clearEvents();
// Step 1: Queue task
@@ -342,7 +342,7 @@ describe('TestHarness', () => {
expect(dispatchResult.success).toBe(true);
// Advance timers for agent completion
await vi.runAllTimersAsync();
await harness.advanceTimers();
// Clear events for cleaner verification
harness.clearEvents();

View File

@@ -280,6 +280,13 @@ export interface TestHarness {
*/
advanceTimers(): Promise<void>;
/**
* Run a test body with fake timers enabled.
* Activates fake timers before the callback and restores real timers after,
* even if the callback throws.
*/
withFakeTimers(fn: () => Promise<void>): Promise<void>;
// ==========================================================================
// Architect Mode Helpers
// ==========================================================================
@@ -513,6 +520,15 @@ export function createTestHarness(): TestHarness {
// Timer helper - requires vi.useFakeTimers() to be active
advanceTimers: () => vi.runAllTimersAsync(),
withFakeTimers: async (fn: () => Promise<void>) => {
vi.useFakeTimers();
try {
await fn();
} finally {
vi.useRealTimers();
}
},
// ========================================================================
// Architect Mode Helpers
// ========================================================================