Files
stack/packages/comms/src/__tests__/matrix-client.test.ts
jason.woltje 2698ddb7b5
Some checks failed
ci/woodpecker/push/ci-image Pipeline was successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline failed
feat(comms): P1 presence — minimal Synapse + fleet presence room + mosaic.presence heartbeat + liveness (#888)
Co-authored-by: jason.woltje <jason@diversecanvas.com>
Co-committed-by: jason.woltje <jason@diversecanvas.com>
2026-07-25 21:03:18 +00:00

132 lines
5.0 KiB
TypeScript

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<Response>) => 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');
});
});