@mastra/upstash 0.0.0-vnextWorkflows-20250422142014 → 0.0.0-workflow-deno-20250616130925
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.
- package/.turbo/turbo-build.log +39 -0
- package/CHANGELOG.md +563 -2
- package/PAGINATION.md +397 -0
- package/dist/_tsup-dts-rollup.d.cts +150 -48
- package/dist/_tsup-dts-rollup.d.ts +150 -48
- package/dist/chunk-HSTZWXH7.js +1666 -0
- package/dist/chunk-IGKEDEDE.js +12 -0
- package/dist/chunk-N2CPQVE3.cjs +35 -0
- package/dist/chunk-U74OJRHU.cjs +1678 -0
- package/dist/getMachineId-bsd-HDZ73WR7.cjs +30 -0
- package/dist/getMachineId-bsd-KKIDU47O.js +28 -0
- package/dist/getMachineId-darwin-3PL23DL6.cjs +31 -0
- package/dist/getMachineId-darwin-UTKBTJ2U.js +29 -0
- package/dist/getMachineId-linux-K3QXQYAB.js +23 -0
- package/dist/getMachineId-linux-KYLPK3HC.cjs +25 -0
- package/dist/getMachineId-unsupported-DEDJN4ZS.cjs +17 -0
- package/dist/getMachineId-unsupported-VPWBQCK7.js +15 -0
- package/dist/getMachineId-win-L2EYIM5A.js +30 -0
- package/dist/getMachineId-win-ZTI2LRDJ.cjs +52 -0
- package/dist/index.cjs +44133 -247
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +44134 -249
- package/docker-compose.yaml +3 -3
- package/package.json +16 -12
- package/src/index.ts +1 -0
- package/src/storage/index.ts +872 -304
- package/src/storage/upstash.test.ts +729 -110
- package/src/vector/index.test.ts +23 -105
- package/src/vector/index.ts +72 -50
- package/src/vector/prompt.ts +77 -0
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { randomUUID } from 'crypto';
|
|
2
|
-
import
|
|
2
|
+
import {
|
|
3
|
+
checkWorkflowSnapshot,
|
|
4
|
+
createSampleMessageV2,
|
|
5
|
+
createSampleThread,
|
|
6
|
+
createSampleWorkflowSnapshot,
|
|
7
|
+
} from '@internal/storage-test-utils';
|
|
8
|
+
import type { MastraMessageV2 } from '@mastra/core';
|
|
3
9
|
import type { TABLE_NAMES } from '@mastra/core/storage';
|
|
4
10
|
import {
|
|
5
11
|
TABLE_MESSAGES,
|
|
@@ -9,58 +15,19 @@ import {
|
|
|
9
15
|
TABLE_TRACES,
|
|
10
16
|
} from '@mastra/core/storage';
|
|
11
17
|
import type { WorkflowRunState } from '@mastra/core/workflows';
|
|
12
|
-
import { describe, it, expect, beforeAll, beforeEach, afterAll, vi } from 'vitest';
|
|
18
|
+
import { describe, it, expect, beforeAll, beforeEach, afterAll, vi, afterEach } from 'vitest';
|
|
13
19
|
|
|
14
20
|
import { UpstashStore } from './index';
|
|
15
21
|
|
|
16
22
|
// Increase timeout for all tests in this file to 30 seconds
|
|
17
|
-
vi.setConfig({ testTimeout:
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
createdAt:
|
|
24
|
-
|
|
25
|
-
metadata: { key: 'value' },
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
const createSampleMessage = (threadId: string, content: string = 'Hello') =>
|
|
29
|
-
({
|
|
30
|
-
id: `msg-${randomUUID()}`,
|
|
31
|
-
role: 'user',
|
|
32
|
-
type: 'text',
|
|
33
|
-
threadId,
|
|
34
|
-
content: [{ type: 'text', text: content }],
|
|
35
|
-
createdAt: new Date(),
|
|
36
|
-
}) as any;
|
|
37
|
-
|
|
38
|
-
const createSampleWorkflowSnapshot = (status: string, createdAt?: Date) => {
|
|
39
|
-
const runId = `run-${randomUUID()}`;
|
|
40
|
-
const stepId = `step-${randomUUID()}`;
|
|
41
|
-
const timestamp = createdAt || new Date();
|
|
42
|
-
const snapshot = {
|
|
43
|
-
result: { success: true },
|
|
44
|
-
value: {},
|
|
45
|
-
context: {
|
|
46
|
-
steps: {
|
|
47
|
-
[stepId]: {
|
|
48
|
-
status,
|
|
49
|
-
payload: {},
|
|
50
|
-
error: undefined,
|
|
51
|
-
},
|
|
52
|
-
},
|
|
53
|
-
triggerData: {},
|
|
54
|
-
attempts: {},
|
|
55
|
-
},
|
|
56
|
-
activePaths: [],
|
|
57
|
-
runId,
|
|
58
|
-
timestamp: timestamp.getTime(),
|
|
59
|
-
} as WorkflowRunState;
|
|
60
|
-
return { snapshot, runId, stepId };
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
const createSampleTrace = (name: string, scope?: string, attributes?: Record<string, string>) => ({
|
|
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
|
+
) => ({
|
|
64
31
|
id: `trace-${randomUUID()}`,
|
|
65
32
|
parentSpanId: `span-${randomUUID()}`,
|
|
66
33
|
traceId: `trace-${randomUUID()}`,
|
|
@@ -68,16 +35,16 @@ const createSampleTrace = (name: string, scope?: string, attributes?: Record<str
|
|
|
68
35
|
scope,
|
|
69
36
|
kind: 'internal',
|
|
70
37
|
status: JSON.stringify({ code: 'success' }),
|
|
71
|
-
events: JSON.stringify([{ name: 'start', timestamp:
|
|
38
|
+
events: JSON.stringify([{ name: 'start', timestamp: createdAt.getTime() }]),
|
|
72
39
|
links: JSON.stringify([]),
|
|
73
40
|
attributes: attributes ? JSON.stringify(attributes) : undefined,
|
|
74
|
-
startTime:
|
|
75
|
-
endTime: new Date().toISOString(),
|
|
41
|
+
startTime: createdAt.toISOString(),
|
|
42
|
+
endTime: new Date(createdAt.getTime() + 1000).toISOString(),
|
|
76
43
|
other: JSON.stringify({ custom: 'data' }),
|
|
77
|
-
createdAt:
|
|
44
|
+
createdAt: createdAt.toISOString(),
|
|
78
45
|
});
|
|
79
46
|
|
|
80
|
-
const createSampleEval = (agentName: string, isTest = false) => {
|
|
47
|
+
const createSampleEval = (agentName: string, isTest = false, createdAt: Date = new Date()) => {
|
|
81
48
|
const testInfo = isTest ? { testPath: 'test/path.ts', testName: 'Test Name' } : undefined;
|
|
82
49
|
|
|
83
50
|
return {
|
|
@@ -90,7 +57,7 @@ const createSampleEval = (agentName: string, isTest = false) => {
|
|
|
90
57
|
test_info: testInfo ? JSON.stringify(testInfo) : undefined,
|
|
91
58
|
global_run_id: `global-${randomUUID()}`,
|
|
92
59
|
run_id: `run-${randomUUID()}`,
|
|
93
|
-
created_at:
|
|
60
|
+
created_at: createdAt.toISOString(),
|
|
94
61
|
};
|
|
95
62
|
};
|
|
96
63
|
|
|
@@ -170,7 +137,7 @@ describe('UpstashStore', () => {
|
|
|
170
137
|
|
|
171
138
|
it('should create and retrieve a thread', async () => {
|
|
172
139
|
const now = new Date();
|
|
173
|
-
const thread = createSampleThread(now);
|
|
140
|
+
const thread = createSampleThread({ date: now });
|
|
174
141
|
|
|
175
142
|
const savedThread = await store.saveThread({ thread });
|
|
176
143
|
expect(savedThread).toEqual(thread);
|
|
@@ -190,7 +157,7 @@ describe('UpstashStore', () => {
|
|
|
190
157
|
|
|
191
158
|
it('should get threads by resource ID', async () => {
|
|
192
159
|
const thread1 = createSampleThread();
|
|
193
|
-
const thread2 =
|
|
160
|
+
const thread2 = createSampleThread({ resourceId: thread1.resourceId });
|
|
194
161
|
const threads = [thread1, thread2];
|
|
195
162
|
|
|
196
163
|
const resourceId = threads[0].resourceId;
|
|
@@ -220,6 +187,56 @@ describe('UpstashStore', () => {
|
|
|
220
187
|
updated: 'value',
|
|
221
188
|
});
|
|
222
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
|
+
});
|
|
223
240
|
});
|
|
224
241
|
|
|
225
242
|
describe('Date Handling', () => {
|
|
@@ -229,7 +246,7 @@ describe('UpstashStore', () => {
|
|
|
229
246
|
|
|
230
247
|
it('should handle Date objects in thread operations', async () => {
|
|
231
248
|
const now = new Date();
|
|
232
|
-
const thread = createSampleThread(now);
|
|
249
|
+
const thread = createSampleThread({ date: now });
|
|
233
250
|
|
|
234
251
|
await store.saveThread({ thread });
|
|
235
252
|
const retrievedThread = await store.getThreadById({ threadId: thread.id });
|
|
@@ -241,7 +258,7 @@ describe('UpstashStore', () => {
|
|
|
241
258
|
|
|
242
259
|
it('should handle ISO string dates in thread operations', async () => {
|
|
243
260
|
const now = new Date();
|
|
244
|
-
const thread = createSampleThread(now);
|
|
261
|
+
const thread = createSampleThread({ date: now });
|
|
245
262
|
|
|
246
263
|
await store.saveThread({ thread });
|
|
247
264
|
const retrievedThread = await store.getThreadById({ threadId: thread.id });
|
|
@@ -253,7 +270,7 @@ describe('UpstashStore', () => {
|
|
|
253
270
|
|
|
254
271
|
it('should handle mixed date formats in thread operations', async () => {
|
|
255
272
|
const now = new Date();
|
|
256
|
-
const thread = createSampleThread(now);
|
|
273
|
+
const thread = createSampleThread({ date: now });
|
|
257
274
|
|
|
258
275
|
await store.saveThread({ thread });
|
|
259
276
|
const retrievedThread = await store.getThreadById({ threadId: thread.id });
|
|
@@ -265,8 +282,8 @@ describe('UpstashStore', () => {
|
|
|
265
282
|
|
|
266
283
|
it('should handle date serialization in getThreadsByResourceId', async () => {
|
|
267
284
|
const now = new Date();
|
|
268
|
-
const thread1 = createSampleThread(now);
|
|
269
|
-
const thread2 = { ...createSampleThread(now), resourceId: thread1.resourceId };
|
|
285
|
+
const thread1 = createSampleThread({ date: now });
|
|
286
|
+
const thread2 = { ...createSampleThread({ date: now }), resourceId: thread1.resourceId };
|
|
270
287
|
const threads = [thread1, thread2];
|
|
271
288
|
|
|
272
289
|
await Promise.all(threads.map(thread => store.saveThread({ thread })));
|
|
@@ -303,17 +320,121 @@ describe('UpstashStore', () => {
|
|
|
303
320
|
});
|
|
304
321
|
|
|
305
322
|
it('should save and retrieve messages in order', async () => {
|
|
306
|
-
const messages = [
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
323
|
+
const messages: MastraMessageV2[] = [
|
|
324
|
+
createSampleMessageV2({ threadId, content: 'First' }),
|
|
325
|
+
createSampleMessageV2({ threadId, content: 'Second' }),
|
|
326
|
+
createSampleMessageV2({ threadId, 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({ threadId: 'thread-one', content: 'First', resourceId: 'cross-thread-resource' }),
|
|
348
|
+
createSampleMessageV2({ threadId: 'thread-one', content: 'Second', resourceId: 'cross-thread-resource' }),
|
|
349
|
+
createSampleMessageV2({ threadId: 'thread-one', content: 'Third', resourceId: 'cross-thread-resource' }),
|
|
350
|
+
|
|
351
|
+
createSampleMessageV2({ threadId: 'thread-two', content: 'Fourth', resourceId: 'cross-thread-resource' }),
|
|
352
|
+
createSampleMessageV2({ threadId: 'thread-two', content: 'Fifth', resourceId: 'cross-thread-resource' }),
|
|
353
|
+
createSampleMessageV2({ threadId: 'thread-two', content: 'Sixth', resourceId: 'cross-thread-resource' }),
|
|
354
|
+
|
|
355
|
+
createSampleMessageV2({ threadId: 'thread-three', content: 'Seventh', resourceId: 'other-resource' }),
|
|
356
|
+
createSampleMessageV2({ threadId: 'thread-three', content: 'Eighth', resourceId: 'other-resource' }),
|
|
310
357
|
];
|
|
311
358
|
|
|
312
|
-
await store.saveMessages({ messages: messages
|
|
359
|
+
await store.saveMessages({ messages: messages, format: 'v2' });
|
|
313
360
|
|
|
314
|
-
const retrievedMessages = await store.getMessages({ threadId });
|
|
361
|
+
const retrievedMessages = await store.getMessages({ threadId: 'thread-one', format: 'v2' });
|
|
315
362
|
expect(retrievedMessages).toHaveLength(3);
|
|
316
|
-
expect(retrievedMessages.map(m => m.content[0].text)).toEqual(['First', 'Second', 'Third']);
|
|
363
|
+
expect(retrievedMessages.map((m: any) => m.content.parts[0].text)).toEqual(['First', 'Second', 'Third']);
|
|
364
|
+
|
|
365
|
+
const retrievedMessages2 = await store.getMessages({ threadId: 'thread-two', format: 'v2' });
|
|
366
|
+
expect(retrievedMessages2).toHaveLength(3);
|
|
367
|
+
expect(retrievedMessages2.map((m: any) => m.content.parts[0].text)).toEqual(['Fourth', 'Fifth', 'Sixth']);
|
|
368
|
+
|
|
369
|
+
const retrievedMessages3 = await store.getMessages({ threadId: 'thread-three', format: 'v2' });
|
|
370
|
+
expect(retrievedMessages3).toHaveLength(2);
|
|
371
|
+
expect(retrievedMessages3.map((m: any) => m.content.parts[0].text)).toEqual(['Seventh', 'Eighth']);
|
|
372
|
+
|
|
373
|
+
const crossThreadMessages = await store.getMessages({
|
|
374
|
+
threadId: 'thread-doesnt-exist',
|
|
375
|
+
format: 'v2',
|
|
376
|
+
selectBy: {
|
|
377
|
+
last: 0,
|
|
378
|
+
include: [
|
|
379
|
+
{
|
|
380
|
+
id: messages[1].id,
|
|
381
|
+
threadId: 'thread-one',
|
|
382
|
+
withNextMessages: 2,
|
|
383
|
+
withPreviousMessages: 2,
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
id: messages[4].id,
|
|
387
|
+
threadId: 'thread-two',
|
|
388
|
+
withPreviousMessages: 2,
|
|
389
|
+
withNextMessages: 2,
|
|
390
|
+
},
|
|
391
|
+
],
|
|
392
|
+
},
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
expect(crossThreadMessages).toHaveLength(6);
|
|
396
|
+
expect(crossThreadMessages.filter(m => m.threadId === `thread-one`)).toHaveLength(3);
|
|
397
|
+
expect(crossThreadMessages.filter(m => m.threadId === `thread-two`)).toHaveLength(3);
|
|
398
|
+
|
|
399
|
+
const crossThreadMessages2 = await store.getMessages({
|
|
400
|
+
threadId: 'thread-one',
|
|
401
|
+
format: 'v2',
|
|
402
|
+
selectBy: {
|
|
403
|
+
last: 0,
|
|
404
|
+
include: [
|
|
405
|
+
{
|
|
406
|
+
id: messages[4].id,
|
|
407
|
+
threadId: 'thread-two',
|
|
408
|
+
withPreviousMessages: 1,
|
|
409
|
+
withNextMessages: 1,
|
|
410
|
+
},
|
|
411
|
+
],
|
|
412
|
+
},
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
expect(crossThreadMessages2).toHaveLength(3);
|
|
416
|
+
expect(crossThreadMessages2.filter(m => m.threadId === `thread-one`)).toHaveLength(0);
|
|
417
|
+
expect(crossThreadMessages2.filter(m => m.threadId === `thread-two`)).toHaveLength(3);
|
|
418
|
+
|
|
419
|
+
const crossThreadMessages3 = await store.getMessages({
|
|
420
|
+
threadId: 'thread-two',
|
|
421
|
+
format: 'v2',
|
|
422
|
+
selectBy: {
|
|
423
|
+
last: 0,
|
|
424
|
+
include: [
|
|
425
|
+
{
|
|
426
|
+
id: messages[1].id,
|
|
427
|
+
threadId: 'thread-one',
|
|
428
|
+
withNextMessages: 1,
|
|
429
|
+
withPreviousMessages: 1,
|
|
430
|
+
},
|
|
431
|
+
],
|
|
432
|
+
},
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
expect(crossThreadMessages3).toHaveLength(3);
|
|
436
|
+
expect(crossThreadMessages3.filter(m => m.threadId === `thread-one`)).toHaveLength(3);
|
|
437
|
+
expect(crossThreadMessages3.filter(m => m.threadId === `thread-two`)).toHaveLength(0);
|
|
317
438
|
});
|
|
318
439
|
|
|
319
440
|
it('should handle empty message array', async () => {
|
|
@@ -327,21 +448,131 @@ describe('UpstashStore', () => {
|
|
|
327
448
|
id: 'msg-1',
|
|
328
449
|
threadId,
|
|
329
450
|
role: 'user',
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
451
|
+
content: {
|
|
452
|
+
format: 2,
|
|
453
|
+
parts: [
|
|
454
|
+
{ type: 'text', text: 'Message with' },
|
|
455
|
+
{ type: 'code', text: 'code block', language: 'typescript' },
|
|
456
|
+
{ type: 'text', text: 'and more text' },
|
|
457
|
+
],
|
|
458
|
+
},
|
|
336
459
|
createdAt: new Date(),
|
|
337
460
|
},
|
|
338
|
-
];
|
|
461
|
+
] as MastraMessageV2[];
|
|
339
462
|
|
|
340
|
-
await store.saveMessages({ messages:
|
|
463
|
+
await store.saveMessages({ messages, format: 'v2' });
|
|
341
464
|
|
|
342
|
-
const retrievedMessages = await store.getMessages({ threadId });
|
|
465
|
+
const retrievedMessages = await store.getMessages({ threadId, format: 'v2' });
|
|
343
466
|
expect(retrievedMessages[0].content).toEqual(messages[0].content);
|
|
344
467
|
});
|
|
468
|
+
|
|
469
|
+
describe('getMessagesPaginated', () => {
|
|
470
|
+
it('should return paginated messages with total count', async () => {
|
|
471
|
+
const thread = createSampleThread();
|
|
472
|
+
await store.saveThread({ thread });
|
|
473
|
+
|
|
474
|
+
const messages = Array.from({ length: 15 }, (_, i) =>
|
|
475
|
+
createSampleMessageV2({ threadId: thread.id, content: `Message ${i + 1}` }),
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
await store.saveMessages({ messages, format: 'v2' });
|
|
479
|
+
|
|
480
|
+
const page1 = await store.getMessagesPaginated({
|
|
481
|
+
threadId: thread.id,
|
|
482
|
+
selectBy: { pagination: { page: 0, perPage: 5 } },
|
|
483
|
+
format: 'v2',
|
|
484
|
+
});
|
|
485
|
+
expect(page1.messages).toHaveLength(5);
|
|
486
|
+
expect(page1.total).toBe(15);
|
|
487
|
+
expect(page1.page).toBe(0);
|
|
488
|
+
expect(page1.perPage).toBe(5);
|
|
489
|
+
expect(page1.hasMore).toBe(true);
|
|
490
|
+
|
|
491
|
+
const page3 = await store.getMessagesPaginated({
|
|
492
|
+
threadId: thread.id,
|
|
493
|
+
selectBy: { pagination: { page: 2, perPage: 5 } },
|
|
494
|
+
format: 'v2',
|
|
495
|
+
});
|
|
496
|
+
expect(page3.messages).toHaveLength(5);
|
|
497
|
+
expect(page3.total).toBe(15);
|
|
498
|
+
expect(page3.hasMore).toBe(false);
|
|
499
|
+
|
|
500
|
+
const page4 = await store.getMessagesPaginated({
|
|
501
|
+
threadId: thread.id,
|
|
502
|
+
selectBy: { pagination: { page: 3, perPage: 5 } },
|
|
503
|
+
format: 'v2',
|
|
504
|
+
});
|
|
505
|
+
expect(page4.messages).toHaveLength(0);
|
|
506
|
+
expect(page4.total).toBe(15);
|
|
507
|
+
expect(page4.hasMore).toBe(false);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it('should maintain chronological order in pagination', async () => {
|
|
511
|
+
const thread = createSampleThread();
|
|
512
|
+
await store.saveThread({ thread });
|
|
513
|
+
|
|
514
|
+
const messages = Array.from({ length: 10 }, (_, i) => {
|
|
515
|
+
const message = createSampleMessageV2({ threadId: thread.id, content: `Message ${i + 1}` });
|
|
516
|
+
// Ensure different timestamps
|
|
517
|
+
message.createdAt = new Date(Date.now() + i * 1000);
|
|
518
|
+
return message;
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
await store.saveMessages({ messages, format: 'v2' });
|
|
522
|
+
|
|
523
|
+
const page1 = await store.getMessagesPaginated({
|
|
524
|
+
threadId: thread.id,
|
|
525
|
+
selectBy: { pagination: { page: 0, perPage: 3 } },
|
|
526
|
+
format: 'v2',
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
// Check that messages are in chronological order
|
|
530
|
+
for (let i = 1; i < page1.messages.length; i++) {
|
|
531
|
+
const prevMessage = page1.messages[i - 1] as MastraMessageV2;
|
|
532
|
+
const currentMessage = page1.messages[i] as MastraMessageV2;
|
|
533
|
+
expect(new Date(prevMessage.createdAt).getTime()).toBeLessThanOrEqual(
|
|
534
|
+
new Date(currentMessage.createdAt).getTime(),
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
it('should support date filtering with pagination', async () => {
|
|
540
|
+
const thread = createSampleThread();
|
|
541
|
+
await store.saveThread({ thread });
|
|
542
|
+
|
|
543
|
+
const now = new Date();
|
|
544
|
+
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
|
545
|
+
const tomorrow = new Date(now.getTime() + 24 * 60 * 60 * 1000);
|
|
546
|
+
|
|
547
|
+
const oldMessages = Array.from({ length: 3 }, (_, i) => {
|
|
548
|
+
const message = createSampleMessageV2({ threadId: thread.id, content: `Old Message ${i + 1}` });
|
|
549
|
+
message.createdAt = yesterday;
|
|
550
|
+
return message;
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
const newMessages = Array.from({ length: 4 }, (_, i) => {
|
|
554
|
+
const message = createSampleMessageV2({ threadId: thread.id, content: `New Message ${i + 1}` });
|
|
555
|
+
message.createdAt = tomorrow;
|
|
556
|
+
return message;
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
await store.saveMessages({ messages: [...oldMessages, ...newMessages], format: 'v2' });
|
|
560
|
+
|
|
561
|
+
const recentMessages = await store.getMessagesPaginated({
|
|
562
|
+
threadId: thread.id,
|
|
563
|
+
selectBy: {
|
|
564
|
+
pagination: {
|
|
565
|
+
page: 0,
|
|
566
|
+
perPage: 10,
|
|
567
|
+
dateRange: { start: now },
|
|
568
|
+
},
|
|
569
|
+
},
|
|
570
|
+
format: 'v2',
|
|
571
|
+
});
|
|
572
|
+
expect(recentMessages.messages).toHaveLength(4);
|
|
573
|
+
expect(recentMessages.total).toBe(4);
|
|
574
|
+
});
|
|
575
|
+
});
|
|
345
576
|
});
|
|
346
577
|
|
|
347
578
|
describe('Trace Operations', () => {
|
|
@@ -448,22 +679,27 @@ describe('UpstashStore', () => {
|
|
|
448
679
|
const mockSnapshot = {
|
|
449
680
|
value: { step1: 'completed' },
|
|
450
681
|
context: {
|
|
451
|
-
|
|
452
|
-
|
|
682
|
+
step1: {
|
|
683
|
+
status: 'success',
|
|
684
|
+
output: { result: 'done' },
|
|
685
|
+
payload: {},
|
|
686
|
+
startedAt: new Date().getTime(),
|
|
687
|
+
endedAt: new Date(Date.now() + 15000).getTime(),
|
|
453
688
|
},
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
},
|
|
689
|
+
} as WorkflowRunState['context'],
|
|
690
|
+
serializedStepGraph: [],
|
|
457
691
|
runId: testRunId,
|
|
458
692
|
activePaths: [],
|
|
693
|
+
suspendedPaths: {},
|
|
459
694
|
timestamp: Date.now(),
|
|
460
|
-
|
|
695
|
+
status: 'success',
|
|
696
|
+
};
|
|
461
697
|
|
|
462
698
|
await store.persistWorkflowSnapshot({
|
|
463
699
|
namespace: testNamespace,
|
|
464
700
|
workflowName: testWorkflow,
|
|
465
701
|
runId: testRunId,
|
|
466
|
-
snapshot: mockSnapshot,
|
|
702
|
+
snapshot: mockSnapshot as WorkflowRunState,
|
|
467
703
|
});
|
|
468
704
|
|
|
469
705
|
const loadedSnapshot = await store.loadWorkflowSnapshot({
|
|
@@ -556,8 +792,8 @@ describe('UpstashStore', () => {
|
|
|
556
792
|
const workflowName1 = 'default_test_1';
|
|
557
793
|
const workflowName2 = 'default_test_2';
|
|
558
794
|
|
|
559
|
-
const { snapshot: workflow1, runId: runId1, stepId: stepId1 } = createSampleWorkflowSnapshot('
|
|
560
|
-
const { snapshot: workflow2, runId: runId2, stepId: stepId2 } = createSampleWorkflowSnapshot('
|
|
795
|
+
const { snapshot: workflow1, runId: runId1, stepId: stepId1 } = createSampleWorkflowSnapshot('success');
|
|
796
|
+
const { snapshot: workflow2, runId: runId2, stepId: stepId2 } = createSampleWorkflowSnapshot('waiting');
|
|
561
797
|
|
|
562
798
|
await store.persistWorkflowSnapshot({
|
|
563
799
|
namespace: testNamespace,
|
|
@@ -578,17 +814,17 @@ describe('UpstashStore', () => {
|
|
|
578
814
|
expect(total).toBe(2);
|
|
579
815
|
expect(runs[0]!.workflowName).toBe(workflowName2); // Most recent first
|
|
580
816
|
expect(runs[1]!.workflowName).toBe(workflowName1);
|
|
581
|
-
const firstSnapshot = runs[0]!.snapshot
|
|
582
|
-
const secondSnapshot = runs[1]!.snapshot
|
|
583
|
-
|
|
584
|
-
|
|
817
|
+
const firstSnapshot = runs[0]!.snapshot;
|
|
818
|
+
const secondSnapshot = runs[1]!.snapshot;
|
|
819
|
+
checkWorkflowSnapshot(firstSnapshot, stepId2, 'waiting');
|
|
820
|
+
checkWorkflowSnapshot(secondSnapshot, stepId1, 'success');
|
|
585
821
|
});
|
|
586
822
|
|
|
587
823
|
it('filters by workflow name', async () => {
|
|
588
824
|
const workflowName1 = 'filter_test_1';
|
|
589
825
|
const workflowName2 = 'filter_test_2';
|
|
590
826
|
|
|
591
|
-
const { snapshot: workflow1, runId: runId1, stepId: stepId1 } = createSampleWorkflowSnapshot('
|
|
827
|
+
const { snapshot: workflow1, runId: runId1, stepId: stepId1 } = createSampleWorkflowSnapshot('success');
|
|
592
828
|
const { snapshot: workflow2, runId: runId2 } = createSampleWorkflowSnapshot('failed');
|
|
593
829
|
|
|
594
830
|
await store.persistWorkflowSnapshot({
|
|
@@ -609,8 +845,8 @@ describe('UpstashStore', () => {
|
|
|
609
845
|
expect(runs).toHaveLength(1);
|
|
610
846
|
expect(total).toBe(1);
|
|
611
847
|
expect(runs[0]!.workflowName).toBe(workflowName1);
|
|
612
|
-
const snapshot = runs[0]!.snapshot
|
|
613
|
-
|
|
848
|
+
const snapshot = runs[0]!.snapshot;
|
|
849
|
+
checkWorkflowSnapshot(snapshot, stepId1, 'success');
|
|
614
850
|
});
|
|
615
851
|
|
|
616
852
|
it('filters by date range', async () => {
|
|
@@ -621,9 +857,9 @@ describe('UpstashStore', () => {
|
|
|
621
857
|
const workflowName2 = 'date_test_2';
|
|
622
858
|
const workflowName3 = 'date_test_3';
|
|
623
859
|
|
|
624
|
-
const { snapshot: workflow1, runId: runId1 } = createSampleWorkflowSnapshot('
|
|
625
|
-
const { snapshot: workflow2, runId: runId2, stepId: stepId2 } = createSampleWorkflowSnapshot('
|
|
626
|
-
const { snapshot: workflow3, runId: runId3, stepId: stepId3 } = createSampleWorkflowSnapshot('
|
|
860
|
+
const { snapshot: workflow1, runId: runId1 } = createSampleWorkflowSnapshot('success');
|
|
861
|
+
const { snapshot: workflow2, runId: runId2, stepId: stepId2 } = createSampleWorkflowSnapshot('waiting');
|
|
862
|
+
const { snapshot: workflow3, runId: runId3, stepId: stepId3 } = createSampleWorkflowSnapshot('skipped');
|
|
627
863
|
|
|
628
864
|
await store.insert({
|
|
629
865
|
tableName: TABLE_WORKFLOW_SNAPSHOT,
|
|
@@ -668,10 +904,10 @@ describe('UpstashStore', () => {
|
|
|
668
904
|
expect(runs).toHaveLength(2);
|
|
669
905
|
expect(runs[0]!.workflowName).toBe(workflowName3);
|
|
670
906
|
expect(runs[1]!.workflowName).toBe(workflowName2);
|
|
671
|
-
const firstSnapshot = runs[0]!.snapshot
|
|
672
|
-
const secondSnapshot = runs[1]!.snapshot
|
|
673
|
-
|
|
674
|
-
|
|
907
|
+
const firstSnapshot = runs[0]!.snapshot;
|
|
908
|
+
const secondSnapshot = runs[1]!.snapshot;
|
|
909
|
+
checkWorkflowSnapshot(firstSnapshot, stepId3, 'skipped');
|
|
910
|
+
checkWorkflowSnapshot(secondSnapshot, stepId2, 'waiting');
|
|
675
911
|
});
|
|
676
912
|
|
|
677
913
|
it('handles pagination', async () => {
|
|
@@ -679,9 +915,9 @@ describe('UpstashStore', () => {
|
|
|
679
915
|
const workflowName2 = 'page_test_2';
|
|
680
916
|
const workflowName3 = 'page_test_3';
|
|
681
917
|
|
|
682
|
-
const { snapshot: workflow1, runId: runId1, stepId: stepId1 } = createSampleWorkflowSnapshot('
|
|
683
|
-
const { snapshot: workflow2, runId: runId2, stepId: stepId2 } = createSampleWorkflowSnapshot('
|
|
684
|
-
const { snapshot: workflow3, runId: runId3, stepId: stepId3 } = createSampleWorkflowSnapshot('
|
|
918
|
+
const { snapshot: workflow1, runId: runId1, stepId: stepId1 } = createSampleWorkflowSnapshot('success');
|
|
919
|
+
const { snapshot: workflow2, runId: runId2, stepId: stepId2 } = createSampleWorkflowSnapshot('waiting');
|
|
920
|
+
const { snapshot: workflow3, runId: runId3, stepId: stepId3 } = createSampleWorkflowSnapshot('skipped');
|
|
685
921
|
|
|
686
922
|
await store.persistWorkflowSnapshot({
|
|
687
923
|
namespace: testNamespace,
|
|
@@ -714,10 +950,10 @@ describe('UpstashStore', () => {
|
|
|
714
950
|
expect(page1.total).toBe(3); // Total count of all records
|
|
715
951
|
expect(page1.runs[0]!.workflowName).toBe(workflowName3);
|
|
716
952
|
expect(page1.runs[1]!.workflowName).toBe(workflowName2);
|
|
717
|
-
const firstSnapshot = page1.runs[0]!.snapshot
|
|
718
|
-
const secondSnapshot = page1.runs[1]!.snapshot
|
|
719
|
-
|
|
720
|
-
|
|
953
|
+
const firstSnapshot = page1.runs[0]!.snapshot;
|
|
954
|
+
const secondSnapshot = page1.runs[1]!.snapshot;
|
|
955
|
+
checkWorkflowSnapshot(firstSnapshot, stepId3, 'skipped');
|
|
956
|
+
checkWorkflowSnapshot(secondSnapshot, stepId2, 'waiting');
|
|
721
957
|
|
|
722
958
|
// Get second page
|
|
723
959
|
const page2 = await store.getWorkflowRuns({
|
|
@@ -728,8 +964,391 @@ describe('UpstashStore', () => {
|
|
|
728
964
|
expect(page2.runs).toHaveLength(1);
|
|
729
965
|
expect(page2.total).toBe(3);
|
|
730
966
|
expect(page2.runs[0]!.workflowName).toBe(workflowName1);
|
|
731
|
-
const snapshot = page2.runs[0]!.snapshot
|
|
732
|
-
|
|
967
|
+
const snapshot = page2.runs[0]!.snapshot;
|
|
968
|
+
checkWorkflowSnapshot(snapshot, stepId1, 'success');
|
|
969
|
+
});
|
|
970
|
+
});
|
|
971
|
+
describe('getWorkflowRunById', () => {
|
|
972
|
+
const testNamespace = 'test-workflows-id';
|
|
973
|
+
const workflowName = 'workflow-id-test';
|
|
974
|
+
let runId: string;
|
|
975
|
+
let stepId: string;
|
|
976
|
+
|
|
977
|
+
beforeAll(async () => {
|
|
978
|
+
// Insert a workflow run for positive test
|
|
979
|
+
const sample = createSampleWorkflowSnapshot('success');
|
|
980
|
+
runId = sample.runId;
|
|
981
|
+
stepId = sample.stepId;
|
|
982
|
+
await store.insert({
|
|
983
|
+
tableName: TABLE_WORKFLOW_SNAPSHOT,
|
|
984
|
+
record: {
|
|
985
|
+
namespace: testNamespace,
|
|
986
|
+
workflow_name: workflowName,
|
|
987
|
+
run_id: runId,
|
|
988
|
+
resourceId: 'resource-abc',
|
|
989
|
+
snapshot: sample.snapshot,
|
|
990
|
+
createdAt: new Date(),
|
|
991
|
+
updatedAt: new Date(),
|
|
992
|
+
},
|
|
993
|
+
});
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
it('should retrieve a workflow run by ID', async () => {
|
|
997
|
+
const found = await store.getWorkflowRunById({
|
|
998
|
+
namespace: testNamespace,
|
|
999
|
+
runId,
|
|
1000
|
+
workflowName,
|
|
1001
|
+
});
|
|
1002
|
+
expect(found).not.toBeNull();
|
|
1003
|
+
expect(found?.runId).toBe(runId);
|
|
1004
|
+
const snapshot = found?.snapshot;
|
|
1005
|
+
checkWorkflowSnapshot(snapshot!, stepId, 'success');
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
it('should return null for non-existent workflow run ID', async () => {
|
|
1009
|
+
const notFound = await store.getWorkflowRunById({
|
|
1010
|
+
namespace: testNamespace,
|
|
1011
|
+
runId: 'non-existent-id',
|
|
1012
|
+
workflowName,
|
|
1013
|
+
});
|
|
1014
|
+
expect(notFound).toBeNull();
|
|
1015
|
+
});
|
|
1016
|
+
});
|
|
1017
|
+
describe('getWorkflowRuns with resourceId', () => {
|
|
1018
|
+
const testNamespace = 'test-workflows-id';
|
|
1019
|
+
const workflowName = 'workflow-id-test';
|
|
1020
|
+
let resourceId: string;
|
|
1021
|
+
let runIds: string[] = [];
|
|
1022
|
+
|
|
1023
|
+
beforeAll(async () => {
|
|
1024
|
+
// Insert multiple workflow runs for the same resourceId
|
|
1025
|
+
resourceId = 'resource-shared';
|
|
1026
|
+
for (const status of ['success', 'waiting']) {
|
|
1027
|
+
const sample = createSampleWorkflowSnapshot(status);
|
|
1028
|
+
runIds.push(sample.runId);
|
|
1029
|
+
await store.insert({
|
|
1030
|
+
tableName: TABLE_WORKFLOW_SNAPSHOT,
|
|
1031
|
+
record: {
|
|
1032
|
+
namespace: testNamespace,
|
|
1033
|
+
workflow_name: workflowName,
|
|
1034
|
+
run_id: sample.runId,
|
|
1035
|
+
resourceId,
|
|
1036
|
+
snapshot: sample.snapshot,
|
|
1037
|
+
createdAt: new Date(),
|
|
1038
|
+
updatedAt: new Date(),
|
|
1039
|
+
},
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
// Insert a run with a different resourceId
|
|
1043
|
+
const other = createSampleWorkflowSnapshot('waiting');
|
|
1044
|
+
await store.insert({
|
|
1045
|
+
tableName: TABLE_WORKFLOW_SNAPSHOT,
|
|
1046
|
+
record: {
|
|
1047
|
+
namespace: testNamespace,
|
|
1048
|
+
workflow_name: workflowName,
|
|
1049
|
+
run_id: other.runId,
|
|
1050
|
+
resourceId: 'resource-other',
|
|
1051
|
+
snapshot: other.snapshot,
|
|
1052
|
+
createdAt: new Date(),
|
|
1053
|
+
updatedAt: new Date(),
|
|
1054
|
+
},
|
|
1055
|
+
});
|
|
1056
|
+
});
|
|
1057
|
+
|
|
1058
|
+
it('should retrieve all workflow runs by resourceId', async () => {
|
|
1059
|
+
const { runs } = await store.getWorkflowRuns({
|
|
1060
|
+
namespace: testNamespace,
|
|
1061
|
+
resourceId,
|
|
1062
|
+
workflowName,
|
|
1063
|
+
});
|
|
1064
|
+
expect(Array.isArray(runs)).toBe(true);
|
|
1065
|
+
expect(runs.length).toBeGreaterThanOrEqual(2);
|
|
1066
|
+
for (const run of runs) {
|
|
1067
|
+
expect(run.resourceId).toBe(resourceId);
|
|
1068
|
+
}
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
it('should return an empty array if no workflow runs match resourceId', async () => {
|
|
1072
|
+
const { runs } = await store.getWorkflowRuns({
|
|
1073
|
+
namespace: testNamespace,
|
|
1074
|
+
resourceId: 'non-existent-resource',
|
|
1075
|
+
workflowName,
|
|
1076
|
+
});
|
|
1077
|
+
expect(Array.isArray(runs)).toBe(true);
|
|
1078
|
+
expect(runs.length).toBe(0);
|
|
1079
|
+
});
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
describe('alterTable (no-op/schemaless)', () => {
|
|
1083
|
+
const TEST_TABLE = 'test_alter_table'; // Use "table" or "collection" as appropriate
|
|
1084
|
+
beforeEach(async () => {
|
|
1085
|
+
await store.clearTable({ tableName: TEST_TABLE as TABLE_NAMES });
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1088
|
+
afterEach(async () => {
|
|
1089
|
+
await store.clearTable({ tableName: TEST_TABLE as TABLE_NAMES });
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1092
|
+
it('allows inserting records with new fields without alterTable', async () => {
|
|
1093
|
+
await store.insert({
|
|
1094
|
+
tableName: TEST_TABLE as TABLE_NAMES,
|
|
1095
|
+
record: { id: '1', name: 'Alice' },
|
|
1096
|
+
});
|
|
1097
|
+
await store.insert({
|
|
1098
|
+
tableName: TEST_TABLE as TABLE_NAMES,
|
|
1099
|
+
record: { id: '2', name: 'Bob', newField: 123 },
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
const row = await store.load<{ id: string; name: string; newField?: number }>({
|
|
1103
|
+
tableName: TEST_TABLE as TABLE_NAMES,
|
|
1104
|
+
keys: { id: '2' },
|
|
1105
|
+
});
|
|
1106
|
+
expect(row?.newField).toBe(123);
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
it('does not throw when calling alterTable (no-op)', async () => {
|
|
1110
|
+
await expect(
|
|
1111
|
+
store.alterTable({
|
|
1112
|
+
tableName: TEST_TABLE as TABLE_NAMES,
|
|
1113
|
+
schema: {
|
|
1114
|
+
id: { type: 'text', primaryKey: true, nullable: false },
|
|
1115
|
+
name: { type: 'text', nullable: true },
|
|
1116
|
+
extra: { type: 'integer', nullable: true },
|
|
1117
|
+
},
|
|
1118
|
+
ifNotExists: [],
|
|
1119
|
+
}),
|
|
1120
|
+
).resolves.not.toThrow();
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
it('can add multiple new fields at write time', async () => {
|
|
1124
|
+
await store.insert({
|
|
1125
|
+
tableName: TEST_TABLE as TABLE_NAMES,
|
|
1126
|
+
record: { id: '3', name: 'Charlie', age: 30, city: 'Paris' },
|
|
1127
|
+
});
|
|
1128
|
+
const row = await store.load<{ id: string; name: string; age?: number; city?: string }>({
|
|
1129
|
+
tableName: TEST_TABLE as TABLE_NAMES,
|
|
1130
|
+
keys: { id: '3' },
|
|
1131
|
+
});
|
|
1132
|
+
expect(row?.age).toBe(30);
|
|
1133
|
+
expect(row?.city).toBe('Paris');
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
it('can retrieve all fields, including dynamically added ones', async () => {
|
|
1137
|
+
await store.insert({
|
|
1138
|
+
tableName: TEST_TABLE as TABLE_NAMES,
|
|
1139
|
+
record: { id: '4', name: 'Dana', hobby: 'skiing' },
|
|
1140
|
+
});
|
|
1141
|
+
const row = await store.load<{ id: string; name: string; hobby?: string }>({
|
|
1142
|
+
tableName: TEST_TABLE as TABLE_NAMES,
|
|
1143
|
+
keys: { id: '4' },
|
|
1144
|
+
});
|
|
1145
|
+
expect(row?.hobby).toBe('skiing');
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
it('does not restrict or error on arbitrary new fields', async () => {
|
|
1149
|
+
await expect(
|
|
1150
|
+
store.insert({
|
|
1151
|
+
tableName: TEST_TABLE as TABLE_NAMES,
|
|
1152
|
+
record: { id: '5', weirdField: { nested: true }, another: [1, 2, 3] },
|
|
1153
|
+
}),
|
|
1154
|
+
).resolves.not.toThrow();
|
|
1155
|
+
|
|
1156
|
+
const row = await store.load<{ id: string; weirdField?: any; another?: any }>({
|
|
1157
|
+
tableName: TEST_TABLE as TABLE_NAMES,
|
|
1158
|
+
keys: { id: '5' },
|
|
1159
|
+
});
|
|
1160
|
+
expect(row?.weirdField).toEqual({ nested: true });
|
|
1161
|
+
expect(row?.another).toEqual([1, 2, 3]);
|
|
1162
|
+
});
|
|
1163
|
+
});
|
|
1164
|
+
|
|
1165
|
+
describe('Pagination Features', () => {
|
|
1166
|
+
beforeEach(async () => {
|
|
1167
|
+
// Clear all test data
|
|
1168
|
+
await store.clearTable({ tableName: TABLE_THREADS });
|
|
1169
|
+
await store.clearTable({ tableName: TABLE_MESSAGES });
|
|
1170
|
+
await store.clearTable({ tableName: TABLE_EVALS });
|
|
1171
|
+
await store.clearTable({ tableName: TABLE_TRACES });
|
|
1172
|
+
});
|
|
1173
|
+
|
|
1174
|
+
describe('getEvals with pagination', () => {
|
|
1175
|
+
it('should return paginated evals with total count', async () => {
|
|
1176
|
+
const agentName = 'test-agent';
|
|
1177
|
+
const evals = Array.from({ length: 25 }, (_, i) => createSampleEval(agentName, i % 2 === 0));
|
|
1178
|
+
|
|
1179
|
+
// Insert all evals
|
|
1180
|
+
for (const evalRecord of evals) {
|
|
1181
|
+
await store.insert({
|
|
1182
|
+
tableName: TABLE_EVALS,
|
|
1183
|
+
record: evalRecord,
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// Test page-based pagination
|
|
1188
|
+
const page1 = await store.getEvals({ agentName, page: 0, perPage: 10 });
|
|
1189
|
+
expect(page1.evals).toHaveLength(10);
|
|
1190
|
+
expect(page1.total).toBe(25);
|
|
1191
|
+
expect(page1.page).toBe(0);
|
|
1192
|
+
expect(page1.perPage).toBe(10);
|
|
1193
|
+
expect(page1.hasMore).toBe(true);
|
|
1194
|
+
|
|
1195
|
+
const page2 = await store.getEvals({ agentName, page: 1, perPage: 10 });
|
|
1196
|
+
expect(page2.evals).toHaveLength(10);
|
|
1197
|
+
expect(page2.total).toBe(25);
|
|
1198
|
+
expect(page2.hasMore).toBe(true);
|
|
1199
|
+
|
|
1200
|
+
const page3 = await store.getEvals({ agentName, page: 2, perPage: 10 });
|
|
1201
|
+
expect(page3.evals).toHaveLength(5);
|
|
1202
|
+
expect(page3.total).toBe(25);
|
|
1203
|
+
expect(page3.hasMore).toBe(false);
|
|
1204
|
+
});
|
|
1205
|
+
|
|
1206
|
+
it('should support page/perPage pagination', async () => {
|
|
1207
|
+
const agentName = 'test-agent-2';
|
|
1208
|
+
const evals = Array.from({ length: 15 }, () => createSampleEval(agentName));
|
|
1209
|
+
|
|
1210
|
+
for (const evalRecord of evals) {
|
|
1211
|
+
await store.insert({
|
|
1212
|
+
tableName: TABLE_EVALS,
|
|
1213
|
+
record: evalRecord,
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// Test offset-based pagination
|
|
1218
|
+
const result1 = await store.getEvals({ agentName, page: 0, perPage: 5 });
|
|
1219
|
+
expect(result1.evals).toHaveLength(5);
|
|
1220
|
+
expect(result1.total).toBe(15);
|
|
1221
|
+
expect(result1.hasMore).toBe(true);
|
|
1222
|
+
|
|
1223
|
+
const result2 = await store.getEvals({ agentName, page: 2, perPage: 5 });
|
|
1224
|
+
expect(result2.evals).toHaveLength(5);
|
|
1225
|
+
expect(result2.total).toBe(15);
|
|
1226
|
+
expect(result2.hasMore).toBe(false);
|
|
1227
|
+
});
|
|
1228
|
+
|
|
1229
|
+
it('should filter by type with pagination', async () => {
|
|
1230
|
+
const agentName = 'test-agent-3';
|
|
1231
|
+
const testEvals = Array.from({ length: 10 }, () => createSampleEval(agentName, true));
|
|
1232
|
+
const liveEvals = Array.from({ length: 8 }, () => createSampleEval(agentName, false));
|
|
1233
|
+
|
|
1234
|
+
for (const evalRecord of [...testEvals, ...liveEvals]) {
|
|
1235
|
+
await store.insert({
|
|
1236
|
+
tableName: TABLE_EVALS,
|
|
1237
|
+
record: evalRecord,
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
const testResults = await store.getEvals({ agentName, type: 'test', page: 0, perPage: 5 });
|
|
1242
|
+
expect(testResults.evals).toHaveLength(5);
|
|
1243
|
+
expect(testResults.total).toBe(10);
|
|
1244
|
+
|
|
1245
|
+
const liveResults = await store.getEvals({ agentName, type: 'live', page: 0, perPage: 5 });
|
|
1246
|
+
expect(liveResults.evals).toHaveLength(5);
|
|
1247
|
+
expect(liveResults.total).toBe(8);
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
it('should filter by date with pagination', async () => {
|
|
1251
|
+
const agentName = 'test-agent-date';
|
|
1252
|
+
const now = new Date();
|
|
1253
|
+
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
|
1254
|
+
const evals = [createSampleEval(agentName, false, now), createSampleEval(agentName, false, yesterday)];
|
|
1255
|
+
for (const evalRecord of evals) {
|
|
1256
|
+
await store.insert({
|
|
1257
|
+
tableName: TABLE_EVALS,
|
|
1258
|
+
record: evalRecord,
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
const result = await store.getEvals({
|
|
1262
|
+
agentName,
|
|
1263
|
+
page: 0,
|
|
1264
|
+
perPage: 10,
|
|
1265
|
+
dateRange: { start: now },
|
|
1266
|
+
});
|
|
1267
|
+
expect(result.evals).toHaveLength(1);
|
|
1268
|
+
expect(result.total).toBe(1);
|
|
1269
|
+
});
|
|
1270
|
+
});
|
|
1271
|
+
|
|
1272
|
+
describe('getTraces with pagination', () => {
|
|
1273
|
+
it('should return paginated traces with total count', async () => {
|
|
1274
|
+
const traces = Array.from({ length: 18 }, (_, i) => createSampleTrace(`test-trace-${i}`, 'test-scope'));
|
|
1275
|
+
|
|
1276
|
+
for (const trace of traces) {
|
|
1277
|
+
await store.insert({
|
|
1278
|
+
tableName: TABLE_TRACES,
|
|
1279
|
+
record: trace,
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
const page1 = await store.getTracesPaginated({
|
|
1284
|
+
scope: 'test-scope',
|
|
1285
|
+
page: 0,
|
|
1286
|
+
perPage: 8,
|
|
1287
|
+
});
|
|
1288
|
+
expect(page1.traces).toHaveLength(8);
|
|
1289
|
+
expect(page1.total).toBe(18);
|
|
1290
|
+
expect(page1.page).toBe(0);
|
|
1291
|
+
expect(page1.perPage).toBe(8);
|
|
1292
|
+
expect(page1.hasMore).toBe(true);
|
|
1293
|
+
|
|
1294
|
+
const page3 = await store.getTracesPaginated({
|
|
1295
|
+
scope: 'test-scope',
|
|
1296
|
+
page: 2,
|
|
1297
|
+
perPage: 8,
|
|
1298
|
+
});
|
|
1299
|
+
expect(page3.traces).toHaveLength(2);
|
|
1300
|
+
expect(page3.total).toBe(18);
|
|
1301
|
+
expect(page3.hasMore).toBe(false);
|
|
1302
|
+
});
|
|
1303
|
+
|
|
1304
|
+
it('should filter by date with pagination', async () => {
|
|
1305
|
+
const scope = 'test-scope-date';
|
|
1306
|
+
const now = new Date();
|
|
1307
|
+
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
|
1308
|
+
|
|
1309
|
+
const traces = [
|
|
1310
|
+
createSampleTrace(`test-trace-now`, scope, undefined, now),
|
|
1311
|
+
createSampleTrace(`test-trace-yesterday`, scope, undefined, yesterday),
|
|
1312
|
+
];
|
|
1313
|
+
|
|
1314
|
+
for (const trace of traces) {
|
|
1315
|
+
await store.insert({
|
|
1316
|
+
tableName: TABLE_TRACES,
|
|
1317
|
+
record: trace,
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
const result = await store.getTracesPaginated({
|
|
1322
|
+
scope,
|
|
1323
|
+
page: 0,
|
|
1324
|
+
perPage: 10,
|
|
1325
|
+
dateRange: { start: now },
|
|
1326
|
+
});
|
|
1327
|
+
|
|
1328
|
+
expect(result.traces).toHaveLength(1);
|
|
1329
|
+
expect(result.traces[0].name).toBe('test-trace-now');
|
|
1330
|
+
expect(result.total).toBe(1);
|
|
1331
|
+
});
|
|
1332
|
+
});
|
|
1333
|
+
|
|
1334
|
+
describe('Enhanced existing methods with pagination', () => {
|
|
1335
|
+
it('should support pagination in getThreadsByResourceId', async () => {
|
|
1336
|
+
const resourceId = 'enhanced-resource';
|
|
1337
|
+
const threads = Array.from({ length: 17 }, () => createSampleThread({ resourceId }));
|
|
1338
|
+
|
|
1339
|
+
for (const thread of threads) {
|
|
1340
|
+
await store.saveThread({ thread });
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
const page1 = await store.getThreadsByResourceIdPaginated({ resourceId, page: 0, perPage: 7 });
|
|
1344
|
+
expect(page1.threads).toHaveLength(7);
|
|
1345
|
+
|
|
1346
|
+
const page3 = await store.getThreadsByResourceIdPaginated({ resourceId, page: 2, perPage: 7 });
|
|
1347
|
+
expect(page3.threads).toHaveLength(3);
|
|
1348
|
+
|
|
1349
|
+
const limited = await store.getThreadsByResourceIdPaginated({ resourceId, page: 1, perPage: 5 });
|
|
1350
|
+
expect(limited.threads).toHaveLength(5);
|
|
1351
|
+
});
|
|
733
1352
|
});
|
|
734
1353
|
});
|
|
735
1354
|
});
|