📄 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