Restructure chat service, web routes, and add test suite
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.
This commit is contained in:
1 parent
dfa0580859
commit
633bb0ec04
84 files changed
+19278
-3888
No files matched your search
@@ -0,0 +1,100 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user