import { beforeEach, describe, expect, it } from 'vitest'; import { FakeMeshMonitorClient } from './fake-mesh-monitor-client.js'; import { BROADCAST_ADDR } from './mesh-monitor-client.js'; const BOT_NUM = 3994646728; // !ee1978c8 const KAMIL_NUM = 2927200947; // !ae798ab3 describe('FakeMeshMonitorClient', () => { let fake: FakeMeshMonitorClient; beforeEach(() => { fake = new FakeMeshMonitorClient({ localNum: BOT_NUM }); }); // ------------------------------------------------------------------ // status // ------------------------------------------------------------------ describe('getStatus', () => { it('returns a default status until configured', async () => { expect(await fake.getStatus()).toEqual({ connected: true, myNodeNum: 0, }); }); it('returns the configured status (defensively copied)', async () => { fake.setStatus({ connected: false, myNodeNum: BOT_NUM, longName: 'Bot' }); const s = await fake.getStatus(); s.connected = true; // mutate the returned copy expect((await fake.getStatus()).connected).toBe(false); }); }); // ------------------------------------------------------------------ // nodes // ------------------------------------------------------------------ describe('getNodes / getNode', () => { beforeEach(() => { fake.setNodes([ { num: BOT_NUM, longName: 'SP8MK Bot', shortName: '8MKb', position: { lat: 51, lon: 22 }, }, { num: KAMIL_NUM, longName: 'Kamil', shortName: 'KK' }, ]); }); it('returns all configured nodes', async () => { expect(await fake.getNodes()).toHaveLength(2); }); it('finds a node by number', async () => { expect((await fake.getNode(KAMIL_NUM))?.longName).toBe('Kamil'); }); it('finds a node by !hex id', async () => { expect((await fake.getNode('!ae798ab3'))?.longName).toBe('Kamil'); expect((await fake.getNode('!ee1978c8'))?.longName).toBe('SP8MK Bot'); }); it('finds a node by numeric string', async () => { expect((await fake.getNode(String(KAMIL_NUM)))?.longName).toBe('Kamil'); }); it('finds a node by name (case-insensitive)', async () => { expect((await fake.getNode('kamil'))?.num).toBe(KAMIL_NUM); expect((await fake.getNode('8mkB'))?.num).toBe(BOT_NUM); }); it('returns null for an unknown node', async () => { expect(await fake.getNode(999)).toBeNull(); expect(await fake.getNode('!deadbeef')).toBeNull(); expect(await fake.getNode('nobody')).toBeNull(); }); }); // ------------------------------------------------------------------ // channels + telemetry (passthrough of configured data) // ------------------------------------------------------------------ describe('getChannels / getTelemetry', () => { it('returns configured channels and telemetry', async () => { fake.setChannels([ { index: 0, name: '', psk: 'REDACTED' }, { index: 3, name: 'test' }, ]); fake.setTelemetry([ { fromNum: BOT_NUM, at: 1, kind: 'airUtilTx', data: { value: 0.1 } }, ]); expect(await fake.getChannels()).toHaveLength(2); expect((await fake.getTelemetry())[0].kind).toBe('airUtilTx'); }); it('returns empty arrays by default', async () => { expect(await fake.getChannels()).toEqual([]); expect(await fake.getTelemetry()).toEqual([]); }); }); // ------------------------------------------------------------------ // pushInbound + getMessages // ------------------------------------------------------------------ describe('getMessages', () => { it('returns inbound messages newest first', async () => { fake.pushInbound({ packetId: 1, text: 'first', channel: 0, at: 100 }); fake.pushInbound({ packetId: 2, text: 'second', channel: 0, at: 200 }); const msgs = await fake.getMessages(); expect(msgs.map((m) => m.text)).toEqual(['second', 'first']); }); it('filters by channel', async () => { fake.pushInbound({ packetId: 1, text: 'a', channel: 0, at: 1 }); fake.pushInbound({ packetId: 2, text: 'b', channel: 3, at: 2 }); const msgs = await fake.getMessages({ channel: 3 }); expect(msgs.map((m) => m.text)).toEqual(['b']); }); it('filters by toNodeId and fromNodeId', async () => { fake.pushInbound({ packetId: 1, fromId: '!ee1978c8', toId: '!ffffffff', text: 'bcast', channel: 3, at: 1, }); fake.pushInbound({ packetId: 2, fromId: '!ae798ab3', toId: '!ee1978c8', text: 'dm', channel: -1, at: 2, }); expect( (await fake.getMessages({ toNodeId: '!ffffffff' })).map((m) => m.text), ).toEqual(['bcast']); expect( (await fake.getMessages({ fromNodeId: '!ae798ab3' })).map( (m) => m.text, ), ).toEqual(['dm']); }); it('applies limit (newest N) after filtering', async () => { fake.pushInbound({ packetId: 1, text: 'a', channel: 0, at: 1 }); fake.pushInbound({ packetId: 2, text: 'b', channel: 0, at: 2 }); fake.pushInbound({ packetId: 3, text: 'c', channel: 3, at: 3 }); // newest-first: c, b, a — limit 2 → c, b expect((await fake.getMessages({ limit: 2 })).map((m) => m.text)).toEqual( ['c', 'b'], ); // limit + channel: only c is on ch3 expect( (await fake.getMessages({ limit: 2, channel: 3 })).map((m) => m.text), ).toEqual(['c']); }); it('returns a defensive copy (mutating the result does not leak)', async () => { fake.pushInbound({ packetId: 1, text: 'x', channel: 0, at: 1 }); const m = (await fake.getMessages())[0]; m.text = 'mutated'; expect((await fake.getMessages())[0].text).toBe('x'); }); }); // ------------------------------------------------------------------ // send // ------------------------------------------------------------------ describe('send', () => { it('records a directed send and mirrors it as an outbound message', async () => { const out = await fake.send({ text: 'hi kamil', toNodeId: '!ae798ab3' }); expect(out.deliveryState).toBe('pending'); expect(out.packetId).toBeGreaterThan(0); expect(fake.sends).toHaveLength(1); expect(fake.sends[0]).toMatchObject({ text: 'hi kamil', toNodeId: '!ae798ab3', packetId: out.packetId, }); const msgs = await fake.getMessages(); expect(msgs).toHaveLength(1); expect(msgs[0]).toMatchObject({ packetId: out.packetId, fromNum: BOT_NUM, fromId: '!ee1978c8', toNum: KAMIL_NUM, toId: '!ae798ab3', text: 'hi kamil', channel: -1, // DM rides channel -1 requestId: out.packetId, deliveryState: 'pending', }); }); it('records a broadcast send with the broadcast destination', async () => { const out = await fake.send({ text: 'hello mesh', channel: 3 }); const msgs = await fake.getMessages(); expect(msgs[0]).toMatchObject({ toNum: BROADCAST_ADDR, toId: '!ffffffff', channel: 3, text: 'hello mesh', }); expect(out.packetId).toBe(msgs[0].packetId); }); it('assigns a distinct, non-zero packet id per send', async () => { const a = await fake.send({ text: 'one', toNodeId: '!ae798ab3' }); const b = await fake.send({ text: 'two', channel: 0 }); expect(a.packetId).not.toBe(b.packetId); expect(a.packetId).toBeGreaterThan(0); expect(b.packetId).toBeGreaterThan(0); }); it('carries replyId onto the recorded message', async () => { await fake.send({ text: 'replying', toNodeId: '!ae798ab3', replyId: 55 }); expect((await fake.getMessages())[0].replyId).toBe(55); expect(fake.sends[0].replyId).toBe(55); }); it('rejects XOR violations exactly like the HTTP client', async () => { await expect( fake.send({ text: 'x', channel: 3, toNodeId: '!1' }), ).rejects.toThrow(/mutually exclusive/); await expect(fake.send({ text: 'x' })).rejects.toThrow( /required \(XOR\)/, ); await expect(fake.send({ text: '', channel: 3 })).rejects.toThrow( /non-empty/, ); // none of the rejected sends were recorded expect(fake.sends).toHaveLength(0); expect(await fake.getMessages()).toHaveLength(0); }); it('clearTraffic() resets sends + messages + the packet-id counter', async () => { await fake.send({ text: 'a', channel: 0 }); fake.pushInbound({ packetId: 1, text: 'in', channel: 0, at: 1 }); fake.clearTraffic(); expect(fake.sends).toHaveLength(0); expect(await fake.getMessages()).toHaveLength(0); // counter reset → first id is 1 again expect((await fake.send({ text: 'b', channel: 0 })).packetId).toBe(1); }); }); });