import { describe, it, expect, beforeEach } from 'vitest'; import { MatrixConnector, buildMessageBody, parseSyncResponse, registerMatrixConnector, type FetchLike, } from './matrix.js'; import { createConnector, _resetConnectorRegistry } from './registry.js'; import type { MatrixConnectorConfig } from './types.js'; const CONFIG: MatrixConnectorConfig = { homeserverUrl: 'https://matrix.internal/', userId: '@mos:internal', roomId: '!room:internal', }; /** A fetch mock that returns queued responses and records calls. */ function mockFetch(responses: Array<{ ok?: boolean; status?: number; body?: unknown }>): { fetchImpl: FetchLike; calls: Array<{ url: string; method?: string; body?: string }>; } { const calls: Array<{ url: string; method?: string; body?: string }> = []; let i = 0; const fetchImpl: FetchLike = async (url, init) => { calls.push({ url, method: init?.method, body: init?.body }); const r = responses[Math.min(i, responses.length - 1)] ?? {}; i += 1; return { ok: r.ok ?? true, status: r.status ?? 200, json: async () => r.body ?? {}, text: async () => JSON.stringify(r.body ?? {}), }; }; return { fetchImpl, calls }; } describe('buildMessageBody', () => { it('builds an m.text event', () => { expect(buildMessageBody({ text: 'hi' })).toEqual({ msgtype: 'm.text', body: 'hi' }); }); it('adds an m.thread relation when threadId is set', () => { expect(buildMessageBody({ text: 'hi', threadId: '$evt' })).toEqual({ msgtype: 'm.text', body: 'hi', 'm.relates_to': { rel_type: 'm.thread', event_id: '$evt' }, }); }); }); describe('parseSyncResponse', () => { it('extracts operator messages and skips the orchestrator’s own echoes', () => { const data = { next_batch: 's2', rooms: { join: { '!room:internal': { timeline: { events: [ { type: 'm.room.message', sender: '@jason:internal', origin_server_ts: 1_700_000_000_000, content: { body: 'status?' }, }, { type: 'm.room.message', sender: '@mos:internal', // self — skipped origin_server_ts: 1_700_000_001_000, content: { body: 'working on it' }, }, { type: 'm.reaction', sender: '@jason:internal', content: {} }, // non-message ], }, }, }, }, }; const msgs = parseSyncResponse(data, '!room:internal', '@mos:internal'); expect(msgs).toHaveLength(1); expect(msgs[0]).toMatchObject({ text: 'status?', sender: '@jason:internal' }); expect(msgs[0]!.ts).toBe(new Date(1_700_000_000_000).toISOString()); }); it('carries threadId through thread-relments', () => { const data = { rooms: { join: { '!room:internal': { timeline: { events: [ { type: 'm.room.message', sender: '@jason:internal', origin_server_ts: 1, content: { body: 'in thread', 'm.relates_to': { rel_type: 'm.thread', event_id: '$root' }, }, }, ], }, }, }, }, }; expect(parseSyncResponse(data, '!room:internal', '@mos:internal')[0]!.threadId).toBe('$root'); }); it('returns [] for an empty/foreign sync', () => { expect(parseSyncResponse({}, '!room:internal', '@mos:internal')).toEqual([]); }); }); describe('MatrixConnector', () => { it('throws without an access token', () => { expect(() => new MatrixConnector(CONFIG, { accessToken: '' })).toThrow(/access token/i); }); it('send PUTs an m.text event and returns the event id', async () => { const { fetchImpl, calls } = mockFetch([{ body: { event_id: '$abc' } }]); const c = new MatrixConnector(CONFIG, { accessToken: 'tok', fetchImpl }); const res = await c.send({ text: 'pong' }, 1234); expect(res).toEqual({ delivered: true, messageId: '$abc' }); expect(calls[0]!.method).toBe('PUT'); expect(calls[0]!.url).toContain( '/_matrix/client/v3/rooms/!room%3Ainternal/send/m.room.message/mosaic-1234-1', ); expect(JSON.parse(calls[0]!.body!)).toEqual({ msgtype: 'm.text', body: 'pong' }); }); it('send reports not-delivered on a non-2xx', async () => { const { fetchImpl } = mockFetch([{ ok: false, status: 403 }]); const c = new MatrixConnector(CONFIG, { accessToken: 'tok', fetchImpl }); const res = await c.send({ text: 'x' }); expect(res.delivered).toBe(false); expect(res.error).toContain('403'); }); it('health reports reachable + authenticated when whoami matches', async () => { const { fetchImpl } = mockFetch([ { body: { versions: ['v1.11'] } }, // /versions { body: { user_id: '@mos:internal' } }, // /whoami ]); const c = new MatrixConnector(CONFIG, { accessToken: 'tok', fetchImpl }); const h = await c.health(); expect(h.reachable).toBe(true); expect(h.authenticated).toBe(true); }); it('health flags auth mismatch', async () => { const { fetchImpl } = mockFetch([ { body: {} }, { body: { user_id: '@someone-else:internal' } }, ]); const c = new MatrixConnector(CONFIG, { accessToken: 'tok', fetchImpl }); const h = await c.health(); expect(h.reachable).toBe(true); expect(h.authenticated).toBe(false); }); it('health reports unreachable when /versions fails', async () => { const { fetchImpl } = mockFetch([{ ok: false, status: 502 }]); const c = new MatrixConnector(CONFIG, { accessToken: 'tok', fetchImpl }); const h = await c.health(); expect(h.reachable).toBe(false); }); }); describe('registerMatrixConnector', () => { beforeEach(() => _resetConnectorRegistry()); it('registers a matrix factory createConnector can build', () => { registerMatrixConnector({ MATRIX_ACCESS_TOKEN: 'tok' } as NodeJS.ProcessEnv); const c = createConnector({ kind: 'matrix', matrix: CONFIG }); expect(c.kind).toBe('matrix'); }); it('the factory rejects config missing the matrix block', () => { registerMatrixConnector({ MATRIX_ACCESS_TOKEN: 'tok' } as NodeJS.ProcessEnv); expect(() => createConnector({ kind: 'matrix' })).toThrow(/missing the .matrix. block/i); }); });