Files
social-mcp/test/errors.test.ts
T

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');
});
});