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,99 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ChatService } from '../src/services/chat/service.js';
|
||||
import { MatrixClient, MatrixRateLimitError } from '../src/services/chat/matrix/client.js';
|
||||
import { createMockLogger } from './mockLogger.js';
|
||||
|
||||
const creds = {
|
||||
homeserver: 'https://matrix.example.test',
|
||||
accessToken: 'tok',
|
||||
userId: '@me:example.test',
|
||||
capturedAt: Date.now(),
|
||||
};
|
||||
|
||||
function buildService() {
|
||||
const logger = createMockLogger();
|
||||
// Fetch impl never matters here — we drive cooldowns directly via the
|
||||
// public API, which is how MatrixClient surfaces them at runtime.
|
||||
const fetchImpl = (() =>
|
||||
Promise.resolve(
|
||||
new Response('{}', { status: 200, headers: { 'content-type': 'application/json' } }),
|
||||
)) as typeof fetch;
|
||||
const matrix = new MatrixClient(creds, logger, { minRequestIntervalMs: 0, fetchImpl });
|
||||
const svc = new ChatService(matrix, logger);
|
||||
return { svc, logger };
|
||||
}
|
||||
|
||||
describe('ChatService invitation cooldown', () => {
|
||||
it('is null by default', () => {
|
||||
const { svc } = buildService();
|
||||
expect(svc.getInvitationCooldown()).toBeNull();
|
||||
});
|
||||
|
||||
it('records a cooldown from a MatrixRateLimitError', () => {
|
||||
const { svc } = buildService();
|
||||
const err = new MatrixRateLimitError(
|
||||
'429',
|
||||
60_000,
|
||||
'M_LIMIT_EXCEEDED',
|
||||
'rate.invitation_limit_mature_account',
|
||||
);
|
||||
svc.setInvitationCooldown(err);
|
||||
const active = svc.getInvitationCooldown();
|
||||
expect(active).not.toBeNull();
|
||||
expect(active!.redditCode).toBe('rate.invitation_limit_mature_account');
|
||||
expect(active!.until).toBeGreaterThan(Date.now() + 50_000);
|
||||
expect(active!.reason).toMatch(/throttling new chat invitations/);
|
||||
});
|
||||
|
||||
it('auto-expires once the until timestamp has passed', () => {
|
||||
const { svc } = buildService();
|
||||
const err = new MatrixRateLimitError('429', 10, 'M_LIMIT_EXCEEDED', 'rate.foo');
|
||||
svc.setInvitationCooldown(err);
|
||||
expect(svc.getInvitationCooldown()).not.toBeNull();
|
||||
|
||||
// Force the clock past the cooldown window.
|
||||
const cooldown = svc.getInvitationCooldown()!;
|
||||
const realNow = Date.now;
|
||||
Date.now = () => cooldown.until + 1;
|
||||
try {
|
||||
expect(svc.getInvitationCooldown()).toBeNull();
|
||||
} finally {
|
||||
Date.now = realNow;
|
||||
}
|
||||
});
|
||||
|
||||
it('startConversation short-circuits when a cooldown is active', async () => {
|
||||
const { svc } = buildService();
|
||||
svc.setInvitationCooldown(
|
||||
new MatrixRateLimitError('429', 60_000, 'M_LIMIT_EXCEEDED', 'rate.foo'),
|
||||
);
|
||||
const result = await svc.startConversation('someuser', 'hi');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('sendChatMessage short-circuits when a cooldown is active', async () => {
|
||||
const { svc } = buildService();
|
||||
svc.setInvitationCooldown(
|
||||
new MatrixRateLimitError('429', 60_000, 'M_LIMIT_EXCEEDED', null),
|
||||
);
|
||||
const result = await svc.sendChatMessage('!room:example.test', 'hi');
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('sendChatMessage records a cooldown when the homeserver rate-limits a reply', async () => {
|
||||
const logger = createMockLogger();
|
||||
const matrix = new MatrixClient(creds, logger, { minRequestIntervalMs: 0 });
|
||||
// Force sendTextMessage to throw a rate-limit error — simulates Reddit
|
||||
// returning M_LIMIT_EXCEEDED on a reply into an existing room.
|
||||
matrix.sendTextMessage = async () => {
|
||||
throw new MatrixRateLimitError('429', 90_000, 'M_LIMIT_EXCEEDED', null);
|
||||
};
|
||||
const svc = new ChatService(matrix, logger);
|
||||
|
||||
const result = await svc.sendChatMessage('!room:example.test', 'hi');
|
||||
expect(result).toBe(false);
|
||||
const active = svc.getInvitationCooldown();
|
||||
expect(active).not.toBeNull();
|
||||
expect(active!.until).toBeGreaterThan(Date.now() + 80_000);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user