@studious-lms/server 1.4.1 → 1.4.3

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.
Files changed (248) hide show
  1. package/.coderabbit.yaml +4 -1
  2. package/.env.example +6 -0
  3. package/.env.test.example +2 -0
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +101 -76
  6. package/dist/index.js.map +1 -1
  7. package/dist/lib/config/cors.d.ts +16 -0
  8. package/dist/lib/config/cors.d.ts.map +1 -0
  9. package/dist/lib/config/cors.js +75 -0
  10. package/dist/lib/config/cors.js.map +1 -0
  11. package/dist/lib/config/env.d.ts +14 -0
  12. package/dist/lib/config/env.d.ts.map +1 -1
  13. package/dist/lib/config/env.js +9 -2
  14. package/dist/lib/config/env.js.map +1 -1
  15. package/dist/lib/fileUpload.d.ts +10 -2
  16. package/dist/lib/fileUpload.d.ts.map +1 -1
  17. package/dist/lib/fileUpload.js +38 -6
  18. package/dist/lib/fileUpload.js.map +1 -1
  19. package/dist/lib/prisma.d.ts +14 -2
  20. package/dist/lib/prisma.d.ts.map +1 -1
  21. package/dist/lib/prisma.js +27 -8
  22. package/dist/lib/prisma.js.map +1 -1
  23. package/dist/middleware/auth.d.ts.map +1 -1
  24. package/dist/middleware/auth.js +18 -5
  25. package/dist/middleware/auth.js.map +1 -1
  26. package/dist/middleware/security.d.ts.map +1 -1
  27. package/dist/middleware/security.js +3 -3
  28. package/dist/middleware/security.js.map +1 -1
  29. package/dist/models/agenda.d.ts +16 -16
  30. package/dist/models/announcement.d.ts +59 -23
  31. package/dist/models/announcement.d.ts.map +1 -1
  32. package/dist/models/assignment.d.ts +363 -276
  33. package/dist/models/assignment.d.ts.map +1 -1
  34. package/dist/models/attendance.d.ts +63 -21
  35. package/dist/models/attendance.d.ts.map +1 -1
  36. package/dist/models/auth.d.ts +115 -26
  37. package/dist/models/auth.d.ts.map +1 -1
  38. package/dist/models/auth.js +53 -30
  39. package/dist/models/auth.js.map +1 -1
  40. package/dist/models/class.d.ts +115 -65
  41. package/dist/models/class.d.ts.map +1 -1
  42. package/dist/models/class.js +16 -4
  43. package/dist/models/class.js.map +1 -1
  44. package/dist/models/comment.d.ts +52 -16
  45. package/dist/models/comment.d.ts.map +1 -1
  46. package/dist/models/conversation.d.ts +46 -16
  47. package/dist/models/conversation.d.ts.map +1 -1
  48. package/dist/models/event.d.ts +107 -53
  49. package/dist/models/event.d.ts.map +1 -1
  50. package/dist/models/file.d.ts +213 -165
  51. package/dist/models/file.d.ts.map +1 -1
  52. package/dist/models/folder.d.ts +161 -77
  53. package/dist/models/folder.d.ts.map +1 -1
  54. package/dist/models/labChat.d.ts +73 -31
  55. package/dist/models/labChat.d.ts.map +1 -1
  56. package/dist/models/marketing.d.ts +25 -7
  57. package/dist/models/marketing.d.ts.map +1 -1
  58. package/dist/models/message.d.ts +31 -13
  59. package/dist/models/message.d.ts.map +1 -1
  60. package/dist/models/newtonChat.d.ts +34 -10
  61. package/dist/models/newtonChat.d.ts.map +1 -1
  62. package/dist/models/notification.d.ts +29 -10
  63. package/dist/models/notification.d.ts.map +1 -1
  64. package/dist/models/notification.js +13 -9
  65. package/dist/models/notification.js.map +1 -1
  66. package/dist/models/section.d.ts +71 -23
  67. package/dist/models/section.d.ts.map +1 -1
  68. package/dist/models/user.d.ts +27 -9
  69. package/dist/models/user.d.ts.map +1 -1
  70. package/dist/models/worksheet.d.ts +606 -106
  71. package/dist/models/worksheet.d.ts.map +1 -1
  72. package/dist/models/worksheet.js +168 -2
  73. package/dist/models/worksheet.js.map +1 -1
  74. package/dist/pipelines/aiLabChat.d.ts +22 -2
  75. package/dist/pipelines/aiLabChat.d.ts.map +1 -1
  76. package/dist/pipelines/aiLabChat.js +133 -95
  77. package/dist/pipelines/aiLabChat.js.map +1 -1
  78. package/dist/pipelines/aiLabChatContract.d.ts +25 -25
  79. package/dist/pipelines/aiNewtonChat.d.ts.map +1 -1
  80. package/dist/pipelines/aiNewtonChat.js +25 -14
  81. package/dist/pipelines/aiNewtonChat.js.map +1 -1
  82. package/dist/pipelines/gradeWorksheet.d.ts +8 -8
  83. package/dist/pipelines/gradeWorksheet.d.ts.map +1 -1
  84. package/dist/pipelines/gradeWorksheet.js +12 -5
  85. package/dist/pipelines/gradeWorksheet.js.map +1 -1
  86. package/dist/pipelines/labChatPrompt.d.ts +27 -0
  87. package/dist/pipelines/labChatPrompt.d.ts.map +1 -1
  88. package/dist/pipelines/labChatPrompt.js +143 -69
  89. package/dist/pipelines/labChatPrompt.js.map +1 -1
  90. package/dist/routers/_app.d.ts +1579 -1257
  91. package/dist/routers/_app.d.ts.map +1 -1
  92. package/dist/routers/agenda.d.ts +16 -16
  93. package/dist/routers/announcement.d.ts +19 -19
  94. package/dist/routers/assignment.d.ts +307 -291
  95. package/dist/routers/assignment.d.ts.map +1 -1
  96. package/dist/routers/assignment.js +3 -2
  97. package/dist/routers/assignment.js.map +1 -1
  98. package/dist/routers/attendance.d.ts +7 -7
  99. package/dist/routers/auth.d.ts +1 -1
  100. package/dist/routers/class.d.ts +79 -72
  101. package/dist/routers/class.d.ts.map +1 -1
  102. package/dist/routers/class.js +3 -3
  103. package/dist/routers/class.js.map +1 -1
  104. package/dist/routers/comment.d.ts +6 -6
  105. package/dist/routers/conversation.d.ts +11 -11
  106. package/dist/routers/event.d.ts +35 -35
  107. package/dist/routers/file.d.ts +12 -12
  108. package/dist/routers/folder.d.ts +54 -54
  109. package/dist/routers/labChat.d.ts +18 -12
  110. package/dist/routers/labChat.d.ts.map +1 -1
  111. package/dist/routers/labChat.js +5 -3
  112. package/dist/routers/labChat.js.map +1 -1
  113. package/dist/routers/marketing.d.ts +2 -2
  114. package/dist/routers/message.d.ts +33 -2
  115. package/dist/routers/message.d.ts.map +1 -1
  116. package/dist/routers/message.js +27 -4
  117. package/dist/routers/message.js.map +1 -1
  118. package/dist/routers/newtonChat.d.ts +7 -1
  119. package/dist/routers/newtonChat.d.ts.map +1 -1
  120. package/dist/routers/newtonChat.js +5 -4
  121. package/dist/routers/newtonChat.js.map +1 -1
  122. package/dist/routers/notifications.d.ts +13 -6
  123. package/dist/routers/notifications.d.ts.map +1 -1
  124. package/dist/routers/notifications.js +4 -4
  125. package/dist/routers/notifications.js.map +1 -1
  126. package/dist/routers/section.d.ts +7 -7
  127. package/dist/routers/studentProgress.d.ts +86 -0
  128. package/dist/routers/studentProgress.d.ts.map +1 -1
  129. package/dist/routers/studentProgress.js +14 -4
  130. package/dist/routers/studentProgress.js.map +1 -1
  131. package/dist/routers/user.d.ts +1 -1
  132. package/dist/routers/worksheet.d.ts +64 -62
  133. package/dist/routers/worksheet.d.ts.map +1 -1
  134. package/dist/routers/worksheet.js +18 -16
  135. package/dist/routers/worksheet.js.map +1 -1
  136. package/dist/seedDatabase.d.ts +1 -1
  137. package/dist/services/agenda.d.ts +16 -16
  138. package/dist/services/announcement.d.ts +8 -8
  139. package/dist/services/assignment.d.ts +299 -283
  140. package/dist/services/assignment.d.ts.map +1 -1
  141. package/dist/services/assignment.js +24 -5
  142. package/dist/services/assignment.js.map +1 -1
  143. package/dist/services/attendance.d.ts +7 -7
  144. package/dist/services/auth.d.ts +2 -2
  145. package/dist/services/auth.d.ts.map +1 -1
  146. package/dist/services/auth.js +19 -12
  147. package/dist/services/auth.js.map +1 -1
  148. package/dist/services/class.d.ts +74 -68
  149. package/dist/services/class.d.ts.map +1 -1
  150. package/dist/services/class.js +6 -3
  151. package/dist/services/class.js.map +1 -1
  152. package/dist/services/comment.d.ts +6 -6
  153. package/dist/services/conversation.d.ts +11 -11
  154. package/dist/services/event.d.ts +31 -31
  155. package/dist/services/file.d.ts +12 -12
  156. package/dist/services/file.d.ts.map +1 -1
  157. package/dist/services/file.js +7 -2
  158. package/dist/services/file.js.map +1 -1
  159. package/dist/services/folder.d.ts +52 -52
  160. package/dist/services/labChat.d.ts +18 -12
  161. package/dist/services/labChat.d.ts.map +1 -1
  162. package/dist/services/labChat.js +30 -18
  163. package/dist/services/labChat.js.map +1 -1
  164. package/dist/services/marketing.d.ts +2 -2
  165. package/dist/services/message.d.ts +30 -0
  166. package/dist/services/message.d.ts.map +1 -1
  167. package/dist/services/message.js +97 -21
  168. package/dist/services/message.js.map +1 -1
  169. package/dist/services/newtonChat.d.ts +6 -1
  170. package/dist/services/newtonChat.d.ts.map +1 -1
  171. package/dist/services/newtonChat.js +36 -18
  172. package/dist/services/newtonChat.js.map +1 -1
  173. package/dist/services/notification.d.ts +16 -9
  174. package/dist/services/notification.d.ts.map +1 -1
  175. package/dist/services/notification.js +24 -9
  176. package/dist/services/notification.js.map +1 -1
  177. package/dist/services/section.d.ts +6 -6
  178. package/dist/services/studentProgress.d.ts +75 -0
  179. package/dist/services/studentProgress.d.ts.map +1 -1
  180. package/dist/services/studentProgress.js +300 -107
  181. package/dist/services/studentProgress.js.map +1 -1
  182. package/dist/services/worksheet.d.ts +64 -64
  183. package/dist/services/worksheet.d.ts.map +1 -1
  184. package/dist/services/worksheet.js +138 -54
  185. package/dist/services/worksheet.js.map +1 -1
  186. package/dist/trpc.d.ts.map +1 -1
  187. package/dist/trpc.js +11 -17
  188. package/dist/trpc.js.map +1 -1
  189. package/dist/utils/inference.d.ts +9 -12
  190. package/dist/utils/inference.d.ts.map +1 -1
  191. package/dist/utils/inference.js +135 -95
  192. package/dist/utils/inference.js.map +1 -1
  193. package/package.json +1 -1
  194. package/prisma/migrations/20260410124000_add_submission_recommendation_state/migration.sql +14 -0
  195. package/prisma/migrations/20260415103500_add_session_type/migration.sql +33 -0
  196. package/prisma/migrations/20260415133000_add_ai_usage_logs/migration.sql +19 -0
  197. package/prisma/schema.prisma +41 -1
  198. package/scripts/test-pre-push.ts +6 -2
  199. package/src/index.ts +119 -79
  200. package/src/lib/config/cors.ts +96 -0
  201. package/src/lib/config/env.ts +12 -1
  202. package/src/lib/fileUpload.ts +55 -5
  203. package/src/lib/prisma.ts +25 -6
  204. package/src/middleware/auth.ts +19 -4
  205. package/src/middleware/security.ts +1 -1
  206. package/src/models/auth.ts +59 -29
  207. package/src/models/class.ts +15 -4
  208. package/src/models/notification.ts +13 -7
  209. package/src/models/worksheet.ts +202 -0
  210. package/src/pipelines/aiLabChat.ts +183 -104
  211. package/src/pipelines/aiNewtonChat.ts +24 -14
  212. package/src/pipelines/gradeWorksheet.ts +10 -2
  213. package/src/pipelines/labChatPrompt.ts +196 -68
  214. package/src/routers/assignment.ts +1 -0
  215. package/src/routers/class.ts +2 -2
  216. package/src/routers/labChat.ts +3 -1
  217. package/src/routers/message.ts +38 -1
  218. package/src/routers/newtonChat.ts +4 -2
  219. package/src/routers/notifications.ts +2 -2
  220. package/src/routers/studentProgress.ts +25 -1
  221. package/src/routers/worksheet.ts +31 -19
  222. package/src/services/assignment.ts +30 -2
  223. package/src/services/auth.ts +26 -12
  224. package/src/services/class.ts +4 -1
  225. package/src/services/file.ts +6 -0
  226. package/src/services/labChat.ts +42 -18
  227. package/src/services/message.ts +163 -18
  228. package/src/services/newtonChat.ts +49 -18
  229. package/src/services/notification.ts +27 -8
  230. package/src/services/studentProgress.ts +425 -121
  231. package/src/services/worksheet.ts +191 -51
  232. package/src/trpc.ts +12 -18
  233. package/src/utils/inference.ts +205 -112
  234. package/tests/globalSetup.ts +13 -4
  235. package/tests/lib/cors.test.ts +103 -0
  236. package/tests/pipelines/aiLabChat.test.ts +64 -84
  237. package/tests/routers/auth.test.ts +120 -17
  238. package/tests/routers/class.test.ts +7 -1
  239. package/tests/routers/message.test.ts +236 -9
  240. package/tests/routers/newtonChat.test.ts +81 -0
  241. package/tests/routers/notification.test.ts +12 -0
  242. package/tests/routers/studentProgress.test.ts +2 -31
  243. package/tests/routers/worksheet.test.ts +194 -0
  244. package/tests/setup.ts +22 -5
  245. package/tests/utils/aiLabChatPrompt.test.ts +114 -6
  246. package/tests/utils/inference.test.ts +218 -0
  247. package/tests/utils/studentProgress.test.ts +361 -0
  248. package/vitest.unit.config.ts +1 -0
@@ -0,0 +1,81 @@
1
+ import { beforeAll, describe, expect, test } from 'vitest';
2
+ import { prisma } from '../../src/lib/prisma';
3
+ import { user1Caller, user2Caller } from '../setup';
4
+
5
+ describe('Newton Chat Router', () => {
6
+ let testClass: any;
7
+ let assignment: any;
8
+ let submission: any;
9
+ let newtonChatId: string;
10
+ let conversationId: string;
11
+ let user2Id: string;
12
+
13
+ beforeAll(async () => {
14
+ testClass = await user1Caller.class.create({
15
+ name: 'Newton Chat Test Class',
16
+ subject: 'Physics',
17
+ section: '10th Grade',
18
+ });
19
+
20
+ const user2Profile = await user2Caller.user.getProfile();
21
+ user2Id = user2Profile.id;
22
+
23
+ await user1Caller.class.addStudent({
24
+ classId: testClass.id,
25
+ studentId: user2Id,
26
+ });
27
+
28
+ const dueDate = new Date();
29
+ dueDate.setDate(dueDate.getDate() + 7);
30
+
31
+ assignment = await user1Caller.assignment.create({
32
+ classId: testClass.id,
33
+ title: 'Newton Chat Assignment',
34
+ instructions: 'Ask Newton for help with this assignment.',
35
+ dueDate: dueDate.toISOString(),
36
+ type: 'HOMEWORK',
37
+ });
38
+
39
+ submission = await user2Caller.assignment.getSubmission({
40
+ classId: testClass.id,
41
+ assignmentId: assignment.id,
42
+ });
43
+
44
+ const tutorConversation = await user2Caller.newtonChat.getTutorConversation({
45
+ assignmentId: assignment.id,
46
+ classId: testClass.id,
47
+ });
48
+
49
+ newtonChatId = tutorConversation.newtonChatId;
50
+ conversationId = tutorConversation.conversationId;
51
+ });
52
+
53
+ test('rejects attachment-only Newton chat posts', async () => {
54
+ const [uploadTarget] = await user2Caller.message.getUploadUrls({
55
+ conversationId,
56
+ files: [
57
+ {
58
+ name: 'newton-attachment-only.pdf',
59
+ type: 'application/pdf',
60
+ size: 1024,
61
+ },
62
+ ],
63
+ });
64
+
65
+ await prisma.file.update({
66
+ where: { id: uploadTarget!.id },
67
+ data: {
68
+ uploadStatus: 'COMPLETED',
69
+ uploadedAt: new Date(),
70
+ },
71
+ });
72
+
73
+ await expect(
74
+ user2Caller.newtonChat.postToNewtonChat({
75
+ newtonChatId,
76
+ content: ' ',
77
+ fileIds: [uploadTarget!.id],
78
+ }),
79
+ ).rejects.toThrow();
80
+ });
81
+ });
@@ -40,6 +40,18 @@ describe('Notification Router', () => {
40
40
  expect(notification!.title).toBe('Test Notification');
41
41
  });
42
42
 
43
+ test('should not get another user\'s notification', async () => {
44
+ await expect(
45
+ user1Caller.notification.get({ id: notificationId })
46
+ ).rejects.toMatchObject({ code: 'NOT_FOUND' });
47
+ });
48
+
49
+ test('should not mark another user\'s notification as read', async () => {
50
+ await expect(
51
+ user1Caller.notification.markAsRead({ id: notificationId })
52
+ ).rejects.toMatchObject({ code: 'NOT_FOUND' });
53
+ });
54
+
43
55
  test('should mark notification as read', async () => {
44
56
  const notification = await user2Caller.notification.markAsRead({ id: notificationId });
45
57
 
@@ -53,16 +53,6 @@ describe("Student Progress Router", () => {
53
53
  const futureDueDate = new Date();
54
54
  futureDueDate.setDate(futureDueDate.getDate() + 2);
55
55
 
56
- const futureAssignment = await user1Caller.assignment.create({
57
- classId: testClass.id,
58
- title: "Upcoming Quiz",
59
- instructions: "Prepare for the upcoming quiz.",
60
- dueDate: futureDueDate.toISOString(),
61
- maxGrade: 100,
62
- graded: true,
63
- type: "QUIZ",
64
- });
65
-
66
56
  const futureCompletedAssignment = await user1Caller.assignment.create({
67
57
  classId: testClass.id,
68
58
  title: "Completed Future Quiz",
@@ -110,17 +100,6 @@ describe("Student Progress Router", () => {
110
100
  expect(result.recommendations[0].assignmentId).toBe(assignment.id);
111
101
  expect(result.recommendations[0].reasons).toContain("Scored 50%");
112
102
 
113
- const futureRecommendation = result.recommendations.find(
114
- (recommendation) =>
115
- recommendation.assignmentId === futureAssignment.id,
116
- );
117
- expect(futureRecommendation?.reasons).toContain(
118
- "Upcoming graded assignment",
119
- );
120
- expect(futureRecommendation?.reasons).not.toContain(
121
- "Awaiting grade or feedback",
122
- );
123
-
124
103
  const completedFutureRecommendation = result.recommendations.find(
125
104
  (recommendation) =>
126
105
  recommendation.assignmentId === futureCompletedAssignment.id,
@@ -189,11 +168,7 @@ describe("Student Progress Router", () => {
189
168
  });
190
169
 
191
170
  expect(result.isFallback).toBe(true);
192
- expect(result.message).toContain("current overall progress");
193
- expect(result.message).toContain(
194
- "not enough graded work to determine a recent trend",
195
- );
196
- expect(result.message).not.toContain("missing or low-scoring");
171
+ expect(result.message).toContain("current overall grade");
197
172
  });
198
173
 
199
174
  test("returns a flagged fallback chat response for empty model output", async () => {
@@ -208,11 +183,7 @@ describe("Student Progress Router", () => {
208
183
  });
209
184
 
210
185
  expect(result.isFallback).toBe(true);
211
- expect(result.message).toContain("current overall progress");
212
- expect(result.message).toContain(
213
- "not enough graded work to determine a recent trend",
214
- );
215
- expect(result.message).not.toContain("missing or low-scoring");
186
+ expect(result.message).toContain("current overall grade");
216
187
  });
217
188
 
218
189
  test("prevents a student from chatting about another student's progress", async () => {
@@ -0,0 +1,194 @@
1
+ import { beforeEach, describe, expect, test } from "vitest";
2
+ import { user1Caller, user2Caller, user3Caller } from "../setup";
3
+
4
+ const expectTRPCError = async (promise: Promise<unknown>, code: string) => {
5
+ await expect(promise).rejects.toMatchObject({ code });
6
+ };
7
+
8
+ describe("Worksheet Router Security", () => {
9
+ let testClass: any;
10
+ let worksheet: any;
11
+ let question: any;
12
+ let multipleChoiceQuestion: any;
13
+ let submission: any;
14
+ let worksheetResponse: any;
15
+ let progressId: string;
16
+
17
+ beforeEach(async () => {
18
+ testClass = await user1Caller.class.create({
19
+ name: `Worksheet Security ${Date.now()}`,
20
+ subject: "Physics",
21
+ section: "12th Grade",
22
+ });
23
+
24
+ const user2Profile = await user2Caller.user.getProfile();
25
+ await user1Caller.class.addStudent({
26
+ classId: testClass.id,
27
+ studentId: user2Profile.id,
28
+ });
29
+
30
+ worksheet = await user1Caller.worksheet.create({
31
+ classId: testClass.id,
32
+ name: "Security Worksheet",
33
+ });
34
+
35
+ question = await user1Caller.worksheet.addQuestion({
36
+ worksheetId: worksheet.id,
37
+ question: "What is force?",
38
+ answer: "Mass times acceleration",
39
+ points: 5,
40
+ type: "SHORT_ANSWER",
41
+ });
42
+
43
+ multipleChoiceQuestion = await user1Caller.worksheet.addQuestion({
44
+ worksheetId: worksheet.id,
45
+ question: "What is the SI unit of force?",
46
+ answer: "newton",
47
+ points: 1,
48
+ type: "MULTIPLE_CHOICE",
49
+ options: [
50
+ { id: "a", text: "Joule", isCorrect: false },
51
+ { id: "b", text: "Newton", isCorrect: true },
52
+ ],
53
+ });
54
+
55
+ const dueDate = new Date();
56
+ dueDate.setDate(dueDate.getDate() + 7);
57
+
58
+ const assignment = await user1Caller.assignment.create({
59
+ classId: testClass.id,
60
+ title: "Worksheet Assignment",
61
+ instructions: "Complete the worksheet.",
62
+ dueDate: dueDate.toISOString(),
63
+ acceptWorksheet: true,
64
+ gradeWithAI: true,
65
+ worksheetIds: [worksheet.id],
66
+ type: "HOMEWORK",
67
+ });
68
+
69
+ submission = await user2Caller.assignment.getSubmission({
70
+ classId: testClass.id,
71
+ assignmentId: assignment.id,
72
+ });
73
+
74
+ worksheetResponse = await user2Caller.worksheet.getWorksheetSubmission({
75
+ worksheetId: worksheet.id,
76
+ submissionId: submission.id,
77
+ });
78
+
79
+ const answered = await user2Caller.worksheet.answerQuestion({
80
+ worksheetResponseId: worksheetResponse.id,
81
+ questionId: question.id,
82
+ response: "Mass times acceleration",
83
+ });
84
+
85
+ progressId = answered.responses[0].id;
86
+ });
87
+
88
+ test("redacts worksheet answers and correctness metadata for students", async () => {
89
+ const studentWorksheet = await user2Caller.worksheet.getWorksheet({
90
+ worksheetId: worksheet.id,
91
+ });
92
+
93
+ const shortAnswerQuestion = studentWorksheet.questions.find(
94
+ (item: any) => item.id === question.id,
95
+ );
96
+ expect(shortAnswerQuestion.answer).toBe("");
97
+ expect(shortAnswerQuestion.markScheme).toBeNull();
98
+
99
+ const studentMcq = studentWorksheet.questions.find(
100
+ (item: any) => item.id === multipleChoiceQuestion.id,
101
+ );
102
+ expect(studentMcq.answer).toBe("");
103
+ expect(studentMcq.options).toEqual([
104
+ { id: "a", text: "Joule" },
105
+ { id: "b", text: "Newton" },
106
+ ]);
107
+ });
108
+
109
+ test("blocks cross-class worksheet reads by id", async () => {
110
+ await expectTRPCError(
111
+ user3Caller.worksheet.getWorksheet({ worksheetId: worksheet.id }),
112
+ "FORBIDDEN",
113
+ );
114
+ });
115
+
116
+ test("blocks cross-class worksheet submission reads by id", async () => {
117
+ await expectTRPCError(
118
+ user3Caller.worksheet.getWorksheetSubmission({
119
+ worksheetId: worksheet.id,
120
+ submissionId: submission.id,
121
+ }),
122
+ "FORBIDDEN",
123
+ );
124
+ });
125
+
126
+ test("blocks cross-class worksheet answer mutations", async () => {
127
+ await expectTRPCError(
128
+ user3Caller.worksheet.answerQuestion({
129
+ worksheetResponseId: worksheetResponse.id,
130
+ questionId: question.id,
131
+ response: "Unauthorized answer",
132
+ }),
133
+ "FORBIDDEN",
134
+ );
135
+ });
136
+
137
+ test("blocks students from teacher worksheet mutations", async () => {
138
+ await expectTRPCError(
139
+ user2Caller.worksheet.updateWorksheet({
140
+ worksheetId: worksheet.id,
141
+ name: "Student update",
142
+ }),
143
+ "FORBIDDEN",
144
+ );
145
+
146
+ await expectTRPCError(
147
+ user2Caller.worksheet.addQuestion({
148
+ worksheetId: worksheet.id,
149
+ question: "Student-created question",
150
+ answer: "Nope",
151
+ type: "SHORT_ANSWER",
152
+ }),
153
+ "FORBIDDEN",
154
+ );
155
+ });
156
+
157
+ test("blocks students from grading and feedback actions", async () => {
158
+ await expectTRPCError(
159
+ user2Caller.worksheet.gradeAnswer({
160
+ questionId: question.id,
161
+ responseId: progressId,
162
+ studentWorksheetResponseId: worksheetResponse.id,
163
+ isCorrect: true,
164
+ feedback: "Nice work",
165
+ points: 5,
166
+ }),
167
+ "FORBIDDEN",
168
+ );
169
+
170
+ await expectTRPCError(
171
+ user2Caller.worksheet.addComment({
172
+ responseId: progressId,
173
+ comment: "Teacher feedback",
174
+ }),
175
+ "FORBIDDEN",
176
+ );
177
+
178
+ await expectTRPCError(
179
+ user2Caller.worksheet.cancelGrading({
180
+ worksheetResponseId: worksheetResponse.id,
181
+ progressId,
182
+ }),
183
+ "FORBIDDEN",
184
+ );
185
+
186
+ await expectTRPCError(
187
+ user2Caller.worksheet.regradeQuestion({
188
+ worksheetResponseId: worksheetResponse.id,
189
+ progressId,
190
+ }),
191
+ "FORBIDDEN",
192
+ );
193
+ });
194
+ });
package/tests/setup.ts CHANGED
@@ -6,7 +6,7 @@ import { beforeAll, beforeEach, afterAll, afterEach } from 'vitest';
6
6
  import { logger } from '../src/utils/logger';
7
7
  import { appRouter } from '../src/routers/_app';
8
8
  import { createTRPCContext } from '../src/trpc';
9
- import { Session } from '@prisma/client';
9
+ import { Session, SessionType } from '@prisma/client';
10
10
 
11
11
  // Load test environment variables
12
12
  config({ path: resolve(process.cwd(), '.env.test') });
@@ -35,9 +35,26 @@ beforeAll(async () => {
35
35
  throw new Error('Failed to get login tokens — ensure globalSetup ran');
36
36
  }
37
37
 
38
- session1 = await prisma.session.findFirst({ where: { userId: login1.user.id } }) as Session;
39
- session2 = await prisma.session.findFirst({ where: { userId: login2.user.id } }) as Session;
40
- session3 = await prisma.session.findFirst({ where: { userId: login3.user.id } }) as Session;
38
+ const authSession1 = await prisma.session.findFirst({
39
+ where: { userId: login1.user.id, type: SessionType.AUTH },
40
+ orderBy: { createdAt: 'desc' },
41
+ });
42
+ const authSession2 = await prisma.session.findFirst({
43
+ where: { userId: login2.user.id, type: SessionType.AUTH },
44
+ orderBy: { createdAt: 'desc' },
45
+ });
46
+ const authSession3 = await prisma.session.findFirst({
47
+ where: { userId: login3.user.id, type: SessionType.AUTH },
48
+ orderBy: { createdAt: 'desc' },
49
+ });
50
+
51
+ if (!authSession1 || !authSession2 || !authSession3) {
52
+ throw new Error('Failed to retrieve AUTH sessions for test users');
53
+ }
54
+
55
+ session1 = authSession1;
56
+ session2 = authSession2;
57
+ session3 = authSession3;
41
58
 
42
59
  verification1 = { verified: true };
43
60
  verification2 = { verified: true };
@@ -69,4 +86,4 @@ export let verification2: any;
69
86
  export let verification3: any;
70
87
  export let login1: any;
71
88
  export let login2: any;
72
- export let login3: any;
89
+ export let login3: any;
@@ -1,18 +1,126 @@
1
1
  import { describe, expect, test } from "vitest";
2
2
 
3
- import { buildLabChatSystemPrompt } from "../../src/pipelines/labChatPrompt";
3
+ import {
4
+ buildLabChatResponseMessages,
5
+ buildLabChatSystemPrompt,
6
+ } from "../../src/pipelines/labChatPrompt";
7
+ import { labChatArrayFieldInstructions, labChatResponseFormat } from "../../src/pipelines/aiLabChatContract";
4
8
 
5
9
  describe("buildLabChatSystemPrompt", () => {
6
10
  test("requires arrays for worksheet and section structured output fields", () => {
7
11
  const prompt = buildLabChatSystemPrompt("Context");
8
12
 
9
- expect(prompt).toContain(
10
- '{ "text": string, "docs": null | array, "worksheetsToCreate": array, "sectionsToCreate": array, "assignmentsToCreate": null | array }',
11
- );
12
- expect(prompt).toContain('"worksheetsToCreate" must always be an array. Use [] when there are no worksheets to create.');
13
- expect(prompt).toContain('"sectionsToCreate" must always be an array. Use [] when there are no sections to create.');
13
+ expect(prompt).toContain(labChatResponseFormat);
14
+ expect(prompt).toContain(labChatArrayFieldInstructions);
14
15
  expect(prompt).toContain('Do not return null for "worksheetsToCreate" or "sectionsToCreate".');
15
16
  expect(prompt).not.toContain('"worksheetsToCreate": null | array');
16
17
  expect(prompt).not.toContain('"sectionsToCreate": null | array');
17
18
  });
18
19
  });
20
+
21
+ describe("buildLabChatResponseMessages", () => {
22
+ test("builds response messages from history without replaying the latest teacher turn", () => {
23
+ const messages = buildLabChatResponseMessages({
24
+ context: "Topic context",
25
+ classContext: "CLASS: Biology",
26
+ recentMessages: [
27
+ {
28
+ senderId: "teacher-1",
29
+ content: "Create a quiz on osmosis",
30
+ sender: {
31
+ username: "teacher",
32
+ profile: { displayName: "Ms Patel" },
33
+ },
34
+ },
35
+ {
36
+ senderId: "ai-user",
37
+ content: "I can do that.",
38
+ },
39
+ ],
40
+ isAIUser: (senderId) => senderId === "ai-user",
41
+ });
42
+
43
+ const teacherTurns = messages.filter(
44
+ (message) =>
45
+ message.role === "user" &&
46
+ typeof message.content === "string" &&
47
+ message.content.includes("Create a quiz on osmosis"),
48
+ );
49
+
50
+ expect(teacherTurns).toHaveLength(1);
51
+ expect(messages.at(-2)).toMatchObject({
52
+ role: "developer",
53
+ content: expect.stringContaining("CLASS CONTEXT"),
54
+ });
55
+ });
56
+
57
+ test("sorts timestamped history into chronological order before prompt assembly", () => {
58
+ const messages = buildLabChatResponseMessages({
59
+ context: "Topic context",
60
+ classContext: "CLASS: Biology",
61
+ recentMessages: [
62
+ {
63
+ id: "assistant-later",
64
+ senderId: "ai-user",
65
+ content: "Second response",
66
+ createdAt: new Date("2026-04-08T12:02:00Z"),
67
+ },
68
+ {
69
+ id: "teacher-earlier",
70
+ senderId: "teacher-1",
71
+ content: "First request",
72
+ createdAt: new Date("2026-04-08T12:01:00Z"),
73
+ sender: {
74
+ username: "teacher",
75
+ profile: { displayName: "Ms Patel" },
76
+ },
77
+ },
78
+ ],
79
+ isAIUser: (senderId) => senderId === "ai-user",
80
+ });
81
+
82
+ expect(messages[1]).toMatchObject({
83
+ role: "user",
84
+ content: "Ms Patel: First request",
85
+ });
86
+ expect(messages[2]).toMatchObject({
87
+ role: "assistant",
88
+ content: "Second response",
89
+ });
90
+ });
91
+
92
+ test("keeps original order among messages without timestamps", () => {
93
+ const messages = buildLabChatResponseMessages({
94
+ context: "Topic context",
95
+ classContext: "CLASS: Biology",
96
+ recentMessages: [
97
+ {
98
+ senderId: "teacher-1",
99
+ content: "First untimed request",
100
+ sender: {
101
+ username: "teacher",
102
+ profile: { displayName: "Ms Patel" },
103
+ },
104
+ },
105
+ {
106
+ senderId: "teacher-1",
107
+ content: "Second untimed request",
108
+ sender: {
109
+ username: "teacher",
110
+ profile: { displayName: "Ms Patel" },
111
+ },
112
+ },
113
+ ],
114
+ isAIUser: () => false,
115
+ });
116
+
117
+ expect(messages[1]).toMatchObject({
118
+ role: "user",
119
+ content: "Ms Patel: First untimed request",
120
+ });
121
+ expect(messages[2]).toMatchObject({
122
+ role: "user",
123
+ content: "Ms Patel: Second untimed request",
124
+ });
125
+ });
126
+ });