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