@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.
- package/.turbo/turbo-build.log +7 -7
- package/CHANGELOG.md +25 -0
- package/dist/_tsup-dts-rollup.d.cts +342 -40
- package/dist/_tsup-dts-rollup.d.ts +342 -40
- package/dist/index.cjs +1133 -612
- package/dist/index.js +1134 -613
- package/docker-compose.yaml +1 -1
- package/package.json +5 -5
- package/src/storage/domains/legacy-evals/index.ts +279 -0
- package/src/storage/domains/memory/index.ts +902 -0
- package/src/storage/domains/operations/index.ts +168 -0
- package/src/storage/domains/scores/index.ts +216 -0
- package/src/storage/domains/traces/index.ts +172 -0
- package/src/storage/domains/utils.ts +57 -0
- package/src/storage/domains/workflows/index.ts +243 -0
- package/src/storage/index.test.ts +13 -0
- package/src/storage/index.ts +143 -1416
- package/src/storage/upstash.test.ts +0 -1461
|
@@ -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
|
-
});
|