152 lines
5.0 KiB
TypeScript
152 lines
5.0 KiB
TypeScript
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.success).toBe(false);
|
|
expect(payload.error.tool).toBe('publish_post');
|
|
expect(payload.error.code).toBe(ErrorCategory.TIMEOUT);
|
|
expect(typeof payload.error.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.success).toBe(false);
|
|
expect(payload.error.tool).toBe('my_tool');
|
|
expect(payload.error.code).toBe(ErrorCategory.INTERNAL);
|
|
expect(payload.error.message).toContain('raw string error');
|
|
});
|
|
});
|