FERS 1.0.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
syncQueue.test.ts
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (c) 2026-present FERS Contributors (see AUTHORS.md).
3
4import { beforeEach, describe, expect, test } from 'bun:test';
5import {
6 enqueueGranularSync,
7 type GranularSyncFailure,
8 registerGranularSyncFailureHandler,
9 resetSyncQueueForTests,
10 setSyncQueueInvokerForTests,
11 waitForSyncIdle,
12} from './syncQueue';
13
14type InvokeFn = typeof import('@tauri-apps/api/core').invoke;
15
16function delay(ms: number): Promise<void> {
17 return new Promise((resolve) => setTimeout(resolve, ms));
18}
19
20function createDeferred() {
21 let resolve!: () => void;
22 const promise = new Promise<void>((res) => {
23 resolve = res;
24 });
25 return { promise, resolve };
26}
27
28describe('syncQueue granular recovery', () => {
29 beforeEach(() => {
30 resetSyncQueueForTests();
31 });
32
33 test('does not invoke recovery when granular sync succeeds', async () => {
34 const invocations: string[] = [];
35 const failures: GranularSyncFailure[] = [];
36
37 setSyncQueueInvokerForTests((async (
38 command: string,
39 args?: Record<string, unknown>
40 ) => {
41 invocations.push(`${command}:${String(args?.itemId ?? '')}`);
42 }) as InvokeFn);
43 registerGranularSyncFailureHandler((failure) => {
44 failures.push(failure);
45 });
46
47 await enqueueGranularSync('Waveform', '10', '{"id":"10"}');
48 await waitForSyncIdle();
49
50 expect(invocations).toEqual(['update_item_from_json:10']);
51 expect(failures).toHaveLength(0);
52 });
53
54 test('recovers once and discards stale queued granular flushes after a failure', async () => {
55 const invocations: string[] = [];
56 const failures: GranularSyncFailure[] = [];
57 const firstFailureGate = createDeferred();
58 const recoveryGate = createDeferred();
59 let signalRecoveryStarted!: () => void;
60 const recoveryStarted = new Promise<void>((resolve) => {
61 signalRecoveryStarted = resolve;
62 });
63
64 setSyncQueueInvokerForTests((async (
65 command: string,
66 args?: Record<string, unknown>
67 ) => {
68 const itemId = String(args?.itemId ?? '');
69 invocations.push(`${command}:${itemId}`);
70
71 if (command === 'update_item_from_json' && itemId === '1') {
72 await firstFailureGate.promise;
73 throw new Error('backend rejected update');
74 }
75 }) as InvokeFn);
76 registerGranularSyncFailureHandler(async (failure) => {
77 failures.push(failure);
78 signalRecoveryStarted();
79 await recoveryGate.promise;
80 });
81
82 const failingFlush = enqueueGranularSync('Waveform', '1', '{"id":"1"}');
83 await delay(25);
84
85 const staleFlush = enqueueGranularSync('Waveform', '2', '{"id":"2"}');
86 await delay(25);
87
88 firstFailureGate.resolve();
89 await recoveryStarted;
90
91 const freshFlush = enqueueGranularSync('Waveform', '3', '{"id":"3"}');
92 await delay(25);
93
94 recoveryGate.resolve();
95
96 await expect(failingFlush).rejects.toThrow('backend rejected update');
97 await staleFlush;
98 await freshFlush;
99 await waitForSyncIdle();
100
101 expect(failures).toHaveLength(1);
102 expect(failures[0]?.itemId).toBe('1');
103 expect(invocations).toEqual([
104 'update_item_from_json:1',
105 'update_item_from_json:3',
106 ]);
107 });
108});