@mastra/upstash 0.1.0-alpha.2

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.
@@ -0,0 +1,381 @@
1
+ import { MastraStorage } from '@mastra/core/storage';
2
+ import { describe, it, expect, beforeAll, beforeEach, afterAll } from 'vitest';
3
+
4
+ import { UpstashStore } from './index';
5
+
6
+ describe('UpstashStore', () => {
7
+ let store: UpstashStore;
8
+ const testTableName = 'test_table';
9
+ const testTableName2 = 'test_table2';
10
+
11
+ beforeAll(async () => {
12
+ store = new UpstashStore({
13
+ url: 'http://localhost:8079',
14
+ token: 'test_token',
15
+ });
16
+ await store.init();
17
+ });
18
+
19
+ afterAll(async () => {
20
+ // Clean up test tables
21
+ await store.clearTable({ tableName: testTableName });
22
+ await store.clearTable({ tableName: testTableName2 });
23
+ await store.clearTable({ tableName: MastraStorage.TABLE_THREADS });
24
+ await store.clearTable({ tableName: MastraStorage.TABLE_MESSAGES });
25
+ });
26
+
27
+ describe('Table Operations', () => {
28
+ it('should create a new table with schema', async () => {
29
+ await store.createTable({
30
+ tableName: testTableName,
31
+ schema: {
32
+ id: { type: 'text', primaryKey: true },
33
+ data: { type: 'text', nullable: true },
34
+ },
35
+ });
36
+
37
+ // Verify table exists by inserting and retrieving data
38
+ await store.insert({
39
+ tableName: testTableName,
40
+ record: { id: 'test1', data: 'test-data' },
41
+ });
42
+
43
+ const result = await store.load({ tableName: testTableName, keys: { id: 'test1' } });
44
+ expect(result).toBeTruthy();
45
+ });
46
+
47
+ it('should handle multiple table creation', async () => {
48
+ await store.createTable({
49
+ tableName: testTableName2,
50
+ schema: {
51
+ id: { type: 'text', primaryKey: true },
52
+ data: { type: 'text', nullable: true },
53
+ },
54
+ });
55
+
56
+ // Verify both tables work independently
57
+ await store.insert({
58
+ tableName: testTableName2,
59
+ record: { id: 'test2', data: 'test-data-2' },
60
+ });
61
+
62
+ const result = await store.load({ tableName: testTableName2, keys: { id: 'test2' } });
63
+ expect(result).toBeTruthy();
64
+ });
65
+ });
66
+
67
+ describe('Thread Operations', () => {
68
+ beforeEach(async () => {
69
+ await store.clearTable({ tableName: MastraStorage.TABLE_THREADS });
70
+ });
71
+
72
+ it('should create and retrieve a thread', async () => {
73
+ const now = new Date();
74
+ const thread = {
75
+ id: 'thread-1',
76
+ resourceId: 'resource-1',
77
+ title: 'Test Thread',
78
+ createdAt: now,
79
+ updatedAt: now,
80
+ metadata: { key: 'value' },
81
+ };
82
+
83
+ const savedThread = await store.saveThread({ thread });
84
+ expect(savedThread).toEqual(thread);
85
+
86
+ const retrievedThread = await store.getThreadById({ threadId: thread.id });
87
+ expect(retrievedThread).toEqual({
88
+ ...thread,
89
+ createdAt: new Date(now.toISOString()),
90
+ updatedAt: new Date(now.toISOString()),
91
+ });
92
+ });
93
+
94
+ it('should return null for non-existent thread', async () => {
95
+ const result = await store.getThreadById({ threadId: 'non-existent' });
96
+ expect(result).toBeNull();
97
+ });
98
+
99
+ it('should get threads by resource ID', async () => {
100
+ const resourceId = 'resource-1';
101
+ const threads = [
102
+ {
103
+ id: 'thread-1',
104
+ resourceId,
105
+ title: 'Thread 1',
106
+ createdAt: new Date(),
107
+ updatedAt: new Date(),
108
+ metadata: {},
109
+ },
110
+ {
111
+ id: 'thread-2',
112
+ resourceId,
113
+ title: 'Thread 2',
114
+ createdAt: new Date(),
115
+ updatedAt: new Date(),
116
+ metadata: {},
117
+ },
118
+ ];
119
+
120
+ await Promise.all(threads.map(thread => store.saveThread({ thread })));
121
+
122
+ const retrievedThreads = await store.getThreadsByResourceId({ resourceId });
123
+ expect(retrievedThreads).toHaveLength(2);
124
+ expect(retrievedThreads.map(t => t.id)).toEqual(expect.arrayContaining(['thread-1', 'thread-2']));
125
+ });
126
+
127
+ it('should update thread metadata', async () => {
128
+ const thread = {
129
+ id: 'thread-1',
130
+ resourceId: 'resource-1',
131
+ title: 'Test Thread',
132
+ createdAt: new Date(),
133
+ updatedAt: new Date(),
134
+ metadata: { initial: 'value' },
135
+ };
136
+
137
+ await store.saveThread({ thread });
138
+
139
+ const updatedThread = await store.updateThread({
140
+ id: thread.id,
141
+ title: 'Updated Title',
142
+ metadata: { updated: 'value' },
143
+ });
144
+
145
+ expect(updatedThread.title).toBe('Updated Title');
146
+ expect(updatedThread.metadata).toEqual({
147
+ initial: 'value',
148
+ updated: 'value',
149
+ });
150
+ });
151
+ });
152
+
153
+ describe('Date Handling', () => {
154
+ beforeEach(async () => {
155
+ await store.clearTable({ tableName: MastraStorage.TABLE_THREADS });
156
+ });
157
+
158
+ it('should handle Date objects in thread operations', async () => {
159
+ const now = new Date();
160
+ const thread = {
161
+ id: 'thread-1',
162
+ resourceId: 'resource-1',
163
+ title: 'Test Thread',
164
+ createdAt: now,
165
+ updatedAt: now,
166
+ metadata: {},
167
+ };
168
+
169
+ await store.saveThread({ thread });
170
+ const retrievedThread = await store.getThreadById({ threadId: thread.id });
171
+ expect(retrievedThread?.createdAt).toBeInstanceOf(Date);
172
+ expect(retrievedThread?.updatedAt).toBeInstanceOf(Date);
173
+ expect(retrievedThread?.createdAt.toISOString()).toBe(now.toISOString());
174
+ expect(retrievedThread?.updatedAt.toISOString()).toBe(now.toISOString());
175
+ });
176
+
177
+ it('should handle ISO string dates in thread operations', async () => {
178
+ const now = new Date();
179
+ const thread = {
180
+ id: 'thread-2',
181
+ resourceId: 'resource-1',
182
+ title: 'Test Thread',
183
+ createdAt: now.toISOString(),
184
+ updatedAt: now.toISOString(),
185
+ metadata: {},
186
+ };
187
+
188
+ await store.saveThread({ thread: thread as any });
189
+ const retrievedThread = await store.getThreadById({ threadId: thread.id });
190
+ expect(retrievedThread?.createdAt).toBeInstanceOf(Date);
191
+ expect(retrievedThread?.updatedAt).toBeInstanceOf(Date);
192
+ expect(retrievedThread?.createdAt.toISOString()).toBe(now.toISOString());
193
+ expect(retrievedThread?.updatedAt.toISOString()).toBe(now.toISOString());
194
+ });
195
+
196
+ it('should handle mixed date formats in thread operations', async () => {
197
+ const now = new Date();
198
+ const thread = {
199
+ id: 'thread-3',
200
+ resourceId: 'resource-1',
201
+ title: 'Test Thread',
202
+ createdAt: now,
203
+ updatedAt: now.toISOString(),
204
+ metadata: {},
205
+ };
206
+
207
+ await store.saveThread({ thread: thread as any });
208
+ const retrievedThread = await store.getThreadById({ threadId: thread.id });
209
+ expect(retrievedThread?.createdAt).toBeInstanceOf(Date);
210
+ expect(retrievedThread?.updatedAt).toBeInstanceOf(Date);
211
+ expect(retrievedThread?.createdAt.toISOString()).toBe(now.toISOString());
212
+ expect(retrievedThread?.updatedAt.toISOString()).toBe(now.toISOString());
213
+ });
214
+
215
+ it('should handle date serialization in getThreadsByResourceId', async () => {
216
+ const now = new Date();
217
+ const threads = [
218
+ {
219
+ id: 'thread-1',
220
+ resourceId: 'resource-1',
221
+ title: 'Thread 1',
222
+ createdAt: now,
223
+ updatedAt: now.toISOString(),
224
+ metadata: {},
225
+ },
226
+ {
227
+ id: 'thread-2',
228
+ resourceId: 'resource-1',
229
+ title: 'Thread 2',
230
+ createdAt: now.toISOString(),
231
+ updatedAt: now,
232
+ metadata: {},
233
+ },
234
+ ];
235
+
236
+ await Promise.all(threads.map(thread => store.saveThread({ thread: thread as any })));
237
+
238
+ const retrievedThreads = await store.getThreadsByResourceId({ resourceId: 'resource-1' });
239
+ expect(retrievedThreads).toHaveLength(2);
240
+ retrievedThreads.forEach(thread => {
241
+ expect(thread.createdAt).toBeInstanceOf(Date);
242
+ expect(thread.updatedAt).toBeInstanceOf(Date);
243
+ expect(thread.createdAt.toISOString()).toBe(now.toISOString());
244
+ expect(thread.updatedAt.toISOString()).toBe(now.toISOString());
245
+ });
246
+ });
247
+ });
248
+
249
+ describe('Message Operations', () => {
250
+ const threadId = 'test-thread';
251
+
252
+ beforeEach(async () => {
253
+ await store.clearTable({ tableName: MastraStorage.TABLE_MESSAGES });
254
+ await store.clearTable({ tableName: MastraStorage.TABLE_THREADS });
255
+
256
+ // Create a test thread
257
+ await store.saveThread({
258
+ thread: {
259
+ id: threadId,
260
+ resourceId: 'resource-1',
261
+ title: 'Test Thread',
262
+ createdAt: new Date(),
263
+ updatedAt: new Date(),
264
+ metadata: {},
265
+ },
266
+ });
267
+ });
268
+
269
+ it('should save and retrieve messages in order', async () => {
270
+ const messages = [
271
+ {
272
+ id: 'msg-1',
273
+ threadId,
274
+ role: 'user',
275
+ type: 'text',
276
+ content: [{ type: 'text', text: 'First' }],
277
+ createdAt: new Date().toISOString(),
278
+ },
279
+ {
280
+ id: 'msg-2',
281
+ threadId,
282
+ role: 'assistant',
283
+ type: 'text',
284
+ content: [{ type: 'text', text: 'Second' }],
285
+ createdAt: new Date().toISOString(),
286
+ },
287
+ {
288
+ id: 'msg-3',
289
+ threadId,
290
+ role: 'user',
291
+ type: 'text',
292
+ content: [{ type: 'text', text: 'Third' }],
293
+ createdAt: new Date().toISOString(),
294
+ },
295
+ ];
296
+
297
+ await store.saveMessages({ messages });
298
+
299
+ const retrievedMessages = await store.getMessages({ threadId });
300
+ expect(retrievedMessages).toHaveLength(3);
301
+ expect(retrievedMessages.map(m => m.content[0].text)).toEqual(['First', 'Second', 'Third']);
302
+ });
303
+
304
+ it('should handle empty message array', async () => {
305
+ const result = await store.saveMessages({ messages: [] });
306
+ expect(result).toEqual([]);
307
+ });
308
+
309
+ it('should handle messages with complex content', async () => {
310
+ const messages = [
311
+ {
312
+ id: 'msg-1',
313
+ threadId,
314
+ role: 'user',
315
+ type: 'text',
316
+ content: [
317
+ { type: 'text', text: 'Message with' },
318
+ { type: 'code', text: 'code block', language: 'typescript' },
319
+ { type: 'text', text: 'and more text' },
320
+ ],
321
+ createdAt: new Date().toISOString(),
322
+ },
323
+ ];
324
+
325
+ await store.saveMessages({ messages });
326
+
327
+ const retrievedMessages = await store.getMessages({ threadId });
328
+ expect(retrievedMessages[0].content).toEqual(messages[0].content);
329
+ });
330
+ });
331
+
332
+ describe('Workflow Operations', () => {
333
+ const testNamespace = 'test';
334
+ const testWorkflow = 'test-workflow';
335
+ const testRunId = 'test-run';
336
+
337
+ beforeEach(async () => {
338
+ await store.clearTable({ tableName: MastraStorage.TABLE_WORKFLOW_SNAPSHOT });
339
+ });
340
+
341
+ it('should persist and load workflow snapshots', async () => {
342
+ const mockSnapshot = {
343
+ value: { step1: 'completed' },
344
+ context: {
345
+ stepResults: {
346
+ step1: { status: 'success', payload: { result: 'done' } },
347
+ },
348
+ attempts: {},
349
+ triggerData: {},
350
+ },
351
+ runId: testRunId,
352
+ activePaths: [],
353
+ timestamp: Date.now(),
354
+ };
355
+
356
+ await store.persistWorkflowSnapshot({
357
+ namespace: testNamespace,
358
+ workflowName: testWorkflow,
359
+ runId: testRunId,
360
+ snapshot: mockSnapshot,
361
+ });
362
+
363
+ const loadedSnapshot = await store.loadWorkflowSnapshot({
364
+ namespace: testNamespace,
365
+ workflowName: testWorkflow,
366
+ runId: testRunId,
367
+ });
368
+
369
+ expect(loadedSnapshot).toEqual(mockSnapshot);
370
+ });
371
+
372
+ it('should return null for non-existent snapshot', async () => {
373
+ const result = await store.loadWorkflowSnapshot({
374
+ namespace: testNamespace,
375
+ workflowName: 'non-existent',
376
+ runId: 'non-existent',
377
+ });
378
+ expect(result).toBeNull();
379
+ });
380
+ });
381
+ });