import { describe, it, expect, vi } from 'vitest'; // Mock the logger before importing the module under test. vi.mock('../src/utils/logger.js', () => ({ logger: { error: vi.fn(), warn: vi.fn(), info: vi.fn(), debug: vi.fn(), child: vi.fn(() => ({ error: vi.fn(), warn: vi.fn(), info: vi.fn(), debug: vi.fn(), })), }, })); import { classifyError, sanitizeErrorMessage, withErrorHandling, ErrorCategory, } from '../src/utils/errors.js'; // --------------------------------------------------------------------------- // classifyError // --------------------------------------------------------------------------- describe('classifyError', () => { it('returns TIMEOUT when error name is "TimeoutError"', () => { const err = new Error('something happened'); err.name = 'TimeoutError'; expect(classifyError(err)).toBe(ErrorCategory.TIMEOUT); }); it('returns TIMEOUT when message contains "timeout"', () => { const err = new Error('Connection timeout after 30s'); expect(classifyError(err)).toBe(ErrorCategory.TIMEOUT); }); it('returns NETWORK when message contains "net::err_"', () => { const err = new Error('net::err_connection_refused'); expect(classifyError(err)).toBe(ErrorCategory.NETWORK); }); it('returns AUTH_REQUIRED when message contains "login"', () => { const err = new Error('Please login to continue'); expect(classifyError(err)).toBe(ErrorCategory.AUTH_REQUIRED); }); it('returns AUTH_REQUIRED when message contains Chinese login word', () => { const err = new Error('请先登录'); expect(classifyError(err)).toBe(ErrorCategory.AUTH_REQUIRED); }); it('returns SELECTOR_NOT_FOUND when message contains "waiting for selector"', () => { const err = new Error('Timeout waiting for selector "#submit-btn"'); expect(classifyError(err)).toBe(ErrorCategory.SELECTOR_NOT_FOUND); }); it('returns INTERNAL for unrecognised errors', () => { const err = new Error('Something unexpected happened'); expect(classifyError(err)).toBe(ErrorCategory.INTERNAL); }); }); // --------------------------------------------------------------------------- // sanitizeErrorMessage // --------------------------------------------------------------------------- describe('sanitizeErrorMessage', () => { it('replaces absolute file-system paths with [path]', () => { const msg = 'Failed to read /home/user/data/secrets.json'; const result = sanitizeErrorMessage(msg); expect(result).toContain('[path]'); expect(result).not.toContain('/home/user/data/secrets.json'); }); it('replaces URLs with [url]', () => { const msg = 'Fetch failed for https://api.example.com/v1/token'; const result = sanitizeErrorMessage(msg); expect(result).toContain('[url]'); expect(result).not.toContain('https://api.example.com'); }); it('replaces long hex strings (>=32 chars) with [hash]', () => { const hex = 'a'.repeat(32); const msg = `Invalid session id: ${hex}`; const result = sanitizeErrorMessage(msg); expect(result).toContain('[hash]'); expect(result).not.toContain(hex); }); it('truncates messages longer than 200 characters', () => { const msg = 'x'.repeat(300); const result = sanitizeErrorMessage(msg); expect(result.length).toBe(200); }); it('leaves short plain messages unchanged', () => { const msg = 'Something went wrong'; expect(sanitizeErrorMessage(msg)).toBe(msg); }); }); // --------------------------------------------------------------------------- // withErrorHandling // --------------------------------------------------------------------------- describe('withErrorHandling', () => { it('passes through successful results', async () => { const expected = { content: [{ type: 'text' as const, text: 'ok' }], }; const result = await withErrorHandling('test_tool', async () => expected); expect(result).toEqual(expected); expect(result.isError).toBeUndefined(); }); it('returns isError:true with classified error JSON on failure', async () => { const result = await withErrorHandling('publish_post', async () => { throw new Error('Connection timeout after 30s'); }); expect(result.isError).toBe(true); expect(result.content).toHaveLength(1); const payload = JSON.parse(result.content[0]!.text); expect(payload.tool).toBe('publish_post'); expect(payload.error).toBe(ErrorCategory.TIMEOUT); expect(typeof payload.message).toBe('string'); }); it('wraps non-Error throws into an Error', async () => { const result = await withErrorHandling('my_tool', async () => { throw 'raw string error'; }); expect(result.isError).toBe(true); const payload = JSON.parse(result.content[0]!.text); expect(payload.tool).toBe('my_tool'); expect(payload.error).toBe(ErrorCategory.INTERNAL); expect(payload.message).toContain('raw string error'); }); });