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:
jjbrunton committed 2026-05-21 01:32:10 +01:00
1 parent dfa0580859
commit 633bb0ec04
84 files changed
+19278 -3888

No files matched your search

+100
View File
@@ -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);
});
});