mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-03-28 17:43:05 +01:00
404 lines
14 KiB
TypeScript
404 lines
14 KiB
TypeScript
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<string, unknown>(),
|
|
}));
|
|
|
|
vi.mock('../utils/visualizerUtils', async () => {
|
|
const actual = await vi.importActual<typeof import('../utils/visualizerUtils')>(
|
|
'../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);
|
|
});
|
|
});
|