@ixo/sqlite-saver 1.0.4

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 (35) hide show
  1. package/.eslintrc.js +9 -0
  2. package/.prettierignore +3 -0
  3. package/.prettierrc.js +4 -0
  4. package/.turbo/turbo-build.log +4 -0
  5. package/CHANGELOG.md +25 -0
  6. package/README.md +0 -0
  7. package/dist/index.d.ts +38 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +567 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/migrations/001_add_created_at_to_messages.d.ts +8 -0
  12. package/dist/migrations/001_add_created_at_to_messages.d.ts.map +1 -0
  13. package/dist/migrations/001_add_created_at_to_messages.js +32 -0
  14. package/dist/migrations/001_add_created_at_to_messages.js.map +1 -0
  15. package/dist/tests/agent-with-checkpoiner.test.d.ts +2 -0
  16. package/dist/tests/agent-with-checkpoiner.test.d.ts.map +1 -0
  17. package/dist/tests/agent-with-checkpoiner.test.js +206 -0
  18. package/dist/tests/agent-with-checkpoiner.test.js.map +1 -0
  19. package/dist/tests/checkpointer.test.d.ts +2 -0
  20. package/dist/tests/checkpointer.test.d.ts.map +1 -0
  21. package/dist/tests/checkpointer.test.js +426 -0
  22. package/dist/tests/checkpointer.test.js.map +1 -0
  23. package/dist/utils.d.ts +15 -0
  24. package/dist/utils.d.ts.map +1 -0
  25. package/dist/utils.js +284 -0
  26. package/dist/utils.js.map +1 -0
  27. package/jest.config.js +6 -0
  28. package/package.json +41 -0
  29. package/src/index.ts +929 -0
  30. package/src/migrations/001_add_created_at_to_messages.ts +48 -0
  31. package/src/tests/agent-with-checkpoiner.test.ts +264 -0
  32. package/src/tests/checkpointer.test.ts +628 -0
  33. package/src/utils.ts +358 -0
  34. package/tsconfig.json +11 -0
  35. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,628 @@
1
+ import type { RunnableConfig } from '@langchain/core/runnables';
2
+ import {
3
+ Checkpoint,
4
+ CheckpointTuple,
5
+ emptyCheckpoint,
6
+ TASKS,
7
+ uuid6,
8
+ } from '@langchain/langgraph-checkpoint';
9
+ import { SqliteSaver } from '../index';
10
+
11
+ const checkpoint1: Checkpoint = {
12
+ v: 1,
13
+ id: uuid6(-1),
14
+ ts: '2024-04-19T17:19:07.952Z',
15
+ channel_values: {
16
+ someKey1: 'someValue1',
17
+ },
18
+ channel_versions: {
19
+ someKey2: 1,
20
+ },
21
+ versions_seen: {
22
+ someKey3: {
23
+ someKey4: 1,
24
+ },
25
+ },
26
+ };
27
+ const checkpoint2: Checkpoint = {
28
+ v: 1,
29
+ id: uuid6(1),
30
+ ts: '2024-04-20T17:19:07.952Z',
31
+ channel_values: {
32
+ someKey1: 'someValue2',
33
+ },
34
+ channel_versions: {
35
+ someKey2: 2,
36
+ },
37
+ versions_seen: {
38
+ someKey3: {
39
+ someKey4: 2,
40
+ },
41
+ },
42
+ };
43
+
44
+ describe('SqliteSaver', () => {
45
+ it('should save and retrieve checkpoints correctly', async () => {
46
+ const sqliteSaver = SqliteSaver.fromConnString(':memory:');
47
+
48
+ // get undefined checkpoint
49
+ const undefinedCheckpoint = await sqliteSaver.getTuple({
50
+ configurable: { thread_id: '1' },
51
+ });
52
+ expect(undefinedCheckpoint).toBeUndefined();
53
+
54
+ // save first checkpoint
55
+ const runnableConfig = await sqliteSaver.put(
56
+ { configurable: { thread_id: '1' } },
57
+ checkpoint1,
58
+ { source: 'update', step: -1, parents: {} },
59
+ );
60
+ expect(runnableConfig).toEqual({
61
+ configurable: {
62
+ thread_id: '1',
63
+ checkpoint_ns: '',
64
+ checkpoint_id: checkpoint1.id,
65
+ },
66
+ });
67
+
68
+ // add some writes
69
+ await sqliteSaver.putWrites(
70
+ {
71
+ configurable: {
72
+ checkpoint_id: checkpoint1.id,
73
+ checkpoint_ns: '',
74
+ thread_id: '1',
75
+ },
76
+ },
77
+ [['bar', 'baz']],
78
+ 'foo',
79
+ );
80
+
81
+ // get first checkpoint tuple
82
+ const firstCheckpointTuple = await sqliteSaver.getTuple({
83
+ configurable: { thread_id: '1' },
84
+ });
85
+ expect(firstCheckpointTuple?.config).toEqual({
86
+ configurable: {
87
+ thread_id: '1',
88
+ checkpoint_ns: '',
89
+ checkpoint_id: checkpoint1.id,
90
+ },
91
+ });
92
+ expect(firstCheckpointTuple?.checkpoint).toEqual(checkpoint1);
93
+ expect(firstCheckpointTuple?.parentConfig).toBeUndefined();
94
+ expect(firstCheckpointTuple?.pendingWrites).toEqual([
95
+ ['foo', 'bar', 'baz'],
96
+ ]);
97
+
98
+ // save second checkpoint
99
+ await sqliteSaver.put(
100
+ {
101
+ configurable: {
102
+ thread_id: '1',
103
+ checkpoint_id: '2024-04-18T17:19:07.952Z',
104
+ },
105
+ },
106
+ checkpoint2,
107
+ {
108
+ source: 'update',
109
+ step: -1,
110
+ parents: { '': checkpoint1.id },
111
+ },
112
+ );
113
+
114
+ // verify that parentTs is set and retrieved correctly for second checkpoint
115
+ const secondCheckpointTuple = await sqliteSaver.getTuple({
116
+ configurable: { thread_id: '1' },
117
+ });
118
+ expect(secondCheckpointTuple?.parentConfig).toEqual({
119
+ configurable: {
120
+ thread_id: '1',
121
+ checkpoint_ns: '',
122
+ checkpoint_id: '2024-04-18T17:19:07.952Z',
123
+ },
124
+ });
125
+
126
+ // list checkpoints
127
+ const checkpointTupleGenerator = await sqliteSaver.list(
128
+ {
129
+ configurable: { thread_id: '1' },
130
+ },
131
+ {
132
+ filter: {
133
+ source: 'update',
134
+ step: -1,
135
+ parents: { '': checkpoint1.id },
136
+ },
137
+ },
138
+ );
139
+ const checkpointTuples: CheckpointTuple[] = [];
140
+ for await (const checkpoint of checkpointTupleGenerator) {
141
+ checkpointTuples.push(checkpoint);
142
+ }
143
+ expect(checkpointTuples.length).toBe(1);
144
+
145
+ const checkpointTuple1 = checkpointTuples[0];
146
+ expect(checkpointTuple1?.checkpoint.ts).toBe('2024-04-20T17:19:07.952Z');
147
+ });
148
+
149
+ it('should delete thread', async () => {
150
+ const saver = SqliteSaver.fromConnString(':memory:');
151
+ await saver.put({ configurable: { thread_id: '1' } }, emptyCheckpoint(), {
152
+ source: 'update',
153
+ step: -1,
154
+ parents: {},
155
+ });
156
+
157
+ await saver.put({ configurable: { thread_id: '2' } }, emptyCheckpoint(), {
158
+ source: 'update',
159
+ step: -1,
160
+ parents: {},
161
+ });
162
+
163
+ await saver.deleteThread('1');
164
+
165
+ expect(
166
+ await saver.getTuple({ configurable: { thread_id: '1' } }),
167
+ ).toBeUndefined();
168
+
169
+ expect(
170
+ await saver.getTuple({ configurable: { thread_id: '2' } }),
171
+ ).toBeDefined();
172
+ });
173
+
174
+ it('pending sends migration', async () => {
175
+ const saver = SqliteSaver.fromConnString(':memory:');
176
+
177
+ let config: RunnableConfig = {
178
+ configurable: { thread_id: 'thread-1', checkpoint_ns: '' },
179
+ };
180
+
181
+ const checkpoint0 = emptyCheckpoint();
182
+
183
+ config = await saver.put(config, checkpoint0, {
184
+ source: 'loop',
185
+ parents: {},
186
+ step: 0,
187
+ });
188
+
189
+ await saver.putWrites(
190
+ config,
191
+ [
192
+ [TASKS, 'send-1'],
193
+ [TASKS, 'send-2'],
194
+ ],
195
+ 'task-1',
196
+ );
197
+ await saver.putWrites(config, [[TASKS, 'send-3']], 'task-2');
198
+
199
+ // check that fetching checkpount 0 doesn't attach pending sends
200
+ // (they should be attached to the next checkpoint)
201
+ const tuple0 = await saver.getTuple(config);
202
+ expect(tuple0?.checkpoint.channel_values).toEqual({});
203
+ expect(tuple0?.checkpoint.channel_versions).toEqual({});
204
+
205
+ // create second checkpoint
206
+ const checkpoint1: Checkpoint = {
207
+ v: 1,
208
+ id: uuid6(1),
209
+ ts: '2024-04-20T17:19:07.952Z',
210
+ channel_values: {},
211
+ channel_versions: checkpoint0.channel_versions,
212
+ versions_seen: checkpoint0.versions_seen,
213
+ };
214
+ config = await saver.put(config, checkpoint1, {
215
+ source: 'loop',
216
+ parents: {},
217
+ step: 1,
218
+ });
219
+
220
+ // check that pending sends are attached to checkpoint1
221
+ const checkpoint1Tuple = await saver.getTuple(config);
222
+ expect(checkpoint1Tuple?.checkpoint.channel_values).toEqual({
223
+ [TASKS]: ['send-1', 'send-2', 'send-3'],
224
+ });
225
+ expect(checkpoint1Tuple?.checkpoint.channel_versions[TASKS]).toBeDefined();
226
+
227
+ // check that the list also applies the migration
228
+ const checkpointTupleGenerator = saver.list({
229
+ configurable: { thread_id: 'thread-1' },
230
+ });
231
+
232
+ const checkpointTuples: CheckpointTuple[] = [];
233
+ for await (const checkpoint of checkpointTupleGenerator) {
234
+ checkpointTuples.push(checkpoint);
235
+ }
236
+ expect(checkpointTuples.length).toBe(2);
237
+ expect(checkpointTuples[0]?.checkpoint.channel_values).toEqual({
238
+ [TASKS]: ['send-1', 'send-2', 'send-3'],
239
+ });
240
+ expect(
241
+ checkpointTuples[0]?.checkpoint.channel_versions[TASKS],
242
+ ).toBeDefined();
243
+ });
244
+
245
+ it('should filter list by checkpoint_ns', async () => {
246
+ const saver = SqliteSaver.fromConnString(':memory:');
247
+
248
+ // Create checkpoints in different namespaces
249
+ const checkpoint1 = emptyCheckpoint();
250
+ const checkpoint2 = emptyCheckpoint();
251
+ const checkpoint3 = emptyCheckpoint();
252
+
253
+ await saver.put(
254
+ { configurable: { thread_id: 'thread-1', checkpoint_ns: 'ns1' } },
255
+ checkpoint1,
256
+ { source: 'update', step: -1, parents: {} },
257
+ );
258
+
259
+ await saver.put(
260
+ { configurable: { thread_id: 'thread-1', checkpoint_ns: 'ns2' } },
261
+ checkpoint2,
262
+ { source: 'update', step: -1, parents: {} },
263
+ );
264
+
265
+ await saver.put(
266
+ { configurable: { thread_id: 'thread-1', checkpoint_ns: 'ns1' } },
267
+ checkpoint3,
268
+ { source: 'update', step: -1, parents: {} },
269
+ );
270
+
271
+ // List checkpoints in ns1
272
+ const ns1Generator = saver.list({
273
+ configurable: { thread_id: 'thread-1', checkpoint_ns: 'ns1' },
274
+ });
275
+
276
+ const ns1Tuples: CheckpointTuple[] = [];
277
+ for await (const checkpoint of ns1Generator) {
278
+ ns1Tuples.push(checkpoint);
279
+ }
280
+ expect(ns1Tuples.length).toBe(2);
281
+ expect(
282
+ ns1Tuples.every((t) => t.config.configurable?.checkpoint_ns === 'ns1'),
283
+ ).toBe(true);
284
+
285
+ // List checkpoints in ns2
286
+ const ns2Generator = saver.list({
287
+ configurable: { thread_id: 'thread-1', checkpoint_ns: 'ns2' },
288
+ });
289
+
290
+ const ns2Tuples: CheckpointTuple[] = [];
291
+ for await (const checkpoint of ns2Generator) {
292
+ ns2Tuples.push(checkpoint);
293
+ }
294
+ expect(ns2Tuples.length).toBe(1);
295
+ expect(ns2Tuples[0]?.config.configurable?.checkpoint_ns).toBe('ns2');
296
+ });
297
+
298
+ it('should paginate list with before cursor', async () => {
299
+ const saver = SqliteSaver.fromConnString(':memory:');
300
+
301
+ // Create multiple checkpoints with sequential timestamps
302
+ const checkpoint1: Checkpoint = {
303
+ v: 1,
304
+ id: uuid6(-2),
305
+ ts: '2024-04-19T17:19:07.952Z',
306
+ channel_values: {},
307
+ channel_versions: {},
308
+ versions_seen: {},
309
+ };
310
+ const checkpoint2: Checkpoint = {
311
+ v: 1,
312
+ id: uuid6(-1),
313
+ ts: '2024-04-20T17:19:07.952Z',
314
+ channel_values: {},
315
+ channel_versions: {},
316
+ versions_seen: {},
317
+ };
318
+ const checkpoint3: Checkpoint = {
319
+ v: 1,
320
+ id: uuid6(0),
321
+ ts: '2024-04-21T17:19:07.952Z',
322
+ channel_values: {},
323
+ channel_versions: {},
324
+ versions_seen: {},
325
+ };
326
+
327
+ await saver.put({ configurable: { thread_id: 'thread-1' } }, checkpoint1, {
328
+ source: 'update',
329
+ step: -1,
330
+ parents: {},
331
+ });
332
+ await saver.put({ configurable: { thread_id: 'thread-1' } }, checkpoint2, {
333
+ source: 'update',
334
+ step: -1,
335
+ parents: {},
336
+ });
337
+ const config3 = await saver.put(
338
+ { configurable: { thread_id: 'thread-1' } },
339
+ checkpoint3,
340
+ { source: 'update', step: -1, parents: {} },
341
+ );
342
+
343
+ // List all checkpoints
344
+ const allGenerator = saver.list({
345
+ configurable: { thread_id: 'thread-1' },
346
+ });
347
+ const allTuples: CheckpointTuple[] = [];
348
+ for await (const checkpoint of allGenerator) {
349
+ allTuples.push(checkpoint);
350
+ }
351
+ expect(allTuples.length).toBe(3);
352
+
353
+ // List with before cursor (should return checkpoints before checkpoint3)
354
+ const beforeGenerator = saver.list(
355
+ {
356
+ configurable: { thread_id: 'thread-1' },
357
+ },
358
+ {
359
+ before: {
360
+ configurable: {
361
+ checkpoint_id: config3.configurable?.checkpoint_id,
362
+ },
363
+ },
364
+ },
365
+ );
366
+
367
+ const beforeTuples: CheckpointTuple[] = [];
368
+ for await (const checkpoint of beforeGenerator) {
369
+ beforeTuples.push(checkpoint);
370
+ }
371
+ expect(beforeTuples.length).toBe(2);
372
+ expect(
373
+ beforeTuples.every(
374
+ (t) =>
375
+ t.config.configurable?.checkpoint_id !==
376
+ config3.configurable?.checkpoint_id,
377
+ ),
378
+ ).toBe(true);
379
+ });
380
+
381
+ it('should limit list results', async () => {
382
+ const saver = SqliteSaver.fromConnString(':memory:');
383
+
384
+ // Create 5 checkpoints
385
+ for (let i = 0; i < 5; i++) {
386
+ const checkpoint: Checkpoint = {
387
+ v: 1,
388
+ id: uuid6(i - 3),
389
+ ts: `2024-04-${19 + i}T17:19:07.952Z`,
390
+ channel_values: {},
391
+ channel_versions: {},
392
+ versions_seen: {},
393
+ };
394
+ await saver.put({ configurable: { thread_id: 'thread-1' } }, checkpoint, {
395
+ source: 'update',
396
+ step: -1,
397
+ parents: {},
398
+ });
399
+ }
400
+
401
+ // List without limit
402
+ const allGenerator = saver.list({
403
+ configurable: { thread_id: 'thread-1' },
404
+ });
405
+ const allTuples: CheckpointTuple[] = [];
406
+ for await (const checkpoint of allGenerator) {
407
+ allTuples.push(checkpoint);
408
+ }
409
+ expect(allTuples.length).toBe(5);
410
+
411
+ // List with limit of 2
412
+ const limitedGenerator = saver.list(
413
+ {
414
+ configurable: { thread_id: 'thread-1' },
415
+ },
416
+ { limit: 2 },
417
+ );
418
+
419
+ const limitedTuples: CheckpointTuple[] = [];
420
+ for await (const checkpoint of limitedGenerator) {
421
+ limitedTuples.push(checkpoint);
422
+ }
423
+ expect(limitedTuples.length).toBe(2);
424
+ });
425
+
426
+ it('should filter list by metadata fields', async () => {
427
+ const saver = SqliteSaver.fromConnString(':memory:');
428
+
429
+ const checkpoint1 = emptyCheckpoint();
430
+ const checkpoint2 = emptyCheckpoint();
431
+ const checkpoint3 = emptyCheckpoint();
432
+
433
+ await saver.put({ configurable: { thread_id: 'thread-1' } }, checkpoint1, {
434
+ source: 'update',
435
+ step: 1,
436
+ parents: {},
437
+ });
438
+
439
+ await saver.put({ configurable: { thread_id: 'thread-1' } }, checkpoint2, {
440
+ source: 'loop',
441
+ step: 2,
442
+ parents: {},
443
+ });
444
+
445
+ await saver.put({ configurable: { thread_id: 'thread-1' } }, checkpoint3, {
446
+ source: 'update',
447
+ step: 3,
448
+ parents: {},
449
+ });
450
+
451
+ // Filter by source
452
+ const updateGenerator = saver.list(
453
+ {
454
+ configurable: { thread_id: 'thread-1' },
455
+ },
456
+ {
457
+ filter: { source: 'update' },
458
+ },
459
+ );
460
+
461
+ const updateTuples: CheckpointTuple[] = [];
462
+ for await (const checkpoint of updateGenerator) {
463
+ updateTuples.push(checkpoint);
464
+ }
465
+ expect(updateTuples.length).toBe(2);
466
+ expect(updateTuples.every((t) => t.metadata?.source === 'update')).toBe(
467
+ true,
468
+ );
469
+
470
+ // Filter by step
471
+ const stepGenerator = saver.list(
472
+ {
473
+ configurable: { thread_id: 'thread-1' },
474
+ },
475
+ {
476
+ filter: { step: 2 },
477
+ },
478
+ );
479
+
480
+ const stepTuples: CheckpointTuple[] = [];
481
+ for await (const checkpoint of stepGenerator) {
482
+ stepTuples.push(checkpoint);
483
+ }
484
+ expect(stepTuples.length).toBe(1);
485
+ expect(stepTuples[0]?.metadata?.step).toBe(2);
486
+ });
487
+
488
+ it('should throw error when put() is called without config.configurable', async () => {
489
+ const saver = SqliteSaver.fromConnString(':memory:');
490
+ const checkpoint = emptyCheckpoint();
491
+
492
+ await expect(
493
+ saver.put({} as RunnableConfig, checkpoint, {
494
+ source: 'update',
495
+ step: -1,
496
+ parents: {},
497
+ }),
498
+ ).rejects.toThrow('Empty configuration supplied.');
499
+ });
500
+
501
+ it('should throw error when put() is called without thread_id', async () => {
502
+ const saver = SqliteSaver.fromConnString(':memory:');
503
+ const checkpoint = emptyCheckpoint();
504
+
505
+ await expect(
506
+ saver.put({ configurable: {} } as RunnableConfig, checkpoint, {
507
+ source: 'update',
508
+ step: -1,
509
+ parents: {},
510
+ }),
511
+ ).rejects.toThrow(
512
+ 'Missing "thread_id" field in passed "config.configurable".',
513
+ );
514
+ });
515
+
516
+ it('should throw error when putWrites() is called without config.configurable', async () => {
517
+ const saver = SqliteSaver.fromConnString(':memory:');
518
+
519
+ await expect(
520
+ saver.putWrites({} as RunnableConfig, [['channel', 'value']], 'task-1'),
521
+ ).rejects.toThrow('Empty configuration supplied.');
522
+ });
523
+
524
+ it('should throw error when putWrites() is called without thread_id', async () => {
525
+ const saver = SqliteSaver.fromConnString(':memory:');
526
+
527
+ await expect(
528
+ saver.putWrites(
529
+ { configurable: {} } as RunnableConfig,
530
+ [['channel', 'value']],
531
+ 'task-1',
532
+ ),
533
+ ).rejects.toThrow('Missing thread_id field in config.configurable.');
534
+ });
535
+
536
+ it('should throw error when putWrites() is called without checkpoint_id', async () => {
537
+ const saver = SqliteSaver.fromConnString(':memory:');
538
+
539
+ await expect(
540
+ saver.putWrites(
541
+ { configurable: { thread_id: 'thread-1' } } as RunnableConfig,
542
+ [['channel', 'value']],
543
+ 'task-1',
544
+ ),
545
+ ).rejects.toThrow('Missing checkpoint_id field in config.configurable.');
546
+ });
547
+
548
+ it('should migrate pending sends for v<4 checkpoints in list()', async () => {
549
+ const saver = SqliteSaver.fromConnString(':memory:');
550
+
551
+ // Create v<4 checkpoint (v: 1)
552
+ const checkpoint0: Checkpoint = {
553
+ v: 1,
554
+ id: uuid6(-1),
555
+ ts: '2024-04-19T17:19:07.952Z',
556
+ channel_values: {},
557
+ channel_versions: {},
558
+ versions_seen: {},
559
+ };
560
+
561
+ let config = await saver.put(
562
+ { configurable: { thread_id: 'thread-1' } },
563
+ checkpoint0,
564
+ {
565
+ source: 'loop',
566
+ parents: {},
567
+ step: 0,
568
+ },
569
+ );
570
+
571
+ // Add pending sends to checkpoint0
572
+ await saver.putWrites(
573
+ config,
574
+ [
575
+ [TASKS, 'send-1'],
576
+ [TASKS, 'send-2'],
577
+ ],
578
+ 'task-1',
579
+ );
580
+
581
+ // Create v<4 child checkpoint that should inherit pending sends
582
+ const checkpoint1: Checkpoint = {
583
+ v: 1,
584
+ id: uuid6(0),
585
+ ts: '2024-04-20T17:19:07.952Z',
586
+ channel_values: {},
587
+ channel_versions: {},
588
+ versions_seen: {},
589
+ };
590
+
591
+ config = await saver.put(
592
+ {
593
+ configurable: {
594
+ thread_id: 'thread-1',
595
+ checkpoint_id: checkpoint0.id,
596
+ },
597
+ },
598
+ checkpoint1,
599
+ {
600
+ source: 'loop',
601
+ parents: { '': checkpoint0.id },
602
+ step: 1,
603
+ },
604
+ );
605
+
606
+ // List should migrate pending sends for v<4 checkpoint
607
+ const listGenerator = saver.list({
608
+ configurable: { thread_id: 'thread-1' },
609
+ });
610
+
611
+ const listTuples: CheckpointTuple[] = [];
612
+ for await (const checkpoint of listGenerator) {
613
+ listTuples.push(checkpoint);
614
+ }
615
+
616
+ // Find checkpoint1 in the list
617
+ const checkpoint1Tuple = listTuples.find(
618
+ (t) => t.config.configurable?.checkpoint_id === checkpoint1.id,
619
+ );
620
+
621
+ expect(checkpoint1Tuple).toBeDefined();
622
+ expect(checkpoint1Tuple?.checkpoint.channel_values[TASKS]).toEqual([
623
+ 'send-1',
624
+ 'send-2',
625
+ ]);
626
+ expect(checkpoint1Tuple?.checkpoint.channel_versions[TASKS]).toBeDefined();
627
+ });
628
+ });