@mastra/lance 0.2.0 → 0.2.1-alpha.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.
@@ -1,1336 +1,10 @@
1
- import { randomUUID } from 'crypto';
2
- import { checkWorkflowSnapshot } from '@internal/storage-test-utils';
3
- import type { EvalRow, MastraMessageV2, StorageThreadType, TraceType, WorkflowRunState } from '@mastra/core';
4
- import {
5
- TABLE_EVALS,
6
- TABLE_MESSAGES,
7
- TABLE_SCHEMAS,
8
- TABLE_THREADS,
9
- TABLE_TRACES,
10
- TABLE_WORKFLOW_SNAPSHOT,
11
- } from '@mastra/core/storage';
12
- import type { StorageColumn, TABLE_NAMES } from '@mastra/core/storage';
13
- import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest';
1
+ import { createTestSuite } from '@internal/storage-test-utils';
2
+ import { vi } from 'vitest';
14
3
  import { LanceStorage } from './index';
15
4
 
16
- /**
17
- * Represents a message record in the storage system
18
- */
19
- interface MessageRecord {
20
- id: number;
21
- threadId: string;
22
- referenceId: number;
23
- messageType: string;
24
- content: string;
25
- createdAt: Date;
26
- metadata: Record<string, unknown>;
27
- }
5
+ // Increase timeout for all tests in this file to 30 seconds
6
+ vi.setConfig({ testTimeout: 200_000, hookTimeout: 200_000 });
28
7
 
29
- /**
30
- * Generates an array of random records for testing purposes
31
- * @param count - Number of records to generate
32
- * @returns Array of message records with random values
33
- */
34
- function generateRecords(count: number): MessageRecord[] {
35
- return Array.from({ length: count }, (_, index) => ({
36
- id: index + 1,
37
- threadId: `12333d567-e89b-12d3-a456-${(426614174000 + index).toString()}`,
38
- referenceId: index + 1,
39
- messageType: 'text',
40
- content: `Test message ${index + 1}`,
41
- createdAt: new Date(),
42
- metadata: { testIndex: index, foo: 'bar' },
43
- }));
44
- }
8
+ const storage = await LanceStorage.create('test', 'lancedb-storage');
45
9
 
46
- function generateMessageRecords(count: number, threadId?: string): MastraMessageV2[] {
47
- return Array.from({ length: count }, (_, index) => ({
48
- id: (index + 1).toString(),
49
- content: { format: 2, parts: [{ type: 'text', text: `Test message ${index + 1}` }] },
50
- role: 'user',
51
- createdAt: new Date(),
52
- threadId: threadId ?? `12333d567-e89b-12d3-a456-${(426614174000 + index).toString()}`,
53
- resourceId: `12333d567-e89b-12d3-a456-${(426614174000 + index).toString()}`,
54
- toolCallIds: [],
55
- toolCallArgs: [],
56
- toolNames: [],
57
- type: 'v2',
58
- }));
59
- }
60
-
61
- function generateTraceRecords(count: number): TraceType[] {
62
- return Array.from({ length: count }, (_, index) => ({
63
- id: (index + 1).toString(),
64
- name: `Test trace ${index + 1}`,
65
- scope: 'test',
66
- kind: 0,
67
- parentSpanId: `12333d567-e89b-12d3-a456-${(426614174000 + index).toString()}`,
68
- traceId: `12333d567-e89b-12d3-a456-${(426614174000 + index).toString()}`,
69
- attributes: { attribute1: 'value1' },
70
- status: { code: 0, description: 'OK' },
71
- events: { event1: 'value1' },
72
- links: { link1: 'value1' },
73
- other: { other1: 'value1' },
74
- startTime: new Date().getTime(),
75
- endTime: new Date().getTime(),
76
- createdAt: new Date(),
77
- }));
78
- }
79
-
80
- function generateEvalRecords(count: number): EvalRow[] {
81
- return Array.from({ length: count }, (_, index) => ({
82
- input: `Test input ${index + 1}`,
83
- output: `Test output ${index + 1}`,
84
- result: { score: index + 1, info: { testIndex: index + 1 } },
85
- agentName: `Test agent ${index + 1}`,
86
- metricName: `Test metric ${index + 1}`,
87
- instructions: 'Test instructions',
88
- testInfo: { testName: `Test ${index + 1}`, testPath: `TestPath ${index + 1}` },
89
- runId: `12333d567-e89b-12d3-a456-${(426614174000 + index).toString()}`,
90
- globalRunId: `12333d567-e89b-12d3-a456-${(426614174000 + index).toString()}`,
91
- createdAt: new Date().toString(),
92
- }));
93
- }
94
-
95
- const generateWorkflowSnapshot = (status: WorkflowRunState['context']['steps']['status'], createdAt?: Date) => {
96
- const runId = `run-${randomUUID()}`;
97
- const stepId = `step-${randomUUID()}`;
98
- const timestamp = createdAt || new Date();
99
- const snapshot = {
100
- result: { success: true },
101
- value: {},
102
- context: {
103
- [stepId]: {
104
- status,
105
- payload: {},
106
- error: undefined,
107
- startedAt: timestamp.getTime(),
108
- endedAt: new Date(timestamp.getTime() + 15000).getTime(),
109
- },
110
- input: {},
111
- },
112
- serializedStepGraph: [],
113
- activePaths: [],
114
- suspendedPaths: {},
115
- runId,
116
- timestamp: timestamp.getTime(),
117
- status,
118
- } as unknown as WorkflowRunState;
119
- return { snapshot, runId, stepId };
120
- };
121
-
122
- describe('LanceStorage tests', async () => {
123
- let storage!: LanceStorage;
124
-
125
- beforeAll(async () => {
126
- storage = await LanceStorage.create('test', 'lancedb-storage');
127
- });
128
-
129
- it('should create a new instance of LanceStorage', async () => {
130
- const storage = await LanceStorage.create('test', 'lancedb-storage');
131
- expect(storage).toBeInstanceOf(LanceStorage);
132
- expect(storage.name).toBe('test');
133
- });
134
-
135
- describe('Create table', () => {
136
- beforeAll(async () => {
137
- // Clean up any existing tables
138
- try {
139
- await storage.dropTable(TABLE_MESSAGES);
140
- } catch {
141
- // Ignore if table doesn't exist
142
- }
143
- });
144
-
145
- afterAll(async () => {
146
- await storage.dropTable(TABLE_MESSAGES);
147
- });
148
-
149
- it('should create an empty table with given schema', async () => {
150
- const schema: Record<string, StorageColumn> = {
151
- id: { type: 'integer', nullable: false },
152
- threadId: { type: 'uuid', nullable: false },
153
- referenceId: { type: 'bigint', nullable: true },
154
- messageType: { type: 'text', nullable: true },
155
- content: { type: 'text', nullable: true },
156
- createdAt: { type: 'timestamp', nullable: true },
157
- metadata: { type: 'jsonb', nullable: true },
158
- };
159
-
160
- await storage.createTable({ tableName: TABLE_MESSAGES, schema });
161
-
162
- // Verify table exists and schema is correct
163
- const table = await storage.getTableSchema(TABLE_MESSAGES);
164
-
165
- expect(table.fields.length).toBe(7);
166
- expect(table.names).toEqual(
167
- expect.arrayContaining(['id', 'threadId', 'referenceId', 'messageType', 'content', 'createdAt', 'metadata']),
168
- );
169
- // check the types of the fields
170
- expect(table.fields[0].type.toString().toLowerCase()).toBe('int32');
171
- expect(table.fields[1].type.toString().toLowerCase()).toBe('utf8');
172
- expect(table.fields[2].type.toString().toLowerCase()).toBe('float64');
173
- expect(table.fields[3].type.toString().toLowerCase()).toBe('utf8');
174
- expect(table.fields[4].type.toString().toLowerCase()).toBe('utf8');
175
- expect(table.fields[5].type.toString().toLowerCase()).toBe('float64');
176
- expect(table.fields[6].type.toString().toLowerCase()).toBe('utf8');
177
- });
178
- });
179
-
180
- describe('Insert data', () => {
181
- beforeAll(async () => {
182
- const schema: Record<string, StorageColumn> = {
183
- id: { type: 'integer', nullable: false },
184
- threadId: { type: 'uuid', nullable: false },
185
- referenceId: { type: 'bigint', nullable: true },
186
- messageType: { type: 'text', nullable: true },
187
- content: { type: 'text', nullable: true },
188
- createdAt: { type: 'timestamp', nullable: true },
189
- metadata: { type: 'jsonb', nullable: true },
190
- };
191
-
192
- await storage.createTable({ tableName: TABLE_MESSAGES, schema });
193
- });
194
-
195
- afterAll(async () => {
196
- await storage.dropTable(TABLE_MESSAGES);
197
- });
198
-
199
- it('should insert a single record without throwing exceptions', async () => {
200
- const record = {
201
- id: 1,
202
- threadId: '123e4567-e89b-12d3-a456-426614174000',
203
- referenceId: 1,
204
- messageType: 'text',
205
- content: 'Hello, world!',
206
- createdAt: new Date(),
207
- metadata: { foo: 'bar' },
208
- };
209
-
210
- await storage.insert({ tableName: TABLE_MESSAGES, record });
211
-
212
- // Verify the record was inserted
213
- const loadedRecord = await storage.load({ tableName: TABLE_MESSAGES, keys: { id: 1 } });
214
-
215
- // Custom comparison to handle date precision differences
216
- expect(loadedRecord.id).toEqual(record.id);
217
- expect(loadedRecord.threadId).toEqual(record.threadId);
218
- expect(loadedRecord.referenceId).toEqual(record.referenceId);
219
- expect(loadedRecord.messageType).toEqual(record.messageType);
220
- expect(loadedRecord.content).toEqual(record.content);
221
- expect(loadedRecord.metadata).toEqual(record.metadata);
222
-
223
- // Compare dates ignoring millisecond precision
224
- const loadedDate = new Date(loadedRecord.createdAt);
225
- const originalDate = new Date(record.createdAt);
226
- expect(loadedDate.getFullYear()).toEqual(originalDate.getFullYear());
227
- expect(loadedDate.getMonth()).toEqual(originalDate.getMonth());
228
- expect(loadedDate.getDate()).toEqual(originalDate.getDate());
229
- expect(loadedDate.getHours()).toEqual(originalDate.getHours());
230
- expect(loadedDate.getMinutes()).toEqual(originalDate.getMinutes());
231
- expect(loadedDate.getSeconds()).toEqual(originalDate.getSeconds());
232
- });
233
-
234
- it('should throw error when invalid key type is provided', async () => {
235
- await expect(storage.load({ tableName: TABLE_MESSAGES, keys: { id: '1' } })).rejects.toThrowError(
236
- /Expected numeric value for field 'id', got string/,
237
- );
238
- });
239
-
240
- it('should insert batch records without throwing exceptions', async () => {
241
- const recordCount = 100;
242
- const records: MessageRecord[] = generateRecords(recordCount);
243
-
244
- await storage.batchInsert({ tableName: TABLE_MESSAGES, records });
245
-
246
- // Verify records were inserted
247
- const loadedRecords = await storage.load({ tableName: TABLE_MESSAGES, keys: { id: 1 } });
248
- expect(loadedRecords).not.toBeNull();
249
- expect(loadedRecords.id).toEqual(records[0].id);
250
- expect(loadedRecords.threadId).toEqual(records[0].threadId);
251
- expect(loadedRecords.referenceId).toEqual(records[0].referenceId);
252
- expect(loadedRecords.messageType).toEqual(records[0].messageType);
253
- expect(loadedRecords.content).toEqual(records[0].content);
254
- expect(new Date(loadedRecords.createdAt)).toEqual(new Date(records[0].createdAt));
255
- expect(loadedRecords.metadata).toEqual(records[0].metadata);
256
-
257
- // Verify the last record
258
- const lastRecord = await storage.load({ tableName: TABLE_MESSAGES, keys: { id: recordCount } });
259
- expect(lastRecord).not.toBeNull();
260
- expect(lastRecord.id).toEqual(records[recordCount - 1].id);
261
- expect(lastRecord.threadId).toEqual(records[recordCount - 1].threadId);
262
- expect(lastRecord.referenceId).toEqual(records[recordCount - 1].referenceId);
263
- expect(lastRecord.messageType).toEqual(records[recordCount - 1].messageType);
264
- expect(lastRecord.content).toEqual(records[recordCount - 1].content);
265
- expect(new Date(lastRecord.createdAt)).toEqual(new Date(records[recordCount - 1].createdAt));
266
- expect(lastRecord.metadata).toEqual(records[recordCount - 1].metadata);
267
- });
268
- });
269
-
270
- describe('Query data', () => {
271
- beforeAll(async () => {
272
- const schema: Record<string, StorageColumn> = {
273
- id: { type: 'integer', nullable: false },
274
- threadId: { type: 'uuid', nullable: false },
275
- referenceId: { type: 'bigint', nullable: true },
276
- messageType: { type: 'text', nullable: true },
277
- content: { type: 'text', nullable: true },
278
- createdAt: { type: 'timestamp', nullable: true },
279
- metadata: { type: 'jsonb', nullable: true },
280
- };
281
-
282
- await storage.createTable({ tableName: TABLE_MESSAGES, schema });
283
- });
284
-
285
- afterAll(async () => {
286
- await storage.dropTable(TABLE_MESSAGES);
287
- });
288
-
289
- it('should query data by one key only', async () => {
290
- const record = {
291
- id: 1,
292
- threadId: '123e4567-e89b-12d3-a456-426614174000',
293
- referenceId: 1,
294
- messageType: 'text',
295
- content: 'Hello, world!',
296
- createdAt: new Date(),
297
- metadata: { foo: 'bar' },
298
- };
299
-
300
- await storage.insert({ tableName: TABLE_MESSAGES, record });
301
-
302
- const loadedRecord = await storage.load({ tableName: TABLE_MESSAGES, keys: { id: 1 } });
303
- expect(loadedRecord).not.toBeNull();
304
- expect(loadedRecord.id).toEqual(record.id);
305
- expect(loadedRecord.threadId).toEqual(record.threadId);
306
- expect(loadedRecord.referenceId).toEqual(record.referenceId);
307
- expect(loadedRecord.messageType).toEqual(record.messageType);
308
- expect(loadedRecord.content).toEqual(record.content);
309
- expect(new Date(loadedRecord.createdAt)).toEqual(new Date(record.createdAt));
310
- expect(loadedRecord.metadata).toEqual(record.metadata);
311
- });
312
-
313
- it('should query data by multiple keys', async () => {
314
- const record = {
315
- id: 1,
316
- threadId: '123e4567-e89b-12d3-a456-426614174000',
317
- referenceId: 1,
318
- messageType: 'hi',
319
- content: 'Hello, world!',
320
- createdAt: new Date(),
321
- metadata: { foo: 'bar' },
322
- };
323
-
324
- await storage.insert({ tableName: TABLE_MESSAGES, record });
325
-
326
- const loadedRecord = await storage.load({
327
- tableName: TABLE_MESSAGES,
328
- keys: { id: 1, messageType: 'hi' },
329
- });
330
-
331
- expect(loadedRecord).not.toBeNull();
332
- expect(loadedRecord.id).toEqual(record.id);
333
- expect(loadedRecord.threadId).toEqual(record.threadId);
334
- expect(loadedRecord.referenceId).toEqual(record.referenceId);
335
- expect(loadedRecord.messageType).toEqual(record.messageType);
336
- expect(loadedRecord.content).toEqual(record.content);
337
- expect(new Date(loadedRecord.createdAt)).toEqual(new Date(record.createdAt));
338
- expect(loadedRecord.metadata).toEqual(record.metadata);
339
-
340
- const recordsQueriedWithIdAndThreadId = await storage.load({
341
- tableName: TABLE_MESSAGES,
342
- keys: { id: 1, threadId: '123e4567-e89b-12d3-a456-426614174000' },
343
- });
344
-
345
- expect(recordsQueriedWithIdAndThreadId).not.toBeNull();
346
- expect(recordsQueriedWithIdAndThreadId.id).toEqual(record.id);
347
- expect(recordsQueriedWithIdAndThreadId.threadId).toEqual(record.threadId);
348
- expect(recordsQueriedWithIdAndThreadId.referenceId).toEqual(record.referenceId);
349
- expect(recordsQueriedWithIdAndThreadId.messageType).toEqual(record.messageType);
350
- expect(recordsQueriedWithIdAndThreadId.content).toEqual(record.content);
351
- expect(new Date(recordsQueriedWithIdAndThreadId.createdAt)).toEqual(new Date(record.createdAt));
352
- expect(recordsQueriedWithIdAndThreadId.metadata).toEqual(record.metadata);
353
- });
354
- });
355
-
356
- describe('Thread operations', () => {
357
- beforeAll(async () => {
358
- const threadTableSchema: Record<string, StorageColumn> = {
359
- id: { type: 'uuid', nullable: false },
360
- resourceId: { type: 'uuid', nullable: false },
361
- title: { type: 'text', nullable: true },
362
- createdAt: { type: 'timestamp', nullable: true },
363
- updatedAt: { type: 'timestamp', nullable: true },
364
- metadata: { type: 'jsonb', nullable: true },
365
- };
366
-
367
- await storage.createTable({ tableName: TABLE_THREADS, schema: threadTableSchema });
368
- });
369
-
370
- afterAll(async () => {
371
- await storage.dropTable(TABLE_THREADS);
372
- });
373
-
374
- beforeEach(async () => {
375
- await storage.clearTable({ tableName: TABLE_THREADS });
376
- });
377
-
378
- it('should get thread by ID', async () => {
379
- const thread = {
380
- id: '123e4567-e89b-12d3-a456-426614174000',
381
- resourceId: '123e4567-e89b-12d3-a456-426614174000',
382
- title: 'Test Thread',
383
- createdAt: new Date(),
384
- updatedAt: new Date(),
385
- metadata: { foo: 'bar' },
386
- };
387
-
388
- await storage.insert({ tableName: TABLE_THREADS, record: thread });
389
-
390
- const loadedThread = (await storage.getThreadById({ threadId: thread.id })) as StorageThreadType;
391
- expect(loadedThread).not.toBeNull();
392
- expect(loadedThread?.id).toEqual(thread.id);
393
- expect(loadedThread?.resourceId).toEqual(thread.resourceId);
394
- expect(loadedThread?.title).toEqual(thread.title);
395
- expect(new Date(loadedThread?.createdAt)).toEqual(new Date(thread.createdAt));
396
- expect(new Date(loadedThread?.updatedAt)).toEqual(new Date(thread.updatedAt));
397
- expect(loadedThread?.metadata).toEqual(thread.metadata);
398
- });
399
-
400
- it('should save thread', async () => {
401
- const thread = {
402
- id: '123e4567-e89b-12d3-a456-426614174000',
403
- resourceId: '123e4567-e89b-12d3-a456-426614174000',
404
- title: 'Test Thread',
405
- createdAt: new Date(),
406
- updatedAt: new Date(),
407
- metadata: { foo: 'bar' },
408
- };
409
-
410
- await storage.saveThread({ thread });
411
-
412
- const loadedThread = (await storage.getThreadById({ threadId: thread.id })) as StorageThreadType;
413
- expect(loadedThread).not.toBeNull();
414
- expect(loadedThread?.id).toEqual(thread.id);
415
- expect(loadedThread?.resourceId).toEqual(thread.resourceId);
416
- expect(loadedThread?.title).toEqual(thread.title);
417
- expect(new Date(loadedThread?.createdAt)).toEqual(new Date(thread.createdAt));
418
- expect(new Date(loadedThread?.updatedAt)).toEqual(new Date(thread.updatedAt));
419
- expect(loadedThread?.metadata).toEqual(thread.metadata);
420
- });
421
-
422
- it('should get threads by resource ID', async () => {
423
- const resourceId = '123e4567-e89b-12d3-a456-426614174000';
424
- const thread1 = {
425
- id: '123e4567-e89b-12d3-a456-426614174000',
426
- resourceId,
427
- title: 'Test Thread',
428
- createdAt: new Date(),
429
- updatedAt: new Date(),
430
- metadata: { foo: 'bar' },
431
- };
432
-
433
- const thread2 = {
434
- id: '123e4567-e89b-12d3-a456-426614174001',
435
- resourceId,
436
- title: 'Test Thread',
437
- createdAt: new Date(),
438
- updatedAt: new Date(),
439
- metadata: { foo: 'bar' },
440
- };
441
-
442
- await storage.saveThread({ thread: thread1 });
443
- await storage.saveThread({ thread: thread2 });
444
-
445
- const loadedThreads = await storage.getThreadsByResourceId({ resourceId });
446
-
447
- expect(loadedThreads).not.toBeNull();
448
- expect(loadedThreads.length).toEqual(2);
449
-
450
- expect(loadedThreads[0].id).toEqual(thread1.id);
451
- expect(loadedThreads[0].resourceId).toEqual(resourceId);
452
- expect(loadedThreads[0].title).toEqual(thread1.title);
453
- expect(new Date(loadedThreads[0].createdAt)).toEqual(new Date(thread1.createdAt));
454
- expect(new Date(loadedThreads[0].updatedAt)).toEqual(new Date(thread1.updatedAt));
455
- expect(loadedThreads[0].metadata).toEqual(thread1.metadata);
456
-
457
- expect(loadedThreads[1].id).toEqual(thread2.id);
458
- expect(loadedThreads[1].resourceId).toEqual(resourceId);
459
- expect(loadedThreads[1].title).toEqual(thread2.title);
460
- expect(new Date(loadedThreads[1].createdAt)).toEqual(new Date(thread2.createdAt));
461
- expect(new Date(loadedThreads[1].updatedAt)).toEqual(new Date(thread2.updatedAt));
462
- expect(loadedThreads[1].metadata).toEqual(thread2.metadata);
463
- });
464
-
465
- it('should update thread', async () => {
466
- const thread = {
467
- id: '123e4567-e89b-12d3-a456-426614174000',
468
- resourceId: '123e4567-e89b-12d3-a456-426614174000',
469
- title: 'Test Thread',
470
- createdAt: new Date(),
471
- updatedAt: new Date(),
472
- metadata: { foo: 'bar' },
473
- };
474
-
475
- await storage.saveThread({ thread });
476
-
477
- const updatedThread = await storage.updateThread({
478
- id: thread.id,
479
- title: 'Updated Thread',
480
- metadata: { foo: 'hi' },
481
- });
482
-
483
- expect(updatedThread).not.toBeNull();
484
- expect(updatedThread.id).toEqual(thread.id);
485
- expect(updatedThread.title).toEqual('Updated Thread');
486
- expect(updatedThread.metadata).toEqual({ foo: 'hi' });
487
- });
488
-
489
- it('should delete thread', async () => {
490
- await storage.dropTable(TABLE_THREADS);
491
- // create new table
492
- const threadTableSchema: Record<string, StorageColumn> = {
493
- id: { type: 'uuid', nullable: false },
494
- resourceId: { type: 'uuid', nullable: false },
495
- title: { type: 'text', nullable: true },
496
- createdAt: { type: 'timestamp', nullable: true },
497
- updatedAt: { type: 'timestamp', nullable: true },
498
- metadata: { type: 'jsonb', nullable: true },
499
- };
500
-
501
- await storage.createTable({ tableName: TABLE_THREADS, schema: threadTableSchema });
502
-
503
- const thread = {
504
- id: '123e4567-e89b-12d3-a456-426614174023',
505
- resourceId: '123e4567-e89b-12d3-a456-426614234020',
506
- title: 'Test Thread',
507
- createdAt: new Date(),
508
- updatedAt: new Date(),
509
- metadata: { foo: 'bar' },
510
- } as StorageThreadType;
511
-
512
- await storage.saveThread({ thread });
513
-
514
- await storage.deleteThread({ threadId: thread.id });
515
-
516
- const loadedThread = await storage.getThreadById({ threadId: thread.id });
517
- expect(loadedThread).toBeNull();
518
- });
519
- });
520
-
521
- describe('Message operations', () => {
522
- beforeAll(async () => {
523
- const messageTableSchema: Record<string, StorageColumn> = {
524
- id: { type: 'uuid', nullable: false },
525
- content: { type: 'text', nullable: true },
526
- role: { type: 'text', nullable: true },
527
- createdAt: { type: 'timestamp', nullable: false },
528
- threadId: { type: 'uuid', nullable: false },
529
- resourceId: { type: 'uuid', nullable: true },
530
- toolCallIds: { type: 'text', nullable: true },
531
- toolCallArgs: { type: 'jsonb', nullable: true },
532
- toolNames: { type: 'text', nullable: true },
533
- type: { type: 'text', nullable: true },
534
- };
535
-
536
- await storage.createTable({ tableName: TABLE_MESSAGES, schema: messageTableSchema });
537
- });
538
-
539
- afterAll(async () => {
540
- await storage.dropTable(TABLE_MESSAGES);
541
- });
542
-
543
- afterEach(async () => {
544
- await storage.clearTable({ tableName: TABLE_MESSAGES });
545
- });
546
-
547
- it('should save messages without error', async () => {
548
- const messages = generateMessageRecords(10);
549
- expect(async () => {
550
- await storage.saveMessages({ messages, format: 'v2' });
551
- }).not.toThrow();
552
- });
553
-
554
- it('should get messages by thread ID', async () => {
555
- const threadId = '12333d567-e89b-12d3-a456-426614174000';
556
- const messages = generateMessageRecords(10, threadId);
557
- await storage.saveMessages({ messages, format: 'v2' });
558
- const loadedMessages = await storage.getMessages({ threadId, format: 'v2' });
559
-
560
- expect(loadedMessages).not.toBeNull();
561
- expect(loadedMessages.length).toEqual(10);
562
-
563
- loadedMessages.forEach((message, index) => {
564
- expect(message.threadId).toEqual(threadId);
565
- expect(message.id.toString()).toEqual(messages[index].id);
566
- expect(message.content).toEqual(messages[index].content);
567
- expect(message.role).toEqual(messages[index].role);
568
- expect(message.resourceId).toEqual(messages[index].resourceId);
569
- expect(message.type).toEqual(messages[index].type);
570
- });
571
- });
572
-
573
- it('should get the last N messages when selectBy.last is specified', async () => {
574
- const threadId = '12333d567-e89b-12d3-a456-426614174000';
575
- const messages = generateMessageRecords(10, threadId);
576
- await storage.saveMessages({ messages, format: 'v2' });
577
-
578
- // Get the last 3 messages
579
- const loadedMessages = await storage.getMessages({
580
- threadId,
581
- selectBy: { last: 3 },
582
- format: 'v2',
583
- });
584
-
585
- expect(loadedMessages).not.toBeNull();
586
- expect(loadedMessages.length).toEqual(3);
587
-
588
- // Verify that we got the last 3 messages in chronological order
589
- for (let i = 0; i < 3; i++) {
590
- expect(loadedMessages[i].id.toString()).toEqual(messages[messages.length - 3 + i].id);
591
- expect(loadedMessages[i].content).toEqual(messages[messages.length - 3 + i].content);
592
- }
593
- });
594
-
595
- it('should get specific messages when selectBy.include is specified', async () => {
596
- const threadId = '12333d567-e89b-12d3-a456-426614174000';
597
- const messages = generateMessageRecords(10, threadId);
598
- await storage.saveMessages({ messages, format: 'v2' });
599
-
600
- // Select specific messages by ID
601
- const messageIds = [messages[2].id, messages[5].id, messages[8].id];
602
- const loadedMessages = await storage.getMessages({
603
- threadId,
604
- selectBy: {
605
- include: messageIds.map(id => ({ id })),
606
- },
607
- format: 'v2',
608
- });
609
-
610
- expect(loadedMessages).not.toBeNull();
611
- // We should get either the specified messages or all thread messages
612
- expect(loadedMessages.length).toBeGreaterThanOrEqual(3);
613
-
614
- // Verify that the selected messages are included in the results
615
- const loadedIds = loadedMessages.map(m => m.id.toString());
616
- messageIds.forEach(id => {
617
- expect(loadedIds).toContain(id);
618
- });
619
- });
620
-
621
- it('should handle empty results when using selectBy filters', async () => {
622
- const threadId = '12333d567-e89b-12d3-a456-426614174000';
623
- // Create messages for a different thread ID
624
- const messages = generateMessageRecords(5, 'different-thread-id');
625
- await storage.saveMessages({ messages, format: 'v2' });
626
-
627
- // Try to get messages for our test threadId, which should return empty
628
- const loadedMessages = await storage.getMessages({
629
- threadId,
630
- selectBy: { last: 3 },
631
- format: 'v2',
632
- });
633
-
634
- expect(loadedMessages).not.toBeNull();
635
- expect(loadedMessages.length).toEqual(0);
636
- });
637
-
638
- it('should throw error when threadConfig is provided', async () => {
639
- const threadId = '12333d567-e89b-12d3-a456-426614174000';
640
- const messages = generateMessageRecords(5, threadId);
641
- await storage.saveMessages({ messages, format: 'v2' });
642
-
643
- // Test that providing a threadConfig throws an error
644
- await expect(
645
- storage.getMessages({
646
- threadId,
647
- threadConfig: {
648
- lastMessages: 10,
649
- semanticRecall: {
650
- topK: 5,
651
- messageRange: { before: 3, after: 3 },
652
- },
653
- workingMemory: {
654
- enabled: true,
655
- },
656
- threads: {
657
- generateTitle: true,
658
- },
659
- },
660
- }),
661
- ).rejects.toThrow('ThreadConfig is not supported by LanceDB storage');
662
- });
663
-
664
- it('should retrieve messages with context using withPreviousMessages and withNextMessages', async () => {
665
- const threadId = '12333d567-e89b-12d3-a456-426614174000';
666
- const messages = generateMessageRecords(10, threadId);
667
- await storage.saveMessages({ messages, format: 'v2' });
668
-
669
- // Get a specific message with context (previous and next messages)
670
- const targetMessageId = messages[5].id;
671
- const loadedMessages = await storage.getMessages({
672
- threadId,
673
- selectBy: {
674
- include: [
675
- {
676
- id: targetMessageId,
677
- withPreviousMessages: 2,
678
- withNextMessages: 1,
679
- },
680
- ],
681
- },
682
- format: 'v2',
683
- });
684
-
685
- expect(loadedMessages).not.toBeNull();
686
-
687
- // We should get the target message plus 2 previous and 1 next message
688
- // So a total of 4 messages (the target message, 2 before, and 1 after)
689
- expect(loadedMessages.length).toEqual(4);
690
-
691
- // Extract the IDs from the results for easier checking
692
- const loadedIds = loadedMessages.map(m => m.id.toString());
693
-
694
- // Check that the target message is included
695
- expect(loadedIds).toContain(targetMessageId);
696
-
697
- // Check that the previous 2 messages are included (messages[3] and messages[4])
698
- expect(loadedIds).toContain(messages[3].id);
699
- expect(loadedIds).toContain(messages[4].id);
700
-
701
- // Check that the next message is included (messages[6])
702
- expect(loadedIds).toContain(messages[6].id);
703
-
704
- // Verify correct chronological order
705
- for (let i = 0; i < loadedMessages.length - 1; i++) {
706
- const currentDate = new Date(loadedMessages[i].createdAt).getTime();
707
- const nextDate = new Date(loadedMessages[i + 1].createdAt).getTime();
708
- expect(currentDate).toBeLessThanOrEqual(nextDate);
709
- }
710
- });
711
-
712
- it('should upsert messages: duplicate id+threadId results in update, not duplicate row', async () => {
713
- const thread = 'thread-1';
714
- const baseMessage = generateMessageRecords(1, thread)[0];
715
-
716
- // Insert the message for the first time
717
- await storage.saveMessages({ messages: [baseMessage], format: 'v2' });
718
-
719
- // Insert again with the same id and threadId but different content
720
- const updatedMessage: MastraMessageV2 = {
721
- ...generateMessageRecords(1, thread)[0],
722
- id: baseMessage.id,
723
- content: { format: 2, parts: [{ type: 'text', text: 'Updated' }] },
724
- };
725
-
726
- await storage.saveMessages({ messages: [updatedMessage], format: 'v2' });
727
-
728
- // Retrieve messages for the thread
729
- const retrievedMessages = await storage.getMessages({ threadId: thread, format: 'v2' });
730
- // Only one message should exist for that id+threadId
731
- expect(retrievedMessages.filter(m => m.id.toString() === baseMessage.id)).toHaveLength(1);
732
-
733
- // The content should be the updated one
734
- expect(retrievedMessages.find(m => m.id.toString() === baseMessage.id)?.content.parts[0].text).toBe('Updated');
735
- });
736
-
737
- it('should upsert messages: duplicate id and different threadid', async () => {
738
- const thread1 = 'thread-1';
739
- const thread2 = 'thread-2';
740
- const thread3 = 'thread-3';
741
-
742
- const message = generateMessageRecords(1, thread1)[0];
743
-
744
- // Insert message into thread1
745
- await storage.saveMessages({ messages: [message], format: 'v2' });
746
-
747
- // Attempt to insert a message with the same id but different threadId
748
- const conflictingMessage: MastraMessageV2 = {
749
- ...generateMessageRecords(1, thread2)[0],
750
- id: message.id,
751
- content: { format: 2, parts: [{ type: 'text', text: 'Thread2 Content' }] },
752
- };
753
-
754
- const differentMessage: MastraMessageV2 = {
755
- ...generateMessageRecords(1, thread3)[0],
756
- id: '2',
757
- content: { format: 2, parts: [{ type: 'text', text: 'Another Message Content' }] },
758
- };
759
-
760
- // Save should move the message to the new thread
761
- await storage.saveMessages({ messages: [conflictingMessage], format: 'v2' });
762
-
763
- await storage.saveMessages({ messages: [differentMessage], format: 'v2' });
764
-
765
- // Retrieve messages for both threads
766
- const thread1Messages = await storage.getMessages({ threadId: thread1, format: 'v2' });
767
- const thread2Messages = await storage.getMessages({ threadId: thread2, format: 'v2' });
768
- const thread3Messages = await storage.getMessages({ threadId: thread3, format: 'v2' });
769
-
770
- // Thread 1 should NOT have the message with that id
771
- expect(thread1Messages.find(m => m.id.toString() === message.id)).toBeUndefined();
772
-
773
- // Thread 2 should have the message with that id
774
- expect(thread2Messages.find(m => m.id.toString() === message.id)?.content.parts[0].text).toBe('Thread2 Content');
775
-
776
- // Thread 2 should have the other message
777
- expect(thread3Messages.find(m => m.id.toString() === differentMessage.id)?.content.parts[0].text).toBe(
778
- 'Another Message Content',
779
- );
780
- });
781
- });
782
-
783
- describe('Trace operations', () => {
784
- beforeAll(async () => {
785
- const traceTableSchema = TABLE_SCHEMAS[TABLE_TRACES];
786
- await storage.createTable({ tableName: TABLE_TRACES, schema: traceTableSchema });
787
- });
788
-
789
- afterAll(async () => {
790
- await storage.dropTable(TABLE_TRACES);
791
- });
792
-
793
- afterEach(async () => {
794
- await storage.clearTable({ tableName: TABLE_TRACES });
795
- });
796
-
797
- it('should save trace', async () => {
798
- const trace = {
799
- id: '123e4567-e89b-12d3-a456-426614174023',
800
- parentSpanId: '123e4567-e89b-12d3-a456-426614174023',
801
- name: 'Test Trace',
802
- traceId: '123e4567-e89b-12d3-a456-426614234020',
803
- scope: 'test',
804
- kind: 0,
805
- attributes: { attribute1: 'value1' },
806
- status: { code: 0, description: 'OK' },
807
- events: { event1: 'value1' },
808
- links: { link1: 'value1' },
809
- other: { other1: 'value1' },
810
- startTime: new Date().getTime(),
811
- endTime: new Date().getTime(),
812
- createdAt: new Date(),
813
- } as TraceType;
814
-
815
- await storage.saveTrace({ trace });
816
-
817
- const loadedTrace = await storage.getTraceById({ traceId: trace.id });
818
-
819
- expect(loadedTrace).not.toBeNull();
820
- expect(loadedTrace.id).toEqual(trace.id);
821
- expect(loadedTrace.name).toEqual('Test Trace');
822
- expect(loadedTrace.parentSpanId).toEqual(trace.parentSpanId);
823
- expect(loadedTrace.traceId).toEqual(trace.traceId);
824
- expect(loadedTrace.scope).toEqual(trace.scope);
825
- expect(loadedTrace.kind).toEqual(trace.kind);
826
- expect(loadedTrace.attributes).toEqual(trace.attributes);
827
- expect(loadedTrace.status).toEqual(trace.status);
828
- expect(loadedTrace.events).toEqual(trace.events);
829
- expect(loadedTrace.links).toEqual(trace.links);
830
- expect(loadedTrace.other).toEqual(trace.other);
831
- expect(loadedTrace.startTime).toEqual(trace.startTime);
832
- expect(loadedTrace.endTime).toEqual(trace.endTime);
833
- expect(new Date(loadedTrace.createdAt)).toEqual(trace.createdAt);
834
- });
835
-
836
- it('should get traces by page', async () => {
837
- const traces = generateTraceRecords(10);
838
-
839
- await Promise.all(traces.map(trace => storage.saveTrace({ trace })));
840
-
841
- const loadedTrace = await storage.getTraces({
842
- page: 1,
843
- perPage: 10,
844
- });
845
-
846
- expect(loadedTrace).not.toBeNull();
847
- expect(loadedTrace.length).toEqual(10);
848
-
849
- loadedTrace.forEach(trace => {
850
- expect(trace.id).toEqual(traces.find(t => t.id === trace.id)?.id);
851
- expect(trace.name).toEqual(traces.find(t => t.id === trace.id)?.name);
852
- expect(trace.parentSpanId).toEqual(traces.find(t => t.id === trace.id)?.parentSpanId);
853
- expect(trace.traceId).toEqual(traces.find(t => t.id === trace.id)?.traceId);
854
- expect(trace.scope).toEqual(traces.find(t => t.id === trace.id)?.scope);
855
- expect(trace.kind).toEqual(traces.find(t => t.id === trace.id)?.kind);
856
- expect(trace.attributes).toEqual(traces.find(t => t.id === trace.id)?.attributes);
857
- expect(trace.status).toEqual(traces.find(t => t.id === trace.id)?.status);
858
- expect(trace.events).toEqual(traces.find(t => t.id === trace.id)?.events);
859
- expect(trace.links).toEqual(traces.find(t => t.id === trace.id)?.links);
860
- expect(trace.other).toEqual(traces.find(t => t.id === trace.id)?.other);
861
- expect(new Date(trace.startTime)).toEqual(new Date(traces.find(t => t.id === trace.id)?.startTime ?? ''));
862
- expect(new Date(trace.endTime)).toEqual(new Date(traces.find(t => t.id === trace.id)?.endTime ?? ''));
863
- expect(new Date(trace.createdAt)).toEqual(new Date(traces.find(t => t.id === trace.id)?.createdAt ?? ''));
864
- });
865
- });
866
-
867
- it('should get trace by name and scope', async () => {
868
- const trace = generateTraceRecords(1)[0];
869
- await storage.saveTrace({ trace });
870
-
871
- const loadedTrace = await storage.getTraces({ name: trace.name, scope: trace.scope, page: 1, perPage: 10 });
872
-
873
- expect(loadedTrace).not.toBeNull();
874
- expect(loadedTrace[0].id).toEqual(trace.id);
875
- expect(loadedTrace[0].name).toEqual(trace.name);
876
- expect(loadedTrace[0].parentSpanId).toEqual(trace.parentSpanId);
877
- expect(loadedTrace[0].scope).toEqual(trace.scope);
878
- expect(loadedTrace[0].kind).toEqual(trace.kind);
879
- expect(loadedTrace[0].attributes).toEqual(trace.attributes);
880
- expect(loadedTrace[0].status).toEqual(trace.status);
881
- expect(loadedTrace[0].events).toEqual(trace.events);
882
- expect(loadedTrace[0].links).toEqual(trace.links);
883
- expect(loadedTrace[0].other).toEqual(trace.other);
884
- expect(loadedTrace[0].startTime).toEqual(new Date(trace.startTime));
885
- expect(loadedTrace[0].endTime).toEqual(new Date(trace.endTime));
886
- expect(loadedTrace[0].createdAt).toEqual(new Date(trace.createdAt));
887
- });
888
- });
889
-
890
- describe('Eval operations', () => {
891
- beforeAll(async () => {
892
- const evalSchema = TABLE_SCHEMAS[TABLE_EVALS];
893
- await storage.createTable({ tableName: TABLE_EVALS, schema: evalSchema });
894
- });
895
-
896
- afterAll(async () => {
897
- await storage.dropTable(TABLE_EVALS);
898
- });
899
-
900
- it('should get evals by agent name', async () => {
901
- const evals = generateEvalRecords(1);
902
- await storage.saveEvals({ evals });
903
-
904
- const loadedEvals = await storage.getEvalsByAgentName(evals[0].agentName);
905
-
906
- expect(loadedEvals).not.toBeNull();
907
- expect(loadedEvals.length).toBe(1);
908
- expect(loadedEvals[0].input).toEqual(evals[0].input);
909
- expect(loadedEvals[0].output).toEqual(evals[0].output);
910
- expect(loadedEvals[0].agentName).toEqual(evals[0].agentName);
911
- expect(loadedEvals[0].metricName).toEqual(evals[0].metricName);
912
- expect(loadedEvals[0].result).toEqual(evals[0].result);
913
- expect(loadedEvals[0].testInfo).toEqual(evals[0].testInfo);
914
- expect(new Date(loadedEvals[0].createdAt)).toEqual(new Date(evals[0].createdAt));
915
- });
916
- });
917
-
918
- describe('Workflow Operations', () => {
919
- beforeAll(async () => {
920
- const workflowSchema = TABLE_SCHEMAS[TABLE_WORKFLOW_SNAPSHOT];
921
- await storage.createTable({ tableName: TABLE_WORKFLOW_SNAPSHOT, schema: workflowSchema });
922
- });
923
-
924
- afterAll(async () => {
925
- await storage.dropTable(TABLE_WORKFLOW_SNAPSHOT);
926
- });
927
-
928
- it('should save and retrieve workflow snapshots', async () => {
929
- const { snapshot, runId } = generateWorkflowSnapshot('running');
930
-
931
- await storage.persistWorkflowSnapshot({
932
- workflowName: 'test-workflow',
933
- runId,
934
- snapshot,
935
- });
936
- const retrieved = await storage.loadWorkflowSnapshot({
937
- workflowName: 'test-workflow',
938
- runId,
939
- });
940
- expect(retrieved).toEqual(snapshot);
941
- });
942
-
943
- it('should handle non-existent workflow snapshots', async () => {
944
- const result = await storage.loadWorkflowSnapshot({
945
- workflowName: 'test-workflow',
946
- runId: 'non-existent',
947
- });
948
- expect(result).toBeNull();
949
- });
950
-
951
- it('should update workflow snapshot status', async () => {
952
- const { snapshot, runId } = generateWorkflowSnapshot('running');
953
-
954
- await storage.persistWorkflowSnapshot({
955
- workflowName: 'test-workflow',
956
- runId,
957
- snapshot,
958
- });
959
-
960
- const updatedSnapshot = {
961
- ...snapshot,
962
- value: { [runId]: 'success' },
963
- timestamp: Date.now(),
964
- };
965
-
966
- await storage.persistWorkflowSnapshot({
967
- workflowName: 'test-workflow',
968
- runId,
969
- snapshot: updatedSnapshot,
970
- });
971
-
972
- const retrieved = await storage.loadWorkflowSnapshot({
973
- workflowName: 'test-workflow',
974
- runId,
975
- });
976
-
977
- expect(retrieved?.value[runId]).toBe('success');
978
- expect(retrieved?.timestamp).toBeGreaterThan(snapshot.timestamp);
979
- });
980
-
981
- it('should handle complex workflow state', async () => {
982
- const runId = `run-${randomUUID()}`;
983
- const workflowName = 'complex-workflow';
984
-
985
- const complexSnapshot = {
986
- runId,
987
- value: { currentState: 'running' },
988
- timestamp: Date.now(),
989
- context: {
990
- 'step-1': {
991
- status: 'success',
992
- output: {
993
- nestedData: {
994
- array: [1, 2, 3],
995
- object: { key: 'value' },
996
- date: new Date().toISOString(),
997
- },
998
- },
999
- },
1000
- 'step-2': {
1001
- status: 'suspended',
1002
- dependencies: ['step-3', 'step-4'],
1003
- },
1004
- input: {
1005
- type: 'scheduled',
1006
- metadata: {
1007
- schedule: '0 0 * * *',
1008
- timezone: 'UTC',
1009
- },
1010
- },
1011
- },
1012
- activePaths: [],
1013
- suspendedPaths: {},
1014
- status: 'suspended',
1015
- serializedStepGraph: [],
1016
- } as unknown as WorkflowRunState;
1017
-
1018
- await storage.persistWorkflowSnapshot({
1019
- workflowName,
1020
- runId,
1021
- snapshot: complexSnapshot,
1022
- });
1023
-
1024
- const loadedSnapshot = await storage.loadWorkflowSnapshot({
1025
- workflowName,
1026
- runId,
1027
- });
1028
-
1029
- expect(loadedSnapshot).toEqual(complexSnapshot);
1030
- });
1031
- });
1032
-
1033
- describe('getWorkflowRuns', () => {
1034
- beforeAll(async () => {
1035
- const workflowSchema = TABLE_SCHEMAS[TABLE_WORKFLOW_SNAPSHOT];
1036
- await storage.createTable({ tableName: TABLE_WORKFLOW_SNAPSHOT, schema: workflowSchema });
1037
- });
1038
-
1039
- afterAll(async () => {
1040
- await storage.dropTable(TABLE_WORKFLOW_SNAPSHOT);
1041
- });
1042
-
1043
- beforeEach(async () => {
1044
- await storage.clearTable({ tableName: TABLE_WORKFLOW_SNAPSHOT });
1045
- });
1046
- it('returns empty array when no workflows exist', async () => {
1047
- const { runs, total } = await storage.getWorkflowRuns();
1048
- expect(runs).toEqual([]);
1049
- expect(total).toBe(0);
1050
- });
1051
-
1052
- it('returns all workflows by default', async () => {
1053
- const workflowName1 = 'default_test_1';
1054
- const workflowName2 = 'default_test_2';
1055
-
1056
- const { snapshot: workflow1, runId: runId1, stepId: stepId1 } = generateWorkflowSnapshot('success');
1057
- const { snapshot: workflow2, runId: runId2, stepId: stepId2 } = generateWorkflowSnapshot('running');
1058
-
1059
- await storage.persistWorkflowSnapshot({ workflowName: workflowName1, runId: runId1, snapshot: workflow1 });
1060
- await new Promise(resolve => setTimeout(resolve, 10)); // Small delay to ensure different timestamps
1061
- await storage.persistWorkflowSnapshot({ workflowName: workflowName2, runId: runId2, snapshot: workflow2 });
1062
-
1063
- const { runs, total } = await storage.getWorkflowRuns();
1064
- expect(runs).toHaveLength(2);
1065
- expect(total).toBe(2);
1066
- expect(runs[0]!.workflowName).toBe(workflowName1);
1067
- expect(runs[1]!.workflowName).toBe(workflowName2);
1068
- const firstSnapshot = runs[0]!.snapshot as WorkflowRunState;
1069
- const secondSnapshot = runs[1]!.snapshot as WorkflowRunState;
1070
- expect(firstSnapshot.context?.[stepId1]?.status).toBe('success');
1071
- expect(secondSnapshot.context?.[stepId2]?.status).toBe('running');
1072
- });
1073
-
1074
- // it('filters by workflow name', async () => {
1075
- // const workflowName1 = 'filter_test_1';
1076
- // const workflowName2 = 'filter_test_2';
1077
-
1078
- // const { snapshot: workflow1, runId: runId1, stepId: stepId1 } = generateWorkflowSnapshot('success');
1079
- // const { snapshot: workflow2, runId: runId2 } = generateWorkflowSnapshot('failed');
1080
-
1081
- // await storage.persistWorkflowSnapshot({ workflowName: workflowName1, runId: runId1, snapshot: workflow1 });
1082
- // await new Promise(resolve => setTimeout(resolve, 10)); // Small delay to ensure different timestamps
1083
- // await storage.persistWorkflowSnapshot({ workflowName: workflowName2, runId: runId2, snapshot: workflow2 });
1084
-
1085
- // const { runs, total } = await storage.getWorkflowRuns({ workflowName: workflowName1 });
1086
- // expect(runs).toHaveLength(1);
1087
- // expect(total).toBe(1);
1088
- // expect(runs[0]!.workflowName).toBe(workflowName1);
1089
- // const snapshot = runs[0]!.snapshot as WorkflowRunState;
1090
- // expect(snapshot.context?.[stepId1]?.status).toBe('success');
1091
- // });
1092
-
1093
- // it('filters by date range', async () => {
1094
- // const now = new Date();
1095
- // const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
1096
- // const twoDaysAgo = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000);
1097
- // const workflowName1 = 'date_test_1';
1098
- // const workflowName2 = 'date_test_2';
1099
- // const workflowName3 = 'date_test_3';
1100
-
1101
- // const { snapshot: workflow1, runId: runId1 } = generateWorkflowSnapshot('success');
1102
- // const { snapshot: workflow2, runId: runId2, stepId: stepId2 } = generateWorkflowSnapshot('failed');
1103
- // const { snapshot: workflow3, runId: runId3, stepId: stepId3 } = generateWorkflowSnapshot('suspended');
1104
-
1105
- // await storage.insert({
1106
- // tableName: TABLE_WORKFLOW_SNAPSHOT,
1107
- // record: {
1108
- // workflow_name: workflowName1,
1109
- // run_id: runId1,
1110
- // snapshot: workflow1,
1111
- // createdAt: twoDaysAgo,
1112
- // updatedAt: twoDaysAgo,
1113
- // },
1114
- // });
1115
- // await storage.insert({
1116
- // tableName: TABLE_WORKFLOW_SNAPSHOT,
1117
- // record: {
1118
- // workflow_name: workflowName2,
1119
- // run_id: runId2,
1120
- // snapshot: workflow2,
1121
- // createdAt: yesterday,
1122
- // updatedAt: yesterday,
1123
- // },
1124
- // });
1125
- // await storage.insert({
1126
- // tableName: TABLE_WORKFLOW_SNAPSHOT,
1127
- // record: {
1128
- // workflow_name: workflowName3,
1129
- // run_id: runId3,
1130
- // snapshot: workflow3,
1131
- // createdAt: now,
1132
- // updatedAt: now,
1133
- // },
1134
- // });
1135
-
1136
- // const { runs } = await storage.getWorkflowRuns({
1137
- // fromDate: yesterday,
1138
- // toDate: now,
1139
- // });
1140
-
1141
- // expect(runs).toHaveLength(2);
1142
- // expect(runs[0]!.workflowName).toBe(workflowName3);
1143
- // expect(runs[1]!.workflowName).toBe(workflowName2);
1144
- // const firstSnapshot = runs[0]!.snapshot as WorkflowRunState;
1145
- // const secondSnapshot = runs[1]!.snapshot as WorkflowRunState;
1146
- // expect(firstSnapshot.context?.[stepId3]?.status).toBe('waiting');
1147
- // expect(secondSnapshot.context?.[stepId2]?.status).toBe('running');
1148
- // });
1149
-
1150
- // it('handles pagination', async () => {
1151
- // const workflowName1 = 'page_test_1';
1152
- // const workflowName2 = 'page_test_2';
1153
- // const workflowName3 = 'page_test_3';
1154
-
1155
- // const { snapshot: workflow1, runId: runId1, stepId: stepId1 } = generateWorkflowSnapshot('success');
1156
- // const { snapshot: workflow2, runId: runId2, stepId: stepId2 } = generateWorkflowSnapshot('failed');
1157
- // const { snapshot: workflow3, runId: runId3, stepId: stepId3 } = generateWorkflowSnapshot('suspended');
1158
-
1159
- // await storage.persistWorkflowSnapshot({ workflowName: workflowName1, runId: runId1, snapshot: workflow1 });
1160
- // await new Promise(resolve => setTimeout(resolve, 10)); // Small delay to ensure different timestamps
1161
- // await storage.persistWorkflowSnapshot({ workflowName: workflowName2, runId: runId2, snapshot: workflow2 });
1162
- // await new Promise(resolve => setTimeout(resolve, 10)); // Small delay to ensure different timestamps
1163
- // await storage.persistWorkflowSnapshot({ workflowName: workflowName3, runId: runId3, snapshot: workflow3 });
1164
-
1165
- // // Get first page
1166
- // const page1 = await storage.getWorkflowRuns({ limit: 2, offset: 0 });
1167
- // expect(page1.runs).toHaveLength(2);
1168
- // expect(page1.total).toBe(3); // Total count of all records
1169
- // expect(page1.runs[0]!.workflowName).toBe(workflowName3);
1170
- // expect(page1.runs[1]!.workflowName).toBe(workflowName2);
1171
- // const firstSnapshot = page1.runs[0]!.snapshot as WorkflowRunState;
1172
- // const secondSnapshot = page1.runs[1]!.snapshot as WorkflowRunState;
1173
- // expect(firstSnapshot.context?.[stepId3]?.status).toBe('waiting');
1174
- // expect(secondSnapshot.context?.[stepId2]?.status).toBe('running');
1175
-
1176
- // // Get second page
1177
- // const page2 = await storage.getWorkflowRuns({ limit: 2, offset: 2 });
1178
- // expect(page2.runs).toHaveLength(1);
1179
- // expect(page2.total).toBe(3);
1180
- // expect(page2.runs[0]!.workflowName).toBe(workflowName1);
1181
- // const snapshot = page2.runs[0]!.snapshot as WorkflowRunState;
1182
- // expect(snapshot.context?.[stepId1]?.status).toBe('completed');
1183
- // });
1184
- });
1185
-
1186
- describe('getWorkflowRunById', () => {
1187
- const workflowName = 'workflow-id-test';
1188
- let runId: string;
1189
- let stepId: string;
1190
-
1191
- beforeAll(async () => {
1192
- const workflowSchema = TABLE_SCHEMAS[TABLE_WORKFLOW_SNAPSHOT];
1193
- await storage.createTable({ tableName: TABLE_WORKFLOW_SNAPSHOT, schema: workflowSchema });
1194
- });
1195
-
1196
- afterAll(async () => {
1197
- await storage.dropTable(TABLE_WORKFLOW_SNAPSHOT);
1198
- });
1199
-
1200
- beforeEach(async () => {
1201
- // Insert a workflow run for positive test
1202
- const sample = generateWorkflowSnapshot('success');
1203
- runId = sample.runId;
1204
- stepId = sample.stepId;
1205
- await storage.insert({
1206
- tableName: TABLE_WORKFLOW_SNAPSHOT,
1207
- record: {
1208
- workflow_name: workflowName,
1209
- run_id: runId,
1210
- resourceId: 'resource-abc',
1211
- snapshot: sample.snapshot,
1212
- createdAt: new Date(),
1213
- updatedAt: new Date(),
1214
- },
1215
- });
1216
- });
1217
-
1218
- it('should retrieve a workflow run by ID', async () => {
1219
- const found = await storage.getWorkflowRunById({
1220
- runId,
1221
- workflowName,
1222
- });
1223
- expect(found).not.toBeNull();
1224
- expect(found?.runId).toBe(runId);
1225
- checkWorkflowSnapshot(found?.snapshot!, stepId, 'success');
1226
- });
1227
-
1228
- it('should return null for non-existent workflow run ID', async () => {
1229
- const notFound = await storage.getWorkflowRunById({
1230
- runId: 'non-existent-id',
1231
- workflowName,
1232
- });
1233
- expect(notFound).toBeNull();
1234
- });
1235
- });
1236
-
1237
- describe('alterTable', () => {
1238
- const TEST_TABLE = 'test_alter_table';
1239
- const BASE_SCHEMA = {
1240
- id: { type: 'integer', primaryKey: true, nullable: false },
1241
- name: { type: 'text', nullable: true },
1242
- createdAt: { type: 'timestamp', nullable: false },
1243
- } as Record<string, StorageColumn>;
1244
-
1245
- beforeEach(async () => {
1246
- await storage.dropTable(TEST_TABLE as TABLE_NAMES);
1247
- await storage.createTable({ tableName: TEST_TABLE as TABLE_NAMES, schema: BASE_SCHEMA });
1248
- });
1249
-
1250
- afterEach(async () => {
1251
- await storage.clearTable({ tableName: TEST_TABLE as TABLE_NAMES });
1252
- });
1253
-
1254
- it('adds a new column to an existing table', async () => {
1255
- await storage.alterTable({
1256
- tableName: TEST_TABLE as TABLE_NAMES,
1257
- schema: { ...BASE_SCHEMA, age: { type: 'integer', nullable: true } },
1258
- ifNotExists: ['age'],
1259
- });
1260
-
1261
- await storage.insert({
1262
- tableName: TEST_TABLE as TABLE_NAMES,
1263
- record: { id: 1, name: 'Alice', age: 42, createdAt: new Date() },
1264
- });
1265
-
1266
- const row = await storage.load({
1267
- tableName: TEST_TABLE as TABLE_NAMES,
1268
- keys: { id: 1 },
1269
- });
1270
- expect(row?.age).toBe(42);
1271
- });
1272
-
1273
- it('is idempotent when adding an existing column', async () => {
1274
- await storage.alterTable({
1275
- tableName: TEST_TABLE as TABLE_NAMES,
1276
- schema: { ...BASE_SCHEMA, foo: { type: 'text', nullable: true } },
1277
- ifNotExists: ['foo'],
1278
- });
1279
- // Add the column again (should not throw)
1280
- await expect(
1281
- storage.alterTable({
1282
- tableName: TEST_TABLE as TABLE_NAMES,
1283
- schema: { ...BASE_SCHEMA, foo: { type: 'text', nullable: true } },
1284
- ifNotExists: ['foo'],
1285
- }),
1286
- ).resolves.not.toThrow();
1287
- });
1288
-
1289
- it('should add a default value to a column when using not null', async () => {
1290
- await storage.insert({
1291
- tableName: TEST_TABLE as TABLE_NAMES,
1292
- record: { id: 1, name: 'Bob', createdAt: new Date() },
1293
- });
1294
-
1295
- await expect(
1296
- storage.alterTable({
1297
- tableName: TEST_TABLE as TABLE_NAMES,
1298
- schema: { ...BASE_SCHEMA, text_column: { type: 'text', nullable: false } },
1299
- ifNotExists: ['text_column'],
1300
- }),
1301
- ).resolves.not.toThrow();
1302
-
1303
- await expect(
1304
- storage.alterTable({
1305
- tableName: TEST_TABLE as TABLE_NAMES,
1306
- schema: { ...BASE_SCHEMA, timestamp_column: { type: 'timestamp', nullable: false } },
1307
- ifNotExists: ['timestamp_column'],
1308
- }),
1309
- ).resolves.not.toThrow();
1310
-
1311
- await expect(
1312
- storage.alterTable({
1313
- tableName: TEST_TABLE as TABLE_NAMES,
1314
- schema: { ...BASE_SCHEMA, bigint_column: { type: 'bigint', nullable: false } },
1315
- ifNotExists: ['bigint_column'],
1316
- }),
1317
- ).resolves.not.toThrow();
1318
-
1319
- await expect(
1320
- storage.alterTable({
1321
- tableName: TEST_TABLE as TABLE_NAMES,
1322
- schema: { ...BASE_SCHEMA, jsonb_column: { type: 'jsonb', nullable: false } },
1323
- ifNotExists: ['jsonb_column'],
1324
- }),
1325
- ).resolves.not.toThrow();
1326
-
1327
- await expect(
1328
- storage.alterTable({
1329
- tableName: TEST_TABLE as TABLE_NAMES,
1330
- schema: { ...BASE_SCHEMA, uuid_column: { type: 'uuid', nullable: false } },
1331
- ifNotExists: ['uuid_column'],
1332
- }),
1333
- ).resolves.not.toThrow();
1334
- });
1335
- });
1336
- });
10
+ createTestSuite(storage);