π 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