Files
MeshBot/src/mesh/mesh.service.spec.ts
T
kamilkrzywda 75a10c195f feat: /api/channels endpoint, /api namespace, Scalar at root (v0.1.0) (#2)
feat: /api/channels endpoint, /api namespace, Scalar at root (v0.1.0) — closes #1
2026-09-23 21:05:22 +02:00

119 lines
3.5 KiB
TypeScript

import { create, toBinary } from '@bufbuild/protobuf';
import * as P from '@meshtastic/protobufs';
import * as net from 'node:net';
import type { AddressInfo } from 'node:net';
import { describe, expect, it } from 'vitest';
import { frame } from './codec.js';
import { MeshService } from './mesh.service.js';
function startBridge(): Promise<{
port: number;
sockets: net.Socket[];
close: () => void;
}> {
const sockets: net.Socket[] = [];
const server = net.createServer((socket) => sockets.push(socket));
return new Promise((resolve) => {
server.listen(0, '127.0.0.1', () => {
resolve({
port: (server.address() as AddressInfo).port,
sockets,
close: () => server.close(),
});
});
});
}
async function waitFor(cond: () => boolean, timeoutMs = 5000): Promise<void> {
const t0 = Date.now();
while (!cond()) {
if (Date.now() - t0 > timeoutMs) throw new Error('timed out waiting');
await new Promise((r) => setTimeout(r, 10));
}
}
function channelFrame(
index: number,
settings?: { name?: string; uplinkEnabled?: boolean },
): Uint8Array {
const ch = create(P.Channel.ChannelSchema, {
index,
...(settings
? {
// psk must never leak into channels() output
settings: {
name: settings.name ?? '',
psk: new Uint8Array(16).fill(7),
uplinkEnabled: settings.uplinkEnabled ?? false,
},
}
: {}),
});
const fr = create(P.Mesh.FromRadioSchema, {
payloadVariant: { case: 'channel', value: ch },
});
return frame(toBinary(P.Mesh.FromRadioSchema, fr));
}
describe('MeshService channel pipeline', () => {
it('collects channels from FromRadio frames and redacts keys', async () => {
const bridge = await startBridge();
process.env.MESH_HOST = '127.0.0.1';
process.env.MESH_PORT = String(bridge.port);
const svc = new MeshService();
try {
expect(svc.channels()).toEqual([]);
svc.onModuleInit();
await waitFor(
() => bridge.sockets.length === 1 && svc.status().connected,
);
bridge.sockets[0].write(
channelFrame(2, { name: 'LongFast', uplinkEnabled: true }),
);
bridge.sockets[0].write(channelFrame(0)); // channel with no settings at all
await waitFor(() => svc.channels().length === 2);
expect(svc.channels()).toEqual([
{ index: 0, name: '', uplinkEnabled: false, downlinkEnabled: false },
{
index: 2,
name: 'LongFast',
uplinkEnabled: true,
downlinkEnabled: false,
},
]);
} finally {
svc.onModuleDestroy();
bridge.close();
}
});
it('resets channels on reconnect (no stale data)', async () => {
const bridge = await startBridge();
process.env.MESH_HOST = '127.0.0.1';
process.env.MESH_PORT = String(bridge.port);
process.env.MESH_RECONNECT_MS = '50';
const svc = new MeshService();
try {
svc.onModuleInit();
await waitFor(
() => bridge.sockets.length === 1 && svc.status().connected,
);
bridge.sockets[0].write(channelFrame(1, { name: 'Stale' }));
await waitFor(() => svc.channels().length === 1);
// drop the connection; bot reconnects and must start with an empty table
bridge.sockets[0].destroy();
await waitFor(() => bridge.sockets.length >= 2 && svc.status().connected);
await waitFor(() => svc.channels().length === 0);
} finally {
delete process.env.MESH_RECONNECT_MS;
svc.onModuleDestroy();
bridge.close();
}
});
});