📄 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