πŸ“„ src/models/test/patcher.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 { describe, it, expect } from 'vitest';
16import { Patches } from '../patcher.ts';
17import { PatchOperation, PatchAction, type AnyPatch } from '../binary.ts';
18import { ActionTypes } from '../enums.ts';
19import type { QueueItem } from '../interfaces.ts';
20
21const stringEdit = (langId: number, value: string, propertyId = 1, objectId = 10): AnyPatch => ({
22    operation: PatchOperation.EditPropertyString,
23    objectType: 0, objectId, langId, propertyId, value,
24});
25
26const numberEdit = (langId: number, value: number, propertyId = 11, objectId = 10): AnyPatch => ({
27    operation: PatchOperation.EditPropertyNumber,
28    objectType: 0, objectId, langId, propertyId, value,
29});
30
31describe('Patches.cleanData β€” lang-aware LWW dedup (SPEC Β§2.2)', () => {
32    it('keeps the latest edit per (propertyId, langId) slot', () => {
33        const data: QueueItem[] = [
34            stringEdit(1, 'old english'),
35            stringEdit(1, 'new english'),
36        ];
37        const cleaned = Patches.cleanData(data);
38        expect(cleaned.length).toBe(1);
39        expect((cleaned[0] as any).value).toBe('new english');
40    });
41
42    it('French string edits never erase English ones', () => {
43        const data: QueueItem[] = [
44            stringEdit(1, 'english description'),
45            stringEdit(2, 'description franΓ§aise'),
46        ];
47        const cleaned = Patches.cleanData(data);
48        expect(cleaned.length).toBe(2);
49    });
50
51    it('langId 0 (lang-agnostic) is a distinct slot', () => {
52        const data: QueueItem[] = [
53            stringEdit(0, 'agnostic'),
54            stringEdit(1, 'english'),
55        ];
56        expect(Patches.cleanData(data).length).toBe(2);
57    });
58
59    it('number edits dedupe per (propertyId, langId) too', () => {
60        const data: QueueItem[] = [
61            numberEdit(1, 3),
62            numberEdit(1, 5),
63            numberEdit(2, 4),
64        ];
65        const cleaned = Patches.cleanData(data);
66        expect(cleaned.length).toBe(2);
67        const values = cleaned.map(c => (c as any).value).sort();
68        expect(values).toEqual([4, 5]);
69    });
70
71    it('instance field edits key on instanceIndex as well', () => {
72        const edit = (instanceIndex: number, value: number): AnyPatch => ({
73            operation: PatchOperation.EditInstanceFieldNumber,
74            objectType: 0, objectId: 10, langId: 1,
75            instanceIndex, propertyId: 11, value,
76        });
77        const data: QueueItem[] = [edit(0, 1), edit(0, 2), edit(1, 3)];
78        const cleaned = Patches.cleanData(data);
79        expect(cleaned.length).toBe(2);
80        expect((cleaned.find(c => (c as any).instanceIndex === 0) as any).value).toBe(2);
81    });
82
83    it('relations keep the latest action per (relationId, targetId)', () => {
84        const rel = (action: PatchAction, targetId: number): AnyPatch => ({
85            operation: PatchOperation.EditRelation,
86            objectType: 0, objectId: 10, langId: 0,
87            relationId: 12, action, targetId,
88        });
89        const data: QueueItem[] = [
90            rel(PatchAction.Add, 5),
91            rel(PatchAction.Remove, 5),
92            rel(PatchAction.Add, 7),
93        ];
94        const cleaned = Patches.cleanData(data);
95        expect(cleaned.length).toBe(2);
96        expect((cleaned.find(c => (c as any).targetId === 5) as any).action).toBe(PatchAction.Remove);
97    });
98
99    it('local analytics ops pass through (visits kept, rates deduped)', () => {
100        const visit = { ts: 1, action: ActionTypes.Visit, objectType: 0, objectId: 10 };
101        const rate = (rate: number, ts: number) => ({ ts, action: ActionTypes.Rate, objectType: 0, objectId: 10, rate });
102        const data: QueueItem[] = [visit, visit, rate(1, 2), rate(3, 3)];
103        const cleaned = Patches.cleanData(data);
104        const visits = cleaned.filter(c => (c as any).action === ActionTypes.Visit);
105        const rates = cleaned.filter(c => (c as any).action === ActionTypes.Rate);
106        expect(visits.length).toBe(2);
107        expect(rates.length).toBe(1);
108        expect((rates[0] as any).rate).toBe(3); // newest wins
109    });
110
111    it('preserves chronological order after dedup', () => {
112        const data: QueueItem[] = [
113            stringEdit(1, 'a', 1),
114            numberEdit(1, 2, 11),
115            stringEdit(1, 'b', 1),
116        ];
117        const cleaned = Patches.cleanData(data);
118        expect(cleaned.length).toBe(2);
119        // number edit (older) must come before the surviving string edit
120        expect((cleaned[0] as any).operation).toBe(PatchOperation.EditPropertyNumber);
121        expect((cleaned[1] as any).value).toBe('b');
122    });
123});
124