📄 src/models/test/patcher_queue.test.ts
D-OPEN SOVEREIGN
1/*
2 * Copyright (c) 2026 DataPond D-Library Pty Ltd. ("The Sanctuary")
3 * Non-Profit Public Library — The World Library
4 *
5 * D-SAFE Certification issued by POND Enterprise.
6 * Published under the D-Open Code Sovereign Licence v1.0 framework
7 * DataPond D-Library All exclusive Right reserved on modifiying the code - CC Attribution to data pond, Non modifiable, Non Commercial.
8 * https://registry.world.bibliotech.com/licence
9 * Source code donated to DataPond D-Library by it's author: data pond.
10 *
11 * Technical Guardian ("The Shield"): Pond Enterprise Pty Ltd. (ACN 694 747 987)
12 * All liability claims about D-SAFE certification issues coming from the published D-Safe direction must be addressed with Pond Enterprise Pty Ltd.
13 * Code Modification automatically voids the certified liability protection provided by Pond Enterprise.
14 */
15import 'fake-indexeddb/auto';
16import { describe, it, expect } from 'vitest';
17import { Patches } from '../patcher.ts';
18import { PatchOperation, type AnyPatch } from '../binary.ts';
19import { WorkerCommands } from '../enums.ts';
20import type { IThread, WorkerPayload, QueueItem } from '../interfaces.ts';
21
22const stringEdit = (value: string): AnyPatch => ({
23 operation: PatchOperation.EditPropertyString,
24 objectType: 0, objectId: 1, langId: 1, propertyId: 1, value,
25});
26
27class MockThread implements IThread {
28 received: Array<QueueItem[]> = [];
29 async sendRequest(payload: WorkerPayload): Promise<any> {
30 if (payload.cmd === WorkerCommands.SavePatches) {
31 this.received.push(payload.payload.patches);
32 }
33 return true;
34 }
35}
36
37const flush = () => new Promise(resolve => setTimeout(resolve, 5));
38
39describe('Patches — queue retention before worker init (Gemini review fix)', () => {
40 it('retains patches queued before initThread instead of dropping them', async () => {
41 const patches = new Patches('queue-test');
42
43 patches.addPatch(stringEdit('written before worker is ready'));
44 patches.addPatch(stringEdit('also before'));
45 await flush();
46
47 // Before the fix, sync() cleared the queue and dropped both edits.
48 expect(patches.patches.length).toBe(2);
49 });
50
51 it('flushes the retained queue as soon as initThread is called', async () => {
52 const patches = new Patches('queue-test-2');
53 patches.addPatch(stringEdit('early edit'));
54 await flush();
55 expect(patches.patches.length).toBe(1);
56
57 const thread = new MockThread();
58 patches.initThread(thread, 0);
59 await flush();
60
61 expect(patches.patches.length).toBe(0);
62 expect(thread.received.length).toBe(1);
63 expect((thread.received[0][0] as any).value).toBe('early edit');
64 });
65
66 it('post-init patches sync through the debounced path in order', async () => {
67 const patches = new Patches('queue-test-3');
68 const thread = new MockThread();
69 patches.initThread(thread, 0);
70
71 patches.addPatch(stringEdit('a'));
72 patches.addPatch(stringEdit('b')); // same slot → LWW keeps 'b'
73 await flush();
74
75 expect(thread.received.length).toBe(1);
76 expect(thread.received[0].length).toBe(1);
77 expect((thread.received[0][0] as any).value).toBe('b');
78 });
79
80 it('repeated initThread calls do not nest debounce wrappers', async () => {
81 const patches = new Patches('queue-test-4');
82 const thread = new MockThread();
83 patches.initThread(thread, 0);
84 patches.initThread(thread, 0); // second call re-wraps the prototype, not the wrapper
85
86 patches.addPatch(stringEdit('x'));
87 await flush();
88 expect(thread.received.length).toBe(1);
89 });
90});
91