/** * ErrorAnalyzer Tests — Verify error classification patterns. */ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { AgentErrorAnalyzer } from './error-analyzer.js'; import type { SignalManager } from './signal-manager.js'; describe('AgentErrorAnalyzer', () => { let errorAnalyzer: AgentErrorAnalyzer; let mockSignalManager: SignalManager; beforeEach(() => { mockSignalManager = { clearSignal: vi.fn(), checkSignalExists: vi.fn(), readSignal: vi.fn(), waitForSignal: vi.fn(), validateSignalFile: vi.fn(), }; errorAnalyzer = new AgentErrorAnalyzer(mockSignalManager); }); describe('analyzeError', () => { describe('auth failure detection', () => { it('should detect unauthorized errors', async () => { const error = new Error('Unauthorized access'); const result = await errorAnalyzer.analyzeError(error); expect(result.type).toBe('auth_failure'); expect(result.isTransient).toBe(true); expect(result.requiresAccountSwitch).toBe(false); expect(result.shouldPersistToDB).toBe(true); }); it('should detect invalid token errors', async () => { const error = new Error('Invalid token provided'); const result = await errorAnalyzer.analyzeError(error); expect(result.type).toBe('auth_failure'); expect(result.isTransient).toBe(true); }); it('should detect 401 errors', async () => { const error = new Error('HTTP 401 - Authentication failed'); const result = await errorAnalyzer.analyzeError(error); expect(result.type).toBe('auth_failure'); }); it('should detect auth failures in stderr', async () => { const error = new Error('Process failed'); const stderr = 'Error: Authentication failed - expired token'; const result = await errorAnalyzer.analyzeError(error, null, stderr); expect(result.type).toBe('auth_failure'); }); }); describe('usage limit detection', () => { it('should detect rate limit errors', async () => { const error = new Error('Rate limit exceeded'); const result = await errorAnalyzer.analyzeError(error); expect(result.type).toBe('usage_limit'); expect(result.isTransient).toBe(false); expect(result.requiresAccountSwitch).toBe(true); expect(result.shouldPersistToDB).toBe(true); }); it('should detect quota exceeded errors', async () => { const error = new Error('Quota exceeded for this month'); const result = await errorAnalyzer.analyzeError(error); expect(result.type).toBe('usage_limit'); }); it('should detect 429 errors', async () => { const error = new Error('HTTP 429 - Too many requests'); const result = await errorAnalyzer.analyzeError(error); expect(result.type).toBe('usage_limit'); }); it('should detect usage limits in stderr', async () => { const error = new Error('Request failed'); const stderr = 'API usage limit reached. Try again later.'; const result = await errorAnalyzer.analyzeError(error, null, stderr); expect(result.type).toBe('usage_limit'); }); }); describe('timeout detection', () => { it('should detect timeout errors', async () => { const error = new Error('Request timeout'); const result = await errorAnalyzer.analyzeError(error); expect(result.type).toBe('timeout'); expect(result.isTransient).toBe(true); expect(result.requiresAccountSwitch).toBe(false); }); it('should detect timed out errors', async () => { const error = new Error('Connection timed out'); const result = await errorAnalyzer.analyzeError(error); expect(result.type).toBe('timeout'); }); }); describe('missing signal detection', () => { it('should detect missing signal when process exits successfully', async () => { vi.mocked(mockSignalManager.checkSignalExists).mockResolvedValue(false); const error = new Error('No output'); const result = await errorAnalyzer.analyzeError(error, 0, undefined, '/agent/workdir'); expect(result.type).toBe('missing_signal'); expect(result.isTransient).toBe(true); expect(result.requiresAccountSwitch).toBe(false); expect(result.shouldPersistToDB).toBe(false); expect(mockSignalManager.checkSignalExists).toHaveBeenCalledWith('/agent/workdir'); }); it('should not detect missing signal when signal exists', async () => { vi.mocked(mockSignalManager.checkSignalExists).mockResolvedValue(true); const error = new Error('No output'); const result = await errorAnalyzer.analyzeError(error, 0, undefined, '/agent/workdir'); expect(result.type).toBe('unknown'); }); it('should not detect missing signal for non-zero exit codes', async () => { const error = new Error('Process failed'); const result = await errorAnalyzer.analyzeError(error, 1, undefined, '/agent/workdir'); expect(result.type).toBe('process_crash'); }); }); describe('process crash detection', () => { it('should detect crashes with non-zero exit code', async () => { const error = new Error('Process exited with code 1'); const result = await errorAnalyzer.analyzeError(error, 1); expect(result.type).toBe('process_crash'); expect(result.exitCode).toBe(1); expect(result.shouldPersistToDB).toBe(true); }); it('should detect transient crashes based on exit code', async () => { const error = new Error('Process interrupted'); const result = await errorAnalyzer.analyzeError(error, 130); // SIGINT expect(result.type).toBe('process_crash'); expect(result.isTransient).toBe(true); }); it('should detect signal-based crashes as transient', async () => { const error = new Error('Segmentation fault'); const result = await errorAnalyzer.analyzeError(error, 139); // SIGSEGV (128+11, signal-based) expect(result.type).toBe('process_crash'); expect(result.isTransient).toBe(true); // signal-based exit codes (128-255) are transient }); it('should detect transient patterns in stderr', async () => { const error = new Error('Process failed'); const stderr = 'Network error: connection refused'; const result = await errorAnalyzer.analyzeError(error, 1, stderr); expect(result.type).toBe('process_crash'); expect(result.isTransient).toBe(true); }); }); describe('unknown error handling', () => { it('should classify unrecognized errors as unknown', async () => { const error = new Error('Something very weird happened'); const result = await errorAnalyzer.analyzeError(error); expect(result.type).toBe('unknown'); expect(result.isTransient).toBe(false); expect(result.requiresAccountSwitch).toBe(false); expect(result.shouldPersistToDB).toBe(true); }); it('should handle string errors', async () => { const result = await errorAnalyzer.analyzeError('String error message'); expect(result.type).toBe('unknown'); expect(result.message).toBe('String error message'); }); }); describe('error context preservation', () => { it('should preserve original error object', async () => { const originalError = new Error('Test error'); const result = await errorAnalyzer.analyzeError(originalError); expect(result.originalError).toBe(originalError); }); it('should preserve exit code and signal', async () => { const error = new Error('Process failed'); const result = await errorAnalyzer.analyzeError(error, 42, 'stderr output'); expect(result.exitCode).toBe(42); }); }); }); });