/** * AgentLifecycleController Tests — Regression coverage for event emissions. */ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { AgentLifecycleController } from './controller.js'; import type { AgentRepository } from '../../db/repositories/agent-repository.js'; import type { AccountRepository } from '../../db/repositories/account-repository.js'; import type { SignalManager } from './signal-manager.js'; import type { RetryPolicy } from './retry-policy.js'; import type { AgentErrorAnalyzer } from './error-analyzer.js'; import type { ProcessManager } from '../process-manager.js'; import type { CleanupManager } from '../cleanup-manager.js'; import type { CleanupStrategy } from './cleanup-strategy.js'; import type { EventBus, AgentAccountSwitchedEvent } from '../../events/types.js'; function makeController(overrides: { repository?: Partial; accountRepository?: Partial; eventBus?: EventBus; }): AgentLifecycleController { const signalManager: SignalManager = { clearSignal: vi.fn(), checkSignalExists: vi.fn(), readSignal: vi.fn(), waitForSignal: vi.fn(), validateSignalFile: vi.fn(), }; const retryPolicy: RetryPolicy = { maxAttempts: 3, backoffMs: [1000, 2000, 4000], shouldRetry: vi.fn().mockReturnValue(false), getRetryDelay: vi.fn().mockReturnValue(0), }; const errorAnalyzer = { analyzeError: vi.fn() } as unknown as AgentErrorAnalyzer; const processManager = { getAgentWorkdir: vi.fn() } as unknown as ProcessManager; const cleanupManager = {} as unknown as CleanupManager; const cleanupStrategy = { shouldCleanup: vi.fn(), executeCleanup: vi.fn(), } as unknown as CleanupStrategy; return new AgentLifecycleController( signalManager, retryPolicy, errorAnalyzer, processManager, overrides.repository as AgentRepository, cleanupManager, cleanupStrategy, overrides.accountRepository as AccountRepository | undefined, false, ); } describe('AgentLifecycleController', () => { describe('handleAccountExhaustion', () => { it('emits agent:account_switched with correct payload when new account is available', async () => { const emittedEvents: AgentAccountSwitchedEvent[] = []; const eventBus: EventBus = { emit: vi.fn((event) => { emittedEvents.push(event as AgentAccountSwitchedEvent); }), on: vi.fn(), off: vi.fn(), once: vi.fn(), }; const agentRecord = { id: 'agent-1', name: 'test-agent', accountId: 'old-account-id', provider: 'claude', }; const newAccount = { id: 'new-account-id' }; const repository: Partial = { findById: vi.fn().mockResolvedValue(agentRecord), }; const accountRepository: Partial = { markExhausted: vi.fn().mockResolvedValue(agentRecord), findNextAvailable: vi.fn().mockResolvedValue(newAccount), }; const controller = makeController({ repository, accountRepository, eventBus }); // Call private method via any-cast await (controller as any).handleAccountExhaustion('agent-1'); const accountSwitchedEvents = emittedEvents.filter( (e) => e.type === 'agent:account_switched' ); expect(accountSwitchedEvents).toHaveLength(1); const event = accountSwitchedEvents[0]; expect(event.type).toBe('agent:account_switched'); expect(event.payload.agentId).toBe('agent-1'); expect(event.payload.name).toBe('test-agent'); expect(event.payload.previousAccountId).toBe('old-account-id'); expect(event.payload.newAccountId).toBe('new-account-id'); expect(event.payload.reason).toBe('account_exhausted'); }); it('does not emit agent:account_switched when no new account is available', async () => { const eventBus: EventBus = { emit: vi.fn(), on: vi.fn(), off: vi.fn(), once: vi.fn(), }; const agentRecord = { id: 'agent-2', name: 'test-agent-2', accountId: 'old-account-id', provider: 'claude', }; const repository: Partial = { findById: vi.fn().mockResolvedValue(agentRecord), }; const accountRepository: Partial = { markExhausted: vi.fn().mockResolvedValue(agentRecord), findNextAvailable: vi.fn().mockResolvedValue(null), }; const controller = makeController({ repository, accountRepository, eventBus }); await (controller as any).handleAccountExhaustion('agent-2'); expect(eventBus.emit).not.toHaveBeenCalled(); }); it('does not emit when agent has no accountId', async () => { const eventBus: EventBus = { emit: vi.fn(), on: vi.fn(), off: vi.fn(), once: vi.fn(), }; const repository: Partial = { findById: vi.fn().mockResolvedValue({ id: 'agent-3', name: 'x', accountId: null }), }; const accountRepository: Partial = { markExhausted: vi.fn(), findNextAvailable: vi.fn(), }; const controller = makeController({ repository, accountRepository, eventBus }); await (controller as any).handleAccountExhaustion('agent-3'); expect(eventBus.emit).not.toHaveBeenCalled(); }); }); });