import { describe, expect, it, vi } from 'vitest'; import { ErrorCategory, isRecoverable, isRetryable, toAppError, withRetry } from '../src/utils/errorHandler'; import { LogLevel, createLogger } from '../src/utils/logger'; describe('errorHandler - toAppError', () => { it('把 Error 归一化为 AppError', () => { const appErr = toAppError(new Error('test'), ErrorCategory.NETWORK); expect(appErr.category).toBe(ErrorCategory.NETWORK); expect(appErr.message).toBe('test'); expect(appErr.retryable).toBe(true); // NETWORK 可重试 }); it('把字符串归一化', () => { const appErr = toAppError('字符串错误', ErrorCategory.PARSING); expect(appErr.message).toBe('字符串错误'); expect(appErr.retryable).toBe(false); // PARSING 不可重试 }); it('AppError 再经过 toAppError 保持不变', () => { const appErr = toAppError(new Error('x'), ErrorCategory.DATABASE); const again = toAppError(appErr); expect(again).toBe(appErr); }); }); describe('errorHandler - withRetry', () => { it('首次成功不重试', async () => { const fn = vi.fn().mockResolvedValue('ok'); const result = await withRetry(fn, 3, 10); expect(result).toBe('ok'); expect(fn).toHaveBeenCalledTimes(1); }); it('失败后重试到成功', async () => { const fn = vi.fn() .mockRejectedValueOnce(new Error('fail1')) .mockRejectedValueOnce(new Error('fail2')) .mockResolvedValueOnce('ok'); const result = await withRetry(fn, 3, 10); expect(result).toBe('ok'); expect(fn).toHaveBeenCalledTimes(3); }); it('重试次数耗尽后抛出最后一个错误', async () => { const fn = vi.fn().mockRejectedValue(new Error('always fail')); await expect(withRetry(fn, 2, 10)).rejects.toThrow('always fail'); expect(fn).toHaveBeenCalledTimes(3); // 首次 + 2 次重试 }); it('retries=0 时不重试', async () => { const fn = vi.fn().mockRejectedValue(new Error('x')); await expect(withRetry(fn, 0, 10)).rejects.toThrow('x'); expect(fn).toHaveBeenCalledTimes(1); }); }); describe('errorHandler - isRetryable/isRecoverable', () => { it('NETWORK 错误可重试', () => { const err = toAppError(new Error('x'), ErrorCategory.NETWORK); expect(isRetryable(err)).toBe(true); expect(isRecoverable(err)).toBe(true); }); it('PARSING 错误不可重试', () => { const err = toAppError(new Error('x'), ErrorCategory.PARSING); expect(isRetryable(err)).toBe(false); expect(isRecoverable(err)).toBe(false); }); it('未知错误默认可恢复', () => { expect(isRecoverable(new Error('x'))).toBe(true); expect(isRetryable(new Error('x'))).toBe(false); }); }); describe('logger', () => { it('记录各级别日志', () => { const log = createLogger(100); log.consoleOutput = false; // 测试不输出 console log.debug('tag1', 'debug msg'); log.info('tag1', 'info msg'); log.warn('tag2', 'warn msg'); log.error('tag2', 'error msg', { code: 500 }); const logs = log.getLogs(); expect(logs).toHaveLength(4); expect(logs[0].level).toBe(LogLevel.DEBUG); expect(logs[3].level).toBe(LogLevel.ERROR); expect(logs[3].data).toEqual({ code: 500 }); }); it('循环缓冲:超出上限丢弃最旧', () => { const log = createLogger(3); log.consoleOutput = false; log.info('t', 'msg1'); log.info('t', 'msg2'); log.info('t', 'msg3'); log.info('t', 'msg4'); // 触发裁剪,丢弃 msg1 const logs = log.getLogs(); expect(logs).toHaveLength(3); expect(logs[0].message).toBe('msg2'); expect(logs[2].message).toBe('msg4'); }); it('minLevel 过滤低级别', () => { const log = createLogger(100); log.consoleOutput = false; log.minLevel = LogLevel.WARN; log.debug('t', 'debug'); // 被过滤 log.info('t', 'info'); // 被过滤 log.warn('t', 'warn'); log.error('t', 'error'); expect(log.getLogs()).toHaveLength(2); }); it('query 按级别和标签过滤', () => { const log = createLogger(100); log.consoleOutput = false; log.info('alpha', 'a1'); log.error('alpha', 'a2'); log.error('beta', 'b1'); expect(log.query({ tag: 'alpha' })).toHaveLength(2); expect(log.query({ minLevel: LogLevel.ERROR })).toHaveLength(2); expect(log.query({ minLevel: LogLevel.ERROR, tag: 'alpha' })).toHaveLength(1); }); it('clearLogs 清空', () => { const log = createLogger(100); log.consoleOutput = false; log.info('t', 'x'); log.clearLogs(); expect(log.getLogs()).toHaveLength(0); }); it('setMaxBuffer 动态调整并立即裁剪', () => { const log = createLogger(100); log.consoleOutput = false; log.info('t', 'm1'); log.info('t', 'm2'); log.info('t', 'm3'); log.setMaxBuffer(2); expect(log.getLogs()).toHaveLength(2); expect(log.getLogs()[0].message).toBe('m2'); }); it('日志条目含 ISO 时间戳', () => { const log = createLogger(100); log.consoleOutput = false; log.info('t', 'x'); const entry = log.getLogs()[0]; expect(() => new Date(entry.timestamp).toISOString()).not.toThrow(); expect(new Date(entry.timestamp).getTime()).toBeLessThanOrEqual(Date.now()); }); });