@mastra/mongodb 0.12.0 → 0.12.1
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 +57 -0
- package/LICENSE.md +11 -42
- package/dist/_tsup-dts-rollup.d.cts +376 -54
- package/dist/_tsup-dts-rollup.d.ts +376 -54
- package/dist/index.cjs +1420 -424
- package/dist/index.d.cts +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +1414 -418
- package/docker-compose.yaml +1 -1
- package/package.json +6 -6
- package/src/storage/ConnectorHandler.ts +7 -0
- package/src/storage/MongoDBConnector.ts +93 -0
- package/src/storage/connectors/MongoDBConnector.ts +93 -0
- package/src/storage/connectors/base.ts +7 -0
- package/src/storage/domains/legacy-evals/index.ts +193 -0
- package/src/storage/domains/memory/index.ts +741 -0
- package/src/storage/domains/operations/index.ts +152 -0
- package/src/storage/domains/scores/index.ts +379 -0
- package/src/storage/domains/traces/index.ts +142 -0
- package/src/storage/domains/utils.ts +43 -0
- package/src/storage/domains/workflows/index.ts +196 -0
- package/src/storage/index.test.ts +24 -1226
- package/src/storage/index.ts +218 -776
- package/src/storage/types.ts +14 -0
- package/src/vector/index.test.ts +16 -1
- package/src/vector/index.ts +34 -11
|
@@ -1,159 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import type {
|
|
4
|
-
import {
|
|
5
|
-
TABLE_EVALS,
|
|
6
|
-
TABLE_MESSAGES,
|
|
7
|
-
TABLE_THREADS,
|
|
8
|
-
TABLE_TRACES,
|
|
9
|
-
TABLE_WORKFLOW_SNAPSHOT,
|
|
10
|
-
} from '@mastra/core/storage';
|
|
11
|
-
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
|
12
|
-
import type { MongoDBConfig } from './index';
|
|
1
|
+
import { createTestSuite } from '@internal/storage-test-utils';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import type { ConnectorHandler } from './connectors/base';
|
|
4
|
+
import type { MongoDBConfig } from './types';
|
|
13
5
|
import { MongoDBStore } from './index';
|
|
14
6
|
|
|
15
|
-
class Test {
|
|
16
|
-
store: MongoDBStore;
|
|
17
|
-
|
|
18
|
-
constructor(store: MongoDBStore) {
|
|
19
|
-
this.store = store;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
build() {
|
|
23
|
-
return this;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async clearTables() {
|
|
27
|
-
try {
|
|
28
|
-
await this.store.clearTable({ tableName: TABLE_WORKFLOW_SNAPSHOT });
|
|
29
|
-
await this.store.clearTable({ tableName: TABLE_MESSAGES });
|
|
30
|
-
await this.store.clearTable({ tableName: TABLE_THREADS });
|
|
31
|
-
await this.store.clearTable({ tableName: TABLE_EVALS });
|
|
32
|
-
await this.store.clearTable({ tableName: TABLE_TRACES });
|
|
33
|
-
} catch (error) {
|
|
34
|
-
// Ignore errors during table clearing
|
|
35
|
-
console.warn('Error clearing tables:', error);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
generateSampleThread(options: any = {}) {
|
|
40
|
-
return {
|
|
41
|
-
id: `thread-${randomUUID()}`,
|
|
42
|
-
resourceId: `resource-${randomUUID()}`,
|
|
43
|
-
title: 'Test Thread',
|
|
44
|
-
createdAt: new Date(),
|
|
45
|
-
updatedAt: new Date(),
|
|
46
|
-
metadata: { key: 'value' },
|
|
47
|
-
...options,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
generateSampleMessageV1({
|
|
52
|
-
threadId,
|
|
53
|
-
resourceId = randomUUID(),
|
|
54
|
-
content = 'Hello',
|
|
55
|
-
}: {
|
|
56
|
-
threadId: string;
|
|
57
|
-
resourceId?: string;
|
|
58
|
-
content?: string;
|
|
59
|
-
}): MastraMessageV1 {
|
|
60
|
-
return {
|
|
61
|
-
id: `msg-${randomUUID()}`,
|
|
62
|
-
role: 'user',
|
|
63
|
-
type: 'text',
|
|
64
|
-
threadId,
|
|
65
|
-
content: [{ type: 'text', text: content }],
|
|
66
|
-
createdAt: new Date(),
|
|
67
|
-
resourceId,
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
generateSampleMessageV2({
|
|
72
|
-
threadId,
|
|
73
|
-
resourceId = randomUUID(),
|
|
74
|
-
content = 'Hello',
|
|
75
|
-
}: {
|
|
76
|
-
threadId: string;
|
|
77
|
-
resourceId?: string;
|
|
78
|
-
content?: string;
|
|
79
|
-
}): MastraMessageV2 {
|
|
80
|
-
return {
|
|
81
|
-
id: `msg-${randomUUID()}`,
|
|
82
|
-
role: 'user',
|
|
83
|
-
type: 'text',
|
|
84
|
-
threadId,
|
|
85
|
-
content: {
|
|
86
|
-
format: 2,
|
|
87
|
-
parts: [{ type: 'text', text: content }],
|
|
88
|
-
content: content,
|
|
89
|
-
},
|
|
90
|
-
createdAt: new Date(),
|
|
91
|
-
resourceId,
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
generateSampleEval(isTest: boolean, options: any = {}) {
|
|
96
|
-
const testInfo = isTest ? { testPath: 'test/path.ts', testName: 'Test Name' } : undefined;
|
|
97
|
-
|
|
98
|
-
return {
|
|
99
|
-
id: randomUUID(),
|
|
100
|
-
agentName: 'Agent Name',
|
|
101
|
-
input: 'Sample input',
|
|
102
|
-
output: 'Sample output',
|
|
103
|
-
result: { score: 0.8 } as MetricResult,
|
|
104
|
-
metricName: 'sample-metric',
|
|
105
|
-
instructions: 'Sample instructions',
|
|
106
|
-
testInfo,
|
|
107
|
-
globalRunId: `global-${randomUUID()}`,
|
|
108
|
-
runId: `run-${randomUUID()}`,
|
|
109
|
-
createdAt: new Date().toISOString(),
|
|
110
|
-
...options,
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
generateSampleWorkflowSnapshot(options: any = {}) {
|
|
115
|
-
const runId = `run-${randomUUID()}`;
|
|
116
|
-
const stepId = `step-${randomUUID()}`;
|
|
117
|
-
const timestamp = options.createdAt || new Date();
|
|
118
|
-
const snapshot = {
|
|
119
|
-
result: { success: true },
|
|
120
|
-
value: {},
|
|
121
|
-
context: {
|
|
122
|
-
[stepId]: {
|
|
123
|
-
status: options.status,
|
|
124
|
-
payload: {},
|
|
125
|
-
error: undefined,
|
|
126
|
-
startedAt: timestamp.getTime(),
|
|
127
|
-
endedAt: new Date(timestamp.getTime() + 15000).getTime(),
|
|
128
|
-
},
|
|
129
|
-
input: {},
|
|
130
|
-
},
|
|
131
|
-
serializedStepGraph: [],
|
|
132
|
-
activePaths: [],
|
|
133
|
-
suspendedPaths: {},
|
|
134
|
-
runId,
|
|
135
|
-
timestamp: timestamp.getTime(),
|
|
136
|
-
status: options.status,
|
|
137
|
-
} as WorkflowRunState;
|
|
138
|
-
return { snapshot, runId, stepId };
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
7
|
const TEST_CONFIG: MongoDBConfig = {
|
|
143
8
|
url: process.env.MONGODB_URL || 'mongodb://localhost:27017',
|
|
144
9
|
dbName: process.env.MONGODB_DB_NAME || 'mastra-test-db',
|
|
145
10
|
};
|
|
146
11
|
|
|
147
|
-
describe('
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
beforeAll(async () => {
|
|
151
|
-
store = new MongoDBStore(TEST_CONFIG);
|
|
152
|
-
await store.init();
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
// --- Validation tests ---
|
|
156
|
-
describe('Validation', () => {
|
|
12
|
+
describe('Validation', () => {
|
|
13
|
+
describe('with database options', () => {
|
|
157
14
|
const validConfig = TEST_CONFIG;
|
|
158
15
|
it('throws if url is empty', () => {
|
|
159
16
|
expect(() => new MongoDBStore({ ...validConfig, url: '' })).toThrow(/url must be provided and cannot be empty/);
|
|
@@ -166,1094 +23,35 @@ describe('MongoDBStore', () => {
|
|
|
166
23
|
const { dbName, ...rest } = validConfig;
|
|
167
24
|
expect(() => new MongoDBStore(rest as any)).toThrow(/dbName must be provided and cannot be empty/);
|
|
168
25
|
});
|
|
26
|
+
|
|
169
27
|
it('does not throw on valid config (host-based)', () => {
|
|
170
28
|
expect(() => new MongoDBStore(validConfig)).not.toThrow();
|
|
171
29
|
});
|
|
172
30
|
});
|
|
173
31
|
|
|
174
|
-
describe('
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const thread = test.generateSampleThread();
|
|
179
|
-
|
|
180
|
-
// Save thread
|
|
181
|
-
const savedThread = await store.saveThread({ thread });
|
|
182
|
-
expect(savedThread).toEqual(thread);
|
|
183
|
-
|
|
184
|
-
// Retrieve thread
|
|
185
|
-
const retrievedThread = await store.getThreadById({ threadId: thread.id });
|
|
186
|
-
expect(retrievedThread?.title).toEqual(thread.title);
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
it('should return null for non-existent thread', async () => {
|
|
190
|
-
const test = new Test(store).build();
|
|
191
|
-
await test.clearTables();
|
|
192
|
-
|
|
193
|
-
const result = await store.getThreadById({ threadId: 'non-existent' });
|
|
194
|
-
expect(result).toBeNull();
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
it('should get threads by resource ID', async () => {
|
|
198
|
-
const test = new Test(store).build();
|
|
199
|
-
await test.clearTables();
|
|
200
|
-
|
|
201
|
-
const thread1 = test.generateSampleThread();
|
|
202
|
-
const thread2 = test.generateSampleThread({ resourceId: thread1.resourceId });
|
|
203
|
-
|
|
204
|
-
await store.saveThread({ thread: thread1 });
|
|
205
|
-
await store.saveThread({ thread: thread2 });
|
|
206
|
-
|
|
207
|
-
const threads = await store.getThreadsByResourceId({ resourceId: thread1.resourceId });
|
|
208
|
-
expect(threads).toHaveLength(2);
|
|
209
|
-
expect(threads.map(t => t.id)).toEqual(expect.arrayContaining([thread1.id, thread2.id]));
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
it('should update thread title and metadata', async () => {
|
|
213
|
-
const test = new Test(store).build();
|
|
214
|
-
await test.clearTables();
|
|
215
|
-
|
|
216
|
-
const thread = test.generateSampleThread();
|
|
217
|
-
await store.saveThread({ thread });
|
|
218
|
-
|
|
219
|
-
const newMetadata = { newKey: 'newValue' };
|
|
220
|
-
const updatedThread = await store.updateThread({
|
|
221
|
-
id: thread.id,
|
|
222
|
-
title: 'Updated Title',
|
|
223
|
-
metadata: newMetadata,
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
expect(updatedThread.title).toBe('Updated Title');
|
|
227
|
-
expect(updatedThread.metadata).toEqual({
|
|
228
|
-
...thread.metadata,
|
|
229
|
-
...newMetadata,
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
// Verify persistence
|
|
233
|
-
const retrievedThread = await store.getThreadById({ threadId: thread.id });
|
|
234
|
-
expect(retrievedThread).toEqual(updatedThread);
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
it('should delete thread and its messages', async () => {
|
|
238
|
-
const test = new Test(store).build();
|
|
239
|
-
await test.clearTables();
|
|
240
|
-
|
|
241
|
-
const thread = test.generateSampleThread();
|
|
242
|
-
await store.saveThread({ thread });
|
|
243
|
-
|
|
244
|
-
// Add some messages
|
|
245
|
-
const messages = [
|
|
246
|
-
test.generateSampleMessageV1({ threadId: thread.id }),
|
|
247
|
-
test.generateSampleMessageV1({ threadId: thread.id }),
|
|
248
|
-
];
|
|
249
|
-
await store.saveMessages({ messages });
|
|
250
|
-
|
|
251
|
-
await store.deleteThread({ threadId: thread.id });
|
|
252
|
-
|
|
253
|
-
const retrievedThread = await store.getThreadById({ threadId: thread.id });
|
|
254
|
-
expect(retrievedThread).toBeNull();
|
|
255
|
-
|
|
256
|
-
// Verify messages were also deleted
|
|
257
|
-
const retrievedMessages = await store.getMessages({ threadId: thread.id });
|
|
258
|
-
expect(retrievedMessages).toHaveLength(0);
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
it('should not create duplicate threads with the same threadId but update the existing one', async () => {
|
|
262
|
-
const test = new Test(store).build();
|
|
263
|
-
await test.clearTables();
|
|
264
|
-
const thread = test.generateSampleThread();
|
|
265
|
-
|
|
266
|
-
// Save the thread for the first time
|
|
267
|
-
await store.saveThread({ thread });
|
|
268
|
-
|
|
269
|
-
// Modify the thread and save again with the same id
|
|
270
|
-
const updatedThread = { ...thread, title: 'Updated Title', metadata: { key: 'newValue' } };
|
|
271
|
-
await store.saveThread({ thread: updatedThread });
|
|
272
|
-
|
|
273
|
-
// Retrieve all threads with this id (should only be one)
|
|
274
|
-
const collection = await store['getCollection'](TABLE_THREADS);
|
|
275
|
-
const allThreads = await collection.find({ id: thread.id }).toArray();
|
|
276
|
-
expect(allThreads).toHaveLength(1);
|
|
277
|
-
|
|
278
|
-
// Retrieve the thread and check it was updated
|
|
279
|
-
const retrievedThread = await store.getThreadById({ threadId: thread.id });
|
|
280
|
-
expect(retrievedThread?.title).toBe('Updated Title');
|
|
281
|
-
expect(retrievedThread?.metadata).toEqual({ key: 'newValue' });
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
it('should update thread updatedAt when a message is saved to it', async () => {
|
|
285
|
-
const test = new Test(store).build();
|
|
286
|
-
await test.clearTables();
|
|
287
|
-
|
|
288
|
-
const thread = test.generateSampleThread();
|
|
289
|
-
await store.saveThread({ thread });
|
|
290
|
-
|
|
291
|
-
const initialThread = await store.getThreadById({ threadId: thread.id });
|
|
292
|
-
expect(initialThread).toBeDefined();
|
|
293
|
-
const originalUpdatedAt = initialThread!.updatedAt;
|
|
294
|
-
|
|
295
|
-
await new Promise(resolve => setTimeout(resolve, 10));
|
|
296
|
-
|
|
297
|
-
const message = test.generateSampleMessageV1({ threadId: thread.id });
|
|
298
|
-
await store.saveMessages({ messages: [message] });
|
|
299
|
-
|
|
300
|
-
const updatedThread = await store.getThreadById({ threadId: thread.id });
|
|
301
|
-
expect(updatedThread).toBeDefined();
|
|
302
|
-
expect(updatedThread!.updatedAt.getTime()).toBeGreaterThan(originalUpdatedAt.getTime());
|
|
303
|
-
});
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
describe('Message Operations', () => {
|
|
307
|
-
it('should save and retrieve messages', async () => {
|
|
308
|
-
const test = new Test(store).build();
|
|
309
|
-
await test.clearTables();
|
|
310
|
-
const thread = test.generateSampleThread();
|
|
311
|
-
await store.saveThread({ thread });
|
|
312
|
-
|
|
313
|
-
const messages = [
|
|
314
|
-
test.generateSampleMessageV1({ threadId: thread.id }),
|
|
315
|
-
{ ...test.generateSampleMessageV1({ threadId: thread.id }), role: 'assistant' as const },
|
|
316
|
-
];
|
|
317
|
-
|
|
318
|
-
// Save messages
|
|
319
|
-
const savedMessages = await store.saveMessages({ messages });
|
|
320
|
-
expect(savedMessages).toEqual(messages);
|
|
321
|
-
|
|
322
|
-
// Retrieve messages
|
|
323
|
-
const retrievedMessages = await store.getMessages({ threadId: thread.id });
|
|
324
|
-
expect(retrievedMessages).toHaveLength(2);
|
|
325
|
-
expect(messages[0]).toEqual(expect.objectContaining(retrievedMessages[0]));
|
|
326
|
-
expect(messages[1]).toEqual(expect.objectContaining(retrievedMessages[1]));
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
it('should handle empty message array', async () => {
|
|
330
|
-
const test = new Test(store).build();
|
|
331
|
-
await test.clearTables();
|
|
332
|
-
|
|
333
|
-
const result = await store.saveMessages({ messages: [] });
|
|
334
|
-
expect(result).toEqual([]);
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
it('should maintain message order', async () => {
|
|
338
|
-
const test = new Test(store).build();
|
|
339
|
-
await test.clearTables();
|
|
340
|
-
const thread = test.generateSampleThread();
|
|
341
|
-
await store.saveThread({ thread });
|
|
342
|
-
|
|
343
|
-
const messages = [
|
|
344
|
-
{
|
|
345
|
-
...test.generateSampleMessageV2({ threadId: thread.id, content: 'First' }),
|
|
346
|
-
},
|
|
347
|
-
{
|
|
348
|
-
...test.generateSampleMessageV2({ threadId: thread.id, content: 'Second' }),
|
|
349
|
-
},
|
|
350
|
-
{
|
|
351
|
-
...test.generateSampleMessageV2({ threadId: thread.id, content: 'Third' }),
|
|
352
|
-
},
|
|
353
|
-
];
|
|
354
|
-
|
|
355
|
-
await store.saveMessages({ messages, format: 'v2' });
|
|
356
|
-
|
|
357
|
-
const retrievedMessages = await store.getMessages({ threadId: thread.id, format: 'v2' });
|
|
358
|
-
expect(retrievedMessages).toHaveLength(3);
|
|
359
|
-
|
|
360
|
-
// Verify order is maintained
|
|
361
|
-
retrievedMessages.forEach((msg, idx) => {
|
|
362
|
-
expect((msg as any).content.parts).toEqual(messages[idx]!.content.parts);
|
|
363
|
-
});
|
|
364
|
-
});
|
|
365
|
-
it('should upsert messages: duplicate id+threadId results in update, not duplicate row', async () => {
|
|
366
|
-
const test = new Test(store).build();
|
|
367
|
-
await test.clearTables();
|
|
368
|
-
const thread = test.generateSampleThread();
|
|
369
|
-
await store.saveThread({ thread });
|
|
370
|
-
const baseMessage = test.generateSampleMessageV2({
|
|
371
|
-
threadId: thread.id,
|
|
372
|
-
content: 'Original',
|
|
373
|
-
resourceId: thread.resourceId,
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
// Insert the message for the first time
|
|
377
|
-
await store.saveMessages({ messages: [baseMessage], format: 'v2' });
|
|
378
|
-
|
|
379
|
-
// Insert again with the same id and threadId but different content
|
|
380
|
-
const updatedMessage = {
|
|
381
|
-
...test.generateSampleMessageV2({
|
|
382
|
-
threadId: thread.id,
|
|
383
|
-
content: 'Updated',
|
|
384
|
-
resourceId: thread.resourceId,
|
|
385
|
-
}),
|
|
386
|
-
createdAt: baseMessage.createdAt,
|
|
387
|
-
id: baseMessage.id,
|
|
388
|
-
};
|
|
389
|
-
|
|
390
|
-
await store.saveMessages({ messages: [updatedMessage], format: 'v2' });
|
|
391
|
-
|
|
392
|
-
// Retrieve messages for the thread
|
|
393
|
-
const retrievedMessages = await store.getMessages({ threadId: thread.id, format: 'v2' });
|
|
394
|
-
|
|
395
|
-
// Only one message should exist for that id+threadId
|
|
396
|
-
expect(retrievedMessages.filter(m => m.id === baseMessage.id)).toHaveLength(1);
|
|
397
|
-
|
|
398
|
-
// The content should be the updated one
|
|
399
|
-
expect(retrievedMessages.find(m => m.id === baseMessage.id)?.content.content).toBe('Updated');
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
it('should upsert messages: duplicate id and different threadid', async () => {
|
|
403
|
-
const test = new Test(store).build();
|
|
404
|
-
const thread1 = test.generateSampleThread();
|
|
405
|
-
const thread2 = test.generateSampleThread();
|
|
406
|
-
await store.saveThread({ thread: thread1 });
|
|
407
|
-
await store.saveThread({ thread: thread2 });
|
|
408
|
-
|
|
409
|
-
const message = test.generateSampleMessageV2({
|
|
410
|
-
threadId: thread1.id,
|
|
411
|
-
content: 'Thread1 Content',
|
|
412
|
-
resourceId: thread1.resourceId,
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
// Insert message into thread1
|
|
416
|
-
await store.saveMessages({ messages: [message], format: 'v2' });
|
|
417
|
-
|
|
418
|
-
// Attempt to insert a message with the same id but different threadId
|
|
419
|
-
const conflictingMessage = {
|
|
420
|
-
...test.generateSampleMessageV2({
|
|
421
|
-
threadId: thread2.id, // different thread
|
|
422
|
-
content: 'Thread2 Content',
|
|
423
|
-
resourceId: thread2.resourceId,
|
|
424
|
-
}),
|
|
425
|
-
createdAt: message.createdAt,
|
|
426
|
-
id: message.id,
|
|
427
|
-
};
|
|
428
|
-
|
|
429
|
-
// Save should move the message to the new thread
|
|
430
|
-
await store.saveMessages({ messages: [conflictingMessage], format: 'v2' });
|
|
431
|
-
|
|
432
|
-
// Retrieve messages for both threads
|
|
433
|
-
const thread1Messages = await store.getMessages({ threadId: thread1.id, format: 'v2' });
|
|
434
|
-
const thread2Messages = await store.getMessages({ threadId: thread2.id, format: 'v2' });
|
|
435
|
-
|
|
436
|
-
// Thread 1 should NOT have the message with that id
|
|
437
|
-
expect(thread1Messages.find(m => m.id === message.id)).toBeUndefined();
|
|
438
|
-
|
|
439
|
-
// Thread 2 should have the message with that id
|
|
440
|
-
expect(thread2Messages.find(m => m.id === message.id)?.content.content).toBe('Thread2 Content');
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
// it('should retrieve messages w/ next/prev messages by message id + resource id', async () => {
|
|
444
|
-
// const test = new Test(store).build();
|
|
445
|
-
// const messages: MastraMessageV2[] = [
|
|
446
|
-
// test.generateSampleMessageV2({ threadId: 'thread-one', content: 'First', resourceId: 'cross-thread-resource' }),
|
|
447
|
-
// test.generateSampleMessageV2({
|
|
448
|
-
// threadId: 'thread-one',
|
|
449
|
-
// content: 'Second',
|
|
450
|
-
// resourceId: 'cross-thread-resource',
|
|
451
|
-
// }),
|
|
452
|
-
// test.generateSampleMessageV2({ threadId: 'thread-one', content: 'Third', resourceId: 'cross-thread-resource' }),
|
|
453
|
-
|
|
454
|
-
// test.generateSampleMessageV2({
|
|
455
|
-
// threadId: 'thread-two',
|
|
456
|
-
// content: 'Fourth',
|
|
457
|
-
// resourceId: 'cross-thread-resource',
|
|
458
|
-
// }),
|
|
459
|
-
// test.generateSampleMessageV2({ threadId: 'thread-two', content: 'Fifth', resourceId: 'cross-thread-resource' }),
|
|
460
|
-
// test.generateSampleMessageV2({ threadId: 'thread-two', content: 'Sixth', resourceId: 'cross-thread-resource' }),
|
|
461
|
-
|
|
462
|
-
// test.generateSampleMessageV2({ threadId: 'thread-three', content: 'Seventh', resourceId: 'other-resource' }),
|
|
463
|
-
// test.generateSampleMessageV2({ threadId: 'thread-three', content: 'Eighth', resourceId: 'other-resource' }),
|
|
464
|
-
// ];
|
|
465
|
-
|
|
466
|
-
// await store.saveMessages({ messages: messages, format: 'v2' });
|
|
467
|
-
|
|
468
|
-
// const retrievedMessages: MastraMessageV2[] = await store.getMessages({ threadId: 'thread-one', format: 'v2' });
|
|
469
|
-
// expect(retrievedMessages).toHaveLength(3);
|
|
470
|
-
// expect(retrievedMessages.map(m => (m.content.parts[0] as any).text)).toEqual(['First', 'Second', 'Third']);
|
|
471
|
-
|
|
472
|
-
// const retrievedMessages2: MastraMessageV2[] = await store.getMessages({ threadId: 'thread-two', format: 'v2' });
|
|
473
|
-
// expect(retrievedMessages2).toHaveLength(3);
|
|
474
|
-
// expect(retrievedMessages2.map(m => (m.content.parts[0] as any).text)).toEqual(['Fourth', 'Fifth', 'Sixth']);
|
|
475
|
-
|
|
476
|
-
// const retrievedMessages3: MastraMessageV2[] = await store.getMessages({ threadId: 'thread-three', format: 'v2' });
|
|
477
|
-
// expect(retrievedMessages3).toHaveLength(2);
|
|
478
|
-
// expect(retrievedMessages3.map(m => (m.content.parts[0] as any).text)).toEqual(['Seventh', 'Eighth']);
|
|
479
|
-
|
|
480
|
-
// const crossThreadMessages: MastraMessageV2[] = await store.getMessages({
|
|
481
|
-
// threadId: 'thread-doesnt-exist',
|
|
482
|
-
// resourceId: 'cross-thread-resource',
|
|
483
|
-
// format: 'v2',
|
|
484
|
-
// selectBy: {
|
|
485
|
-
// last: 0,
|
|
486
|
-
// include: [
|
|
487
|
-
// {
|
|
488
|
-
// id: messages[1].id,
|
|
489
|
-
// withNextMessages: 2,
|
|
490
|
-
// withPreviousMessages: 2,
|
|
491
|
-
// },
|
|
492
|
-
// {
|
|
493
|
-
// id: messages[4].id,
|
|
494
|
-
// withPreviousMessages: 2,
|
|
495
|
-
// withNextMessages: 2,
|
|
496
|
-
// },
|
|
497
|
-
// ],
|
|
498
|
-
// },
|
|
499
|
-
// });
|
|
500
|
-
|
|
501
|
-
// expect(crossThreadMessages).toHaveLength(6);
|
|
502
|
-
// expect(crossThreadMessages.filter(m => m.threadId === `thread-one`)).toHaveLength(3);
|
|
503
|
-
// expect(crossThreadMessages.filter(m => m.threadId === `thread-two`)).toHaveLength(3);
|
|
504
|
-
|
|
505
|
-
// const crossThreadMessages2: MastraMessageV2[] = await store.getMessages({
|
|
506
|
-
// threadId: 'thread-one',
|
|
507
|
-
// resourceId: 'cross-thread-resource',
|
|
508
|
-
// format: 'v2',
|
|
509
|
-
// selectBy: {
|
|
510
|
-
// last: 0,
|
|
511
|
-
// include: [
|
|
512
|
-
// {
|
|
513
|
-
// id: messages[4].id,
|
|
514
|
-
// withPreviousMessages: 1,
|
|
515
|
-
// withNextMessages: 30,
|
|
516
|
-
// },
|
|
517
|
-
// ],
|
|
518
|
-
// },
|
|
519
|
-
// });
|
|
520
|
-
|
|
521
|
-
// expect(crossThreadMessages2).toHaveLength(3);
|
|
522
|
-
// expect(crossThreadMessages2.filter(m => m.threadId === `thread-one`)).toHaveLength(0);
|
|
523
|
-
// expect(crossThreadMessages2.filter(m => m.threadId === `thread-two`)).toHaveLength(3);
|
|
524
|
-
|
|
525
|
-
// const crossThreadMessages3: MastraMessageV2[] = await store.getMessages({
|
|
526
|
-
// threadId: 'thread-two',
|
|
527
|
-
// resourceId: 'cross-thread-resource',
|
|
528
|
-
// format: 'v2',
|
|
529
|
-
// selectBy: {
|
|
530
|
-
// last: 0,
|
|
531
|
-
// include: [
|
|
532
|
-
// {
|
|
533
|
-
// id: messages[1].id,
|
|
534
|
-
// withNextMessages: 1,
|
|
535
|
-
// withPreviousMessages: 1,
|
|
536
|
-
// },
|
|
537
|
-
// ],
|
|
538
|
-
// },
|
|
539
|
-
// });
|
|
540
|
-
|
|
541
|
-
// expect(crossThreadMessages3).toHaveLength(3);
|
|
542
|
-
// expect(crossThreadMessages3.filter(m => m.threadId === `thread-one`)).toHaveLength(3);
|
|
543
|
-
// expect(crossThreadMessages3.filter(m => m.threadId === `thread-two`)).toHaveLength(0);
|
|
544
|
-
// });
|
|
545
|
-
});
|
|
546
|
-
|
|
547
|
-
describe('Edge Cases and Error Handling', () => {
|
|
548
|
-
it('should handle large metadata objects', async () => {
|
|
549
|
-
const test = new Test(store).build();
|
|
550
|
-
await test.clearTables();
|
|
551
|
-
const thread = test.generateSampleThread();
|
|
552
|
-
const largeMetadata = {
|
|
553
|
-
...thread.metadata,
|
|
554
|
-
largeArray: Array.from({ length: 1000 }, (_, i) => ({ index: i, data: 'test'.repeat(100) })),
|
|
555
|
-
};
|
|
556
|
-
|
|
557
|
-
const threadWithLargeMetadata = {
|
|
558
|
-
...thread,
|
|
559
|
-
metadata: largeMetadata,
|
|
560
|
-
};
|
|
561
|
-
|
|
562
|
-
await store.saveThread({ thread: threadWithLargeMetadata });
|
|
563
|
-
const retrieved = await store.getThreadById({ threadId: thread.id });
|
|
564
|
-
|
|
565
|
-
expect(retrieved?.metadata).toEqual(largeMetadata);
|
|
566
|
-
});
|
|
567
|
-
|
|
568
|
-
it('should handle special characters in thread titles', async () => {
|
|
569
|
-
const test = new Test(store).build();
|
|
570
|
-
await test.clearTables();
|
|
571
|
-
const thread = test.generateSampleThread({
|
|
572
|
-
title: 'Special \'quotes\' and "double quotes" and emoji 🎉',
|
|
573
|
-
});
|
|
574
|
-
|
|
575
|
-
await store.saveThread({ thread });
|
|
576
|
-
const retrieved = await store.getThreadById({ threadId: thread.id });
|
|
577
|
-
|
|
578
|
-
expect(retrieved?.title).toBe(thread.title);
|
|
579
|
-
});
|
|
580
|
-
|
|
581
|
-
it('should handle concurrent thread updates', async () => {
|
|
582
|
-
const test = new Test(store).build();
|
|
583
|
-
await test.clearTables();
|
|
584
|
-
const thread = test.generateSampleThread();
|
|
585
|
-
await store.saveThread({ thread });
|
|
32
|
+
describe('with connection handler', () => {
|
|
33
|
+
const validWithConnectionHandlerConfig = {
|
|
34
|
+
connectorHandler: {} as ConnectorHandler,
|
|
35
|
+
};
|
|
586
36
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
id: thread.id,
|
|
591
|
-
title: `Update ${i}`,
|
|
592
|
-
metadata: { update: i },
|
|
593
|
-
}),
|
|
37
|
+
it('not throws if url is empty', () => {
|
|
38
|
+
expect(() => new MongoDBStore({ ...validWithConnectionHandlerConfig, url: '' })).not.toThrow(
|
|
39
|
+
/url must be provided and cannot be empty/,
|
|
594
40
|
);
|
|
595
|
-
|
|
596
|
-
await expect(Promise.all(updates)).resolves.toBeDefined();
|
|
597
|
-
|
|
598
|
-
// Verify final state
|
|
599
|
-
const finalThread = await store.getThreadById({ threadId: thread.id });
|
|
600
|
-
expect(finalThread).toBeDefined();
|
|
601
|
-
});
|
|
602
|
-
});
|
|
603
|
-
|
|
604
|
-
describe('Workflow Snapshots', () => {
|
|
605
|
-
it('should persist and load workflow snapshots', async () => {
|
|
606
|
-
const test = new Test(store).build();
|
|
607
|
-
await test.clearTables();
|
|
608
|
-
const workflowName = 'test-workflow';
|
|
609
|
-
const runId = `run-${randomUUID()}`;
|
|
610
|
-
const snapshot = {
|
|
611
|
-
status: 'running',
|
|
612
|
-
context: {
|
|
613
|
-
stepResults: {},
|
|
614
|
-
attempts: {},
|
|
615
|
-
triggerData: { type: 'manual' },
|
|
616
|
-
},
|
|
617
|
-
} as any;
|
|
618
|
-
|
|
619
|
-
await store.persistWorkflowSnapshot({
|
|
620
|
-
workflowName,
|
|
621
|
-
runId,
|
|
622
|
-
snapshot,
|
|
623
|
-
});
|
|
624
|
-
|
|
625
|
-
const loadedSnapshot = await store.loadWorkflowSnapshot({
|
|
626
|
-
workflowName,
|
|
627
|
-
runId,
|
|
628
|
-
});
|
|
629
|
-
|
|
630
|
-
expect(loadedSnapshot).toEqual(snapshot);
|
|
631
|
-
});
|
|
632
|
-
|
|
633
|
-
it('should return null for non-existent workflow snapshot', async () => {
|
|
634
|
-
const result = await store.loadWorkflowSnapshot({
|
|
635
|
-
workflowName: 'non-existent',
|
|
636
|
-
runId: 'non-existent',
|
|
637
|
-
});
|
|
638
|
-
|
|
639
|
-
expect(result).toBeNull();
|
|
640
|
-
});
|
|
641
|
-
|
|
642
|
-
it('should update existing workflow snapshot', async () => {
|
|
643
|
-
const workflowName = 'test-workflow';
|
|
644
|
-
const runId = `run-${randomUUID()}`;
|
|
645
|
-
const initialSnapshot = {
|
|
646
|
-
status: 'running',
|
|
647
|
-
context: {
|
|
648
|
-
stepResults: {},
|
|
649
|
-
attempts: {},
|
|
650
|
-
triggerData: { type: 'manual' },
|
|
651
|
-
},
|
|
652
|
-
};
|
|
653
|
-
|
|
654
|
-
await store.persistWorkflowSnapshot({
|
|
655
|
-
workflowName,
|
|
656
|
-
runId,
|
|
657
|
-
snapshot: initialSnapshot as any,
|
|
658
|
-
});
|
|
659
|
-
|
|
660
|
-
const updatedSnapshot = {
|
|
661
|
-
status: 'completed',
|
|
662
|
-
context: {
|
|
663
|
-
stepResults: {
|
|
664
|
-
'step-1': { status: 'success', result: { data: 'test' } },
|
|
665
|
-
},
|
|
666
|
-
attempts: { 'step-1': 1 },
|
|
667
|
-
triggerData: { type: 'manual' },
|
|
668
|
-
},
|
|
669
|
-
} as any;
|
|
670
|
-
|
|
671
|
-
await store.persistWorkflowSnapshot({
|
|
672
|
-
workflowName,
|
|
673
|
-
runId,
|
|
674
|
-
snapshot: updatedSnapshot,
|
|
675
|
-
});
|
|
676
|
-
|
|
677
|
-
const loadedSnapshot = await store.loadWorkflowSnapshot({
|
|
678
|
-
workflowName,
|
|
679
|
-
runId,
|
|
680
|
-
});
|
|
681
|
-
|
|
682
|
-
expect(loadedSnapshot).toEqual(updatedSnapshot);
|
|
683
|
-
});
|
|
684
|
-
|
|
685
|
-
it('should handle complex workflow state', async () => {
|
|
686
|
-
const workflowName = 'complex-workflow';
|
|
687
|
-
const runId = `run-${randomUUID()}`;
|
|
688
|
-
const complexSnapshot = {
|
|
689
|
-
value: { currentState: 'running' },
|
|
690
|
-
context: {
|
|
691
|
-
stepResults: {
|
|
692
|
-
'step-1': {
|
|
693
|
-
status: 'success',
|
|
694
|
-
result: {
|
|
695
|
-
nestedData: {
|
|
696
|
-
array: [1, 2, 3],
|
|
697
|
-
object: { key: 'value' },
|
|
698
|
-
date: new Date().toISOString(),
|
|
699
|
-
},
|
|
700
|
-
},
|
|
701
|
-
},
|
|
702
|
-
'step-2': {
|
|
703
|
-
status: 'waiting',
|
|
704
|
-
dependencies: ['step-3', 'step-4'],
|
|
705
|
-
},
|
|
706
|
-
},
|
|
707
|
-
attempts: { 'step-1': 1, 'step-2': 0 },
|
|
708
|
-
triggerData: {
|
|
709
|
-
type: 'scheduled',
|
|
710
|
-
metadata: {
|
|
711
|
-
schedule: '0 0 * * *',
|
|
712
|
-
timezone: 'UTC',
|
|
713
|
-
},
|
|
714
|
-
},
|
|
715
|
-
},
|
|
716
|
-
activePaths: [
|
|
717
|
-
{
|
|
718
|
-
stepPath: ['step-1'],
|
|
719
|
-
stepId: 'step-1',
|
|
720
|
-
status: 'success',
|
|
721
|
-
},
|
|
722
|
-
{
|
|
723
|
-
stepPath: ['step-2'],
|
|
724
|
-
stepId: 'step-2',
|
|
725
|
-
status: 'waiting',
|
|
726
|
-
},
|
|
727
|
-
],
|
|
728
|
-
serializedStepGraph: [],
|
|
729
|
-
runId: runId,
|
|
730
|
-
status: 'running',
|
|
731
|
-
timestamp: Date.now(),
|
|
732
|
-
};
|
|
733
|
-
|
|
734
|
-
await store.persistWorkflowSnapshot({
|
|
735
|
-
workflowName,
|
|
736
|
-
runId,
|
|
737
|
-
snapshot: complexSnapshot as unknown as WorkflowRunState,
|
|
738
|
-
});
|
|
739
|
-
|
|
740
|
-
const loadedSnapshot = await store.loadWorkflowSnapshot({
|
|
741
|
-
workflowName,
|
|
742
|
-
runId,
|
|
743
|
-
});
|
|
744
|
-
|
|
745
|
-
expect(loadedSnapshot).toEqual(complexSnapshot);
|
|
746
|
-
});
|
|
747
|
-
});
|
|
748
|
-
|
|
749
|
-
describe('getWorkflowRuns', () => {
|
|
750
|
-
it('returns empty array when no workflows exist', async () => {
|
|
751
|
-
const test = new Test(store).build();
|
|
752
|
-
await test.clearTables();
|
|
753
|
-
|
|
754
|
-
const { runs, total } = await store.getWorkflowRuns();
|
|
755
|
-
expect(runs).toEqual([]);
|
|
756
|
-
expect(total).toBe(0);
|
|
757
41
|
});
|
|
758
42
|
|
|
759
|
-
it('
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
const
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
const {
|
|
767
|
-
snapshot: workflow1,
|
|
768
|
-
runId: runId1,
|
|
769
|
-
stepId: stepId1,
|
|
770
|
-
} = test.generateSampleWorkflowSnapshot({ status: 'completed' });
|
|
771
|
-
const {
|
|
772
|
-
snapshot: workflow2,
|
|
773
|
-
runId: runId2,
|
|
774
|
-
stepId: stepId2,
|
|
775
|
-
} = test.generateSampleWorkflowSnapshot({ status: 'running' });
|
|
776
|
-
|
|
777
|
-
await store.persistWorkflowSnapshot({ workflowName: workflowName1, runId: runId1, snapshot: workflow1 });
|
|
778
|
-
await new Promise(resolve => setTimeout(resolve, 10)); // Small delay to ensure different timestamps
|
|
779
|
-
await store.persistWorkflowSnapshot({ workflowName: workflowName2, runId: runId2, snapshot: workflow2 });
|
|
780
|
-
|
|
781
|
-
const { runs, total } = await store.getWorkflowRuns();
|
|
782
|
-
expect(runs).toHaveLength(2);
|
|
783
|
-
expect(total).toBe(2);
|
|
784
|
-
expect(runs[0]!.workflowName).toBe(workflowName2); // Most recent first
|
|
785
|
-
expect(runs[1]!.workflowName).toBe(workflowName1);
|
|
786
|
-
const firstSnapshot = runs[0]!.snapshot as WorkflowRunState;
|
|
787
|
-
const secondSnapshot = runs[1]!.snapshot as WorkflowRunState;
|
|
788
|
-
expect(firstSnapshot.context?.[stepId2]?.status).toBe('running');
|
|
789
|
-
expect(secondSnapshot.context?.[stepId1]?.status).toBe('completed');
|
|
790
|
-
});
|
|
791
|
-
|
|
792
|
-
it('filters by workflow name', async () => {
|
|
793
|
-
const test = new Test(store).build();
|
|
794
|
-
await test.clearTables();
|
|
795
|
-
const workflowName1 = 'filter_test_1';
|
|
796
|
-
const workflowName2 = 'filter_test_2';
|
|
797
|
-
|
|
798
|
-
const {
|
|
799
|
-
snapshot: workflow1,
|
|
800
|
-
runId: runId1,
|
|
801
|
-
stepId: stepId1,
|
|
802
|
-
} = test.generateSampleWorkflowSnapshot({ status: 'completed' });
|
|
803
|
-
const { snapshot: workflow2, runId: runId2 } = test.generateSampleWorkflowSnapshot({ status: 'failed' });
|
|
804
|
-
|
|
805
|
-
await store.persistWorkflowSnapshot({ workflowName: workflowName1, runId: runId1, snapshot: workflow1 });
|
|
806
|
-
await new Promise(resolve => setTimeout(resolve, 10)); // Small delay to ensure different timestamps
|
|
807
|
-
await store.persistWorkflowSnapshot({ workflowName: workflowName2, runId: runId2, snapshot: workflow2 });
|
|
808
|
-
|
|
809
|
-
const { runs, total } = await store.getWorkflowRuns({ workflowName: workflowName1 });
|
|
810
|
-
expect(runs).toHaveLength(1);
|
|
811
|
-
expect(total).toBe(1);
|
|
812
|
-
expect(runs[0]!.workflowName).toBe(workflowName1);
|
|
813
|
-
const snapshot = runs[0]!.snapshot as WorkflowRunState;
|
|
814
|
-
expect(snapshot.context?.[stepId1]?.status).toBe('completed');
|
|
815
|
-
});
|
|
816
|
-
|
|
817
|
-
it('filters by date range', async () => {
|
|
818
|
-
const test = new Test(store).build();
|
|
819
|
-
await test.clearTables();
|
|
820
|
-
const now = new Date();
|
|
821
|
-
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
|
822
|
-
const twoDaysAgo = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000);
|
|
823
|
-
const workflowName1 = 'date_test_1';
|
|
824
|
-
const workflowName2 = 'date_test_2';
|
|
825
|
-
const workflowName3 = 'date_test_3';
|
|
826
|
-
|
|
827
|
-
const { snapshot: workflow1, runId: runId1 } = test.generateSampleWorkflowSnapshot({ status: 'completed' });
|
|
828
|
-
const {
|
|
829
|
-
snapshot: workflow2,
|
|
830
|
-
runId: runId2,
|
|
831
|
-
stepId: stepId2,
|
|
832
|
-
} = test.generateSampleWorkflowSnapshot({ status: 'running' });
|
|
833
|
-
const {
|
|
834
|
-
snapshot: workflow3,
|
|
835
|
-
runId: runId3,
|
|
836
|
-
stepId: stepId3,
|
|
837
|
-
} = test.generateSampleWorkflowSnapshot({ status: 'waiting' });
|
|
838
|
-
|
|
839
|
-
await store.insert({
|
|
840
|
-
tableName: TABLE_WORKFLOW_SNAPSHOT,
|
|
841
|
-
record: {
|
|
842
|
-
workflow_name: workflowName1,
|
|
843
|
-
run_id: runId1,
|
|
844
|
-
snapshot: workflow1,
|
|
845
|
-
createdAt: twoDaysAgo,
|
|
846
|
-
updatedAt: twoDaysAgo,
|
|
847
|
-
},
|
|
848
|
-
});
|
|
849
|
-
await store.insert({
|
|
850
|
-
tableName: TABLE_WORKFLOW_SNAPSHOT,
|
|
851
|
-
record: {
|
|
852
|
-
workflow_name: workflowName2,
|
|
853
|
-
run_id: runId2,
|
|
854
|
-
snapshot: workflow2,
|
|
855
|
-
createdAt: yesterday,
|
|
856
|
-
updatedAt: yesterday,
|
|
857
|
-
},
|
|
858
|
-
});
|
|
859
|
-
await store.insert({
|
|
860
|
-
tableName: TABLE_WORKFLOW_SNAPSHOT,
|
|
861
|
-
record: {
|
|
862
|
-
workflow_name: workflowName3,
|
|
863
|
-
run_id: runId3,
|
|
864
|
-
snapshot: workflow3,
|
|
865
|
-
createdAt: now,
|
|
866
|
-
updatedAt: now,
|
|
867
|
-
},
|
|
868
|
-
});
|
|
869
|
-
|
|
870
|
-
const { runs } = await store.getWorkflowRuns({
|
|
871
|
-
fromDate: yesterday,
|
|
872
|
-
toDate: now,
|
|
873
|
-
});
|
|
874
|
-
|
|
875
|
-
expect(runs).toHaveLength(2);
|
|
876
|
-
expect(runs[0]!.workflowName).toBe(workflowName3);
|
|
877
|
-
expect(runs[1]!.workflowName).toBe(workflowName2);
|
|
878
|
-
const firstSnapshot = runs[0]!.snapshot as WorkflowRunState;
|
|
879
|
-
const secondSnapshot = runs[1]!.snapshot as WorkflowRunState;
|
|
880
|
-
expect(firstSnapshot.context?.[stepId3]?.status).toBe('waiting');
|
|
881
|
-
expect(secondSnapshot.context?.[stepId2]?.status).toBe('running');
|
|
882
|
-
});
|
|
883
|
-
|
|
884
|
-
it('handles pagination', async () => {
|
|
885
|
-
const test = new Test(store).build();
|
|
886
|
-
await test.clearTables();
|
|
887
|
-
const workflowName1 = 'page_test_1';
|
|
888
|
-
const workflowName2 = 'page_test_2';
|
|
889
|
-
const workflowName3 = 'page_test_3';
|
|
890
|
-
|
|
891
|
-
const {
|
|
892
|
-
snapshot: workflow1,
|
|
893
|
-
runId: runId1,
|
|
894
|
-
stepId: stepId1,
|
|
895
|
-
} = test.generateSampleWorkflowSnapshot({ status: 'completed' });
|
|
896
|
-
const {
|
|
897
|
-
snapshot: workflow2,
|
|
898
|
-
runId: runId2,
|
|
899
|
-
stepId: stepId2,
|
|
900
|
-
} = test.generateSampleWorkflowSnapshot({ status: 'running' });
|
|
901
|
-
const {
|
|
902
|
-
snapshot: workflow3,
|
|
903
|
-
runId: runId3,
|
|
904
|
-
stepId: stepId3,
|
|
905
|
-
} = test.generateSampleWorkflowSnapshot({ status: 'waiting' });
|
|
906
|
-
|
|
907
|
-
await store.persistWorkflowSnapshot({ workflowName: workflowName1, runId: runId1, snapshot: workflow1 });
|
|
908
|
-
await new Promise(resolve => setTimeout(resolve, 10)); // Small delay to ensure different timestamps
|
|
909
|
-
await store.persistWorkflowSnapshot({ workflowName: workflowName2, runId: runId2, snapshot: workflow2 });
|
|
910
|
-
await new Promise(resolve => setTimeout(resolve, 10)); // Small delay to ensure different timestamps
|
|
911
|
-
await store.persistWorkflowSnapshot({ workflowName: workflowName3, runId: runId3, snapshot: workflow3 });
|
|
912
|
-
|
|
913
|
-
// Get first page
|
|
914
|
-
const page1 = await store.getWorkflowRuns({ limit: 2, offset: 0 });
|
|
915
|
-
expect(page1.runs).toHaveLength(2);
|
|
916
|
-
expect(page1.total).toBe(3); // Total count of all records
|
|
917
|
-
expect(page1.runs[0]!.workflowName).toBe(workflowName3);
|
|
918
|
-
expect(page1.runs[1]!.workflowName).toBe(workflowName2);
|
|
919
|
-
const firstSnapshot = page1.runs[0]!.snapshot as WorkflowRunState;
|
|
920
|
-
const secondSnapshot = page1.runs[1]!.snapshot as WorkflowRunState;
|
|
921
|
-
expect(firstSnapshot.context?.[stepId3]?.status).toBe('waiting');
|
|
922
|
-
expect(secondSnapshot.context?.[stepId2]?.status).toBe('running');
|
|
923
|
-
|
|
924
|
-
// Get second page
|
|
925
|
-
const page2 = await store.getWorkflowRuns({ limit: 2, offset: 2 });
|
|
926
|
-
expect(page2.runs).toHaveLength(1);
|
|
927
|
-
expect(page2.total).toBe(3);
|
|
928
|
-
expect(page2.runs[0]!.workflowName).toBe(workflowName1);
|
|
929
|
-
const snapshot = page2.runs[0]!.snapshot as WorkflowRunState;
|
|
930
|
-
expect(snapshot.context?.[stepId1]?.status).toBe('completed');
|
|
931
|
-
});
|
|
932
|
-
});
|
|
933
|
-
|
|
934
|
-
describe('Trace Operations', () => {
|
|
935
|
-
const sampleTrace = (
|
|
936
|
-
name: string,
|
|
937
|
-
scope: string,
|
|
938
|
-
startTime = Date.now(),
|
|
939
|
-
attributes: Record<string, string> = {},
|
|
940
|
-
) => ({
|
|
941
|
-
id: `trace-${randomUUID()}`,
|
|
942
|
-
parentSpanId: `span-${randomUUID()}`,
|
|
943
|
-
traceId: `traceid-${randomUUID()}`,
|
|
944
|
-
name,
|
|
945
|
-
scope,
|
|
946
|
-
kind: 1,
|
|
947
|
-
startTime: startTime,
|
|
948
|
-
endTime: startTime + 100,
|
|
949
|
-
status: JSON.stringify({ code: 0 }),
|
|
950
|
-
attributes: JSON.stringify({ key: 'value', scopeAttr: scope, ...attributes }),
|
|
951
|
-
events: JSON.stringify([{ name: 'event1', timestamp: startTime + 50 }]),
|
|
952
|
-
links: JSON.stringify([]),
|
|
953
|
-
createdAt: new Date(startTime).toISOString(),
|
|
954
|
-
updatedAt: new Date(startTime).toISOString(),
|
|
955
|
-
});
|
|
956
|
-
|
|
957
|
-
beforeEach(async () => {
|
|
958
|
-
const test = new Test(store).build();
|
|
959
|
-
await test.clearTables();
|
|
960
|
-
});
|
|
961
|
-
|
|
962
|
-
it('should batch insert and retrieve traces', async () => {
|
|
963
|
-
const trace1 = sampleTrace('trace-op-1', 'scope-A');
|
|
964
|
-
const trace2 = sampleTrace('trace-op-2', 'scope-A', Date.now() + 10);
|
|
965
|
-
const trace3 = sampleTrace('trace-op-3', 'scope-B', Date.now() + 20);
|
|
966
|
-
const records = [trace1, trace2, trace3];
|
|
967
|
-
|
|
968
|
-
await store.batchInsert({ tableName: TABLE_TRACES, records });
|
|
969
|
-
|
|
970
|
-
const allTraces = await store.getTraces();
|
|
971
|
-
expect(allTraces.length).toBe(3);
|
|
972
|
-
});
|
|
973
|
-
|
|
974
|
-
it('should handle Date objects for createdAt/updatedAt fields in batchInsert', async () => {
|
|
975
|
-
const now = new Date();
|
|
976
|
-
const traceWithDateObjects = {
|
|
977
|
-
id: `trace-${randomUUID()}`,
|
|
978
|
-
parentSpanId: `span-${randomUUID()}`,
|
|
979
|
-
traceId: `traceid-${randomUUID()}`,
|
|
980
|
-
name: 'test-trace-with-dates',
|
|
981
|
-
scope: 'default-tracer',
|
|
982
|
-
kind: 1,
|
|
983
|
-
startTime: now.getTime(),
|
|
984
|
-
endTime: now.getTime() + 100,
|
|
985
|
-
status: JSON.stringify({ code: 0 }),
|
|
986
|
-
attributes: JSON.stringify({ key: 'value' }),
|
|
987
|
-
events: JSON.stringify([]),
|
|
988
|
-
links: JSON.stringify([]),
|
|
989
|
-
createdAt: now,
|
|
990
|
-
updatedAt: now,
|
|
991
|
-
};
|
|
992
|
-
|
|
993
|
-
await store.batchInsert({ tableName: TABLE_TRACES, records: [traceWithDateObjects] });
|
|
994
|
-
|
|
995
|
-
const allTraces = await store.getTraces({ name: 'test-trace-with-dates', page: 0, perPage: 10 });
|
|
996
|
-
expect(allTraces.length).toBe(1);
|
|
997
|
-
expect(allTraces[0].name).toBe('test-trace-with-dates');
|
|
998
|
-
});
|
|
999
|
-
|
|
1000
|
-
it('should retrieve traces filtered by name', async () => {
|
|
1001
|
-
const now = Date.now();
|
|
1002
|
-
const trace1 = sampleTrace('trace-filter-name', 'scope-X', now);
|
|
1003
|
-
const trace2 = sampleTrace('trace-filter-name', 'scope-Y', now + 10);
|
|
1004
|
-
const trace3 = sampleTrace('other-name', 'scope-X', now + 20);
|
|
1005
|
-
await store.batchInsert({ tableName: TABLE_TRACES, records: [trace1, trace2, trace3] });
|
|
1006
|
-
|
|
1007
|
-
const filteredTraces = await store.getTraces({ name: 'trace-filter-name', page: 0, perPage: 10 });
|
|
1008
|
-
expect(filteredTraces.length).toBe(2);
|
|
1009
|
-
expect(filteredTraces.every(t => t.name === 'trace-filter-name')).toBe(true);
|
|
1010
|
-
expect(filteredTraces[0].scope).toBe('scope-Y');
|
|
1011
|
-
expect(filteredTraces[1].scope).toBe('scope-X');
|
|
1012
|
-
});
|
|
1013
|
-
|
|
1014
|
-
it('should retrieve traces filtered by attributes', async () => {
|
|
1015
|
-
const now = Date.now();
|
|
1016
|
-
const trace1 = sampleTrace('trace-filter-attribute-A', 'scope-X', now, { componentName: 'component-TARGET' });
|
|
1017
|
-
const trace2 = sampleTrace('trace-filter-attribute-B', 'scope-Y', now + 10, { componentName: 'component-OTHER' });
|
|
1018
|
-
const trace3 = sampleTrace('trace-filter-attribute-C', 'scope-Z', now + 20, {
|
|
1019
|
-
componentName: 'component-TARGET',
|
|
1020
|
-
andFilterTest: 'TARGET',
|
|
1021
|
-
});
|
|
1022
|
-
await store.batchInsert({ tableName: TABLE_TRACES, records: [trace1, trace2, trace3] });
|
|
1023
|
-
|
|
1024
|
-
const filteredTraces = await store.getTraces({
|
|
1025
|
-
attributes: { componentName: 'component-TARGET' },
|
|
1026
|
-
page: 0,
|
|
1027
|
-
perPage: 10,
|
|
1028
|
-
});
|
|
1029
|
-
expect(filteredTraces.length).toBe(2);
|
|
1030
|
-
expect(filteredTraces[0].name).toBe('trace-filter-attribute-C');
|
|
1031
|
-
expect(filteredTraces[1].name).toBe('trace-filter-attribute-A');
|
|
1032
|
-
|
|
1033
|
-
const filteredTraces2 = await store.getTraces({
|
|
1034
|
-
attributes: { componentName: 'component-TARGET', andFilterTest: 'TARGET' },
|
|
1035
|
-
page: 0,
|
|
1036
|
-
perPage: 10,
|
|
1037
|
-
});
|
|
1038
|
-
expect(filteredTraces2.length).toBe(1);
|
|
1039
|
-
expect(filteredTraces2[0].name).toBe('trace-filter-attribute-C');
|
|
1040
|
-
});
|
|
1041
|
-
|
|
1042
|
-
it('should retrieve traces filtered by scope', async () => {
|
|
1043
|
-
const now = Date.now();
|
|
1044
|
-
const trace1 = sampleTrace('trace-filter-scope-A', 'scope-TARGET', now);
|
|
1045
|
-
const trace2 = sampleTrace('trace-filter-scope-B', 'scope-OTHER', now + 10);
|
|
1046
|
-
const trace3 = sampleTrace('trace-filter-scope-C', 'scope-TARGET', now + 20);
|
|
1047
|
-
await store.batchInsert({ tableName: TABLE_TRACES, records: [trace1, trace2, trace3] });
|
|
1048
|
-
|
|
1049
|
-
const filteredTraces = await store.getTraces({ scope: 'scope-TARGET', page: 0, perPage: 10 });
|
|
1050
|
-
expect(filteredTraces.length).toBe(2);
|
|
1051
|
-
expect(filteredTraces.every(t => t.scope === 'scope-TARGET')).toBe(true);
|
|
1052
|
-
expect(filteredTraces[0].name).toBe('trace-filter-scope-C');
|
|
1053
|
-
expect(filteredTraces[1].name).toBe('trace-filter-scope-A');
|
|
1054
|
-
});
|
|
1055
|
-
|
|
1056
|
-
it('should handle pagination for getTraces', async () => {
|
|
1057
|
-
const now = Date.now();
|
|
1058
|
-
const traceData = Array.from({ length: 5 }, (_, i) => sampleTrace('trace-page', `scope-page`, now + i * 10));
|
|
1059
|
-
await store.batchInsert({ tableName: TABLE_TRACES, records: traceData });
|
|
1060
|
-
|
|
1061
|
-
const page1 = await store.getTraces({ name: 'trace-page', page: 0, perPage: 2 });
|
|
1062
|
-
expect(page1.length).toBe(2);
|
|
1063
|
-
expect(page1[0]!.startTime).toBe(traceData[4]!.startTime);
|
|
1064
|
-
expect(page1[1]!.startTime).toBe(traceData[3]!.startTime);
|
|
1065
|
-
|
|
1066
|
-
const page2 = await store.getTraces({ name: 'trace-page', page: 1, perPage: 2 });
|
|
1067
|
-
expect(page2.length).toBe(2);
|
|
1068
|
-
expect(page2[0]!.startTime).toBe(traceData[2]!.startTime);
|
|
1069
|
-
expect(page2[1]!.startTime).toBe(traceData[1]!.startTime);
|
|
1070
|
-
|
|
1071
|
-
const page3 = await store.getTraces({ name: 'trace-page', page: 2, perPage: 2 });
|
|
1072
|
-
expect(page3.length).toBe(1);
|
|
1073
|
-
expect(page3[0]!.startTime).toBe(traceData[0]!.startTime);
|
|
1074
|
-
|
|
1075
|
-
const page4 = await store.getTraces({ name: 'trace-page', page: 3, perPage: 2 });
|
|
1076
|
-
expect(page4.length).toBe(0);
|
|
1077
|
-
});
|
|
1078
|
-
});
|
|
1079
|
-
|
|
1080
|
-
describe('Eval Operations', () => {
|
|
1081
|
-
it('should retrieve evals by agent name', async () => {
|
|
1082
|
-
const test = new Test(store).build();
|
|
1083
|
-
await test.clearTables();
|
|
1084
|
-
const agentName = `test-agent-${randomUUID()}`;
|
|
1085
|
-
|
|
1086
|
-
// Create sample evals
|
|
1087
|
-
const liveEval = test.generateSampleEval(false, { agentName });
|
|
1088
|
-
const testEval = test.generateSampleEval(true, { agentName });
|
|
1089
|
-
const otherAgentEval = test.generateSampleEval(false, { agentName: `other-agent-${randomUUID()}` });
|
|
1090
|
-
|
|
1091
|
-
// Insert evals
|
|
1092
|
-
await store.insert({
|
|
1093
|
-
tableName: TABLE_EVALS,
|
|
1094
|
-
record: {
|
|
1095
|
-
agent_name: liveEval.agentName,
|
|
1096
|
-
input: liveEval.input,
|
|
1097
|
-
output: liveEval.output,
|
|
1098
|
-
result: JSON.stringify(liveEval.result),
|
|
1099
|
-
metric_name: liveEval.metricName,
|
|
1100
|
-
instructions: liveEval.instructions,
|
|
1101
|
-
test_info: null,
|
|
1102
|
-
global_run_id: liveEval.globalRunId,
|
|
1103
|
-
run_id: liveEval.runId,
|
|
1104
|
-
created_at: liveEval.createdAt,
|
|
1105
|
-
createdAt: new Date(liveEval.createdAt),
|
|
1106
|
-
},
|
|
1107
|
-
});
|
|
1108
|
-
|
|
1109
|
-
await store.insert({
|
|
1110
|
-
tableName: TABLE_EVALS,
|
|
1111
|
-
record: {
|
|
1112
|
-
agent_name: testEval.agentName,
|
|
1113
|
-
input: testEval.input,
|
|
1114
|
-
output: testEval.output,
|
|
1115
|
-
result: JSON.stringify(testEval.result),
|
|
1116
|
-
metric_name: testEval.metricName,
|
|
1117
|
-
instructions: testEval.instructions,
|
|
1118
|
-
test_info: JSON.stringify(testEval.testInfo),
|
|
1119
|
-
global_run_id: testEval.globalRunId,
|
|
1120
|
-
run_id: testEval.runId,
|
|
1121
|
-
created_at: testEval.createdAt,
|
|
1122
|
-
createdAt: new Date(testEval.createdAt),
|
|
1123
|
-
},
|
|
1124
|
-
});
|
|
1125
|
-
|
|
1126
|
-
await store.insert({
|
|
1127
|
-
tableName: TABLE_EVALS,
|
|
1128
|
-
record: {
|
|
1129
|
-
agent_name: otherAgentEval.agentName,
|
|
1130
|
-
input: otherAgentEval.input,
|
|
1131
|
-
output: otherAgentEval.output,
|
|
1132
|
-
result: JSON.stringify(otherAgentEval.result),
|
|
1133
|
-
metric_name: otherAgentEval.metricName,
|
|
1134
|
-
instructions: otherAgentEval.instructions,
|
|
1135
|
-
test_info: null,
|
|
1136
|
-
global_run_id: otherAgentEval.globalRunId,
|
|
1137
|
-
run_id: otherAgentEval.runId,
|
|
1138
|
-
created_at: otherAgentEval.createdAt,
|
|
1139
|
-
createdAt: new Date(otherAgentEval.createdAt),
|
|
1140
|
-
},
|
|
1141
|
-
});
|
|
1142
|
-
|
|
1143
|
-
// Test getting all evals for the agent
|
|
1144
|
-
const allEvals = await store.getEvalsByAgentName(agentName);
|
|
1145
|
-
expect(allEvals).toHaveLength(2);
|
|
1146
|
-
expect(allEvals.map(e => e.runId)).toEqual(expect.arrayContaining([liveEval.runId, testEval.runId]));
|
|
1147
|
-
expect(allEvals[0]!.result.score).toEqual(liveEval.result.score);
|
|
1148
|
-
expect(allEvals[1]!.result.score).toEqual(testEval.result.score);
|
|
1149
|
-
|
|
1150
|
-
// Test getting only live evals
|
|
1151
|
-
const liveEvals = await store.getEvalsByAgentName(agentName, 'live');
|
|
1152
|
-
expect(liveEvals).toHaveLength(1);
|
|
1153
|
-
expect(liveEvals[0]!.runId).toBe(liveEval.runId);
|
|
1154
|
-
expect(liveEvals[0]!.result.score).toEqual(liveEval.result.score);
|
|
1155
|
-
|
|
1156
|
-
// Test getting only test evals
|
|
1157
|
-
const testEvals = await store.getEvalsByAgentName(agentName, 'test');
|
|
1158
|
-
expect(testEvals).toHaveLength(1);
|
|
1159
|
-
expect(testEvals[0]!.runId).toBe(testEval.runId);
|
|
1160
|
-
expect(testEvals[0]!.result.score).toEqual(testEval.result.score);
|
|
1161
|
-
expect(testEvals[0]!.testInfo).toEqual(testEval.testInfo);
|
|
1162
|
-
|
|
1163
|
-
// Test getting evals for non-existent agent
|
|
1164
|
-
const nonExistentEvals = await store.getEvalsByAgentName('non-existent-agent');
|
|
1165
|
-
expect(nonExistentEvals).toHaveLength(0);
|
|
1166
|
-
});
|
|
1167
|
-
});
|
|
1168
|
-
|
|
1169
|
-
describe('alterTable (no-op/schemaless)', () => {
|
|
1170
|
-
const TEST_TABLE = 'test_alter_table'; // Use "table" or "collection" as appropriate
|
|
1171
|
-
beforeEach(async () => {
|
|
1172
|
-
await store.clearTable({ tableName: TEST_TABLE as TABLE_NAMES });
|
|
1173
|
-
});
|
|
1174
|
-
|
|
1175
|
-
afterEach(async () => {
|
|
1176
|
-
await store.clearTable({ tableName: TEST_TABLE as TABLE_NAMES });
|
|
1177
|
-
});
|
|
1178
|
-
|
|
1179
|
-
it('allows inserting records with new fields without alterTable', async () => {
|
|
1180
|
-
await store.insert({
|
|
1181
|
-
tableName: TEST_TABLE as TABLE_NAMES,
|
|
1182
|
-
record: { id: '1', name: 'Alice' },
|
|
1183
|
-
});
|
|
1184
|
-
await store.insert({
|
|
1185
|
-
tableName: TEST_TABLE as TABLE_NAMES,
|
|
1186
|
-
record: { id: '2', name: 'Bob', newField: 123 },
|
|
1187
|
-
});
|
|
1188
|
-
|
|
1189
|
-
const row = await store.load<{ id: string; name: string; newField?: number }[]>({
|
|
1190
|
-
tableName: TEST_TABLE as TABLE_NAMES,
|
|
1191
|
-
keys: { id: '2' },
|
|
1192
|
-
});
|
|
1193
|
-
expect(row?.[0]?.newField).toBe(123);
|
|
1194
|
-
});
|
|
1195
|
-
|
|
1196
|
-
it('does not throw when calling alterTable (no-op)', async () => {
|
|
1197
|
-
await expect(
|
|
1198
|
-
store.alterTable({
|
|
1199
|
-
tableName: TEST_TABLE as TABLE_NAMES,
|
|
1200
|
-
schema: {
|
|
1201
|
-
id: { type: 'text', primaryKey: true, nullable: false },
|
|
1202
|
-
name: { type: 'text', nullable: true },
|
|
1203
|
-
extra: { type: 'integer', nullable: true },
|
|
1204
|
-
},
|
|
1205
|
-
ifNotExists: ['extra'],
|
|
1206
|
-
}),
|
|
1207
|
-
).resolves.not.toThrow();
|
|
1208
|
-
});
|
|
1209
|
-
|
|
1210
|
-
it('can add multiple new fields at write time', async () => {
|
|
1211
|
-
await store.insert({
|
|
1212
|
-
tableName: TEST_TABLE as TABLE_NAMES,
|
|
1213
|
-
record: { id: '3', name: 'Charlie', age: 30, city: 'Paris' },
|
|
1214
|
-
});
|
|
1215
|
-
const row = await store.load<{ id: string; name: string; age?: number; city?: string }[]>({
|
|
1216
|
-
tableName: TEST_TABLE as TABLE_NAMES,
|
|
1217
|
-
keys: { id: '3' },
|
|
1218
|
-
});
|
|
1219
|
-
expect(row?.[0]?.age).toBe(30);
|
|
1220
|
-
expect(row?.[0]?.city).toBe('Paris');
|
|
1221
|
-
});
|
|
1222
|
-
|
|
1223
|
-
it('can retrieve all fields, including dynamically added ones', async () => {
|
|
1224
|
-
await store.insert({
|
|
1225
|
-
tableName: TEST_TABLE as TABLE_NAMES,
|
|
1226
|
-
record: { id: '4', name: 'Dana', hobby: 'skiing' },
|
|
1227
|
-
});
|
|
1228
|
-
const row = await store.load<{ id: string; name: string; hobby?: string }[]>({
|
|
1229
|
-
tableName: TEST_TABLE as TABLE_NAMES,
|
|
1230
|
-
keys: { id: '4' },
|
|
1231
|
-
});
|
|
1232
|
-
expect(row?.[0]?.hobby).toBe('skiing');
|
|
43
|
+
it('not throws if dbName is missing or empty', () => {
|
|
44
|
+
expect(() => new MongoDBStore({ ...validWithConnectionHandlerConfig, dbName: '' })).not.toThrow(
|
|
45
|
+
/dbName must be provided and cannot be empty/,
|
|
46
|
+
);
|
|
47
|
+
const { dbName, ...rest } = validWithConnectionHandlerConfig as any;
|
|
48
|
+
expect(() => new MongoDBStore(rest as any)).not.toThrow(/dbName must be provided and cannot be empty/);
|
|
1233
49
|
});
|
|
1234
50
|
|
|
1235
|
-
it('does not
|
|
1236
|
-
|
|
1237
|
-
store.insert({
|
|
1238
|
-
tableName: TEST_TABLE as TABLE_NAMES,
|
|
1239
|
-
record: { id: '5', weirdField: { nested: true }, another: [1, 2, 3] },
|
|
1240
|
-
}),
|
|
1241
|
-
).resolves.not.toThrow();
|
|
1242
|
-
|
|
1243
|
-
const row = await store.load<{ id: string; weirdField?: any; another?: any }[]>({
|
|
1244
|
-
tableName: TEST_TABLE as TABLE_NAMES,
|
|
1245
|
-
keys: { id: '5' },
|
|
1246
|
-
});
|
|
1247
|
-
expect(row?.[0]?.weirdField).toEqual({ nested: true });
|
|
1248
|
-
expect(row?.[0]?.another).toEqual([1, 2, 3]);
|
|
51
|
+
it('does not throw on valid config', () => {
|
|
52
|
+
expect(() => new MongoDBStore(validWithConnectionHandlerConfig)).not.toThrow();
|
|
1249
53
|
});
|
|
1250
54
|
});
|
|
1251
|
-
|
|
1252
|
-
afterAll(async () => {
|
|
1253
|
-
try {
|
|
1254
|
-
await store.close();
|
|
1255
|
-
} catch (error) {
|
|
1256
|
-
console.warn('Error closing store:', error);
|
|
1257
|
-
}
|
|
1258
|
-
});
|
|
1259
55
|
});
|
|
56
|
+
|
|
57
|
+
createTestSuite(new MongoDBStore(TEST_CONFIG));
|