@affectively/aeon-pages 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +625 -0
  3. package/examples/basic/aeon.config.ts +39 -0
  4. package/examples/basic/components/Cursor.tsx +86 -0
  5. package/examples/basic/components/OfflineIndicator.tsx +103 -0
  6. package/examples/basic/components/PresenceBar.tsx +77 -0
  7. package/examples/basic/package.json +20 -0
  8. package/examples/basic/pages/index.tsx +80 -0
  9. package/package.json +101 -0
  10. package/packages/analytics/README.md +309 -0
  11. package/packages/analytics/build.ts +35 -0
  12. package/packages/analytics/package.json +50 -0
  13. package/packages/analytics/src/click-tracker.ts +368 -0
  14. package/packages/analytics/src/context-bridge.ts +319 -0
  15. package/packages/analytics/src/data-layer.ts +302 -0
  16. package/packages/analytics/src/gtm-loader.ts +239 -0
  17. package/packages/analytics/src/index.ts +230 -0
  18. package/packages/analytics/src/merkle-tree.ts +489 -0
  19. package/packages/analytics/src/provider.tsx +300 -0
  20. package/packages/analytics/src/types.ts +320 -0
  21. package/packages/analytics/src/use-analytics.ts +296 -0
  22. package/packages/analytics/tsconfig.json +19 -0
  23. package/packages/benchmarks/src/benchmark.test.ts +691 -0
  24. package/packages/cli/dist/index.js +61899 -0
  25. package/packages/cli/package.json +43 -0
  26. package/packages/cli/src/commands/build.test.ts +682 -0
  27. package/packages/cli/src/commands/build.ts +890 -0
  28. package/packages/cli/src/commands/dev.ts +473 -0
  29. package/packages/cli/src/commands/init.ts +409 -0
  30. package/packages/cli/src/commands/start.ts +297 -0
  31. package/packages/cli/src/index.ts +105 -0
  32. package/packages/directives/src/use-aeon.ts +272 -0
  33. package/packages/mcp-server/package.json +51 -0
  34. package/packages/mcp-server/src/index.ts +178 -0
  35. package/packages/mcp-server/src/resources.ts +346 -0
  36. package/packages/mcp-server/src/tools/index.ts +36 -0
  37. package/packages/mcp-server/src/tools/navigation.ts +545 -0
  38. package/packages/mcp-server/tsconfig.json +21 -0
  39. package/packages/react/package.json +40 -0
  40. package/packages/react/src/Link.tsx +388 -0
  41. package/packages/react/src/components/InstallPrompt.tsx +286 -0
  42. package/packages/react/src/components/OfflineDiagnostics.tsx +677 -0
  43. package/packages/react/src/components/PushNotifications.tsx +453 -0
  44. package/packages/react/src/hooks/useAeonNavigation.ts +219 -0
  45. package/packages/react/src/hooks/useConflicts.ts +277 -0
  46. package/packages/react/src/hooks/useNetworkState.ts +209 -0
  47. package/packages/react/src/hooks/usePilotNavigation.ts +254 -0
  48. package/packages/react/src/hooks/useServiceWorker.ts +278 -0
  49. package/packages/react/src/hooks.ts +195 -0
  50. package/packages/react/src/index.ts +151 -0
  51. package/packages/react/src/provider.tsx +467 -0
  52. package/packages/react/tsconfig.json +19 -0
  53. package/packages/runtime/README.md +399 -0
  54. package/packages/runtime/build.ts +48 -0
  55. package/packages/runtime/package.json +71 -0
  56. package/packages/runtime/schema.sql +40 -0
  57. package/packages/runtime/src/api-routes.ts +465 -0
  58. package/packages/runtime/src/benchmark.ts +171 -0
  59. package/packages/runtime/src/cache.ts +479 -0
  60. package/packages/runtime/src/durable-object.ts +1341 -0
  61. package/packages/runtime/src/index.ts +360 -0
  62. package/packages/runtime/src/navigation.test.ts +421 -0
  63. package/packages/runtime/src/navigation.ts +422 -0
  64. package/packages/runtime/src/nextjs-adapter.ts +272 -0
  65. package/packages/runtime/src/offline/encrypted-queue.test.ts +607 -0
  66. package/packages/runtime/src/offline/encrypted-queue.ts +478 -0
  67. package/packages/runtime/src/offline/encryption.test.ts +412 -0
  68. package/packages/runtime/src/offline/encryption.ts +397 -0
  69. package/packages/runtime/src/offline/types.ts +465 -0
  70. package/packages/runtime/src/predictor.ts +371 -0
  71. package/packages/runtime/src/registry.ts +351 -0
  72. package/packages/runtime/src/router/context-extractor.ts +661 -0
  73. package/packages/runtime/src/router/esi-control-react.tsx +2053 -0
  74. package/packages/runtime/src/router/esi-control.ts +541 -0
  75. package/packages/runtime/src/router/esi-cyrano.ts +779 -0
  76. package/packages/runtime/src/router/esi-format-react.tsx +1744 -0
  77. package/packages/runtime/src/router/esi-react.tsx +1065 -0
  78. package/packages/runtime/src/router/esi-translate-observer.ts +476 -0
  79. package/packages/runtime/src/router/esi-translate-react.tsx +556 -0
  80. package/packages/runtime/src/router/esi-translate.ts +503 -0
  81. package/packages/runtime/src/router/esi.ts +666 -0
  82. package/packages/runtime/src/router/heuristic-adapter.test.ts +295 -0
  83. package/packages/runtime/src/router/heuristic-adapter.ts +557 -0
  84. package/packages/runtime/src/router/index.ts +298 -0
  85. package/packages/runtime/src/router/merkle-capability.ts +473 -0
  86. package/packages/runtime/src/router/speculation.ts +451 -0
  87. package/packages/runtime/src/router/types.ts +630 -0
  88. package/packages/runtime/src/router.test.ts +470 -0
  89. package/packages/runtime/src/router.ts +302 -0
  90. package/packages/runtime/src/server.ts +481 -0
  91. package/packages/runtime/src/service-worker-push.ts +319 -0
  92. package/packages/runtime/src/service-worker.ts +553 -0
  93. package/packages/runtime/src/skeleton-hydrate.ts +237 -0
  94. package/packages/runtime/src/speculation.test.ts +389 -0
  95. package/packages/runtime/src/speculation.ts +486 -0
  96. package/packages/runtime/src/storage.test.ts +1297 -0
  97. package/packages/runtime/src/storage.ts +1048 -0
  98. package/packages/runtime/src/sync/conflict-resolver.test.ts +528 -0
  99. package/packages/runtime/src/sync/conflict-resolver.ts +565 -0
  100. package/packages/runtime/src/sync/coordinator.test.ts +608 -0
  101. package/packages/runtime/src/sync/coordinator.ts +596 -0
  102. package/packages/runtime/src/tree-compiler.ts +295 -0
  103. package/packages/runtime/src/types.ts +728 -0
  104. package/packages/runtime/src/worker.ts +327 -0
  105. package/packages/runtime/tsconfig.json +20 -0
  106. package/packages/runtime/wasm/aeon_pages_runtime.d.ts +504 -0
  107. package/packages/runtime/wasm/aeon_pages_runtime.js +1657 -0
  108. package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm +0 -0
  109. package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm.d.ts +196 -0
  110. package/packages/runtime/wasm/package.json +21 -0
  111. package/packages/runtime/wrangler.toml +41 -0
  112. package/packages/runtime-wasm/Cargo.lock +436 -0
  113. package/packages/runtime-wasm/Cargo.toml +29 -0
  114. package/packages/runtime-wasm/pkg/aeon_pages_runtime.d.ts +480 -0
  115. package/packages/runtime-wasm/pkg/aeon_pages_runtime.js +1568 -0
  116. package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm +0 -0
  117. package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm.d.ts +192 -0
  118. package/packages/runtime-wasm/pkg/package.json +21 -0
  119. package/packages/runtime-wasm/src/hydrate.rs +352 -0
  120. package/packages/runtime-wasm/src/lib.rs +191 -0
  121. package/packages/runtime-wasm/src/render.rs +629 -0
  122. package/packages/runtime-wasm/src/router.rs +298 -0
  123. package/packages/runtime-wasm/src/skeleton.rs +430 -0
  124. package/rfcs/RFC-001-ZERO-DEPENDENCY-RENDERING.md +1446 -0
@@ -0,0 +1,528 @@
1
+ /**
2
+ * Tests for Conflict Resolver
3
+ */
4
+
5
+ import { describe, test, expect, beforeEach } from 'bun:test';
6
+ import {
7
+ ConflictResolver,
8
+ getConflictResolver,
9
+ createConflictResolver,
10
+ resetConflictResolver,
11
+ } from './conflict-resolver';
12
+ import type { OfflineOperation } from '../offline/types';
13
+
14
+ // Helper to create mock operations
15
+ function createMockOperation(
16
+ overrides: Partial<OfflineOperation> = {},
17
+ ): OfflineOperation {
18
+ return {
19
+ id: `op-${Math.random().toString(36).slice(2)}`,
20
+ type: 'update',
21
+ sessionId: 'session-123',
22
+ status: 'pending',
23
+ data: { value: 'test' },
24
+ priority: 'normal',
25
+ bytesSize: 100,
26
+ createdAt: Date.now(),
27
+ failedCount: 0,
28
+ retryCount: 0,
29
+ maxRetries: 5,
30
+ ...overrides,
31
+ };
32
+ }
33
+
34
+ describe('ConflictResolver', () => {
35
+ let resolver: ConflictResolver;
36
+
37
+ beforeEach(() => {
38
+ resetConflictResolver();
39
+ resolver = new ConflictResolver();
40
+ });
41
+
42
+ describe('detectConflict', () => {
43
+ test('detects update-update conflict', () => {
44
+ const localOp = createMockOperation({
45
+ type: 'update',
46
+ data: { value: 'local' },
47
+ });
48
+
49
+ const remoteOp = createMockOperation({
50
+ type: 'update',
51
+ data: { value: 'remote' },
52
+ });
53
+
54
+ const conflict = resolver.detectConflict(localOp, remoteOp);
55
+
56
+ expect(conflict).not.toBeNull();
57
+ expect(conflict!.type).toBe('update_update');
58
+ });
59
+
60
+ test('detects delete-update conflict', () => {
61
+ const localOp = createMockOperation({
62
+ type: 'delete',
63
+ data: { id: '123' },
64
+ });
65
+
66
+ const remoteOp = createMockOperation({
67
+ type: 'update',
68
+ data: { value: 'updated' },
69
+ });
70
+
71
+ const conflict = resolver.detectConflict(localOp, remoteOp);
72
+
73
+ expect(conflict).not.toBeNull();
74
+ expect(conflict!.type).toBe('delete_update');
75
+ });
76
+
77
+ test('detects update-delete conflict', () => {
78
+ const localOp = createMockOperation({
79
+ type: 'update',
80
+ data: { value: 'updated' },
81
+ });
82
+
83
+ const remoteOp = createMockOperation({
84
+ type: 'delete',
85
+ data: { id: '123' },
86
+ });
87
+
88
+ const conflict = resolver.detectConflict(localOp, remoteOp);
89
+
90
+ expect(conflict).not.toBeNull();
91
+ expect(conflict!.type).toBe('update_delete');
92
+ });
93
+
94
+ test('returns null for different sessions', () => {
95
+ const localOp = createMockOperation({
96
+ sessionId: 'session-1',
97
+ });
98
+
99
+ const remoteOp = createMockOperation({
100
+ sessionId: 'session-2',
101
+ });
102
+
103
+ const conflict = resolver.detectConflict(localOp, remoteOp);
104
+
105
+ expect(conflict).toBeNull();
106
+ });
107
+
108
+ test('returns null for both deletes', () => {
109
+ const localOp = createMockOperation({
110
+ type: 'delete',
111
+ });
112
+
113
+ const remoteOp = createMockOperation({
114
+ type: 'delete',
115
+ });
116
+
117
+ const conflict = resolver.detectConflict(localOp, remoteOp);
118
+
119
+ expect(conflict).toBeNull();
120
+ });
121
+
122
+ test('emits conflict:detected event', () => {
123
+ let eventReceived = false;
124
+
125
+ resolver.on('conflict:detected', () => {
126
+ eventReceived = true;
127
+ });
128
+
129
+ const localOp = createMockOperation({ data: { value: 'local' } });
130
+ const remoteOp = createMockOperation({ data: { value: 'remote' } });
131
+
132
+ resolver.detectConflict(localOp, remoteOp);
133
+
134
+ expect(eventReceived).toBe(true);
135
+ });
136
+
137
+ test('assigns high severity to delete conflicts', () => {
138
+ const localOp = createMockOperation({ type: 'delete' });
139
+ const remoteOp = createMockOperation({ type: 'update' });
140
+
141
+ const conflict = resolver.detectConflict(localOp, remoteOp);
142
+
143
+ expect(conflict!.severity).toBe('high');
144
+ });
145
+
146
+ test('assigns low severity to similar updates', () => {
147
+ const localOp = createMockOperation({
148
+ data: { value: 'test', extra: 'local' },
149
+ });
150
+ const remoteOp = createMockOperation({
151
+ data: { value: 'test', extra: 'remote' },
152
+ });
153
+
154
+ const conflict = resolver.detectConflict(localOp, remoteOp);
155
+
156
+ expect(conflict!.severity).toBe('low');
157
+ });
158
+
159
+ test('updates statistics', () => {
160
+ const localOp = createMockOperation({ data: { value: 'local' } });
161
+ const remoteOp = createMockOperation({ data: { value: 'remote' } });
162
+
163
+ resolver.detectConflict(localOp, remoteOp);
164
+
165
+ const stats = resolver.getStats();
166
+ expect(stats.totalConflicts).toBe(1);
167
+ expect(stats.conflictsByType['update_update']).toBe(1);
168
+ });
169
+
170
+ test('auto-resolves low severity conflicts', () => {
171
+ const resolver = createConflictResolver({
172
+ defaultStrategy: 'local-wins',
173
+ });
174
+
175
+ const localOp = createMockOperation({
176
+ data: { value: 'test', extra: 'local' },
177
+ });
178
+ const remoteOp = createMockOperation({
179
+ data: { value: 'test', extra: 'remote' },
180
+ });
181
+
182
+ const conflict = resolver.detectConflict(localOp, remoteOp);
183
+
184
+ // Low severity should be auto-resolved
185
+ expect(conflict!.resolution).toBeDefined();
186
+ });
187
+ });
188
+
189
+ describe('resolveConflict', () => {
190
+ test('resolves with local-wins strategy', () => {
191
+ const localOp = createMockOperation({ data: { value: 'local' } });
192
+ const remoteOp = createMockOperation({ data: { value: 'remote' } });
193
+
194
+ const conflict = resolver.detectConflict(localOp, remoteOp);
195
+ // Clear auto-resolution to test manual resolution
196
+ conflict!.resolution = undefined;
197
+
198
+ const resolution = resolver.resolveConflict(conflict!.id, 'local-wins');
199
+
200
+ expect(resolution).not.toBeNull();
201
+ expect(resolution!.strategy).toBe('local-wins');
202
+ expect(resolution!.resolvedData).toEqual({ value: 'local' });
203
+ });
204
+
205
+ test('resolves with remote-wins strategy', () => {
206
+ const localOp = createMockOperation({ data: { value: 'local' } });
207
+ const remoteOp = createMockOperation({ data: { value: 'remote' } });
208
+
209
+ const conflict = resolver.detectConflict(localOp, remoteOp);
210
+ conflict!.resolution = undefined;
211
+
212
+ const resolution = resolver.resolveConflict(conflict!.id, 'remote-wins');
213
+
214
+ expect(resolution).not.toBeNull();
215
+ expect(resolution!.strategy).toBe('remote-wins');
216
+ expect(resolution!.resolvedData).toEqual({ value: 'remote' });
217
+ });
218
+
219
+ test('resolves with merge strategy', () => {
220
+ const resolver = createConflictResolver({ enableAutoMerge: true });
221
+
222
+ const localOp = createMockOperation({
223
+ data: { localField: 'local', shared: 'local-value' },
224
+ });
225
+ const remoteOp = createMockOperation({
226
+ data: { remoteField: 'remote', shared: 'remote-value' },
227
+ });
228
+
229
+ const conflict = resolver.detectConflict(localOp, remoteOp);
230
+ conflict!.resolution = undefined;
231
+
232
+ const resolution = resolver.resolveConflict(conflict!.id, 'merge');
233
+
234
+ expect(resolution).not.toBeNull();
235
+ expect(resolution!.strategy).toBe('merge');
236
+ // Merge should include both fields, with local winning on conflicts
237
+ expect(resolution!.resolvedData).toHaveProperty('localField', 'local');
238
+ expect(resolution!.resolvedData).toHaveProperty('remoteField', 'remote');
239
+ expect(resolution!.resolvedData).toHaveProperty('shared', 'local-value');
240
+ });
241
+
242
+ test('returns null for manual strategy', () => {
243
+ const localOp = createMockOperation({ data: { value: 'local' } });
244
+ const remoteOp = createMockOperation({ data: { value: 'remote' } });
245
+
246
+ const conflict = resolver.detectConflict(localOp, remoteOp);
247
+ conflict!.resolution = undefined;
248
+
249
+ const resolution = resolver.resolveConflict(conflict!.id, 'manual');
250
+
251
+ expect(resolution).toBeNull();
252
+ });
253
+
254
+ test('returns null for non-existent conflict', () => {
255
+ const resolution = resolver.resolveConflict(
256
+ 'non-existent-id',
257
+ 'local-wins',
258
+ );
259
+
260
+ expect(resolution).toBeNull();
261
+ });
262
+
263
+ test('emits conflict:resolved event', () => {
264
+ let eventReceived = false;
265
+
266
+ resolver.on('conflict:resolved', () => {
267
+ eventReceived = true;
268
+ });
269
+
270
+ const localOp = createMockOperation({ data: { value: 'local' } });
271
+ const remoteOp = createMockOperation({ data: { value: 'remote' } });
272
+
273
+ const conflict = resolver.detectConflict(localOp, remoteOp);
274
+ conflict!.resolution = undefined;
275
+
276
+ resolver.resolveConflict(conflict!.id, 'local-wins');
277
+
278
+ expect(eventReceived).toBe(true);
279
+ });
280
+
281
+ test('updates statistics on resolution', () => {
282
+ // Use high severity conflict to avoid auto-resolution
283
+ const localOp = createMockOperation({
284
+ type: 'delete',
285
+ data: { id: '123' },
286
+ });
287
+ const remoteOp = createMockOperation({
288
+ type: 'update',
289
+ data: { value: 'updated' },
290
+ });
291
+
292
+ const conflict = resolver.detectConflict(localOp, remoteOp);
293
+ // High severity conflicts are not auto-resolved
294
+
295
+ resolver.resolveConflict(conflict!.id, 'local-wins');
296
+
297
+ const stats = resolver.getStats();
298
+ expect(stats.resolvedConflicts).toBe(1);
299
+ expect(stats.resolutionsByStrategy['local-wins']).toBe(1);
300
+ });
301
+
302
+ test('uses default strategy when none specified', () => {
303
+ const resolver = createConflictResolver({
304
+ defaultStrategy: 'remote-wins',
305
+ });
306
+
307
+ const localOp = createMockOperation({ data: { value: 'local' } });
308
+ const remoteOp = createMockOperation({ data: { value: 'remote' } });
309
+
310
+ const conflict = resolver.detectConflict(localOp, remoteOp);
311
+ conflict!.resolution = undefined;
312
+
313
+ const resolution = resolver.resolveConflict(conflict!.id);
314
+
315
+ expect(resolution!.strategy).toBe('remote-wins');
316
+ });
317
+ });
318
+
319
+ describe('getConflict', () => {
320
+ test('returns conflict by ID', () => {
321
+ const localOp = createMockOperation({ data: { value: 'local' } });
322
+ const remoteOp = createMockOperation({ data: { value: 'remote' } });
323
+
324
+ const conflict = resolver.detectConflict(localOp, remoteOp);
325
+
326
+ const retrieved = resolver.getConflict(conflict!.id);
327
+
328
+ expect(retrieved).toBeDefined();
329
+ expect(retrieved!.id).toBe(conflict!.id);
330
+ });
331
+
332
+ test('returns undefined for non-existent conflict', () => {
333
+ const retrieved = resolver.getConflict('non-existent-id');
334
+
335
+ expect(retrieved).toBeUndefined();
336
+ });
337
+ });
338
+
339
+ describe('getUnresolvedConflicts', () => {
340
+ test('returns only unresolved conflicts', () => {
341
+ const localOp1 = createMockOperation({ data: { value: 'local1' } });
342
+ const remoteOp1 = createMockOperation({ data: { value: 'remote1' } });
343
+ const conflict1 = resolver.detectConflict(localOp1, remoteOp1);
344
+ conflict1!.resolution = undefined;
345
+
346
+ const localOp2 = createMockOperation({ data: { value: 'local2' } });
347
+ const remoteOp2 = createMockOperation({ data: { value: 'remote2' } });
348
+ const conflict2 = resolver.detectConflict(localOp2, remoteOp2);
349
+ conflict2!.resolution = undefined;
350
+
351
+ // Resolve one conflict
352
+ resolver.resolveConflict(conflict1!.id, 'local-wins');
353
+
354
+ const unresolved = resolver.getUnresolvedConflicts();
355
+
356
+ expect(unresolved).toHaveLength(1);
357
+ expect(unresolved[0].id).toBe(conflict2!.id);
358
+ });
359
+ });
360
+
361
+ describe('getConflictsForSession', () => {
362
+ test('returns conflicts for specific session', () => {
363
+ const localOp1 = createMockOperation({
364
+ sessionId: 'session-1',
365
+ data: { v: 'l1' },
366
+ });
367
+ const remoteOp1 = createMockOperation({
368
+ sessionId: 'session-1',
369
+ data: { v: 'r1' },
370
+ });
371
+ resolver.detectConflict(localOp1, remoteOp1);
372
+
373
+ const localOp2 = createMockOperation({
374
+ sessionId: 'session-1',
375
+ data: { v: 'l2' },
376
+ });
377
+ const remoteOp2 = createMockOperation({
378
+ sessionId: 'session-1',
379
+ data: { v: 'r2' },
380
+ });
381
+ resolver.detectConflict(localOp2, remoteOp2);
382
+
383
+ const conflicts = resolver.getConflictsForSession('session-1');
384
+
385
+ expect(conflicts).toHaveLength(2);
386
+ });
387
+
388
+ test('returns empty array for session with no conflicts', () => {
389
+ const conflicts = resolver.getConflictsForSession('non-existent-session');
390
+
391
+ expect(conflicts).toHaveLength(0);
392
+ });
393
+ });
394
+
395
+ describe('getHighSeverityConflicts', () => {
396
+ test('returns only high severity unresolved conflicts', () => {
397
+ // Create high severity conflict (delete-update)
398
+ const deleteOp = createMockOperation({ type: 'delete' });
399
+ const updateOp = createMockOperation({ type: 'update' });
400
+ const highConflict = resolver.detectConflict(deleteOp, updateOp);
401
+
402
+ // Create low severity conflict (similar updates)
403
+ const localOp = createMockOperation({ data: { value: 'test', x: 1 } });
404
+ const remoteOp = createMockOperation({ data: { value: 'test', x: 2 } });
405
+ resolver.detectConflict(localOp, remoteOp);
406
+
407
+ const highSeverity = resolver.getHighSeverityConflicts();
408
+
409
+ expect(highSeverity).toHaveLength(1);
410
+ expect(highSeverity[0].id).toBe(highConflict!.id);
411
+ });
412
+ });
413
+
414
+ describe('getStats', () => {
415
+ test('returns correct statistics', () => {
416
+ // Use high severity conflicts to avoid auto-resolution
417
+ const localOp1 = createMockOperation({
418
+ type: 'delete',
419
+ data: { id: '1' },
420
+ });
421
+ const remoteOp1 = createMockOperation({
422
+ type: 'update',
423
+ data: { v: 'r1' },
424
+ });
425
+ const conflict = resolver.detectConflict(localOp1, remoteOp1);
426
+
427
+ resolver.resolveConflict(conflict!.id, 'local-wins');
428
+
429
+ const localOp2 = createMockOperation({
430
+ type: 'delete',
431
+ data: { id: '2' },
432
+ });
433
+ const remoteOp2 = createMockOperation({
434
+ type: 'update',
435
+ data: { v: 'r2' },
436
+ });
437
+ resolver.detectConflict(localOp2, remoteOp2);
438
+
439
+ const stats = resolver.getStats();
440
+
441
+ expect(stats.totalConflicts).toBe(2);
442
+ expect(stats.resolvedConflicts).toBe(1);
443
+ expect(stats.unresolvedConflicts).toBe(1);
444
+ });
445
+ });
446
+
447
+ describe('configure', () => {
448
+ test('updates configuration', () => {
449
+ resolver.configure({
450
+ defaultStrategy: 'remote-wins',
451
+ mergeThreshold: 80,
452
+ });
453
+
454
+ const config = resolver.getConfig();
455
+
456
+ expect(config.defaultStrategy).toBe('remote-wins');
457
+ expect(config.mergeThreshold).toBe(80);
458
+ });
459
+
460
+ test('emits config:updated event', () => {
461
+ let eventReceived = false;
462
+
463
+ resolver.on('config:updated', () => {
464
+ eventReceived = true;
465
+ });
466
+
467
+ resolver.configure({ defaultStrategy: 'merge' });
468
+
469
+ expect(eventReceived).toBe(true);
470
+ });
471
+ });
472
+
473
+ describe('clear', () => {
474
+ test('clears all conflicts', () => {
475
+ const localOp = createMockOperation({ data: { v: 'l' } });
476
+ const remoteOp = createMockOperation({ data: { v: 'r' } });
477
+ resolver.detectConflict(localOp, remoteOp);
478
+
479
+ resolver.clear();
480
+
481
+ const unresolved = resolver.getUnresolvedConflicts();
482
+ expect(unresolved).toHaveLength(0);
483
+ });
484
+ });
485
+
486
+ describe('reset', () => {
487
+ test('resets all state and statistics', () => {
488
+ const localOp = createMockOperation({ data: { v: 'l' } });
489
+ const remoteOp = createMockOperation({ data: { v: 'r' } });
490
+ resolver.detectConflict(localOp, remoteOp);
491
+
492
+ resolver.reset();
493
+
494
+ const stats = resolver.getStats();
495
+ expect(stats.totalConflicts).toBe(0);
496
+ expect(stats.resolvedConflicts).toBe(0);
497
+ });
498
+ });
499
+ });
500
+
501
+ describe('getConflictResolver', () => {
502
+ beforeEach(() => {
503
+ resetConflictResolver();
504
+ });
505
+
506
+ test('returns singleton instance', () => {
507
+ const instance1 = getConflictResolver();
508
+ const instance2 = getConflictResolver();
509
+
510
+ expect(instance1).toBe(instance2);
511
+ });
512
+ });
513
+
514
+ describe('createConflictResolver', () => {
515
+ test('creates resolver with custom config', () => {
516
+ const resolver = createConflictResolver({
517
+ defaultStrategy: 'merge',
518
+ enableAutoMerge: false,
519
+ mergeThreshold: 90,
520
+ });
521
+
522
+ const config = resolver.getConfig();
523
+
524
+ expect(config.defaultStrategy).toBe('merge');
525
+ expect(config.enableAutoMerge).toBe(false);
526
+ expect(config.mergeThreshold).toBe(90);
527
+ });
528
+ });