import { describe, expect, test } from 'bun:test' import { AbortError, ClaudeError, MalformedCommandError, ConfigParseError, ShellError, TelemetrySafeError_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, isAbortError, hasExactErrorMessage, toError, errorMessage, getErrnoCode, isENOENT, getErrnoPath, shortErrorStack, isFsInaccessible, classifyAxiosError, } from '../errors' // ─── Error classes ────────────────────────────────────────────────────── describe('ClaudeError', () => { test('sets name to constructor name', () => { const e = new ClaudeError('test') expect(e.name).toBe('ClaudeError') expect(e.message).toBe('test') }) }) describe('AbortError', () => { test('sets name to AbortError', () => { const e = new AbortError('cancelled') expect(e.name).toBe('AbortError') }) }) describe('ConfigParseError', () => { test('stores filePath and defaultConfig', () => { const e = new ConfigParseError('bad', '/tmp/cfg', { x: 1 }) expect(e.filePath).toBe('/tmp/cfg') expect(e.defaultConfig).toEqual({ x: 1 }) }) }) describe('ShellError', () => { test('stores stdout, stderr, code, interrupted', () => { const e = new ShellError('out', 'err', 1, false) expect(e.stdout).toBe('out') expect(e.stderr).toBe('err') expect(e.code).toBe(1) expect(e.interrupted).toBe(false) }) }) describe('TelemetrySafeError', () => { test('uses message as telemetryMessage by default', () => { const e = new TelemetrySafeError_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS( 'msg', ) expect(e.telemetryMessage).toBe('msg') }) test('uses separate telemetryMessage when provided', () => { const e = new TelemetrySafeError_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS( 'full msg', 'safe msg', ) expect(e.message).toBe('full msg') expect(e.telemetryMessage).toBe('safe msg') }) }) // ─── isAbortError ─────────────────────────────────────────────────────── describe('isAbortError', () => { test('returns true for AbortError instance', () => { expect(isAbortError(new AbortError())).toBe(true) }) test('returns true for DOMException-style abort', () => { const e = new Error('aborted') e.name = 'AbortError' expect(isAbortError(e)).toBe(true) }) test('returns false for regular error', () => { expect(isAbortError(new Error('nope'))).toBe(false) }) test('returns false for non-error', () => { expect(isAbortError('string')).toBe(false) expect(isAbortError(null)).toBe(false) }) }) // ─── hasExactErrorMessage ─────────────────────────────────────────────── describe('hasExactErrorMessage', () => { test('returns true for matching message', () => { expect(hasExactErrorMessage(new Error('test'), 'test')).toBe(true) }) test('returns false for different message', () => { expect(hasExactErrorMessage(new Error('a'), 'b')).toBe(false) }) test('returns false for non-Error', () => { expect(hasExactErrorMessage('string', 'string')).toBe(false) }) }) // ─── toError ──────────────────────────────────────────────────────────── describe('toError', () => { test('returns Error as-is', () => { const e = new Error('test') expect(toError(e)).toBe(e) }) test('wraps string in Error', () => { const e = toError('oops') expect(e).toBeInstanceOf(Error) expect(e.message).toBe('oops') }) test('wraps number in Error', () => { expect(toError(42).message).toBe('42') }) }) // ─── errorMessage ─────────────────────────────────────────────────────── describe('errorMessage', () => { test('extracts message from Error', () => { expect(errorMessage(new Error('hello'))).toBe('hello') }) test('stringifies non-Error', () => { expect(errorMessage(42)).toBe('42') expect(errorMessage(null)).toBe('null') }) }) // ─── getErrnoCode / isENOENT / getErrnoPath ──────────────────────────── describe('getErrnoCode', () => { test('extracts code from errno-like error', () => { const e = Object.assign(new Error(), { code: 'ENOENT' }) expect(getErrnoCode(e)).toBe('ENOENT') }) test('returns undefined for no code', () => { expect(getErrnoCode(new Error())).toBeUndefined() }) test('returns undefined for non-string code', () => { expect(getErrnoCode({ code: 123 })).toBeUndefined() }) test('returns undefined for non-object', () => { expect(getErrnoCode(null)).toBeUndefined() expect(getErrnoCode('string')).toBeUndefined() }) }) describe('isENOENT', () => { test('returns true for ENOENT', () => { expect(isENOENT(Object.assign(new Error(), { code: 'ENOENT' }))).toBe(true) }) test('returns false for other codes', () => { expect(isENOENT(Object.assign(new Error(), { code: 'EACCES' }))).toBe(false) }) }) describe('getErrnoPath', () => { test('extracts path from errno error', () => { const e = Object.assign(new Error(), { path: '/tmp/file' }) expect(getErrnoPath(e)).toBe('/tmp/file') }) test('returns undefined when no path', () => { expect(getErrnoPath(new Error())).toBeUndefined() }) }) // ─── shortErrorStack ──────────────────────────────────────────────────── describe('shortErrorStack', () => { test('returns string for non-Error', () => { expect(shortErrorStack('oops')).toBe('oops') }) test('returns message when no stack', () => { const e = new Error('test') e.stack = undefined expect(shortErrorStack(e)).toBe('test') }) test('truncates long stacks', () => { const e = new Error('test') const frames = Array.from({ length: 20 }, (_, i) => ` at frame${i}`) e.stack = `Error: test\n${frames.join('\n')}` const result = shortErrorStack(e, 3) const lines = result.split('\n') expect(lines).toHaveLength(4) // header + 3 frames }) test('preserves short stacks', () => { const e = new Error('test') e.stack = 'Error: test\n at frame1\n at frame2' expect(shortErrorStack(e, 5)).toBe(e.stack) }) }) // ─── isFsInaccessible ────────────────────────────────────────────────── describe('isFsInaccessible', () => { test('returns true for ENOENT', () => { expect( isFsInaccessible(Object.assign(new Error(), { code: 'ENOENT' })), ).toBe(true) }) test('returns true for EACCES', () => { expect( isFsInaccessible(Object.assign(new Error(), { code: 'EACCES' })), ).toBe(true) }) test('returns true for EPERM', () => { expect( isFsInaccessible(Object.assign(new Error(), { code: 'EPERM' })), ).toBe(true) }) test('returns true for ENOTDIR', () => { expect( isFsInaccessible(Object.assign(new Error(), { code: 'ENOTDIR' })), ).toBe(true) }) test('returns true for ELOOP', () => { expect( isFsInaccessible(Object.assign(new Error(), { code: 'ELOOP' })), ).toBe(true) }) test('returns false for other codes', () => { expect( isFsInaccessible(Object.assign(new Error(), { code: 'EEXIST' })), ).toBe(false) }) }) // ─── classifyAxiosError ───────────────────────────────────────────────── describe('classifyAxiosError', () => { test("returns 'other' for non-axios error", () => { expect(classifyAxiosError(new Error('test')).kind).toBe('other') }) test("returns 'auth' for 401", () => { const e = { isAxiosError: true, response: { status: 401 }, message: 'unauth', } expect(classifyAxiosError(e).kind).toBe('auth') }) test("returns 'auth' for 403", () => { const e = { isAxiosError: true, response: { status: 403 }, message: 'forbidden', } expect(classifyAxiosError(e).kind).toBe('auth') }) test("returns 'timeout' for ECONNABORTED", () => { const e = { isAxiosError: true, code: 'ECONNABORTED', message: 'timeout' } expect(classifyAxiosError(e).kind).toBe('timeout') }) test("returns 'network' for ECONNREFUSED", () => { const e = { isAxiosError: true, code: 'ECONNREFUSED', message: 'refused' } expect(classifyAxiosError(e).kind).toBe('network') }) test("returns 'network' for ENOTFOUND", () => { const e = { isAxiosError: true, code: 'ENOTFOUND', message: 'nope' } expect(classifyAxiosError(e).kind).toBe('network') }) test("returns 'http' for other axios errors", () => { const e = { isAxiosError: true, response: { status: 500 }, message: 'err' } const result = classifyAxiosError(e) expect(result.kind).toBe('http') expect(result.status).toBe(500) }) test("returns 'other' for null", () => { expect(classifyAxiosError(null).kind).toBe('other') }) })