failover-error.test.js 4.39 KB
/**
 * 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');
    });
  });
});