@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.
@@ -1,7 +1,22 @@
1
1
  import { randomUUID } from 'crypto';
2
- import type { MetricResult } from '@mastra/core/eval';
3
- import type { MastraMessageV1 } from '@mastra/core/memory';
4
- import { TABLE_WORKFLOW_SNAPSHOT, TABLE_MESSAGES, TABLE_THREADS, TABLE_EVALS } from '@mastra/core/storage';
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 = [createSampleMessage(thread.id), createSampleMessage(thread.id)];
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 = [createSampleMessage(thread.id), createSampleMessage(thread.id)];
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 messages = [
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
- await store.saveMessages({ messages });
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<MastraMessageV1>({ threadId: thread.id });
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
- // @ts-expect-error
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
- createSampleMessage(thread.id),
295
- { ...createSampleMessage(thread.id), id: null } as any, // This will cause an error
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.agentName,
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.metricName,
708
+ metric_name: liveEval.metric_name, // Use snake_case
769
709
  instructions: liveEval.instructions,
770
- test_info: null,
771
- global_run_id: liveEval.globalRunId,
772
- run_id: liveEval.runId,
773
- created_at: liveEval.createdAt,
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.agentName,
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.metricName,
724
+ metric_name: testEval.metric_name,
786
725
  instructions: testEval.instructions,
787
- test_info: JSON.stringify(testEval.testInfo),
788
- global_run_id: testEval.globalRunId,
789
- run_id: testEval.runId,
790
- created_at: testEval.createdAt,
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.agentName,
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.metricName,
740
+ metric_name: otherAgentEval.metric_name,
803
741
  instructions: otherAgentEval.instructions,
804
- test_info: null,
805
- global_run_id: otherAgentEval.globalRunId,
806
- run_id: otherAgentEval.runId,
807
- created_at: otherAgentEval.createdAt,
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
- expect(allEvals.map(e => e.runId)).toEqual(expect.arrayContaining([liveEval.runId, testEval.runId]));
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.runId);
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 testEvals = await store.getEvalsByAgentName(agentName, 'test');
824
- expect(testEvals).toHaveLength(1);
825
- expect(testEvals[0].runId).toBe(testEval.runId);
826
- expect(testEvals[0].testInfo).toEqual(testEval.testInfo);
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';