@powersync/service-core-tests 0.17.1 → 0.18.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 (31) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/test-utils/AbstractStreamTestContext.d.ts +1 -1
  3. package/dist/test-utils/AbstractStreamTestContext.js +2 -3
  4. package/dist/test-utils/AbstractStreamTestContext.js.map +1 -1
  5. package/dist/test-utils/StorageDataHelpers.d.ts +3 -2
  6. package/dist/test-utils/StorageDataHelpers.js +40 -2
  7. package/dist/test-utils/StorageDataHelpers.js.map +1 -1
  8. package/dist/test-utils/general-utils.d.ts +3 -1
  9. package/dist/test-utils/general-utils.js +13 -1
  10. package/dist/test-utils/general-utils.js.map +1 -1
  11. package/dist/test-utils/stream_utils.d.ts +0 -1
  12. package/dist/test-utils/stream_utils.js +0 -10
  13. package/dist/test-utils/stream_utils.js.map +1 -1
  14. package/dist/tests/register-compacting-tests.js +7 -7
  15. package/dist/tests/register-compacting-tests.js.map +1 -1
  16. package/dist/tests/register-data-storage-checkpoint-tests.js +96 -3
  17. package/dist/tests/register-data-storage-checkpoint-tests.js.map +1 -1
  18. package/dist/tests/register-data-storage-data-tests.js +113 -94
  19. package/dist/tests/register-data-storage-data-tests.js.map +1 -1
  20. package/dist/tests/register-sync-tests.js +259 -75
  21. package/dist/tests/register-sync-tests.js.map +1 -1
  22. package/package.json +5 -5
  23. package/src/test-utils/AbstractStreamTestContext.ts +2 -3
  24. package/src/test-utils/StorageDataHelpers.ts +45 -2
  25. package/src/test-utils/general-utils.ts +27 -3
  26. package/src/test-utils/stream_utils.ts +0 -11
  27. package/src/tests/register-compacting-tests.ts +7 -7
  28. package/src/tests/register-data-storage-checkpoint-tests.ts +122 -3
  29. package/src/tests/register-data-storage-data-tests.ts +129 -132
  30. package/src/tests/register-sync-tests.ts +280 -77
  31. package/tsconfig.tsbuildinfo +1 -1
@@ -12,7 +12,7 @@ import { JSONBig } from '@powersync/service-jsonbig';
12
12
  import path from 'path';
13
13
  import * as timers from 'timers/promises';
14
14
  import { fileURLToPath } from 'url';
15
- import { expect, test } from 'vitest';
15
+ import { expect, test, vi } from 'vitest';
16
16
  import * as test_utils from '../test-utils/test-utils-index.js';
17
17
  import { bucketRequest, METRICS_HELPER } from '../test-utils/test-utils-index.js';
18
18
 
@@ -251,29 +251,36 @@ streams:
251
251
  expect(lines).toMatchSnapshot();
252
252
  });
253
253
 
254
- test('sync interrupts low-priority buckets on new checkpoints', async () => {
254
+ test('carries pending low-priority buckets into a new checkpoint', async () => {
255
255
  await using f = await factory();
256
256
 
257
257
  const syncRules = await updateSyncRules(f, {
258
258
  content: `
259
259
  bucket_definitions:
260
- b0:
260
+ b0a:
261
261
  priority: 2
262
262
  data:
263
- - SELECT * FROM test WHERE LENGTH(id) <= 5;
263
+ - SELECT * FROM test WHERE substring(id, 1, 6) = 'first-';
264
+ b0b:
265
+ priority: 3
266
+ data:
267
+ - SELECT * FROM test WHERE substring(id, 1, 4) = 'low-';
264
268
  b1:
265
269
  priority: 1
266
270
  data:
267
- - SELECT * FROM test WHERE LENGTH(id) > 5;
271
+ - SELECT * FROM test WHERE substring(id, 1, 8) = 'highprio';
268
272
  `
269
273
  });
270
274
 
271
275
  const bucketStorage = f.getInstance(syncRules);
272
276
  await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
273
277
  const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
278
+ const syncRulesContent = syncRules.syncConfigContent[0];
279
+ const b0aBucket = test_utils.bucketRequest(syncRulesContent, 'b0a[]').bucket;
280
+ const b0bBucket = test_utils.bucketRequest(syncRulesContent, 'b0b[]').bucket;
281
+ const b1Bucket = test_utils.bucketRequest(syncRulesContent, 'b1[]').bucket;
274
282
 
275
283
  await writer.markAllSnapshotDone('0/1');
276
- // Initial data: Add one priority row and 10k low-priority rows.
277
284
  await writer.save({
278
285
  sourceTable: testTable,
279
286
  tag: storage.SaveOperationTag.INSERT,
@@ -283,15 +290,26 @@ bucket_definitions:
283
290
  },
284
291
  afterReplicaId: 'highprio'
285
292
  });
286
- for (let i = 0; i < 10_000; i++) {
293
+ for (let i = 0; i < 999; i++) {
287
294
  await writer.save({
288
295
  sourceTable: testTable,
289
296
  tag: storage.SaveOperationTag.INSERT,
290
297
  after: {
291
- id: `${i}`,
292
- description: 'low prio'
298
+ id: `first-${i}`,
299
+ description: 'first low-priority bucket'
293
300
  },
294
- afterReplicaId: `${i}`
301
+ afterReplicaId: `first-${i}`
302
+ });
303
+ }
304
+ for (let i = 0; i < 9_001; i++) {
305
+ await writer.save({
306
+ sourceTable: testTable,
307
+ tag: storage.SaveOperationTag.INSERT,
308
+ after: {
309
+ id: `low-${i}`,
310
+ description: 'last low-priority bucket'
311
+ },
312
+ afterReplicaId: `low-${i}`
295
313
  });
296
314
  }
297
315
 
@@ -311,8 +329,12 @@ bucket_definitions:
311
329
  isEncodingAsBson: false
312
330
  });
313
331
 
314
- let sentCheckpoints = 0;
315
- let sentRows = 0;
332
+ const dataLines: Array<{ bucket: string; objectIds: string[]; afterCheckpointDiff: boolean }> = [];
333
+ const partialCheckpoints: Array<{ priority: number; afterCheckpointDiff: boolean }> = [];
334
+ const checkpoints: any[] = [];
335
+ const checkpointCompletes: any[] = [];
336
+ let afterCheckpointDiff = false;
337
+ let interruptionTriggered = false;
316
338
 
317
339
  for await (let next of stream) {
318
340
  if (typeof next == 'string') {
@@ -320,10 +342,13 @@ bucket_definitions:
320
342
  }
321
343
  if (typeof next === 'object' && next !== null) {
322
344
  if ('partial_checkpoint_complete' in next) {
323
- if (sentCheckpoints == 1) {
324
- // Save new data to interrupt the low-priority sync.
345
+ partialCheckpoints.push({
346
+ priority: next.partial_checkpoint_complete.priority,
347
+ afterCheckpointDiff
348
+ });
325
349
 
326
- // Add another high-priority row. This should interrupt the long-running low-priority sync.
350
+ if (next.partial_checkpoint_complete.priority == 2 && !interruptionTriggered) {
351
+ interruptionTriggered = true;
327
352
  await writer.save({
328
353
  sourceTable: testTable,
329
354
  tag: storage.SaveOperationTag.INSERT,
@@ -335,28 +360,76 @@ bucket_definitions:
335
360
  });
336
361
 
337
362
  await writer.commit('0/2');
338
- } else {
339
- // Low-priority sync from the first checkpoint was interrupted. This should not happen before
340
- // 1000 low-priority items were synchronized.
341
- expect(sentCheckpoints).toBe(2);
342
- expect(sentRows).toBeGreaterThan(1000);
363
+ // Let the checkpoint watcher observe the update before requesting the next priority.
364
+ await timers.setTimeout(50);
343
365
  }
344
366
  }
345
367
  if ('checkpoint' in next || 'checkpoint_diff' in next) {
346
- sentCheckpoints += 1;
368
+ checkpoints.push(next);
369
+ afterCheckpointDiff = 'checkpoint_diff' in next;
347
370
  }
348
371
 
349
372
  if ('data' in next) {
350
- sentRows += next.data.data.length;
373
+ dataLines.push({
374
+ bucket: next.data.bucket,
375
+ objectIds: next.data.data.flatMap((entry: any) => (entry.object_id == null ? [] : [entry.object_id])),
376
+ afterCheckpointDiff
377
+ });
351
378
  }
352
379
  if ('checkpoint_complete' in next) {
380
+ checkpointCompletes.push(next);
353
381
  break;
354
382
  }
355
383
  }
356
384
  }
357
385
 
358
- expect(sentCheckpoints).toBe(2);
359
- expect(sentRows).toBe(10002);
386
+ // Expected flow (data may be split into any number of chunks):
387
+ //
388
+ // checkpoint
389
+ // data: b1 contains highprio
390
+ // partial_checkpoint_complete: priority 1
391
+ // data: b0a contains all 999 first-* rows (1,000 operations including highprio)
392
+ // partial_checkpoint_complete: priority 2
393
+ // ## add highprio2, interrupting before b0b starts
394
+ // checkpoint_diff
395
+ // data: b1 contains highprio2
396
+ // partial_checkpoint_complete: priority 1
397
+ // data: b0b contains all 9,001 low-* rows carried over from the interrupted checkpoint
398
+ // checkpoint_complete: only for the new checkpoint
399
+ expect(interruptionTriggered).toBe(true);
400
+ expect(checkpoints).toHaveLength(2);
401
+ expect(checkpoints[0]).toHaveProperty('checkpoint');
402
+ expect(checkpoints[1]).toHaveProperty('checkpoint_diff');
403
+ expect(partialCheckpoints).toEqual([
404
+ { priority: 1, afterCheckpointDiff: false },
405
+ { priority: 2, afterCheckpointDiff: false },
406
+ { priority: 1, afterCheckpointDiff: true }
407
+ ]);
408
+ expect(checkpointCompletes).toEqual([
409
+ {
410
+ checkpoint_complete: {
411
+ last_op_id: checkpoints[1].checkpoint_diff.last_op_id
412
+ }
413
+ }
414
+ ]);
415
+
416
+ const objectIdsFor = (bucket: string, afterDiff: boolean) =>
417
+ dataLines
418
+ .filter((line) => line.bucket == bucket && line.afterCheckpointDiff == afterDiff)
419
+ .flatMap((line) => line.objectIds);
420
+
421
+ expect(objectIdsFor(b1Bucket, false)).toEqual(['highprio']);
422
+ const b0aObjectIds = objectIdsFor(b0aBucket, false);
423
+ expect(new Set(b0aObjectIds)).toEqual(new Set(Array.from({ length: 999 }, (_, i) => `first-${i}`)));
424
+ expect(b0aObjectIds).toHaveLength(999);
425
+ expect(objectIdsFor(b0bBucket, false)).toEqual([]);
426
+
427
+ expect(objectIdsFor(b1Bucket, true)).toEqual(['highprio2']);
428
+ expect(objectIdsFor(b0aBucket, true)).toEqual([]);
429
+
430
+ const b0bObjectIds = objectIdsFor(b0bBucket, true);
431
+ expect(new Set(b0bObjectIds)).toEqual(new Set(Array.from({ length: 9_001 }, (_, i) => `low-${i}`)));
432
+ expect(b0bObjectIds).toHaveLength(9_001);
360
433
  });
361
434
 
362
435
  test('sync interruptions with unrelated data', async () => {
@@ -490,37 +563,36 @@ bucket_definitions:
490
563
  expect(sentRows).toBe(10002);
491
564
  });
492
565
 
493
- test('sync interrupts low-priority buckets on new checkpoints (2)', async () => {
566
+ test('restarts updated low-priority buckets at a new checkpoint', async () => {
494
567
  await using f = await factory();
495
568
 
496
- // bucket0a -> send all data
497
- // then interrupt checkpoint with new data for all buckets
498
- // -> data for all buckets should be sent in the new checkpoint
499
-
500
569
  const syncRules = await updateSyncRules(f, {
501
570
  content: `
502
571
  bucket_definitions:
503
572
  b0a:
504
573
  priority: 2
505
574
  data:
506
- - SELECT * FROM test WHERE LENGTH(id) <= 5;
575
+ - SELECT * FROM test WHERE substring(id, 1, 6) = 'first-';
507
576
  b0b:
508
- priority: 2
577
+ priority: 3
509
578
  data:
510
- - SELECT * FROM test WHERE LENGTH(id) <= 5;
579
+ - SELECT * FROM test WHERE substring(id, 1, 4) = 'low-';
511
580
  b1:
512
581
  priority: 1
513
582
  data:
514
- - SELECT * FROM test WHERE LENGTH(id) > 5;
583
+ - SELECT * FROM test WHERE substring(id, 1, 8) = 'highprio';
515
584
  `
516
585
  });
517
586
 
518
587
  const bucketStorage = f.getInstance(syncRules);
519
588
  await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
520
589
  const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
590
+ const syncRulesContent = syncRules.syncConfigContent[0];
591
+ const b0aBucket = test_utils.bucketRequest(syncRulesContent, 'b0a[]').bucket;
592
+ const b0bBucket = test_utils.bucketRequest(syncRulesContent, 'b0b[]').bucket;
593
+ const b1Bucket = test_utils.bucketRequest(syncRulesContent, 'b1[]').bucket;
521
594
 
522
595
  await writer.markAllSnapshotDone('0/1');
523
- // Initial data: Add one priority row and 10k low-priority rows.
524
596
  await writer.save({
525
597
  sourceTable: testTable,
526
598
  tag: storage.SaveOperationTag.INSERT,
@@ -530,15 +602,26 @@ bucket_definitions:
530
602
  },
531
603
  afterReplicaId: 'highprio'
532
604
  });
605
+ for (let i = 0; i < 999; i++) {
606
+ await writer.save({
607
+ sourceTable: testTable,
608
+ tag: storage.SaveOperationTag.INSERT,
609
+ after: {
610
+ id: `first-${i}`,
611
+ description: 'first low-priority bucket'
612
+ },
613
+ afterReplicaId: `first-${i}`
614
+ });
615
+ }
533
616
  for (let i = 0; i < 2_000; i++) {
534
617
  await writer.save({
535
618
  sourceTable: testTable,
536
619
  tag: storage.SaveOperationTag.INSERT,
537
620
  after: {
538
- id: `${i}`,
621
+ id: `low-${i}`,
539
622
  description: 'low prio'
540
623
  },
541
- afterReplicaId: `${i}`
624
+ afterReplicaId: `low-${i}`
542
625
  });
543
626
  }
544
627
 
@@ -558,8 +641,12 @@ bucket_definitions:
558
641
  isEncodingAsBson: false
559
642
  });
560
643
 
561
- let sentRows = 0;
562
- let lines: any[] = [];
644
+ const dataLines: Array<{ bucket: string; objectIds: string[]; afterCheckpointDiff: boolean }> = [];
645
+ const partialCheckpoints: Array<{ priority: number; afterCheckpointDiff: boolean }> = [];
646
+ const checkpoints: any[] = [];
647
+ const checkpointCompletes: any[] = [];
648
+ let afterCheckpointDiff = false;
649
+ let interruptionTriggered = false;
563
650
 
564
651
  for await (let next of stream) {
565
652
  if (typeof next == 'string') {
@@ -567,19 +654,13 @@ bucket_definitions:
567
654
  }
568
655
  if (typeof next === 'object' && next !== null) {
569
656
  if ('partial_checkpoint_complete' in next) {
570
- lines.push(next);
571
- }
572
- if ('checkpoint' in next || 'checkpoint_diff' in next) {
573
- lines.push(next);
574
- }
657
+ partialCheckpoints.push({
658
+ priority: next.partial_checkpoint_complete.priority,
659
+ afterCheckpointDiff
660
+ });
575
661
 
576
- if ('data' in next) {
577
- lines.push({ data: { ...next.data, data: undefined } });
578
- sentRows += next.data.data.length;
579
-
580
- if (sentRows == 1001) {
581
- // Save new data to interrupt the low-priority sync.
582
- // Add another high-priority row. This should interrupt the long-running low-priority sync.
662
+ if (next.partial_checkpoint_complete.priority == 2 && !interruptionTriggered) {
663
+ interruptionTriggered = true;
583
664
  await writer.save({
584
665
  sourceTable: testTable,
585
666
  tag: storage.SaveOperationTag.INSERT,
@@ -590,51 +671,87 @@ bucket_definitions:
590
671
  afterReplicaId: 'highprio2'
591
672
  });
592
673
 
593
- // Also add a low-priority row
594
674
  await writer.save({
595
675
  sourceTable: testTable,
596
676
  tag: storage.SaveOperationTag.INSERT,
597
677
  after: {
598
- id: '2001',
678
+ id: 'low-2000',
599
679
  description: 'Another low-priority row'
600
680
  },
601
- afterReplicaId: '2001'
681
+ afterReplicaId: 'low-2000'
602
682
  });
603
683
 
604
684
  await writer.commit('0/2');
605
- }
606
-
607
- if (sentRows >= 1000 && sentRows <= 2001) {
608
- // pause for a bit to give the stream time to process interruptions.
609
- // This covers the data batch above and the next one.
685
+ // Let the checkpoint watcher observe the update before requesting the next priority.
610
686
  await timers.setTimeout(50);
611
687
  }
612
688
  }
689
+ if ('checkpoint' in next || 'checkpoint_diff' in next) {
690
+ checkpoints.push(next);
691
+ afterCheckpointDiff = 'checkpoint_diff' in next;
692
+ }
693
+
694
+ if ('data' in next) {
695
+ dataLines.push({
696
+ bucket: next.data.bucket,
697
+ objectIds: next.data.data.flatMap((entry: any) => (entry.object_id == null ? [] : [entry.object_id])),
698
+ afterCheckpointDiff
699
+ });
700
+ }
613
701
  if ('checkpoint_complete' in next) {
614
- lines.push(next);
702
+ checkpointCompletes.push(next);
615
703
  break;
616
704
  }
617
705
  }
618
706
  }
619
707
 
620
- // Expected lines (full details in snapshot):
708
+ // Expected flow (data may be split into any number of chunks):
621
709
  //
622
- // checkpoint (4001)
623
- // data (b1[] 0 -> 1)
624
- // partial_checkpoint_complete (4001, priority 1)
625
- // data (b0a[], 0 -> 2000)
626
- // ## adds new data, interrupting the checkpoint
627
- // data (b0a[], 2000 -> 4000) # expected - stream is already busy with this by the time it receives the interruption
628
- // checkpoint_diff (4004)
629
- // data (b1[], 1 -> 4002)
630
- // partial_checkpoint_complete (4004, priority 1)
631
- // data (b0a[], 4000 -> 4003)
632
- // data (b0b[], 0 -> 1999)
633
- // data (b0b[], 1999 -> 3999)
634
- // data (b0b[], 3999 -> 4004)
635
- // checkpoint_complete (4004)
636
- expect(lines).toMatchSnapshot();
637
- expect(sentRows).toBe(4004);
710
+ // checkpoint
711
+ // data: b1 contains highprio
712
+ // partial_checkpoint_complete: priority 1
713
+ // data: b0a contains all 999 first-* rows (1,000 operations including highprio)
714
+ // partial_checkpoint_complete: priority 2
715
+ // ## add highprio2 and low-2000, interrupting before b0b starts
716
+ // checkpoint_diff
717
+ // data: b1 contains highprio2
718
+ // partial_checkpoint_complete: priority 1
719
+ // data: b0b contains all 2,001 low-* rows
720
+ // checkpoint_complete: only for the new checkpoint
721
+ expect(interruptionTriggered).toBe(true);
722
+ expect(checkpoints).toHaveLength(2);
723
+ expect(checkpoints[0]).toHaveProperty('checkpoint');
724
+ expect(checkpoints[1]).toHaveProperty('checkpoint_diff');
725
+ expect(partialCheckpoints).toEqual([
726
+ { priority: 1, afterCheckpointDiff: false },
727
+ { priority: 2, afterCheckpointDiff: false },
728
+ { priority: 1, afterCheckpointDiff: true }
729
+ ]);
730
+ expect(checkpointCompletes).toEqual([
731
+ {
732
+ checkpoint_complete: {
733
+ last_op_id: checkpoints[1].checkpoint_diff.last_op_id
734
+ }
735
+ }
736
+ ]);
737
+
738
+ const objectIdsFor = (bucket: string, afterDiff: boolean) =>
739
+ dataLines
740
+ .filter((line) => line.bucket == bucket && line.afterCheckpointDiff == afterDiff)
741
+ .flatMap((line) => line.objectIds);
742
+
743
+ expect(objectIdsFor(b1Bucket, false)).toEqual(['highprio']);
744
+ const b0aObjectIds = objectIdsFor(b0aBucket, false);
745
+ expect(new Set(b0aObjectIds)).toEqual(new Set(Array.from({ length: 999 }, (_, i) => `first-${i}`)));
746
+ expect(b0aObjectIds).toHaveLength(999);
747
+ expect(objectIdsFor(b0bBucket, false)).toEqual([]);
748
+
749
+ expect(objectIdsFor(b1Bucket, true)).toEqual(['highprio2']);
750
+ expect(objectIdsFor(b0aBucket, true)).toEqual([]);
751
+
752
+ const b0bObjectIds = objectIdsFor(b0bBucket, true);
753
+ expect(new Set(b0bObjectIds)).toEqual(new Set(Array.from({ length: 2_001 }, (_, i) => `low-${i}`)));
754
+ expect(b0bObjectIds).toHaveLength(2_001);
638
755
  });
639
756
 
640
757
  test('sends checkpoint complete line for empty checkpoint', async () => {
@@ -1090,6 +1207,92 @@ bucket_definitions:
1090
1207
  expect(expLines).toMatchSnapshot();
1091
1208
  });
1092
1209
 
1210
+ test('checksum invalidation skips the candidate checkpoint', async (context) => {
1211
+ await using f = await factory();
1212
+
1213
+ const syncRules = await updateSyncRules(f, {
1214
+ content: BASIC_SYNC_RULES
1215
+ });
1216
+
1217
+ const bucketStorage = await f.getInstance(syncRules);
1218
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
1219
+ const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
1220
+
1221
+ await writer.markAllSnapshotDone('0/1');
1222
+ await writer.save({
1223
+ sourceTable: testTable,
1224
+ tag: storage.SaveOperationTag.INSERT,
1225
+ after: {
1226
+ id: 't1',
1227
+ description: 'Test 1'
1228
+ },
1229
+ afterReplicaId: 't1'
1230
+ });
1231
+ await writer.commit('0/1');
1232
+
1233
+ let resolveInvalidatedChecksum!: () => void;
1234
+ const invalidatedChecksum = new Promise<void>((resolve) => {
1235
+ resolveInvalidatedChecksum = resolve;
1236
+ });
1237
+ const checksumSpy = vi.spyOn(bucketStorage, 'getChecksums').mockImplementationOnce(async (checkpoint, buckets) => {
1238
+ resolveInvalidatedChecksum();
1239
+ throw new storage.CheckpointChecksumInvalidatedError(checkpoint.checkpoint, buckets[0].bucket);
1240
+ });
1241
+
1242
+ const stream = sync.streamResponse({
1243
+ syncContext,
1244
+ bucketStorage,
1245
+ syncRules: bucketStorage.getParsedSyncRules(test_utils.PARSE_OPTIONS),
1246
+ params: {
1247
+ buckets: [],
1248
+ include_checksum: true,
1249
+ raw_data: true
1250
+ },
1251
+ tracker,
1252
+ token: new JwtPayload({ sub: '', exp: Date.now() / 1000 + 10 }),
1253
+ isEncodingAsBson: false
1254
+ });
1255
+
1256
+ const iter = stream[Symbol.asyncIterator]();
1257
+ context.onTestFinished(() => {
1258
+ iter.return?.();
1259
+ });
1260
+
1261
+ const linesPromise = getCheckpointLines(iter, { consume: true });
1262
+ await invalidatedChecksum;
1263
+
1264
+ // The first candidate was discarded before CheckpointLine.advance(). A later
1265
+ // checkpoint must therefore be calculated from the original connection state.
1266
+ await writer.save({
1267
+ sourceTable: testTable,
1268
+ tag: storage.SaveOperationTag.INSERT,
1269
+ after: {
1270
+ id: 't2',
1271
+ description: 'Test 2'
1272
+ },
1273
+ afterReplicaId: 't2'
1274
+ });
1275
+ await writer.commit('0/2');
1276
+
1277
+ const lines = await linesPromise;
1278
+ expect(checksumSpy).toHaveBeenCalledTimes(2);
1279
+ expect(lines[0]).toEqual({
1280
+ checkpoint: expect.objectContaining({
1281
+ last_op_id: '2'
1282
+ })
1283
+ });
1284
+ expect(lines).not.toContainEqual({
1285
+ checkpoint: expect.objectContaining({
1286
+ last_op_id: '1'
1287
+ })
1288
+ });
1289
+ expect(lines.at(-1)).toEqual({
1290
+ checkpoint_complete: expect.objectContaining({
1291
+ last_op_id: '2'
1292
+ })
1293
+ });
1294
+ });
1295
+
1093
1296
  test('compacting data - invalidate checkpoint', async (context) => {
1094
1297
  // This tests a case of a compact operation invalidating a checkpoint in the
1095
1298
  // middle of syncing data.
@@ -1403,7 +1606,7 @@ bucket_definitions:
1403
1606
  heads: { '1': '1/0' }
1404
1607
  }
1405
1608
  ])
1406
- ).get('test')!;
1609
+ ).writeCheckpoints.get('test')!;
1407
1610
 
1408
1611
  const params: sync.SyncStreamParameters = {
1409
1612
  syncContext,