Co-authored-by: jason.woltje <jason@diversecanvas.com> Co-committed-by: jason.woltje <jason@diversecanvas.com>
132 lines
5.0 KiB
TypeScript
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');
|
|
});
|
|
});
|