@mastra/pg 0.10.1 → 0.10.2-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.
@@ -1,7 +1,13 @@
1
1
  import { randomUUID } from 'crypto';
2
- import type { MetricResult } from '@mastra/core/eval';
2
+ import { createSampleEval, createSampleTraceForDB } from '@internal/storage-test-utils';
3
3
  import type { MastraMessageV1 } from '@mastra/core/memory';
4
- import { TABLE_WORKFLOW_SNAPSHOT, TABLE_MESSAGES, TABLE_THREADS, TABLE_EVALS } from '@mastra/core/storage';
4
+ import {
5
+ TABLE_WORKFLOW_SNAPSHOT,
6
+ TABLE_MESSAGES,
7
+ TABLE_THREADS,
8
+ TABLE_EVALS,
9
+ TABLE_TRACES,
10
+ } from '@mastra/core/storage';
5
11
  import type { WorkflowRunState } from '@mastra/core/workflows';
6
12
  import pgPromise from 'pg-promise';
7
13
  import { describe, it, expect, beforeAll, beforeEach, afterAll, afterEach, vi } from 'vitest';
@@ -73,24 +79,6 @@ const createSampleWorkflowSnapshot = (status: WorkflowRunState['context'][string
73
79
  return { snapshot, runId, stepId };
74
80
  };
75
81
 
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
82
  const checkWorkflowSnapshot = (snapshot: WorkflowRunState | string, stepId: string, status: string) => {
95
83
  if (typeof snapshot === 'string') {
96
84
  throw new Error('Expected WorkflowRunState, got string');
@@ -114,6 +102,7 @@ describe('PostgresStore', () => {
114
102
  await store.clearTable({ tableName: TABLE_MESSAGES });
115
103
  await store.clearTable({ tableName: TABLE_THREADS });
116
104
  await store.clearTable({ tableName: TABLE_EVALS });
105
+ await store.clearTable({ tableName: TABLE_TRACES });
117
106
  } catch (error) {
118
107
  // Ignore errors during table clearing
119
108
  console.warn('Error clearing tables:', error);
@@ -247,7 +236,7 @@ describe('PostgresStore', () => {
247
236
  expect(savedMessages).toEqual(messages);
248
237
 
249
238
  // Retrieve messages
250
- const retrievedMessages = await store.getMessages({ threadId: thread.id });
239
+ const retrievedMessages = await store.getMessages({ threadId: thread.id, format: 'v1' });
251
240
  expect(retrievedMessages).toHaveLength(2);
252
241
  const checkMessages = messages.map(m => {
253
242
  const { resourceId, ...rest } = m;
@@ -276,7 +265,7 @@ describe('PostgresStore', () => {
276
265
 
277
266
  await store.saveMessages({ messages });
278
267
 
279
- const retrievedMessages = await store.getMessages<MastraMessageV1>({ threadId: thread.id });
268
+ const retrievedMessages = await store.getMessages({ threadId: thread.id, format: 'v1' });
280
269
  expect(retrievedMessages).toHaveLength(3);
281
270
 
282
271
  // Verify order is maintained
@@ -301,6 +290,43 @@ describe('PostgresStore', () => {
301
290
  const savedMessages = await store.getMessages({ threadId: thread.id });
302
291
  expect(savedMessages).toHaveLength(0);
303
292
  });
293
+
294
+ it('should filter by date with pagination for getMessages', async () => {
295
+ const thread = createSampleThread();
296
+ await store.saveThread({ thread });
297
+ const now = new Date();
298
+ const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
299
+ const dayBeforeYesterday = new Date(now.getTime() - 48 * 60 * 60 * 1000);
300
+
301
+ const createMsgAtDate = (date: Date) => {
302
+ return store.saveMessages({ messages: [{ ...createSampleMessage(thread.id), createdAt: date }] });
303
+ };
304
+ await Promise.all([
305
+ createMsgAtDate(dayBeforeYesterday),
306
+ createMsgAtDate(dayBeforeYesterday),
307
+ createMsgAtDate(yesterday),
308
+ createMsgAtDate(yesterday),
309
+ createMsgAtDate(yesterday),
310
+ createMsgAtDate(now),
311
+ createMsgAtDate(now),
312
+ ]);
313
+
314
+ const resultPage = await store.getMessages({
315
+ threadId: thread.id,
316
+ fromDate: yesterday,
317
+ page: 0,
318
+ perPage: 3,
319
+ format: 'v1',
320
+ });
321
+ expect(resultPage.total).toBe(5);
322
+ expect(resultPage.messages).toHaveLength(3);
323
+
324
+ expect(new Date((resultPage.messages[0] as MastraMessageV1).createdAt).toISOString()).toBe(now.toISOString());
325
+ expect(new Date((resultPage.messages[1] as MastraMessageV1).createdAt).toISOString()).toBe(now.toISOString());
326
+ expect(new Date((resultPage.messages[2] as MastraMessageV1).createdAt).toISOString()).toBe(
327
+ yesterday.toISOString(),
328
+ );
329
+ });
304
330
  });
305
331
 
306
332
  describe('Edge Cases and Error Handling', () => {
@@ -752,78 +778,76 @@ describe('PostgresStore', () => {
752
778
  it('should retrieve evals by agent name', async () => {
753
779
  const agentName = `test-agent-${randomUUID()}`;
754
780
 
755
- // Create sample evals
756
- const liveEval = createSampleEval(agentName, false);
781
+ // Create sample evals using the imported helper
782
+ const liveEval = createSampleEval(agentName, false); // createSampleEval returns snake_case
757
783
  const testEval = createSampleEval(agentName, true);
758
784
  const otherAgentEval = createSampleEval(`other-agent-${randomUUID()}`, false);
759
785
 
760
- // Insert evals
786
+ // Insert evals - ensure DB columns are snake_case
761
787
  await store.insert({
762
788
  tableName: TABLE_EVALS,
763
789
  record: {
764
- agent_name: liveEval.agentName,
790
+ agent_name: liveEval.agent_name, // Use snake_case
765
791
  input: liveEval.input,
766
792
  output: liveEval.output,
767
793
  result: liveEval.result,
768
- metric_name: liveEval.metricName,
794
+ metric_name: liveEval.metric_name, // Use snake_case
769
795
  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),
796
+ test_info: liveEval.test_info, // test_info from helper can be undefined or object
797
+ global_run_id: liveEval.global_run_id, // Use snake_case
798
+ run_id: liveEval.run_id, // Use snake_case
799
+ created_at: new Date(liveEval.created_at as string), // created_at from helper is string or Date
775
800
  },
776
801
  });
777
802
 
778
803
  await store.insert({
779
804
  tableName: TABLE_EVALS,
780
805
  record: {
781
- agent_name: testEval.agentName,
806
+ agent_name: testEval.agent_name,
782
807
  input: testEval.input,
783
808
  output: testEval.output,
784
809
  result: testEval.result,
785
- metric_name: testEval.metricName,
810
+ metric_name: testEval.metric_name,
786
811
  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),
812
+ test_info: testEval.test_info ? JSON.stringify(testEval.test_info) : null,
813
+ global_run_id: testEval.global_run_id,
814
+ run_id: testEval.run_id,
815
+ created_at: new Date(testEval.created_at as string),
792
816
  },
793
817
  });
794
818
 
795
819
  await store.insert({
796
820
  tableName: TABLE_EVALS,
797
821
  record: {
798
- agent_name: otherAgentEval.agentName,
822
+ agent_name: otherAgentEval.agent_name,
799
823
  input: otherAgentEval.input,
800
824
  output: otherAgentEval.output,
801
825
  result: otherAgentEval.result,
802
- metric_name: otherAgentEval.metricName,
826
+ metric_name: otherAgentEval.metric_name,
803
827
  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),
828
+ test_info: otherAgentEval.test_info, // Can be null/undefined directly
829
+ global_run_id: otherAgentEval.global_run_id,
830
+ run_id: otherAgentEval.run_id,
831
+ created_at: new Date(otherAgentEval.created_at as string),
809
832
  },
810
833
  });
811
834
 
812
835
  // Test getting all evals for the agent
813
836
  const allEvals = await store.getEvalsByAgentName(agentName);
814
837
  expect(allEvals).toHaveLength(2);
815
- expect(allEvals.map(e => e.runId)).toEqual(expect.arrayContaining([liveEval.runId, testEval.runId]));
838
+ // EvalRow type expects camelCase, but PostgresStore.transformEvalRow converts snake_case from DB to camelCase
839
+ expect(allEvals.map(e => e.runId)).toEqual(expect.arrayContaining([liveEval.run_id, testEval.run_id]));
816
840
 
817
841
  // Test getting only live evals
818
842
  const liveEvals = await store.getEvalsByAgentName(agentName, 'live');
819
843
  expect(liveEvals).toHaveLength(1);
820
- expect(liveEvals[0].runId).toBe(liveEval.runId);
844
+ expect(liveEvals[0].runId).toBe(liveEval.run_id); // Comparing with snake_case run_id from original data
821
845
 
822
846
  // 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);
847
+ const testEvalsResult = await store.getEvalsByAgentName(agentName, 'test');
848
+ expect(testEvalsResult).toHaveLength(1);
849
+ expect(testEvalsResult[0].runId).toBe(testEval.run_id);
850
+ expect(testEvalsResult[0].testInfo).toEqual(testEval.test_info);
827
851
 
828
852
  // Test getting evals for non-existent agent
829
853
  const nonExistentEvals = await store.getEvalsByAgentName('non-existent-agent');
@@ -931,6 +955,402 @@ describe('PostgresStore', () => {
931
955
  });
932
956
  });
933
957
 
958
+ describe('Pagination Features', () => {
959
+ beforeEach(async () => {
960
+ await store.clearTable({ tableName: TABLE_EVALS });
961
+ await store.clearTable({ tableName: TABLE_TRACES });
962
+ await store.clearTable({ tableName: TABLE_MESSAGES });
963
+ await store.clearTable({ tableName: TABLE_THREADS });
964
+ });
965
+
966
+ describe('getEvals with pagination', () => {
967
+ it('should return paginated evals with total count (page/perPage)', async () => {
968
+ const agentName = 'pagination-agent-evals';
969
+ const evalPromises = Array.from({ length: 25 }, (_, i) => {
970
+ const evalData = createSampleEval(agentName, i % 2 === 0);
971
+ return store.insert({
972
+ tableName: TABLE_EVALS,
973
+ record: {
974
+ run_id: evalData.run_id,
975
+ agent_name: evalData.agent_name,
976
+ input: evalData.input,
977
+ output: evalData.output,
978
+ result: evalData.result,
979
+ metric_name: evalData.metric_name,
980
+ instructions: evalData.instructions,
981
+ test_info: evalData.test_info,
982
+ global_run_id: evalData.global_run_id,
983
+ created_at: new Date(evalData.created_at as string),
984
+ },
985
+ });
986
+ });
987
+ await Promise.all(evalPromises);
988
+
989
+ const page1 = await store.getEvals({ agentName, page: 0, perPage: 10 });
990
+ expect(page1.evals).toHaveLength(10);
991
+ expect(page1.total).toBe(25);
992
+ expect(page1.page).toBe(0);
993
+ expect(page1.perPage).toBe(10);
994
+ expect(page1.hasMore).toBe(true);
995
+
996
+ const page3 = await store.getEvals({ agentName, page: 2, perPage: 10 });
997
+ expect(page3.evals).toHaveLength(5);
998
+ expect(page3.total).toBe(25);
999
+ expect(page3.page).toBe(2);
1000
+ expect(page3.hasMore).toBe(false);
1001
+ });
1002
+
1003
+ it('should support limit/offset pagination for getEvals', async () => {
1004
+ const agentName = 'pagination-agent-lo-evals';
1005
+ const evalPromises = Array.from({ length: 15 }, () => {
1006
+ const evalData = createSampleEval(agentName);
1007
+ return store.insert({
1008
+ tableName: TABLE_EVALS,
1009
+ record: {
1010
+ run_id: evalData.run_id,
1011
+ agent_name: evalData.agent_name,
1012
+ input: evalData.input,
1013
+ output: evalData.output,
1014
+ result: evalData.result,
1015
+ metric_name: evalData.metric_name,
1016
+ instructions: evalData.instructions,
1017
+ test_info: evalData.test_info,
1018
+ global_run_id: evalData.global_run_id,
1019
+ created_at: new Date(evalData.created_at as string),
1020
+ },
1021
+ });
1022
+ });
1023
+ await Promise.all(evalPromises);
1024
+
1025
+ const result = await store.getEvals({ agentName, limit: 5, offset: 10 });
1026
+ expect(result.evals).toHaveLength(5);
1027
+ expect(result.total).toBe(15);
1028
+ expect(result.page).toBeUndefined(); // Page is undefined for limit/offset
1029
+ expect(result.perPage).toBeUndefined(); // PerPage is undefined for limit/offset
1030
+ expect(result.hasMore).toBe(false);
1031
+ });
1032
+
1033
+ it('should filter by type with pagination for getEvals', async () => {
1034
+ const agentName = 'pagination-agent-type-evals';
1035
+ const testEvalPromises = Array.from({ length: 10 }, () => {
1036
+ const evalData = createSampleEval(agentName, true);
1037
+ return store.insert({
1038
+ tableName: TABLE_EVALS,
1039
+ record: {
1040
+ run_id: evalData.run_id,
1041
+ agent_name: evalData.agent_name,
1042
+ input: evalData.input,
1043
+ output: evalData.output,
1044
+ result: evalData.result,
1045
+ metric_name: evalData.metric_name,
1046
+ instructions: evalData.instructions,
1047
+ test_info: evalData.test_info,
1048
+ global_run_id: evalData.global_run_id,
1049
+ created_at: new Date(evalData.created_at as string),
1050
+ },
1051
+ });
1052
+ });
1053
+ const liveEvalPromises = Array.from({ length: 8 }, () => {
1054
+ const evalData = createSampleEval(agentName, false);
1055
+ return store.insert({
1056
+ tableName: TABLE_EVALS,
1057
+ record: {
1058
+ run_id: evalData.run_id,
1059
+ agent_name: evalData.agent_name,
1060
+ input: evalData.input,
1061
+ output: evalData.output,
1062
+ result: evalData.result,
1063
+ metric_name: evalData.metric_name,
1064
+ instructions: evalData.instructions,
1065
+ test_info: evalData.test_info,
1066
+ global_run_id: evalData.global_run_id,
1067
+ created_at: new Date(evalData.created_at as string),
1068
+ },
1069
+ });
1070
+ });
1071
+ await Promise.all([...testEvalPromises, ...liveEvalPromises]);
1072
+
1073
+ const testResults = await store.getEvals({ agentName, type: 'test', page: 0, perPage: 5 });
1074
+ expect(testResults.evals).toHaveLength(5);
1075
+ expect(testResults.total).toBe(10);
1076
+
1077
+ const liveResults = await store.getEvals({ agentName, type: 'live', page: 1, perPage: 3 });
1078
+ expect(liveResults.evals).toHaveLength(3);
1079
+ expect(liveResults.total).toBe(8);
1080
+ expect(liveResults.hasMore).toBe(true);
1081
+ });
1082
+
1083
+ it('should filter by date with pagination for getEvals', async () => {
1084
+ const agentName = 'pagination-agent-date-evals';
1085
+ const now = new Date();
1086
+ const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
1087
+ const dayBeforeYesterday = new Date(now.getTime() - 48 * 60 * 60 * 1000);
1088
+
1089
+ const createEvalAtDate = (date: Date) => {
1090
+ const evalData = createSampleEval(agentName, false, date); // Pass date to helper
1091
+ return store.insert({
1092
+ tableName: TABLE_EVALS,
1093
+ record: {
1094
+ run_id: evalData.run_id, // Use snake_case from helper
1095
+ agent_name: evalData.agent_name,
1096
+ input: evalData.input,
1097
+ output: evalData.output,
1098
+ result: evalData.result,
1099
+ metric_name: evalData.metric_name,
1100
+ instructions: evalData.instructions,
1101
+ test_info: evalData.test_info,
1102
+ global_run_id: evalData.global_run_id,
1103
+ created_at: evalData.created_at, // Use created_at from helper (already Date or ISO string)
1104
+ },
1105
+ });
1106
+ };
1107
+
1108
+ await Promise.all([
1109
+ createEvalAtDate(dayBeforeYesterday),
1110
+ createEvalAtDate(dayBeforeYesterday),
1111
+ createEvalAtDate(yesterday),
1112
+ createEvalAtDate(yesterday),
1113
+ createEvalAtDate(yesterday),
1114
+ createEvalAtDate(now),
1115
+ createEvalAtDate(now),
1116
+ createEvalAtDate(now),
1117
+ createEvalAtDate(now),
1118
+ ]);
1119
+
1120
+ const fromYesterday = await store.getEvals({ agentName, fromDate: yesterday, page: 0, perPage: 3 });
1121
+ expect(fromYesterday.total).toBe(7); // 3 yesterday + 4 now
1122
+ expect(fromYesterday.evals).toHaveLength(3);
1123
+ // Evals are sorted DESC, so first 3 are from 'now'
1124
+ fromYesterday.evals.forEach(e =>
1125
+ expect(new Date(e.createdAt).getTime()).toBeGreaterThanOrEqual(yesterday.getTime()),
1126
+ );
1127
+
1128
+ const onlyDayBefore = await store.getEvals({
1129
+ agentName,
1130
+ toDate: new Date(yesterday.getTime() - 1),
1131
+ page: 0,
1132
+ perPage: 5,
1133
+ });
1134
+ expect(onlyDayBefore.total).toBe(2);
1135
+ expect(onlyDayBefore.evals).toHaveLength(2);
1136
+ });
1137
+ });
1138
+
1139
+ describe('getTraces with pagination', () => {
1140
+ it('should return paginated traces with total count', async () => {
1141
+ const tracePromises = Array.from({ length: 18 }, (_, i) =>
1142
+ store.insert({ tableName: TABLE_TRACES, record: createSampleTraceForDB(`test-trace-${i}`, 'pg-test-scope') }),
1143
+ );
1144
+ await Promise.all(tracePromises);
1145
+
1146
+ const page1 = await store.getTraces({
1147
+ scope: 'pg-test-scope',
1148
+ page: 0,
1149
+ perPage: 8,
1150
+ returnPaginationResults: true,
1151
+ });
1152
+ expect(page1.traces).toHaveLength(8);
1153
+ expect(page1.total).toBe(18);
1154
+ expect(page1.page).toBe(0);
1155
+ expect(page1.perPage).toBe(8);
1156
+ expect(page1.hasMore).toBe(true);
1157
+
1158
+ const page3 = await store.getTraces({
1159
+ scope: 'pg-test-scope',
1160
+ page: 2,
1161
+ perPage: 8,
1162
+ returnPaginationResults: true,
1163
+ });
1164
+ expect(page3.traces).toHaveLength(2);
1165
+ expect(page3.total).toBe(18);
1166
+ expect(page3.hasMore).toBe(false);
1167
+ });
1168
+
1169
+ it('should filter by attributes with pagination for getTraces', async () => {
1170
+ const tracesWithAttr = Array.from({ length: 8 }, (_, i) =>
1171
+ store.insert({
1172
+ tableName: TABLE_TRACES,
1173
+ record: createSampleTraceForDB(`trace-${i}`, 'pg-attr-scope', { environment: 'prod' }),
1174
+ }),
1175
+ );
1176
+ const tracesWithoutAttr = Array.from({ length: 5 }, (_, i) =>
1177
+ store.insert({
1178
+ tableName: TABLE_TRACES,
1179
+ record: createSampleTraceForDB(`trace-other-${i}`, 'pg-attr-scope', { environment: 'dev' }),
1180
+ }),
1181
+ );
1182
+ await Promise.all([...tracesWithAttr, ...tracesWithoutAttr]);
1183
+
1184
+ const prodTraces = await store.getTraces({
1185
+ scope: 'pg-attr-scope',
1186
+ attributes: { environment: 'prod' },
1187
+ page: 0,
1188
+ perPage: 5,
1189
+ returnPaginationResults: true,
1190
+ });
1191
+ expect(prodTraces.traces).toHaveLength(5);
1192
+ expect(prodTraces.total).toBe(8);
1193
+ expect(prodTraces.hasMore).toBe(true);
1194
+ });
1195
+
1196
+ it('should filter by date with pagination for getTraces', async () => {
1197
+ const scope = 'pg-date-traces';
1198
+ const now = new Date();
1199
+ const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
1200
+ const dayBeforeYesterday = new Date(now.getTime() - 48 * 60 * 60 * 1000);
1201
+
1202
+ await Promise.all([
1203
+ store.insert({
1204
+ tableName: TABLE_TRACES,
1205
+ record: createSampleTraceForDB('t1', scope, undefined, dayBeforeYesterday),
1206
+ }),
1207
+ store.insert({ tableName: TABLE_TRACES, record: createSampleTraceForDB('t2', scope, undefined, yesterday) }),
1208
+ store.insert({ tableName: TABLE_TRACES, record: createSampleTraceForDB('t3', scope, undefined, yesterday) }),
1209
+ store.insert({ tableName: TABLE_TRACES, record: createSampleTraceForDB('t4', scope, undefined, now) }),
1210
+ store.insert({ tableName: TABLE_TRACES, record: createSampleTraceForDB('t5', scope, undefined, now) }),
1211
+ ]);
1212
+
1213
+ const fromYesterday = await store.getTraces({
1214
+ scope,
1215
+ fromDate: yesterday,
1216
+ page: 0,
1217
+ perPage: 2,
1218
+ returnPaginationResults: true,
1219
+ });
1220
+ expect(fromYesterday.total).toBe(4); // 2 yesterday + 2 now
1221
+ expect(fromYesterday.traces).toHaveLength(2);
1222
+ fromYesterday.traces.forEach(t =>
1223
+ expect(new Date(t.createdAt).getTime()).toBeGreaterThanOrEqual(yesterday.getTime()),
1224
+ );
1225
+
1226
+ const onlyNow = await store.getTraces({
1227
+ scope,
1228
+ fromDate: now,
1229
+ toDate: now,
1230
+ page: 0,
1231
+ perPage: 5,
1232
+ returnPaginationResults: true,
1233
+ });
1234
+ expect(onlyNow.total).toBe(2);
1235
+ expect(onlyNow.traces).toHaveLength(2);
1236
+ });
1237
+
1238
+ it('should return array when returnPaginationResults is false or undefined', async () => {
1239
+ await store.insert({ tableName: TABLE_TRACES, record: createSampleTraceForDB('trace-arr', 'pg-array-scope') });
1240
+ const tracesArray = await store.getTraces({ scope: 'pg-array-scope', page: 0, perPage: 5 }); // returnPaginationResults is undefined
1241
+ expect(Array.isArray(tracesArray)).toBe(true);
1242
+ expect(tracesArray.length).toBe(1);
1243
+ });
1244
+ });
1245
+
1246
+ describe('getMessages with pagination', () => {
1247
+ it('should return paginated messages with total count', async () => {
1248
+ const thread = createSampleThread();
1249
+ await store.saveThread({ thread });
1250
+ const messagePromises = Array.from({ length: 15 }, (_, i) =>
1251
+ store.saveMessages({
1252
+ messages: [{ ...createSampleMessage(thread.id), content: [{ type: 'text', text: `Message ${i + 1}` }] }],
1253
+ }),
1254
+ );
1255
+ await Promise.all(messagePromises);
1256
+
1257
+ const page1 = await store.getMessages({ threadId: thread.id, page: 0, perPage: 5, format: 'v1' });
1258
+ expect(page1.messages).toHaveLength(5);
1259
+ expect(page1.total).toBe(15);
1260
+ expect(page1.page).toBe(0);
1261
+ expect(page1.perPage).toBe(5);
1262
+ expect(page1.hasMore).toBe(true);
1263
+
1264
+ const page3 = await store.getMessages({ threadId: thread.id, page: 2, perPage: 5, format: 'v1' });
1265
+ expect(page3.messages).toHaveLength(5);
1266
+ expect(page3.total).toBe(15);
1267
+ expect(page3.hasMore).toBe(false);
1268
+ });
1269
+
1270
+ it('should filter by date with pagination for getMessages', async () => {
1271
+ const thread = createSampleThread();
1272
+ await store.saveThread({ thread });
1273
+ const now = new Date();
1274
+ const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
1275
+ const dayBeforeYesterday = new Date(now.getTime() - 48 * 60 * 60 * 1000);
1276
+
1277
+ const createMsgAtDate = (date: Date) => {
1278
+ return store.saveMessages({ messages: [{ ...createSampleMessage(thread.id), createdAt: date }] });
1279
+ };
1280
+ await Promise.all([
1281
+ createMsgAtDate(dayBeforeYesterday),
1282
+ createMsgAtDate(dayBeforeYesterday),
1283
+ createMsgAtDate(yesterday),
1284
+ createMsgAtDate(yesterday),
1285
+ createMsgAtDate(yesterday),
1286
+ createMsgAtDate(now),
1287
+ createMsgAtDate(now),
1288
+ ]);
1289
+
1290
+ const resultPage = await store.getMessages({
1291
+ threadId: thread.id,
1292
+ fromDate: yesterday,
1293
+ page: 0,
1294
+ perPage: 3,
1295
+ format: 'v1',
1296
+ });
1297
+ expect(resultPage.total).toBe(5);
1298
+ expect(resultPage.messages).toHaveLength(3);
1299
+
1300
+ expect(new Date((resultPage.messages[0] as MastraMessageV1).createdAt).toISOString()).toBe(now.toISOString());
1301
+ expect(new Date((resultPage.messages[1] as MastraMessageV1).createdAt).toISOString()).toBe(now.toISOString());
1302
+ expect(new Date((resultPage.messages[2] as MastraMessageV1).createdAt).toISOString()).toBe(
1303
+ yesterday.toISOString(),
1304
+ );
1305
+ });
1306
+
1307
+ it('should maintain backward compatibility for getMessages (no pagination params)', async () => {
1308
+ const thread = createSampleThread();
1309
+ await store.saveThread({ thread });
1310
+ await store.saveMessages({ messages: [createSampleMessage(thread.id)] });
1311
+
1312
+ const messages = await store.getMessages({ threadId: thread.id, format: 'v1' });
1313
+ expect(Array.isArray(messages)).toBe(true);
1314
+ expect(messages.length).toBe(1);
1315
+ // @ts-expect-error - messages should not have pagination properties
1316
+ expect(messages.total).toBeUndefined();
1317
+ });
1318
+ });
1319
+
1320
+ describe('getThreadsByResourceId with pagination', () => {
1321
+ it('should return paginated threads with total count', async () => {
1322
+ const resourceId = `pg-paginated-resource-${randomUUID()}`;
1323
+ const threadPromises = Array.from({ length: 17 }, () =>
1324
+ store.saveThread({ thread: { ...createSampleThread(), resourceId } }),
1325
+ );
1326
+ await Promise.all(threadPromises);
1327
+
1328
+ const page1 = await store.getThreadsByResourceId({ resourceId, page: 0, perPage: 7 });
1329
+ expect(page1.threads).toHaveLength(7);
1330
+ expect(page1.total).toBe(17);
1331
+ expect(page1.page).toBe(0);
1332
+ expect(page1.perPage).toBe(7);
1333
+ expect(page1.hasMore).toBe(true);
1334
+
1335
+ const page3 = await store.getThreadsByResourceId({ resourceId, page: 2, perPage: 7 });
1336
+ expect(page3.threads).toHaveLength(3); // 17 total, 7 per page, 3rd page has 17 - 2*7 = 3
1337
+ expect(page3.total).toBe(17);
1338
+ expect(page3.hasMore).toBe(false);
1339
+ });
1340
+
1341
+ it('should return array when no pagination params for getThreadsByResourceId', async () => {
1342
+ const resourceId = `pg-non-paginated-resource-${randomUUID()}`;
1343
+ await store.saveThread({ thread: { ...createSampleThread(), resourceId } });
1344
+
1345
+ const threads = await store.getThreadsByResourceId({ resourceId });
1346
+ expect(Array.isArray(threads)).toBe(true);
1347
+ expect(threads.length).toBe(1);
1348
+ // @ts-expect-error - threads should not have pagination properties
1349
+ expect(threads.total).toBeUndefined();
1350
+ });
1351
+ });
1352
+ });
1353
+
934
1354
  describe('Permission Handling', () => {
935
1355
  const schemaRestrictedUser = 'mastra_schema_restricted_storage';
936
1356
  const restrictedPassword = 'test123';