Device info endpoint + richer node summaries (GET /api/device, MCP device_info) (#13)
CI / build + unit (push) Successful in 36s

This commit was merged in pull request #13.
This commit is contained in:
2026-09-24 22:46:40 +02:00
parent b31fb35464
commit 4beaa377c4
10 changed files with 369 additions and 10 deletions
+143
View File
@@ -4,7 +4,10 @@ import { describe, expect, it } from 'vitest';
import { BROADCAST_ADDR, extractFrames, frame } from './codec.js';
import {
KAMIL_NUM,
configSectionFrame,
kamilNodeInfo,
metadataFrame,
moduleConfigSectionFrame,
nodeInfoFrame,
startBridge,
waitFor,
@@ -614,3 +617,143 @@ describe('MeshService resolveDest', () => {
}
});
});
describe('MeshService device info', () => {
it('captures DeviceMetadata and exposes firmwareVersion', 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 {
svc.onModuleInit();
await waitFor(
() => bridge.sockets.length === 1 && svc.status().connected,
);
// before the sync: only the node number (null until myInfo arrives)
expect(svc.deviceInfo()).toEqual({
myNodeNum: null,
localConfig: {},
moduleConfig: {},
});
bridge.sockets[0].write(
metadataFrame({ firmwareVersion: '2.7.0.1', hasWifi: false, role: 3 }),
);
await waitFor(() => svc.deviceInfo().firmwareVersion === '2.7.0.1');
const info = svc.deviceInfo();
expect(info.firmwareVersion).toBe('2.7.0.1');
expect(info.metadata?.firmwareVersion).toBe('2.7.0.1'); // metadata stored as-is
} finally {
svc.onModuleDestroy();
bridge.close();
}
});
it('captures config/module-config sections and redacts key material', 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 {
svc.onModuleInit();
await waitFor(
() => bridge.sockets.length === 1 && svc.status().connected,
);
const secret = 'super-secret-wifi-psk';
bridge.sockets[0].write(
configSectionFrame('security', {
publicKey: new Uint8Array([1, 2, 3]),
privateKey: new Uint8Array([4, 5, 6]),
adminKey: [new Uint8Array([7])],
isManaged: true,
}),
);
bridge.sockets[0].write(
configSectionFrame('network', {
wifiEnabled: false,
wifiSsid: 'home',
wifiPsk: secret,
}),
);
bridge.sockets[0].write(configSectionFrame('device', { role: 3 }));
bridge.sockets[0].write(
moduleConfigSectionFrame('telemetry', {
environmentUpdateInterval: 900,
powerMeasurementEnabled: true,
}),
);
await waitFor(
() =>
Object.keys(svc.deviceInfo().localConfig).length === 3 &&
Object.keys(svc.deviceInfo().moduleConfig).length === 1,
);
const info = svc.deviceInfo();
expect(Object.keys(info.localConfig).sort()).toEqual([
'device',
'network',
'security',
]);
expect(info.moduleConfig.telemetry).toMatchObject({
environmentUpdateInterval: 900,
powerMeasurementEnabled: true,
});
// key material is replaced with a marker — never exposed
expect(info.localConfig.security).toMatchObject({
publicKey: 'REDACTED',
privateKey: 'REDACTED',
adminKey: 'REDACTED',
isManaged: true, // non-secret fields pass through
});
expect(info.localConfig.network?.wifiPsk).toBe('REDACTED');
const json = JSON.stringify(info);
expect(json).not.toContain(secret);
} finally {
svc.onModuleDestroy();
bridge.close();
}
});
it('summarizes NodeInfo snr/hopsAway/viaMqtt when present', 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 {
svc.onModuleInit();
await waitFor(
() => bridge.sockets.length === 1 && svc.status().connected,
);
bridge.sockets[0].write(nodeInfoFrame(KAMIL_NUM, kamilNodeInfo()));
bridge.sockets[0].write(
nodeInfoFrame(42, {
user: { id: '!ot', longName: 'Other Node' },
snr: -2.5,
hopsAway: 1,
viaMqtt: true,
}),
);
await waitFor(async () => (await svc.nodes()).length === 2);
const nodes = await svc.nodes();
expect(nodes.find((n) => n.num === 42)).toEqual({
num: 42,
longName: 'Other Node',
lastHeard: null,
snr: -2.5,
hopsAway: 1,
viaMqtt: true,
});
// absent in the proto → omitted from the summary
expect(nodes.find((n) => n.num === KAMIL_NUM)).not.toHaveProperty('snr');
} finally {
svc.onModuleDestroy();
bridge.close();
}
});
});