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

+74
View File
@@ -0,0 +1,74 @@
import { describe, it, expect } from 'vitest';
import { isValidImageMimeType, bufferToBase64 } from '../src/utils/image.js';
describe('isValidImageMimeType', () => {
it('accepts valid image MIME types', () => {
expect(isValidImageMimeType('image/jpeg')).toBe(true);
expect(isValidImageMimeType('image/png')).toBe(true);
expect(isValidImageMimeType('image/gif')).toBe(true);
expect(isValidImageMimeType('image/webp')).toBe(true);
});
it('rejects invalid MIME types', () => {
expect(isValidImageMimeType('text/plain')).toBe(false);
expect(isValidImageMimeType('application/pdf')).toBe(false);
expect(isValidImageMimeType('video/mp4')).toBe(false);
expect(isValidImageMimeType('application/json')).toBe(false);
expect(isValidImageMimeType('text/html')).toBe(false);
});
it('rejects empty string', () => {
expect(isValidImageMimeType('')).toBe(false);
});
it('rejects null-like values', () => {
expect(isValidImageMimeType('null' as unknown as string)).toBe(false);
expect(isValidImageMimeType('undefined' as unknown as string)).toBe(false);
});
it('is case-sensitive (lowercase required)', () => {
// The function checks exact matches against lowercase values
expect(isValidImageMimeType('IMAGE/JPEG')).toBe(false);
expect(isValidImageMimeType('Image/Png')).toBe(false);
});
it('rejects MIME types with parameters', () => {
expect(isValidImageMimeType('image/jpeg; charset=utf-8')).toBe(false);
expect(isValidImageMimeType('image/png; alpha')).toBe(false);
});
});
describe('bufferToBase64', () => {
it('converts buffer to base64 string', () => {
const buffer = Buffer.from('hello world');
const result = bufferToBase64(buffer);
expect(result).toBe(btoa('hello world'));
});
it('handles empty buffer', () => {
const buffer = Buffer.from('');
const result = bufferToBase64(buffer);
expect(result).toBe('');
});
it('handles binary data', () => {
const buffer = Buffer.from([0x00, 0x01, 0x02, 0xff, 0xfe, 0xfd]);
const result = bufferToBase64(buffer);
expect(result).toBe(btoa('\x00\x01\x02\xff\xfe\xfd'));
});
it('handles large buffer', () => {
const buffer = Buffer.alloc(1024 * 1024); // 1MB
buffer.fill(0x41); // 'A'
const result = bufferToBase64(buffer);
expect(result.length).toBeGreaterThan(0);
// Base64 encodes each 3 bytes as 4 chars, rounding up.
expect(result.length).toBe(Math.ceil((1024 * 1024) / 3) * 4);
});
it('produces valid base64 (only valid chars)', () => {
const buffer = Buffer.from(Math.random().toString(36).repeat(100));
const result = bufferToBase64(buffer);
expect(result).toMatch(/^[A-Za-z0-9+/]+=*$/);
});
});