📄 src/models/test/patch_storage.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 { bulkAddPatches, getNbPatches, getAllPatchesByUsername, getAllPatchesSince, getAllPatches, deletePatches, clearAllPatches } from '../indexdb/patch_storage.ts';
18import { PatchOperation, type AnyPatch } from '../binary.ts';
19import { ActionTypes } from '../enums.ts';
20import type { QueueItem } from '../interfaces.ts';
21
22const stringEdit = (value: string, objectId = 10): AnyPatch => ({
23    operation: PatchOperation.EditPropertyString,
24    objectType: 0, objectId, langId: 1, propertyId: 1, value,
25});
26
27const visit = (ts: number): QueueItem => ({
28    ts, action: ActionTypes.Visit, objectType: 0, objectId: 10,
29});
30
31describe('bulkAddPatches — duplicate detection (Gemini review fix)', () => {
32    it('is idempotent: re-adding the same patches does not duplicate them', async () => {
33        const patches: QueueItem[] = [stringEdit('hello'), visit(100)];
34
35        await bulkAddPatches('mine', patches);
36        expect(await getNbPatches('mine')).toBe(2);
37
38        // Simulates a repeated blockchain restore (deserializePatches replay).
39        // Before the fix, stored rows carried `username` inside the signature
40        // and every replay re-inserted the full history.
41        await bulkAddPatches('mine', patches);
42        expect(await getNbPatches('mine')).toBe(2);
43    });
44
45    it('still accepts genuinely new patches after a replay', async () => {
46        await bulkAddPatches('mine', [stringEdit('hello'), stringEdit('different', 99)]);
47        const all = await getAllPatchesByUsername('mine');
48        const values = all.map(p => (p as any).value).filter(Boolean).sort();
49        expect(values).toEqual(['different', 'hello']);
50        expect(await getNbPatches('mine')).toBe(3); // 2 from previous test + 1 new
51    });
52
53    it('deduplicates per username — another user can store the same patch', async () => {
54        await bulkAddPatches('alice', [stringEdit('hello')]);
55        await bulkAddPatches('alice', [stringEdit('hello')]);
56        expect(await getNbPatches('alice')).toBe(1);
57        expect(await getNbPatches('mine')).toBe(3); // untouched
58    });
59
60    it('stored rows expose pk and username metadata', async () => {
61        const all = await getAllPatchesByUsername('alice');
62        expect(all.length).toBe(1);
63        expect(all[0].pk).toBe(1);
64        expect(all[0].username).toBe('alice');
65    });
66});
67
68describe('Additional Storage APIs', () => {
69    it('getAllPatchesSince correctly filters and sorts', async () => {
70        await clearAllPatches();
71        
72        await bulkAddPatches('bob', [stringEdit('patch1'), stringEdit('patch2'), stringEdit('patch3')]);
73        const patches = await getAllPatchesSince('bob', 2); // pk 2 and 3
74        expect(patches.length).toBe(2);
75        expect((patches[0] as any).value).toBe('patch2');
76        expect(patches[0].pk).toBe(2);
77        expect((patches[1] as any).value).toBe('patch3');
78        expect(patches[1].pk).toBe(3);
79    });
80
81    it('getAllPatches groups correctly across users', async () => {
82        await clearAllPatches();
83        
84        await bulkAddPatches('u1', [stringEdit('a')]);
85        await bulkAddPatches('u2', [stringEdit('b')]);
86        
87        const grouped = await getAllPatches();
88        expect(Object.keys(grouped).sort()).toEqual(['u1', 'u2']);
89        expect(grouped['u1'].length).toBe(1);
90        expect(grouped['u2'].length).toBe(1);
91        expect((grouped['u1'][0] as any).value).toBe('a');
92    });
93
94    it('deletePatches removes specific pks', async () => {
95        await clearAllPatches();
96        
97        await bulkAddPatches('charlie', [stringEdit('1'), stringEdit('2'), stringEdit('3')]);
98        expect(await getNbPatches('charlie')).toBe(3);
99        
100        await deletePatches('charlie', [1, 3]);
101        
102        const remaining = await getAllPatchesByUsername('charlie');
103        expect(remaining.length).toBe(1);
104        expect(remaining[0].pk).toBe(2);
105        expect((remaining[0] as any).value).toBe('2');
106    });
107
108    it('clearAllPatches removes everything', async () => {
109        await clearAllPatches();
110        await bulkAddPatches('dave', [stringEdit('test')]);
111        expect(await getNbPatches('dave')).toBe(1);
112        
113        await clearAllPatches();
114        expect(await getNbPatches('dave')).toBe(0);
115        
116        const all = await getAllPatches();
117        expect(Object.keys(all).length).toBe(0);
118    });
119});
120
121