import { describe, expect, it, vi } from 'vitest'; import { PayloadType } from '@michaelhart/meshcore-decoder'; import { buildPacketNetworkContext, createPacketNetworkState, ingestPacketIntoPacketNetwork, projectCanonicalPath, projectPacketNetwork, snapshotNeighborIds, } from '../networkGraph/packetNetworkGraph'; import { buildLinkKey } from '../utils/visualizerUtils'; import type { Contact, RadioConfig, RawPacket } from '../types'; import { CONTACT_TYPE_REPEATER } from '../types'; const { packetFixtures } = vi.hoisted(() => ({ packetFixtures: new Map(), })); vi.mock('../utils/visualizerUtils', async () => { const actual = await vi.importActual( '../utils/visualizerUtils' ); return { ...actual, parsePacket: vi.fn( (hexData: string) => packetFixtures.get(hexData) ?? actual.parsePacket(hexData) ), }; }); function createConfig(publicKey: string): RadioConfig { return { public_key: publicKey, name: 'Me', lat: 0, lon: 0, tx_power: 0, max_tx_power: 0, radio: { freq: 0, bw: 0, sf: 0, cr: 0 }, path_hash_mode: 0, path_hash_mode_supported: true, advert_location_source: 'off', }; } function createContact(publicKey: string, name: string, type = 1): Contact { return { public_key: publicKey, name, type, flags: 0, direct_path: null, direct_path_len: 0, direct_path_hash_mode: 0, route_override_path: null, route_override_len: null, route_override_hash_mode: null, last_advert: null, lat: null, lon: null, last_seen: null, on_radio: false, last_contacted: null, last_read_at: null, first_seen: null, }; } function createPacket(data: string): RawPacket { return { id: 1, observation_id: 1, timestamp: 1_700_000_000, data, payload_type: 'TEXT', snr: null, rssi: null, decrypted: false, decrypted_info: null, }; } describe('packetNetworkGraph', () => { it('preserves canonical adjacency while projection hides ambiguous repeaters', () => { const selfKey = 'ffffffffffff0000000000000000000000000000000000000000000000000000'; const aliceKey = 'aaaaaaaaaaaa0000000000000000000000000000000000000000000000000000'; packetFixtures.set('dm-semantic-hide', { payloadType: PayloadType.TextMessage, messageHash: 'dm-semantic-hide', pathBytes: ['32'], srcHash: 'aaaaaaaaaaaa', dstHash: 'ffffffffffff', advertPubkey: null, groupTextSender: null, anonRequestPubkey: null, }); const state = createPacketNetworkState('Me'); const context = buildPacketNetworkContext({ contacts: [createContact(aliceKey, 'Alice')], config: createConfig(selfKey), repeaterAdvertPaths: [], splitAmbiguousByTraffic: false, useAdvertPathHints: false, }); ingestPacketIntoPacketNetwork(state, context, createPacket('dm-semantic-hide')); const hiddenProjection = projectPacketNetwork(state, { showAmbiguousNodes: false, showAmbiguousPaths: false, collapseLikelyKnownSiblingRepeaters: true, }); const shownProjection = projectPacketNetwork(state, { showAmbiguousNodes: false, showAmbiguousPaths: true, collapseLikelyKnownSiblingRepeaters: true, }); expect(snapshotNeighborIds(state)).toEqual( new Map([ ['?32', ['aaaaaaaaaaaa', 'self']], ['aaaaaaaaaaaa', ['?32']], ['self', ['?32']], ]) ); expect(hiddenProjection.links.has('aaaaaaaaaaaa->self')).toBe(true); expect(shownProjection.links.has('?32->aaaaaaaaaaaa')).toBe(true); expect(shownProjection.links.has('?32->self')).toBe(true); }); it('projects hidden ambiguous runs as dashed bridges but keeps later known repeaters visible', () => { const selfKey = 'ffffffffffff0000000000000000000000000000000000000000000000000000'; const aliceKey = 'aaaaaaaaaaaa0000000000000000000000000000000000000000000000000000'; const repeaterKey = '5656565656560000000000000000000000000000000000000000000000000000'; packetFixtures.set('dm-hidden-chain', { payloadType: PayloadType.TextMessage, messageHash: 'dm-hidden-chain', pathBytes: ['32', '565656565656'], srcHash: 'aaaaaaaaaaaa', dstHash: 'ffffffffffff', advertPubkey: null, groupTextSender: null, anonRequestPubkey: null, }); const state = createPacketNetworkState('Me'); const context = buildPacketNetworkContext({ contacts: [ createContact(aliceKey, 'Alice'), createContact(repeaterKey, 'Relay B', CONTACT_TYPE_REPEATER), ], config: createConfig(selfKey), repeaterAdvertPaths: [], splitAmbiguousByTraffic: false, useAdvertPathHints: false, }); const ingested = ingestPacketIntoPacketNetwork(state, context, createPacket('dm-hidden-chain')); expect(ingested?.canonicalPath).toEqual(['aaaaaaaaaaaa', '?32', '565656565656', 'self']); const projectedPath = projectCanonicalPath(state, ingested!.canonicalPath, { showAmbiguousNodes: false, showAmbiguousPaths: false, collapseLikelyKnownSiblingRepeaters: true, }); const projection = projectPacketNetwork(state, { showAmbiguousNodes: false, showAmbiguousPaths: false, collapseLikelyKnownSiblingRepeaters: true, }); expect(projectedPath.nodes).toEqual(['aaaaaaaaaaaa', '565656565656', 'self']); expect(Array.from(projectedPath.dashedLinkDetails.keys())).toEqual([ '565656565656->aaaaaaaaaaaa', ]); expect(projection.links.get('565656565656->aaaaaaaaaaaa')?.hasHiddenIntermediate).toBe(true); expect(projection.links.get('565656565656->self')?.hasDirectObservation).toBe(true); }); it('does not bridge across hidden ambiguous sender endpoints', () => { const selfKey = 'ffffffffffff0000000000000000000000000000000000000000000000000000'; const repeaterOneKey = '1111111111110000000000000000000000000000000000000000000000000000'; const repeaterTwoKey = '2222222222220000000000000000000000000000000000000000000000000000'; const repeaterThreeKey = '3333333333330000000000000000000000000000000000000000000000000000'; const repeaterFourKey = '4444444444440000000000000000000000000000000000000000000000000000'; packetFixtures.set('dm-hidden-ambiguous-sender-a', { payloadType: PayloadType.TextMessage, messageHash: 'dm-hidden-ambiguous-sender-a', pathBytes: ['111111111111', '222222222222'], srcHash: '32', dstHash: 'ffffffffffff', advertPubkey: null, groupTextSender: null, anonRequestPubkey: null, }); packetFixtures.set('dm-hidden-ambiguous-sender-b', { payloadType: PayloadType.TextMessage, messageHash: 'dm-hidden-ambiguous-sender-b', pathBytes: ['333333333333', '444444444444'], srcHash: '32', dstHash: 'ffffffffffff', advertPubkey: null, groupTextSender: null, anonRequestPubkey: null, }); const state = createPacketNetworkState('Me'); const context = buildPacketNetworkContext({ contacts: [ createContact(repeaterOneKey, 'Relay 1', CONTACT_TYPE_REPEATER), createContact(repeaterTwoKey, 'Relay 2', CONTACT_TYPE_REPEATER), createContact(repeaterThreeKey, 'Relay 3', CONTACT_TYPE_REPEATER), createContact(repeaterFourKey, 'Relay 4', CONTACT_TYPE_REPEATER), ], config: createConfig(selfKey), repeaterAdvertPaths: [], splitAmbiguousByTraffic: false, useAdvertPathHints: false, }); ingestPacketIntoPacketNetwork(state, context, createPacket('dm-hidden-ambiguous-sender-a')); ingestPacketIntoPacketNetwork(state, context, createPacket('dm-hidden-ambiguous-sender-b')); const projection = projectPacketNetwork(state, { showAmbiguousNodes: false, showAmbiguousPaths: true, collapseLikelyKnownSiblingRepeaters: true, }); expect(projection.links.has(buildLinkKey('111111111111', '222222222222'))).toBe(true); expect(projection.links.has(buildLinkKey('333333333333', '444444444444'))).toBe(true); expect(projection.links.has(buildLinkKey('111111111111', '333333333333'))).toBe(false); expect(projection.links.has(buildLinkKey('222222222222', '444444444444'))).toBe(false); }); it('does not add a DM recipient node from destination metadata alone', () => { const selfKey = 'ffffffffffff0000000000000000000000000000000000000000000000000000'; const aliceKey = 'aaaaaaaaaaaa0000000000000000000000000000000000000000000000000000'; const bobKey = 'bbbbbbbbbbbb0000000000000000000000000000000000000000000000000000'; const repeaterKey = '5656565656560000000000000000000000000000000000000000000000000000'; packetFixtures.set('dm-third-party-no-dst-node', { payloadType: PayloadType.TextMessage, messageHash: 'dm-third-party-no-dst-node', pathBytes: ['565656565656'], srcHash: 'aaaaaaaaaaaa', dstHash: 'bbbbbbbbbbbb', advertPubkey: null, groupTextSender: null, anonRequestPubkey: null, }); const state = createPacketNetworkState('Me'); const context = buildPacketNetworkContext({ contacts: [ createContact(aliceKey, 'Alice'), createContact(bobKey, 'Bob'), createContact(repeaterKey, 'Relay', CONTACT_TYPE_REPEATER), ], config: createConfig(selfKey), repeaterAdvertPaths: [], splitAmbiguousByTraffic: false, useAdvertPathHints: false, }); const ingested = ingestPacketIntoPacketNetwork( state, context, createPacket('dm-third-party-no-dst-node') ); expect(ingested?.canonicalPath).toEqual(['aaaaaaaaaaaa', '565656565656', 'self']); expect(state.nodes.has('bbbbbbbbbbbb')).toBe(false); expect(snapshotNeighborIds(state)).toEqual( new Map([ ['565656565656', ['aaaaaaaaaaaa', 'self']], ['aaaaaaaaaaaa', ['565656565656']], ['self', ['565656565656']], ]) ); }); it('replays real advert packets through the semantic layer', () => { const state = createPacketNetworkState('Me'); const context = buildPacketNetworkContext({ contacts: [], config: createConfig('ffffffffffff0000000000000000000000000000000000000000000000000000'), repeaterAdvertPaths: [], splitAmbiguousByTraffic: false, useAdvertPathHints: false, }); const packet = createPacket( '1106538B1CD273868576DC7F679B493F9AB5AC316173E1A56D3388BC3BA75F583F63AB0D1BA2A8ABD0BC6669DBF719E67E4C8517BA4E0D6F8C96A323E9D13A77F2630DED965A5C17C3EC6ED1601EEFE857749DA24E9F39CBEACD722C3708F433DB5FA9BAF0BAF9BC5B1241069290FEEB029A839EF843616E204F204D657368203220F09FA5AB' ); packet.payload_type = 'ADVERT'; const ingested = ingestPacketIntoPacketNetwork(state, context, packet); expect(ingested?.canonicalPath).toEqual([ '8576dc7f679b', '?53', '?8b', '?1c', '?d2', '?73', '?86', 'self', ]); expect(snapshotNeighborIds(state).get('?73')).toEqual(['?86', '?d2']); }); it('collapses a likely ambiguous repeater into its known sibling when both share the same next hop', () => { const selfKey = 'ffffffffffff0000000000000000000000000000000000000000000000000000'; const state = createPacketNetworkState('Me'); const context = buildPacketNetworkContext({ contacts: [ createContact('aaaaaaaaaaaa0000000000000000000000000000000000000000000000000000', 'Alice'), createContact('cccccccccccc0000000000000000000000000000000000000000000000000000', 'Carol'), createContact( '3232323232320000000000000000000000000000000000000000000000000000', 'Relay A', CONTACT_TYPE_REPEATER ), createContact( '32ababababab0000000000000000000000000000000000000000000000000000', 'Relay B', CONTACT_TYPE_REPEATER ), createContact( '5656565656560000000000000000000000000000000000000000000000000000', 'Relay Next', CONTACT_TYPE_REPEATER ), ], config: createConfig(selfKey), repeaterAdvertPaths: [ { public_key: '3232323232320000000000000000000000000000000000000000000000000000', paths: [ { path: '', path_len: 1, next_hop: '565656565656', first_seen: 1, last_seen: 2, heard_count: 4, }, ], }, ], splitAmbiguousByTraffic: false, useAdvertPathHints: true, }); packetFixtures.set('graph-ambiguous-sibling', { payloadType: PayloadType.TextMessage, messageHash: 'graph-ambiguous-sibling', pathBytes: ['32', '565656565656'], srcHash: 'aaaaaaaaaaaa', dstHash: 'ffffffffffff', advertPubkey: null, groupTextSender: null, anonRequestPubkey: null, }); packetFixtures.set('graph-known-sibling', { payloadType: PayloadType.TextMessage, messageHash: 'graph-known-sibling', pathBytes: ['323232323232', '565656565656'], srcHash: 'cccccccccccc', dstHash: 'ffffffffffff', advertPubkey: null, groupTextSender: null, anonRequestPubkey: null, }); ingestPacketIntoPacketNetwork(state, context, createPacket('graph-ambiguous-sibling')); ingestPacketIntoPacketNetwork(state, context, createPacket('graph-known-sibling')); const collapsed = projectPacketNetwork(state, { showAmbiguousNodes: false, showAmbiguousPaths: true, collapseLikelyKnownSiblingRepeaters: true, }); const separated = projectPacketNetwork(state, { showAmbiguousNodes: false, showAmbiguousPaths: true, collapseLikelyKnownSiblingRepeaters: false, }); expect(collapsed.renderedNodeIds.has('?32')).toBe(false); expect(collapsed.renderedNodeIds.has('323232323232')).toBe(true); expect(collapsed.links.has('323232323232->aaaaaaaaaaaa')).toBe(true); expect(separated.renderedNodeIds.has('?32')).toBe(true); expect(separated.links.has('?32->aaaaaaaaaaaa')).toBe(true); }); });