Add packet feed clickable packet inspection. Closes #75 again.

This commit is contained in:
Jack Kingsman
2026-03-19 09:43:57 -07:00
parent 41a297c944
commit b85d451e26
10 changed files with 1320 additions and 240 deletions
+152 -63
View File
@@ -3,7 +3,23 @@ import { afterEach, describe, expect, it, vi } from 'vitest';
import { RawPacketFeedView } from '../components/RawPacketFeedView';
import type { RawPacketStatsSessionState } from '../utils/rawPacketStats';
import type { Contact, RawPacket } from '../types';
import type { Channel, Contact, RawPacket } from '../types';
const GROUP_TEXT_PACKET_HEX =
'1500E69C7A89DD0AF6A2D69F5823B88F9720731E4B887C56932BF889255D8D926D99195927144323A42DD8A158F878B518B8304DF55E80501C7D02A9FFD578D3518283156BBA257BF8413E80A237393B2E4149BBBC864371140A9BBC4E23EB9BF203EF0D029214B3E3AAC3C0295690ACDB89A28619E7E5F22C83E16073AD679D25FA904D07E5ACF1DB5A7C77D7E1719FB9AE5BF55541EE0D7F59ED890E12CF0FEED6700818';
const TEST_CHANNEL: Channel = {
key: '7ABA109EDCF304A84433CB71D0F3AB73',
name: '#six77',
is_hashtag: true,
on_radio: false,
last_read_at: null,
};
const COLLIDING_TEST_CHANNEL: Channel = {
...TEST_CHANNEL,
name: '#collision',
};
function createSession(
overrides: Partial<RawPacketStatsSessionState> = {}
@@ -78,15 +94,34 @@ function createContact(overrides: Partial<Contact> = {}): Contact {
};
}
function renderView({
packets = [],
contacts = [],
channels = [],
rawPacketStatsSession = createSession(),
}: {
packets?: RawPacket[];
contacts?: Contact[];
channels?: Channel[];
rawPacketStatsSession?: RawPacketStatsSessionState;
} = {}) {
return render(
<RawPacketFeedView
packets={packets}
rawPacketStatsSession={rawPacketStatsSession}
contacts={contacts}
channels={channels}
/>
);
}
describe('RawPacketFeedView', () => {
afterEach(() => {
vi.unstubAllGlobals();
});
it('opens a stats drawer with window controls and grouped summaries', () => {
render(
<RawPacketFeedView packets={[]} rawPacketStatsSession={createSession()} contacts={[]} />
);
renderView();
expect(screen.getByText('Raw Packet Feed')).toBeInTheDocument();
expect(screen.queryByText('Packet Types')).not.toBeInTheDocument();
@@ -95,6 +130,7 @@ describe('RawPacketFeedView', () => {
expect(screen.getByLabelText('Stats window')).toBeInTheDocument();
expect(screen.getByText('Packet Types')).toBeInTheDocument();
expect(screen.getByText('Hop Byte Width')).toBeInTheDocument();
expect(screen.getByText('Most-Heard Neighbors')).toBeInTheDocument();
expect(screen.getByText('Traffic Timeline')).toBeInTheDocument();
});
@@ -114,11 +150,10 @@ describe('RawPacketFeedView', () => {
}))
);
render(
<RawPacketFeedView packets={[]} rawPacketStatsSession={createSession()} contacts={[]} />
);
renderView();
expect(screen.getByText('Packet Types')).toBeInTheDocument();
expect(screen.getByText('Hop Byte Width')).toBeInTheDocument();
expect(screen.getByRole('button', { name: /hide stats/i })).toBeInTheDocument();
});
@@ -161,13 +196,11 @@ describe('RawPacketFeedView', () => {
],
});
const { rerender } = render(
<RawPacketFeedView
packets={initialPackets}
rawPacketStatsSession={initialSession}
contacts={[]}
/>
);
const { rerender } = renderView({
packets: initialPackets,
rawPacketStatsSession: initialSession,
contacts: [],
});
fireEvent.click(screen.getByRole('button', { name: /show stats/i }));
fireEvent.change(screen.getByLabelText('Stats window'), { target: { value: '1m' } });
@@ -179,6 +212,7 @@ describe('RawPacketFeedView', () => {
packets={nextPackets}
rawPacketStatsSession={initialSession}
contacts={[]}
channels={[]}
/>
);
expect(screen.getByText(/only covered for 50 sec/i)).toBeInTheDocument();
@@ -195,7 +229,12 @@ describe('RawPacketFeedView', () => {
],
};
rerender(
<RawPacketFeedView packets={nextPackets} rawPacketStatsSession={nextSession} contacts={[]} />
<RawPacketFeedView
packets={nextPackets}
rawPacketStatsSession={nextSession}
contacts={[]}
channels={[]}
/>
);
expect(screen.getByText(/only covered for 10 sec/i)).toBeInTheDocument();
@@ -203,30 +242,27 @@ describe('RawPacketFeedView', () => {
});
it('resolves neighbor labels from matching contacts when identity is available', () => {
render(
<RawPacketFeedView
packets={[]}
rawPacketStatsSession={createSession({
totalObservedPackets: 1,
observations: [
{
observationKey: 'obs-1',
timestamp: 1_700_000_000,
payloadType: 'Advert',
routeType: 'Flood',
decrypted: false,
rssi: -70,
snr: 6,
sourceKey: 'AA11BB22CC33',
sourceLabel: 'AA11BB22CC33',
pathTokenCount: 1,
pathSignature: '01',
},
],
})}
contacts={[createContact()]}
/>
);
renderView({
rawPacketStatsSession: createSession({
totalObservedPackets: 1,
observations: [
{
observationKey: 'obs-1',
timestamp: 1_700_000_000,
payloadType: 'Advert',
routeType: 'Flood',
decrypted: false,
rssi: -70,
snr: 6,
sourceKey: 'AA11BB22CC33',
sourceLabel: 'AA11BB22CC33',
pathTokenCount: 1,
pathSignature: '01',
},
],
}),
contacts: [createContact()],
});
fireEvent.click(screen.getByRole('button', { name: /show stats/i }));
fireEvent.change(screen.getByLabelText('Stats window'), { target: { value: 'session' } });
@@ -234,33 +270,86 @@ describe('RawPacketFeedView', () => {
});
it('marks unresolved neighbor identities explicitly', () => {
render(
<RawPacketFeedView
packets={[]}
rawPacketStatsSession={createSession({
totalObservedPackets: 1,
observations: [
{
observationKey: 'obs-1',
timestamp: 1_700_000_000,
payloadType: 'Advert',
routeType: 'Flood',
decrypted: false,
rssi: -70,
snr: 6,
sourceKey: 'DEADBEEF1234',
sourceLabel: 'DEADBEEF1234',
pathTokenCount: 1,
pathSignature: '01',
},
],
})}
contacts={[]}
/>
);
renderView({
rawPacketStatsSession: createSession({
totalObservedPackets: 1,
observations: [
{
observationKey: 'obs-1',
timestamp: 1_700_000_000,
payloadType: 'Advert',
routeType: 'Flood',
decrypted: false,
rssi: -70,
snr: 6,
sourceKey: 'DEADBEEF1234',
sourceLabel: 'DEADBEEF1234',
pathTokenCount: 1,
pathSignature: '01',
},
],
}),
contacts: [],
});
fireEvent.click(screen.getByRole('button', { name: /show stats/i }));
fireEvent.change(screen.getByLabelText('Stats window'), { target: { value: 'session' } });
expect(screen.getAllByText('Identity not resolvable').length).toBeGreaterThan(0);
});
it('opens a packet detail modal from the raw feed and decrypts room messages when a key is loaded', () => {
renderView({
packets: [
{
id: 1,
observation_id: 10,
timestamp: 1_700_000_000,
data: GROUP_TEXT_PACKET_HEX,
decrypted: false,
payload_type: 'GroupText',
rssi: -72,
snr: 5.5,
decrypted_info: null,
},
],
channels: [TEST_CHANNEL],
});
fireEvent.click(screen.getByRole('button', { name: /gt from flightless/i }));
expect(screen.getByText('Packet Details')).toBeInTheDocument();
expect(screen.getByText('Payload fields')).toBeInTheDocument();
expect(screen.getByText('Full packet hex')).toBeInTheDocument();
expect(screen.getByText('#six77')).toBeInTheDocument();
expect(screen.getByText(/bytes · decrypted/i)).toBeInTheDocument();
expect(screen.getAllByText(/sender: flightless/i).length).toBeGreaterThan(0);
expect(
screen.getByText(/hello there; this hashtag room is essentially public/i)
).toBeInTheDocument();
});
it('does not guess a room name when multiple loaded channels collide on the group hash', () => {
renderView({
packets: [
{
id: 1,
observation_id: 10,
timestamp: 1_700_000_000,
data: GROUP_TEXT_PACKET_HEX,
decrypted: false,
payload_type: 'GroupText',
rssi: -72,
snr: 5.5,
decrypted_info: null,
},
],
channels: [TEST_CHANNEL, COLLIDING_TEST_CHANNEL],
});
fireEvent.click(screen.getByRole('button', { name: /gt from flightless/i }));
expect(screen.getByText(/channel hash e6/i)).toBeInTheDocument();
expect(screen.queryByText('#six77')).not.toBeInTheDocument();
expect(screen.queryByText('#collision')).not.toBeInTheDocument();
});
});