@syncular/core 0.0.1-100

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 (99) hide show
  1. package/dist/blobs.d.ts +146 -0
  2. package/dist/blobs.d.ts.map +1 -0
  3. package/dist/blobs.js +47 -0
  4. package/dist/blobs.js.map +1 -0
  5. package/dist/conflict.d.ts +22 -0
  6. package/dist/conflict.d.ts.map +1 -0
  7. package/dist/conflict.js +81 -0
  8. package/dist/conflict.js.map +1 -0
  9. package/dist/index.d.ts +24 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +36 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/kysely-serialize.d.ts +22 -0
  14. package/dist/kysely-serialize.d.ts.map +1 -0
  15. package/dist/kysely-serialize.js +147 -0
  16. package/dist/kysely-serialize.js.map +1 -0
  17. package/dist/logger.d.ts +29 -0
  18. package/dist/logger.d.ts.map +1 -0
  19. package/dist/logger.js +26 -0
  20. package/dist/logger.js.map +1 -0
  21. package/dist/proxy/index.d.ts +5 -0
  22. package/dist/proxy/index.d.ts.map +1 -0
  23. package/dist/proxy/index.js +5 -0
  24. package/dist/proxy/index.js.map +1 -0
  25. package/dist/proxy/types.d.ts +54 -0
  26. package/dist/proxy/types.d.ts.map +1 -0
  27. package/dist/proxy/types.js +7 -0
  28. package/dist/proxy/types.js.map +1 -0
  29. package/dist/schemas/blobs.d.ts +76 -0
  30. package/dist/schemas/blobs.d.ts.map +1 -0
  31. package/dist/schemas/blobs.js +63 -0
  32. package/dist/schemas/blobs.js.map +1 -0
  33. package/dist/schemas/common.d.ts +28 -0
  34. package/dist/schemas/common.d.ts.map +1 -0
  35. package/dist/schemas/common.js +26 -0
  36. package/dist/schemas/common.js.map +1 -0
  37. package/dist/schemas/index.d.ts +7 -0
  38. package/dist/schemas/index.d.ts.map +1 -0
  39. package/dist/schemas/index.js +7 -0
  40. package/dist/schemas/index.js.map +1 -0
  41. package/dist/schemas/sync.d.ts +391 -0
  42. package/dist/schemas/sync.d.ts.map +1 -0
  43. package/dist/schemas/sync.js +157 -0
  44. package/dist/schemas/sync.js.map +1 -0
  45. package/dist/scopes/index.d.ts +65 -0
  46. package/dist/scopes/index.d.ts.map +1 -0
  47. package/dist/scopes/index.js +67 -0
  48. package/dist/scopes/index.js.map +1 -0
  49. package/dist/snapshot-chunks.d.ts +26 -0
  50. package/dist/snapshot-chunks.d.ts.map +1 -0
  51. package/dist/snapshot-chunks.js +89 -0
  52. package/dist/snapshot-chunks.js.map +1 -0
  53. package/dist/telemetry.d.ts +114 -0
  54. package/dist/telemetry.d.ts.map +1 -0
  55. package/dist/telemetry.js +113 -0
  56. package/dist/telemetry.js.map +1 -0
  57. package/dist/transforms.d.ts +146 -0
  58. package/dist/transforms.d.ts.map +1 -0
  59. package/dist/transforms.js +155 -0
  60. package/dist/transforms.js.map +1 -0
  61. package/dist/types.d.ts +129 -0
  62. package/dist/types.d.ts.map +1 -0
  63. package/dist/types.js +20 -0
  64. package/dist/types.js.map +1 -0
  65. package/dist/utils/id.d.ts +2 -0
  66. package/dist/utils/id.d.ts.map +1 -0
  67. package/dist/utils/id.js +8 -0
  68. package/dist/utils/id.js.map +1 -0
  69. package/dist/utils/index.d.ts +3 -0
  70. package/dist/utils/index.d.ts.map +1 -0
  71. package/dist/utils/index.js +3 -0
  72. package/dist/utils/index.js.map +1 -0
  73. package/dist/utils/object.d.ts +2 -0
  74. package/dist/utils/object.d.ts.map +1 -0
  75. package/dist/utils/object.js +4 -0
  76. package/dist/utils/object.js.map +1 -0
  77. package/package.json +57 -0
  78. package/src/__tests__/conflict.test.ts +325 -0
  79. package/src/__tests__/telemetry.test.ts +170 -0
  80. package/src/__tests__/utils.test.ts +27 -0
  81. package/src/blobs.ts +202 -0
  82. package/src/conflict.ts +92 -0
  83. package/src/index.ts +36 -0
  84. package/src/kysely-serialize.ts +214 -0
  85. package/src/logger.ts +38 -0
  86. package/src/proxy/index.ts +10 -0
  87. package/src/proxy/types.ts +57 -0
  88. package/src/schemas/blobs.ts +101 -0
  89. package/src/schemas/common.ts +45 -0
  90. package/src/schemas/index.ts +7 -0
  91. package/src/schemas/sync.ts +226 -0
  92. package/src/scopes/index.ts +122 -0
  93. package/src/snapshot-chunks.ts +112 -0
  94. package/src/telemetry.ts +238 -0
  95. package/src/transforms.ts +256 -0
  96. package/src/types.ts +158 -0
  97. package/src/utils/id.ts +7 -0
  98. package/src/utils/index.ts +2 -0
  99. package/src/utils/object.ts +3 -0
@@ -0,0 +1,325 @@
1
+ /**
2
+ * Unit tests for conflict detection utilities.
3
+ *
4
+ * Tests the field-level merge logic for sync push operations.
5
+ * These are pure function tests that don't require database setup.
6
+ */
7
+ import { describe, expect, test } from 'bun:test';
8
+ import { performFieldLevelMerge } from '../conflict';
9
+
10
+ describe('performFieldLevelMerge', () => {
11
+ describe('no base row (new insert)', () => {
12
+ test('client payload wins entirely when base row is null', () => {
13
+ const result = performFieldLevelMerge(
14
+ null, // no base row
15
+ { id: 'team-1', name: 'Server Name', type: 'praxis' }, // server row
16
+ { name: 'Client Name', type: 'op' } // client payload
17
+ );
18
+
19
+ expect(result.canMerge).toBe(true);
20
+ if (result.canMerge) {
21
+ expect(result.mergedPayload).toEqual({
22
+ name: 'Client Name',
23
+ type: 'op',
24
+ });
25
+ }
26
+ });
27
+ });
28
+
29
+ describe('only client changed', () => {
30
+ test('uses client value when only client changed a field', () => {
31
+ const baseRow = { id: 'team-1', name: 'Original', type: 'praxis' };
32
+ const serverRow = { id: 'team-1', name: 'Original', type: 'praxis' };
33
+ const clientPayload = { name: 'Client Updated', type: 'praxis' };
34
+
35
+ const result = performFieldLevelMerge(baseRow, serverRow, clientPayload);
36
+
37
+ expect(result.canMerge).toBe(true);
38
+ if (result.canMerge) {
39
+ expect(result.mergedPayload.name).toBe('Client Updated');
40
+ expect(result.mergedPayload.type).toBe('praxis');
41
+ }
42
+ });
43
+
44
+ test('handles multiple fields changed by client only', () => {
45
+ const baseRow = { id: 'team-1', name: 'Original', type: 'praxis' };
46
+ const serverRow = { id: 'team-1', name: 'Original', type: 'praxis' };
47
+ const clientPayload = { name: 'New Name', type: 'op' };
48
+
49
+ const result = performFieldLevelMerge(baseRow, serverRow, clientPayload);
50
+
51
+ expect(result.canMerge).toBe(true);
52
+ if (result.canMerge) {
53
+ expect(result.mergedPayload.name).toBe('New Name');
54
+ expect(result.mergedPayload.type).toBe('op');
55
+ }
56
+ });
57
+ });
58
+
59
+ describe('only server changed', () => {
60
+ test('keeps server value when only server changed a field', () => {
61
+ const baseRow = { id: 'team-1', name: 'Original', type: 'praxis' };
62
+ const serverRow = {
63
+ id: 'team-1',
64
+ name: 'Server Updated',
65
+ type: 'praxis',
66
+ };
67
+ const clientPayload = { name: 'Original', type: 'praxis' };
68
+
69
+ const result = performFieldLevelMerge(baseRow, serverRow, clientPayload);
70
+
71
+ expect(result.canMerge).toBe(true);
72
+ if (result.canMerge) {
73
+ // Server's value is kept since client didn't change it
74
+ expect(result.mergedPayload.name).toBe('Server Updated');
75
+ expect(result.mergedPayload.type).toBe('praxis');
76
+ }
77
+ });
78
+ });
79
+
80
+ describe('both changed to same value', () => {
81
+ test('no conflict when both changed field to same value', () => {
82
+ const baseRow = { id: 'team-1', name: 'Original', type: 'praxis' };
83
+ const serverRow = { id: 'team-1', name: 'Same Value', type: 'praxis' };
84
+ const clientPayload = { name: 'Same Value', type: 'praxis' };
85
+
86
+ const result = performFieldLevelMerge(baseRow, serverRow, clientPayload);
87
+
88
+ expect(result.canMerge).toBe(true);
89
+ if (result.canMerge) {
90
+ expect(result.mergedPayload.name).toBe('Same Value');
91
+ }
92
+ });
93
+ });
94
+
95
+ describe('both changed to different values (true conflict)', () => {
96
+ test('returns conflict when both changed same field to different values', () => {
97
+ const baseRow = { id: 'team-1', name: 'Original', type: 'praxis' };
98
+ const serverRow = { id: 'team-1', name: 'Server Value', type: 'praxis' };
99
+ const clientPayload = { name: 'Client Value', type: 'praxis' };
100
+
101
+ const result = performFieldLevelMerge(baseRow, serverRow, clientPayload);
102
+
103
+ expect(result.canMerge).toBe(false);
104
+ if (!result.canMerge) {
105
+ expect(result.conflictingFields).toContain('name');
106
+ }
107
+ });
108
+
109
+ test('reports multiple conflicting fields', () => {
110
+ const baseRow = { id: 'team-1', name: 'Original', type: 'praxis' };
111
+ const serverRow = {
112
+ id: 'team-1',
113
+ name: 'Server Name',
114
+ type: 'server-type',
115
+ };
116
+ const clientPayload = { name: 'Client Name', type: 'client-type' };
117
+
118
+ const result = performFieldLevelMerge(baseRow, serverRow, clientPayload);
119
+
120
+ expect(result.canMerge).toBe(false);
121
+ if (!result.canMerge) {
122
+ expect(result.conflictingFields).toContain('name');
123
+ expect(result.conflictingFields).toContain('type');
124
+ expect(result.conflictingFields.length).toBe(2);
125
+ }
126
+ });
127
+ });
128
+
129
+ describe('mixed changes', () => {
130
+ test('handles client changed one field, server changed another', () => {
131
+ const baseRow = { id: 'team-1', name: 'Original', type: 'praxis' };
132
+ const serverRow = { id: 'team-1', name: 'Original', type: 'server-type' };
133
+ const clientPayload = { name: 'Client Name', type: 'praxis' };
134
+
135
+ const result = performFieldLevelMerge(baseRow, serverRow, clientPayload);
136
+
137
+ expect(result.canMerge).toBe(true);
138
+ if (result.canMerge) {
139
+ // Client's name change is applied
140
+ expect(result.mergedPayload.name).toBe('Client Name');
141
+ // Server's type change is kept
142
+ expect(result.mergedPayload.type).toBe('server-type');
143
+ }
144
+ });
145
+
146
+ test('handles conflict in one field but not another', () => {
147
+ const baseRow = { id: 'team-1', name: 'Original', type: 'praxis' };
148
+ const serverRow = { id: 'team-1', name: 'Server Name', type: 'praxis' };
149
+ const clientPayload = { name: 'Client Name', type: 'op' };
150
+
151
+ const result = performFieldLevelMerge(baseRow, serverRow, clientPayload);
152
+
153
+ expect(result.canMerge).toBe(false);
154
+ if (!result.canMerge) {
155
+ // Only name conflicts (both changed from Original to different values)
156
+ expect(result.conflictingFields).toContain('name');
157
+ // Type was only changed by client, so no conflict
158
+ expect(result.conflictingFields).not.toContain('type');
159
+ }
160
+ });
161
+ });
162
+
163
+ describe('neither changed', () => {
164
+ test('returns server values when neither changed', () => {
165
+ const baseRow = { id: 'team-1', name: 'Original', type: 'praxis' };
166
+ const serverRow = { id: 'team-1', name: 'Original', type: 'praxis' };
167
+ const clientPayload = { name: 'Original', type: 'praxis' };
168
+
169
+ const result = performFieldLevelMerge(baseRow, serverRow, clientPayload);
170
+
171
+ expect(result.canMerge).toBe(true);
172
+ if (result.canMerge) {
173
+ expect(result.mergedPayload.name).toBe('Original');
174
+ expect(result.mergedPayload.type).toBe('praxis');
175
+ }
176
+ });
177
+ });
178
+ });
179
+
180
+ describe('deepEqual (tested through performFieldLevelMerge)', () => {
181
+ describe('primitive equality', () => {
182
+ test('detects change in string values', () => {
183
+ const baseRow = { name: 'a' };
184
+ const serverRow = { name: 'b' };
185
+ const clientPayload = { name: 'c' };
186
+
187
+ const result = performFieldLevelMerge(baseRow, serverRow, clientPayload);
188
+
189
+ expect(result.canMerge).toBe(false);
190
+ });
191
+
192
+ test('detects change in number values', () => {
193
+ const baseRow = { count: 1 };
194
+ const serverRow = { count: 2 };
195
+ const clientPayload = { count: 3 };
196
+
197
+ const result = performFieldLevelMerge(baseRow, serverRow, clientPayload);
198
+
199
+ expect(result.canMerge).toBe(false);
200
+ });
201
+
202
+ test('handles null values', () => {
203
+ const baseRow = { value: null };
204
+ const serverRow = { value: null };
205
+ const clientPayload = { value: 'not null' };
206
+
207
+ const result = performFieldLevelMerge(baseRow, serverRow, clientPayload);
208
+
209
+ expect(result.canMerge).toBe(true);
210
+ if (result.canMerge) {
211
+ expect(result.mergedPayload.value).toBe('not null');
212
+ }
213
+ });
214
+
215
+ test('detects null to value change', () => {
216
+ const baseRow = { value: null };
217
+ const serverRow = { value: 'server' };
218
+ const clientPayload = { value: 'client' };
219
+
220
+ const result = performFieldLevelMerge(baseRow, serverRow, clientPayload);
221
+
222
+ expect(result.canMerge).toBe(false);
223
+ });
224
+ });
225
+
226
+ describe('array equality', () => {
227
+ test('detects equal arrays', () => {
228
+ const baseRow = { tags: ['a', 'b'] };
229
+ const serverRow = { tags: ['a', 'b'] };
230
+ const clientPayload = { tags: ['a', 'b', 'c'] };
231
+
232
+ const result = performFieldLevelMerge(baseRow, serverRow, clientPayload);
233
+
234
+ expect(result.canMerge).toBe(true);
235
+ if (result.canMerge) {
236
+ expect(result.mergedPayload.tags).toEqual(['a', 'b', 'c']);
237
+ }
238
+ });
239
+
240
+ test('detects array length difference', () => {
241
+ const baseRow = { tags: ['a'] };
242
+ const serverRow = { tags: ['a', 'b'] };
243
+ const clientPayload = { tags: ['a', 'c'] };
244
+
245
+ const result = performFieldLevelMerge(baseRow, serverRow, clientPayload);
246
+
247
+ expect(result.canMerge).toBe(false);
248
+ });
249
+
250
+ test('detects array element difference', () => {
251
+ const baseRow = { tags: ['a', 'b'] };
252
+ const serverRow = { tags: ['a', 'x'] };
253
+ const clientPayload = { tags: ['a', 'y'] };
254
+
255
+ const result = performFieldLevelMerge(baseRow, serverRow, clientPayload);
256
+
257
+ expect(result.canMerge).toBe(false);
258
+ });
259
+ });
260
+
261
+ describe('object equality', () => {
262
+ test('detects equal objects', () => {
263
+ const baseRow = { meta: { key: 'value' } };
264
+ const serverRow = { meta: { key: 'value' } };
265
+ const clientPayload = { meta: { key: 'new-value' } };
266
+
267
+ const result = performFieldLevelMerge(baseRow, serverRow, clientPayload);
268
+
269
+ expect(result.canMerge).toBe(true);
270
+ if (result.canMerge) {
271
+ expect(result.mergedPayload.meta).toEqual({ key: 'new-value' });
272
+ }
273
+ });
274
+
275
+ test('detects object key count difference', () => {
276
+ const baseRow = { meta: { a: 1 } };
277
+ const serverRow = { meta: { a: 1, b: 2 } };
278
+ const clientPayload = { meta: { a: 1, c: 3 } };
279
+
280
+ const result = performFieldLevelMerge(baseRow, serverRow, clientPayload);
281
+
282
+ expect(result.canMerge).toBe(false);
283
+ });
284
+
285
+ test('detects object value difference', () => {
286
+ const baseRow = { meta: { key: 'original' } };
287
+ const serverRow = { meta: { key: 'server' } };
288
+ const clientPayload = { meta: { key: 'client' } };
289
+
290
+ const result = performFieldLevelMerge(baseRow, serverRow, clientPayload);
291
+
292
+ expect(result.canMerge).toBe(false);
293
+ });
294
+ });
295
+
296
+ describe('nested structures', () => {
297
+ test('handles deeply nested objects', () => {
298
+ const baseRow = { data: { nested: { deep: 'original' } } };
299
+ const serverRow = { data: { nested: { deep: 'original' } } };
300
+ const clientPayload = { data: { nested: { deep: 'updated' } } };
301
+
302
+ const result = performFieldLevelMerge(baseRow, serverRow, clientPayload);
303
+
304
+ expect(result.canMerge).toBe(true);
305
+ if (result.canMerge) {
306
+ expect(result.mergedPayload.data).toEqual({
307
+ nested: { deep: 'updated' },
308
+ });
309
+ }
310
+ });
311
+
312
+ test('handles arrays within objects', () => {
313
+ const baseRow = { config: { items: [1, 2, 3] } };
314
+ const serverRow = { config: { items: [1, 2, 3] } };
315
+ const clientPayload = { config: { items: [1, 2, 3, 4] } };
316
+
317
+ const result = performFieldLevelMerge(baseRow, serverRow, clientPayload);
318
+
319
+ expect(result.canMerge).toBe(true);
320
+ if (result.canMerge) {
321
+ expect(result.mergedPayload.config).toEqual({ items: [1, 2, 3, 4] });
322
+ }
323
+ });
324
+ });
325
+ });
@@ -0,0 +1,170 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { logSyncEvent } from '../logger';
3
+ import {
4
+ captureSyncException,
5
+ configureSyncTelemetry,
6
+ countSyncMetric,
7
+ distributionSyncMetric,
8
+ gaugeSyncMetric,
9
+ getSyncTelemetry,
10
+ resetSyncTelemetry,
11
+ type SyncMetricOptions,
12
+ type SyncSpan,
13
+ type SyncSpanOptions,
14
+ type SyncTelemetry,
15
+ type SyncTelemetryEvent,
16
+ startSyncSpan,
17
+ } from '../telemetry';
18
+
19
+ interface CapturedCountMetric {
20
+ name: string;
21
+ value: number | undefined;
22
+ options: SyncMetricOptions | undefined;
23
+ }
24
+
25
+ interface CapturedValueMetric {
26
+ name: string;
27
+ value: number;
28
+ options: SyncMetricOptions | undefined;
29
+ }
30
+
31
+ function createTestTelemetry(calls: {
32
+ logs: SyncTelemetryEvent[];
33
+ countMetrics: CapturedCountMetric[];
34
+ gaugeMetrics: CapturedValueMetric[];
35
+ distributionMetrics: CapturedValueMetric[];
36
+ spans: SyncSpanOptions[];
37
+ exceptions: Array<{
38
+ error: unknown;
39
+ context: Record<string, unknown> | undefined;
40
+ }>;
41
+ }): SyncTelemetry {
42
+ return {
43
+ log(event) {
44
+ calls.logs.push(event);
45
+ },
46
+ tracer: {
47
+ startSpan(options, callback) {
48
+ calls.spans.push(options);
49
+ const span: SyncSpan = {
50
+ setAttribute() {},
51
+ setAttributes() {},
52
+ setStatus() {},
53
+ };
54
+ return callback(span);
55
+ },
56
+ },
57
+ metrics: {
58
+ count(name, value, options) {
59
+ calls.countMetrics.push({ name, value, options });
60
+ },
61
+ gauge(name, value, options) {
62
+ calls.gaugeMetrics.push({ name, value, options });
63
+ },
64
+ distribution(name, value, options) {
65
+ calls.distributionMetrics.push({ name, value, options });
66
+ },
67
+ },
68
+ captureException(error, context) {
69
+ calls.exceptions.push({ error, context });
70
+ },
71
+ };
72
+ }
73
+
74
+ describe('sync telemetry configuration', () => {
75
+ test('routes logger, metrics, spans, and exceptions to configured backend', () => {
76
+ const calls = {
77
+ logs: [] as SyncTelemetryEvent[],
78
+ countMetrics: [] as CapturedCountMetric[],
79
+ gaugeMetrics: [] as CapturedValueMetric[],
80
+ distributionMetrics: [] as CapturedValueMetric[],
81
+ spans: [] as SyncSpanOptions[],
82
+ exceptions: [] as Array<{
83
+ error: unknown;
84
+ context: Record<string, unknown> | undefined;
85
+ }>,
86
+ };
87
+ const telemetry = createTestTelemetry(calls);
88
+ const previous = getSyncTelemetry();
89
+
90
+ try {
91
+ configureSyncTelemetry(telemetry);
92
+
93
+ logSyncEvent({ event: 'sync.test.log', rowCount: 3 });
94
+
95
+ const spanResult = startSyncSpan(
96
+ {
97
+ name: 'sync.test.span',
98
+ op: 'sync.test',
99
+ attributes: { transport: 'ws' },
100
+ },
101
+ () => 'done'
102
+ );
103
+
104
+ countSyncMetric('sync.test.count', 2, {
105
+ attributes: { source: 'unit-test' },
106
+ });
107
+ gaugeSyncMetric('sync.test.gauge', 7, { unit: 'millisecond' });
108
+ distributionSyncMetric('sync.test.dist', 13);
109
+ captureSyncException(new Error('boom'), {
110
+ operation: 'unit-test',
111
+ });
112
+
113
+ expect(spanResult).toBe('done');
114
+ expect(calls.logs).toEqual([{ event: 'sync.test.log', rowCount: 3 }]);
115
+ expect(calls.spans).toEqual([
116
+ {
117
+ name: 'sync.test.span',
118
+ op: 'sync.test',
119
+ attributes: { transport: 'ws' },
120
+ },
121
+ ]);
122
+ expect(calls.countMetrics).toEqual([
123
+ {
124
+ name: 'sync.test.count',
125
+ value: 2,
126
+ options: { attributes: { source: 'unit-test' } },
127
+ },
128
+ ]);
129
+ expect(calls.gaugeMetrics).toEqual([
130
+ {
131
+ name: 'sync.test.gauge',
132
+ value: 7,
133
+ options: { unit: 'millisecond' },
134
+ },
135
+ ]);
136
+ expect(calls.distributionMetrics).toEqual([
137
+ {
138
+ name: 'sync.test.dist',
139
+ value: 13,
140
+ options: undefined,
141
+ },
142
+ ]);
143
+ expect(calls.exceptions).toHaveLength(1);
144
+ expect(calls.exceptions[0]?.context).toEqual({ operation: 'unit-test' });
145
+ } finally {
146
+ configureSyncTelemetry(previous);
147
+ }
148
+ });
149
+
150
+ test('resetSyncTelemetry swaps out custom telemetry backend', () => {
151
+ const calls = {
152
+ logs: [] as SyncTelemetryEvent[],
153
+ countMetrics: [] as CapturedCountMetric[],
154
+ gaugeMetrics: [] as CapturedValueMetric[],
155
+ distributionMetrics: [] as CapturedValueMetric[],
156
+ spans: [] as SyncSpanOptions[],
157
+ exceptions: [] as Array<{
158
+ error: unknown;
159
+ context: Record<string, unknown> | undefined;
160
+ }>,
161
+ };
162
+ const telemetry = createTestTelemetry(calls);
163
+
164
+ configureSyncTelemetry(telemetry);
165
+ resetSyncTelemetry();
166
+ logSyncEvent({ event: 'sync.default.logger' });
167
+
168
+ expect(calls.logs).toHaveLength(0);
169
+ });
170
+ });
@@ -0,0 +1,27 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import { isRecord, randomId } from '../utils';
3
+
4
+ describe('isRecord', () => {
5
+ it('returns true for plain objects', () => {
6
+ expect(isRecord({ a: 1 })).toBe(true);
7
+ });
8
+
9
+ it('returns false for null and arrays', () => {
10
+ expect(isRecord(null)).toBe(false);
11
+ expect(isRecord([])).toBe(false);
12
+ });
13
+ });
14
+
15
+ describe('randomId', () => {
16
+ it('returns a non-empty string id', () => {
17
+ const id = randomId();
18
+ expect(typeof id).toBe('string');
19
+ expect(id.length > 0).toBe(true);
20
+ });
21
+
22
+ it('generates different ids across sequential calls', () => {
23
+ const first = randomId();
24
+ const second = randomId();
25
+ expect(first).not.toBe(second);
26
+ });
27
+ });