mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-05-02 19:43:10 +02:00
185 lines
5.1 KiB
TypeScript
185 lines
5.1 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import {
|
|
buildRawPacketStatsSnapshot,
|
|
summarizeRawPacketForStats,
|
|
type RawPacketStatsSessionState,
|
|
} from '../utils/rawPacketStats';
|
|
import type { RawPacket } from '../types';
|
|
|
|
const TEXT_MESSAGE_PACKET = '09046F17C47ED00A13E16AB5B94B1CC2D1A5059C6E5A6253C60D';
|
|
|
|
function createSession(
|
|
overrides: Partial<RawPacketStatsSessionState> = {}
|
|
): RawPacketStatsSessionState {
|
|
return {
|
|
sessionStartedAt: 700_000,
|
|
totalObservedPackets: 4,
|
|
trimmedObservationCount: 0,
|
|
observations: [
|
|
{
|
|
observationKey: 'obs-1',
|
|
timestamp: 850,
|
|
payloadType: 'Advert',
|
|
routeType: 'Flood',
|
|
decrypted: false,
|
|
rssi: -68,
|
|
snr: 7,
|
|
sourceKey: 'AA11',
|
|
sourceLabel: 'AA11',
|
|
pathTokenCount: 2,
|
|
pathSignature: '01>02',
|
|
hopByteWidth: 1,
|
|
},
|
|
{
|
|
observationKey: 'obs-2',
|
|
timestamp: 910,
|
|
payloadType: 'TextMessage',
|
|
routeType: 'Direct',
|
|
decrypted: true,
|
|
rssi: -74,
|
|
snr: 5,
|
|
sourceKey: 'BB22',
|
|
sourceLabel: 'BB22',
|
|
pathTokenCount: 0,
|
|
pathSignature: null,
|
|
hopByteWidth: null,
|
|
},
|
|
{
|
|
observationKey: 'obs-3',
|
|
timestamp: 960,
|
|
payloadType: 'Advert',
|
|
routeType: 'Flood',
|
|
decrypted: false,
|
|
rssi: -64,
|
|
snr: 8,
|
|
sourceKey: 'AA11',
|
|
sourceLabel: 'AA11',
|
|
pathTokenCount: 1,
|
|
pathSignature: '02',
|
|
hopByteWidth: 2,
|
|
},
|
|
{
|
|
observationKey: 'obs-4',
|
|
timestamp: 990,
|
|
payloadType: 'Ack',
|
|
routeType: 'Direct',
|
|
decrypted: true,
|
|
rssi: -88,
|
|
snr: 3,
|
|
sourceKey: null,
|
|
sourceLabel: null,
|
|
pathTokenCount: 0,
|
|
pathSignature: null,
|
|
hopByteWidth: null,
|
|
},
|
|
],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('buildRawPacketStatsSnapshot', () => {
|
|
it('prefers decrypted contact identity over one-byte sourceHash for stats bucketing', () => {
|
|
const packet: RawPacket = {
|
|
id: 1,
|
|
observation_id: 10,
|
|
timestamp: 1_700_000_000,
|
|
data: TEXT_MESSAGE_PACKET,
|
|
payload_type: 'TextMessage',
|
|
snr: 4,
|
|
rssi: -72,
|
|
decrypted: true,
|
|
decrypted_info: {
|
|
channel_name: null,
|
|
sender: 'Alpha',
|
|
channel_key: null,
|
|
contact_key: '0a'.repeat(32),
|
|
},
|
|
};
|
|
|
|
const summary = summarizeRawPacketForStats(packet);
|
|
|
|
expect(summary.sourceKey).toBe('0A'.repeat(32));
|
|
expect(summary.sourceLabel).toBe('Alpha');
|
|
});
|
|
|
|
it('tags unresolved one-byte source hashes so they do not collide with full contact keys', () => {
|
|
const packet: RawPacket = {
|
|
id: 2,
|
|
observation_id: 11,
|
|
timestamp: 1_700_000_000,
|
|
data: TEXT_MESSAGE_PACKET,
|
|
payload_type: 'TextMessage',
|
|
snr: 4,
|
|
rssi: -72,
|
|
decrypted: false,
|
|
decrypted_info: null,
|
|
};
|
|
|
|
const summary = summarizeRawPacketForStats(packet);
|
|
|
|
expect(summary.sourceKey).toBe('hash1:0A');
|
|
expect(summary.sourceLabel).toBe('0A');
|
|
});
|
|
|
|
it('computes counts, rankings, and rolling-window coverage from session observations', () => {
|
|
const stats = buildRawPacketStatsSnapshot(createSession(), '5m', 1_000);
|
|
|
|
expect(stats.packetCount).toBe(4);
|
|
expect(stats.uniqueSources).toBe(2);
|
|
expect(stats.pathBearingCount).toBe(2);
|
|
expect(stats.payloadBreakdown.slice(0, 3).map((item) => item.label)).toEqual([
|
|
'Advert',
|
|
'Ack',
|
|
'TextMessage',
|
|
]);
|
|
expect(stats.payloadBreakdown).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ label: 'GroupText', count: 0 }),
|
|
expect.objectContaining({ label: 'Control', count: 0 }),
|
|
])
|
|
);
|
|
expect(stats.hopProfile.map((item) => item.label)).toEqual([
|
|
'0',
|
|
'1',
|
|
'2-5',
|
|
'6-10',
|
|
'11-15',
|
|
'16+',
|
|
]);
|
|
expect(stats.hopProfile).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ label: '0', count: 2 }),
|
|
expect.objectContaining({ label: '1', count: 1 }),
|
|
expect.objectContaining({ label: '2-5', count: 1 }),
|
|
expect.objectContaining({ label: '6-10', count: 0 }),
|
|
expect.objectContaining({ label: '11-15', count: 0 }),
|
|
expect.objectContaining({ label: '16+', count: 0 }),
|
|
])
|
|
);
|
|
expect(stats.hopByteWidthProfile).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ label: 'No path', count: 2 }),
|
|
expect.objectContaining({ label: '1 byte / hop', count: 1 }),
|
|
expect.objectContaining({ label: '2 bytes / hop', count: 1 }),
|
|
])
|
|
);
|
|
expect(stats.strongestNeighbors[0]).toMatchObject({ label: 'AA11', bestRssi: -64 });
|
|
expect(stats.mostActiveNeighbors[0]).toMatchObject({ label: 'AA11', count: 2 });
|
|
expect(stats.windowFullyCovered).toBe(true);
|
|
});
|
|
|
|
it('flags incomplete session coverage when detailed history has been trimmed', () => {
|
|
const stats = buildRawPacketStatsSnapshot(
|
|
createSession({
|
|
trimmedObservationCount: 25,
|
|
}),
|
|
'session',
|
|
1_000
|
|
);
|
|
|
|
expect(stats.windowFullyCovered).toBe(false);
|
|
expect(stats.packetCount).toBe(4);
|
|
});
|
|
});
|