Move src/ → apps/server/ and packages/web/ → apps/web/ to adopt standard monorepo conventions (apps/ for runnable apps, packages/ for reusable libraries). Update all config files, shared package imports, test fixtures, and documentation to reflect new paths. Key fixes: - Update workspace config to ["apps/*", "packages/*"] - Update tsconfig.json rootDir/include for apps/server/ - Add apps/web/** to vitest exclude list - Update drizzle.config.ts schema path - Fix ensure-schema.ts migration path detection (3 levels up in dev, 2 levels up in dist) - Fix tests/integration/cli-server.test.ts import paths - Update packages/shared imports to apps/server/ paths - Update all docs/ files with new paths
214 lines
7.8 KiB
TypeScript
214 lines
7.8 KiB
TypeScript
/**
|
|
* 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);
|
|
});
|
|
});
|
|
});
|
|
}); |