@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
@@ -3,7 +3,7 @@ import { JSONBig } from '@powersync/service-jsonbig';
3
3
  import path from 'path';
4
4
  import * as timers from 'timers/promises';
5
5
  import { fileURLToPath } from 'url';
6
- import { expect, test } from 'vitest';
6
+ import { expect, test, vi } from 'vitest';
7
7
  import * as test_utils from '../test-utils/test-utils-index.js';
8
8
  import { bucketRequest, METRICS_HELPER } from '../test-utils/test-utils-index.js';
9
9
  const __filename = fileURLToPath(import.meta.url);
@@ -205,26 +205,33 @@ streams:
205
205
  const lines = await consumeCheckpointLines(stream);
206
206
  expect(lines).toMatchSnapshot();
207
207
  });
208
- test('sync interrupts low-priority buckets on new checkpoints', async () => {
208
+ test('carries pending low-priority buckets into a new checkpoint', async () => {
209
209
  await using f = await factory();
210
210
  const syncRules = await updateSyncRules(f, {
211
211
  content: `
212
212
  bucket_definitions:
213
- b0:
213
+ b0a:
214
214
  priority: 2
215
215
  data:
216
- - SELECT * FROM test WHERE LENGTH(id) <= 5;
216
+ - SELECT * FROM test WHERE substring(id, 1, 6) = 'first-';
217
+ b0b:
218
+ priority: 3
219
+ data:
220
+ - SELECT * FROM test WHERE substring(id, 1, 4) = 'low-';
217
221
  b1:
218
222
  priority: 1
219
223
  data:
220
- - SELECT * FROM test WHERE LENGTH(id) > 5;
224
+ - SELECT * FROM test WHERE substring(id, 1, 8) = 'highprio';
221
225
  `
222
226
  });
223
227
  const bucketStorage = f.getInstance(syncRules);
224
228
  await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
225
229
  const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
230
+ const syncRulesContent = syncRules.syncConfigContent[0];
231
+ const b0aBucket = test_utils.bucketRequest(syncRulesContent, 'b0a[]').bucket;
232
+ const b0bBucket = test_utils.bucketRequest(syncRulesContent, 'b0b[]').bucket;
233
+ const b1Bucket = test_utils.bucketRequest(syncRulesContent, 'b1[]').bucket;
226
234
  await writer.markAllSnapshotDone('0/1');
227
- // Initial data: Add one priority row and 10k low-priority rows.
228
235
  await writer.save({
229
236
  sourceTable: testTable,
230
237
  tag: storage.SaveOperationTag.INSERT,
@@ -234,15 +241,26 @@ bucket_definitions:
234
241
  },
235
242
  afterReplicaId: 'highprio'
236
243
  });
237
- for (let i = 0; i < 10_000; i++) {
244
+ for (let i = 0; i < 999; i++) {
238
245
  await writer.save({
239
246
  sourceTable: testTable,
240
247
  tag: storage.SaveOperationTag.INSERT,
241
248
  after: {
242
- id: `${i}`,
243
- description: 'low prio'
249
+ id: `first-${i}`,
250
+ description: 'first low-priority bucket'
244
251
  },
245
- afterReplicaId: `${i}`
252
+ afterReplicaId: `first-${i}`
253
+ });
254
+ }
255
+ for (let i = 0; i < 9_001; i++) {
256
+ await writer.save({
257
+ sourceTable: testTable,
258
+ tag: storage.SaveOperationTag.INSERT,
259
+ after: {
260
+ id: `low-${i}`,
261
+ description: 'last low-priority bucket'
262
+ },
263
+ afterReplicaId: `low-${i}`
246
264
  });
247
265
  }
248
266
  await writer.commit('0/1');
@@ -259,17 +277,24 @@ bucket_definitions:
259
277
  token: new JwtPayload({ sub: '', exp: Date.now() / 1000 + 10 }),
260
278
  isEncodingAsBson: false
261
279
  });
262
- let sentCheckpoints = 0;
263
- let sentRows = 0;
280
+ const dataLines = [];
281
+ const partialCheckpoints = [];
282
+ const checkpoints = [];
283
+ const checkpointCompletes = [];
284
+ let afterCheckpointDiff = false;
285
+ let interruptionTriggered = false;
264
286
  for await (let next of stream) {
265
287
  if (typeof next == 'string') {
266
288
  next = JSON.parse(next);
267
289
  }
268
290
  if (typeof next === 'object' && next !== null) {
269
291
  if ('partial_checkpoint_complete' in next) {
270
- if (sentCheckpoints == 1) {
271
- // Save new data to interrupt the low-priority sync.
272
- // Add another high-priority row. This should interrupt the long-running low-priority sync.
292
+ partialCheckpoints.push({
293
+ priority: next.partial_checkpoint_complete.priority,
294
+ afterCheckpointDiff
295
+ });
296
+ if (next.partial_checkpoint_complete.priority == 2 && !interruptionTriggered) {
297
+ interruptionTriggered = true;
273
298
  await writer.save({
274
299
  sourceTable: testTable,
275
300
  tag: storage.SaveOperationTag.INSERT,
@@ -280,27 +305,69 @@ bucket_definitions:
280
305
  afterReplicaId: 'highprio2'
281
306
  });
282
307
  await writer.commit('0/2');
283
- }
284
- else {
285
- // Low-priority sync from the first checkpoint was interrupted. This should not happen before
286
- // 1000 low-priority items were synchronized.
287
- expect(sentCheckpoints).toBe(2);
288
- expect(sentRows).toBeGreaterThan(1000);
308
+ // Let the checkpoint watcher observe the update before requesting the next priority.
309
+ await timers.setTimeout(50);
289
310
  }
290
311
  }
291
312
  if ('checkpoint' in next || 'checkpoint_diff' in next) {
292
- sentCheckpoints += 1;
313
+ checkpoints.push(next);
314
+ afterCheckpointDiff = 'checkpoint_diff' in next;
293
315
  }
294
316
  if ('data' in next) {
295
- sentRows += next.data.data.length;
317
+ dataLines.push({
318
+ bucket: next.data.bucket,
319
+ objectIds: next.data.data.flatMap((entry) => (entry.object_id == null ? [] : [entry.object_id])),
320
+ afterCheckpointDiff
321
+ });
296
322
  }
297
323
  if ('checkpoint_complete' in next) {
324
+ checkpointCompletes.push(next);
298
325
  break;
299
326
  }
300
327
  }
301
328
  }
302
- expect(sentCheckpoints).toBe(2);
303
- expect(sentRows).toBe(10002);
329
+ // Expected flow (data may be split into any number of chunks):
330
+ //
331
+ // checkpoint
332
+ // data: b1 contains highprio
333
+ // partial_checkpoint_complete: priority 1
334
+ // data: b0a contains all 999 first-* rows (1,000 operations including highprio)
335
+ // partial_checkpoint_complete: priority 2
336
+ // ## add highprio2, interrupting before b0b starts
337
+ // checkpoint_diff
338
+ // data: b1 contains highprio2
339
+ // partial_checkpoint_complete: priority 1
340
+ // data: b0b contains all 9,001 low-* rows carried over from the interrupted checkpoint
341
+ // checkpoint_complete: only for the new checkpoint
342
+ expect(interruptionTriggered).toBe(true);
343
+ expect(checkpoints).toHaveLength(2);
344
+ expect(checkpoints[0]).toHaveProperty('checkpoint');
345
+ expect(checkpoints[1]).toHaveProperty('checkpoint_diff');
346
+ expect(partialCheckpoints).toEqual([
347
+ { priority: 1, afterCheckpointDiff: false },
348
+ { priority: 2, afterCheckpointDiff: false },
349
+ { priority: 1, afterCheckpointDiff: true }
350
+ ]);
351
+ expect(checkpointCompletes).toEqual([
352
+ {
353
+ checkpoint_complete: {
354
+ last_op_id: checkpoints[1].checkpoint_diff.last_op_id
355
+ }
356
+ }
357
+ ]);
358
+ const objectIdsFor = (bucket, afterDiff) => dataLines
359
+ .filter((line) => line.bucket == bucket && line.afterCheckpointDiff == afterDiff)
360
+ .flatMap((line) => line.objectIds);
361
+ expect(objectIdsFor(b1Bucket, false)).toEqual(['highprio']);
362
+ const b0aObjectIds = objectIdsFor(b0aBucket, false);
363
+ expect(new Set(b0aObjectIds)).toEqual(new Set(Array.from({ length: 999 }, (_, i) => `first-${i}`)));
364
+ expect(b0aObjectIds).toHaveLength(999);
365
+ expect(objectIdsFor(b0bBucket, false)).toEqual([]);
366
+ expect(objectIdsFor(b1Bucket, true)).toEqual(['highprio2']);
367
+ expect(objectIdsFor(b0aBucket, true)).toEqual([]);
368
+ const b0bObjectIds = objectIdsFor(b0bBucket, true);
369
+ expect(new Set(b0bObjectIds)).toEqual(new Set(Array.from({ length: 9_001 }, (_, i) => `low-${i}`)));
370
+ expect(b0bObjectIds).toHaveLength(9_001);
304
371
  });
305
372
  test('sync interruptions with unrelated data', async () => {
306
373
  await using f = await factory();
@@ -420,33 +487,33 @@ bucket_definitions:
420
487
  expect(sentCheckpoints).toBe(2);
421
488
  expect(sentRows).toBe(10002);
422
489
  });
423
- test('sync interrupts low-priority buckets on new checkpoints (2)', async () => {
490
+ test('restarts updated low-priority buckets at a new checkpoint', async () => {
424
491
  await using f = await factory();
425
- // bucket0a -> send all data
426
- // then interrupt checkpoint with new data for all buckets
427
- // -> data for all buckets should be sent in the new checkpoint
428
492
  const syncRules = await updateSyncRules(f, {
429
493
  content: `
430
494
  bucket_definitions:
431
495
  b0a:
432
496
  priority: 2
433
497
  data:
434
- - SELECT * FROM test WHERE LENGTH(id) <= 5;
498
+ - SELECT * FROM test WHERE substring(id, 1, 6) = 'first-';
435
499
  b0b:
436
- priority: 2
500
+ priority: 3
437
501
  data:
438
- - SELECT * FROM test WHERE LENGTH(id) <= 5;
502
+ - SELECT * FROM test WHERE substring(id, 1, 4) = 'low-';
439
503
  b1:
440
504
  priority: 1
441
505
  data:
442
- - SELECT * FROM test WHERE LENGTH(id) > 5;
506
+ - SELECT * FROM test WHERE substring(id, 1, 8) = 'highprio';
443
507
  `
444
508
  });
445
509
  const bucketStorage = f.getInstance(syncRules);
446
510
  await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
447
511
  const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
512
+ const syncRulesContent = syncRules.syncConfigContent[0];
513
+ const b0aBucket = test_utils.bucketRequest(syncRulesContent, 'b0a[]').bucket;
514
+ const b0bBucket = test_utils.bucketRequest(syncRulesContent, 'b0b[]').bucket;
515
+ const b1Bucket = test_utils.bucketRequest(syncRulesContent, 'b1[]').bucket;
448
516
  await writer.markAllSnapshotDone('0/1');
449
- // Initial data: Add one priority row and 10k low-priority rows.
450
517
  await writer.save({
451
518
  sourceTable: testTable,
452
519
  tag: storage.SaveOperationTag.INSERT,
@@ -456,15 +523,26 @@ bucket_definitions:
456
523
  },
457
524
  afterReplicaId: 'highprio'
458
525
  });
526
+ for (let i = 0; i < 999; i++) {
527
+ await writer.save({
528
+ sourceTable: testTable,
529
+ tag: storage.SaveOperationTag.INSERT,
530
+ after: {
531
+ id: `first-${i}`,
532
+ description: 'first low-priority bucket'
533
+ },
534
+ afterReplicaId: `first-${i}`
535
+ });
536
+ }
459
537
  for (let i = 0; i < 2_000; i++) {
460
538
  await writer.save({
461
539
  sourceTable: testTable,
462
540
  tag: storage.SaveOperationTag.INSERT,
463
541
  after: {
464
- id: `${i}`,
542
+ id: `low-${i}`,
465
543
  description: 'low prio'
466
544
  },
467
- afterReplicaId: `${i}`
545
+ afterReplicaId: `low-${i}`
468
546
  });
469
547
  }
470
548
  await writer.commit('0/1');
@@ -481,25 +559,24 @@ bucket_definitions:
481
559
  token: new JwtPayload({ sub: '', exp: Date.now() / 1000 + 10 }),
482
560
  isEncodingAsBson: false
483
561
  });
484
- let sentRows = 0;
485
- let lines = [];
562
+ const dataLines = [];
563
+ const partialCheckpoints = [];
564
+ const checkpoints = [];
565
+ const checkpointCompletes = [];
566
+ let afterCheckpointDiff = false;
567
+ let interruptionTriggered = false;
486
568
  for await (let next of stream) {
487
569
  if (typeof next == 'string') {
488
570
  next = JSON.parse(next);
489
571
  }
490
572
  if (typeof next === 'object' && next !== null) {
491
573
  if ('partial_checkpoint_complete' in next) {
492
- lines.push(next);
493
- }
494
- if ('checkpoint' in next || 'checkpoint_diff' in next) {
495
- lines.push(next);
496
- }
497
- if ('data' in next) {
498
- lines.push({ data: { ...next.data, data: undefined } });
499
- sentRows += next.data.data.length;
500
- if (sentRows == 1001) {
501
- // Save new data to interrupt the low-priority sync.
502
- // Add another high-priority row. This should interrupt the long-running low-priority sync.
574
+ partialCheckpoints.push({
575
+ priority: next.partial_checkpoint_complete.priority,
576
+ afterCheckpointDiff
577
+ });
578
+ if (next.partial_checkpoint_complete.priority == 2 && !interruptionTriggered) {
579
+ interruptionTriggered = true;
503
580
  await writer.save({
504
581
  sourceTable: testTable,
505
582
  tag: storage.SaveOperationTag.INSERT,
@@ -509,48 +586,79 @@ bucket_definitions:
509
586
  },
510
587
  afterReplicaId: 'highprio2'
511
588
  });
512
- // Also add a low-priority row
513
589
  await writer.save({
514
590
  sourceTable: testTable,
515
591
  tag: storage.SaveOperationTag.INSERT,
516
592
  after: {
517
- id: '2001',
593
+ id: 'low-2000',
518
594
  description: 'Another low-priority row'
519
595
  },
520
- afterReplicaId: '2001'
596
+ afterReplicaId: 'low-2000'
521
597
  });
522
598
  await writer.commit('0/2');
523
- }
524
- if (sentRows >= 1000 && sentRows <= 2001) {
525
- // pause for a bit to give the stream time to process interruptions.
526
- // This covers the data batch above and the next one.
599
+ // Let the checkpoint watcher observe the update before requesting the next priority.
527
600
  await timers.setTimeout(50);
528
601
  }
529
602
  }
603
+ if ('checkpoint' in next || 'checkpoint_diff' in next) {
604
+ checkpoints.push(next);
605
+ afterCheckpointDiff = 'checkpoint_diff' in next;
606
+ }
607
+ if ('data' in next) {
608
+ dataLines.push({
609
+ bucket: next.data.bucket,
610
+ objectIds: next.data.data.flatMap((entry) => (entry.object_id == null ? [] : [entry.object_id])),
611
+ afterCheckpointDiff
612
+ });
613
+ }
530
614
  if ('checkpoint_complete' in next) {
531
- lines.push(next);
615
+ checkpointCompletes.push(next);
532
616
  break;
533
617
  }
534
618
  }
535
619
  }
536
- // Expected lines (full details in snapshot):
620
+ // Expected flow (data may be split into any number of chunks):
537
621
  //
538
- // checkpoint (4001)
539
- // data (b1[] 0 -> 1)
540
- // partial_checkpoint_complete (4001, priority 1)
541
- // data (b0a[], 0 -> 2000)
542
- // ## adds new data, interrupting the checkpoint
543
- // data (b0a[], 2000 -> 4000) # expected - stream is already busy with this by the time it receives the interruption
544
- // checkpoint_diff (4004)
545
- // data (b1[], 1 -> 4002)
546
- // partial_checkpoint_complete (4004, priority 1)
547
- // data (b0a[], 4000 -> 4003)
548
- // data (b0b[], 0 -> 1999)
549
- // data (b0b[], 1999 -> 3999)
550
- // data (b0b[], 3999 -> 4004)
551
- // checkpoint_complete (4004)
552
- expect(lines).toMatchSnapshot();
553
- expect(sentRows).toBe(4004);
622
+ // checkpoint
623
+ // data: b1 contains highprio
624
+ // partial_checkpoint_complete: priority 1
625
+ // data: b0a contains all 999 first-* rows (1,000 operations including highprio)
626
+ // partial_checkpoint_complete: priority 2
627
+ // ## add highprio2 and low-2000, interrupting before b0b starts
628
+ // checkpoint_diff
629
+ // data: b1 contains highprio2
630
+ // partial_checkpoint_complete: priority 1
631
+ // data: b0b contains all 2,001 low-* rows
632
+ // checkpoint_complete: only for the new checkpoint
633
+ expect(interruptionTriggered).toBe(true);
634
+ expect(checkpoints).toHaveLength(2);
635
+ expect(checkpoints[0]).toHaveProperty('checkpoint');
636
+ expect(checkpoints[1]).toHaveProperty('checkpoint_diff');
637
+ expect(partialCheckpoints).toEqual([
638
+ { priority: 1, afterCheckpointDiff: false },
639
+ { priority: 2, afterCheckpointDiff: false },
640
+ { priority: 1, afterCheckpointDiff: true }
641
+ ]);
642
+ expect(checkpointCompletes).toEqual([
643
+ {
644
+ checkpoint_complete: {
645
+ last_op_id: checkpoints[1].checkpoint_diff.last_op_id
646
+ }
647
+ }
648
+ ]);
649
+ const objectIdsFor = (bucket, afterDiff) => dataLines
650
+ .filter((line) => line.bucket == bucket && line.afterCheckpointDiff == afterDiff)
651
+ .flatMap((line) => line.objectIds);
652
+ expect(objectIdsFor(b1Bucket, false)).toEqual(['highprio']);
653
+ const b0aObjectIds = objectIdsFor(b0aBucket, false);
654
+ expect(new Set(b0aObjectIds)).toEqual(new Set(Array.from({ length: 999 }, (_, i) => `first-${i}`)));
655
+ expect(b0aObjectIds).toHaveLength(999);
656
+ expect(objectIdsFor(b0bBucket, false)).toEqual([]);
657
+ expect(objectIdsFor(b1Bucket, true)).toEqual(['highprio2']);
658
+ expect(objectIdsFor(b0aBucket, true)).toEqual([]);
659
+ const b0bObjectIds = objectIdsFor(b0bBucket, true);
660
+ expect(new Set(b0bObjectIds)).toEqual(new Set(Array.from({ length: 2_001 }, (_, i) => `low-${i}`)));
661
+ expect(b0bObjectIds).toHaveLength(2_001);
554
662
  });
555
663
  test('sends checkpoint complete line for empty checkpoint', async () => {
556
664
  await using f = await factory();
@@ -931,6 +1039,82 @@ bucket_definitions:
931
1039
  const expLines = await getCheckpointLines(iter);
932
1040
  expect(expLines).toMatchSnapshot();
933
1041
  });
1042
+ test('checksum invalidation skips the candidate checkpoint', async (context) => {
1043
+ await using f = await factory();
1044
+ const syncRules = await updateSyncRules(f, {
1045
+ content: BASIC_SYNC_RULES
1046
+ });
1047
+ const bucketStorage = await f.getInstance(syncRules);
1048
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
1049
+ const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
1050
+ await writer.markAllSnapshotDone('0/1');
1051
+ await writer.save({
1052
+ sourceTable: testTable,
1053
+ tag: storage.SaveOperationTag.INSERT,
1054
+ after: {
1055
+ id: 't1',
1056
+ description: 'Test 1'
1057
+ },
1058
+ afterReplicaId: 't1'
1059
+ });
1060
+ await writer.commit('0/1');
1061
+ let resolveInvalidatedChecksum;
1062
+ const invalidatedChecksum = new Promise((resolve) => {
1063
+ resolveInvalidatedChecksum = resolve;
1064
+ });
1065
+ const checksumSpy = vi.spyOn(bucketStorage, 'getChecksums').mockImplementationOnce(async (checkpoint, buckets) => {
1066
+ resolveInvalidatedChecksum();
1067
+ throw new storage.CheckpointChecksumInvalidatedError(checkpoint.checkpoint, buckets[0].bucket);
1068
+ });
1069
+ const stream = sync.streamResponse({
1070
+ syncContext,
1071
+ bucketStorage,
1072
+ syncRules: bucketStorage.getParsedSyncRules(test_utils.PARSE_OPTIONS),
1073
+ params: {
1074
+ buckets: [],
1075
+ include_checksum: true,
1076
+ raw_data: true
1077
+ },
1078
+ tracker,
1079
+ token: new JwtPayload({ sub: '', exp: Date.now() / 1000 + 10 }),
1080
+ isEncodingAsBson: false
1081
+ });
1082
+ const iter = stream[Symbol.asyncIterator]();
1083
+ context.onTestFinished(() => {
1084
+ iter.return?.();
1085
+ });
1086
+ const linesPromise = getCheckpointLines(iter, { consume: true });
1087
+ await invalidatedChecksum;
1088
+ // The first candidate was discarded before CheckpointLine.advance(). A later
1089
+ // checkpoint must therefore be calculated from the original connection state.
1090
+ await writer.save({
1091
+ sourceTable: testTable,
1092
+ tag: storage.SaveOperationTag.INSERT,
1093
+ after: {
1094
+ id: 't2',
1095
+ description: 'Test 2'
1096
+ },
1097
+ afterReplicaId: 't2'
1098
+ });
1099
+ await writer.commit('0/2');
1100
+ const lines = await linesPromise;
1101
+ expect(checksumSpy).toHaveBeenCalledTimes(2);
1102
+ expect(lines[0]).toEqual({
1103
+ checkpoint: expect.objectContaining({
1104
+ last_op_id: '2'
1105
+ })
1106
+ });
1107
+ expect(lines).not.toContainEqual({
1108
+ checkpoint: expect.objectContaining({
1109
+ last_op_id: '1'
1110
+ })
1111
+ });
1112
+ expect(lines.at(-1)).toEqual({
1113
+ checkpoint_complete: expect.objectContaining({
1114
+ last_op_id: '2'
1115
+ })
1116
+ });
1117
+ });
934
1118
  test('compacting data - invalidate checkpoint', async (context) => {
935
1119
  // This tests a case of a compact operation invalidating a checkpoint in the
936
1120
  // middle of syncing data.
@@ -1200,7 +1384,7 @@ bucket_definitions:
1200
1384
  user_id: 'test',
1201
1385
  heads: { '1': '1/0' }
1202
1386
  }
1203
- ])).get('test');
1387
+ ])).writeCheckpoints.get('test');
1204
1388
  const params = {
1205
1389
  syncContext,
1206
1390
  bucketStorage,