@mastra/clickhouse 0.0.0-trigger-playground-ui-package-20250506151043 → 0.0.0-tsconfig-compile-20250703214351
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/CHANGELOG.md +426 -2
- package/dist/_tsup-dts-rollup.d.cts +61 -8
- package/dist/_tsup-dts-rollup.d.ts +61 -8
- package/dist/index.cjs +466 -114
- package/dist/index.js +449 -97
- package/package.json +14 -10
- package/src/storage/index.test.ts +417 -119
- package/src/storage/index.ts +520 -114
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { randomUUID } from 'crypto';
|
|
2
|
-
import
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
createSampleMessageV1,
|
|
4
|
+
createSampleThread,
|
|
5
|
+
createSampleWorkflowSnapshot,
|
|
6
|
+
checkWorkflowSnapshot,
|
|
7
|
+
createSampleMessageV2,
|
|
8
|
+
} from '@internal/storage-test-utils';
|
|
9
|
+
import type { MastraMessageV1, MastraMessageV2, StorageColumn, WorkflowRunState } from '@mastra/core';
|
|
10
|
+
import type { TABLE_NAMES } from '@mastra/core/storage';
|
|
4
11
|
import { TABLE_THREADS, TABLE_MESSAGES, TABLE_WORKFLOW_SNAPSHOT } from '@mastra/core/storage';
|
|
5
12
|
import { describe, it, expect, beforeAll, beforeEach, afterAll, vi, afterEach } from 'vitest';
|
|
6
13
|
|
|
7
|
-
import { ClickhouseStore } from '.';
|
|
14
|
+
import { ClickhouseStore, TABLE_ENGINES } from '.';
|
|
8
15
|
import type { ClickhouseConfig } from '.';
|
|
9
16
|
|
|
10
17
|
vi.setConfig({ testTimeout: 60_000, hookTimeout: 60_000 });
|
|
@@ -25,26 +32,6 @@ const TEST_CONFIG: ClickhouseConfig = {
|
|
|
25
32
|
},
|
|
26
33
|
};
|
|
27
34
|
|
|
28
|
-
// Sample test data factory functions
|
|
29
|
-
const createSampleThread = () => ({
|
|
30
|
-
id: `thread-${randomUUID()}`,
|
|
31
|
-
resourceId: `resource-${randomUUID()}`,
|
|
32
|
-
title: 'Test Thread',
|
|
33
|
-
createdAt: new Date(),
|
|
34
|
-
updatedAt: new Date(),
|
|
35
|
-
metadata: { key: 'value' },
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
const createSampleMessage = (threadId: string, createdAt: Date = new Date()): MessageType => ({
|
|
39
|
-
id: `msg-${randomUUID()}`,
|
|
40
|
-
resourceId: `resource-${randomUUID()}`,
|
|
41
|
-
role: 'user',
|
|
42
|
-
type: 'text',
|
|
43
|
-
threadId,
|
|
44
|
-
content: [{ type: 'text', text: 'Hello' }] as MessageType['content'],
|
|
45
|
-
createdAt,
|
|
46
|
-
});
|
|
47
|
-
|
|
48
35
|
const createSampleTrace = () => ({
|
|
49
36
|
id: `trace-${randomUUID()}`,
|
|
50
37
|
name: 'Test Trace',
|
|
@@ -60,42 +47,6 @@ const createSampleEval = () => ({
|
|
|
60
47
|
createdAt: new Date(),
|
|
61
48
|
});
|
|
62
49
|
|
|
63
|
-
const createSampleWorkflowSnapshot = (
|
|
64
|
-
status: WorkflowRunState['context']['steps'][string]['status'],
|
|
65
|
-
createdAt?: Date,
|
|
66
|
-
) => {
|
|
67
|
-
const runId = `run-${randomUUID()}`;
|
|
68
|
-
const stepId = `step-${randomUUID()}`;
|
|
69
|
-
const timestamp = createdAt || new Date();
|
|
70
|
-
const snapshot = {
|
|
71
|
-
result: { success: true },
|
|
72
|
-
value: {},
|
|
73
|
-
context: {
|
|
74
|
-
steps: {
|
|
75
|
-
[stepId]: {
|
|
76
|
-
status,
|
|
77
|
-
payload: {},
|
|
78
|
-
error: undefined,
|
|
79
|
-
},
|
|
80
|
-
},
|
|
81
|
-
triggerData: {},
|
|
82
|
-
attempts: {},
|
|
83
|
-
},
|
|
84
|
-
activePaths: [],
|
|
85
|
-
suspendedPaths: {},
|
|
86
|
-
runId,
|
|
87
|
-
timestamp: timestamp.getTime(),
|
|
88
|
-
};
|
|
89
|
-
return { snapshot, runId, stepId };
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
const checkWorkflowSnapshot = (snapshot: WorkflowRunState | string, stepId: string, status: string) => {
|
|
93
|
-
if (typeof snapshot === 'string') {
|
|
94
|
-
throw new Error('Expected WorkflowRunState, got string');
|
|
95
|
-
}
|
|
96
|
-
expect(snapshot.context?.steps[stepId]?.status).toBe(status);
|
|
97
|
-
};
|
|
98
|
-
|
|
99
50
|
describe('ClickhouseStore', () => {
|
|
100
51
|
let store: ClickhouseStore;
|
|
101
52
|
|
|
@@ -168,7 +119,10 @@ describe('ClickhouseStore', () => {
|
|
|
168
119
|
await store.saveThread({ thread });
|
|
169
120
|
|
|
170
121
|
// Add some messages
|
|
171
|
-
const messages = [
|
|
122
|
+
const messages = [
|
|
123
|
+
createSampleMessageV1({ threadId: thread.id, resourceId: 'clickhouse-test' }),
|
|
124
|
+
createSampleMessageV1({ threadId: thread.id, resourceId: 'clickhouse-test' }),
|
|
125
|
+
];
|
|
172
126
|
await store.saveMessages({ messages });
|
|
173
127
|
|
|
174
128
|
await store.deleteThread({ threadId: thread.id });
|
|
@@ -180,6 +134,28 @@ describe('ClickhouseStore', () => {
|
|
|
180
134
|
const retrievedMessages = await store.getMessages({ threadId: thread.id });
|
|
181
135
|
expect(retrievedMessages).toHaveLength(0);
|
|
182
136
|
}, 10e3);
|
|
137
|
+
|
|
138
|
+
it('should update thread updatedAt when a message is saved to it', async () => {
|
|
139
|
+
const thread = createSampleThread();
|
|
140
|
+
await store.saveThread({ thread });
|
|
141
|
+
|
|
142
|
+
// Get the initial thread to capture the original updatedAt
|
|
143
|
+
const initialThread = await store.getThreadById({ threadId: thread.id });
|
|
144
|
+
expect(initialThread).toBeDefined();
|
|
145
|
+
const originalUpdatedAt = initialThread!.updatedAt;
|
|
146
|
+
|
|
147
|
+
// Wait a small amount to ensure different timestamp
|
|
148
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
149
|
+
|
|
150
|
+
// Create and save a message to the thread
|
|
151
|
+
const message = createSampleMessageV1({ threadId: thread.id });
|
|
152
|
+
await store.saveMessages({ messages: [message] });
|
|
153
|
+
|
|
154
|
+
// Retrieve the thread again and check that updatedAt was updated
|
|
155
|
+
const updatedThread = await store.getThreadById({ threadId: thread.id });
|
|
156
|
+
expect(updatedThread).toBeDefined();
|
|
157
|
+
expect(updatedThread!.updatedAt.getTime()).toBeGreaterThan(originalUpdatedAt.getTime());
|
|
158
|
+
}, 10e3);
|
|
183
159
|
});
|
|
184
160
|
|
|
185
161
|
describe('Message Operations', () => {
|
|
@@ -188,8 +164,12 @@ describe('ClickhouseStore', () => {
|
|
|
188
164
|
await store.saveThread({ thread });
|
|
189
165
|
|
|
190
166
|
const messages = [
|
|
191
|
-
|
|
192
|
-
|
|
167
|
+
createSampleMessageV1({
|
|
168
|
+
threadId: thread.id,
|
|
169
|
+
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24),
|
|
170
|
+
resourceId: 'clickhouse-test',
|
|
171
|
+
}),
|
|
172
|
+
createSampleMessageV1({ threadId: thread.id, resourceId: 'clickhouse-test' }),
|
|
193
173
|
];
|
|
194
174
|
|
|
195
175
|
// Save messages
|
|
@@ -215,24 +195,39 @@ describe('ClickhouseStore', () => {
|
|
|
215
195
|
const thread = createSampleThread();
|
|
216
196
|
await store.saveThread({ thread });
|
|
217
197
|
|
|
218
|
-
const messages
|
|
198
|
+
const messages = [
|
|
219
199
|
{
|
|
220
|
-
...
|
|
221
|
-
|
|
200
|
+
...createSampleMessageV1({
|
|
201
|
+
threadId: thread.id,
|
|
202
|
+
createdAt: new Date(Date.now() - 1000 * 3),
|
|
203
|
+
content: 'First',
|
|
204
|
+
resourceId: 'clickhouse-test',
|
|
205
|
+
}),
|
|
206
|
+
role: 'user',
|
|
222
207
|
},
|
|
223
208
|
{
|
|
224
|
-
...
|
|
225
|
-
|
|
209
|
+
...createSampleMessageV1({
|
|
210
|
+
threadId: thread.id,
|
|
211
|
+
createdAt: new Date(Date.now() - 1000 * 2),
|
|
212
|
+
content: 'Second',
|
|
213
|
+
resourceId: 'clickhouse-test',
|
|
214
|
+
}),
|
|
215
|
+
role: 'assistant',
|
|
226
216
|
},
|
|
227
217
|
{
|
|
228
|
-
...
|
|
229
|
-
|
|
218
|
+
...createSampleMessageV1({
|
|
219
|
+
threadId: thread.id,
|
|
220
|
+
createdAt: new Date(Date.now() - 1000 * 1),
|
|
221
|
+
content: 'Third',
|
|
222
|
+
resourceId: 'clickhouse-test',
|
|
223
|
+
}),
|
|
224
|
+
role: 'user',
|
|
230
225
|
},
|
|
231
|
-
];
|
|
226
|
+
] as MastraMessageV1[];
|
|
232
227
|
|
|
233
228
|
await store.saveMessages({ messages });
|
|
234
229
|
|
|
235
|
-
const retrievedMessages = await store.getMessages
|
|
230
|
+
const retrievedMessages = await store.getMessages({ threadId: thread.id });
|
|
236
231
|
expect(retrievedMessages).toHaveLength(3);
|
|
237
232
|
|
|
238
233
|
// Verify order is maintained
|
|
@@ -242,13 +237,184 @@ describe('ClickhouseStore', () => {
|
|
|
242
237
|
});
|
|
243
238
|
}, 10e3);
|
|
244
239
|
|
|
240
|
+
it('should upsert messages: duplicate id+threadId results in update, not duplicate row', async () => {
|
|
241
|
+
const thread = await createSampleThread({ resourceId: 'clickhouse-test' });
|
|
242
|
+
await store.saveThread({ thread });
|
|
243
|
+
const baseMessage = createSampleMessageV2({
|
|
244
|
+
threadId: thread.id,
|
|
245
|
+
createdAt: new Date(),
|
|
246
|
+
content: { content: 'Original' },
|
|
247
|
+
resourceId: 'clickhouse-test',
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// Insert the message for the first time
|
|
251
|
+
await store.saveMessages({ messages: [baseMessage], format: 'v2' });
|
|
252
|
+
|
|
253
|
+
// Insert again with the same id and threadId but different content
|
|
254
|
+
const updatedMessage = {
|
|
255
|
+
...createSampleMessageV2({
|
|
256
|
+
threadId: thread.id,
|
|
257
|
+
createdAt: new Date(),
|
|
258
|
+
content: { content: 'Updated' },
|
|
259
|
+
resourceId: 'clickhouse-test',
|
|
260
|
+
}),
|
|
261
|
+
id: baseMessage.id,
|
|
262
|
+
};
|
|
263
|
+
await store.saveMessages({ messages: [updatedMessage], format: 'v2' });
|
|
264
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
265
|
+
|
|
266
|
+
// Retrieve messages for the thread
|
|
267
|
+
const retrievedMessages = await store.getMessages({ threadId: thread.id, format: 'v2' });
|
|
268
|
+
|
|
269
|
+
// Only one message should exist for that id+threadId
|
|
270
|
+
expect(retrievedMessages.filter(m => m.id === baseMessage.id)).toHaveLength(1);
|
|
271
|
+
|
|
272
|
+
// The content should be the updated one
|
|
273
|
+
expect(retrievedMessages.find(m => m.id === baseMessage.id)?.content.content).toBe('Updated');
|
|
274
|
+
}, 10e3);
|
|
275
|
+
|
|
276
|
+
it('should upsert messages: duplicate id and different threadid', async () => {
|
|
277
|
+
const thread1 = await createSampleThread();
|
|
278
|
+
const thread2 = await createSampleThread();
|
|
279
|
+
await store.saveThread({ thread: thread1 });
|
|
280
|
+
await store.saveThread({ thread: thread2 });
|
|
281
|
+
|
|
282
|
+
const message = createSampleMessageV2({
|
|
283
|
+
threadId: thread1.id,
|
|
284
|
+
createdAt: new Date(),
|
|
285
|
+
content: { content: 'Thread1 Content' },
|
|
286
|
+
resourceId: thread1.resourceId,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// Insert message into thread1
|
|
290
|
+
await store.saveMessages({ messages: [message], format: 'v2' });
|
|
291
|
+
|
|
292
|
+
// Attempt to insert a message with the same id but different threadId
|
|
293
|
+
const conflictingMessage = {
|
|
294
|
+
...createSampleMessageV2({
|
|
295
|
+
threadId: thread2.id,
|
|
296
|
+
createdAt: new Date(),
|
|
297
|
+
content: { content: 'Thread2 Content' },
|
|
298
|
+
resourceId: thread2.resourceId,
|
|
299
|
+
}),
|
|
300
|
+
id: message.id,
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
// Save should also save the message to the new thread
|
|
304
|
+
await store.saveMessages({ messages: [conflictingMessage], format: 'v2' });
|
|
305
|
+
|
|
306
|
+
// Retrieve messages for both threads
|
|
307
|
+
const thread1Messages = await store.getMessages({ threadId: thread1.id, format: 'v2' });
|
|
308
|
+
const thread2Messages = await store.getMessages({ threadId: thread2.id, format: 'v2' });
|
|
309
|
+
|
|
310
|
+
// Thread 1 should have the message with that id
|
|
311
|
+
expect(thread1Messages.find(m => m.id === message.id)?.content.content).toBe('Thread1 Content');
|
|
312
|
+
|
|
313
|
+
// Thread 2 should have the message with that id
|
|
314
|
+
expect(thread2Messages.find(m => m.id === message.id)?.content.content).toBe('Thread2 Content');
|
|
315
|
+
}, 10e3);
|
|
316
|
+
|
|
317
|
+
// it('should retrieve messages w/ next/prev messages by message id + resource id', async () => {
|
|
318
|
+
// const messages: MastraMessageV2[] = [
|
|
319
|
+
// createSampleMessageV2({ threadId: 'thread-one', content: 'First', resourceId: 'cross-thread-resource' }),
|
|
320
|
+
// createSampleMessageV2({ threadId: 'thread-one', content: 'Second', resourceId: 'cross-thread-resource' }),
|
|
321
|
+
// createSampleMessageV2({ threadId: 'thread-one', content: 'Third', resourceId: 'cross-thread-resource' }),
|
|
322
|
+
|
|
323
|
+
// createSampleMessageV2({ threadId: 'thread-two', content: 'Fourth', resourceId: 'cross-thread-resource' }),
|
|
324
|
+
// createSampleMessageV2({ threadId: 'thread-two', content: 'Fifth', resourceId: 'cross-thread-resource' }),
|
|
325
|
+
// createSampleMessageV2({ threadId: 'thread-two', content: 'Sixth', resourceId: 'cross-thread-resource' }),
|
|
326
|
+
|
|
327
|
+
// createSampleMessageV2({ threadId: 'thread-three', content: 'Seventh', resourceId: 'other-resource' }),
|
|
328
|
+
// createSampleMessageV2({ threadId: 'thread-three', content: 'Eighth', resourceId: 'other-resource' }),
|
|
329
|
+
// ];
|
|
330
|
+
|
|
331
|
+
// await store.saveMessages({ messages: messages, format: 'v2' });
|
|
332
|
+
|
|
333
|
+
// const retrievedMessages = await store.getMessages({ threadId: 'thread-one', format: 'v2' });
|
|
334
|
+
// expect(retrievedMessages).toHaveLength(3);
|
|
335
|
+
// expect(retrievedMessages.map((m: any) => m.content.parts[0].text)).toEqual(['First', 'Second', 'Third']);
|
|
336
|
+
|
|
337
|
+
// const retrievedMessages2 = await store.getMessages({ threadId: 'thread-two', format: 'v2' });
|
|
338
|
+
// expect(retrievedMessages2).toHaveLength(3);
|
|
339
|
+
// expect(retrievedMessages2.map((m: any) => m.content.parts[0].text)).toEqual(['Fourth', 'Fifth', 'Sixth']);
|
|
340
|
+
|
|
341
|
+
// const retrievedMessages3 = await store.getMessages({ threadId: 'thread-three', format: 'v2' });
|
|
342
|
+
// expect(retrievedMessages3).toHaveLength(2);
|
|
343
|
+
// expect(retrievedMessages3.map((m: any) => m.content.parts[0].text)).toEqual(['Seventh', 'Eighth']);
|
|
344
|
+
|
|
345
|
+
// const crossThreadMessages = await store.getMessages({
|
|
346
|
+
// threadId: 'thread-doesnt-exist',
|
|
347
|
+
// resourceId: 'cross-thread-resource',
|
|
348
|
+
// format: 'v2',
|
|
349
|
+
// selectBy: {
|
|
350
|
+
// last: 0,
|
|
351
|
+
// include: [
|
|
352
|
+
// {
|
|
353
|
+
// id: messages[1].id,
|
|
354
|
+
// withNextMessages: 2,
|
|
355
|
+
// withPreviousMessages: 2,
|
|
356
|
+
// },
|
|
357
|
+
// {
|
|
358
|
+
// id: messages[4].id,
|
|
359
|
+
// withPreviousMessages: 2,
|
|
360
|
+
// withNextMessages: 2,
|
|
361
|
+
// },
|
|
362
|
+
// ],
|
|
363
|
+
// },
|
|
364
|
+
// });
|
|
365
|
+
|
|
366
|
+
// expect(crossThreadMessages).toHaveLength(6);
|
|
367
|
+
// expect(crossThreadMessages.filter(m => m.threadId === `thread-one`)).toHaveLength(3);
|
|
368
|
+
// expect(crossThreadMessages.filter(m => m.threadId === `thread-two`)).toHaveLength(3);
|
|
369
|
+
|
|
370
|
+
// const crossThreadMessages2 = await store.getMessages({
|
|
371
|
+
// threadId: 'thread-one',
|
|
372
|
+
// resourceId: 'cross-thread-resource',
|
|
373
|
+
// format: 'v2',
|
|
374
|
+
// selectBy: {
|
|
375
|
+
// last: 0,
|
|
376
|
+
// include: [
|
|
377
|
+
// {
|
|
378
|
+
// id: messages[4].id,
|
|
379
|
+
// withPreviousMessages: 1,
|
|
380
|
+
// withNextMessages: 30,
|
|
381
|
+
// },
|
|
382
|
+
// ],
|
|
383
|
+
// },
|
|
384
|
+
// });
|
|
385
|
+
|
|
386
|
+
// expect(crossThreadMessages2).toHaveLength(3);
|
|
387
|
+
// expect(crossThreadMessages2.filter(m => m.threadId === `thread-one`)).toHaveLength(0);
|
|
388
|
+
// expect(crossThreadMessages2.filter(m => m.threadId === `thread-two`)).toHaveLength(3);
|
|
389
|
+
|
|
390
|
+
// const crossThreadMessages3 = await store.getMessages({
|
|
391
|
+
// threadId: 'thread-two',
|
|
392
|
+
// resourceId: 'cross-thread-resource',
|
|
393
|
+
// format: 'v2',
|
|
394
|
+
// selectBy: {
|
|
395
|
+
// last: 0,
|
|
396
|
+
// include: [
|
|
397
|
+
// {
|
|
398
|
+
// id: messages[1].id,
|
|
399
|
+
// withNextMessages: 1,
|
|
400
|
+
// withPreviousMessages: 1,
|
|
401
|
+
// },
|
|
402
|
+
// ],
|
|
403
|
+
// },
|
|
404
|
+
// });
|
|
405
|
+
|
|
406
|
+
// expect(crossThreadMessages3).toHaveLength(3);
|
|
407
|
+
// expect(crossThreadMessages3.filter(m => m.threadId === `thread-one`)).toHaveLength(3);
|
|
408
|
+
// expect(crossThreadMessages3.filter(m => m.threadId === `thread-two`)).toHaveLength(0);
|
|
409
|
+
// });
|
|
410
|
+
|
|
245
411
|
// it('should rollback on error during message save', async () => {
|
|
246
412
|
// const thread = createSampleThread();
|
|
247
413
|
// await store.saveThread({ thread });
|
|
248
414
|
|
|
249
415
|
// const messages = [
|
|
250
|
-
//
|
|
251
|
-
// { ...
|
|
416
|
+
// createSampleMessageV1({ threadId: thread.id }),
|
|
417
|
+
// { ...createSampleMessageV1({ threadId: thread.id }), id: null }, // This will cause an error
|
|
252
418
|
// ];
|
|
253
419
|
|
|
254
420
|
// await expect(store.saveMessages({ messages })).rejects.toThrow();
|
|
@@ -371,17 +537,14 @@ describe('ClickhouseStore', () => {
|
|
|
371
537
|
const snapshot = {
|
|
372
538
|
status: 'running',
|
|
373
539
|
context: {
|
|
374
|
-
|
|
375
|
-
stepResults: {},
|
|
376
|
-
attempts: {},
|
|
377
|
-
triggerData: { type: 'manual' },
|
|
540
|
+
input: { type: 'manual' },
|
|
378
541
|
},
|
|
379
542
|
value: {},
|
|
380
543
|
activePaths: [],
|
|
381
544
|
suspendedPaths: {},
|
|
382
545
|
runId,
|
|
383
546
|
timestamp: new Date().getTime(),
|
|
384
|
-
};
|
|
547
|
+
} as unknown as WorkflowRunState;
|
|
385
548
|
|
|
386
549
|
await store.persistWorkflowSnapshot({
|
|
387
550
|
workflowName,
|
|
@@ -412,17 +575,14 @@ describe('ClickhouseStore', () => {
|
|
|
412
575
|
const initialSnapshot = {
|
|
413
576
|
status: 'running',
|
|
414
577
|
context: {
|
|
415
|
-
|
|
416
|
-
stepResults: {},
|
|
417
|
-
attempts: {},
|
|
418
|
-
triggerData: { type: 'manual' },
|
|
578
|
+
input: { type: 'manual' },
|
|
419
579
|
},
|
|
420
580
|
value: {},
|
|
421
581
|
activePaths: [],
|
|
422
582
|
suspendedPaths: {},
|
|
423
583
|
runId,
|
|
424
584
|
timestamp: new Date().getTime(),
|
|
425
|
-
};
|
|
585
|
+
} as unknown as WorkflowRunState;
|
|
426
586
|
|
|
427
587
|
await store.persistWorkflowSnapshot({
|
|
428
588
|
workflowName,
|
|
@@ -433,19 +593,15 @@ describe('ClickhouseStore', () => {
|
|
|
433
593
|
const updatedSnapshot = {
|
|
434
594
|
status: 'completed',
|
|
435
595
|
context: {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
'step-1': { status: 'success', result: { data: 'test' } },
|
|
439
|
-
},
|
|
440
|
-
attempts: { 'step-1': 1 },
|
|
441
|
-
triggerData: { type: 'manual' },
|
|
596
|
+
input: { type: 'manual' },
|
|
597
|
+
'step-1': { status: 'success', result: { data: 'test' } },
|
|
442
598
|
},
|
|
443
599
|
value: {},
|
|
444
600
|
activePaths: [],
|
|
445
601
|
suspendedPaths: {},
|
|
446
602
|
runId,
|
|
447
603
|
timestamp: new Date().getTime(),
|
|
448
|
-
};
|
|
604
|
+
} as unknown as WorkflowRunState;
|
|
449
605
|
|
|
450
606
|
await store.persistWorkflowSnapshot({
|
|
451
607
|
workflowName,
|
|
@@ -467,25 +623,21 @@ describe('ClickhouseStore', () => {
|
|
|
467
623
|
const complexSnapshot = {
|
|
468
624
|
value: { currentState: 'running' },
|
|
469
625
|
context: {
|
|
470
|
-
|
|
471
|
-
'
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
date: new Date().toISOString(),
|
|
478
|
-
},
|
|
626
|
+
'step-1': {
|
|
627
|
+
status: 'success',
|
|
628
|
+
output: {
|
|
629
|
+
nestedData: {
|
|
630
|
+
array: [1, 2, 3],
|
|
631
|
+
object: { key: 'value' },
|
|
632
|
+
date: new Date().toISOString(),
|
|
479
633
|
},
|
|
480
634
|
},
|
|
481
|
-
'step-2': {
|
|
482
|
-
status: 'waiting',
|
|
483
|
-
dependencies: ['step-3', 'step-4'],
|
|
484
|
-
},
|
|
485
635
|
},
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
636
|
+
'step-2': {
|
|
637
|
+
status: 'waiting',
|
|
638
|
+
dependencies: ['step-3', 'step-4'],
|
|
639
|
+
},
|
|
640
|
+
input: {
|
|
489
641
|
type: 'scheduled',
|
|
490
642
|
metadata: {
|
|
491
643
|
schedule: '0 0 * * *',
|
|
@@ -508,7 +660,7 @@ describe('ClickhouseStore', () => {
|
|
|
508
660
|
suspendedPaths: {},
|
|
509
661
|
runId: runId,
|
|
510
662
|
timestamp: Date.now(),
|
|
511
|
-
};
|
|
663
|
+
} as unknown as WorkflowRunState;
|
|
512
664
|
|
|
513
665
|
await store.persistWorkflowSnapshot({
|
|
514
666
|
workflowName,
|
|
@@ -540,7 +692,7 @@ describe('ClickhouseStore', () => {
|
|
|
540
692
|
const workflowName2 = 'default_test_2';
|
|
541
693
|
|
|
542
694
|
const { snapshot: workflow1, runId: runId1, stepId: stepId1 } = createSampleWorkflowSnapshot('success');
|
|
543
|
-
const { snapshot: workflow2, runId: runId2, stepId: stepId2 } = createSampleWorkflowSnapshot('
|
|
695
|
+
const { snapshot: workflow2, runId: runId2, stepId: stepId2 } = createSampleWorkflowSnapshot('suspended');
|
|
544
696
|
|
|
545
697
|
await store.persistWorkflowSnapshot({
|
|
546
698
|
workflowName: workflowName1,
|
|
@@ -561,7 +713,7 @@ describe('ClickhouseStore', () => {
|
|
|
561
713
|
expect(runs[1]!.workflowName).toBe(workflowName1);
|
|
562
714
|
const firstSnapshot = runs[0]!.snapshot;
|
|
563
715
|
const secondSnapshot = runs[1]!.snapshot;
|
|
564
|
-
checkWorkflowSnapshot(firstSnapshot, stepId2, '
|
|
716
|
+
checkWorkflowSnapshot(firstSnapshot, stepId2, 'suspended');
|
|
565
717
|
checkWorkflowSnapshot(secondSnapshot, stepId1, 'success');
|
|
566
718
|
});
|
|
567
719
|
|
|
@@ -603,8 +755,8 @@ describe('ClickhouseStore', () => {
|
|
|
603
755
|
const workflowName3 = 'date_test_3';
|
|
604
756
|
|
|
605
757
|
const { snapshot: workflow1, runId: runId1 } = createSampleWorkflowSnapshot('success');
|
|
606
|
-
const { snapshot: workflow2, runId: runId2, stepId: stepId2 } = createSampleWorkflowSnapshot('
|
|
607
|
-
const { snapshot: workflow3, runId: runId3, stepId: stepId3 } = createSampleWorkflowSnapshot('
|
|
758
|
+
const { snapshot: workflow2, runId: runId2, stepId: stepId2 } = createSampleWorkflowSnapshot('suspended');
|
|
759
|
+
const { snapshot: workflow3, runId: runId3, stepId: stepId3 } = createSampleWorkflowSnapshot('failed');
|
|
608
760
|
|
|
609
761
|
await store.insert({
|
|
610
762
|
tableName: TABLE_WORKFLOW_SNAPSHOT,
|
|
@@ -647,8 +799,8 @@ describe('ClickhouseStore', () => {
|
|
|
647
799
|
expect(runs[1]!.workflowName).toBe(workflowName2);
|
|
648
800
|
const firstSnapshot = runs[0]!.snapshot;
|
|
649
801
|
const secondSnapshot = runs[1]!.snapshot;
|
|
650
|
-
checkWorkflowSnapshot(firstSnapshot, stepId3, '
|
|
651
|
-
checkWorkflowSnapshot(secondSnapshot, stepId2, '
|
|
802
|
+
checkWorkflowSnapshot(firstSnapshot, stepId3, 'failed');
|
|
803
|
+
checkWorkflowSnapshot(secondSnapshot, stepId2, 'suspended');
|
|
652
804
|
});
|
|
653
805
|
|
|
654
806
|
it('handles pagination', async () => {
|
|
@@ -657,8 +809,8 @@ describe('ClickhouseStore', () => {
|
|
|
657
809
|
const workflowName3 = 'page_test_3';
|
|
658
810
|
|
|
659
811
|
const { snapshot: workflow1, runId: runId1, stepId: stepId1 } = createSampleWorkflowSnapshot('success');
|
|
660
|
-
const { snapshot: workflow2, runId: runId2, stepId: stepId2 } = createSampleWorkflowSnapshot('
|
|
661
|
-
const { snapshot: workflow3, runId: runId3, stepId: stepId3 } = createSampleWorkflowSnapshot('
|
|
812
|
+
const { snapshot: workflow2, runId: runId2, stepId: stepId2 } = createSampleWorkflowSnapshot('suspended');
|
|
813
|
+
const { snapshot: workflow3, runId: runId3, stepId: stepId3 } = createSampleWorkflowSnapshot('failed');
|
|
662
814
|
|
|
663
815
|
await store.persistWorkflowSnapshot({
|
|
664
816
|
workflowName: workflowName1,
|
|
@@ -689,8 +841,8 @@ describe('ClickhouseStore', () => {
|
|
|
689
841
|
expect(page1.runs[1]!.workflowName).toBe(workflowName2);
|
|
690
842
|
const firstSnapshot = page1.runs[0]!.snapshot;
|
|
691
843
|
const secondSnapshot = page1.runs[1]!.snapshot;
|
|
692
|
-
checkWorkflowSnapshot(firstSnapshot, stepId3, '
|
|
693
|
-
checkWorkflowSnapshot(secondSnapshot, stepId2, '
|
|
844
|
+
checkWorkflowSnapshot(firstSnapshot, stepId3, 'failed');
|
|
845
|
+
checkWorkflowSnapshot(secondSnapshot, stepId2, 'suspended');
|
|
694
846
|
|
|
695
847
|
// Get second page
|
|
696
848
|
const page2 = await store.getWorkflowRuns({
|
|
@@ -754,7 +906,7 @@ describe('ClickhouseStore', () => {
|
|
|
754
906
|
// Insert multiple workflow runs for the same resourceId
|
|
755
907
|
resourceId = 'resource-shared';
|
|
756
908
|
for (const status of ['completed', 'running']) {
|
|
757
|
-
const sample = createSampleWorkflowSnapshot(status as WorkflowRunState['context']['steps'][
|
|
909
|
+
const sample = createSampleWorkflowSnapshot(status as WorkflowRunState['context']['steps']['status']);
|
|
758
910
|
runIds.push(sample.runId);
|
|
759
911
|
await store.insert({
|
|
760
912
|
tableName: TABLE_WORKFLOW_SNAPSHOT,
|
|
@@ -769,7 +921,7 @@ describe('ClickhouseStore', () => {
|
|
|
769
921
|
});
|
|
770
922
|
}
|
|
771
923
|
// Insert a run with a different resourceId
|
|
772
|
-
const other = createSampleWorkflowSnapshot('
|
|
924
|
+
const other = createSampleWorkflowSnapshot('suspended');
|
|
773
925
|
await store.insert({
|
|
774
926
|
tableName: TABLE_WORKFLOW_SNAPSHOT,
|
|
775
927
|
record: {
|
|
@@ -850,6 +1002,152 @@ describe('ClickhouseStore', () => {
|
|
|
850
1002
|
});
|
|
851
1003
|
});
|
|
852
1004
|
|
|
1005
|
+
describe('alterTable', () => {
|
|
1006
|
+
const TEST_TABLE = 'test_alter_table';
|
|
1007
|
+
const BASE_SCHEMA = {
|
|
1008
|
+
id: { type: 'integer', primaryKey: true, nullable: false },
|
|
1009
|
+
name: { type: 'text', nullable: true },
|
|
1010
|
+
createdAt: { type: 'timestamp', nullable: false },
|
|
1011
|
+
updatedAt: { type: 'timestamp', nullable: false },
|
|
1012
|
+
} as Record<string, StorageColumn>;
|
|
1013
|
+
|
|
1014
|
+
TABLE_ENGINES[TEST_TABLE] = 'MergeTree()';
|
|
1015
|
+
|
|
1016
|
+
beforeEach(async () => {
|
|
1017
|
+
await store.createTable({ tableName: TEST_TABLE as TABLE_NAMES, schema: BASE_SCHEMA });
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
afterEach(async () => {
|
|
1021
|
+
await store.clearTable({ tableName: TEST_TABLE as TABLE_NAMES });
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
it('adds a new column to an existing table', async () => {
|
|
1025
|
+
await store.alterTable({
|
|
1026
|
+
tableName: TEST_TABLE as TABLE_NAMES,
|
|
1027
|
+
schema: { ...BASE_SCHEMA, age: { type: 'integer', nullable: true } },
|
|
1028
|
+
ifNotExists: ['age'],
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
await store.insert({
|
|
1032
|
+
tableName: TEST_TABLE as TABLE_NAMES,
|
|
1033
|
+
record: { id: 1, name: 'Alice', age: 42, createdAt: new Date(), updatedAt: new Date() },
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
const row = await store.load<{ id: string; name: string; age?: number }>({
|
|
1037
|
+
tableName: TEST_TABLE as TABLE_NAMES,
|
|
1038
|
+
keys: { id: '1' },
|
|
1039
|
+
});
|
|
1040
|
+
expect(row?.age).toBe(42);
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
it('is idempotent when adding an existing column', async () => {
|
|
1044
|
+
await store.alterTable({
|
|
1045
|
+
tableName: TEST_TABLE as TABLE_NAMES,
|
|
1046
|
+
schema: { ...BASE_SCHEMA, foo: { type: 'text', nullable: true } },
|
|
1047
|
+
ifNotExists: ['foo'],
|
|
1048
|
+
});
|
|
1049
|
+
// Add the column again (should not throw)
|
|
1050
|
+
await expect(
|
|
1051
|
+
store.alterTable({
|
|
1052
|
+
tableName: TEST_TABLE as TABLE_NAMES,
|
|
1053
|
+
schema: { ...BASE_SCHEMA, foo: { type: 'text', nullable: true } },
|
|
1054
|
+
ifNotExists: ['foo'],
|
|
1055
|
+
}),
|
|
1056
|
+
).resolves.not.toThrow();
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
it('should add a default value to a column when using not null', async () => {
|
|
1060
|
+
await store.insert({
|
|
1061
|
+
tableName: TEST_TABLE as TABLE_NAMES,
|
|
1062
|
+
record: { id: 1, name: 'Bob', createdAt: new Date(), updatedAt: new Date() },
|
|
1063
|
+
});
|
|
1064
|
+
|
|
1065
|
+
await expect(
|
|
1066
|
+
store.alterTable({
|
|
1067
|
+
tableName: TEST_TABLE as TABLE_NAMES,
|
|
1068
|
+
schema: { ...BASE_SCHEMA, text_column: { type: 'text', nullable: false } },
|
|
1069
|
+
ifNotExists: ['text_column'],
|
|
1070
|
+
}),
|
|
1071
|
+
).resolves.not.toThrow();
|
|
1072
|
+
|
|
1073
|
+
await expect(
|
|
1074
|
+
store.alterTable({
|
|
1075
|
+
tableName: TEST_TABLE as TABLE_NAMES,
|
|
1076
|
+
schema: { ...BASE_SCHEMA, timestamp_column: { type: 'timestamp', nullable: false } },
|
|
1077
|
+
ifNotExists: ['timestamp_column'],
|
|
1078
|
+
}),
|
|
1079
|
+
).resolves.not.toThrow();
|
|
1080
|
+
|
|
1081
|
+
await expect(
|
|
1082
|
+
store.alterTable({
|
|
1083
|
+
tableName: TEST_TABLE as TABLE_NAMES,
|
|
1084
|
+
schema: { ...BASE_SCHEMA, bigint_column: { type: 'bigint', nullable: false } },
|
|
1085
|
+
ifNotExists: ['bigint_column'],
|
|
1086
|
+
}),
|
|
1087
|
+
).resolves.not.toThrow();
|
|
1088
|
+
|
|
1089
|
+
await expect(
|
|
1090
|
+
store.alterTable({
|
|
1091
|
+
tableName: TEST_TABLE as TABLE_NAMES,
|
|
1092
|
+
schema: { ...BASE_SCHEMA, jsonb_column: { type: 'jsonb', nullable: false } },
|
|
1093
|
+
ifNotExists: ['jsonb_column'],
|
|
1094
|
+
}),
|
|
1095
|
+
).resolves.not.toThrow();
|
|
1096
|
+
});
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
describe('ClickhouseStore Double-nesting Prevention', () => {
|
|
1100
|
+
beforeEach(async () => {
|
|
1101
|
+
await store.clearTable({ tableName: TABLE_MESSAGES });
|
|
1102
|
+
await store.clearTable({ tableName: TABLE_THREADS });
|
|
1103
|
+
});
|
|
1104
|
+
|
|
1105
|
+
it('should handle stringified JSON content without double-nesting', async () => {
|
|
1106
|
+
const threadData = createSampleThread();
|
|
1107
|
+
const thread = await store.saveThread({ thread: threadData });
|
|
1108
|
+
|
|
1109
|
+
// Simulate user passing stringified JSON as message content (like the original bug report)
|
|
1110
|
+
const stringifiedContent = JSON.stringify({ userInput: 'test data', metadata: { key: 'value' } });
|
|
1111
|
+
const message: MastraMessageV2 = {
|
|
1112
|
+
id: `msg-${randomUUID()}`,
|
|
1113
|
+
role: 'user',
|
|
1114
|
+
threadId: thread.id,
|
|
1115
|
+
resourceId: thread.resourceId,
|
|
1116
|
+
content: {
|
|
1117
|
+
format: 2,
|
|
1118
|
+
parts: [{ type: 'text', text: stringifiedContent }],
|
|
1119
|
+
content: stringifiedContent, // This is the stringified JSON that user passed
|
|
1120
|
+
},
|
|
1121
|
+
createdAt: new Date(),
|
|
1122
|
+
};
|
|
1123
|
+
|
|
1124
|
+
// Save the message - this should stringify the whole content object for storage
|
|
1125
|
+
await store.saveMessages({ messages: [message], format: 'v2' });
|
|
1126
|
+
|
|
1127
|
+
// Retrieve the message - this is where double-nesting could occur
|
|
1128
|
+
const retrievedMessages = await store.getMessages({ threadId: thread.id, format: 'v2' });
|
|
1129
|
+
expect(retrievedMessages).toHaveLength(1);
|
|
1130
|
+
|
|
1131
|
+
const retrievedMessage = retrievedMessages[0] as MastraMessageV2;
|
|
1132
|
+
|
|
1133
|
+
// Check that content is properly structured as a V2 message
|
|
1134
|
+
expect(typeof retrievedMessage.content).toBe('object');
|
|
1135
|
+
expect(retrievedMessage.content.format).toBe(2);
|
|
1136
|
+
|
|
1137
|
+
// CRITICAL: The content.content should still be the original stringified JSON
|
|
1138
|
+
// NOT double-nested like: { content: '{"format":2,"parts":[...],"content":"{\\"userInput\\":\\"test data\\"}"}' }
|
|
1139
|
+
expect(retrievedMessage.content.content).toBe(stringifiedContent);
|
|
1140
|
+
|
|
1141
|
+
// Verify the content can be parsed as the original JSON
|
|
1142
|
+
const parsedContent = JSON.parse(retrievedMessage.content.content as string);
|
|
1143
|
+
expect(parsedContent).toEqual({ userInput: 'test data', metadata: { key: 'value' } });
|
|
1144
|
+
|
|
1145
|
+
// Additional check: ensure the message doesn't have the "Found unhandled message" structure
|
|
1146
|
+
expect(retrievedMessage.content.parts).toBeDefined();
|
|
1147
|
+
expect(Array.isArray(retrievedMessage.content.parts)).toBe(true);
|
|
1148
|
+
});
|
|
1149
|
+
});
|
|
1150
|
+
|
|
853
1151
|
afterAll(async () => {
|
|
854
1152
|
await store.close();
|
|
855
1153
|
});
|