Split monolithic chat service into modular structure with Matrix client support, extract web server into per-resource route modules, introduce bootstrap/main-loop separation, and add comprehensive vitest coverage across services, repositories, and utilities. Adds project docs and CLAUDE.md agent instructions.
101 lines
3.3 KiB
TypeScript
101 lines
3.3 KiB
TypeScript
import { describe, it, expect, vi } from 'vitest';
|
|
import { withRetry } from '../src/utils/retry.js';
|
|
import { createMockLogger } from '../tests/mockLogger.js';
|
|
|
|
describe('withRetry', () => {
|
|
it('succeeds on first attempt', async () => {
|
|
const fn = vi.fn(() => Promise.resolve('success'));
|
|
const result = await withRetry(fn, { retries: 3, minTimeout: 0 });
|
|
expect(result).toBe('success');
|
|
expect(fn).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('retries on failure then succeeds', async () => {
|
|
let attempts = 0;
|
|
const fn = vi.fn(() => {
|
|
attempts++;
|
|
if (attempts < 3) return Promise.reject(new Error(`Attempt ${attempts} failed`));
|
|
return Promise.resolve('success');
|
|
});
|
|
|
|
const result = await withRetry(fn, { retries: 3, minTimeout: 0 });
|
|
expect(result).toBe('success');
|
|
expect(attempts).toBe(3);
|
|
});
|
|
|
|
it('throws after all retries exhausted', async () => {
|
|
const fn = vi.fn(() => Promise.reject(new Error('permanent failure')));
|
|
|
|
await expect(withRetry(fn, { retries: 2, minTimeout: 0 })).rejects.toThrow('permanent failure');
|
|
expect(fn).toHaveBeenCalledTimes(3); // 1 initial + 2 retries
|
|
});
|
|
|
|
it('logs warnings on failed attempts', async () => {
|
|
const logger = createMockLogger();
|
|
let attempts = 0;
|
|
const fn = vi.fn(() => {
|
|
attempts++;
|
|
return Promise.reject(new Error(`fail ${attempts}`));
|
|
});
|
|
|
|
await withRetry(fn, {
|
|
retries: 2,
|
|
logger,
|
|
operationName: 'test-op',
|
|
minTimeout: 0,
|
|
}).catch(() => {});
|
|
|
|
const warnLogs = logger.logs.filter(l => l.level === 'warn');
|
|
// 1 initial + 2 retries = 3 failed attempts, each logs a warning.
|
|
expect(warnLogs.length).toBe(3);
|
|
expect(warnLogs[0].message).toContain('test-op');
|
|
expect(warnLogs[0].message).toContain('fail 1');
|
|
expect(warnLogs[1].message).toContain('fail 2');
|
|
});
|
|
|
|
it('uses default operation name', async () => {
|
|
const logger = createMockLogger();
|
|
const fn = vi.fn(() => Promise.reject(new Error('fail')));
|
|
|
|
await withRetry(fn, { logger, retries: 1, minTimeout: 0 }).catch(() => {});
|
|
|
|
const warnLogs = logger.logs.filter(l => l.level === 'warn');
|
|
expect(warnLogs[0].message).toContain('operation');
|
|
});
|
|
|
|
it('supports custom retry count', async () => {
|
|
const fn = vi.fn(() => Promise.reject(new Error('fail')));
|
|
|
|
await withRetry(fn, { retries: 5, minTimeout: 0 }).catch(() => {});
|
|
expect(fn).toHaveBeenCalledTimes(6); // 1 initial + 5 retries
|
|
});
|
|
|
|
it('handles non-Error rejections', async () => {
|
|
const fn = vi.fn(() => Promise.reject('string error'));
|
|
|
|
// p-retry wraps non-Error rejections in a TypeError. The wrap is treated
|
|
// as an abort (no retry), so only the initial attempt runs.
|
|
await expect(withRetry(fn, { retries: 1, minTimeout: 0 })).rejects.toThrow('string error');
|
|
expect(fn).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('passes options to retry correctly', async () => {
|
|
const logger = createMockLogger();
|
|
let attempts = 0;
|
|
const fn = vi.fn(() => {
|
|
attempts++;
|
|
return Promise.reject(new Error('fail'));
|
|
});
|
|
|
|
await withRetry(fn, {
|
|
retries: 1,
|
|
logger,
|
|
operationName: 'custom-op',
|
|
minTimeout: 0,
|
|
}).catch(() => {});
|
|
|
|
expect(attempts).toBe(2);
|
|
expect(logger.logs.some(l => l.message.includes('custom-op'))).toBe(true);
|
|
});
|
|
});
|