@mastra/upstash 0.12.1 → 0.12.2-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,1461 +0,0 @@
1
- import { randomUUID } from 'crypto';
2
- import {
3
- checkWorkflowSnapshot,
4
- createSampleMessageV2,
5
- createSampleThread,
6
- createSampleWorkflowSnapshot,
7
- } from '@internal/storage-test-utils';
8
- import type { MastraMessageV2 } from '@mastra/core';
9
- import type { TABLE_NAMES } from '@mastra/core/storage';
10
- import {
11
- TABLE_MESSAGES,
12
- TABLE_THREADS,
13
- TABLE_WORKFLOW_SNAPSHOT,
14
- TABLE_EVALS,
15
- TABLE_TRACES,
16
- } from '@mastra/core/storage';
17
- import type { WorkflowRunState } from '@mastra/core/workflows';
18
- import { describe, it, expect, beforeAll, beforeEach, afterAll, vi, afterEach } from 'vitest';
19
-
20
- import { UpstashStore } from './index';
21
-
22
- // Increase timeout for all tests in this file to 30 seconds
23
- vi.setConfig({ testTimeout: 200_000, hookTimeout: 200_000 });
24
-
25
- const createSampleTrace = (
26
- name: string,
27
- scope?: string,
28
- attributes?: Record<string, string>,
29
- createdAt: Date = new Date(),
30
- ) => ({
31
- id: `trace-${randomUUID()}`,
32
- parentSpanId: `span-${randomUUID()}`,
33
- traceId: `trace-${randomUUID()}`,
34
- name,
35
- scope,
36
- kind: 'internal',
37
- status: JSON.stringify({ code: 'success' }),
38
- events: JSON.stringify([{ name: 'start', timestamp: createdAt.getTime() }]),
39
- links: JSON.stringify([]),
40
- attributes: attributes ? JSON.stringify(attributes) : undefined,
41
- startTime: createdAt.toISOString(),
42
- endTime: new Date(createdAt.getTime() + 1000).toISOString(),
43
- other: JSON.stringify({ custom: 'data' }),
44
- createdAt: createdAt.toISOString(),
45
- });
46
-
47
- const createSampleEval = (agentName: string, isTest = false, createdAt: Date = new Date()) => {
48
- const testInfo = isTest ? { testPath: 'test/path.ts', testName: 'Test Name' } : undefined;
49
-
50
- return {
51
- agent_name: agentName,
52
- input: 'Sample input',
53
- output: 'Sample output',
54
- result: JSON.stringify({ score: 0.8 }),
55
- metric_name: 'sample-metric',
56
- instructions: 'Sample instructions',
57
- test_info: testInfo ? JSON.stringify(testInfo) : undefined,
58
- global_run_id: `global-${randomUUID()}`,
59
- run_id: `run-${randomUUID()}`,
60
- created_at: createdAt.toISOString(),
61
- };
62
- };
63
-
64
- describe('UpstashStore', () => {
65
- let store: UpstashStore;
66
- const testTableName = 'test_table';
67
- const testTableName2 = 'test_table2';
68
-
69
- beforeAll(async () => {
70
- console.log('Initializing UpstashStore...');
71
-
72
- await new Promise(resolve => setTimeout(resolve, 5000));
73
- store = new UpstashStore({
74
- url: 'http://localhost:8079',
75
- token: 'test_token',
76
- });
77
-
78
- await store.init();
79
- console.log('UpstashStore initialized');
80
- });
81
-
82
- afterAll(async () => {
83
- // Clean up test tables
84
- await store.clearTable({ tableName: testTableName as TABLE_NAMES });
85
- await store.clearTable({ tableName: testTableName2 as TABLE_NAMES });
86
- await store.clearTable({ tableName: TABLE_THREADS });
87
- await store.clearTable({ tableName: TABLE_MESSAGES });
88
- await store.clearTable({ tableName: TABLE_WORKFLOW_SNAPSHOT });
89
- await store.clearTable({ tableName: TABLE_EVALS });
90
- await store.clearTable({ tableName: TABLE_TRACES });
91
- });
92
-
93
- describe('Table Operations', () => {
94
- it('should create a new table with schema', async () => {
95
- await store.createTable({
96
- tableName: testTableName as TABLE_NAMES,
97
- schema: {
98
- id: { type: 'text', primaryKey: true },
99
- data: { type: 'text', nullable: true },
100
- },
101
- });
102
-
103
- // Verify table exists by inserting and retrieving data
104
- await store.insert({
105
- tableName: testTableName as TABLE_NAMES,
106
- record: { id: 'test1', data: 'test-data' },
107
- });
108
-
109
- const result = await store.load({ tableName: testTableName as TABLE_NAMES, keys: { id: 'test1' } });
110
- expect(result).toBeTruthy();
111
- });
112
-
113
- it('should handle multiple table creation', async () => {
114
- await store.createTable({
115
- tableName: testTableName2 as TABLE_NAMES,
116
- schema: {
117
- id: { type: 'text', primaryKey: true },
118
- data: { type: 'text', nullable: true },
119
- },
120
- });
121
-
122
- // Verify both tables work independently
123
- await store.insert({
124
- tableName: testTableName2 as TABLE_NAMES,
125
- record: { id: 'test2', data: 'test-data-2' },
126
- });
127
-
128
- const result = await store.load({ tableName: testTableName2 as TABLE_NAMES, keys: { id: 'test2' } });
129
- expect(result).toBeTruthy();
130
- });
131
- });
132
-
133
- describe('Thread Operations', () => {
134
- beforeEach(async () => {
135
- await store.clearTable({ tableName: TABLE_THREADS });
136
- });
137
-
138
- it('should create and retrieve a thread', async () => {
139
- const now = new Date();
140
- const thread = createSampleThread({ date: now });
141
-
142
- const savedThread = await store.saveThread({ thread });
143
- expect(savedThread).toEqual(thread);
144
-
145
- const retrievedThread = await store.getThreadById({ threadId: thread.id });
146
- expect(retrievedThread).toEqual({
147
- ...thread,
148
- createdAt: new Date(now.toISOString()),
149
- updatedAt: new Date(now.toISOString()),
150
- });
151
- });
152
-
153
- it('should return null for non-existent thread', async () => {
154
- const result = await store.getThreadById({ threadId: 'non-existent' });
155
- expect(result).toBeNull();
156
- });
157
-
158
- it('should get threads by resource ID', async () => {
159
- const thread1 = createSampleThread();
160
- const thread2 = createSampleThread({ resourceId: thread1.resourceId });
161
- const threads = [thread1, thread2];
162
-
163
- const resourceId = threads[0].resourceId;
164
- const threadIds = threads.map(t => t.id);
165
-
166
- await Promise.all(threads.map(thread => store.saveThread({ thread })));
167
-
168
- const retrievedThreads = await store.getThreadsByResourceId({ resourceId });
169
- expect(retrievedThreads).toHaveLength(2);
170
- expect(retrievedThreads.map(t => t.id)).toEqual(expect.arrayContaining(threadIds));
171
- });
172
-
173
- it('should update thread metadata', async () => {
174
- const thread = createSampleThread();
175
-
176
- await store.saveThread({ thread });
177
-
178
- const updatedThread = await store.updateThread({
179
- id: thread.id,
180
- title: 'Updated Title',
181
- metadata: { updated: 'value' },
182
- });
183
-
184
- expect(updatedThread.title).toBe('Updated Title');
185
- expect(updatedThread.metadata).toEqual({
186
- key: 'value',
187
- updated: 'value',
188
- });
189
- });
190
-
191
- it('should update thread updatedAt when a message is saved to it', async () => {
192
- const thread = createSampleThread();
193
- await store.saveThread({ thread });
194
-
195
- // Get the initial thread to capture the original updatedAt
196
- const initialThread = await store.getThreadById({ threadId: thread.id });
197
- expect(initialThread).toBeDefined();
198
- const originalUpdatedAt = initialThread!.updatedAt;
199
-
200
- // Wait a small amount to ensure different timestamp
201
- await new Promise(resolve => setTimeout(resolve, 10));
202
-
203
- // Create and save a message to the thread
204
- const message = createSampleMessageV2({ threadId: thread.id });
205
- await store.saveMessages({ messages: [message], format: 'v2' });
206
-
207
- // Retrieve the thread again and check that updatedAt was updated
208
- const updatedThread = await store.getThreadById({ threadId: thread.id });
209
- expect(updatedThread).toBeDefined();
210
- expect(updatedThread!.updatedAt.getTime()).toBeGreaterThan(originalUpdatedAt.getTime());
211
- });
212
-
213
- it('should fetch >100000 threads by resource ID', async () => {
214
- const resourceId = `resource-${randomUUID()}`;
215
- const total = 100_000;
216
- const threads = Array.from({ length: total }, () => createSampleThread({ resourceId }));
217
-
218
- await store.batchInsert({ tableName: TABLE_THREADS, records: threads });
219
-
220
- const retrievedThreads = await store.getThreadsByResourceId({ resourceId });
221
- expect(retrievedThreads).toHaveLength(total);
222
- });
223
- it('should delete thread and its messages', async () => {
224
- const thread = createSampleThread();
225
- await store.saveThread({ thread });
226
-
227
- // Add some messages
228
- const messages = [createSampleMessageV2({ threadId: thread.id }), createSampleMessageV2({ threadId: thread.id })];
229
- await store.saveMessages({ messages, format: 'v2' });
230
-
231
- await store.deleteThread({ threadId: thread.id });
232
-
233
- const retrievedThread = await store.getThreadById({ threadId: thread.id });
234
- expect(retrievedThread).toBeNull();
235
-
236
- // Verify messages were also deleted
237
- const retrievedMessages = await store.getMessages({ threadId: thread.id });
238
- expect(retrievedMessages).toHaveLength(0);
239
- });
240
- });
241
-
242
- describe('Date Handling', () => {
243
- beforeEach(async () => {
244
- await store.clearTable({ tableName: TABLE_THREADS });
245
- });
246
-
247
- it('should handle Date objects in thread operations', async () => {
248
- const now = new Date();
249
- const thread = createSampleThread({ date: now });
250
-
251
- await store.saveThread({ thread });
252
- const retrievedThread = await store.getThreadById({ threadId: thread.id });
253
- expect(retrievedThread?.createdAt).toBeInstanceOf(Date);
254
- expect(retrievedThread?.updatedAt).toBeInstanceOf(Date);
255
- expect(retrievedThread?.createdAt.toISOString()).toBe(now.toISOString());
256
- expect(retrievedThread?.updatedAt.toISOString()).toBe(now.toISOString());
257
- });
258
-
259
- it('should handle ISO string dates in thread operations', async () => {
260
- const now = new Date();
261
- const thread = createSampleThread({ date: now });
262
-
263
- await store.saveThread({ thread });
264
- const retrievedThread = await store.getThreadById({ threadId: thread.id });
265
- expect(retrievedThread?.createdAt).toBeInstanceOf(Date);
266
- expect(retrievedThread?.updatedAt).toBeInstanceOf(Date);
267
- expect(retrievedThread?.createdAt.toISOString()).toBe(now.toISOString());
268
- expect(retrievedThread?.updatedAt.toISOString()).toBe(now.toISOString());
269
- });
270
-
271
- it('should handle mixed date formats in thread operations', async () => {
272
- const now = new Date();
273
- const thread = createSampleThread({ date: now });
274
-
275
- await store.saveThread({ thread });
276
- const retrievedThread = await store.getThreadById({ threadId: thread.id });
277
- expect(retrievedThread?.createdAt).toBeInstanceOf(Date);
278
- expect(retrievedThread?.updatedAt).toBeInstanceOf(Date);
279
- expect(retrievedThread?.createdAt.toISOString()).toBe(now.toISOString());
280
- expect(retrievedThread?.updatedAt.toISOString()).toBe(now.toISOString());
281
- });
282
-
283
- it('should handle date serialization in getThreadsByResourceId', async () => {
284
- const now = new Date();
285
- const thread1 = createSampleThread({ date: now });
286
- const thread2 = { ...createSampleThread({ date: now }), resourceId: thread1.resourceId };
287
- const threads = [thread1, thread2];
288
-
289
- await Promise.all(threads.map(thread => store.saveThread({ thread })));
290
-
291
- const retrievedThreads = await store.getThreadsByResourceId({ resourceId: threads[0].resourceId });
292
- expect(retrievedThreads).toHaveLength(2);
293
- retrievedThreads.forEach(thread => {
294
- expect(thread.createdAt).toBeInstanceOf(Date);
295
- expect(thread.updatedAt).toBeInstanceOf(Date);
296
- expect(thread.createdAt.toISOString()).toBe(now.toISOString());
297
- expect(thread.updatedAt.toISOString()).toBe(now.toISOString());
298
- });
299
- });
300
- });
301
-
302
- describe('Message Operations', () => {
303
- const threadId = 'test-thread';
304
-
305
- beforeEach(async () => {
306
- await store.clearTable({ tableName: TABLE_MESSAGES });
307
- await store.clearTable({ tableName: TABLE_THREADS });
308
-
309
- // Create a test thread
310
- await store.saveThread({
311
- thread: {
312
- id: threadId,
313
- resourceId: 'resource-1',
314
- title: 'Test Thread',
315
- createdAt: new Date(),
316
- updatedAt: new Date(),
317
- metadata: {},
318
- },
319
- });
320
- });
321
-
322
- it('should save and retrieve messages in order', async () => {
323
- const messages: MastraMessageV2[] = [
324
- createSampleMessageV2({ threadId, content: { content: 'First' } }),
325
- createSampleMessageV2({ threadId, content: { content: 'Second' } }),
326
- createSampleMessageV2({ threadId, content: { content: 'Third' } }),
327
- ];
328
-
329
- await store.saveMessages({ messages, format: 'v2' });
330
-
331
- const retrievedMessages = await store.getMessages({ threadId, format: 'v2' });
332
- expect(retrievedMessages).toHaveLength(3);
333
- expect(retrievedMessages.map((m: any) => m.content.parts[0].text)).toEqual(['First', 'Second', 'Third']);
334
- });
335
-
336
- it('should retrieve messages w/ next/prev messages by message id + resource id', async () => {
337
- const thread = createSampleThread({ id: 'thread-one' });
338
- await store.saveThread({ thread });
339
-
340
- const thread2 = createSampleThread({ id: 'thread-two' });
341
- await store.saveThread({ thread: thread2 });
342
-
343
- const thread3 = createSampleThread({ id: 'thread-three' });
344
- await store.saveThread({ thread: thread3 });
345
-
346
- const messages: MastraMessageV2[] = [
347
- createSampleMessageV2({
348
- threadId: 'thread-one',
349
- content: { content: 'First' },
350
- resourceId: 'cross-thread-resource',
351
- }),
352
- createSampleMessageV2({
353
- threadId: 'thread-one',
354
- content: { content: 'Second' },
355
- resourceId: 'cross-thread-resource',
356
- }),
357
- createSampleMessageV2({
358
- threadId: 'thread-one',
359
- content: { content: 'Third' },
360
- resourceId: 'cross-thread-resource',
361
- }),
362
-
363
- createSampleMessageV2({
364
- threadId: 'thread-two',
365
- content: { content: 'Fourth' },
366
- resourceId: 'cross-thread-resource',
367
- }),
368
- createSampleMessageV2({
369
- threadId: 'thread-two',
370
- content: { content: 'Fifth' },
371
- resourceId: 'cross-thread-resource',
372
- }),
373
- createSampleMessageV2({
374
- threadId: 'thread-two',
375
- content: { content: 'Sixth' },
376
- resourceId: 'cross-thread-resource',
377
- }),
378
-
379
- createSampleMessageV2({
380
- threadId: 'thread-three',
381
- content: { content: 'Seventh' },
382
- resourceId: 'other-resource',
383
- }),
384
- createSampleMessageV2({
385
- threadId: 'thread-three',
386
- content: { content: 'Eighth' },
387
- resourceId: 'other-resource',
388
- }),
389
- ];
390
-
391
- await store.saveMessages({ messages: messages, format: 'v2' });
392
-
393
- const retrievedMessages = await store.getMessages({ threadId: 'thread-one', format: 'v2' });
394
- expect(retrievedMessages).toHaveLength(3);
395
- expect(retrievedMessages.map((m: any) => m.content.parts[0].text)).toEqual(['First', 'Second', 'Third']);
396
-
397
- const retrievedMessages2 = await store.getMessages({ threadId: 'thread-two', format: 'v2' });
398
- expect(retrievedMessages2).toHaveLength(3);
399
- expect(retrievedMessages2.map((m: any) => m.content.parts[0].text)).toEqual(['Fourth', 'Fifth', 'Sixth']);
400
-
401
- const retrievedMessages3 = await store.getMessages({ threadId: 'thread-three', format: 'v2' });
402
- expect(retrievedMessages3).toHaveLength(2);
403
- expect(retrievedMessages3.map((m: any) => m.content.parts[0].text)).toEqual(['Seventh', 'Eighth']);
404
-
405
- const crossThreadMessages = await store.getMessages({
406
- threadId: 'thread-doesnt-exist',
407
- format: 'v2',
408
- selectBy: {
409
- last: 0,
410
- include: [
411
- {
412
- id: messages[1].id,
413
- threadId: 'thread-one',
414
- withNextMessages: 2,
415
- withPreviousMessages: 2,
416
- },
417
- {
418
- id: messages[4].id,
419
- threadId: 'thread-two',
420
- withPreviousMessages: 2,
421
- withNextMessages: 2,
422
- },
423
- ],
424
- },
425
- });
426
-
427
- expect(crossThreadMessages).toHaveLength(6);
428
- expect(crossThreadMessages.filter(m => m.threadId === `thread-one`)).toHaveLength(3);
429
- expect(crossThreadMessages.filter(m => m.threadId === `thread-two`)).toHaveLength(3);
430
-
431
- const crossThreadMessages2 = await store.getMessages({
432
- threadId: 'thread-one',
433
- format: 'v2',
434
- selectBy: {
435
- last: 0,
436
- include: [
437
- {
438
- id: messages[4].id,
439
- threadId: 'thread-two',
440
- withPreviousMessages: 1,
441
- withNextMessages: 1,
442
- },
443
- ],
444
- },
445
- });
446
-
447
- expect(crossThreadMessages2).toHaveLength(3);
448
- expect(crossThreadMessages2.filter(m => m.threadId === `thread-one`)).toHaveLength(0);
449
- expect(crossThreadMessages2.filter(m => m.threadId === `thread-two`)).toHaveLength(3);
450
-
451
- const crossThreadMessages3 = await store.getMessages({
452
- threadId: 'thread-two',
453
- format: 'v2',
454
- selectBy: {
455
- last: 0,
456
- include: [
457
- {
458
- id: messages[1].id,
459
- threadId: 'thread-one',
460
- withNextMessages: 1,
461
- withPreviousMessages: 1,
462
- },
463
- ],
464
- },
465
- });
466
-
467
- expect(crossThreadMessages3).toHaveLength(3);
468
- expect(crossThreadMessages3.filter(m => m.threadId === `thread-one`)).toHaveLength(3);
469
- expect(crossThreadMessages3.filter(m => m.threadId === `thread-two`)).toHaveLength(0);
470
- });
471
-
472
- it('should handle empty message array', async () => {
473
- const result = await store.saveMessages({ messages: [] });
474
- expect(result).toEqual([]);
475
- });
476
-
477
- it('should handle messages with complex content', async () => {
478
- const messages = [
479
- {
480
- id: 'msg-1',
481
- threadId,
482
- role: 'user',
483
- content: {
484
- format: 2,
485
- parts: [
486
- { type: 'text', text: 'Message with' },
487
- { type: 'code', text: 'code block', language: 'typescript' },
488
- { type: 'text', text: 'and more text' },
489
- ],
490
- },
491
- createdAt: new Date(),
492
- },
493
- ] as MastraMessageV2[];
494
-
495
- await store.saveMessages({ messages, format: 'v2' });
496
-
497
- const retrievedMessages = await store.getMessages({ threadId, format: 'v2' });
498
- expect(retrievedMessages[0].content).toEqual(messages[0].content);
499
- });
500
-
501
- it('should upsert messages: duplicate id+threadId results in update, not duplicate row', async () => {
502
- const thread = await createSampleThread();
503
- await store.saveThread({ thread });
504
- const baseMessage = createSampleMessageV2({
505
- threadId: thread.id,
506
- createdAt: new Date(),
507
- content: { content: 'Original' },
508
- resourceId: thread.resourceId,
509
- });
510
-
511
- // Insert the message for the first time
512
- await store.saveMessages({ messages: [baseMessage], format: 'v2' });
513
-
514
- // Insert again with the same id and threadId but different content
515
- const updatedMessage = {
516
- ...createSampleMessageV2({
517
- threadId: thread.id,
518
- createdAt: new Date(),
519
- content: { content: 'Updated' },
520
- resourceId: thread.resourceId,
521
- }),
522
- id: baseMessage.id,
523
- };
524
-
525
- await store.saveMessages({ messages: [updatedMessage], format: 'v2' });
526
-
527
- // Retrieve messages for the thread
528
- const retrievedMessages = await store.getMessages({ threadId: thread.id, format: 'v2' });
529
-
530
- // Only one message should exist for that id+threadId
531
- expect(retrievedMessages.filter(m => m.id === baseMessage.id)).toHaveLength(1);
532
-
533
- // The content should be the updated one
534
- expect(retrievedMessages.find(m => m.id === baseMessage.id)?.content.content).toBe('Updated');
535
- });
536
-
537
- it('should upsert messages: duplicate id and different threadid', async () => {
538
- const thread1 = await createSampleThread();
539
- const thread2 = await createSampleThread();
540
- await store.saveThread({ thread: thread1 });
541
- await store.saveThread({ thread: thread2 });
542
-
543
- const message = createSampleMessageV2({
544
- threadId: thread1.id,
545
- createdAt: new Date(),
546
- content: { content: 'Thread1 Content' },
547
- resourceId: thread1.resourceId,
548
- });
549
-
550
- // Insert message into thread1
551
- await store.saveMessages({ messages: [message], format: 'v2' });
552
-
553
- // Attempt to insert a message with the same id but different threadId
554
- const conflictingMessage = {
555
- ...createSampleMessageV2({
556
- threadId: thread2.id, // different thread
557
- content: { content: 'Thread2 Content' },
558
- resourceId: thread2.resourceId,
559
- }),
560
- id: message.id,
561
- };
562
-
563
- // Save should move the message to the new thread
564
- await store.saveMessages({ messages: [conflictingMessage], format: 'v2' });
565
-
566
- // Retrieve messages for both threads
567
- const thread1Messages = await store.getMessages({ threadId: thread1.id, format: 'v2' });
568
- const thread2Messages = await store.getMessages({ threadId: thread2.id, format: 'v2' });
569
-
570
- // Thread 1 should NOT have the message with that id
571
- expect(thread1Messages.find(m => m.id === message.id)).toBeUndefined();
572
-
573
- // Thread 2 should have the message with that id
574
- expect(thread2Messages.find(m => m.id === message.id)?.content.content).toBe('Thread2 Content');
575
- });
576
- describe('getMessagesPaginated', () => {
577
- it('should return paginated messages with total count', async () => {
578
- const thread = createSampleThread();
579
- await store.saveThread({ thread });
580
-
581
- const messages = Array.from({ length: 15 }, (_, i) =>
582
- createSampleMessageV2({ threadId: thread.id, content: { content: `Message ${i + 1}` } }),
583
- );
584
-
585
- await store.saveMessages({ messages, format: 'v2' });
586
-
587
- const page1 = await store.getMessagesPaginated({
588
- threadId: thread.id,
589
- selectBy: { pagination: { page: 0, perPage: 5 } },
590
- format: 'v2',
591
- });
592
- expect(page1.messages).toHaveLength(5);
593
- expect(page1.total).toBe(15);
594
- expect(page1.page).toBe(0);
595
- expect(page1.perPage).toBe(5);
596
- expect(page1.hasMore).toBe(true);
597
-
598
- const page3 = await store.getMessagesPaginated({
599
- threadId: thread.id,
600
- selectBy: { pagination: { page: 2, perPage: 5 } },
601
- format: 'v2',
602
- });
603
- expect(page3.messages).toHaveLength(5);
604
- expect(page3.total).toBe(15);
605
- expect(page3.hasMore).toBe(false);
606
-
607
- const page4 = await store.getMessagesPaginated({
608
- threadId: thread.id,
609
- selectBy: { pagination: { page: 3, perPage: 5 } },
610
- format: 'v2',
611
- });
612
- expect(page4.messages).toHaveLength(0);
613
- expect(page4.total).toBe(15);
614
- expect(page4.hasMore).toBe(false);
615
- });
616
-
617
- it('should maintain chronological order in pagination', async () => {
618
- const thread = createSampleThread();
619
- await store.saveThread({ thread });
620
-
621
- const messages = Array.from({ length: 10 }, (_, i) => {
622
- const message = createSampleMessageV2({ threadId: thread.id, content: { content: `Message ${i + 1}` } });
623
- // Ensure different timestamps
624
- message.createdAt = new Date(Date.now() + i * 1000);
625
- return message;
626
- });
627
-
628
- await store.saveMessages({ messages, format: 'v2' });
629
-
630
- const page1 = await store.getMessagesPaginated({
631
- threadId: thread.id,
632
- selectBy: { pagination: { page: 0, perPage: 3 } },
633
- format: 'v2',
634
- });
635
-
636
- // Check that messages are in chronological order
637
- for (let i = 1; i < page1.messages.length; i++) {
638
- const prevMessage = page1.messages[i - 1] as MastraMessageV2;
639
- const currentMessage = page1.messages[i] as MastraMessageV2;
640
- expect(new Date(prevMessage.createdAt).getTime()).toBeLessThanOrEqual(
641
- new Date(currentMessage.createdAt).getTime(),
642
- );
643
- }
644
- });
645
-
646
- it('should support date filtering with pagination', async () => {
647
- const thread = createSampleThread();
648
- await store.saveThread({ thread });
649
-
650
- const now = new Date();
651
- const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
652
- const tomorrow = new Date(now.getTime() + 24 * 60 * 60 * 1000);
653
-
654
- const oldMessages = Array.from({ length: 3 }, (_, i) => {
655
- const message = createSampleMessageV2({ threadId: thread.id, content: { content: `Old Message ${i + 1}` } });
656
- message.createdAt = yesterday;
657
- return message;
658
- });
659
-
660
- const newMessages = Array.from({ length: 4 }, (_, i) => {
661
- const message = createSampleMessageV2({ threadId: thread.id, content: { content: `New Message ${i + 1}` } });
662
- message.createdAt = tomorrow;
663
- return message;
664
- });
665
-
666
- await store.saveMessages({ messages: [...oldMessages, ...newMessages], format: 'v2' });
667
-
668
- const recentMessages = await store.getMessagesPaginated({
669
- threadId: thread.id,
670
- selectBy: {
671
- pagination: {
672
- page: 0,
673
- perPage: 10,
674
- dateRange: { start: now },
675
- },
676
- },
677
- format: 'v2',
678
- });
679
- expect(recentMessages.messages).toHaveLength(4);
680
- expect(recentMessages.total).toBe(4);
681
- });
682
- });
683
- });
684
-
685
- describe('Trace Operations', () => {
686
- beforeEach(async () => {
687
- await store.clearTable({ tableName: TABLE_TRACES });
688
- });
689
-
690
- it('should retrieve traces with filtering and pagination', async () => {
691
- // Insert sample traces
692
- const trace1 = createSampleTrace('test-trace-1', 'scope1', { env: 'prod' });
693
- const trace2 = createSampleTrace('test-trace-2', 'scope1', { env: 'dev' });
694
- const trace3 = createSampleTrace('other-trace', 'scope2', { env: 'prod' });
695
-
696
- await store.insert({ tableName: TABLE_TRACES, record: trace1 });
697
- await store.insert({ tableName: TABLE_TRACES, record: trace2 });
698
- await store.insert({ tableName: TABLE_TRACES, record: trace3 });
699
-
700
- // Test name filter
701
- const testTraces = await store.getTraces({ name: 'test-trace', page: 0, perPage: 10 });
702
- expect(testTraces).toHaveLength(2);
703
- expect(testTraces.map(t => t.name)).toContain('test-trace-1');
704
- expect(testTraces.map(t => t.name)).toContain('test-trace-2');
705
-
706
- // Test scope filter
707
- const scope1Traces = await store.getTraces({ scope: 'scope1', page: 0, perPage: 10 });
708
- expect(scope1Traces).toHaveLength(2);
709
- expect(scope1Traces.every(t => t.scope === 'scope1')).toBe(true);
710
-
711
- // Test attributes filter
712
- const prodTraces = await store.getTraces({
713
- attributes: { env: 'prod' },
714
- page: 0,
715
- perPage: 10,
716
- });
717
- expect(prodTraces).toHaveLength(2);
718
- expect(prodTraces.every(t => t.attributes.env === 'prod')).toBe(true);
719
-
720
- // Test pagination
721
- const pagedTraces = await store.getTraces({ page: 0, perPage: 2 });
722
- expect(pagedTraces).toHaveLength(2);
723
-
724
- // Test combined filters
725
- const combinedTraces = await store.getTraces({
726
- scope: 'scope1',
727
- attributes: { env: 'prod' },
728
- page: 0,
729
- perPage: 10,
730
- });
731
- expect(combinedTraces).toHaveLength(1);
732
- expect(combinedTraces[0].name).toBe('test-trace-1');
733
-
734
- // Verify trace object structure
735
- const trace = combinedTraces[0];
736
- expect(trace).toHaveProperty('id');
737
- expect(trace).toHaveProperty('parentSpanId');
738
- expect(trace).toHaveProperty('traceId');
739
- expect(trace).toHaveProperty('name');
740
- expect(trace).toHaveProperty('scope');
741
- expect(trace).toHaveProperty('kind');
742
- expect(trace).toHaveProperty('status');
743
- expect(trace).toHaveProperty('events');
744
- expect(trace).toHaveProperty('links');
745
- expect(trace).toHaveProperty('attributes');
746
- expect(trace).toHaveProperty('startTime');
747
- expect(trace).toHaveProperty('endTime');
748
- expect(trace).toHaveProperty('other');
749
- expect(trace).toHaveProperty('createdAt');
750
-
751
- // Verify JSON fields are parsed
752
- expect(typeof trace.status).toBe('object');
753
- expect(typeof trace.events).toBe('object');
754
- expect(typeof trace.links).toBe('object');
755
- expect(typeof trace.attributes).toBe('object');
756
- expect(typeof trace.other).toBe('object');
757
- });
758
-
759
- it('should handle empty results', async () => {
760
- const traces = await store.getTraces({ page: 0, perPage: 10 });
761
- expect(traces).toHaveLength(0);
762
- });
763
-
764
- it('should handle invalid JSON in fields', async () => {
765
- const trace = createSampleTrace('test-trace');
766
- trace.status = 'invalid-json{'; // Intentionally invalid JSON
767
-
768
- await store.insert({ tableName: TABLE_TRACES, record: trace });
769
- const traces = await store.getTraces({ page: 0, perPage: 10 });
770
-
771
- expect(traces).toHaveLength(1);
772
- expect(traces[0].status).toBe('invalid-json{'); // Should return raw string when JSON parsing fails
773
- });
774
- });
775
-
776
- describe('Workflow Operations', () => {
777
- const testNamespace = 'test';
778
- const testWorkflow = 'test-workflow';
779
- const testRunId = 'test-run';
780
-
781
- beforeEach(async () => {
782
- await store.clearTable({ tableName: TABLE_WORKFLOW_SNAPSHOT });
783
- });
784
-
785
- it('should persist and load workflow snapshots', async () => {
786
- const mockSnapshot = {
787
- value: { step1: 'completed' },
788
- context: {
789
- step1: {
790
- status: 'success',
791
- output: { result: 'done' },
792
- payload: {},
793
- startedAt: new Date().getTime(),
794
- endedAt: new Date(Date.now() + 15000).getTime(),
795
- },
796
- } as WorkflowRunState['context'],
797
- serializedStepGraph: [],
798
- runId: testRunId,
799
- activePaths: [],
800
- suspendedPaths: {},
801
- timestamp: Date.now(),
802
- status: 'success',
803
- };
804
-
805
- await store.persistWorkflowSnapshot({
806
- namespace: testNamespace,
807
- workflowName: testWorkflow,
808
- runId: testRunId,
809
- snapshot: mockSnapshot as WorkflowRunState,
810
- });
811
-
812
- const loadedSnapshot = await store.loadWorkflowSnapshot({
813
- namespace: testNamespace,
814
- workflowName: testWorkflow,
815
- runId: testRunId,
816
- });
817
-
818
- expect(loadedSnapshot).toEqual(mockSnapshot);
819
- });
820
-
821
- it('should return null for non-existent snapshot', async () => {
822
- const result = await store.loadWorkflowSnapshot({
823
- namespace: testNamespace,
824
- workflowName: 'non-existent',
825
- runId: 'non-existent',
826
- });
827
- expect(result).toBeNull();
828
- });
829
- });
830
-
831
- describe('Eval Operations', () => {
832
- beforeEach(async () => {
833
- await store.clearTable({ tableName: TABLE_EVALS });
834
- });
835
-
836
- it('should retrieve evals by agent name', async () => {
837
- const agentName = `test-agent-${randomUUID()}`;
838
-
839
- // Create sample evals
840
- const liveEval = createSampleEval(agentName, false);
841
- const testEval = createSampleEval(agentName, true);
842
- const otherAgentEval = createSampleEval(`other-agent-${randomUUID()}`, false);
843
-
844
- // Insert evals
845
- await store.insert({
846
- tableName: TABLE_EVALS,
847
- record: liveEval,
848
- });
849
-
850
- await store.insert({
851
- tableName: TABLE_EVALS,
852
- record: testEval,
853
- });
854
-
855
- await store.insert({
856
- tableName: TABLE_EVALS,
857
- record: otherAgentEval,
858
- });
859
-
860
- // Test getting all evals for the agent
861
- const allEvals = await store.getEvalsByAgentName(agentName);
862
- expect(allEvals).toHaveLength(2);
863
- expect(allEvals.map(e => e.runId)).toEqual(expect.arrayContaining([liveEval.run_id, testEval.run_id]));
864
-
865
- // Test getting only live evals
866
- const liveEvals = await store.getEvalsByAgentName(agentName, 'live');
867
- expect(liveEvals).toHaveLength(1);
868
- expect(liveEvals[0].runId).toBe(liveEval.run_id);
869
-
870
- // Test getting only test evals
871
- const testEvals = await store.getEvalsByAgentName(agentName, 'test');
872
- expect(testEvals).toHaveLength(1);
873
- expect(testEvals[0].runId).toBe(testEval.run_id);
874
-
875
- // Verify the test_info was properly parsed
876
- if (testEval.test_info) {
877
- const expectedTestInfo = JSON.parse(testEval.test_info);
878
- expect(testEvals[0].testInfo).toEqual(expectedTestInfo);
879
- }
880
-
881
- // Test getting evals for non-existent agent
882
- const nonExistentEvals = await store.getEvalsByAgentName('non-existent-agent');
883
- expect(nonExistentEvals).toHaveLength(0);
884
- });
885
- });
886
-
887
- describe('getWorkflowRuns', () => {
888
- const testNamespace = 'test-namespace';
889
- beforeEach(async () => {
890
- await store.clearTable({ tableName: TABLE_WORKFLOW_SNAPSHOT });
891
- });
892
- it('returns empty array when no workflows exist', async () => {
893
- const { runs, total } = await store.getWorkflowRuns();
894
- expect(runs).toEqual([]);
895
- expect(total).toBe(0);
896
- });
897
-
898
- it('returns all workflows by default', async () => {
899
- const workflowName1 = 'default_test_1';
900
- const workflowName2 = 'default_test_2';
901
-
902
- const { snapshot: workflow1, runId: runId1, stepId: stepId1 } = createSampleWorkflowSnapshot('success');
903
- const { snapshot: workflow2, runId: runId2, stepId: stepId2 } = createSampleWorkflowSnapshot('waiting');
904
-
905
- await store.persistWorkflowSnapshot({
906
- namespace: testNamespace,
907
- workflowName: workflowName1,
908
- runId: runId1,
909
- snapshot: workflow1,
910
- });
911
- await new Promise(resolve => setTimeout(resolve, 10)); // Small delay to ensure different timestamps
912
- await store.persistWorkflowSnapshot({
913
- namespace: testNamespace,
914
- workflowName: workflowName2,
915
- runId: runId2,
916
- snapshot: workflow2,
917
- });
918
-
919
- const { runs, total } = await store.getWorkflowRuns({ namespace: testNamespace });
920
- expect(runs).toHaveLength(2);
921
- expect(total).toBe(2);
922
- expect(runs[0]!.workflowName).toBe(workflowName2); // Most recent first
923
- expect(runs[1]!.workflowName).toBe(workflowName1);
924
- const firstSnapshot = runs[0]!.snapshot;
925
- const secondSnapshot = runs[1]!.snapshot;
926
- checkWorkflowSnapshot(firstSnapshot, stepId2, 'waiting');
927
- checkWorkflowSnapshot(secondSnapshot, stepId1, 'success');
928
- });
929
-
930
- it('filters by workflow name', async () => {
931
- const workflowName1 = 'filter_test_1';
932
- const workflowName2 = 'filter_test_2';
933
-
934
- const { snapshot: workflow1, runId: runId1, stepId: stepId1 } = createSampleWorkflowSnapshot('success');
935
- const { snapshot: workflow2, runId: runId2 } = createSampleWorkflowSnapshot('failed');
936
-
937
- await store.persistWorkflowSnapshot({
938
- namespace: testNamespace,
939
- workflowName: workflowName1,
940
- runId: runId1,
941
- snapshot: workflow1,
942
- });
943
- await new Promise(resolve => setTimeout(resolve, 10)); // Small delay to ensure different timestamps
944
- await store.persistWorkflowSnapshot({
945
- namespace: testNamespace,
946
- workflowName: workflowName2,
947
- runId: runId2,
948
- snapshot: workflow2,
949
- });
950
-
951
- const { runs, total } = await store.getWorkflowRuns({ namespace: testNamespace, workflowName: workflowName1 });
952
- expect(runs).toHaveLength(1);
953
- expect(total).toBe(1);
954
- expect(runs[0]!.workflowName).toBe(workflowName1);
955
- const snapshot = runs[0]!.snapshot;
956
- checkWorkflowSnapshot(snapshot, stepId1, 'success');
957
- });
958
-
959
- it('filters by date range', async () => {
960
- const now = new Date();
961
- const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
962
- const twoDaysAgo = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000);
963
- const workflowName1 = 'date_test_1';
964
- const workflowName2 = 'date_test_2';
965
- const workflowName3 = 'date_test_3';
966
-
967
- const { snapshot: workflow1, runId: runId1 } = createSampleWorkflowSnapshot('success');
968
- const { snapshot: workflow2, runId: runId2, stepId: stepId2 } = createSampleWorkflowSnapshot('waiting');
969
- const { snapshot: workflow3, runId: runId3, stepId: stepId3 } = createSampleWorkflowSnapshot('skipped');
970
-
971
- await store.insert({
972
- tableName: TABLE_WORKFLOW_SNAPSHOT,
973
- record: {
974
- namespace: testNamespace,
975
- workflow_name: workflowName1,
976
- run_id: runId1,
977
- snapshot: workflow1,
978
- createdAt: twoDaysAgo,
979
- updatedAt: twoDaysAgo,
980
- },
981
- });
982
- await store.insert({
983
- tableName: TABLE_WORKFLOW_SNAPSHOT,
984
- record: {
985
- namespace: testNamespace,
986
- workflow_name: workflowName2,
987
- run_id: runId2,
988
- snapshot: workflow2,
989
- createdAt: yesterday,
990
- updatedAt: yesterday,
991
- },
992
- });
993
- await store.insert({
994
- tableName: TABLE_WORKFLOW_SNAPSHOT,
995
- record: {
996
- namespace: testNamespace,
997
- workflow_name: workflowName3,
998
- run_id: runId3,
999
- snapshot: workflow3,
1000
- createdAt: now,
1001
- updatedAt: now,
1002
- },
1003
- });
1004
-
1005
- const { runs } = await store.getWorkflowRuns({
1006
- namespace: testNamespace,
1007
- fromDate: yesterday,
1008
- toDate: now,
1009
- });
1010
-
1011
- expect(runs).toHaveLength(2);
1012
- expect(runs[0]!.workflowName).toBe(workflowName3);
1013
- expect(runs[1]!.workflowName).toBe(workflowName2);
1014
- const firstSnapshot = runs[0]!.snapshot;
1015
- const secondSnapshot = runs[1]!.snapshot;
1016
- checkWorkflowSnapshot(firstSnapshot, stepId3, 'skipped');
1017
- checkWorkflowSnapshot(secondSnapshot, stepId2, 'waiting');
1018
- });
1019
-
1020
- it('handles pagination', async () => {
1021
- const workflowName1 = 'page_test_1';
1022
- const workflowName2 = 'page_test_2';
1023
- const workflowName3 = 'page_test_3';
1024
-
1025
- const { snapshot: workflow1, runId: runId1, stepId: stepId1 } = createSampleWorkflowSnapshot('success');
1026
- const { snapshot: workflow2, runId: runId2, stepId: stepId2 } = createSampleWorkflowSnapshot('waiting');
1027
- const { snapshot: workflow3, runId: runId3, stepId: stepId3 } = createSampleWorkflowSnapshot('skipped');
1028
-
1029
- await store.persistWorkflowSnapshot({
1030
- namespace: testNamespace,
1031
- workflowName: workflowName1,
1032
- runId: runId1,
1033
- snapshot: workflow1,
1034
- });
1035
- await new Promise(resolve => setTimeout(resolve, 10)); // Small delay to ensure different timestamps
1036
- await store.persistWorkflowSnapshot({
1037
- namespace: testNamespace,
1038
- workflowName: workflowName2,
1039
- runId: runId2,
1040
- snapshot: workflow2,
1041
- });
1042
- await new Promise(resolve => setTimeout(resolve, 10)); // Small delay to ensure different timestamps
1043
- await store.persistWorkflowSnapshot({
1044
- namespace: testNamespace,
1045
- workflowName: workflowName3,
1046
- runId: runId3,
1047
- snapshot: workflow3,
1048
- });
1049
-
1050
- // Get first page
1051
- const page1 = await store.getWorkflowRuns({
1052
- namespace: testNamespace,
1053
- limit: 2,
1054
- offset: 0,
1055
- });
1056
- expect(page1.runs).toHaveLength(2);
1057
- expect(page1.total).toBe(3); // Total count of all records
1058
- expect(page1.runs[0]!.workflowName).toBe(workflowName3);
1059
- expect(page1.runs[1]!.workflowName).toBe(workflowName2);
1060
- const firstSnapshot = page1.runs[0]!.snapshot;
1061
- const secondSnapshot = page1.runs[1]!.snapshot;
1062
- checkWorkflowSnapshot(firstSnapshot, stepId3, 'skipped');
1063
- checkWorkflowSnapshot(secondSnapshot, stepId2, 'waiting');
1064
-
1065
- // Get second page
1066
- const page2 = await store.getWorkflowRuns({
1067
- namespace: testNamespace,
1068
- limit: 2,
1069
- offset: 2,
1070
- });
1071
- expect(page2.runs).toHaveLength(1);
1072
- expect(page2.total).toBe(3);
1073
- expect(page2.runs[0]!.workflowName).toBe(workflowName1);
1074
- const snapshot = page2.runs[0]!.snapshot;
1075
- checkWorkflowSnapshot(snapshot, stepId1, 'success');
1076
- });
1077
- });
1078
- describe('getWorkflowRunById', () => {
1079
- const testNamespace = 'test-workflows-id';
1080
- const workflowName = 'workflow-id-test';
1081
- let runId: string;
1082
- let stepId: string;
1083
-
1084
- beforeAll(async () => {
1085
- // Insert a workflow run for positive test
1086
- const sample = createSampleWorkflowSnapshot('success');
1087
- runId = sample.runId;
1088
- stepId = sample.stepId;
1089
- await store.insert({
1090
- tableName: TABLE_WORKFLOW_SNAPSHOT,
1091
- record: {
1092
- namespace: testNamespace,
1093
- workflow_name: workflowName,
1094
- run_id: runId,
1095
- resourceId: 'resource-abc',
1096
- snapshot: sample.snapshot,
1097
- createdAt: new Date(),
1098
- updatedAt: new Date(),
1099
- },
1100
- });
1101
- });
1102
-
1103
- it('should retrieve a workflow run by ID', async () => {
1104
- const found = await store.getWorkflowRunById({
1105
- namespace: testNamespace,
1106
- runId,
1107
- workflowName,
1108
- });
1109
- expect(found).not.toBeNull();
1110
- expect(found?.runId).toBe(runId);
1111
- const snapshot = found?.snapshot;
1112
- checkWorkflowSnapshot(snapshot!, stepId, 'success');
1113
- });
1114
-
1115
- it('should return null for non-existent workflow run ID', async () => {
1116
- const notFound = await store.getWorkflowRunById({
1117
- namespace: testNamespace,
1118
- runId: 'non-existent-id',
1119
- workflowName,
1120
- });
1121
- expect(notFound).toBeNull();
1122
- });
1123
- });
1124
- describe('getWorkflowRuns with resourceId', () => {
1125
- const testNamespace = 'test-workflows-id';
1126
- const workflowName = 'workflow-id-test';
1127
- let resourceId: string;
1128
- let runIds: string[] = [];
1129
-
1130
- beforeAll(async () => {
1131
- // Insert multiple workflow runs for the same resourceId
1132
- resourceId = 'resource-shared';
1133
- for (const status of ['success', 'waiting']) {
1134
- const sample = createSampleWorkflowSnapshot(status);
1135
- runIds.push(sample.runId);
1136
- await store.insert({
1137
- tableName: TABLE_WORKFLOW_SNAPSHOT,
1138
- record: {
1139
- namespace: testNamespace,
1140
- workflow_name: workflowName,
1141
- run_id: sample.runId,
1142
- resourceId,
1143
- snapshot: sample.snapshot,
1144
- createdAt: new Date(),
1145
- updatedAt: new Date(),
1146
- },
1147
- });
1148
- }
1149
- // Insert a run with a different resourceId
1150
- const other = createSampleWorkflowSnapshot('waiting');
1151
- await store.insert({
1152
- tableName: TABLE_WORKFLOW_SNAPSHOT,
1153
- record: {
1154
- namespace: testNamespace,
1155
- workflow_name: workflowName,
1156
- run_id: other.runId,
1157
- resourceId: 'resource-other',
1158
- snapshot: other.snapshot,
1159
- createdAt: new Date(),
1160
- updatedAt: new Date(),
1161
- },
1162
- });
1163
- });
1164
-
1165
- it('should retrieve all workflow runs by resourceId', async () => {
1166
- const { runs } = await store.getWorkflowRuns({
1167
- namespace: testNamespace,
1168
- resourceId,
1169
- workflowName,
1170
- });
1171
- expect(Array.isArray(runs)).toBe(true);
1172
- expect(runs.length).toBeGreaterThanOrEqual(2);
1173
- for (const run of runs) {
1174
- expect(run.resourceId).toBe(resourceId);
1175
- }
1176
- });
1177
-
1178
- it('should return an empty array if no workflow runs match resourceId', async () => {
1179
- const { runs } = await store.getWorkflowRuns({
1180
- namespace: testNamespace,
1181
- resourceId: 'non-existent-resource',
1182
- workflowName,
1183
- });
1184
- expect(Array.isArray(runs)).toBe(true);
1185
- expect(runs.length).toBe(0);
1186
- });
1187
- });
1188
-
1189
- describe('alterTable (no-op/schemaless)', () => {
1190
- const TEST_TABLE = 'test_alter_table'; // Use "table" or "collection" as appropriate
1191
- beforeEach(async () => {
1192
- await store.clearTable({ tableName: TEST_TABLE as TABLE_NAMES });
1193
- });
1194
-
1195
- afterEach(async () => {
1196
- await store.clearTable({ tableName: TEST_TABLE as TABLE_NAMES });
1197
- });
1198
-
1199
- it('allows inserting records with new fields without alterTable', async () => {
1200
- await store.insert({
1201
- tableName: TEST_TABLE as TABLE_NAMES,
1202
- record: { id: '1', name: 'Alice' },
1203
- });
1204
- await store.insert({
1205
- tableName: TEST_TABLE as TABLE_NAMES,
1206
- record: { id: '2', name: 'Bob', newField: 123 },
1207
- });
1208
-
1209
- const row = await store.load<{ id: string; name: string; newField?: number }>({
1210
- tableName: TEST_TABLE as TABLE_NAMES,
1211
- keys: { id: '2' },
1212
- });
1213
- expect(row?.newField).toBe(123);
1214
- });
1215
-
1216
- it('does not throw when calling alterTable (no-op)', async () => {
1217
- await expect(
1218
- store.alterTable({
1219
- tableName: TEST_TABLE as TABLE_NAMES,
1220
- schema: {
1221
- id: { type: 'text', primaryKey: true, nullable: false },
1222
- name: { type: 'text', nullable: true },
1223
- extra: { type: 'integer', nullable: true },
1224
- },
1225
- ifNotExists: [],
1226
- }),
1227
- ).resolves.not.toThrow();
1228
- });
1229
-
1230
- it('can add multiple new fields at write time', async () => {
1231
- await store.insert({
1232
- tableName: TEST_TABLE as TABLE_NAMES,
1233
- record: { id: '3', name: 'Charlie', age: 30, city: 'Paris' },
1234
- });
1235
- const row = await store.load<{ id: string; name: string; age?: number; city?: string }>({
1236
- tableName: TEST_TABLE as TABLE_NAMES,
1237
- keys: { id: '3' },
1238
- });
1239
- expect(row?.age).toBe(30);
1240
- expect(row?.city).toBe('Paris');
1241
- });
1242
-
1243
- it('can retrieve all fields, including dynamically added ones', async () => {
1244
- await store.insert({
1245
- tableName: TEST_TABLE as TABLE_NAMES,
1246
- record: { id: '4', name: 'Dana', hobby: 'skiing' },
1247
- });
1248
- const row = await store.load<{ id: string; name: string; hobby?: string }>({
1249
- tableName: TEST_TABLE as TABLE_NAMES,
1250
- keys: { id: '4' },
1251
- });
1252
- expect(row?.hobby).toBe('skiing');
1253
- });
1254
-
1255
- it('does not restrict or error on arbitrary new fields', async () => {
1256
- await expect(
1257
- store.insert({
1258
- tableName: TEST_TABLE as TABLE_NAMES,
1259
- record: { id: '5', weirdField: { nested: true }, another: [1, 2, 3] },
1260
- }),
1261
- ).resolves.not.toThrow();
1262
-
1263
- const row = await store.load<{ id: string; weirdField?: any; another?: any }>({
1264
- tableName: TEST_TABLE as TABLE_NAMES,
1265
- keys: { id: '5' },
1266
- });
1267
- expect(row?.weirdField).toEqual({ nested: true });
1268
- expect(row?.another).toEqual([1, 2, 3]);
1269
- });
1270
- });
1271
-
1272
- describe('Pagination Features', () => {
1273
- beforeEach(async () => {
1274
- // Clear all test data
1275
- await store.clearTable({ tableName: TABLE_THREADS });
1276
- await store.clearTable({ tableName: TABLE_MESSAGES });
1277
- await store.clearTable({ tableName: TABLE_EVALS });
1278
- await store.clearTable({ tableName: TABLE_TRACES });
1279
- });
1280
-
1281
- describe('getEvals with pagination', () => {
1282
- it('should return paginated evals with total count', async () => {
1283
- const agentName = 'test-agent';
1284
- const evals = Array.from({ length: 25 }, (_, i) => createSampleEval(agentName, i % 2 === 0));
1285
-
1286
- // Insert all evals
1287
- for (const evalRecord of evals) {
1288
- await store.insert({
1289
- tableName: TABLE_EVALS,
1290
- record: evalRecord,
1291
- });
1292
- }
1293
-
1294
- // Test page-based pagination
1295
- const page1 = await store.getEvals({ agentName, page: 0, perPage: 10 });
1296
- expect(page1.evals).toHaveLength(10);
1297
- expect(page1.total).toBe(25);
1298
- expect(page1.page).toBe(0);
1299
- expect(page1.perPage).toBe(10);
1300
- expect(page1.hasMore).toBe(true);
1301
-
1302
- const page2 = await store.getEvals({ agentName, page: 1, perPage: 10 });
1303
- expect(page2.evals).toHaveLength(10);
1304
- expect(page2.total).toBe(25);
1305
- expect(page2.hasMore).toBe(true);
1306
-
1307
- const page3 = await store.getEvals({ agentName, page: 2, perPage: 10 });
1308
- expect(page3.evals).toHaveLength(5);
1309
- expect(page3.total).toBe(25);
1310
- expect(page3.hasMore).toBe(false);
1311
- });
1312
-
1313
- it('should support page/perPage pagination', async () => {
1314
- const agentName = 'test-agent-2';
1315
- const evals = Array.from({ length: 15 }, () => createSampleEval(agentName));
1316
-
1317
- for (const evalRecord of evals) {
1318
- await store.insert({
1319
- tableName: TABLE_EVALS,
1320
- record: evalRecord,
1321
- });
1322
- }
1323
-
1324
- // Test offset-based pagination
1325
- const result1 = await store.getEvals({ agentName, page: 0, perPage: 5 });
1326
- expect(result1.evals).toHaveLength(5);
1327
- expect(result1.total).toBe(15);
1328
- expect(result1.hasMore).toBe(true);
1329
-
1330
- const result2 = await store.getEvals({ agentName, page: 2, perPage: 5 });
1331
- expect(result2.evals).toHaveLength(5);
1332
- expect(result2.total).toBe(15);
1333
- expect(result2.hasMore).toBe(false);
1334
- });
1335
-
1336
- it('should filter by type with pagination', async () => {
1337
- const agentName = 'test-agent-3';
1338
- const testEvals = Array.from({ length: 10 }, () => createSampleEval(agentName, true));
1339
- const liveEvals = Array.from({ length: 8 }, () => createSampleEval(agentName, false));
1340
-
1341
- for (const evalRecord of [...testEvals, ...liveEvals]) {
1342
- await store.insert({
1343
- tableName: TABLE_EVALS,
1344
- record: evalRecord,
1345
- });
1346
- }
1347
-
1348
- const testResults = await store.getEvals({ agentName, type: 'test', page: 0, perPage: 5 });
1349
- expect(testResults.evals).toHaveLength(5);
1350
- expect(testResults.total).toBe(10);
1351
-
1352
- const liveResults = await store.getEvals({ agentName, type: 'live', page: 0, perPage: 5 });
1353
- expect(liveResults.evals).toHaveLength(5);
1354
- expect(liveResults.total).toBe(8);
1355
- });
1356
-
1357
- it('should filter by date with pagination', async () => {
1358
- const agentName = 'test-agent-date';
1359
- const now = new Date();
1360
- const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
1361
- const evals = [createSampleEval(agentName, false, now), createSampleEval(agentName, false, yesterday)];
1362
- for (const evalRecord of evals) {
1363
- await store.insert({
1364
- tableName: TABLE_EVALS,
1365
- record: evalRecord,
1366
- });
1367
- }
1368
- const result = await store.getEvals({
1369
- agentName,
1370
- page: 0,
1371
- perPage: 10,
1372
- dateRange: { start: now },
1373
- });
1374
- expect(result.evals).toHaveLength(1);
1375
- expect(result.total).toBe(1);
1376
- });
1377
- });
1378
-
1379
- describe('getTraces with pagination', () => {
1380
- it('should return paginated traces with total count', async () => {
1381
- const traces = Array.from({ length: 18 }, (_, i) => createSampleTrace(`test-trace-${i}`, 'test-scope'));
1382
-
1383
- for (const trace of traces) {
1384
- await store.insert({
1385
- tableName: TABLE_TRACES,
1386
- record: trace,
1387
- });
1388
- }
1389
-
1390
- const page1 = await store.getTracesPaginated({
1391
- scope: 'test-scope',
1392
- page: 0,
1393
- perPage: 8,
1394
- });
1395
- expect(page1.traces).toHaveLength(8);
1396
- expect(page1.total).toBe(18);
1397
- expect(page1.page).toBe(0);
1398
- expect(page1.perPage).toBe(8);
1399
- expect(page1.hasMore).toBe(true);
1400
-
1401
- const page3 = await store.getTracesPaginated({
1402
- scope: 'test-scope',
1403
- page: 2,
1404
- perPage: 8,
1405
- });
1406
- expect(page3.traces).toHaveLength(2);
1407
- expect(page3.total).toBe(18);
1408
- expect(page3.hasMore).toBe(false);
1409
- });
1410
-
1411
- it('should filter by date with pagination', async () => {
1412
- const scope = 'test-scope-date';
1413
- const now = new Date();
1414
- const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
1415
-
1416
- const traces = [
1417
- createSampleTrace(`test-trace-now`, scope, undefined, now),
1418
- createSampleTrace(`test-trace-yesterday`, scope, undefined, yesterday),
1419
- ];
1420
-
1421
- for (const trace of traces) {
1422
- await store.insert({
1423
- tableName: TABLE_TRACES,
1424
- record: trace,
1425
- });
1426
- }
1427
-
1428
- const result = await store.getTracesPaginated({
1429
- scope,
1430
- page: 0,
1431
- perPage: 10,
1432
- dateRange: { start: now },
1433
- });
1434
-
1435
- expect(result.traces).toHaveLength(1);
1436
- expect(result.traces[0].name).toBe('test-trace-now');
1437
- expect(result.total).toBe(1);
1438
- });
1439
- });
1440
-
1441
- describe('Enhanced existing methods with pagination', () => {
1442
- it('should support pagination in getThreadsByResourceId', async () => {
1443
- const resourceId = 'enhanced-resource';
1444
- const threads = Array.from({ length: 17 }, () => createSampleThread({ resourceId }));
1445
-
1446
- for (const thread of threads) {
1447
- await store.saveThread({ thread });
1448
- }
1449
-
1450
- const page1 = await store.getThreadsByResourceIdPaginated({ resourceId, page: 0, perPage: 7 });
1451
- expect(page1.threads).toHaveLength(7);
1452
-
1453
- const page3 = await store.getThreadsByResourceIdPaginated({ resourceId, page: 2, perPage: 7 });
1454
- expect(page3.threads).toHaveLength(3);
1455
-
1456
- const limited = await store.getThreadsByResourceIdPaginated({ resourceId, page: 1, perPage: 5 });
1457
- expect(limited.threads).toHaveLength(5);
1458
- });
1459
- });
1460
- });
1461
- });