mirror of
https://github.com/thedotmack/claude-mem
synced 2026-04-25 17:15:04 +02:00
Reduce timeouts to eliminate 10-30s startup delay when worker is dead (common on WSL2 after hibernate). Add stale PID detection, graceful error handling across all handlers, and error classification that distinguishes worker unavailability from handler bugs. - HEALTH_CHECK 30s→3s, new POST_SPAWN_WAIT (5s), PORT_IN_USE_WAIT (3s) - isProcessAlive() with EPERM handling, cleanStalePidFile() - getPluginVersion() try-catch for shutdown race (#1042) - isWorkerUnavailableError: transport+5xx+429→exit 0, 4xx→exit 2 - No-op handler for unknown event types (#984) - Wrap all handler fetch calls in try-catch for graceful degradation - CLAUDE_MEM_HEALTH_TIMEOUT_MS env var override with validation
165 lines
6.4 KiB
TypeScript
165 lines
6.4 KiB
TypeScript
/**
|
|
* Tests for hook-command error classifier
|
|
*
|
|
* Validates that isWorkerUnavailableError correctly distinguishes between:
|
|
* - Transport failures (ECONNREFUSED, etc.) → true (graceful degradation)
|
|
* - Server errors (5xx) → true (graceful degradation)
|
|
* - Client errors (4xx) → false (handler bug, blocking)
|
|
* - Programming errors (TypeError, etc.) → false (code bug, blocking)
|
|
*/
|
|
import { describe, it, expect } from 'bun:test';
|
|
import { isWorkerUnavailableError } from '../src/cli/hook-command.js';
|
|
|
|
describe('isWorkerUnavailableError', () => {
|
|
describe('transport failures → true (graceful)', () => {
|
|
it('should classify ECONNREFUSED as worker unavailable', () => {
|
|
const error = new Error('connect ECONNREFUSED 127.0.0.1:37777');
|
|
expect(isWorkerUnavailableError(error)).toBe(true);
|
|
});
|
|
|
|
it('should classify ECONNRESET as worker unavailable', () => {
|
|
const error = new Error('socket hang up ECONNRESET');
|
|
expect(isWorkerUnavailableError(error)).toBe(true);
|
|
});
|
|
|
|
it('should classify EPIPE as worker unavailable', () => {
|
|
const error = new Error('write EPIPE');
|
|
expect(isWorkerUnavailableError(error)).toBe(true);
|
|
});
|
|
|
|
it('should classify ETIMEDOUT as worker unavailable', () => {
|
|
const error = new Error('connect ETIMEDOUT 127.0.0.1:37777');
|
|
expect(isWorkerUnavailableError(error)).toBe(true);
|
|
});
|
|
|
|
it('should classify "fetch failed" as worker unavailable', () => {
|
|
const error = new TypeError('fetch failed');
|
|
expect(isWorkerUnavailableError(error)).toBe(true);
|
|
});
|
|
|
|
it('should classify "Unable to connect" as worker unavailable', () => {
|
|
const error = new Error('Unable to connect to server');
|
|
expect(isWorkerUnavailableError(error)).toBe(true);
|
|
});
|
|
|
|
it('should classify ENOTFOUND as worker unavailable', () => {
|
|
const error = new Error('getaddrinfo ENOTFOUND localhost');
|
|
expect(isWorkerUnavailableError(error)).toBe(true);
|
|
});
|
|
|
|
it('should classify "socket hang up" as worker unavailable', () => {
|
|
const error = new Error('socket hang up');
|
|
expect(isWorkerUnavailableError(error)).toBe(true);
|
|
});
|
|
|
|
it('should classify ECONNABORTED as worker unavailable', () => {
|
|
const error = new Error('ECONNABORTED');
|
|
expect(isWorkerUnavailableError(error)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('timeout errors → true (graceful)', () => {
|
|
it('should classify "timed out" as worker unavailable', () => {
|
|
const error = new Error('Request timed out after 3000ms');
|
|
expect(isWorkerUnavailableError(error)).toBe(true);
|
|
});
|
|
|
|
it('should classify "timeout" as worker unavailable', () => {
|
|
const error = new Error('Connection timeout');
|
|
expect(isWorkerUnavailableError(error)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('HTTP 5xx server errors → true (graceful)', () => {
|
|
it('should classify 500 status as worker unavailable', () => {
|
|
const error = new Error('Context generation failed: 500');
|
|
expect(isWorkerUnavailableError(error)).toBe(true);
|
|
});
|
|
|
|
it('should classify 502 status as worker unavailable', () => {
|
|
const error = new Error('Observation storage failed: 502');
|
|
expect(isWorkerUnavailableError(error)).toBe(true);
|
|
});
|
|
|
|
it('should classify 503 status as worker unavailable', () => {
|
|
const error = new Error('Request failed: 503');
|
|
expect(isWorkerUnavailableError(error)).toBe(true);
|
|
});
|
|
|
|
it('should classify "status: 500" format as worker unavailable', () => {
|
|
const error = new Error('HTTP error status: 500');
|
|
expect(isWorkerUnavailableError(error)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('HTTP 429 rate limit → true (graceful)', () => {
|
|
it('should classify 429 as worker unavailable (rate limit is transient)', () => {
|
|
const error = new Error('Request failed: 429');
|
|
expect(isWorkerUnavailableError(error)).toBe(true);
|
|
});
|
|
|
|
it('should classify "status: 429" format as worker unavailable', () => {
|
|
const error = new Error('HTTP error status: 429');
|
|
expect(isWorkerUnavailableError(error)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('HTTP 4xx client errors → false (blocking)', () => {
|
|
it('should NOT classify 400 Bad Request as worker unavailable', () => {
|
|
const error = new Error('Request failed: 400');
|
|
expect(isWorkerUnavailableError(error)).toBe(false);
|
|
});
|
|
|
|
it('should NOT classify 404 Not Found as worker unavailable', () => {
|
|
const error = new Error('Observation storage failed: 404');
|
|
expect(isWorkerUnavailableError(error)).toBe(false);
|
|
});
|
|
|
|
it('should NOT classify 422 Validation Error as worker unavailable', () => {
|
|
const error = new Error('Request failed: 422');
|
|
expect(isWorkerUnavailableError(error)).toBe(false);
|
|
});
|
|
|
|
it('should NOT classify "status: 400" format as worker unavailable', () => {
|
|
const error = new Error('HTTP error status: 400');
|
|
expect(isWorkerUnavailableError(error)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('programming errors → false (blocking)', () => {
|
|
it('should NOT classify TypeError as worker unavailable', () => {
|
|
const error = new TypeError('Cannot read properties of undefined');
|
|
// Note: TypeError with "fetch failed" IS classified as unavailable (transport layer)
|
|
// But generic TypeErrors are NOT
|
|
expect(isWorkerUnavailableError(new TypeError('Cannot read properties of undefined'))).toBe(false);
|
|
});
|
|
|
|
it('should NOT classify ReferenceError as worker unavailable', () => {
|
|
const error = new ReferenceError('foo is not defined');
|
|
expect(isWorkerUnavailableError(error)).toBe(false);
|
|
});
|
|
|
|
it('should NOT classify SyntaxError as worker unavailable', () => {
|
|
const error = new SyntaxError('Unexpected token');
|
|
expect(isWorkerUnavailableError(error)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('unknown errors → false (blocking, conservative)', () => {
|
|
it('should NOT classify generic Error as worker unavailable', () => {
|
|
const error = new Error('Something unexpected happened');
|
|
expect(isWorkerUnavailableError(error)).toBe(false);
|
|
});
|
|
|
|
it('should handle string errors', () => {
|
|
expect(isWorkerUnavailableError('ECONNREFUSED')).toBe(true);
|
|
expect(isWorkerUnavailableError('random error')).toBe(false);
|
|
});
|
|
|
|
it('should handle null/undefined errors', () => {
|
|
expect(isWorkerUnavailableError(null)).toBe(false);
|
|
expect(isWorkerUnavailableError(undefined)).toBe(false);
|
|
});
|
|
});
|
|
});
|