mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-05-02 03:23:00 +02:00
240 lines
8.4 KiB
TypeScript
240 lines
8.4 KiB
TypeScript
/**
|
|
* Tests for useConversationMessages hook utilities.
|
|
*
|
|
* These tests verify the message deduplication key generation.
|
|
*/
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
import { mergePendingAck } from '../hooks/useConversationMessages';
|
|
import { getMessageContentKey } from '../utils/messageIdentity';
|
|
import type { Message } from '../types';
|
|
|
|
function createMessage(overrides: Partial<Message> = {}): Message {
|
|
return {
|
|
id: 1,
|
|
type: 'CHAN',
|
|
conversation_key: 'channel123',
|
|
text: 'Hello world',
|
|
sender_timestamp: 1700000000,
|
|
received_at: 1700000001,
|
|
paths: null,
|
|
txt_type: 0,
|
|
signature: null,
|
|
sender_key: null,
|
|
outgoing: false,
|
|
acked: 0,
|
|
sender_name: null,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('getMessageContentKey', () => {
|
|
it('generates key from type, conversation_key, text, and sender_timestamp', () => {
|
|
const msg = createMessage({
|
|
type: 'CHAN',
|
|
conversation_key: 'abc123',
|
|
text: 'Hello',
|
|
sender_timestamp: 1700000000,
|
|
});
|
|
|
|
const key = getMessageContentKey(msg);
|
|
|
|
expect(key).toBe('CHAN-abc123-Hello-1700000000');
|
|
});
|
|
|
|
it('generates different keys for different message types', () => {
|
|
const chanMsg = createMessage({ type: 'CHAN' });
|
|
const privMsg = createMessage({ type: 'PRIV' });
|
|
|
|
expect(getMessageContentKey(chanMsg)).not.toBe(getMessageContentKey(privMsg));
|
|
});
|
|
|
|
it('generates different keys for different conversation keys', () => {
|
|
const msg1 = createMessage({ conversation_key: 'channel1' });
|
|
const msg2 = createMessage({ conversation_key: 'channel2' });
|
|
|
|
expect(getMessageContentKey(msg1)).not.toBe(getMessageContentKey(msg2));
|
|
});
|
|
|
|
it('generates different keys for different text', () => {
|
|
const msg1 = createMessage({ text: 'Hello' });
|
|
const msg2 = createMessage({ text: 'World' });
|
|
|
|
expect(getMessageContentKey(msg1)).not.toBe(getMessageContentKey(msg2));
|
|
});
|
|
|
|
it('generates different keys for different timestamps', () => {
|
|
const msg1 = createMessage({ sender_timestamp: 1700000000 });
|
|
const msg2 = createMessage({ sender_timestamp: 1700000001 });
|
|
|
|
expect(getMessageContentKey(msg1)).not.toBe(getMessageContentKey(msg2));
|
|
});
|
|
|
|
it('generates same key for messages with same content', () => {
|
|
const msg1 = createMessage({
|
|
id: 1,
|
|
type: 'CHAN',
|
|
conversation_key: 'abc',
|
|
text: 'Test',
|
|
sender_timestamp: 1700000000,
|
|
});
|
|
const msg2 = createMessage({
|
|
id: 2, // Different ID
|
|
type: 'CHAN',
|
|
conversation_key: 'abc',
|
|
text: 'Test',
|
|
sender_timestamp: 1700000000,
|
|
});
|
|
|
|
expect(getMessageContentKey(msg1)).toBe(getMessageContentKey(msg2));
|
|
});
|
|
|
|
it('handles null sender_timestamp by falling back to received_at and id', () => {
|
|
const msg = createMessage({ sender_timestamp: null });
|
|
|
|
const key = getMessageContentKey(msg);
|
|
|
|
// Falls back to `r${received_at}-${id}` when sender_timestamp is null
|
|
expect(key).toBe('CHAN-channel123-Hello world-r1700000001-1');
|
|
});
|
|
|
|
it('handles empty text', () => {
|
|
const msg = createMessage({ text: '' });
|
|
|
|
const key = getMessageContentKey(msg);
|
|
|
|
expect(key).toContain('--'); // Empty text between dashes
|
|
});
|
|
|
|
it('handles text with special characters', () => {
|
|
const msg = createMessage({ text: 'Hello: World! @user #channel' });
|
|
|
|
const key = getMessageContentKey(msg);
|
|
|
|
expect(key).toContain('Hello: World! @user #channel');
|
|
});
|
|
|
|
it('null-timestamp messages with different received_at produce different keys', () => {
|
|
const msg1 = createMessage({ sender_timestamp: null, received_at: 1700000001 });
|
|
const msg2 = createMessage({ sender_timestamp: null, received_at: 1700000002 });
|
|
|
|
expect(getMessageContentKey(msg1)).not.toBe(getMessageContentKey(msg2));
|
|
});
|
|
|
|
it('null-timestamp key does not collide with numeric timestamp key', () => {
|
|
// A message with sender_timestamp=null and received_at=123
|
|
// should not match a message with sender_timestamp that looks similar
|
|
const nullTsMsg = createMessage({ sender_timestamp: null, received_at: 123 });
|
|
const numericTsMsg = createMessage({ sender_timestamp: 123 });
|
|
|
|
expect(getMessageContentKey(nullTsMsg)).not.toBe(getMessageContentKey(numericTsMsg));
|
|
});
|
|
|
|
it('same text and null timestamp but different conversations produce different keys', () => {
|
|
const msg1 = createMessage({
|
|
sender_timestamp: null,
|
|
conversation_key: 'chan1',
|
|
received_at: 1700000001,
|
|
});
|
|
const msg2 = createMessage({
|
|
sender_timestamp: null,
|
|
conversation_key: 'chan2',
|
|
received_at: 1700000001,
|
|
});
|
|
|
|
expect(getMessageContentKey(msg1)).not.toBe(getMessageContentKey(msg2));
|
|
});
|
|
|
|
it('null-timestamp messages with same text and same received_at but different ids produce different keys', () => {
|
|
// This is the key fix: two genuinely different messages arriving in the same second
|
|
// with null sender_timestamp must NOT collide, even if text is identical
|
|
const msg1 = createMessage({ id: 10, sender_timestamp: null, received_at: 1700000001 });
|
|
const msg2 = createMessage({ id: 11, sender_timestamp: null, received_at: 1700000001 });
|
|
|
|
expect(getMessageContentKey(msg1)).not.toBe(getMessageContentKey(msg2));
|
|
});
|
|
|
|
it('null-timestamp messages with same id produce same key (true duplicates dedup)', () => {
|
|
// Same message arriving via WS + API fetch has the same id — should still dedup
|
|
const msg1 = createMessage({ id: 42, sender_timestamp: null, received_at: 1700000001 });
|
|
const msg2 = createMessage({ id: 42, sender_timestamp: null, received_at: 1700000001 });
|
|
|
|
expect(getMessageContentKey(msg1)).toBe(getMessageContentKey(msg2));
|
|
});
|
|
});
|
|
|
|
describe('mergePendingAck', () => {
|
|
const paths1 = [{ path: 'A1B2', received_at: 1700000000 }];
|
|
const paths2 = [
|
|
{ path: 'A1B2', received_at: 1700000000 },
|
|
{ path: 'C3D4', received_at: 1700000001 },
|
|
];
|
|
|
|
it('creates new entry when no existing state', () => {
|
|
const result = mergePendingAck(undefined, 1, paths1);
|
|
expect(result).toEqual({ ackCount: 1, paths: paths1 });
|
|
});
|
|
|
|
it('creates new entry without paths when paths not provided', () => {
|
|
const result = mergePendingAck(undefined, 1);
|
|
expect(result).toEqual({ ackCount: 1 });
|
|
expect('paths' in result).toBe(false);
|
|
});
|
|
|
|
it('higher ack count replaces existing', () => {
|
|
const existing = { ackCount: 1, paths: paths1 };
|
|
const result = mergePendingAck(existing, 3, paths2);
|
|
expect(result).toEqual({ ackCount: 3, paths: paths2 });
|
|
});
|
|
|
|
it('higher ack count preserves existing paths when new paths undefined', () => {
|
|
const existing = { ackCount: 1, paths: paths1 };
|
|
const result = mergePendingAck(existing, 3);
|
|
expect(result).toEqual({ ackCount: 3, paths: paths1 });
|
|
});
|
|
|
|
it('higher ack count drops existing paths when new paths explicitly provided', () => {
|
|
const existing = { ackCount: 1, paths: paths2 };
|
|
const result = mergePendingAck(existing, 3, paths1);
|
|
expect(result).toEqual({ ackCount: 3, paths: paths1 });
|
|
});
|
|
|
|
it('lower ack count is ignored entirely', () => {
|
|
const existing = { ackCount: 5, paths: paths2 };
|
|
const result = mergePendingAck(existing, 2, paths1);
|
|
expect(result).toBe(existing);
|
|
});
|
|
|
|
it('same ack count with no new paths returns existing', () => {
|
|
const existing = { ackCount: 3, paths: paths1 };
|
|
const result = mergePendingAck(existing, 3);
|
|
expect(result).toBe(existing);
|
|
});
|
|
|
|
it('same ack count with more paths replaces', () => {
|
|
const existing = { ackCount: 3, paths: paths1 };
|
|
const result = mergePendingAck(existing, 3, paths2);
|
|
expect(result).toEqual({ ackCount: 3, paths: paths2 });
|
|
});
|
|
|
|
it('same ack count with fewer paths keeps existing', () => {
|
|
const existing = { ackCount: 3, paths: paths2 };
|
|
const result = mergePendingAck(existing, 3, paths1);
|
|
expect(result).toBe(existing);
|
|
});
|
|
|
|
it('same ack count with equal-length paths replaces (uses >=)', () => {
|
|
const existing = { ackCount: 3, paths: paths1 };
|
|
const newPaths = [{ path: 'X1Y2', received_at: 1700000005 }];
|
|
const result = mergePendingAck(existing, 3, newPaths);
|
|
expect(result).toEqual({ ackCount: 3, paths: newPaths });
|
|
});
|
|
|
|
it('same ack count with paths when existing has no paths', () => {
|
|
const existing = { ackCount: 2 };
|
|
const result = mergePendingAck(existing, 2, paths1);
|
|
// existing.paths is undefined → length -1, paths1.length (1) >= -1 → replaces
|
|
expect(result).toEqual({ ackCount: 2, paths: paths1 });
|
|
});
|
|
});
|