@mastra/mongodb 0.10.0 → 0.10.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.
@@ -0,0 +1,779 @@
1
+ import { randomUUID } from 'crypto';
2
+ import type { MessageType, MetricResult, WorkflowRunState } from '@mastra/core';
3
+ import { TABLE_EVALS, TABLE_MESSAGES, TABLE_THREADS, TABLE_WORKFLOW_SNAPSHOT } from '@mastra/core/storage';
4
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
5
+ import type { MongoDBConfig } from './index';
6
+ import { MongoDBStore } from './index';
7
+
8
+ class Test {
9
+ store: MongoDBStore;
10
+
11
+ constructor(store: MongoDBStore) {
12
+ this.store = store;
13
+ }
14
+
15
+ build() {
16
+ return this;
17
+ }
18
+
19
+ async clearTables() {
20
+ try {
21
+ await this.store.clearTable({ tableName: TABLE_WORKFLOW_SNAPSHOT });
22
+ await this.store.clearTable({ tableName: TABLE_MESSAGES });
23
+ await this.store.clearTable({ tableName: TABLE_THREADS });
24
+ await this.store.clearTable({ tableName: TABLE_EVALS });
25
+ } catch (error) {
26
+ // Ignore errors during table clearing
27
+ console.warn('Error clearing tables:', error);
28
+ }
29
+ }
30
+
31
+ generateSampleThread(options: any = {}) {
32
+ return {
33
+ id: `thread-${randomUUID()}`,
34
+ resourceId: `resource-${randomUUID()}`,
35
+ title: 'Test Thread',
36
+ createdAt: new Date(),
37
+ updatedAt: new Date(),
38
+ metadata: { key: 'value' },
39
+ ...options,
40
+ };
41
+ }
42
+
43
+ generateSampleMessage(threadId: string): MessageType {
44
+ return {
45
+ id: `msg-${randomUUID()}`,
46
+ role: 'user',
47
+ type: 'text',
48
+ threadId,
49
+ content: [{ type: 'text', text: 'Hello' }],
50
+ createdAt: new Date(),
51
+ resourceId: randomUUID(),
52
+ };
53
+ }
54
+
55
+ generateSampleEval(isTest: boolean, options: any = {}) {
56
+ const testInfo = isTest ? { testPath: 'test/path.ts', testName: 'Test Name' } : undefined;
57
+
58
+ return {
59
+ id: randomUUID(),
60
+ agentName: 'Agent Name',
61
+ input: 'Sample input',
62
+ output: 'Sample output',
63
+ result: { score: 0.8 } as MetricResult,
64
+ metricName: 'sample-metric',
65
+ instructions: 'Sample instructions',
66
+ testInfo,
67
+ globalRunId: `global-${randomUUID()}`,
68
+ runId: `run-${randomUUID()}`,
69
+ createdAt: new Date().toISOString(),
70
+ ...options,
71
+ };
72
+ }
73
+
74
+ generateSampleWorkflowSnapshot(options: any = {}) {
75
+ const runId = `run-${randomUUID()}`;
76
+ const stepId = `step-${randomUUID()}`;
77
+ const timestamp = options.createdAt || new Date();
78
+ const snapshot = {
79
+ result: { success: true },
80
+ value: {},
81
+ context: {
82
+ steps: {
83
+ [stepId]: {
84
+ status: options.status,
85
+ payload: {},
86
+ error: undefined,
87
+ },
88
+ },
89
+ triggerData: {},
90
+ attempts: {},
91
+ },
92
+ activePaths: [],
93
+ suspendedPaths: {},
94
+ runId,
95
+ timestamp: timestamp.getTime(),
96
+ } as WorkflowRunState;
97
+ return { snapshot, runId, stepId };
98
+ }
99
+ }
100
+
101
+ const TEST_CONFIG: MongoDBConfig = {
102
+ url: process.env.MONGODB_URL || 'mongodb://localhost:27017',
103
+ dbName: process.env.MONGODB_DB_NAME || 'mastra-test-db',
104
+ };
105
+
106
+ describe('MongoDBStore', () => {
107
+ let store: MongoDBStore;
108
+
109
+ beforeAll(async () => {
110
+ store = new MongoDBStore(TEST_CONFIG);
111
+ await store.init();
112
+ });
113
+
114
+ // --- Validation tests ---
115
+ describe('Validation', () => {
116
+ const validConfig = TEST_CONFIG;
117
+ it('throws if url is empty', () => {
118
+ expect(() => new MongoDBStore({ ...validConfig, url: '' })).toThrow(/url must be provided and cannot be empty/);
119
+ });
120
+
121
+ it('throws if dbName is missing or empty', () => {
122
+ expect(() => new MongoDBStore({ ...validConfig, dbName: '' })).toThrow(
123
+ /dbName must be provided and cannot be empty/,
124
+ );
125
+ const { dbName, ...rest } = validConfig;
126
+ expect(() => new MongoDBStore(rest as any)).toThrow(/dbName must be provided and cannot be empty/);
127
+ });
128
+ it('does not throw on valid config (host-based)', () => {
129
+ expect(() => new MongoDBStore(validConfig)).not.toThrow();
130
+ });
131
+ });
132
+
133
+ describe('Thread Operations', () => {
134
+ it('should create and retrieve a thread', async () => {
135
+ const test = new Test(store).build();
136
+ await test.clearTables();
137
+ const thread = test.generateSampleThread();
138
+
139
+ // Save thread
140
+ const savedThread = await store.saveThread({ thread });
141
+ expect(savedThread).toEqual(thread);
142
+
143
+ // Retrieve thread
144
+ const retrievedThread = await store.getThreadById({ threadId: thread.id });
145
+ expect(retrievedThread?.title).toEqual(thread.title);
146
+ });
147
+
148
+ it('should return null for non-existent thread', async () => {
149
+ const test = new Test(store).build();
150
+ await test.clearTables();
151
+
152
+ const result = await store.getThreadById({ threadId: 'non-existent' });
153
+ expect(result).toBeNull();
154
+ });
155
+
156
+ it('should get threads by resource ID', async () => {
157
+ const test = new Test(store).build();
158
+ await test.clearTables();
159
+
160
+ const thread1 = test.generateSampleThread();
161
+ const thread2 = test.generateSampleThread({ resourceId: thread1.resourceId });
162
+
163
+ await store.saveThread({ thread: thread1 });
164
+ await store.saveThread({ thread: thread2 });
165
+
166
+ const threads = await store.getThreadsByResourceId({ resourceId: thread1.resourceId });
167
+ expect(threads).toHaveLength(2);
168
+ expect(threads.map(t => t.id)).toEqual(expect.arrayContaining([thread1.id, thread2.id]));
169
+ });
170
+
171
+ it('should update thread title and metadata', async () => {
172
+ const test = new Test(store).build();
173
+ await test.clearTables();
174
+
175
+ const thread = test.generateSampleThread();
176
+ await store.saveThread({ thread });
177
+
178
+ const newMetadata = { newKey: 'newValue' };
179
+ const updatedThread = await store.updateThread({
180
+ id: thread.id,
181
+ title: 'Updated Title',
182
+ metadata: newMetadata,
183
+ });
184
+
185
+ expect(updatedThread.title).toBe('Updated Title');
186
+ expect(updatedThread.metadata).toEqual({
187
+ ...thread.metadata,
188
+ ...newMetadata,
189
+ });
190
+
191
+ // Verify persistence
192
+ const retrievedThread = await store.getThreadById({ threadId: thread.id });
193
+ expect(retrievedThread).toEqual(updatedThread);
194
+ });
195
+
196
+ it('should delete thread and its messages', async () => {
197
+ const test = new Test(store).build();
198
+ await test.clearTables();
199
+
200
+ const thread = test.generateSampleThread();
201
+ await store.saveThread({ thread });
202
+
203
+ // Add some messages
204
+ const messages = [test.generateSampleMessage(thread.id), test.generateSampleMessage(thread.id)];
205
+ await store.saveMessages({ messages });
206
+
207
+ await store.deleteThread({ threadId: thread.id });
208
+
209
+ const retrievedThread = await store.getThreadById({ threadId: thread.id });
210
+ expect(retrievedThread).toBeNull();
211
+
212
+ // Verify messages were also deleted
213
+ const retrievedMessages = await store.getMessages({ threadId: thread.id });
214
+ expect(retrievedMessages).toHaveLength(0);
215
+ });
216
+
217
+ it('should not create duplicate threads with the same threadId but update the existing one', async () => {
218
+ const test = new Test(store).build();
219
+ await test.clearTables();
220
+ const thread = test.generateSampleThread();
221
+
222
+ // Save the thread for the first time
223
+ await store.saveThread({ thread });
224
+
225
+ // Modify the thread and save again with the same id
226
+ const updatedThread = { ...thread, title: 'Updated Title', metadata: { key: 'newValue' } };
227
+ await store.saveThread({ thread: updatedThread });
228
+
229
+ // Retrieve all threads with this id (should only be one)
230
+ const collection = await store['getCollection'](TABLE_THREADS);
231
+ const allThreads = await collection.find({ id: thread.id }).toArray();
232
+ expect(allThreads).toHaveLength(1);
233
+
234
+ // Retrieve the thread and check it was updated
235
+ const retrievedThread = await store.getThreadById({ threadId: thread.id });
236
+ expect(retrievedThread?.title).toBe('Updated Title');
237
+ expect(retrievedThread?.metadata).toEqual({ key: 'newValue' });
238
+ });
239
+ });
240
+
241
+ describe('Message Operations', () => {
242
+ it('should save and retrieve messages', async () => {
243
+ const test = new Test(store).build();
244
+ await test.clearTables();
245
+ const thread = test.generateSampleThread();
246
+ await store.saveThread({ thread });
247
+
248
+ const messages = [test.generateSampleMessage(thread.id), test.generateSampleMessage(thread.id)];
249
+
250
+ // Save messages
251
+ const savedMessages = await store.saveMessages({ messages });
252
+ expect(savedMessages).toEqual(messages);
253
+
254
+ // Retrieve messages
255
+ const retrievedMessages = await store.getMessages({ threadId: thread.id });
256
+ expect(retrievedMessages).toHaveLength(2);
257
+ expect(messages[0]).toEqual(expect.objectContaining(retrievedMessages[0]));
258
+ expect(messages[1]).toEqual(expect.objectContaining(retrievedMessages[1]));
259
+ });
260
+
261
+ it('should handle empty message array', async () => {
262
+ const test = new Test(store).build();
263
+ await test.clearTables();
264
+
265
+ const result = await store.saveMessages({ messages: [] });
266
+ expect(result).toEqual([]);
267
+ });
268
+
269
+ it('should maintain message order', async () => {
270
+ const test = new Test(store).build();
271
+ await test.clearTables();
272
+ const thread = test.generateSampleThread();
273
+ await store.saveThread({ thread });
274
+
275
+ const messages = [
276
+ {
277
+ ...test.generateSampleMessage(thread.id),
278
+ content: [{ type: 'text', text: 'First' }] as MessageType['content'],
279
+ },
280
+ {
281
+ ...test.generateSampleMessage(thread.id),
282
+ content: [{ type: 'text', text: 'Second' }] as MessageType['content'],
283
+ },
284
+ {
285
+ ...test.generateSampleMessage(thread.id),
286
+ content: [{ type: 'text', text: 'Third' }] as MessageType['content'],
287
+ },
288
+ ];
289
+
290
+ await store.saveMessages({ messages });
291
+
292
+ const retrievedMessages = await store.getMessages({ threadId: thread.id });
293
+ expect(retrievedMessages).toHaveLength(3);
294
+
295
+ // Verify order is maintained
296
+ retrievedMessages.forEach((msg, idx) => {
297
+ expect(((msg as any).content[0] as any).text).toBe((messages[idx]!.content[0] as any).text);
298
+ });
299
+ });
300
+ });
301
+
302
+ describe('Edge Cases and Error Handling', () => {
303
+ it('should handle large metadata objects', async () => {
304
+ const test = new Test(store).build();
305
+ await test.clearTables();
306
+ const thread = test.generateSampleThread();
307
+ const largeMetadata = {
308
+ ...thread.metadata,
309
+ largeArray: Array.from({ length: 1000 }, (_, i) => ({ index: i, data: 'test'.repeat(100) })),
310
+ };
311
+
312
+ const threadWithLargeMetadata = {
313
+ ...thread,
314
+ metadata: largeMetadata,
315
+ };
316
+
317
+ await store.saveThread({ thread: threadWithLargeMetadata });
318
+ const retrieved = await store.getThreadById({ threadId: thread.id });
319
+
320
+ expect(retrieved?.metadata).toEqual(largeMetadata);
321
+ });
322
+
323
+ it('should handle special characters in thread titles', async () => {
324
+ const test = new Test(store).build();
325
+ await test.clearTables();
326
+ const thread = test.generateSampleThread({
327
+ title: 'Special \'quotes\' and "double quotes" and emoji 🎉',
328
+ });
329
+
330
+ await store.saveThread({ thread });
331
+ const retrieved = await store.getThreadById({ threadId: thread.id });
332
+
333
+ expect(retrieved?.title).toBe(thread.title);
334
+ });
335
+
336
+ it('should handle concurrent thread updates', async () => {
337
+ const test = new Test(store).build();
338
+ await test.clearTables();
339
+ const thread = test.generateSampleThread();
340
+ await store.saveThread({ thread });
341
+
342
+ // Perform multiple updates concurrently
343
+ const updates = Array.from({ length: 5 }, (_, i) =>
344
+ store.updateThread({
345
+ id: thread.id,
346
+ title: `Update ${i}`,
347
+ metadata: { update: i },
348
+ }),
349
+ );
350
+
351
+ await expect(Promise.all(updates)).resolves.toBeDefined();
352
+
353
+ // Verify final state
354
+ const finalThread = await store.getThreadById({ threadId: thread.id });
355
+ expect(finalThread).toBeDefined();
356
+ });
357
+ });
358
+
359
+ describe('Workflow Snapshots', () => {
360
+ it('should persist and load workflow snapshots', async () => {
361
+ const test = new Test(store).build();
362
+ await test.clearTables();
363
+ const workflowName = 'test-workflow';
364
+ const runId = `run-${randomUUID()}`;
365
+ const snapshot = {
366
+ status: 'running',
367
+ context: {
368
+ stepResults: {},
369
+ attempts: {},
370
+ triggerData: { type: 'manual' },
371
+ },
372
+ } as any;
373
+
374
+ await store.persistWorkflowSnapshot({
375
+ workflowName,
376
+ runId,
377
+ snapshot,
378
+ });
379
+
380
+ const loadedSnapshot = await store.loadWorkflowSnapshot({
381
+ workflowName,
382
+ runId,
383
+ });
384
+
385
+ expect(loadedSnapshot).toEqual(snapshot);
386
+ });
387
+
388
+ it('should return null for non-existent workflow snapshot', async () => {
389
+ const result = await store.loadWorkflowSnapshot({
390
+ workflowName: 'non-existent',
391
+ runId: 'non-existent',
392
+ });
393
+
394
+ expect(result).toBeNull();
395
+ });
396
+
397
+ it('should update existing workflow snapshot', async () => {
398
+ const workflowName = 'test-workflow';
399
+ const runId = `run-${randomUUID()}`;
400
+ const initialSnapshot = {
401
+ status: 'running',
402
+ context: {
403
+ stepResults: {},
404
+ attempts: {},
405
+ triggerData: { type: 'manual' },
406
+ },
407
+ };
408
+
409
+ await store.persistWorkflowSnapshot({
410
+ workflowName,
411
+ runId,
412
+ snapshot: initialSnapshot as any,
413
+ });
414
+
415
+ const updatedSnapshot = {
416
+ status: 'completed',
417
+ context: {
418
+ stepResults: {
419
+ 'step-1': { status: 'success', result: { data: 'test' } },
420
+ },
421
+ attempts: { 'step-1': 1 },
422
+ triggerData: { type: 'manual' },
423
+ },
424
+ } as any;
425
+
426
+ await store.persistWorkflowSnapshot({
427
+ workflowName,
428
+ runId,
429
+ snapshot: updatedSnapshot,
430
+ });
431
+
432
+ const loadedSnapshot = await store.loadWorkflowSnapshot({
433
+ workflowName,
434
+ runId,
435
+ });
436
+
437
+ expect(loadedSnapshot).toEqual(updatedSnapshot);
438
+ });
439
+
440
+ it('should handle complex workflow state', async () => {
441
+ const workflowName = 'complex-workflow';
442
+ const runId = `run-${randomUUID()}`;
443
+ const complexSnapshot = {
444
+ value: { currentState: 'running' },
445
+ context: {
446
+ stepResults: {
447
+ 'step-1': {
448
+ status: 'success',
449
+ result: {
450
+ nestedData: {
451
+ array: [1, 2, 3],
452
+ object: { key: 'value' },
453
+ date: new Date().toISOString(),
454
+ },
455
+ },
456
+ },
457
+ 'step-2': {
458
+ status: 'waiting',
459
+ dependencies: ['step-3', 'step-4'],
460
+ },
461
+ },
462
+ attempts: { 'step-1': 1, 'step-2': 0 },
463
+ triggerData: {
464
+ type: 'scheduled',
465
+ metadata: {
466
+ schedule: '0 0 * * *',
467
+ timezone: 'UTC',
468
+ },
469
+ },
470
+ },
471
+ activePaths: [
472
+ {
473
+ stepPath: ['step-1'],
474
+ stepId: 'step-1',
475
+ status: 'success',
476
+ },
477
+ {
478
+ stepPath: ['step-2'],
479
+ stepId: 'step-2',
480
+ status: 'waiting',
481
+ },
482
+ ],
483
+ runId: runId,
484
+ timestamp: Date.now(),
485
+ };
486
+
487
+ await store.persistWorkflowSnapshot({
488
+ workflowName,
489
+ runId,
490
+ snapshot: complexSnapshot as unknown as WorkflowRunState,
491
+ });
492
+
493
+ const loadedSnapshot = await store.loadWorkflowSnapshot({
494
+ workflowName,
495
+ runId,
496
+ });
497
+
498
+ expect(loadedSnapshot).toEqual(complexSnapshot);
499
+ });
500
+ });
501
+
502
+ describe('getWorkflowRuns', () => {
503
+ it('returns empty array when no workflows exist', async () => {
504
+ const test = new Test(store).build();
505
+ await test.clearTables();
506
+
507
+ const { runs, total } = await store.getWorkflowRuns();
508
+ expect(runs).toEqual([]);
509
+ expect(total).toBe(0);
510
+ });
511
+
512
+ it('returns all workflows by default', async () => {
513
+ const test = new Test(store).build();
514
+ await test.clearTables();
515
+
516
+ const workflowName1 = 'default_test_1';
517
+ const workflowName2 = 'default_test_2';
518
+
519
+ const {
520
+ snapshot: workflow1,
521
+ runId: runId1,
522
+ stepId: stepId1,
523
+ } = test.generateSampleWorkflowSnapshot({ status: 'completed' });
524
+ const {
525
+ snapshot: workflow2,
526
+ runId: runId2,
527
+ stepId: stepId2,
528
+ } = test.generateSampleWorkflowSnapshot({ status: 'running' });
529
+
530
+ await store.persistWorkflowSnapshot({ workflowName: workflowName1, runId: runId1, snapshot: workflow1 });
531
+ await new Promise(resolve => setTimeout(resolve, 10)); // Small delay to ensure different timestamps
532
+ await store.persistWorkflowSnapshot({ workflowName: workflowName2, runId: runId2, snapshot: workflow2 });
533
+
534
+ const { runs, total } = await store.getWorkflowRuns();
535
+ expect(runs).toHaveLength(2);
536
+ expect(total).toBe(2);
537
+ expect(runs[0]!.workflowName).toBe(workflowName2); // Most recent first
538
+ expect(runs[1]!.workflowName).toBe(workflowName1);
539
+ const firstSnapshot = runs[0]!.snapshot as WorkflowRunState;
540
+ const secondSnapshot = runs[1]!.snapshot as WorkflowRunState;
541
+ expect(firstSnapshot.context?.steps[stepId2]?.status).toBe('running');
542
+ expect(secondSnapshot.context?.steps[stepId1]?.status).toBe('completed');
543
+ });
544
+
545
+ it('filters by workflow name', async () => {
546
+ const test = new Test(store).build();
547
+ await test.clearTables();
548
+ const workflowName1 = 'filter_test_1';
549
+ const workflowName2 = 'filter_test_2';
550
+
551
+ const {
552
+ snapshot: workflow1,
553
+ runId: runId1,
554
+ stepId: stepId1,
555
+ } = test.generateSampleWorkflowSnapshot({ status: 'completed' });
556
+ const { snapshot: workflow2, runId: runId2 } = test.generateSampleWorkflowSnapshot({ status: 'failed' });
557
+
558
+ await store.persistWorkflowSnapshot({ workflowName: workflowName1, runId: runId1, snapshot: workflow1 });
559
+ await new Promise(resolve => setTimeout(resolve, 10)); // Small delay to ensure different timestamps
560
+ await store.persistWorkflowSnapshot({ workflowName: workflowName2, runId: runId2, snapshot: workflow2 });
561
+
562
+ const { runs, total } = await store.getWorkflowRuns({ workflowName: workflowName1 });
563
+ expect(runs).toHaveLength(1);
564
+ expect(total).toBe(1);
565
+ expect(runs[0]!.workflowName).toBe(workflowName1);
566
+ const snapshot = runs[0]!.snapshot as WorkflowRunState;
567
+ expect(snapshot.context?.steps[stepId1]?.status).toBe('completed');
568
+ });
569
+
570
+ it('filters by date range', async () => {
571
+ const test = new Test(store).build();
572
+ await test.clearTables();
573
+ const now = new Date();
574
+ const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
575
+ const twoDaysAgo = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000);
576
+ const workflowName1 = 'date_test_1';
577
+ const workflowName2 = 'date_test_2';
578
+ const workflowName3 = 'date_test_3';
579
+
580
+ const { snapshot: workflow1, runId: runId1 } = test.generateSampleWorkflowSnapshot({ status: 'completed' });
581
+ const {
582
+ snapshot: workflow2,
583
+ runId: runId2,
584
+ stepId: stepId2,
585
+ } = test.generateSampleWorkflowSnapshot({ status: 'running' });
586
+ const {
587
+ snapshot: workflow3,
588
+ runId: runId3,
589
+ stepId: stepId3,
590
+ } = test.generateSampleWorkflowSnapshot({ status: 'waiting' });
591
+
592
+ await store.insert({
593
+ tableName: TABLE_WORKFLOW_SNAPSHOT,
594
+ record: {
595
+ workflow_name: workflowName1,
596
+ run_id: runId1,
597
+ snapshot: workflow1,
598
+ createdAt: twoDaysAgo,
599
+ updatedAt: twoDaysAgo,
600
+ },
601
+ });
602
+ await store.insert({
603
+ tableName: TABLE_WORKFLOW_SNAPSHOT,
604
+ record: {
605
+ workflow_name: workflowName2,
606
+ run_id: runId2,
607
+ snapshot: workflow2,
608
+ createdAt: yesterday,
609
+ updatedAt: yesterday,
610
+ },
611
+ });
612
+ await store.insert({
613
+ tableName: TABLE_WORKFLOW_SNAPSHOT,
614
+ record: {
615
+ workflow_name: workflowName3,
616
+ run_id: runId3,
617
+ snapshot: workflow3,
618
+ createdAt: now,
619
+ updatedAt: now,
620
+ },
621
+ });
622
+
623
+ const { runs } = await store.getWorkflowRuns({
624
+ fromDate: yesterday,
625
+ toDate: now,
626
+ });
627
+
628
+ expect(runs).toHaveLength(2);
629
+ expect(runs[0]!.workflowName).toBe(workflowName3);
630
+ expect(runs[1]!.workflowName).toBe(workflowName2);
631
+ const firstSnapshot = runs[0]!.snapshot as WorkflowRunState;
632
+ const secondSnapshot = runs[1]!.snapshot as WorkflowRunState;
633
+ expect(firstSnapshot.context?.steps[stepId3]?.status).toBe('waiting');
634
+ expect(secondSnapshot.context?.steps[stepId2]?.status).toBe('running');
635
+ });
636
+
637
+ it('handles pagination', async () => {
638
+ const test = new Test(store).build();
639
+ await test.clearTables();
640
+ const workflowName1 = 'page_test_1';
641
+ const workflowName2 = 'page_test_2';
642
+ const workflowName3 = 'page_test_3';
643
+
644
+ const {
645
+ snapshot: workflow1,
646
+ runId: runId1,
647
+ stepId: stepId1,
648
+ } = test.generateSampleWorkflowSnapshot({ status: 'completed' });
649
+ const {
650
+ snapshot: workflow2,
651
+ runId: runId2,
652
+ stepId: stepId2,
653
+ } = test.generateSampleWorkflowSnapshot({ status: 'running' });
654
+ const {
655
+ snapshot: workflow3,
656
+ runId: runId3,
657
+ stepId: stepId3,
658
+ } = test.generateSampleWorkflowSnapshot({ status: 'waiting' });
659
+
660
+ await store.persistWorkflowSnapshot({ workflowName: workflowName1, runId: runId1, snapshot: workflow1 });
661
+ await new Promise(resolve => setTimeout(resolve, 10)); // Small delay to ensure different timestamps
662
+ await store.persistWorkflowSnapshot({ workflowName: workflowName2, runId: runId2, snapshot: workflow2 });
663
+ await new Promise(resolve => setTimeout(resolve, 10)); // Small delay to ensure different timestamps
664
+ await store.persistWorkflowSnapshot({ workflowName: workflowName3, runId: runId3, snapshot: workflow3 });
665
+
666
+ // Get first page
667
+ const page1 = await store.getWorkflowRuns({ limit: 2, offset: 0 });
668
+ expect(page1.runs).toHaveLength(2);
669
+ expect(page1.total).toBe(3); // Total count of all records
670
+ expect(page1.runs[0]!.workflowName).toBe(workflowName3);
671
+ expect(page1.runs[1]!.workflowName).toBe(workflowName2);
672
+ const firstSnapshot = page1.runs[0]!.snapshot as WorkflowRunState;
673
+ const secondSnapshot = page1.runs[1]!.snapshot as WorkflowRunState;
674
+ expect(firstSnapshot.context?.steps[stepId3]?.status).toBe('waiting');
675
+ expect(secondSnapshot.context?.steps[stepId2]?.status).toBe('running');
676
+
677
+ // Get second page
678
+ const page2 = await store.getWorkflowRuns({ limit: 2, offset: 2 });
679
+ expect(page2.runs).toHaveLength(1);
680
+ expect(page2.total).toBe(3);
681
+ expect(page2.runs[0]!.workflowName).toBe(workflowName1);
682
+ const snapshot = page2.runs[0]!.snapshot as WorkflowRunState;
683
+ expect(snapshot.context?.steps[stepId1]?.status).toBe('completed');
684
+ });
685
+ });
686
+
687
+ describe('Eval Operations', () => {
688
+ it('should retrieve evals by agent name', async () => {
689
+ const test = new Test(store).build();
690
+ await test.clearTables();
691
+ const agentName = `test-agent-${randomUUID()}`;
692
+
693
+ // Create sample evals
694
+ const liveEval = test.generateSampleEval(false, { agentName });
695
+ const testEval = test.generateSampleEval(true, { agentName });
696
+ const otherAgentEval = test.generateSampleEval(false, { agentName: `other-agent-${randomUUID()}` });
697
+
698
+ // Insert evals
699
+ await store.insert({
700
+ tableName: TABLE_EVALS,
701
+ record: {
702
+ agent_name: liveEval.agentName,
703
+ input: liveEval.input,
704
+ output: liveEval.output,
705
+ result: liveEval.result,
706
+ metric_name: liveEval.metricName,
707
+ instructions: liveEval.instructions,
708
+ test_info: null,
709
+ global_run_id: liveEval.globalRunId,
710
+ run_id: liveEval.runId,
711
+ created_at: liveEval.createdAt,
712
+ createdAt: new Date(liveEval.createdAt),
713
+ },
714
+ });
715
+
716
+ await store.insert({
717
+ tableName: TABLE_EVALS,
718
+ record: {
719
+ agent_name: testEval.agentName,
720
+ input: testEval.input,
721
+ output: testEval.output,
722
+ result: testEval.result,
723
+ metric_name: testEval.metricName,
724
+ instructions: testEval.instructions,
725
+ test_info: JSON.stringify(testEval.testInfo),
726
+ global_run_id: testEval.globalRunId,
727
+ run_id: testEval.runId,
728
+ created_at: testEval.createdAt,
729
+ createdAt: new Date(testEval.createdAt),
730
+ },
731
+ });
732
+
733
+ await store.insert({
734
+ tableName: TABLE_EVALS,
735
+ record: {
736
+ agent_name: otherAgentEval.agentName,
737
+ input: otherAgentEval.input,
738
+ output: otherAgentEval.output,
739
+ result: otherAgentEval.result,
740
+ metric_name: otherAgentEval.metricName,
741
+ instructions: otherAgentEval.instructions,
742
+ test_info: null,
743
+ global_run_id: otherAgentEval.globalRunId,
744
+ run_id: otherAgentEval.runId,
745
+ created_at: otherAgentEval.createdAt,
746
+ createdAt: new Date(otherAgentEval.createdAt),
747
+ },
748
+ });
749
+
750
+ // Test getting all evals for the agent
751
+ const allEvals = await store.getEvalsByAgentName(agentName);
752
+ expect(allEvals).toHaveLength(2);
753
+ expect(allEvals.map(e => e.runId)).toEqual(expect.arrayContaining([liveEval.runId, testEval.runId]));
754
+
755
+ // Test getting only live evals
756
+ const liveEvals = await store.getEvalsByAgentName(agentName, 'live');
757
+ expect(liveEvals).toHaveLength(1);
758
+ expect(liveEvals[0]!.runId).toBe(liveEval.runId);
759
+
760
+ // Test getting only test evals
761
+ const testEvals = await store.getEvalsByAgentName(agentName, 'test');
762
+ expect(testEvals).toHaveLength(1);
763
+ expect(testEvals[0]!.runId).toBe(testEval.runId);
764
+ expect(testEvals[0]!.testInfo).toEqual(testEval.testInfo);
765
+
766
+ // Test getting evals for non-existent agent
767
+ const nonExistentEvals = await store.getEvalsByAgentName('non-existent-agent');
768
+ expect(nonExistentEvals).toHaveLength(0);
769
+ });
770
+ });
771
+
772
+ afterAll(async () => {
773
+ try {
774
+ await store.close();
775
+ } catch (error) {
776
+ console.warn('Error closing store:', error);
777
+ }
778
+ });
779
+ });