@mastra/pg 0.10.1 → 0.10.2-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +8 -8
- package/CHANGELOG.md +28 -0
- package/dist/_tsup-dts-rollup.d.cts +58 -5
- package/dist/_tsup-dts-rollup.d.ts +58 -5
- package/dist/index.cjs +319 -126
- package/dist/index.js +319 -126
- package/package.json +12 -11
- package/src/storage/index.test.ts +558 -120
- package/src/storage/index.ts +405 -160
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
import { randomUUID } from 'crypto';
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import {
|
|
3
|
+
createSampleEval,
|
|
4
|
+
createSampleTraceForDB,
|
|
5
|
+
createSampleThread,
|
|
6
|
+
createSampleMessageV1,
|
|
7
|
+
createSampleMessageV2,
|
|
8
|
+
createSampleWorkflowSnapshot,
|
|
9
|
+
resetRole,
|
|
10
|
+
} from '@internal/storage-test-utils';
|
|
11
|
+
import type { MastraMessageV1, StorageThreadType } from '@mastra/core/memory';
|
|
12
|
+
import type { StorageColumn, TABLE_NAMES } from '@mastra/core/storage';
|
|
13
|
+
import {
|
|
14
|
+
TABLE_WORKFLOW_SNAPSHOT,
|
|
15
|
+
TABLE_MESSAGES,
|
|
16
|
+
TABLE_THREADS,
|
|
17
|
+
TABLE_EVALS,
|
|
18
|
+
TABLE_TRACES,
|
|
19
|
+
} from '@mastra/core/storage';
|
|
5
20
|
import type { WorkflowRunState } from '@mastra/core/workflows';
|
|
6
21
|
import pgPromise from 'pg-promise';
|
|
7
22
|
import { describe, it, expect, beforeAll, beforeEach, afterAll, afterEach, vi } from 'vitest';
|
|
@@ -21,76 +36,6 @@ const connectionString = `postgresql://${TEST_CONFIG.user}:${TEST_CONFIG.passwor
|
|
|
21
36
|
|
|
22
37
|
vi.setConfig({ testTimeout: 60_000, hookTimeout: 60_000 });
|
|
23
38
|
|
|
24
|
-
// Sample test data factory functions
|
|
25
|
-
const createSampleThread = () => ({
|
|
26
|
-
id: `thread-${randomUUID()}`,
|
|
27
|
-
resourceId: `resource-${randomUUID()}`,
|
|
28
|
-
title: 'Test Thread',
|
|
29
|
-
createdAt: new Date(),
|
|
30
|
-
updatedAt: new Date(),
|
|
31
|
-
metadata: { key: 'value' },
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
let role: 'user' | 'assistant' = 'assistant';
|
|
35
|
-
const getRole = () => {
|
|
36
|
-
if (role === `user`) role = `assistant`;
|
|
37
|
-
else role = `user`;
|
|
38
|
-
return role;
|
|
39
|
-
};
|
|
40
|
-
const createSampleMessage = (threadId: string): MastraMessageV1 => ({
|
|
41
|
-
id: `msg-${randomUUID()}`,
|
|
42
|
-
resourceId: `resource-${randomUUID()}`,
|
|
43
|
-
role: getRole(),
|
|
44
|
-
type: 'text',
|
|
45
|
-
threadId,
|
|
46
|
-
content: [{ type: 'text', text: 'Hello' }],
|
|
47
|
-
createdAt: new Date(),
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
const createSampleWorkflowSnapshot = (status: WorkflowRunState['context'][string]['status'], createdAt?: Date) => {
|
|
51
|
-
const runId = `run-${randomUUID()}`;
|
|
52
|
-
const stepId = `step-${randomUUID()}`;
|
|
53
|
-
const timestamp = createdAt || new Date();
|
|
54
|
-
const snapshot = {
|
|
55
|
-
result: { success: true },
|
|
56
|
-
value: {},
|
|
57
|
-
context: {
|
|
58
|
-
[stepId]: {
|
|
59
|
-
status,
|
|
60
|
-
payload: {},
|
|
61
|
-
error: undefined,
|
|
62
|
-
startedAt: timestamp.getTime(),
|
|
63
|
-
endedAt: new Date(timestamp.getTime() + 15000).getTime(),
|
|
64
|
-
},
|
|
65
|
-
input: {},
|
|
66
|
-
},
|
|
67
|
-
serializedStepGraph: [],
|
|
68
|
-
activePaths: [],
|
|
69
|
-
suspendedPaths: {},
|
|
70
|
-
runId,
|
|
71
|
-
timestamp: timestamp.getTime(),
|
|
72
|
-
} as unknown as WorkflowRunState;
|
|
73
|
-
return { snapshot, runId, stepId };
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
const createSampleEval = (agentName: string, isTest = false) => {
|
|
77
|
-
const testInfo = isTest ? { testPath: 'test/path.ts', testName: 'Test Name' } : undefined;
|
|
78
|
-
|
|
79
|
-
return {
|
|
80
|
-
id: randomUUID(),
|
|
81
|
-
agentName,
|
|
82
|
-
input: 'Sample input',
|
|
83
|
-
output: 'Sample output',
|
|
84
|
-
result: { score: 0.8 } as MetricResult,
|
|
85
|
-
metricName: 'sample-metric',
|
|
86
|
-
instructions: 'Sample instructions',
|
|
87
|
-
testInfo,
|
|
88
|
-
globalRunId: `global-${randomUUID()}`,
|
|
89
|
-
runId: `run-${randomUUID()}`,
|
|
90
|
-
createdAt: new Date().toISOString(),
|
|
91
|
-
};
|
|
92
|
-
};
|
|
93
|
-
|
|
94
39
|
const checkWorkflowSnapshot = (snapshot: WorkflowRunState | string, stepId: string, status: string) => {
|
|
95
40
|
if (typeof snapshot === 'string') {
|
|
96
41
|
throw new Error('Expected WorkflowRunState, got string');
|
|
@@ -114,6 +59,7 @@ describe('PostgresStore', () => {
|
|
|
114
59
|
await store.clearTable({ tableName: TABLE_MESSAGES });
|
|
115
60
|
await store.clearTable({ tableName: TABLE_THREADS });
|
|
116
61
|
await store.clearTable({ tableName: TABLE_EVALS });
|
|
62
|
+
await store.clearTable({ tableName: TABLE_TRACES });
|
|
117
63
|
} catch (error) {
|
|
118
64
|
// Ignore errors during table clearing
|
|
119
65
|
console.warn('Error clearing tables:', error);
|
|
@@ -221,7 +167,7 @@ describe('PostgresStore', () => {
|
|
|
221
167
|
await store.saveThread({ thread });
|
|
222
168
|
|
|
223
169
|
// Add some messages
|
|
224
|
-
const messages = [
|
|
170
|
+
const messages = [createSampleMessageV1({ threadId: thread.id }), createSampleMessageV1({ threadId: thread.id })];
|
|
225
171
|
await store.saveMessages({ messages });
|
|
226
172
|
|
|
227
173
|
await store.deleteThread({ threadId: thread.id });
|
|
@@ -240,14 +186,14 @@ describe('PostgresStore', () => {
|
|
|
240
186
|
const thread = createSampleThread();
|
|
241
187
|
await store.saveThread({ thread });
|
|
242
188
|
|
|
243
|
-
const messages = [
|
|
189
|
+
const messages = [createSampleMessageV1({ threadId: thread.id }), createSampleMessageV1({ threadId: thread.id })];
|
|
244
190
|
|
|
245
191
|
// Save messages
|
|
246
192
|
const savedMessages = await store.saveMessages({ messages });
|
|
247
193
|
expect(savedMessages).toEqual(messages);
|
|
248
194
|
|
|
249
195
|
// Retrieve messages
|
|
250
|
-
const retrievedMessages = await store.getMessages({ threadId: thread.id });
|
|
196
|
+
const retrievedMessages = await store.getMessages({ threadId: thread.id, format: 'v1' });
|
|
251
197
|
expect(retrievedMessages).toHaveLength(2);
|
|
252
198
|
const checkMessages = messages.map(m => {
|
|
253
199
|
const { resourceId, ...rest } = m;
|
|
@@ -265,24 +211,18 @@ describe('PostgresStore', () => {
|
|
|
265
211
|
const thread = createSampleThread();
|
|
266
212
|
await store.saveThread({ thread });
|
|
267
213
|
|
|
268
|
-
const
|
|
269
|
-
{ ...createSampleMessage(thread.id), content: [{ type: 'text', text: 'First' }] },
|
|
270
|
-
{
|
|
271
|
-
...createSampleMessage(thread.id),
|
|
272
|
-
content: [{ type: 'text', text: 'Second' }],
|
|
273
|
-
},
|
|
274
|
-
{ ...createSampleMessage(thread.id), content: [{ type: 'text', text: 'Third' }] },
|
|
275
|
-
] satisfies MastraMessageV1[];
|
|
214
|
+
const messageContent = ['First', 'Second', 'Third'];
|
|
276
215
|
|
|
277
|
-
|
|
216
|
+
const messages = messageContent.map(content => createSampleMessageV2({ threadId: thread.id, content }));
|
|
217
|
+
|
|
218
|
+
await store.saveMessages({ messages, format: 'v2' });
|
|
278
219
|
|
|
279
|
-
const retrievedMessages = await store.getMessages
|
|
220
|
+
const retrievedMessages = await store.getMessages({ threadId: thread.id, format: 'v2' });
|
|
280
221
|
expect(retrievedMessages).toHaveLength(3);
|
|
281
222
|
|
|
282
223
|
// Verify order is maintained
|
|
283
224
|
retrievedMessages.forEach((msg, idx) => {
|
|
284
|
-
|
|
285
|
-
expect(msg.content[0].text).toBe(messages[idx].content[0].text);
|
|
225
|
+
expect((msg.content.parts[0] as any).text).toEqual(messageContent[idx]);
|
|
286
226
|
});
|
|
287
227
|
});
|
|
288
228
|
|
|
@@ -291,8 +231,8 @@ describe('PostgresStore', () => {
|
|
|
291
231
|
await store.saveThread({ thread });
|
|
292
232
|
|
|
293
233
|
const messages = [
|
|
294
|
-
|
|
295
|
-
{ ...
|
|
234
|
+
createSampleMessageV1({ threadId: thread.id }),
|
|
235
|
+
{ ...createSampleMessageV1({ threadId: thread.id }), id: null } as any, // This will cause an error
|
|
296
236
|
];
|
|
297
237
|
|
|
298
238
|
await expect(store.saveMessages({ messages })).rejects.toThrow();
|
|
@@ -752,78 +692,76 @@ describe('PostgresStore', () => {
|
|
|
752
692
|
it('should retrieve evals by agent name', async () => {
|
|
753
693
|
const agentName = `test-agent-${randomUUID()}`;
|
|
754
694
|
|
|
755
|
-
// Create sample evals
|
|
756
|
-
const liveEval = createSampleEval(agentName, false);
|
|
695
|
+
// Create sample evals using the imported helper
|
|
696
|
+
const liveEval = createSampleEval(agentName, false); // createSampleEval returns snake_case
|
|
757
697
|
const testEval = createSampleEval(agentName, true);
|
|
758
698
|
const otherAgentEval = createSampleEval(`other-agent-${randomUUID()}`, false);
|
|
759
699
|
|
|
760
|
-
// Insert evals
|
|
700
|
+
// Insert evals - ensure DB columns are snake_case
|
|
761
701
|
await store.insert({
|
|
762
702
|
tableName: TABLE_EVALS,
|
|
763
703
|
record: {
|
|
764
|
-
agent_name: liveEval.
|
|
704
|
+
agent_name: liveEval.agent_name, // Use snake_case
|
|
765
705
|
input: liveEval.input,
|
|
766
706
|
output: liveEval.output,
|
|
767
707
|
result: liveEval.result,
|
|
768
|
-
metric_name: liveEval.
|
|
708
|
+
metric_name: liveEval.metric_name, // Use snake_case
|
|
769
709
|
instructions: liveEval.instructions,
|
|
770
|
-
test_info:
|
|
771
|
-
global_run_id: liveEval.
|
|
772
|
-
run_id: liveEval.
|
|
773
|
-
created_at: liveEval.
|
|
774
|
-
createdAt: new Date(liveEval.createdAt),
|
|
710
|
+
test_info: liveEval.test_info, // test_info from helper can be undefined or object
|
|
711
|
+
global_run_id: liveEval.global_run_id, // Use snake_case
|
|
712
|
+
run_id: liveEval.run_id, // Use snake_case
|
|
713
|
+
created_at: new Date(liveEval.created_at as string), // created_at from helper is string or Date
|
|
775
714
|
},
|
|
776
715
|
});
|
|
777
716
|
|
|
778
717
|
await store.insert({
|
|
779
718
|
tableName: TABLE_EVALS,
|
|
780
719
|
record: {
|
|
781
|
-
agent_name: testEval.
|
|
720
|
+
agent_name: testEval.agent_name,
|
|
782
721
|
input: testEval.input,
|
|
783
722
|
output: testEval.output,
|
|
784
723
|
result: testEval.result,
|
|
785
|
-
metric_name: testEval.
|
|
724
|
+
metric_name: testEval.metric_name,
|
|
786
725
|
instructions: testEval.instructions,
|
|
787
|
-
test_info: JSON.stringify(testEval.
|
|
788
|
-
global_run_id: testEval.
|
|
789
|
-
run_id: testEval.
|
|
790
|
-
created_at: testEval.
|
|
791
|
-
createdAt: new Date(testEval.createdAt),
|
|
726
|
+
test_info: testEval.test_info ? JSON.stringify(testEval.test_info) : null,
|
|
727
|
+
global_run_id: testEval.global_run_id,
|
|
728
|
+
run_id: testEval.run_id,
|
|
729
|
+
created_at: new Date(testEval.created_at as string),
|
|
792
730
|
},
|
|
793
731
|
});
|
|
794
732
|
|
|
795
733
|
await store.insert({
|
|
796
734
|
tableName: TABLE_EVALS,
|
|
797
735
|
record: {
|
|
798
|
-
agent_name: otherAgentEval.
|
|
736
|
+
agent_name: otherAgentEval.agent_name,
|
|
799
737
|
input: otherAgentEval.input,
|
|
800
738
|
output: otherAgentEval.output,
|
|
801
739
|
result: otherAgentEval.result,
|
|
802
|
-
metric_name: otherAgentEval.
|
|
740
|
+
metric_name: otherAgentEval.metric_name,
|
|
803
741
|
instructions: otherAgentEval.instructions,
|
|
804
|
-
test_info: null
|
|
805
|
-
global_run_id: otherAgentEval.
|
|
806
|
-
run_id: otherAgentEval.
|
|
807
|
-
created_at: otherAgentEval.
|
|
808
|
-
createdAt: new Date(otherAgentEval.createdAt),
|
|
742
|
+
test_info: otherAgentEval.test_info, // Can be null/undefined directly
|
|
743
|
+
global_run_id: otherAgentEval.global_run_id,
|
|
744
|
+
run_id: otherAgentEval.run_id,
|
|
745
|
+
created_at: new Date(otherAgentEval.created_at as string),
|
|
809
746
|
},
|
|
810
747
|
});
|
|
811
748
|
|
|
812
749
|
// Test getting all evals for the agent
|
|
813
750
|
const allEvals = await store.getEvalsByAgentName(agentName);
|
|
814
751
|
expect(allEvals).toHaveLength(2);
|
|
815
|
-
|
|
752
|
+
// EvalRow type expects camelCase, but PostgresStore.transformEvalRow converts snake_case from DB to camelCase
|
|
753
|
+
expect(allEvals.map(e => e.runId)).toEqual(expect.arrayContaining([liveEval.run_id, testEval.run_id]));
|
|
816
754
|
|
|
817
755
|
// Test getting only live evals
|
|
818
756
|
const liveEvals = await store.getEvalsByAgentName(agentName, 'live');
|
|
819
757
|
expect(liveEvals).toHaveLength(1);
|
|
820
|
-
expect(liveEvals[0].runId).toBe(liveEval.
|
|
758
|
+
expect(liveEvals[0].runId).toBe(liveEval.run_id); // Comparing with snake_case run_id from original data
|
|
821
759
|
|
|
822
760
|
// Test getting only test evals
|
|
823
|
-
const
|
|
824
|
-
expect(
|
|
825
|
-
expect(
|
|
826
|
-
expect(
|
|
761
|
+
const testEvalsResult = await store.getEvalsByAgentName(agentName, 'test');
|
|
762
|
+
expect(testEvalsResult).toHaveLength(1);
|
|
763
|
+
expect(testEvalsResult[0].runId).toBe(testEval.run_id);
|
|
764
|
+
expect(testEvalsResult[0].testInfo).toEqual(testEval.test_info);
|
|
827
765
|
|
|
828
766
|
// Test getting evals for non-existent agent
|
|
829
767
|
const nonExistentEvals = await store.getEvalsByAgentName('non-existent-agent');
|
|
@@ -863,6 +801,96 @@ describe('PostgresStore', () => {
|
|
|
863
801
|
});
|
|
864
802
|
});
|
|
865
803
|
|
|
804
|
+
describe('alterTable', () => {
|
|
805
|
+
const TEST_TABLE = 'test_alter_table';
|
|
806
|
+
const BASE_SCHEMA = {
|
|
807
|
+
id: { type: 'integer', primaryKey: true, nullable: false },
|
|
808
|
+
name: { type: 'text', nullable: true },
|
|
809
|
+
} as Record<string, StorageColumn>;
|
|
810
|
+
|
|
811
|
+
beforeEach(async () => {
|
|
812
|
+
await store.createTable({ tableName: TEST_TABLE as TABLE_NAMES, schema: BASE_SCHEMA });
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
afterEach(async () => {
|
|
816
|
+
await store.clearTable({ tableName: TEST_TABLE as TABLE_NAMES });
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
it('adds a new column to an existing table', async () => {
|
|
820
|
+
await store.alterTable({
|
|
821
|
+
tableName: TEST_TABLE as TABLE_NAMES,
|
|
822
|
+
schema: { ...BASE_SCHEMA, age: { type: 'integer', nullable: true } },
|
|
823
|
+
ifNotExists: ['age'],
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
await store.insert({
|
|
827
|
+
tableName: TEST_TABLE as TABLE_NAMES,
|
|
828
|
+
record: { id: 1, name: 'Alice', age: 42 },
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
const row = await store.load<{ id: string; name: string; age?: number }>({
|
|
832
|
+
tableName: TEST_TABLE as TABLE_NAMES,
|
|
833
|
+
keys: { id: '1' },
|
|
834
|
+
});
|
|
835
|
+
expect(row?.age).toBe(42);
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
it('is idempotent when adding an existing column', async () => {
|
|
839
|
+
await store.alterTable({
|
|
840
|
+
tableName: TEST_TABLE as TABLE_NAMES,
|
|
841
|
+
schema: { ...BASE_SCHEMA, foo: { type: 'text', nullable: true } },
|
|
842
|
+
ifNotExists: ['foo'],
|
|
843
|
+
});
|
|
844
|
+
// Add the column again (should not throw)
|
|
845
|
+
await expect(
|
|
846
|
+
store.alterTable({
|
|
847
|
+
tableName: TEST_TABLE as TABLE_NAMES,
|
|
848
|
+
schema: { ...BASE_SCHEMA, foo: { type: 'text', nullable: true } },
|
|
849
|
+
ifNotExists: ['foo'],
|
|
850
|
+
}),
|
|
851
|
+
).resolves.not.toThrow();
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
it('should add a default value to a column when using not null', async () => {
|
|
855
|
+
await store.insert({
|
|
856
|
+
tableName: TEST_TABLE as TABLE_NAMES,
|
|
857
|
+
record: { id: 1, name: 'Bob' },
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
await expect(
|
|
861
|
+
store.alterTable({
|
|
862
|
+
tableName: TEST_TABLE as TABLE_NAMES,
|
|
863
|
+
schema: { ...BASE_SCHEMA, text_column: { type: 'text', nullable: false } },
|
|
864
|
+
ifNotExists: ['text_column'],
|
|
865
|
+
}),
|
|
866
|
+
).resolves.not.toThrow();
|
|
867
|
+
|
|
868
|
+
await expect(
|
|
869
|
+
store.alterTable({
|
|
870
|
+
tableName: TEST_TABLE as TABLE_NAMES,
|
|
871
|
+
schema: { ...BASE_SCHEMA, timestamp_column: { type: 'timestamp', nullable: false } },
|
|
872
|
+
ifNotExists: ['timestamp_column'],
|
|
873
|
+
}),
|
|
874
|
+
).resolves.not.toThrow();
|
|
875
|
+
|
|
876
|
+
await expect(
|
|
877
|
+
store.alterTable({
|
|
878
|
+
tableName: TEST_TABLE as TABLE_NAMES,
|
|
879
|
+
schema: { ...BASE_SCHEMA, bigint_column: { type: 'bigint', nullable: false } },
|
|
880
|
+
ifNotExists: ['bigint_column'],
|
|
881
|
+
}),
|
|
882
|
+
).resolves.not.toThrow();
|
|
883
|
+
|
|
884
|
+
await expect(
|
|
885
|
+
store.alterTable({
|
|
886
|
+
tableName: TEST_TABLE as TABLE_NAMES,
|
|
887
|
+
schema: { ...BASE_SCHEMA, jsonb_column: { type: 'jsonb', nullable: false } },
|
|
888
|
+
ifNotExists: ['jsonb_column'],
|
|
889
|
+
}),
|
|
890
|
+
).resolves.not.toThrow();
|
|
891
|
+
});
|
|
892
|
+
});
|
|
893
|
+
|
|
866
894
|
describe('Schema Support', () => {
|
|
867
895
|
const customSchema = 'mastra_test';
|
|
868
896
|
let customSchemaStore: PostgresStore;
|
|
@@ -931,6 +959,416 @@ describe('PostgresStore', () => {
|
|
|
931
959
|
});
|
|
932
960
|
});
|
|
933
961
|
|
|
962
|
+
describe('Pagination Features', () => {
|
|
963
|
+
beforeEach(async () => {
|
|
964
|
+
await store.clearTable({ tableName: TABLE_EVALS });
|
|
965
|
+
await store.clearTable({ tableName: TABLE_TRACES });
|
|
966
|
+
await store.clearTable({ tableName: TABLE_MESSAGES });
|
|
967
|
+
await store.clearTable({ tableName: TABLE_THREADS });
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
describe('getEvals with pagination', () => {
|
|
971
|
+
it('should return paginated evals with total count (page/perPage)', async () => {
|
|
972
|
+
const agentName = 'pagination-agent-evals';
|
|
973
|
+
const evalPromises = Array.from({ length: 25 }, (_, i) => {
|
|
974
|
+
const evalData = createSampleEval(agentName, i % 2 === 0);
|
|
975
|
+
return store.insert({
|
|
976
|
+
tableName: TABLE_EVALS,
|
|
977
|
+
record: {
|
|
978
|
+
run_id: evalData.run_id,
|
|
979
|
+
agent_name: evalData.agent_name,
|
|
980
|
+
input: evalData.input,
|
|
981
|
+
output: evalData.output,
|
|
982
|
+
result: evalData.result,
|
|
983
|
+
metric_name: evalData.metric_name,
|
|
984
|
+
instructions: evalData.instructions,
|
|
985
|
+
test_info: evalData.test_info,
|
|
986
|
+
global_run_id: evalData.global_run_id,
|
|
987
|
+
created_at: new Date(evalData.created_at as string),
|
|
988
|
+
},
|
|
989
|
+
});
|
|
990
|
+
});
|
|
991
|
+
await Promise.all(evalPromises);
|
|
992
|
+
|
|
993
|
+
const page1 = await store.getEvals({ agentName, page: 0, perPage: 10 });
|
|
994
|
+
expect(page1.evals).toHaveLength(10);
|
|
995
|
+
expect(page1.total).toBe(25);
|
|
996
|
+
expect(page1.page).toBe(0);
|
|
997
|
+
expect(page1.perPage).toBe(10);
|
|
998
|
+
expect(page1.hasMore).toBe(true);
|
|
999
|
+
|
|
1000
|
+
const page3 = await store.getEvals({ agentName, page: 2, perPage: 10 });
|
|
1001
|
+
expect(page3.evals).toHaveLength(5);
|
|
1002
|
+
expect(page3.total).toBe(25);
|
|
1003
|
+
expect(page3.page).toBe(2);
|
|
1004
|
+
expect(page3.hasMore).toBe(false);
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
it('should support limit/offset pagination for getEvals', async () => {
|
|
1008
|
+
const agentName = 'pagination-agent-lo-evals';
|
|
1009
|
+
const evalPromises = Array.from({ length: 15 }, () => {
|
|
1010
|
+
const evalData = createSampleEval(agentName);
|
|
1011
|
+
return store.insert({
|
|
1012
|
+
tableName: TABLE_EVALS,
|
|
1013
|
+
record: {
|
|
1014
|
+
run_id: evalData.run_id,
|
|
1015
|
+
agent_name: evalData.agent_name,
|
|
1016
|
+
input: evalData.input,
|
|
1017
|
+
output: evalData.output,
|
|
1018
|
+
result: evalData.result,
|
|
1019
|
+
metric_name: evalData.metric_name,
|
|
1020
|
+
instructions: evalData.instructions,
|
|
1021
|
+
test_info: evalData.test_info,
|
|
1022
|
+
global_run_id: evalData.global_run_id,
|
|
1023
|
+
created_at: new Date(evalData.created_at as string),
|
|
1024
|
+
},
|
|
1025
|
+
});
|
|
1026
|
+
});
|
|
1027
|
+
await Promise.all(evalPromises);
|
|
1028
|
+
|
|
1029
|
+
const result = await store.getEvals({ agentName, perPage: 5, page: 2 });
|
|
1030
|
+
expect(result.evals).toHaveLength(5);
|
|
1031
|
+
expect(result.total).toBe(15);
|
|
1032
|
+
expect(result.page).toBe(2);
|
|
1033
|
+
expect(result.perPage).toBe(5);
|
|
1034
|
+
expect(result.hasMore).toBe(false);
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
it('should filter by type with pagination for getEvals', async () => {
|
|
1038
|
+
const agentName = 'pagination-agent-type-evals';
|
|
1039
|
+
const testEvalPromises = Array.from({ length: 10 }, () => {
|
|
1040
|
+
const evalData = createSampleEval(agentName, true);
|
|
1041
|
+
return store.insert({
|
|
1042
|
+
tableName: TABLE_EVALS,
|
|
1043
|
+
record: {
|
|
1044
|
+
run_id: evalData.run_id,
|
|
1045
|
+
agent_name: evalData.agent_name,
|
|
1046
|
+
input: evalData.input,
|
|
1047
|
+
output: evalData.output,
|
|
1048
|
+
result: evalData.result,
|
|
1049
|
+
metric_name: evalData.metric_name,
|
|
1050
|
+
instructions: evalData.instructions,
|
|
1051
|
+
test_info: evalData.test_info,
|
|
1052
|
+
global_run_id: evalData.global_run_id,
|
|
1053
|
+
created_at: new Date(evalData.created_at as string),
|
|
1054
|
+
},
|
|
1055
|
+
});
|
|
1056
|
+
});
|
|
1057
|
+
const liveEvalPromises = Array.from({ length: 8 }, () => {
|
|
1058
|
+
const evalData = createSampleEval(agentName, false);
|
|
1059
|
+
return store.insert({
|
|
1060
|
+
tableName: TABLE_EVALS,
|
|
1061
|
+
record: {
|
|
1062
|
+
run_id: evalData.run_id,
|
|
1063
|
+
agent_name: evalData.agent_name,
|
|
1064
|
+
input: evalData.input,
|
|
1065
|
+
output: evalData.output,
|
|
1066
|
+
result: evalData.result,
|
|
1067
|
+
metric_name: evalData.metric_name,
|
|
1068
|
+
instructions: evalData.instructions,
|
|
1069
|
+
test_info: evalData.test_info,
|
|
1070
|
+
global_run_id: evalData.global_run_id,
|
|
1071
|
+
created_at: new Date(evalData.created_at as string),
|
|
1072
|
+
},
|
|
1073
|
+
});
|
|
1074
|
+
});
|
|
1075
|
+
await Promise.all([...testEvalPromises, ...liveEvalPromises]);
|
|
1076
|
+
|
|
1077
|
+
const testResults = await store.getEvals({ agentName, type: 'test', page: 0, perPage: 5 });
|
|
1078
|
+
expect(testResults.evals).toHaveLength(5);
|
|
1079
|
+
expect(testResults.total).toBe(10);
|
|
1080
|
+
|
|
1081
|
+
const liveResults = await store.getEvals({ agentName, type: 'live', page: 1, perPage: 3 });
|
|
1082
|
+
expect(liveResults.evals).toHaveLength(3);
|
|
1083
|
+
expect(liveResults.total).toBe(8);
|
|
1084
|
+
expect(liveResults.hasMore).toBe(true);
|
|
1085
|
+
});
|
|
1086
|
+
|
|
1087
|
+
it('should filter by date with pagination for getEvals', async () => {
|
|
1088
|
+
const agentName = 'pagination-agent-date-evals';
|
|
1089
|
+
const now = new Date();
|
|
1090
|
+
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
|
1091
|
+
const dayBeforeYesterday = new Date(now.getTime() - 48 * 60 * 60 * 1000);
|
|
1092
|
+
|
|
1093
|
+
const createEvalAtDate = (date: Date) => {
|
|
1094
|
+
const evalData = createSampleEval(agentName, false, date); // Pass date to helper
|
|
1095
|
+
return store.insert({
|
|
1096
|
+
tableName: TABLE_EVALS,
|
|
1097
|
+
record: {
|
|
1098
|
+
run_id: evalData.run_id, // Use snake_case from helper
|
|
1099
|
+
agent_name: evalData.agent_name,
|
|
1100
|
+
input: evalData.input,
|
|
1101
|
+
output: evalData.output,
|
|
1102
|
+
result: evalData.result,
|
|
1103
|
+
metric_name: evalData.metric_name,
|
|
1104
|
+
instructions: evalData.instructions,
|
|
1105
|
+
test_info: evalData.test_info,
|
|
1106
|
+
global_run_id: evalData.global_run_id,
|
|
1107
|
+
created_at: evalData.created_at, // Use created_at from helper (already Date or ISO string)
|
|
1108
|
+
},
|
|
1109
|
+
});
|
|
1110
|
+
};
|
|
1111
|
+
|
|
1112
|
+
await Promise.all([
|
|
1113
|
+
createEvalAtDate(dayBeforeYesterday),
|
|
1114
|
+
createEvalAtDate(dayBeforeYesterday),
|
|
1115
|
+
createEvalAtDate(yesterday),
|
|
1116
|
+
createEvalAtDate(yesterday),
|
|
1117
|
+
createEvalAtDate(yesterday),
|
|
1118
|
+
createEvalAtDate(now),
|
|
1119
|
+
createEvalAtDate(now),
|
|
1120
|
+
createEvalAtDate(now),
|
|
1121
|
+
createEvalAtDate(now),
|
|
1122
|
+
]);
|
|
1123
|
+
|
|
1124
|
+
const fromYesterday = await store.getEvals({ agentName, dateRange: { start: yesterday }, page: 0, perPage: 3 });
|
|
1125
|
+
expect(fromYesterday.total).toBe(7); // 3 yesterday + 4 now
|
|
1126
|
+
expect(fromYesterday.evals).toHaveLength(3);
|
|
1127
|
+
// Evals are sorted DESC, so first 3 are from 'now'
|
|
1128
|
+
fromYesterday.evals.forEach(e =>
|
|
1129
|
+
expect(new Date(e.createdAt).getTime()).toBeGreaterThanOrEqual(yesterday.getTime()),
|
|
1130
|
+
);
|
|
1131
|
+
|
|
1132
|
+
const onlyDayBefore = await store.getEvals({
|
|
1133
|
+
agentName,
|
|
1134
|
+
dateRange: {
|
|
1135
|
+
end: new Date(yesterday.getTime() - 1),
|
|
1136
|
+
},
|
|
1137
|
+
page: 0,
|
|
1138
|
+
perPage: 5,
|
|
1139
|
+
});
|
|
1140
|
+
expect(onlyDayBefore.total).toBe(2);
|
|
1141
|
+
expect(onlyDayBefore.evals).toHaveLength(2);
|
|
1142
|
+
});
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
describe('getTraces with pagination', () => {
|
|
1146
|
+
it('should return paginated traces with total count', async () => {
|
|
1147
|
+
const tracePromises = Array.from({ length: 18 }, (_, i) =>
|
|
1148
|
+
store.insert({ tableName: TABLE_TRACES, record: createSampleTraceForDB(`test-trace-${i}`, 'pg-test-scope') }),
|
|
1149
|
+
);
|
|
1150
|
+
await Promise.all(tracePromises);
|
|
1151
|
+
|
|
1152
|
+
const page1 = await store.getTracesPaginated({
|
|
1153
|
+
scope: 'pg-test-scope',
|
|
1154
|
+
page: 0,
|
|
1155
|
+
perPage: 8,
|
|
1156
|
+
});
|
|
1157
|
+
expect(page1.traces).toHaveLength(8);
|
|
1158
|
+
expect(page1.total).toBe(18);
|
|
1159
|
+
expect(page1.page).toBe(0);
|
|
1160
|
+
expect(page1.perPage).toBe(8);
|
|
1161
|
+
expect(page1.hasMore).toBe(true);
|
|
1162
|
+
|
|
1163
|
+
const page3 = await store.getTracesPaginated({
|
|
1164
|
+
scope: 'pg-test-scope',
|
|
1165
|
+
page: 2,
|
|
1166
|
+
perPage: 8,
|
|
1167
|
+
});
|
|
1168
|
+
expect(page3.traces).toHaveLength(2);
|
|
1169
|
+
expect(page3.total).toBe(18);
|
|
1170
|
+
expect(page3.hasMore).toBe(false);
|
|
1171
|
+
});
|
|
1172
|
+
|
|
1173
|
+
it('should filter by attributes with pagination for getTraces', async () => {
|
|
1174
|
+
const tracesWithAttr = Array.from({ length: 8 }, (_, i) =>
|
|
1175
|
+
store.insert({
|
|
1176
|
+
tableName: TABLE_TRACES,
|
|
1177
|
+
record: createSampleTraceForDB(`trace-${i}`, 'pg-attr-scope', { environment: 'prod' }),
|
|
1178
|
+
}),
|
|
1179
|
+
);
|
|
1180
|
+
const tracesWithoutAttr = Array.from({ length: 5 }, (_, i) =>
|
|
1181
|
+
store.insert({
|
|
1182
|
+
tableName: TABLE_TRACES,
|
|
1183
|
+
record: createSampleTraceForDB(`trace-other-${i}`, 'pg-attr-scope', { environment: 'dev' }),
|
|
1184
|
+
}),
|
|
1185
|
+
);
|
|
1186
|
+
await Promise.all([...tracesWithAttr, ...tracesWithoutAttr]);
|
|
1187
|
+
|
|
1188
|
+
const prodTraces = await store.getTracesPaginated({
|
|
1189
|
+
scope: 'pg-attr-scope',
|
|
1190
|
+
attributes: { environment: 'prod' },
|
|
1191
|
+
page: 0,
|
|
1192
|
+
perPage: 5,
|
|
1193
|
+
});
|
|
1194
|
+
expect(prodTraces.traces).toHaveLength(5);
|
|
1195
|
+
expect(prodTraces.total).toBe(8);
|
|
1196
|
+
expect(prodTraces.hasMore).toBe(true);
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
it('should filter by date with pagination for getTraces', async () => {
|
|
1200
|
+
const scope = 'pg-date-traces';
|
|
1201
|
+
const now = new Date();
|
|
1202
|
+
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
|
1203
|
+
const dayBeforeYesterday = new Date(now.getTime() - 48 * 60 * 60 * 1000);
|
|
1204
|
+
|
|
1205
|
+
await Promise.all([
|
|
1206
|
+
store.insert({
|
|
1207
|
+
tableName: TABLE_TRACES,
|
|
1208
|
+
record: createSampleTraceForDB('t1', scope, undefined, dayBeforeYesterday),
|
|
1209
|
+
}),
|
|
1210
|
+
store.insert({ tableName: TABLE_TRACES, record: createSampleTraceForDB('t2', scope, undefined, yesterday) }),
|
|
1211
|
+
store.insert({ tableName: TABLE_TRACES, record: createSampleTraceForDB('t3', scope, undefined, yesterday) }),
|
|
1212
|
+
store.insert({ tableName: TABLE_TRACES, record: createSampleTraceForDB('t4', scope, undefined, now) }),
|
|
1213
|
+
store.insert({ tableName: TABLE_TRACES, record: createSampleTraceForDB('t5', scope, undefined, now) }),
|
|
1214
|
+
]);
|
|
1215
|
+
|
|
1216
|
+
const fromYesterday = await store.getTracesPaginated({
|
|
1217
|
+
scope,
|
|
1218
|
+
dateRange: {
|
|
1219
|
+
start: yesterday,
|
|
1220
|
+
},
|
|
1221
|
+
page: 0,
|
|
1222
|
+
perPage: 2,
|
|
1223
|
+
});
|
|
1224
|
+
expect(fromYesterday.total).toBe(4); // 2 yesterday + 2 now
|
|
1225
|
+
expect(fromYesterday.traces).toHaveLength(2);
|
|
1226
|
+
fromYesterday.traces.forEach(t =>
|
|
1227
|
+
expect(new Date(t.createdAt).getTime()).toBeGreaterThanOrEqual(yesterday.getTime()),
|
|
1228
|
+
);
|
|
1229
|
+
|
|
1230
|
+
const onlyNow = await store.getTracesPaginated({
|
|
1231
|
+
scope,
|
|
1232
|
+
dateRange: {
|
|
1233
|
+
start: now,
|
|
1234
|
+
end: now,
|
|
1235
|
+
},
|
|
1236
|
+
page: 0,
|
|
1237
|
+
perPage: 5,
|
|
1238
|
+
});
|
|
1239
|
+
expect(onlyNow.total).toBe(2);
|
|
1240
|
+
expect(onlyNow.traces).toHaveLength(2);
|
|
1241
|
+
});
|
|
1242
|
+
});
|
|
1243
|
+
|
|
1244
|
+
describe('getMessages with pagination', () => {
|
|
1245
|
+
it('should return paginated messages with total count', async () => {
|
|
1246
|
+
const thread = createSampleThread();
|
|
1247
|
+
await store.saveThread({ thread });
|
|
1248
|
+
// Reset role to 'assistant' before creating messages
|
|
1249
|
+
resetRole();
|
|
1250
|
+
// Create messages sequentially to ensure unique timestamps
|
|
1251
|
+
for (let i = 0; i < 15; i++) {
|
|
1252
|
+
const message = createSampleMessageV1({ threadId: thread.id, content: `Message ${i + 1}` });
|
|
1253
|
+
await store.saveMessages({
|
|
1254
|
+
messages: [message],
|
|
1255
|
+
});
|
|
1256
|
+
await new Promise(r => setTimeout(r, 5));
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
const page1 = await store.getMessagesPaginated({
|
|
1260
|
+
threadId: thread.id,
|
|
1261
|
+
selectBy: { pagination: { page: 0, perPage: 5 } },
|
|
1262
|
+
format: 'v2',
|
|
1263
|
+
});
|
|
1264
|
+
console.log(page1);
|
|
1265
|
+
expect(page1.messages).toHaveLength(5);
|
|
1266
|
+
expect(page1.total).toBe(15);
|
|
1267
|
+
expect(page1.page).toBe(0);
|
|
1268
|
+
expect(page1.perPage).toBe(5);
|
|
1269
|
+
expect(page1.hasMore).toBe(true);
|
|
1270
|
+
|
|
1271
|
+
const page3 = await store.getMessagesPaginated({
|
|
1272
|
+
threadId: thread.id,
|
|
1273
|
+
selectBy: { pagination: { page: 2, perPage: 5 } },
|
|
1274
|
+
format: 'v2',
|
|
1275
|
+
});
|
|
1276
|
+
expect(page3.messages).toHaveLength(5);
|
|
1277
|
+
expect(page3.total).toBe(15);
|
|
1278
|
+
expect(page3.hasMore).toBe(false);
|
|
1279
|
+
});
|
|
1280
|
+
|
|
1281
|
+
it('should filter by date with pagination for getMessages', async () => {
|
|
1282
|
+
const threadData = createSampleThread();
|
|
1283
|
+
const thread = await store.saveThread({ thread: threadData as StorageThreadType });
|
|
1284
|
+
const now = new Date();
|
|
1285
|
+
const yesterday = new Date(
|
|
1286
|
+
now.getFullYear(),
|
|
1287
|
+
now.getMonth(),
|
|
1288
|
+
now.getDate() - 1,
|
|
1289
|
+
now.getHours(),
|
|
1290
|
+
now.getMinutes(),
|
|
1291
|
+
now.getSeconds(),
|
|
1292
|
+
);
|
|
1293
|
+
const dayBeforeYesterday = new Date(
|
|
1294
|
+
now.getFullYear(),
|
|
1295
|
+
now.getMonth(),
|
|
1296
|
+
now.getDate() - 2,
|
|
1297
|
+
now.getHours(),
|
|
1298
|
+
now.getMinutes(),
|
|
1299
|
+
now.getSeconds(),
|
|
1300
|
+
);
|
|
1301
|
+
|
|
1302
|
+
// Ensure timestamps are distinct for reliable sorting by creating them with a slight delay for testing clarity
|
|
1303
|
+
const messagesToSave: MastraMessageV1[] = [];
|
|
1304
|
+
messagesToSave.push(createSampleMessageV1({ threadId: thread.id, createdAt: dayBeforeYesterday }));
|
|
1305
|
+
await new Promise(r => setTimeout(r, 5));
|
|
1306
|
+
messagesToSave.push(createSampleMessageV1({ threadId: thread.id, createdAt: dayBeforeYesterday }));
|
|
1307
|
+
await new Promise(r => setTimeout(r, 5));
|
|
1308
|
+
messagesToSave.push(createSampleMessageV1({ threadId: thread.id, createdAt: yesterday }));
|
|
1309
|
+
await new Promise(r => setTimeout(r, 5));
|
|
1310
|
+
messagesToSave.push(createSampleMessageV1({ threadId: thread.id, createdAt: yesterday }));
|
|
1311
|
+
await new Promise(r => setTimeout(r, 5));
|
|
1312
|
+
messagesToSave.push(createSampleMessageV1({ threadId: thread.id, createdAt: now }));
|
|
1313
|
+
await new Promise(r => setTimeout(r, 5));
|
|
1314
|
+
messagesToSave.push(createSampleMessageV1({ threadId: thread.id, createdAt: now }));
|
|
1315
|
+
|
|
1316
|
+
await store.saveMessages({ messages: messagesToSave, format: 'v1' });
|
|
1317
|
+
// Total 6 messages: 2 now, 2 yesterday, 2 dayBeforeYesterday (oldest to newest)
|
|
1318
|
+
|
|
1319
|
+
const fromYesterday = await store.getMessagesPaginated({
|
|
1320
|
+
threadId: thread.id,
|
|
1321
|
+
selectBy: { pagination: { page: 0, perPage: 3, dateRange: { start: yesterday } } },
|
|
1322
|
+
format: 'v2',
|
|
1323
|
+
});
|
|
1324
|
+
expect(fromYesterday.total).toBe(4);
|
|
1325
|
+
expect(fromYesterday.messages).toHaveLength(3);
|
|
1326
|
+
const firstMessageTime = new Date((fromYesterday.messages[0] as MastraMessageV1).createdAt).getTime();
|
|
1327
|
+
expect(firstMessageTime).toBeGreaterThanOrEqual(new Date(yesterday.toISOString()).getTime());
|
|
1328
|
+
if (fromYesterday.messages.length > 0) {
|
|
1329
|
+
expect(new Date((fromYesterday.messages[0] as MastraMessageV1).createdAt).toISOString().slice(0, 10)).toEqual(
|
|
1330
|
+
yesterday.toISOString().slice(0, 10),
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1333
|
+
});
|
|
1334
|
+
});
|
|
1335
|
+
|
|
1336
|
+
describe('getThreadsByResourceId with pagination', () => {
|
|
1337
|
+
it('should return paginated threads with total count', async () => {
|
|
1338
|
+
const resourceId = `pg-paginated-resource-${randomUUID()}`;
|
|
1339
|
+
const threadPromises = Array.from({ length: 17 }, () =>
|
|
1340
|
+
store.saveThread({ thread: { ...createSampleThread(), resourceId } }),
|
|
1341
|
+
);
|
|
1342
|
+
await Promise.all(threadPromises);
|
|
1343
|
+
|
|
1344
|
+
const page1 = await store.getThreadsByResourceIdPaginated({ resourceId, page: 0, perPage: 7 });
|
|
1345
|
+
expect(page1.threads).toHaveLength(7);
|
|
1346
|
+
expect(page1.total).toBe(17);
|
|
1347
|
+
expect(page1.page).toBe(0);
|
|
1348
|
+
expect(page1.perPage).toBe(7);
|
|
1349
|
+
expect(page1.hasMore).toBe(true);
|
|
1350
|
+
|
|
1351
|
+
const page3 = await store.getThreadsByResourceIdPaginated({ resourceId, page: 2, perPage: 7 });
|
|
1352
|
+
expect(page3.threads).toHaveLength(3); // 17 total, 7 per page, 3rd page has 17 - 2*7 = 3
|
|
1353
|
+
expect(page3.total).toBe(17);
|
|
1354
|
+
expect(page3.hasMore).toBe(false);
|
|
1355
|
+
});
|
|
1356
|
+
|
|
1357
|
+
it('should return paginated results when no pagination params for getThreadsByResourceId', async () => {
|
|
1358
|
+
const resourceId = `pg-non-paginated-resource-${randomUUID()}`;
|
|
1359
|
+
await store.saveThread({ thread: { ...createSampleThread(), resourceId } });
|
|
1360
|
+
|
|
1361
|
+
const results = await store.getThreadsByResourceIdPaginated({ resourceId });
|
|
1362
|
+
expect(Array.isArray(results.threads)).toBe(true);
|
|
1363
|
+
expect(results.threads.length).toBe(1);
|
|
1364
|
+
expect(results.total).toBe(1);
|
|
1365
|
+
expect(results.page).toBe(0);
|
|
1366
|
+
expect(results.perPage).toBe(100);
|
|
1367
|
+
expect(results.hasMore).toBe(false);
|
|
1368
|
+
});
|
|
1369
|
+
});
|
|
1370
|
+
});
|
|
1371
|
+
|
|
934
1372
|
describe('Permission Handling', () => {
|
|
935
1373
|
const schemaRestrictedUser = 'mastra_schema_restricted_storage';
|
|
936
1374
|
const restrictedPassword = 'test123';
|