mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-05-06 13:33:02 +02:00
Add multipath tracking
This commit is contained in:
@@ -268,7 +268,7 @@ describe('Integration: ACK Events', () => {
|
||||
text: 'Hello',
|
||||
sender_timestamp: 1700000000,
|
||||
received_at: 1700000000,
|
||||
path: null,
|
||||
paths: null,
|
||||
txt_type: 0,
|
||||
signature: null,
|
||||
outgoing: true,
|
||||
@@ -301,7 +301,7 @@ describe('Integration: ACK Events', () => {
|
||||
text: 'Hello',
|
||||
sender_timestamp: 1700000000,
|
||||
received_at: 1700000000,
|
||||
path: null,
|
||||
paths: null,
|
||||
txt_type: 0,
|
||||
signature: null,
|
||||
outgoing: true,
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
getHopCount,
|
||||
resolvePath,
|
||||
formatDistance,
|
||||
formatHopCounts,
|
||||
} from '../utils/pathUtils';
|
||||
import type { Contact, RadioConfig } from '../types';
|
||||
import { CONTACT_TYPE_REPEATER, CONTACT_TYPE_CLIENT } from '../types';
|
||||
@@ -573,3 +574,66 @@ describe('formatDistance', () => {
|
||||
expect(formatDistance(0.001)).toBe('1m');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatHopCounts', () => {
|
||||
it('returns empty for null paths', () => {
|
||||
const result = formatHopCounts(null);
|
||||
expect(result.display).toBe('');
|
||||
expect(result.allDirect).toBe(true);
|
||||
expect(result.hasMultiple).toBe(false);
|
||||
});
|
||||
|
||||
it('returns empty for empty paths array', () => {
|
||||
const result = formatHopCounts([]);
|
||||
expect(result.display).toBe('');
|
||||
expect(result.allDirect).toBe(true);
|
||||
expect(result.hasMultiple).toBe(false);
|
||||
});
|
||||
|
||||
it('formats single direct path as "d"', () => {
|
||||
const result = formatHopCounts([{ path: '', received_at: 1700000000 }]);
|
||||
expect(result.display).toBe('d');
|
||||
expect(result.allDirect).toBe(true);
|
||||
expect(result.hasMultiple).toBe(false);
|
||||
});
|
||||
|
||||
it('formats single multi-hop path with hop count', () => {
|
||||
const result = formatHopCounts([{ path: '1A2B', received_at: 1700000000 }]);
|
||||
expect(result.display).toBe('2');
|
||||
expect(result.allDirect).toBe(false);
|
||||
expect(result.hasMultiple).toBe(false);
|
||||
});
|
||||
|
||||
it('formats multiple paths sorted by hop count', () => {
|
||||
const result = formatHopCounts([
|
||||
{ path: '1A2B3C', received_at: 1700000000 }, // 3 hops
|
||||
{ path: '', received_at: 1700000001 }, // direct
|
||||
{ path: '1A', received_at: 1700000002 }, // 1 hop
|
||||
{ path: '1A2B3C', received_at: 1700000003 }, // 3 hops
|
||||
]);
|
||||
expect(result.display).toBe('d/1/3/3');
|
||||
expect(result.allDirect).toBe(false);
|
||||
expect(result.hasMultiple).toBe(true);
|
||||
});
|
||||
|
||||
it('formats multiple direct paths', () => {
|
||||
const result = formatHopCounts([
|
||||
{ path: '', received_at: 1700000000 },
|
||||
{ path: '', received_at: 1700000001 },
|
||||
]);
|
||||
expect(result.display).toBe('d/d');
|
||||
expect(result.allDirect).toBe(true);
|
||||
expect(result.hasMultiple).toBe(true);
|
||||
});
|
||||
|
||||
it('handles mixed paths with multiple direct routes', () => {
|
||||
const result = formatHopCounts([
|
||||
{ path: '1A', received_at: 1700000000 }, // 1 hop
|
||||
{ path: '', received_at: 1700000001 }, // direct
|
||||
{ path: '', received_at: 1700000002 }, // direct
|
||||
]);
|
||||
expect(result.display).toBe('d/d/1');
|
||||
expect(result.allDirect).toBe(false);
|
||||
expect(result.hasMultiple).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -72,7 +72,7 @@ describe('shouldIncrementUnread', () => {
|
||||
text: 'Test',
|
||||
sender_timestamp: null,
|
||||
received_at: Date.now(),
|
||||
path: null,
|
||||
paths: null,
|
||||
txt_type: 0,
|
||||
signature: null,
|
||||
outgoing: false,
|
||||
|
||||
@@ -16,7 +16,7 @@ function createMessage(overrides: Partial<Message> = {}): Message {
|
||||
text: 'Hello world',
|
||||
sender_timestamp: 1700000000,
|
||||
received_at: 1700000001,
|
||||
path: null,
|
||||
paths: null,
|
||||
txt_type: 0,
|
||||
signature: null,
|
||||
outgoing: false,
|
||||
@@ -110,3 +110,75 @@ describe('getMessageContentKey', () => {
|
||||
expect(key).toContain('Hello: World! @user #channel');
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateMessageAck logic', () => {
|
||||
// Test the logic that updateMessageAck applies to messages
|
||||
// This simulates what the setMessages callback does
|
||||
|
||||
function applyAckUpdate(
|
||||
messages: Message[],
|
||||
messageId: number,
|
||||
ackCount: number,
|
||||
paths?: { path: string; received_at: number }[]
|
||||
): Message[] {
|
||||
const idx = messages.findIndex((m) => m.id === messageId);
|
||||
if (idx >= 0) {
|
||||
const updated = [...messages];
|
||||
updated[idx] = {
|
||||
...messages[idx],
|
||||
acked: ackCount,
|
||||
...(paths !== undefined && { paths }),
|
||||
};
|
||||
return updated;
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
it('updates ack count for existing message', () => {
|
||||
const messages = [createMessage({ id: 42, acked: 0 })];
|
||||
|
||||
const updated = applyAckUpdate(messages, 42, 3);
|
||||
|
||||
expect(updated[0].acked).toBe(3);
|
||||
});
|
||||
|
||||
it('updates paths when provided', () => {
|
||||
const messages = [createMessage({ id: 42, acked: 0, paths: null })];
|
||||
const newPaths = [
|
||||
{ path: '1A2B', received_at: 1700000000 },
|
||||
{ path: '1A3C', received_at: 1700000005 },
|
||||
];
|
||||
|
||||
const updated = applyAckUpdate(messages, 42, 2, newPaths);
|
||||
|
||||
expect(updated[0].acked).toBe(2);
|
||||
expect(updated[0].paths).toEqual(newPaths);
|
||||
});
|
||||
|
||||
it('does not modify paths when not provided', () => {
|
||||
const existingPaths = [{ path: '1A2B', received_at: 1700000000 }];
|
||||
const messages = [createMessage({ id: 42, acked: 1, paths: existingPaths })];
|
||||
|
||||
const updated = applyAckUpdate(messages, 42, 2);
|
||||
|
||||
expect(updated[0].acked).toBe(2);
|
||||
expect(updated[0].paths).toEqual(existingPaths); // Unchanged
|
||||
});
|
||||
|
||||
it('returns unchanged array for unknown message id', () => {
|
||||
const messages = [createMessage({ id: 42, acked: 0 })];
|
||||
|
||||
const updated = applyAckUpdate(messages, 999, 3);
|
||||
|
||||
expect(updated).toEqual(messages);
|
||||
expect(updated[0].acked).toBe(0); // Unchanged
|
||||
});
|
||||
|
||||
it('handles empty paths array', () => {
|
||||
const messages = [createMessage({ id: 42, acked: 0, paths: null })];
|
||||
|
||||
const updated = applyAckUpdate(messages, 42, 1, []);
|
||||
|
||||
expect(updated[0].paths).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import type { HealthStatus, Contact, Channel, Message, RawPacket } from '../types';
|
||||
import type { HealthStatus, Contact, Channel, Message, MessagePath, RawPacket } from '../types';
|
||||
|
||||
/**
|
||||
* Parse and route a WebSocket message.
|
||||
@@ -21,7 +21,7 @@ function parseWebSocketMessage(
|
||||
onMessage?: (message: Message) => void;
|
||||
onContact?: (contact: Contact) => void;
|
||||
onRawPacket?: (packet: RawPacket) => void;
|
||||
onMessageAcked?: (messageId: number, ackCount: number) => void;
|
||||
onMessageAcked?: (messageId: number, ackCount: number, paths?: MessagePath[]) => void;
|
||||
}
|
||||
): { type: string; handled: boolean } {
|
||||
try {
|
||||
@@ -47,8 +47,12 @@ function parseWebSocketMessage(
|
||||
handlers.onRawPacket?.(msg.data as RawPacket);
|
||||
return { type: msg.type, handled: !!handlers.onRawPacket };
|
||||
case 'message_acked': {
|
||||
const ackData = msg.data as { message_id: number; ack_count: number };
|
||||
handlers.onMessageAcked?.(ackData.message_id, ackData.ack_count);
|
||||
const ackData = msg.data as {
|
||||
message_id: number;
|
||||
ack_count: number;
|
||||
paths?: MessagePath[];
|
||||
};
|
||||
handlers.onMessageAcked?.(ackData.message_id, ackData.ack_count, ackData.paths);
|
||||
return { type: msg.type, handled: !!handlers.onMessageAcked };
|
||||
}
|
||||
case 'pong':
|
||||
@@ -90,7 +94,25 @@ describe('parseWebSocketMessage', () => {
|
||||
|
||||
expect(result.type).toBe('message_acked');
|
||||
expect(result.handled).toBe(true);
|
||||
expect(onMessageAcked).toHaveBeenCalledWith(42, 3);
|
||||
expect(onMessageAcked).toHaveBeenCalledWith(42, 3, undefined);
|
||||
});
|
||||
|
||||
it('routes message_acked with paths array', () => {
|
||||
const onMessageAcked = vi.fn();
|
||||
const paths = [
|
||||
{ path: '1A2B', received_at: 1700000000 },
|
||||
{ path: '1A3C', received_at: 1700000005 },
|
||||
];
|
||||
const data = JSON.stringify({
|
||||
type: 'message_acked',
|
||||
data: { message_id: 42, ack_count: 2, paths },
|
||||
});
|
||||
|
||||
const result = parseWebSocketMessage(data, { onMessageAcked });
|
||||
|
||||
expect(result.type).toBe('message_acked');
|
||||
expect(result.handled).toBe(true);
|
||||
expect(onMessageAcked).toHaveBeenCalledWith(42, 2, paths);
|
||||
});
|
||||
|
||||
it('routes new message to onMessage handler', () => {
|
||||
|
||||
Reference in New Issue
Block a user