mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-08-07 01:03:34 +02:00
Fix ack/message race condition where out of sequence acks and messages would cause dropped acks
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
import { act, renderHook, waitFor } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import * as messageCache from '../messageCache';
|
||||
import { useConversationMessages } from '../hooks/useConversationMessages';
|
||||
import type { Conversation, Message } from '../types';
|
||||
|
||||
const mockGetMessages = vi.fn<(...args: unknown[]) => Promise<Message[]>>();
|
||||
|
||||
vi.mock('../api', () => ({
|
||||
api: {
|
||||
getMessages: (...args: unknown[]) => mockGetMessages(...args),
|
||||
},
|
||||
isAbortError: (err: unknown) => err instanceof DOMException && err.name === 'AbortError',
|
||||
}));
|
||||
|
||||
function createConversation(): Conversation {
|
||||
return {
|
||||
type: 'contact',
|
||||
id: 'abc123',
|
||||
name: 'Test Contact',
|
||||
};
|
||||
}
|
||||
|
||||
function createMessage(overrides: Partial<Message> = {}): Message {
|
||||
return {
|
||||
id: 42,
|
||||
type: 'PRIV',
|
||||
conversation_key: 'abc123',
|
||||
text: 'hello',
|
||||
sender_timestamp: 1700000000,
|
||||
received_at: 1700000001,
|
||||
paths: null,
|
||||
txt_type: 0,
|
||||
signature: null,
|
||||
outgoing: true,
|
||||
acked: 0,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function createDeferred<T>() {
|
||||
let resolve: (value: T | PromiseLike<T>) => void = () => {};
|
||||
const promise = new Promise<T>((res) => {
|
||||
resolve = res;
|
||||
});
|
||||
return { promise, resolve };
|
||||
}
|
||||
|
||||
describe('useConversationMessages ACK ordering', () => {
|
||||
beforeEach(() => {
|
||||
mockGetMessages.mockReset();
|
||||
messageCache.clear();
|
||||
});
|
||||
|
||||
it('applies buffered ACK when message is added after ACK event', async () => {
|
||||
mockGetMessages.mockResolvedValueOnce([]);
|
||||
|
||||
const { result } = renderHook(() => useConversationMessages(createConversation()));
|
||||
|
||||
await waitFor(() => expect(mockGetMessages).toHaveBeenCalledTimes(1));
|
||||
await waitFor(() => expect(result.current.messagesLoading).toBe(false));
|
||||
|
||||
const paths = [{ path: 'A1B2', received_at: 1700000010 }];
|
||||
act(() => {
|
||||
result.current.updateMessageAck(42, 2, paths);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
const added = result.current.addMessageIfNew(createMessage({ id: 42, acked: 0, paths: null }));
|
||||
expect(added).toBe(true);
|
||||
});
|
||||
|
||||
expect(result.current.messages).toHaveLength(1);
|
||||
expect(result.current.messages[0].acked).toBe(2);
|
||||
expect(result.current.messages[0].paths).toEqual(paths);
|
||||
});
|
||||
|
||||
it('applies buffered ACK to message returned by in-flight fetch', async () => {
|
||||
const deferred = createDeferred<Message[]>();
|
||||
mockGetMessages.mockReturnValueOnce(deferred.promise);
|
||||
|
||||
const { result } = renderHook(() => useConversationMessages(createConversation()));
|
||||
await waitFor(() => expect(mockGetMessages).toHaveBeenCalledTimes(1));
|
||||
|
||||
const paths = [{ path: 'C3D4', received_at: 1700000011 }];
|
||||
act(() => {
|
||||
result.current.updateMessageAck(42, 1, paths);
|
||||
});
|
||||
|
||||
deferred.resolve([createMessage({ id: 42, acked: 0, paths: null })]);
|
||||
|
||||
await waitFor(() => expect(result.current.messages).toHaveLength(1));
|
||||
expect(result.current.messages[0].acked).toBe(1);
|
||||
expect(result.current.messages[0].paths).toEqual(paths);
|
||||
});
|
||||
|
||||
it('keeps highest ACK state when out-of-order ACK updates arrive', async () => {
|
||||
mockGetMessages.mockResolvedValueOnce([]);
|
||||
|
||||
const { result } = renderHook(() => useConversationMessages(createConversation()));
|
||||
|
||||
await waitFor(() => expect(mockGetMessages).toHaveBeenCalledTimes(1));
|
||||
await waitFor(() => expect(result.current.messagesLoading).toBe(false));
|
||||
|
||||
act(() => {
|
||||
result.current.addMessageIfNew(createMessage({ id: 42, acked: 0, paths: null }));
|
||||
});
|
||||
|
||||
const highAckPaths = [
|
||||
{ path: 'A1B2', received_at: 1700000010 },
|
||||
{ path: 'A1C3', received_at: 1700000011 },
|
||||
];
|
||||
const staleAckPaths = [{ path: 'A1B2', received_at: 1700000010 }];
|
||||
|
||||
act(() => {
|
||||
result.current.updateMessageAck(42, 3, highAckPaths);
|
||||
result.current.updateMessageAck(42, 2, staleAckPaths);
|
||||
});
|
||||
|
||||
expect(result.current.messages[0].acked).toBe(3);
|
||||
expect(result.current.messages[0].paths).toEqual(highAckPaths);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user