Files
Remote-Terminal-for-MeshCore/frontend/src/test/rawPacketStats.test.ts
2026-03-19 21:42:39 -07:00

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);
});
});