@mastra/lance 0.2.0 → 0.2.1-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +7 -7
- package/CHANGELOG.md +26 -0
- package/LICENSE.md +11 -42
- package/dist/_tsup-dts-rollup.d.cts +360 -89
- package/dist/_tsup-dts-rollup.d.ts +360 -89
- package/dist/index.cjs +1628 -646
- package/dist/index.js +1563 -581
- package/package.json +6 -6
- package/src/storage/domains/legacy-evals/index.ts +156 -0
- package/src/storage/domains/memory/index.ts +947 -0
- package/src/storage/domains/operations/index.ts +489 -0
- package/src/storage/domains/scores/index.ts +221 -0
- package/src/storage/domains/traces/index.ts +212 -0
- package/src/storage/domains/utils.ts +158 -0
- package/src/storage/domains/workflows/index.ts +207 -0
- package/src/storage/index.test.ts +6 -1332
- package/src/storage/index.ts +156 -1162
|
@@ -1,1336 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import type { EvalRow, MastraMessageV2, StorageThreadType, TraceType, WorkflowRunState } from '@mastra/core';
|
|
4
|
-
import {
|
|
5
|
-
TABLE_EVALS,
|
|
6
|
-
TABLE_MESSAGES,
|
|
7
|
-
TABLE_SCHEMAS,
|
|
8
|
-
TABLE_THREADS,
|
|
9
|
-
TABLE_TRACES,
|
|
10
|
-
TABLE_WORKFLOW_SNAPSHOT,
|
|
11
|
-
} from '@mastra/core/storage';
|
|
12
|
-
import type { StorageColumn, TABLE_NAMES } from '@mastra/core/storage';
|
|
13
|
-
import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest';
|
|
1
|
+
import { createTestSuite } from '@internal/storage-test-utils';
|
|
2
|
+
import { vi } from 'vitest';
|
|
14
3
|
import { LanceStorage } from './index';
|
|
15
4
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
*/
|
|
19
|
-
interface MessageRecord {
|
|
20
|
-
id: number;
|
|
21
|
-
threadId: string;
|
|
22
|
-
referenceId: number;
|
|
23
|
-
messageType: string;
|
|
24
|
-
content: string;
|
|
25
|
-
createdAt: Date;
|
|
26
|
-
metadata: Record<string, unknown>;
|
|
27
|
-
}
|
|
5
|
+
// Increase timeout for all tests in this file to 30 seconds
|
|
6
|
+
vi.setConfig({ testTimeout: 200_000, hookTimeout: 200_000 });
|
|
28
7
|
|
|
29
|
-
|
|
30
|
-
* Generates an array of random records for testing purposes
|
|
31
|
-
* @param count - Number of records to generate
|
|
32
|
-
* @returns Array of message records with random values
|
|
33
|
-
*/
|
|
34
|
-
function generateRecords(count: number): MessageRecord[] {
|
|
35
|
-
return Array.from({ length: count }, (_, index) => ({
|
|
36
|
-
id: index + 1,
|
|
37
|
-
threadId: `12333d567-e89b-12d3-a456-${(426614174000 + index).toString()}`,
|
|
38
|
-
referenceId: index + 1,
|
|
39
|
-
messageType: 'text',
|
|
40
|
-
content: `Test message ${index + 1}`,
|
|
41
|
-
createdAt: new Date(),
|
|
42
|
-
metadata: { testIndex: index, foo: 'bar' },
|
|
43
|
-
}));
|
|
44
|
-
}
|
|
8
|
+
const storage = await LanceStorage.create('test', 'lancedb-storage');
|
|
45
9
|
|
|
46
|
-
|
|
47
|
-
return Array.from({ length: count }, (_, index) => ({
|
|
48
|
-
id: (index + 1).toString(),
|
|
49
|
-
content: { format: 2, parts: [{ type: 'text', text: `Test message ${index + 1}` }] },
|
|
50
|
-
role: 'user',
|
|
51
|
-
createdAt: new Date(),
|
|
52
|
-
threadId: threadId ?? `12333d567-e89b-12d3-a456-${(426614174000 + index).toString()}`,
|
|
53
|
-
resourceId: `12333d567-e89b-12d3-a456-${(426614174000 + index).toString()}`,
|
|
54
|
-
toolCallIds: [],
|
|
55
|
-
toolCallArgs: [],
|
|
56
|
-
toolNames: [],
|
|
57
|
-
type: 'v2',
|
|
58
|
-
}));
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function generateTraceRecords(count: number): TraceType[] {
|
|
62
|
-
return Array.from({ length: count }, (_, index) => ({
|
|
63
|
-
id: (index + 1).toString(),
|
|
64
|
-
name: `Test trace ${index + 1}`,
|
|
65
|
-
scope: 'test',
|
|
66
|
-
kind: 0,
|
|
67
|
-
parentSpanId: `12333d567-e89b-12d3-a456-${(426614174000 + index).toString()}`,
|
|
68
|
-
traceId: `12333d567-e89b-12d3-a456-${(426614174000 + index).toString()}`,
|
|
69
|
-
attributes: { attribute1: 'value1' },
|
|
70
|
-
status: { code: 0, description: 'OK' },
|
|
71
|
-
events: { event1: 'value1' },
|
|
72
|
-
links: { link1: 'value1' },
|
|
73
|
-
other: { other1: 'value1' },
|
|
74
|
-
startTime: new Date().getTime(),
|
|
75
|
-
endTime: new Date().getTime(),
|
|
76
|
-
createdAt: new Date(),
|
|
77
|
-
}));
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function generateEvalRecords(count: number): EvalRow[] {
|
|
81
|
-
return Array.from({ length: count }, (_, index) => ({
|
|
82
|
-
input: `Test input ${index + 1}`,
|
|
83
|
-
output: `Test output ${index + 1}`,
|
|
84
|
-
result: { score: index + 1, info: { testIndex: index + 1 } },
|
|
85
|
-
agentName: `Test agent ${index + 1}`,
|
|
86
|
-
metricName: `Test metric ${index + 1}`,
|
|
87
|
-
instructions: 'Test instructions',
|
|
88
|
-
testInfo: { testName: `Test ${index + 1}`, testPath: `TestPath ${index + 1}` },
|
|
89
|
-
runId: `12333d567-e89b-12d3-a456-${(426614174000 + index).toString()}`,
|
|
90
|
-
globalRunId: `12333d567-e89b-12d3-a456-${(426614174000 + index).toString()}`,
|
|
91
|
-
createdAt: new Date().toString(),
|
|
92
|
-
}));
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const generateWorkflowSnapshot = (status: WorkflowRunState['context']['steps']['status'], createdAt?: Date) => {
|
|
96
|
-
const runId = `run-${randomUUID()}`;
|
|
97
|
-
const stepId = `step-${randomUUID()}`;
|
|
98
|
-
const timestamp = createdAt || new Date();
|
|
99
|
-
const snapshot = {
|
|
100
|
-
result: { success: true },
|
|
101
|
-
value: {},
|
|
102
|
-
context: {
|
|
103
|
-
[stepId]: {
|
|
104
|
-
status,
|
|
105
|
-
payload: {},
|
|
106
|
-
error: undefined,
|
|
107
|
-
startedAt: timestamp.getTime(),
|
|
108
|
-
endedAt: new Date(timestamp.getTime() + 15000).getTime(),
|
|
109
|
-
},
|
|
110
|
-
input: {},
|
|
111
|
-
},
|
|
112
|
-
serializedStepGraph: [],
|
|
113
|
-
activePaths: [],
|
|
114
|
-
suspendedPaths: {},
|
|
115
|
-
runId,
|
|
116
|
-
timestamp: timestamp.getTime(),
|
|
117
|
-
status,
|
|
118
|
-
} as unknown as WorkflowRunState;
|
|
119
|
-
return { snapshot, runId, stepId };
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
describe('LanceStorage tests', async () => {
|
|
123
|
-
let storage!: LanceStorage;
|
|
124
|
-
|
|
125
|
-
beforeAll(async () => {
|
|
126
|
-
storage = await LanceStorage.create('test', 'lancedb-storage');
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it('should create a new instance of LanceStorage', async () => {
|
|
130
|
-
const storage = await LanceStorage.create('test', 'lancedb-storage');
|
|
131
|
-
expect(storage).toBeInstanceOf(LanceStorage);
|
|
132
|
-
expect(storage.name).toBe('test');
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
describe('Create table', () => {
|
|
136
|
-
beforeAll(async () => {
|
|
137
|
-
// Clean up any existing tables
|
|
138
|
-
try {
|
|
139
|
-
await storage.dropTable(TABLE_MESSAGES);
|
|
140
|
-
} catch {
|
|
141
|
-
// Ignore if table doesn't exist
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
afterAll(async () => {
|
|
146
|
-
await storage.dropTable(TABLE_MESSAGES);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it('should create an empty table with given schema', async () => {
|
|
150
|
-
const schema: Record<string, StorageColumn> = {
|
|
151
|
-
id: { type: 'integer', nullable: false },
|
|
152
|
-
threadId: { type: 'uuid', nullable: false },
|
|
153
|
-
referenceId: { type: 'bigint', nullable: true },
|
|
154
|
-
messageType: { type: 'text', nullable: true },
|
|
155
|
-
content: { type: 'text', nullable: true },
|
|
156
|
-
createdAt: { type: 'timestamp', nullable: true },
|
|
157
|
-
metadata: { type: 'jsonb', nullable: true },
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
await storage.createTable({ tableName: TABLE_MESSAGES, schema });
|
|
161
|
-
|
|
162
|
-
// Verify table exists and schema is correct
|
|
163
|
-
const table = await storage.getTableSchema(TABLE_MESSAGES);
|
|
164
|
-
|
|
165
|
-
expect(table.fields.length).toBe(7);
|
|
166
|
-
expect(table.names).toEqual(
|
|
167
|
-
expect.arrayContaining(['id', 'threadId', 'referenceId', 'messageType', 'content', 'createdAt', 'metadata']),
|
|
168
|
-
);
|
|
169
|
-
// check the types of the fields
|
|
170
|
-
expect(table.fields[0].type.toString().toLowerCase()).toBe('int32');
|
|
171
|
-
expect(table.fields[1].type.toString().toLowerCase()).toBe('utf8');
|
|
172
|
-
expect(table.fields[2].type.toString().toLowerCase()).toBe('float64');
|
|
173
|
-
expect(table.fields[3].type.toString().toLowerCase()).toBe('utf8');
|
|
174
|
-
expect(table.fields[4].type.toString().toLowerCase()).toBe('utf8');
|
|
175
|
-
expect(table.fields[5].type.toString().toLowerCase()).toBe('float64');
|
|
176
|
-
expect(table.fields[6].type.toString().toLowerCase()).toBe('utf8');
|
|
177
|
-
});
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
describe('Insert data', () => {
|
|
181
|
-
beforeAll(async () => {
|
|
182
|
-
const schema: Record<string, StorageColumn> = {
|
|
183
|
-
id: { type: 'integer', nullable: false },
|
|
184
|
-
threadId: { type: 'uuid', nullable: false },
|
|
185
|
-
referenceId: { type: 'bigint', nullable: true },
|
|
186
|
-
messageType: { type: 'text', nullable: true },
|
|
187
|
-
content: { type: 'text', nullable: true },
|
|
188
|
-
createdAt: { type: 'timestamp', nullable: true },
|
|
189
|
-
metadata: { type: 'jsonb', nullable: true },
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
await storage.createTable({ tableName: TABLE_MESSAGES, schema });
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
afterAll(async () => {
|
|
196
|
-
await storage.dropTable(TABLE_MESSAGES);
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it('should insert a single record without throwing exceptions', async () => {
|
|
200
|
-
const record = {
|
|
201
|
-
id: 1,
|
|
202
|
-
threadId: '123e4567-e89b-12d3-a456-426614174000',
|
|
203
|
-
referenceId: 1,
|
|
204
|
-
messageType: 'text',
|
|
205
|
-
content: 'Hello, world!',
|
|
206
|
-
createdAt: new Date(),
|
|
207
|
-
metadata: { foo: 'bar' },
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
await storage.insert({ tableName: TABLE_MESSAGES, record });
|
|
211
|
-
|
|
212
|
-
// Verify the record was inserted
|
|
213
|
-
const loadedRecord = await storage.load({ tableName: TABLE_MESSAGES, keys: { id: 1 } });
|
|
214
|
-
|
|
215
|
-
// Custom comparison to handle date precision differences
|
|
216
|
-
expect(loadedRecord.id).toEqual(record.id);
|
|
217
|
-
expect(loadedRecord.threadId).toEqual(record.threadId);
|
|
218
|
-
expect(loadedRecord.referenceId).toEqual(record.referenceId);
|
|
219
|
-
expect(loadedRecord.messageType).toEqual(record.messageType);
|
|
220
|
-
expect(loadedRecord.content).toEqual(record.content);
|
|
221
|
-
expect(loadedRecord.metadata).toEqual(record.metadata);
|
|
222
|
-
|
|
223
|
-
// Compare dates ignoring millisecond precision
|
|
224
|
-
const loadedDate = new Date(loadedRecord.createdAt);
|
|
225
|
-
const originalDate = new Date(record.createdAt);
|
|
226
|
-
expect(loadedDate.getFullYear()).toEqual(originalDate.getFullYear());
|
|
227
|
-
expect(loadedDate.getMonth()).toEqual(originalDate.getMonth());
|
|
228
|
-
expect(loadedDate.getDate()).toEqual(originalDate.getDate());
|
|
229
|
-
expect(loadedDate.getHours()).toEqual(originalDate.getHours());
|
|
230
|
-
expect(loadedDate.getMinutes()).toEqual(originalDate.getMinutes());
|
|
231
|
-
expect(loadedDate.getSeconds()).toEqual(originalDate.getSeconds());
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
it('should throw error when invalid key type is provided', async () => {
|
|
235
|
-
await expect(storage.load({ tableName: TABLE_MESSAGES, keys: { id: '1' } })).rejects.toThrowError(
|
|
236
|
-
/Expected numeric value for field 'id', got string/,
|
|
237
|
-
);
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
it('should insert batch records without throwing exceptions', async () => {
|
|
241
|
-
const recordCount = 100;
|
|
242
|
-
const records: MessageRecord[] = generateRecords(recordCount);
|
|
243
|
-
|
|
244
|
-
await storage.batchInsert({ tableName: TABLE_MESSAGES, records });
|
|
245
|
-
|
|
246
|
-
// Verify records were inserted
|
|
247
|
-
const loadedRecords = await storage.load({ tableName: TABLE_MESSAGES, keys: { id: 1 } });
|
|
248
|
-
expect(loadedRecords).not.toBeNull();
|
|
249
|
-
expect(loadedRecords.id).toEqual(records[0].id);
|
|
250
|
-
expect(loadedRecords.threadId).toEqual(records[0].threadId);
|
|
251
|
-
expect(loadedRecords.referenceId).toEqual(records[0].referenceId);
|
|
252
|
-
expect(loadedRecords.messageType).toEqual(records[0].messageType);
|
|
253
|
-
expect(loadedRecords.content).toEqual(records[0].content);
|
|
254
|
-
expect(new Date(loadedRecords.createdAt)).toEqual(new Date(records[0].createdAt));
|
|
255
|
-
expect(loadedRecords.metadata).toEqual(records[0].metadata);
|
|
256
|
-
|
|
257
|
-
// Verify the last record
|
|
258
|
-
const lastRecord = await storage.load({ tableName: TABLE_MESSAGES, keys: { id: recordCount } });
|
|
259
|
-
expect(lastRecord).not.toBeNull();
|
|
260
|
-
expect(lastRecord.id).toEqual(records[recordCount - 1].id);
|
|
261
|
-
expect(lastRecord.threadId).toEqual(records[recordCount - 1].threadId);
|
|
262
|
-
expect(lastRecord.referenceId).toEqual(records[recordCount - 1].referenceId);
|
|
263
|
-
expect(lastRecord.messageType).toEqual(records[recordCount - 1].messageType);
|
|
264
|
-
expect(lastRecord.content).toEqual(records[recordCount - 1].content);
|
|
265
|
-
expect(new Date(lastRecord.createdAt)).toEqual(new Date(records[recordCount - 1].createdAt));
|
|
266
|
-
expect(lastRecord.metadata).toEqual(records[recordCount - 1].metadata);
|
|
267
|
-
});
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
describe('Query data', () => {
|
|
271
|
-
beforeAll(async () => {
|
|
272
|
-
const schema: Record<string, StorageColumn> = {
|
|
273
|
-
id: { type: 'integer', nullable: false },
|
|
274
|
-
threadId: { type: 'uuid', nullable: false },
|
|
275
|
-
referenceId: { type: 'bigint', nullable: true },
|
|
276
|
-
messageType: { type: 'text', nullable: true },
|
|
277
|
-
content: { type: 'text', nullable: true },
|
|
278
|
-
createdAt: { type: 'timestamp', nullable: true },
|
|
279
|
-
metadata: { type: 'jsonb', nullable: true },
|
|
280
|
-
};
|
|
281
|
-
|
|
282
|
-
await storage.createTable({ tableName: TABLE_MESSAGES, schema });
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
afterAll(async () => {
|
|
286
|
-
await storage.dropTable(TABLE_MESSAGES);
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
it('should query data by one key only', async () => {
|
|
290
|
-
const record = {
|
|
291
|
-
id: 1,
|
|
292
|
-
threadId: '123e4567-e89b-12d3-a456-426614174000',
|
|
293
|
-
referenceId: 1,
|
|
294
|
-
messageType: 'text',
|
|
295
|
-
content: 'Hello, world!',
|
|
296
|
-
createdAt: new Date(),
|
|
297
|
-
metadata: { foo: 'bar' },
|
|
298
|
-
};
|
|
299
|
-
|
|
300
|
-
await storage.insert({ tableName: TABLE_MESSAGES, record });
|
|
301
|
-
|
|
302
|
-
const loadedRecord = await storage.load({ tableName: TABLE_MESSAGES, keys: { id: 1 } });
|
|
303
|
-
expect(loadedRecord).not.toBeNull();
|
|
304
|
-
expect(loadedRecord.id).toEqual(record.id);
|
|
305
|
-
expect(loadedRecord.threadId).toEqual(record.threadId);
|
|
306
|
-
expect(loadedRecord.referenceId).toEqual(record.referenceId);
|
|
307
|
-
expect(loadedRecord.messageType).toEqual(record.messageType);
|
|
308
|
-
expect(loadedRecord.content).toEqual(record.content);
|
|
309
|
-
expect(new Date(loadedRecord.createdAt)).toEqual(new Date(record.createdAt));
|
|
310
|
-
expect(loadedRecord.metadata).toEqual(record.metadata);
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
it('should query data by multiple keys', async () => {
|
|
314
|
-
const record = {
|
|
315
|
-
id: 1,
|
|
316
|
-
threadId: '123e4567-e89b-12d3-a456-426614174000',
|
|
317
|
-
referenceId: 1,
|
|
318
|
-
messageType: 'hi',
|
|
319
|
-
content: 'Hello, world!',
|
|
320
|
-
createdAt: new Date(),
|
|
321
|
-
metadata: { foo: 'bar' },
|
|
322
|
-
};
|
|
323
|
-
|
|
324
|
-
await storage.insert({ tableName: TABLE_MESSAGES, record });
|
|
325
|
-
|
|
326
|
-
const loadedRecord = await storage.load({
|
|
327
|
-
tableName: TABLE_MESSAGES,
|
|
328
|
-
keys: { id: 1, messageType: 'hi' },
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
expect(loadedRecord).not.toBeNull();
|
|
332
|
-
expect(loadedRecord.id).toEqual(record.id);
|
|
333
|
-
expect(loadedRecord.threadId).toEqual(record.threadId);
|
|
334
|
-
expect(loadedRecord.referenceId).toEqual(record.referenceId);
|
|
335
|
-
expect(loadedRecord.messageType).toEqual(record.messageType);
|
|
336
|
-
expect(loadedRecord.content).toEqual(record.content);
|
|
337
|
-
expect(new Date(loadedRecord.createdAt)).toEqual(new Date(record.createdAt));
|
|
338
|
-
expect(loadedRecord.metadata).toEqual(record.metadata);
|
|
339
|
-
|
|
340
|
-
const recordsQueriedWithIdAndThreadId = await storage.load({
|
|
341
|
-
tableName: TABLE_MESSAGES,
|
|
342
|
-
keys: { id: 1, threadId: '123e4567-e89b-12d3-a456-426614174000' },
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
expect(recordsQueriedWithIdAndThreadId).not.toBeNull();
|
|
346
|
-
expect(recordsQueriedWithIdAndThreadId.id).toEqual(record.id);
|
|
347
|
-
expect(recordsQueriedWithIdAndThreadId.threadId).toEqual(record.threadId);
|
|
348
|
-
expect(recordsQueriedWithIdAndThreadId.referenceId).toEqual(record.referenceId);
|
|
349
|
-
expect(recordsQueriedWithIdAndThreadId.messageType).toEqual(record.messageType);
|
|
350
|
-
expect(recordsQueriedWithIdAndThreadId.content).toEqual(record.content);
|
|
351
|
-
expect(new Date(recordsQueriedWithIdAndThreadId.createdAt)).toEqual(new Date(record.createdAt));
|
|
352
|
-
expect(recordsQueriedWithIdAndThreadId.metadata).toEqual(record.metadata);
|
|
353
|
-
});
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
describe('Thread operations', () => {
|
|
357
|
-
beforeAll(async () => {
|
|
358
|
-
const threadTableSchema: Record<string, StorageColumn> = {
|
|
359
|
-
id: { type: 'uuid', nullable: false },
|
|
360
|
-
resourceId: { type: 'uuid', nullable: false },
|
|
361
|
-
title: { type: 'text', nullable: true },
|
|
362
|
-
createdAt: { type: 'timestamp', nullable: true },
|
|
363
|
-
updatedAt: { type: 'timestamp', nullable: true },
|
|
364
|
-
metadata: { type: 'jsonb', nullable: true },
|
|
365
|
-
};
|
|
366
|
-
|
|
367
|
-
await storage.createTable({ tableName: TABLE_THREADS, schema: threadTableSchema });
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
afterAll(async () => {
|
|
371
|
-
await storage.dropTable(TABLE_THREADS);
|
|
372
|
-
});
|
|
373
|
-
|
|
374
|
-
beforeEach(async () => {
|
|
375
|
-
await storage.clearTable({ tableName: TABLE_THREADS });
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
it('should get thread by ID', async () => {
|
|
379
|
-
const thread = {
|
|
380
|
-
id: '123e4567-e89b-12d3-a456-426614174000',
|
|
381
|
-
resourceId: '123e4567-e89b-12d3-a456-426614174000',
|
|
382
|
-
title: 'Test Thread',
|
|
383
|
-
createdAt: new Date(),
|
|
384
|
-
updatedAt: new Date(),
|
|
385
|
-
metadata: { foo: 'bar' },
|
|
386
|
-
};
|
|
387
|
-
|
|
388
|
-
await storage.insert({ tableName: TABLE_THREADS, record: thread });
|
|
389
|
-
|
|
390
|
-
const loadedThread = (await storage.getThreadById({ threadId: thread.id })) as StorageThreadType;
|
|
391
|
-
expect(loadedThread).not.toBeNull();
|
|
392
|
-
expect(loadedThread?.id).toEqual(thread.id);
|
|
393
|
-
expect(loadedThread?.resourceId).toEqual(thread.resourceId);
|
|
394
|
-
expect(loadedThread?.title).toEqual(thread.title);
|
|
395
|
-
expect(new Date(loadedThread?.createdAt)).toEqual(new Date(thread.createdAt));
|
|
396
|
-
expect(new Date(loadedThread?.updatedAt)).toEqual(new Date(thread.updatedAt));
|
|
397
|
-
expect(loadedThread?.metadata).toEqual(thread.metadata);
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
it('should save thread', async () => {
|
|
401
|
-
const thread = {
|
|
402
|
-
id: '123e4567-e89b-12d3-a456-426614174000',
|
|
403
|
-
resourceId: '123e4567-e89b-12d3-a456-426614174000',
|
|
404
|
-
title: 'Test Thread',
|
|
405
|
-
createdAt: new Date(),
|
|
406
|
-
updatedAt: new Date(),
|
|
407
|
-
metadata: { foo: 'bar' },
|
|
408
|
-
};
|
|
409
|
-
|
|
410
|
-
await storage.saveThread({ thread });
|
|
411
|
-
|
|
412
|
-
const loadedThread = (await storage.getThreadById({ threadId: thread.id })) as StorageThreadType;
|
|
413
|
-
expect(loadedThread).not.toBeNull();
|
|
414
|
-
expect(loadedThread?.id).toEqual(thread.id);
|
|
415
|
-
expect(loadedThread?.resourceId).toEqual(thread.resourceId);
|
|
416
|
-
expect(loadedThread?.title).toEqual(thread.title);
|
|
417
|
-
expect(new Date(loadedThread?.createdAt)).toEqual(new Date(thread.createdAt));
|
|
418
|
-
expect(new Date(loadedThread?.updatedAt)).toEqual(new Date(thread.updatedAt));
|
|
419
|
-
expect(loadedThread?.metadata).toEqual(thread.metadata);
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
it('should get threads by resource ID', async () => {
|
|
423
|
-
const resourceId = '123e4567-e89b-12d3-a456-426614174000';
|
|
424
|
-
const thread1 = {
|
|
425
|
-
id: '123e4567-e89b-12d3-a456-426614174000',
|
|
426
|
-
resourceId,
|
|
427
|
-
title: 'Test Thread',
|
|
428
|
-
createdAt: new Date(),
|
|
429
|
-
updatedAt: new Date(),
|
|
430
|
-
metadata: { foo: 'bar' },
|
|
431
|
-
};
|
|
432
|
-
|
|
433
|
-
const thread2 = {
|
|
434
|
-
id: '123e4567-e89b-12d3-a456-426614174001',
|
|
435
|
-
resourceId,
|
|
436
|
-
title: 'Test Thread',
|
|
437
|
-
createdAt: new Date(),
|
|
438
|
-
updatedAt: new Date(),
|
|
439
|
-
metadata: { foo: 'bar' },
|
|
440
|
-
};
|
|
441
|
-
|
|
442
|
-
await storage.saveThread({ thread: thread1 });
|
|
443
|
-
await storage.saveThread({ thread: thread2 });
|
|
444
|
-
|
|
445
|
-
const loadedThreads = await storage.getThreadsByResourceId({ resourceId });
|
|
446
|
-
|
|
447
|
-
expect(loadedThreads).not.toBeNull();
|
|
448
|
-
expect(loadedThreads.length).toEqual(2);
|
|
449
|
-
|
|
450
|
-
expect(loadedThreads[0].id).toEqual(thread1.id);
|
|
451
|
-
expect(loadedThreads[0].resourceId).toEqual(resourceId);
|
|
452
|
-
expect(loadedThreads[0].title).toEqual(thread1.title);
|
|
453
|
-
expect(new Date(loadedThreads[0].createdAt)).toEqual(new Date(thread1.createdAt));
|
|
454
|
-
expect(new Date(loadedThreads[0].updatedAt)).toEqual(new Date(thread1.updatedAt));
|
|
455
|
-
expect(loadedThreads[0].metadata).toEqual(thread1.metadata);
|
|
456
|
-
|
|
457
|
-
expect(loadedThreads[1].id).toEqual(thread2.id);
|
|
458
|
-
expect(loadedThreads[1].resourceId).toEqual(resourceId);
|
|
459
|
-
expect(loadedThreads[1].title).toEqual(thread2.title);
|
|
460
|
-
expect(new Date(loadedThreads[1].createdAt)).toEqual(new Date(thread2.createdAt));
|
|
461
|
-
expect(new Date(loadedThreads[1].updatedAt)).toEqual(new Date(thread2.updatedAt));
|
|
462
|
-
expect(loadedThreads[1].metadata).toEqual(thread2.metadata);
|
|
463
|
-
});
|
|
464
|
-
|
|
465
|
-
it('should update thread', async () => {
|
|
466
|
-
const thread = {
|
|
467
|
-
id: '123e4567-e89b-12d3-a456-426614174000',
|
|
468
|
-
resourceId: '123e4567-e89b-12d3-a456-426614174000',
|
|
469
|
-
title: 'Test Thread',
|
|
470
|
-
createdAt: new Date(),
|
|
471
|
-
updatedAt: new Date(),
|
|
472
|
-
metadata: { foo: 'bar' },
|
|
473
|
-
};
|
|
474
|
-
|
|
475
|
-
await storage.saveThread({ thread });
|
|
476
|
-
|
|
477
|
-
const updatedThread = await storage.updateThread({
|
|
478
|
-
id: thread.id,
|
|
479
|
-
title: 'Updated Thread',
|
|
480
|
-
metadata: { foo: 'hi' },
|
|
481
|
-
});
|
|
482
|
-
|
|
483
|
-
expect(updatedThread).not.toBeNull();
|
|
484
|
-
expect(updatedThread.id).toEqual(thread.id);
|
|
485
|
-
expect(updatedThread.title).toEqual('Updated Thread');
|
|
486
|
-
expect(updatedThread.metadata).toEqual({ foo: 'hi' });
|
|
487
|
-
});
|
|
488
|
-
|
|
489
|
-
it('should delete thread', async () => {
|
|
490
|
-
await storage.dropTable(TABLE_THREADS);
|
|
491
|
-
// create new table
|
|
492
|
-
const threadTableSchema: Record<string, StorageColumn> = {
|
|
493
|
-
id: { type: 'uuid', nullable: false },
|
|
494
|
-
resourceId: { type: 'uuid', nullable: false },
|
|
495
|
-
title: { type: 'text', nullable: true },
|
|
496
|
-
createdAt: { type: 'timestamp', nullable: true },
|
|
497
|
-
updatedAt: { type: 'timestamp', nullable: true },
|
|
498
|
-
metadata: { type: 'jsonb', nullable: true },
|
|
499
|
-
};
|
|
500
|
-
|
|
501
|
-
await storage.createTable({ tableName: TABLE_THREADS, schema: threadTableSchema });
|
|
502
|
-
|
|
503
|
-
const thread = {
|
|
504
|
-
id: '123e4567-e89b-12d3-a456-426614174023',
|
|
505
|
-
resourceId: '123e4567-e89b-12d3-a456-426614234020',
|
|
506
|
-
title: 'Test Thread',
|
|
507
|
-
createdAt: new Date(),
|
|
508
|
-
updatedAt: new Date(),
|
|
509
|
-
metadata: { foo: 'bar' },
|
|
510
|
-
} as StorageThreadType;
|
|
511
|
-
|
|
512
|
-
await storage.saveThread({ thread });
|
|
513
|
-
|
|
514
|
-
await storage.deleteThread({ threadId: thread.id });
|
|
515
|
-
|
|
516
|
-
const loadedThread = await storage.getThreadById({ threadId: thread.id });
|
|
517
|
-
expect(loadedThread).toBeNull();
|
|
518
|
-
});
|
|
519
|
-
});
|
|
520
|
-
|
|
521
|
-
describe('Message operations', () => {
|
|
522
|
-
beforeAll(async () => {
|
|
523
|
-
const messageTableSchema: Record<string, StorageColumn> = {
|
|
524
|
-
id: { type: 'uuid', nullable: false },
|
|
525
|
-
content: { type: 'text', nullable: true },
|
|
526
|
-
role: { type: 'text', nullable: true },
|
|
527
|
-
createdAt: { type: 'timestamp', nullable: false },
|
|
528
|
-
threadId: { type: 'uuid', nullable: false },
|
|
529
|
-
resourceId: { type: 'uuid', nullable: true },
|
|
530
|
-
toolCallIds: { type: 'text', nullable: true },
|
|
531
|
-
toolCallArgs: { type: 'jsonb', nullable: true },
|
|
532
|
-
toolNames: { type: 'text', nullable: true },
|
|
533
|
-
type: { type: 'text', nullable: true },
|
|
534
|
-
};
|
|
535
|
-
|
|
536
|
-
await storage.createTable({ tableName: TABLE_MESSAGES, schema: messageTableSchema });
|
|
537
|
-
});
|
|
538
|
-
|
|
539
|
-
afterAll(async () => {
|
|
540
|
-
await storage.dropTable(TABLE_MESSAGES);
|
|
541
|
-
});
|
|
542
|
-
|
|
543
|
-
afterEach(async () => {
|
|
544
|
-
await storage.clearTable({ tableName: TABLE_MESSAGES });
|
|
545
|
-
});
|
|
546
|
-
|
|
547
|
-
it('should save messages without error', async () => {
|
|
548
|
-
const messages = generateMessageRecords(10);
|
|
549
|
-
expect(async () => {
|
|
550
|
-
await storage.saveMessages({ messages, format: 'v2' });
|
|
551
|
-
}).not.toThrow();
|
|
552
|
-
});
|
|
553
|
-
|
|
554
|
-
it('should get messages by thread ID', async () => {
|
|
555
|
-
const threadId = '12333d567-e89b-12d3-a456-426614174000';
|
|
556
|
-
const messages = generateMessageRecords(10, threadId);
|
|
557
|
-
await storage.saveMessages({ messages, format: 'v2' });
|
|
558
|
-
const loadedMessages = await storage.getMessages({ threadId, format: 'v2' });
|
|
559
|
-
|
|
560
|
-
expect(loadedMessages).not.toBeNull();
|
|
561
|
-
expect(loadedMessages.length).toEqual(10);
|
|
562
|
-
|
|
563
|
-
loadedMessages.forEach((message, index) => {
|
|
564
|
-
expect(message.threadId).toEqual(threadId);
|
|
565
|
-
expect(message.id.toString()).toEqual(messages[index].id);
|
|
566
|
-
expect(message.content).toEqual(messages[index].content);
|
|
567
|
-
expect(message.role).toEqual(messages[index].role);
|
|
568
|
-
expect(message.resourceId).toEqual(messages[index].resourceId);
|
|
569
|
-
expect(message.type).toEqual(messages[index].type);
|
|
570
|
-
});
|
|
571
|
-
});
|
|
572
|
-
|
|
573
|
-
it('should get the last N messages when selectBy.last is specified', async () => {
|
|
574
|
-
const threadId = '12333d567-e89b-12d3-a456-426614174000';
|
|
575
|
-
const messages = generateMessageRecords(10, threadId);
|
|
576
|
-
await storage.saveMessages({ messages, format: 'v2' });
|
|
577
|
-
|
|
578
|
-
// Get the last 3 messages
|
|
579
|
-
const loadedMessages = await storage.getMessages({
|
|
580
|
-
threadId,
|
|
581
|
-
selectBy: { last: 3 },
|
|
582
|
-
format: 'v2',
|
|
583
|
-
});
|
|
584
|
-
|
|
585
|
-
expect(loadedMessages).not.toBeNull();
|
|
586
|
-
expect(loadedMessages.length).toEqual(3);
|
|
587
|
-
|
|
588
|
-
// Verify that we got the last 3 messages in chronological order
|
|
589
|
-
for (let i = 0; i < 3; i++) {
|
|
590
|
-
expect(loadedMessages[i].id.toString()).toEqual(messages[messages.length - 3 + i].id);
|
|
591
|
-
expect(loadedMessages[i].content).toEqual(messages[messages.length - 3 + i].content);
|
|
592
|
-
}
|
|
593
|
-
});
|
|
594
|
-
|
|
595
|
-
it('should get specific messages when selectBy.include is specified', async () => {
|
|
596
|
-
const threadId = '12333d567-e89b-12d3-a456-426614174000';
|
|
597
|
-
const messages = generateMessageRecords(10, threadId);
|
|
598
|
-
await storage.saveMessages({ messages, format: 'v2' });
|
|
599
|
-
|
|
600
|
-
// Select specific messages by ID
|
|
601
|
-
const messageIds = [messages[2].id, messages[5].id, messages[8].id];
|
|
602
|
-
const loadedMessages = await storage.getMessages({
|
|
603
|
-
threadId,
|
|
604
|
-
selectBy: {
|
|
605
|
-
include: messageIds.map(id => ({ id })),
|
|
606
|
-
},
|
|
607
|
-
format: 'v2',
|
|
608
|
-
});
|
|
609
|
-
|
|
610
|
-
expect(loadedMessages).not.toBeNull();
|
|
611
|
-
// We should get either the specified messages or all thread messages
|
|
612
|
-
expect(loadedMessages.length).toBeGreaterThanOrEqual(3);
|
|
613
|
-
|
|
614
|
-
// Verify that the selected messages are included in the results
|
|
615
|
-
const loadedIds = loadedMessages.map(m => m.id.toString());
|
|
616
|
-
messageIds.forEach(id => {
|
|
617
|
-
expect(loadedIds).toContain(id);
|
|
618
|
-
});
|
|
619
|
-
});
|
|
620
|
-
|
|
621
|
-
it('should handle empty results when using selectBy filters', async () => {
|
|
622
|
-
const threadId = '12333d567-e89b-12d3-a456-426614174000';
|
|
623
|
-
// Create messages for a different thread ID
|
|
624
|
-
const messages = generateMessageRecords(5, 'different-thread-id');
|
|
625
|
-
await storage.saveMessages({ messages, format: 'v2' });
|
|
626
|
-
|
|
627
|
-
// Try to get messages for our test threadId, which should return empty
|
|
628
|
-
const loadedMessages = await storage.getMessages({
|
|
629
|
-
threadId,
|
|
630
|
-
selectBy: { last: 3 },
|
|
631
|
-
format: 'v2',
|
|
632
|
-
});
|
|
633
|
-
|
|
634
|
-
expect(loadedMessages).not.toBeNull();
|
|
635
|
-
expect(loadedMessages.length).toEqual(0);
|
|
636
|
-
});
|
|
637
|
-
|
|
638
|
-
it('should throw error when threadConfig is provided', async () => {
|
|
639
|
-
const threadId = '12333d567-e89b-12d3-a456-426614174000';
|
|
640
|
-
const messages = generateMessageRecords(5, threadId);
|
|
641
|
-
await storage.saveMessages({ messages, format: 'v2' });
|
|
642
|
-
|
|
643
|
-
// Test that providing a threadConfig throws an error
|
|
644
|
-
await expect(
|
|
645
|
-
storage.getMessages({
|
|
646
|
-
threadId,
|
|
647
|
-
threadConfig: {
|
|
648
|
-
lastMessages: 10,
|
|
649
|
-
semanticRecall: {
|
|
650
|
-
topK: 5,
|
|
651
|
-
messageRange: { before: 3, after: 3 },
|
|
652
|
-
},
|
|
653
|
-
workingMemory: {
|
|
654
|
-
enabled: true,
|
|
655
|
-
},
|
|
656
|
-
threads: {
|
|
657
|
-
generateTitle: true,
|
|
658
|
-
},
|
|
659
|
-
},
|
|
660
|
-
}),
|
|
661
|
-
).rejects.toThrow('ThreadConfig is not supported by LanceDB storage');
|
|
662
|
-
});
|
|
663
|
-
|
|
664
|
-
it('should retrieve messages with context using withPreviousMessages and withNextMessages', async () => {
|
|
665
|
-
const threadId = '12333d567-e89b-12d3-a456-426614174000';
|
|
666
|
-
const messages = generateMessageRecords(10, threadId);
|
|
667
|
-
await storage.saveMessages({ messages, format: 'v2' });
|
|
668
|
-
|
|
669
|
-
// Get a specific message with context (previous and next messages)
|
|
670
|
-
const targetMessageId = messages[5].id;
|
|
671
|
-
const loadedMessages = await storage.getMessages({
|
|
672
|
-
threadId,
|
|
673
|
-
selectBy: {
|
|
674
|
-
include: [
|
|
675
|
-
{
|
|
676
|
-
id: targetMessageId,
|
|
677
|
-
withPreviousMessages: 2,
|
|
678
|
-
withNextMessages: 1,
|
|
679
|
-
},
|
|
680
|
-
],
|
|
681
|
-
},
|
|
682
|
-
format: 'v2',
|
|
683
|
-
});
|
|
684
|
-
|
|
685
|
-
expect(loadedMessages).not.toBeNull();
|
|
686
|
-
|
|
687
|
-
// We should get the target message plus 2 previous and 1 next message
|
|
688
|
-
// So a total of 4 messages (the target message, 2 before, and 1 after)
|
|
689
|
-
expect(loadedMessages.length).toEqual(4);
|
|
690
|
-
|
|
691
|
-
// Extract the IDs from the results for easier checking
|
|
692
|
-
const loadedIds = loadedMessages.map(m => m.id.toString());
|
|
693
|
-
|
|
694
|
-
// Check that the target message is included
|
|
695
|
-
expect(loadedIds).toContain(targetMessageId);
|
|
696
|
-
|
|
697
|
-
// Check that the previous 2 messages are included (messages[3] and messages[4])
|
|
698
|
-
expect(loadedIds).toContain(messages[3].id);
|
|
699
|
-
expect(loadedIds).toContain(messages[4].id);
|
|
700
|
-
|
|
701
|
-
// Check that the next message is included (messages[6])
|
|
702
|
-
expect(loadedIds).toContain(messages[6].id);
|
|
703
|
-
|
|
704
|
-
// Verify correct chronological order
|
|
705
|
-
for (let i = 0; i < loadedMessages.length - 1; i++) {
|
|
706
|
-
const currentDate = new Date(loadedMessages[i].createdAt).getTime();
|
|
707
|
-
const nextDate = new Date(loadedMessages[i + 1].createdAt).getTime();
|
|
708
|
-
expect(currentDate).toBeLessThanOrEqual(nextDate);
|
|
709
|
-
}
|
|
710
|
-
});
|
|
711
|
-
|
|
712
|
-
it('should upsert messages: duplicate id+threadId results in update, not duplicate row', async () => {
|
|
713
|
-
const thread = 'thread-1';
|
|
714
|
-
const baseMessage = generateMessageRecords(1, thread)[0];
|
|
715
|
-
|
|
716
|
-
// Insert the message for the first time
|
|
717
|
-
await storage.saveMessages({ messages: [baseMessage], format: 'v2' });
|
|
718
|
-
|
|
719
|
-
// Insert again with the same id and threadId but different content
|
|
720
|
-
const updatedMessage: MastraMessageV2 = {
|
|
721
|
-
...generateMessageRecords(1, thread)[0],
|
|
722
|
-
id: baseMessage.id,
|
|
723
|
-
content: { format: 2, parts: [{ type: 'text', text: 'Updated' }] },
|
|
724
|
-
};
|
|
725
|
-
|
|
726
|
-
await storage.saveMessages({ messages: [updatedMessage], format: 'v2' });
|
|
727
|
-
|
|
728
|
-
// Retrieve messages for the thread
|
|
729
|
-
const retrievedMessages = await storage.getMessages({ threadId: thread, format: 'v2' });
|
|
730
|
-
// Only one message should exist for that id+threadId
|
|
731
|
-
expect(retrievedMessages.filter(m => m.id.toString() === baseMessage.id)).toHaveLength(1);
|
|
732
|
-
|
|
733
|
-
// The content should be the updated one
|
|
734
|
-
expect(retrievedMessages.find(m => m.id.toString() === baseMessage.id)?.content.parts[0].text).toBe('Updated');
|
|
735
|
-
});
|
|
736
|
-
|
|
737
|
-
it('should upsert messages: duplicate id and different threadid', async () => {
|
|
738
|
-
const thread1 = 'thread-1';
|
|
739
|
-
const thread2 = 'thread-2';
|
|
740
|
-
const thread3 = 'thread-3';
|
|
741
|
-
|
|
742
|
-
const message = generateMessageRecords(1, thread1)[0];
|
|
743
|
-
|
|
744
|
-
// Insert message into thread1
|
|
745
|
-
await storage.saveMessages({ messages: [message], format: 'v2' });
|
|
746
|
-
|
|
747
|
-
// Attempt to insert a message with the same id but different threadId
|
|
748
|
-
const conflictingMessage: MastraMessageV2 = {
|
|
749
|
-
...generateMessageRecords(1, thread2)[0],
|
|
750
|
-
id: message.id,
|
|
751
|
-
content: { format: 2, parts: [{ type: 'text', text: 'Thread2 Content' }] },
|
|
752
|
-
};
|
|
753
|
-
|
|
754
|
-
const differentMessage: MastraMessageV2 = {
|
|
755
|
-
...generateMessageRecords(1, thread3)[0],
|
|
756
|
-
id: '2',
|
|
757
|
-
content: { format: 2, parts: [{ type: 'text', text: 'Another Message Content' }] },
|
|
758
|
-
};
|
|
759
|
-
|
|
760
|
-
// Save should move the message to the new thread
|
|
761
|
-
await storage.saveMessages({ messages: [conflictingMessage], format: 'v2' });
|
|
762
|
-
|
|
763
|
-
await storage.saveMessages({ messages: [differentMessage], format: 'v2' });
|
|
764
|
-
|
|
765
|
-
// Retrieve messages for both threads
|
|
766
|
-
const thread1Messages = await storage.getMessages({ threadId: thread1, format: 'v2' });
|
|
767
|
-
const thread2Messages = await storage.getMessages({ threadId: thread2, format: 'v2' });
|
|
768
|
-
const thread3Messages = await storage.getMessages({ threadId: thread3, format: 'v2' });
|
|
769
|
-
|
|
770
|
-
// Thread 1 should NOT have the message with that id
|
|
771
|
-
expect(thread1Messages.find(m => m.id.toString() === message.id)).toBeUndefined();
|
|
772
|
-
|
|
773
|
-
// Thread 2 should have the message with that id
|
|
774
|
-
expect(thread2Messages.find(m => m.id.toString() === message.id)?.content.parts[0].text).toBe('Thread2 Content');
|
|
775
|
-
|
|
776
|
-
// Thread 2 should have the other message
|
|
777
|
-
expect(thread3Messages.find(m => m.id.toString() === differentMessage.id)?.content.parts[0].text).toBe(
|
|
778
|
-
'Another Message Content',
|
|
779
|
-
);
|
|
780
|
-
});
|
|
781
|
-
});
|
|
782
|
-
|
|
783
|
-
describe('Trace operations', () => {
|
|
784
|
-
beforeAll(async () => {
|
|
785
|
-
const traceTableSchema = TABLE_SCHEMAS[TABLE_TRACES];
|
|
786
|
-
await storage.createTable({ tableName: TABLE_TRACES, schema: traceTableSchema });
|
|
787
|
-
});
|
|
788
|
-
|
|
789
|
-
afterAll(async () => {
|
|
790
|
-
await storage.dropTable(TABLE_TRACES);
|
|
791
|
-
});
|
|
792
|
-
|
|
793
|
-
afterEach(async () => {
|
|
794
|
-
await storage.clearTable({ tableName: TABLE_TRACES });
|
|
795
|
-
});
|
|
796
|
-
|
|
797
|
-
it('should save trace', async () => {
|
|
798
|
-
const trace = {
|
|
799
|
-
id: '123e4567-e89b-12d3-a456-426614174023',
|
|
800
|
-
parentSpanId: '123e4567-e89b-12d3-a456-426614174023',
|
|
801
|
-
name: 'Test Trace',
|
|
802
|
-
traceId: '123e4567-e89b-12d3-a456-426614234020',
|
|
803
|
-
scope: 'test',
|
|
804
|
-
kind: 0,
|
|
805
|
-
attributes: { attribute1: 'value1' },
|
|
806
|
-
status: { code: 0, description: 'OK' },
|
|
807
|
-
events: { event1: 'value1' },
|
|
808
|
-
links: { link1: 'value1' },
|
|
809
|
-
other: { other1: 'value1' },
|
|
810
|
-
startTime: new Date().getTime(),
|
|
811
|
-
endTime: new Date().getTime(),
|
|
812
|
-
createdAt: new Date(),
|
|
813
|
-
} as TraceType;
|
|
814
|
-
|
|
815
|
-
await storage.saveTrace({ trace });
|
|
816
|
-
|
|
817
|
-
const loadedTrace = await storage.getTraceById({ traceId: trace.id });
|
|
818
|
-
|
|
819
|
-
expect(loadedTrace).not.toBeNull();
|
|
820
|
-
expect(loadedTrace.id).toEqual(trace.id);
|
|
821
|
-
expect(loadedTrace.name).toEqual('Test Trace');
|
|
822
|
-
expect(loadedTrace.parentSpanId).toEqual(trace.parentSpanId);
|
|
823
|
-
expect(loadedTrace.traceId).toEqual(trace.traceId);
|
|
824
|
-
expect(loadedTrace.scope).toEqual(trace.scope);
|
|
825
|
-
expect(loadedTrace.kind).toEqual(trace.kind);
|
|
826
|
-
expect(loadedTrace.attributes).toEqual(trace.attributes);
|
|
827
|
-
expect(loadedTrace.status).toEqual(trace.status);
|
|
828
|
-
expect(loadedTrace.events).toEqual(trace.events);
|
|
829
|
-
expect(loadedTrace.links).toEqual(trace.links);
|
|
830
|
-
expect(loadedTrace.other).toEqual(trace.other);
|
|
831
|
-
expect(loadedTrace.startTime).toEqual(trace.startTime);
|
|
832
|
-
expect(loadedTrace.endTime).toEqual(trace.endTime);
|
|
833
|
-
expect(new Date(loadedTrace.createdAt)).toEqual(trace.createdAt);
|
|
834
|
-
});
|
|
835
|
-
|
|
836
|
-
it('should get traces by page', async () => {
|
|
837
|
-
const traces = generateTraceRecords(10);
|
|
838
|
-
|
|
839
|
-
await Promise.all(traces.map(trace => storage.saveTrace({ trace })));
|
|
840
|
-
|
|
841
|
-
const loadedTrace = await storage.getTraces({
|
|
842
|
-
page: 1,
|
|
843
|
-
perPage: 10,
|
|
844
|
-
});
|
|
845
|
-
|
|
846
|
-
expect(loadedTrace).not.toBeNull();
|
|
847
|
-
expect(loadedTrace.length).toEqual(10);
|
|
848
|
-
|
|
849
|
-
loadedTrace.forEach(trace => {
|
|
850
|
-
expect(trace.id).toEqual(traces.find(t => t.id === trace.id)?.id);
|
|
851
|
-
expect(trace.name).toEqual(traces.find(t => t.id === trace.id)?.name);
|
|
852
|
-
expect(trace.parentSpanId).toEqual(traces.find(t => t.id === trace.id)?.parentSpanId);
|
|
853
|
-
expect(trace.traceId).toEqual(traces.find(t => t.id === trace.id)?.traceId);
|
|
854
|
-
expect(trace.scope).toEqual(traces.find(t => t.id === trace.id)?.scope);
|
|
855
|
-
expect(trace.kind).toEqual(traces.find(t => t.id === trace.id)?.kind);
|
|
856
|
-
expect(trace.attributes).toEqual(traces.find(t => t.id === trace.id)?.attributes);
|
|
857
|
-
expect(trace.status).toEqual(traces.find(t => t.id === trace.id)?.status);
|
|
858
|
-
expect(trace.events).toEqual(traces.find(t => t.id === trace.id)?.events);
|
|
859
|
-
expect(trace.links).toEqual(traces.find(t => t.id === trace.id)?.links);
|
|
860
|
-
expect(trace.other).toEqual(traces.find(t => t.id === trace.id)?.other);
|
|
861
|
-
expect(new Date(trace.startTime)).toEqual(new Date(traces.find(t => t.id === trace.id)?.startTime ?? ''));
|
|
862
|
-
expect(new Date(trace.endTime)).toEqual(new Date(traces.find(t => t.id === trace.id)?.endTime ?? ''));
|
|
863
|
-
expect(new Date(trace.createdAt)).toEqual(new Date(traces.find(t => t.id === trace.id)?.createdAt ?? ''));
|
|
864
|
-
});
|
|
865
|
-
});
|
|
866
|
-
|
|
867
|
-
it('should get trace by name and scope', async () => {
|
|
868
|
-
const trace = generateTraceRecords(1)[0];
|
|
869
|
-
await storage.saveTrace({ trace });
|
|
870
|
-
|
|
871
|
-
const loadedTrace = await storage.getTraces({ name: trace.name, scope: trace.scope, page: 1, perPage: 10 });
|
|
872
|
-
|
|
873
|
-
expect(loadedTrace).not.toBeNull();
|
|
874
|
-
expect(loadedTrace[0].id).toEqual(trace.id);
|
|
875
|
-
expect(loadedTrace[0].name).toEqual(trace.name);
|
|
876
|
-
expect(loadedTrace[0].parentSpanId).toEqual(trace.parentSpanId);
|
|
877
|
-
expect(loadedTrace[0].scope).toEqual(trace.scope);
|
|
878
|
-
expect(loadedTrace[0].kind).toEqual(trace.kind);
|
|
879
|
-
expect(loadedTrace[0].attributes).toEqual(trace.attributes);
|
|
880
|
-
expect(loadedTrace[0].status).toEqual(trace.status);
|
|
881
|
-
expect(loadedTrace[0].events).toEqual(trace.events);
|
|
882
|
-
expect(loadedTrace[0].links).toEqual(trace.links);
|
|
883
|
-
expect(loadedTrace[0].other).toEqual(trace.other);
|
|
884
|
-
expect(loadedTrace[0].startTime).toEqual(new Date(trace.startTime));
|
|
885
|
-
expect(loadedTrace[0].endTime).toEqual(new Date(trace.endTime));
|
|
886
|
-
expect(loadedTrace[0].createdAt).toEqual(new Date(trace.createdAt));
|
|
887
|
-
});
|
|
888
|
-
});
|
|
889
|
-
|
|
890
|
-
describe('Eval operations', () => {
|
|
891
|
-
beforeAll(async () => {
|
|
892
|
-
const evalSchema = TABLE_SCHEMAS[TABLE_EVALS];
|
|
893
|
-
await storage.createTable({ tableName: TABLE_EVALS, schema: evalSchema });
|
|
894
|
-
});
|
|
895
|
-
|
|
896
|
-
afterAll(async () => {
|
|
897
|
-
await storage.dropTable(TABLE_EVALS);
|
|
898
|
-
});
|
|
899
|
-
|
|
900
|
-
it('should get evals by agent name', async () => {
|
|
901
|
-
const evals = generateEvalRecords(1);
|
|
902
|
-
await storage.saveEvals({ evals });
|
|
903
|
-
|
|
904
|
-
const loadedEvals = await storage.getEvalsByAgentName(evals[0].agentName);
|
|
905
|
-
|
|
906
|
-
expect(loadedEvals).not.toBeNull();
|
|
907
|
-
expect(loadedEvals.length).toBe(1);
|
|
908
|
-
expect(loadedEvals[0].input).toEqual(evals[0].input);
|
|
909
|
-
expect(loadedEvals[0].output).toEqual(evals[0].output);
|
|
910
|
-
expect(loadedEvals[0].agentName).toEqual(evals[0].agentName);
|
|
911
|
-
expect(loadedEvals[0].metricName).toEqual(evals[0].metricName);
|
|
912
|
-
expect(loadedEvals[0].result).toEqual(evals[0].result);
|
|
913
|
-
expect(loadedEvals[0].testInfo).toEqual(evals[0].testInfo);
|
|
914
|
-
expect(new Date(loadedEvals[0].createdAt)).toEqual(new Date(evals[0].createdAt));
|
|
915
|
-
});
|
|
916
|
-
});
|
|
917
|
-
|
|
918
|
-
describe('Workflow Operations', () => {
|
|
919
|
-
beforeAll(async () => {
|
|
920
|
-
const workflowSchema = TABLE_SCHEMAS[TABLE_WORKFLOW_SNAPSHOT];
|
|
921
|
-
await storage.createTable({ tableName: TABLE_WORKFLOW_SNAPSHOT, schema: workflowSchema });
|
|
922
|
-
});
|
|
923
|
-
|
|
924
|
-
afterAll(async () => {
|
|
925
|
-
await storage.dropTable(TABLE_WORKFLOW_SNAPSHOT);
|
|
926
|
-
});
|
|
927
|
-
|
|
928
|
-
it('should save and retrieve workflow snapshots', async () => {
|
|
929
|
-
const { snapshot, runId } = generateWorkflowSnapshot('running');
|
|
930
|
-
|
|
931
|
-
await storage.persistWorkflowSnapshot({
|
|
932
|
-
workflowName: 'test-workflow',
|
|
933
|
-
runId,
|
|
934
|
-
snapshot,
|
|
935
|
-
});
|
|
936
|
-
const retrieved = await storage.loadWorkflowSnapshot({
|
|
937
|
-
workflowName: 'test-workflow',
|
|
938
|
-
runId,
|
|
939
|
-
});
|
|
940
|
-
expect(retrieved).toEqual(snapshot);
|
|
941
|
-
});
|
|
942
|
-
|
|
943
|
-
it('should handle non-existent workflow snapshots', async () => {
|
|
944
|
-
const result = await storage.loadWorkflowSnapshot({
|
|
945
|
-
workflowName: 'test-workflow',
|
|
946
|
-
runId: 'non-existent',
|
|
947
|
-
});
|
|
948
|
-
expect(result).toBeNull();
|
|
949
|
-
});
|
|
950
|
-
|
|
951
|
-
it('should update workflow snapshot status', async () => {
|
|
952
|
-
const { snapshot, runId } = generateWorkflowSnapshot('running');
|
|
953
|
-
|
|
954
|
-
await storage.persistWorkflowSnapshot({
|
|
955
|
-
workflowName: 'test-workflow',
|
|
956
|
-
runId,
|
|
957
|
-
snapshot,
|
|
958
|
-
});
|
|
959
|
-
|
|
960
|
-
const updatedSnapshot = {
|
|
961
|
-
...snapshot,
|
|
962
|
-
value: { [runId]: 'success' },
|
|
963
|
-
timestamp: Date.now(),
|
|
964
|
-
};
|
|
965
|
-
|
|
966
|
-
await storage.persistWorkflowSnapshot({
|
|
967
|
-
workflowName: 'test-workflow',
|
|
968
|
-
runId,
|
|
969
|
-
snapshot: updatedSnapshot,
|
|
970
|
-
});
|
|
971
|
-
|
|
972
|
-
const retrieved = await storage.loadWorkflowSnapshot({
|
|
973
|
-
workflowName: 'test-workflow',
|
|
974
|
-
runId,
|
|
975
|
-
});
|
|
976
|
-
|
|
977
|
-
expect(retrieved?.value[runId]).toBe('success');
|
|
978
|
-
expect(retrieved?.timestamp).toBeGreaterThan(snapshot.timestamp);
|
|
979
|
-
});
|
|
980
|
-
|
|
981
|
-
it('should handle complex workflow state', async () => {
|
|
982
|
-
const runId = `run-${randomUUID()}`;
|
|
983
|
-
const workflowName = 'complex-workflow';
|
|
984
|
-
|
|
985
|
-
const complexSnapshot = {
|
|
986
|
-
runId,
|
|
987
|
-
value: { currentState: 'running' },
|
|
988
|
-
timestamp: Date.now(),
|
|
989
|
-
context: {
|
|
990
|
-
'step-1': {
|
|
991
|
-
status: 'success',
|
|
992
|
-
output: {
|
|
993
|
-
nestedData: {
|
|
994
|
-
array: [1, 2, 3],
|
|
995
|
-
object: { key: 'value' },
|
|
996
|
-
date: new Date().toISOString(),
|
|
997
|
-
},
|
|
998
|
-
},
|
|
999
|
-
},
|
|
1000
|
-
'step-2': {
|
|
1001
|
-
status: 'suspended',
|
|
1002
|
-
dependencies: ['step-3', 'step-4'],
|
|
1003
|
-
},
|
|
1004
|
-
input: {
|
|
1005
|
-
type: 'scheduled',
|
|
1006
|
-
metadata: {
|
|
1007
|
-
schedule: '0 0 * * *',
|
|
1008
|
-
timezone: 'UTC',
|
|
1009
|
-
},
|
|
1010
|
-
},
|
|
1011
|
-
},
|
|
1012
|
-
activePaths: [],
|
|
1013
|
-
suspendedPaths: {},
|
|
1014
|
-
status: 'suspended',
|
|
1015
|
-
serializedStepGraph: [],
|
|
1016
|
-
} as unknown as WorkflowRunState;
|
|
1017
|
-
|
|
1018
|
-
await storage.persistWorkflowSnapshot({
|
|
1019
|
-
workflowName,
|
|
1020
|
-
runId,
|
|
1021
|
-
snapshot: complexSnapshot,
|
|
1022
|
-
});
|
|
1023
|
-
|
|
1024
|
-
const loadedSnapshot = await storage.loadWorkflowSnapshot({
|
|
1025
|
-
workflowName,
|
|
1026
|
-
runId,
|
|
1027
|
-
});
|
|
1028
|
-
|
|
1029
|
-
expect(loadedSnapshot).toEqual(complexSnapshot);
|
|
1030
|
-
});
|
|
1031
|
-
});
|
|
1032
|
-
|
|
1033
|
-
describe('getWorkflowRuns', () => {
|
|
1034
|
-
beforeAll(async () => {
|
|
1035
|
-
const workflowSchema = TABLE_SCHEMAS[TABLE_WORKFLOW_SNAPSHOT];
|
|
1036
|
-
await storage.createTable({ tableName: TABLE_WORKFLOW_SNAPSHOT, schema: workflowSchema });
|
|
1037
|
-
});
|
|
1038
|
-
|
|
1039
|
-
afterAll(async () => {
|
|
1040
|
-
await storage.dropTable(TABLE_WORKFLOW_SNAPSHOT);
|
|
1041
|
-
});
|
|
1042
|
-
|
|
1043
|
-
beforeEach(async () => {
|
|
1044
|
-
await storage.clearTable({ tableName: TABLE_WORKFLOW_SNAPSHOT });
|
|
1045
|
-
});
|
|
1046
|
-
it('returns empty array when no workflows exist', async () => {
|
|
1047
|
-
const { runs, total } = await storage.getWorkflowRuns();
|
|
1048
|
-
expect(runs).toEqual([]);
|
|
1049
|
-
expect(total).toBe(0);
|
|
1050
|
-
});
|
|
1051
|
-
|
|
1052
|
-
it('returns all workflows by default', async () => {
|
|
1053
|
-
const workflowName1 = 'default_test_1';
|
|
1054
|
-
const workflowName2 = 'default_test_2';
|
|
1055
|
-
|
|
1056
|
-
const { snapshot: workflow1, runId: runId1, stepId: stepId1 } = generateWorkflowSnapshot('success');
|
|
1057
|
-
const { snapshot: workflow2, runId: runId2, stepId: stepId2 } = generateWorkflowSnapshot('running');
|
|
1058
|
-
|
|
1059
|
-
await storage.persistWorkflowSnapshot({ workflowName: workflowName1, runId: runId1, snapshot: workflow1 });
|
|
1060
|
-
await new Promise(resolve => setTimeout(resolve, 10)); // Small delay to ensure different timestamps
|
|
1061
|
-
await storage.persistWorkflowSnapshot({ workflowName: workflowName2, runId: runId2, snapshot: workflow2 });
|
|
1062
|
-
|
|
1063
|
-
const { runs, total } = await storage.getWorkflowRuns();
|
|
1064
|
-
expect(runs).toHaveLength(2);
|
|
1065
|
-
expect(total).toBe(2);
|
|
1066
|
-
expect(runs[0]!.workflowName).toBe(workflowName1);
|
|
1067
|
-
expect(runs[1]!.workflowName).toBe(workflowName2);
|
|
1068
|
-
const firstSnapshot = runs[0]!.snapshot as WorkflowRunState;
|
|
1069
|
-
const secondSnapshot = runs[1]!.snapshot as WorkflowRunState;
|
|
1070
|
-
expect(firstSnapshot.context?.[stepId1]?.status).toBe('success');
|
|
1071
|
-
expect(secondSnapshot.context?.[stepId2]?.status).toBe('running');
|
|
1072
|
-
});
|
|
1073
|
-
|
|
1074
|
-
// it('filters by workflow name', async () => {
|
|
1075
|
-
// const workflowName1 = 'filter_test_1';
|
|
1076
|
-
// const workflowName2 = 'filter_test_2';
|
|
1077
|
-
|
|
1078
|
-
// const { snapshot: workflow1, runId: runId1, stepId: stepId1 } = generateWorkflowSnapshot('success');
|
|
1079
|
-
// const { snapshot: workflow2, runId: runId2 } = generateWorkflowSnapshot('failed');
|
|
1080
|
-
|
|
1081
|
-
// await storage.persistWorkflowSnapshot({ workflowName: workflowName1, runId: runId1, snapshot: workflow1 });
|
|
1082
|
-
// await new Promise(resolve => setTimeout(resolve, 10)); // Small delay to ensure different timestamps
|
|
1083
|
-
// await storage.persistWorkflowSnapshot({ workflowName: workflowName2, runId: runId2, snapshot: workflow2 });
|
|
1084
|
-
|
|
1085
|
-
// const { runs, total } = await storage.getWorkflowRuns({ workflowName: workflowName1 });
|
|
1086
|
-
// expect(runs).toHaveLength(1);
|
|
1087
|
-
// expect(total).toBe(1);
|
|
1088
|
-
// expect(runs[0]!.workflowName).toBe(workflowName1);
|
|
1089
|
-
// const snapshot = runs[0]!.snapshot as WorkflowRunState;
|
|
1090
|
-
// expect(snapshot.context?.[stepId1]?.status).toBe('success');
|
|
1091
|
-
// });
|
|
1092
|
-
|
|
1093
|
-
// it('filters by date range', async () => {
|
|
1094
|
-
// const now = new Date();
|
|
1095
|
-
// const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
|
1096
|
-
// const twoDaysAgo = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000);
|
|
1097
|
-
// const workflowName1 = 'date_test_1';
|
|
1098
|
-
// const workflowName2 = 'date_test_2';
|
|
1099
|
-
// const workflowName3 = 'date_test_3';
|
|
1100
|
-
|
|
1101
|
-
// const { snapshot: workflow1, runId: runId1 } = generateWorkflowSnapshot('success');
|
|
1102
|
-
// const { snapshot: workflow2, runId: runId2, stepId: stepId2 } = generateWorkflowSnapshot('failed');
|
|
1103
|
-
// const { snapshot: workflow3, runId: runId3, stepId: stepId3 } = generateWorkflowSnapshot('suspended');
|
|
1104
|
-
|
|
1105
|
-
// await storage.insert({
|
|
1106
|
-
// tableName: TABLE_WORKFLOW_SNAPSHOT,
|
|
1107
|
-
// record: {
|
|
1108
|
-
// workflow_name: workflowName1,
|
|
1109
|
-
// run_id: runId1,
|
|
1110
|
-
// snapshot: workflow1,
|
|
1111
|
-
// createdAt: twoDaysAgo,
|
|
1112
|
-
// updatedAt: twoDaysAgo,
|
|
1113
|
-
// },
|
|
1114
|
-
// });
|
|
1115
|
-
// await storage.insert({
|
|
1116
|
-
// tableName: TABLE_WORKFLOW_SNAPSHOT,
|
|
1117
|
-
// record: {
|
|
1118
|
-
// workflow_name: workflowName2,
|
|
1119
|
-
// run_id: runId2,
|
|
1120
|
-
// snapshot: workflow2,
|
|
1121
|
-
// createdAt: yesterday,
|
|
1122
|
-
// updatedAt: yesterday,
|
|
1123
|
-
// },
|
|
1124
|
-
// });
|
|
1125
|
-
// await storage.insert({
|
|
1126
|
-
// tableName: TABLE_WORKFLOW_SNAPSHOT,
|
|
1127
|
-
// record: {
|
|
1128
|
-
// workflow_name: workflowName3,
|
|
1129
|
-
// run_id: runId3,
|
|
1130
|
-
// snapshot: workflow3,
|
|
1131
|
-
// createdAt: now,
|
|
1132
|
-
// updatedAt: now,
|
|
1133
|
-
// },
|
|
1134
|
-
// });
|
|
1135
|
-
|
|
1136
|
-
// const { runs } = await storage.getWorkflowRuns({
|
|
1137
|
-
// fromDate: yesterday,
|
|
1138
|
-
// toDate: now,
|
|
1139
|
-
// });
|
|
1140
|
-
|
|
1141
|
-
// expect(runs).toHaveLength(2);
|
|
1142
|
-
// expect(runs[0]!.workflowName).toBe(workflowName3);
|
|
1143
|
-
// expect(runs[1]!.workflowName).toBe(workflowName2);
|
|
1144
|
-
// const firstSnapshot = runs[0]!.snapshot as WorkflowRunState;
|
|
1145
|
-
// const secondSnapshot = runs[1]!.snapshot as WorkflowRunState;
|
|
1146
|
-
// expect(firstSnapshot.context?.[stepId3]?.status).toBe('waiting');
|
|
1147
|
-
// expect(secondSnapshot.context?.[stepId2]?.status).toBe('running');
|
|
1148
|
-
// });
|
|
1149
|
-
|
|
1150
|
-
// it('handles pagination', async () => {
|
|
1151
|
-
// const workflowName1 = 'page_test_1';
|
|
1152
|
-
// const workflowName2 = 'page_test_2';
|
|
1153
|
-
// const workflowName3 = 'page_test_3';
|
|
1154
|
-
|
|
1155
|
-
// const { snapshot: workflow1, runId: runId1, stepId: stepId1 } = generateWorkflowSnapshot('success');
|
|
1156
|
-
// const { snapshot: workflow2, runId: runId2, stepId: stepId2 } = generateWorkflowSnapshot('failed');
|
|
1157
|
-
// const { snapshot: workflow3, runId: runId3, stepId: stepId3 } = generateWorkflowSnapshot('suspended');
|
|
1158
|
-
|
|
1159
|
-
// await storage.persistWorkflowSnapshot({ workflowName: workflowName1, runId: runId1, snapshot: workflow1 });
|
|
1160
|
-
// await new Promise(resolve => setTimeout(resolve, 10)); // Small delay to ensure different timestamps
|
|
1161
|
-
// await storage.persistWorkflowSnapshot({ workflowName: workflowName2, runId: runId2, snapshot: workflow2 });
|
|
1162
|
-
// await new Promise(resolve => setTimeout(resolve, 10)); // Small delay to ensure different timestamps
|
|
1163
|
-
// await storage.persistWorkflowSnapshot({ workflowName: workflowName3, runId: runId3, snapshot: workflow3 });
|
|
1164
|
-
|
|
1165
|
-
// // Get first page
|
|
1166
|
-
// const page1 = await storage.getWorkflowRuns({ limit: 2, offset: 0 });
|
|
1167
|
-
// expect(page1.runs).toHaveLength(2);
|
|
1168
|
-
// expect(page1.total).toBe(3); // Total count of all records
|
|
1169
|
-
// expect(page1.runs[0]!.workflowName).toBe(workflowName3);
|
|
1170
|
-
// expect(page1.runs[1]!.workflowName).toBe(workflowName2);
|
|
1171
|
-
// const firstSnapshot = page1.runs[0]!.snapshot as WorkflowRunState;
|
|
1172
|
-
// const secondSnapshot = page1.runs[1]!.snapshot as WorkflowRunState;
|
|
1173
|
-
// expect(firstSnapshot.context?.[stepId3]?.status).toBe('waiting');
|
|
1174
|
-
// expect(secondSnapshot.context?.[stepId2]?.status).toBe('running');
|
|
1175
|
-
|
|
1176
|
-
// // Get second page
|
|
1177
|
-
// const page2 = await storage.getWorkflowRuns({ limit: 2, offset: 2 });
|
|
1178
|
-
// expect(page2.runs).toHaveLength(1);
|
|
1179
|
-
// expect(page2.total).toBe(3);
|
|
1180
|
-
// expect(page2.runs[0]!.workflowName).toBe(workflowName1);
|
|
1181
|
-
// const snapshot = page2.runs[0]!.snapshot as WorkflowRunState;
|
|
1182
|
-
// expect(snapshot.context?.[stepId1]?.status).toBe('completed');
|
|
1183
|
-
// });
|
|
1184
|
-
});
|
|
1185
|
-
|
|
1186
|
-
describe('getWorkflowRunById', () => {
|
|
1187
|
-
const workflowName = 'workflow-id-test';
|
|
1188
|
-
let runId: string;
|
|
1189
|
-
let stepId: string;
|
|
1190
|
-
|
|
1191
|
-
beforeAll(async () => {
|
|
1192
|
-
const workflowSchema = TABLE_SCHEMAS[TABLE_WORKFLOW_SNAPSHOT];
|
|
1193
|
-
await storage.createTable({ tableName: TABLE_WORKFLOW_SNAPSHOT, schema: workflowSchema });
|
|
1194
|
-
});
|
|
1195
|
-
|
|
1196
|
-
afterAll(async () => {
|
|
1197
|
-
await storage.dropTable(TABLE_WORKFLOW_SNAPSHOT);
|
|
1198
|
-
});
|
|
1199
|
-
|
|
1200
|
-
beforeEach(async () => {
|
|
1201
|
-
// Insert a workflow run for positive test
|
|
1202
|
-
const sample = generateWorkflowSnapshot('success');
|
|
1203
|
-
runId = sample.runId;
|
|
1204
|
-
stepId = sample.stepId;
|
|
1205
|
-
await storage.insert({
|
|
1206
|
-
tableName: TABLE_WORKFLOW_SNAPSHOT,
|
|
1207
|
-
record: {
|
|
1208
|
-
workflow_name: workflowName,
|
|
1209
|
-
run_id: runId,
|
|
1210
|
-
resourceId: 'resource-abc',
|
|
1211
|
-
snapshot: sample.snapshot,
|
|
1212
|
-
createdAt: new Date(),
|
|
1213
|
-
updatedAt: new Date(),
|
|
1214
|
-
},
|
|
1215
|
-
});
|
|
1216
|
-
});
|
|
1217
|
-
|
|
1218
|
-
it('should retrieve a workflow run by ID', async () => {
|
|
1219
|
-
const found = await storage.getWorkflowRunById({
|
|
1220
|
-
runId,
|
|
1221
|
-
workflowName,
|
|
1222
|
-
});
|
|
1223
|
-
expect(found).not.toBeNull();
|
|
1224
|
-
expect(found?.runId).toBe(runId);
|
|
1225
|
-
checkWorkflowSnapshot(found?.snapshot!, stepId, 'success');
|
|
1226
|
-
});
|
|
1227
|
-
|
|
1228
|
-
it('should return null for non-existent workflow run ID', async () => {
|
|
1229
|
-
const notFound = await storage.getWorkflowRunById({
|
|
1230
|
-
runId: 'non-existent-id',
|
|
1231
|
-
workflowName,
|
|
1232
|
-
});
|
|
1233
|
-
expect(notFound).toBeNull();
|
|
1234
|
-
});
|
|
1235
|
-
});
|
|
1236
|
-
|
|
1237
|
-
describe('alterTable', () => {
|
|
1238
|
-
const TEST_TABLE = 'test_alter_table';
|
|
1239
|
-
const BASE_SCHEMA = {
|
|
1240
|
-
id: { type: 'integer', primaryKey: true, nullable: false },
|
|
1241
|
-
name: { type: 'text', nullable: true },
|
|
1242
|
-
createdAt: { type: 'timestamp', nullable: false },
|
|
1243
|
-
} as Record<string, StorageColumn>;
|
|
1244
|
-
|
|
1245
|
-
beforeEach(async () => {
|
|
1246
|
-
await storage.dropTable(TEST_TABLE as TABLE_NAMES);
|
|
1247
|
-
await storage.createTable({ tableName: TEST_TABLE as TABLE_NAMES, schema: BASE_SCHEMA });
|
|
1248
|
-
});
|
|
1249
|
-
|
|
1250
|
-
afterEach(async () => {
|
|
1251
|
-
await storage.clearTable({ tableName: TEST_TABLE as TABLE_NAMES });
|
|
1252
|
-
});
|
|
1253
|
-
|
|
1254
|
-
it('adds a new column to an existing table', async () => {
|
|
1255
|
-
await storage.alterTable({
|
|
1256
|
-
tableName: TEST_TABLE as TABLE_NAMES,
|
|
1257
|
-
schema: { ...BASE_SCHEMA, age: { type: 'integer', nullable: true } },
|
|
1258
|
-
ifNotExists: ['age'],
|
|
1259
|
-
});
|
|
1260
|
-
|
|
1261
|
-
await storage.insert({
|
|
1262
|
-
tableName: TEST_TABLE as TABLE_NAMES,
|
|
1263
|
-
record: { id: 1, name: 'Alice', age: 42, createdAt: new Date() },
|
|
1264
|
-
});
|
|
1265
|
-
|
|
1266
|
-
const row = await storage.load({
|
|
1267
|
-
tableName: TEST_TABLE as TABLE_NAMES,
|
|
1268
|
-
keys: { id: 1 },
|
|
1269
|
-
});
|
|
1270
|
-
expect(row?.age).toBe(42);
|
|
1271
|
-
});
|
|
1272
|
-
|
|
1273
|
-
it('is idempotent when adding an existing column', async () => {
|
|
1274
|
-
await storage.alterTable({
|
|
1275
|
-
tableName: TEST_TABLE as TABLE_NAMES,
|
|
1276
|
-
schema: { ...BASE_SCHEMA, foo: { type: 'text', nullable: true } },
|
|
1277
|
-
ifNotExists: ['foo'],
|
|
1278
|
-
});
|
|
1279
|
-
// Add the column again (should not throw)
|
|
1280
|
-
await expect(
|
|
1281
|
-
storage.alterTable({
|
|
1282
|
-
tableName: TEST_TABLE as TABLE_NAMES,
|
|
1283
|
-
schema: { ...BASE_SCHEMA, foo: { type: 'text', nullable: true } },
|
|
1284
|
-
ifNotExists: ['foo'],
|
|
1285
|
-
}),
|
|
1286
|
-
).resolves.not.toThrow();
|
|
1287
|
-
});
|
|
1288
|
-
|
|
1289
|
-
it('should add a default value to a column when using not null', async () => {
|
|
1290
|
-
await storage.insert({
|
|
1291
|
-
tableName: TEST_TABLE as TABLE_NAMES,
|
|
1292
|
-
record: { id: 1, name: 'Bob', createdAt: new Date() },
|
|
1293
|
-
});
|
|
1294
|
-
|
|
1295
|
-
await expect(
|
|
1296
|
-
storage.alterTable({
|
|
1297
|
-
tableName: TEST_TABLE as TABLE_NAMES,
|
|
1298
|
-
schema: { ...BASE_SCHEMA, text_column: { type: 'text', nullable: false } },
|
|
1299
|
-
ifNotExists: ['text_column'],
|
|
1300
|
-
}),
|
|
1301
|
-
).resolves.not.toThrow();
|
|
1302
|
-
|
|
1303
|
-
await expect(
|
|
1304
|
-
storage.alterTable({
|
|
1305
|
-
tableName: TEST_TABLE as TABLE_NAMES,
|
|
1306
|
-
schema: { ...BASE_SCHEMA, timestamp_column: { type: 'timestamp', nullable: false } },
|
|
1307
|
-
ifNotExists: ['timestamp_column'],
|
|
1308
|
-
}),
|
|
1309
|
-
).resolves.not.toThrow();
|
|
1310
|
-
|
|
1311
|
-
await expect(
|
|
1312
|
-
storage.alterTable({
|
|
1313
|
-
tableName: TEST_TABLE as TABLE_NAMES,
|
|
1314
|
-
schema: { ...BASE_SCHEMA, bigint_column: { type: 'bigint', nullable: false } },
|
|
1315
|
-
ifNotExists: ['bigint_column'],
|
|
1316
|
-
}),
|
|
1317
|
-
).resolves.not.toThrow();
|
|
1318
|
-
|
|
1319
|
-
await expect(
|
|
1320
|
-
storage.alterTable({
|
|
1321
|
-
tableName: TEST_TABLE as TABLE_NAMES,
|
|
1322
|
-
schema: { ...BASE_SCHEMA, jsonb_column: { type: 'jsonb', nullable: false } },
|
|
1323
|
-
ifNotExists: ['jsonb_column'],
|
|
1324
|
-
}),
|
|
1325
|
-
).resolves.not.toThrow();
|
|
1326
|
-
|
|
1327
|
-
await expect(
|
|
1328
|
-
storage.alterTable({
|
|
1329
|
-
tableName: TEST_TABLE as TABLE_NAMES,
|
|
1330
|
-
schema: { ...BASE_SCHEMA, uuid_column: { type: 'uuid', nullable: false } },
|
|
1331
|
-
ifNotExists: ['uuid_column'],
|
|
1332
|
-
}),
|
|
1333
|
-
).resolves.not.toThrow();
|
|
1334
|
-
});
|
|
1335
|
-
});
|
|
1336
|
-
});
|
|
10
|
+
createTestSuite(storage);
|