重构为Monorepo:拆分xhs/xhh应用与core包并完成双服务部署改造
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
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 PLATFORM_ERROR when message contains "net::err_"', () => {
|
||||
const err = new Error('net::err_connection_refused');
|
||||
expect(classifyError(err)).toBe(ErrorCategory.PLATFORM_ERROR);
|
||||
});
|
||||
|
||||
it('returns CAPTCHA_REQUIRED when message contains captcha keyword', () => {
|
||||
const err = new Error('show_captcha');
|
||||
expect(classifyError(err)).toBe(ErrorCategory.CAPTCHA_REQUIRED);
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user