import { describe, expect, it, vi } from 'vitest'; import { MatrixError, MinimalMatrixClient, toMatrixPresence } from '../matrix-client.js'; const jsonResponse = (status: number, body: unknown): Response => new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' } }); // A fetch mock typed with the (URL, RequestInit?) shape the client actually // calls, so mock.calls has a proper tuple type under noUncheckedIndexedAccess. const mkFetch = (impl: (url: URL, init?: RequestInit) => Promise) => vi.fn(impl); const cfg = { homeserverUrl: 'https://matrix.localhost:8448', accessToken: 'as-secret', actAsUserId: '@agent-alpha:matrix.localhost', }; describe('toMatrixPresence', () => { it('maps liveness states to native presence EDU values', () => { expect(toMatrixPresence('online')).toBe('online'); expect(toMatrixPresence('away')).toBe('unavailable'); expect(toMatrixPresence('offline')).toBe('offline'); }); }); describe('MinimalMatrixClient', () => { it('setPresence PUTs native presence and masquerades via user_id', async () => { const fetchMock = mkFetch(async () => jsonResponse(200, {})); const client = new MinimalMatrixClient(cfg, fetchMock as unknown as typeof fetch); await client.setPresence('@agent-alpha:matrix.localhost', 'away', 'hb'); const [url, init] = fetchMock.mock.calls[0]!; const u = new URL((url as URL).toString()); expect(u.pathname).toBe('/_matrix/client/v3/presence/%40agent-alpha%3Amatrix.localhost/status'); expect(u.searchParams.get('user_id')).toBe('@agent-alpha:matrix.localhost'); expect(JSON.parse((init as RequestInit).body as string)).toEqual({ presence: 'unavailable', status_msg: 'hb', }); expect((init as RequestInit).method).toBe('PUT'); }); it('sendHeartbeat posts an m.room.message and returns the event_id', async () => { const fetchMock = mkFetch(async () => jsonResponse(200, { event_id: '$evt1' })); const client = new MinimalMatrixClient(cfg, fetchMock as unknown as typeof fetch); const id = await client.sendHeartbeat('!room:matrix.localhost', { macp_version: '1.0', macp_type: 'presence', msgtype: 'mosaic.presence', agent: { mxid: cfg.actAsUserId, slug: 'alpha', harness: 'claude-code' }, ts: 1, body: 'alpha online (seq 1)', status: 'online', seq: 1, interval_ms: 1000, }); expect(id).toBe('$evt1'); const [url] = fetchMock.mock.calls[0]!; expect((url as URL).pathname).toContain('/rooms/!room%3Amatrix.localhost/send/m.room.message/'); }); it('throws a MatrixError carrying errcode on a non-2xx', async () => { const fetchMock = mkFetch(async () => jsonResponse(403, { errcode: 'M_FORBIDDEN', error: 'nope' }), ); const client = new MinimalMatrixClient(cfg, fetchMock as unknown as typeof fetch); await expect(client.whoami()).rejects.toMatchObject({ name: 'MatrixError', status: 403, errcode: 'M_FORBIDDEN', }); await expect(client.whoami()).rejects.toBeInstanceOf(MatrixError); }); it('readHeartbeats reduces the timeline to the latest beat per agent', async () => { // Timeline (dir=b => most-recent first). alpha has two beats; keep highest seq. const chunk = [ { sender: '@agent-bravo:matrix.localhost', origin_server_ts: 9000, content: { msgtype: 'mosaic.presence', agent: { slug: 'bravo', mxid: '@agent-bravo:matrix.localhost' }, seq: 5, status: 'online', ts: 8999, }, }, { sender: '@agent-alpha:matrix.localhost', origin_server_ts: 8000, content: { msgtype: 'mosaic.presence', agent: { slug: 'alpha', mxid: '@agent-alpha:matrix.localhost' }, seq: 12, status: 'online', ts: 7999, }, }, { // an ordinary chat message must be ignored sender: '@human:matrix.localhost', origin_server_ts: 7000, content: { msgtype: 'm.text', body: 'hi' }, }, { sender: '@agent-alpha:matrix.localhost', origin_server_ts: 6000, content: { msgtype: 'mosaic.presence', agent: { slug: 'alpha', mxid: '@agent-alpha:matrix.localhost' }, seq: 11, status: 'online', ts: 5999, }, }, ]; const fetchMock = mkFetch(async () => jsonResponse(200, { chunk })); const client = new MinimalMatrixClient(cfg, fetchMock as unknown as typeof fetch); const obs = await client.readHeartbeats('!room:matrix.localhost'); const bySlug = Object.fromEntries(obs.map((o) => [o.slug, o])); expect(Object.keys(bySlug).sort()).toEqual(['alpha', 'bravo']); expect(bySlug.alpha).toMatchObject({ lastSeq: 12, lastSeenTs: 8000 }); // highest seq wins, server ts expect(bySlug.bravo).toMatchObject({ lastSeq: 5, lastSeenTs: 9000 }); const [url] = fetchMock.mock.calls[0]!; const u = new URL((url as URL).toString()); expect(u.searchParams.get('dir')).toBe('b'); }); });