/** * Unit Tests — engine/failover-error.js */ import { describe, it, expect } from 'vitest'; const { FailoverError, FailoverReason, classifyError, RETRYABLE_REASONS } = require('../../src/engine/failover-error'); describe('failover-error', () => { describe('FailoverReason enum', () => { it('should have all expected reasons', () => { expect(FailoverReason.TIMEOUT).toBe('timeout'); expect(FailoverReason.RATE_LIMIT).toBe('rate-limit'); expect(FailoverReason.AUTH).toBe('auth'); expect(FailoverReason.NETWORK).toBe('network'); expect(FailoverReason.SPAWN_ERROR).toBe('spawn-error'); expect(FailoverReason.UNKNOWN).toBe('unknown'); }); it('should be frozen', () => { expect(Object.isFrozen(FailoverReason)).toBe(true); }); }); describe('RETRYABLE_REASONS', () => { it('should include timeout', () => { expect(RETRYABLE_REASONS.has('timeout')).toBe(true); }); it('should include rate-limit', () => { expect(RETRYABLE_REASONS.has('rate-limit')).toBe(true); }); it('should not include auth', () => { expect(RETRYABLE_REASONS.has('auth')).toBe(false); }); it('should not include spawn-error', () => { expect(RETRYABLE_REASONS.has('spawn-error')).toBe(false); }); }); describe('classifyError', () => { it('should classify timeout errors', () => { const c = classifyError(new Error('Request timed out')); expect(c.reason).toBe('timeout'); expect(c.retryable).toBe(true); }); it('should classify overall-timeout', () => { const c = classifyError(new Error('overall-timeout exceeded')); expect(c.reason).toBe('timeout'); }); it('should classify no-output-timeout', () => { const c = classifyError(new Error('no-output-timeout reached')); expect(c.reason).toBe('no-output-timeout'); }); it('should classify rate limit errors', () => { const c = classifyError(new Error('Rate limit exceeded, 429')); expect(c.reason).toBe('rate-limit'); expect(c.retryable).toBe(true); expect(c.delayMs).toBeGreaterThan(0); }); it('should classify auth errors as non-retryable', () => { const c = classifyError(new Error('401 Unauthorized access')); expect(c.reason).toBe('auth'); expect(c.retryable).toBe(false); }); it('should classify model not found', () => { const c = classifyError(new Error('model not found')); expect(c.reason).toBe('model-not-found'); expect(c.retryable).toBe(false); }); it('should classify transport closed', () => { const c = classifyError(new Error('transport is closed')); expect(c.reason).toBe('transport-closed'); expect(c.retryable).toBe(true); }); it('should classify spawn errors', () => { const err = new Error('spawn ENOENT'); err.code = 'ENOENT'; const c = classifyError(err); expect(c.reason).toBe('spawn-error'); expect(c.retryable).toBe(false); }); it('should classify network errors', () => { const err = new Error('connect ECONNREFUSED'); err.code = 'ECONNREFUSED'; const c = classifyError(err); expect(c.reason).toBe('network'); expect(c.retryable).toBe(true); }); it('should return unknown for unrecognized errors', () => { const c = classifyError(new Error('Something completely unexpected')); expect(c.reason).toBe('unknown'); expect(c.retryable).toBe(false); }); it('should handle null/undefined error', () => { const c = classifyError(null); expect(c.reason).toBe('unknown'); }); }); describe('FailoverError class', () => { it('should wrap an error with classification', () => { const original = new Error('Request timed out'); const fe = FailoverError.from(original); expect(fe).toBeInstanceOf(FailoverError); expect(fe.reason).toBe('timeout'); expect(fe.retryable).toBe(true); expect(fe.original).toBe(original); expect(fe.message).toBe('Request timed out'); }); it('should not double-wrap', () => { const original = new Error('rate limit'); const fe1 = FailoverError.from(original); const fe2 = FailoverError.from(fe1); expect(fe2).toBe(fe1); // same reference }); it('should have name = FailoverError', () => { const fe = FailoverError.from(new Error('test')); expect(fe.name).toBe('FailoverError'); }); }); });