@studious-lms/server 1.4.9 → 1.6.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.
Files changed (237) hide show
  1. package/dist/index.js +15 -3
  2. package/dist/index.js.map +1 -1
  3. package/dist/lib/config/cors.d.ts.map +1 -1
  4. package/dist/lib/config/cors.js +2 -3
  5. package/dist/lib/config/cors.js.map +1 -1
  6. package/dist/lib/fileUpload.d.ts.map +1 -1
  7. package/dist/lib/fileUpload.js +4 -5
  8. package/dist/lib/fileUpload.js.map +1 -1
  9. package/dist/lib/pusher.d.ts +7 -1
  10. package/dist/lib/pusher.d.ts.map +1 -1
  11. package/dist/lib/pusher.js +9 -3
  12. package/dist/lib/pusher.js.map +1 -1
  13. package/dist/models/attendance.d.ts +16 -7
  14. package/dist/models/attendance.d.ts.map +1 -1
  15. package/dist/models/attendance.js +17 -2
  16. package/dist/models/attendance.js.map +1 -1
  17. package/dist/models/auth.d.ts +1 -0
  18. package/dist/models/auth.d.ts.map +1 -1
  19. package/dist/models/auth.js +3 -2
  20. package/dist/models/auth.js.map +1 -1
  21. package/dist/models/comment.d.ts +0 -5
  22. package/dist/models/comment.d.ts.map +1 -1
  23. package/dist/models/conversation.d.ts +17 -16
  24. package/dist/models/conversation.d.ts.map +1 -1
  25. package/dist/models/labChat.d.ts +2 -1
  26. package/dist/models/labChat.d.ts.map +1 -1
  27. package/dist/models/message.d.ts +6 -3
  28. package/dist/models/message.d.ts.map +1 -1
  29. package/dist/models/school.d.ts +0 -22
  30. package/dist/models/school.d.ts.map +1 -1
  31. package/dist/models/school.js +2 -14
  32. package/dist/models/school.js.map +1 -1
  33. package/dist/models/worksheet.d.ts +76 -19
  34. package/dist/models/worksheet.d.ts.map +1 -1
  35. package/dist/models/worksheet.js +2 -2
  36. package/dist/models/worksheet.js.map +1 -1
  37. package/dist/pipelines/aiLabChat.d.ts +20 -9
  38. package/dist/pipelines/aiLabChat.d.ts.map +1 -1
  39. package/dist/pipelines/aiLabChat.js +63 -75
  40. package/dist/pipelines/aiLabChat.js.map +1 -1
  41. package/dist/pipelines/aiNewtonChat.d.ts +13 -4
  42. package/dist/pipelines/aiNewtonChat.d.ts.map +1 -1
  43. package/dist/pipelines/aiNewtonChat.js +39 -27
  44. package/dist/pipelines/aiNewtonChat.js.map +1 -1
  45. package/dist/pipelines/gradeWorksheet.d.ts +5 -3
  46. package/dist/pipelines/gradeWorksheet.d.ts.map +1 -1
  47. package/dist/pipelines/gradeWorksheet.js +60 -24
  48. package/dist/pipelines/gradeWorksheet.js.map +1 -1
  49. package/dist/pipelines/pdfToWorksheet.d.ts +34 -2
  50. package/dist/pipelines/pdfToWorksheet.d.ts.map +1 -1
  51. package/dist/pipelines/pdfToWorksheet.js +205 -32
  52. package/dist/pipelines/pdfToWorksheet.js.map +1 -1
  53. package/dist/routers/_app.d.ts +106 -48
  54. package/dist/routers/_app.d.ts.map +1 -1
  55. package/dist/routers/announcement.d.ts +0 -4
  56. package/dist/routers/announcement.d.ts.map +1 -1
  57. package/dist/routers/assignment.d.ts +0 -8
  58. package/dist/routers/assignment.d.ts.map +1 -1
  59. package/dist/routers/assignment.js +64 -4
  60. package/dist/routers/assignment.js.map +1 -1
  61. package/dist/routers/auth.d.ts +1 -0
  62. package/dist/routers/auth.d.ts.map +1 -1
  63. package/dist/routers/comment.d.ts +0 -3
  64. package/dist/routers/comment.d.ts.map +1 -1
  65. package/dist/routers/conversation.d.ts +2 -1
  66. package/dist/routers/conversation.d.ts.map +1 -1
  67. package/dist/routers/labChat.d.ts +3 -1
  68. package/dist/routers/labChat.d.ts.map +1 -1
  69. package/dist/routers/message.d.ts +3 -0
  70. package/dist/routers/message.d.ts.map +1 -1
  71. package/dist/routers/newtonChat.d.ts +1 -0
  72. package/dist/routers/newtonChat.d.ts.map +1 -1
  73. package/dist/routers/school.d.ts +6 -0
  74. package/dist/routers/school.d.ts.map +1 -1
  75. package/dist/routers/school.js +7 -2
  76. package/dist/routers/school.js.map +1 -1
  77. package/dist/routers/worksheet.d.ts +37 -7
  78. package/dist/routers/worksheet.d.ts.map +1 -1
  79. package/dist/routers/worksheet.js +16 -6
  80. package/dist/routers/worksheet.js.map +1 -1
  81. package/dist/services/announcement.js +4 -4
  82. package/dist/services/announcement.js.map +1 -1
  83. package/dist/services/assignment.d.ts.map +1 -1
  84. package/dist/services/assignment.js +16 -6
  85. package/dist/services/assignment.js.map +1 -1
  86. package/dist/services/attendance.d.ts.map +1 -1
  87. package/dist/services/attendance.js +68 -4
  88. package/dist/services/attendance.js.map +1 -1
  89. package/dist/services/auth.d.ts +1 -0
  90. package/dist/services/auth.d.ts.map +1 -1
  91. package/dist/services/comment.d.ts +0 -3
  92. package/dist/services/comment.d.ts.map +1 -1
  93. package/dist/services/conversation.d.ts +2 -1
  94. package/dist/services/conversation.d.ts.map +1 -1
  95. package/dist/services/labChat.d.ts +10 -2
  96. package/dist/services/labChat.d.ts.map +1 -1
  97. package/dist/services/labChat.js +103 -44
  98. package/dist/services/labChat.js.map +1 -1
  99. package/dist/services/message.d.ts +3 -0
  100. package/dist/services/message.d.ts.map +1 -1
  101. package/dist/services/message.js +20 -2
  102. package/dist/services/message.js.map +1 -1
  103. package/dist/services/newtonChat.d.ts +1 -0
  104. package/dist/services/newtonChat.d.ts.map +1 -1
  105. package/dist/services/newtonChat.js +40 -17
  106. package/dist/services/newtonChat.js.map +1 -1
  107. package/dist/services/school.d.ts +6 -0
  108. package/dist/services/school.d.ts.map +1 -1
  109. package/dist/services/school.js +71 -6
  110. package/dist/services/school.js.map +1 -1
  111. package/dist/services/worksheet.d.ts +48 -11
  112. package/dist/services/worksheet.d.ts.map +1 -1
  113. package/dist/services/worksheet.js +73 -29
  114. package/dist/services/worksheet.js.map +1 -1
  115. package/dist/utils/generateUsername.d.ts +8 -0
  116. package/dist/utils/generateUsername.d.ts.map +1 -0
  117. package/dist/utils/generateUsername.js +103 -0
  118. package/dist/utils/generateUsername.js.map +1 -0
  119. package/dist/utils/inference.d.ts +50 -1
  120. package/dist/utils/inference.d.ts.map +1 -1
  121. package/dist/utils/inference.js +167 -2
  122. package/dist/utils/inference.js.map +1 -1
  123. package/dist/workers/connection.d.ts +3 -0
  124. package/dist/workers/connection.d.ts.map +1 -0
  125. package/dist/workers/connection.js +33 -0
  126. package/dist/workers/connection.js.map +1 -0
  127. package/dist/workers/index.d.ts +10 -0
  128. package/dist/workers/index.d.ts.map +1 -0
  129. package/dist/workers/index.js +63 -0
  130. package/dist/workers/index.js.map +1 -0
  131. package/dist/workers/processors/aiLabChat.d.ts +7 -0
  132. package/dist/workers/processors/aiLabChat.d.ts.map +1 -0
  133. package/dist/workers/processors/aiLabChat.js +52 -0
  134. package/dist/workers/processors/aiLabChat.js.map +1 -0
  135. package/dist/workers/processors/aiNewtonChat.d.ts +7 -0
  136. package/dist/workers/processors/aiNewtonChat.d.ts.map +1 -0
  137. package/dist/workers/processors/aiNewtonChat.js +57 -0
  138. package/dist/workers/processors/aiNewtonChat.js.map +1 -0
  139. package/dist/workers/processors/createRunJobWorker.d.ts +46 -0
  140. package/dist/workers/processors/createRunJobWorker.d.ts.map +1 -0
  141. package/dist/workers/processors/createRunJobWorker.js +118 -0
  142. package/dist/workers/processors/createRunJobWorker.js.map +1 -0
  143. package/dist/workers/processors/gradeWorksheet.d.ts +6 -0
  144. package/dist/workers/processors/gradeWorksheet.d.ts.map +1 -0
  145. package/dist/workers/processors/gradeWorksheet.js +19 -0
  146. package/dist/workers/processors/gradeWorksheet.js.map +1 -0
  147. package/dist/workers/processors/pdfToWorksheet.d.ts +8 -0
  148. package/dist/workers/processors/pdfToWorksheet.d.ts.map +1 -0
  149. package/dist/workers/processors/pdfToWorksheet.js +20 -0
  150. package/dist/workers/processors/pdfToWorksheet.js.map +1 -0
  151. package/dist/workers/processors/studentProgressChat.d.ts +8 -0
  152. package/dist/workers/processors/studentProgressChat.d.ts.map +1 -0
  153. package/dist/workers/processors/studentProgressChat.js +27 -0
  154. package/dist/workers/processors/studentProgressChat.js.map +1 -0
  155. package/dist/workers/queues.d.ts +85 -0
  156. package/dist/workers/queues.d.ts.map +1 -0
  157. package/dist/workers/queues.js +119 -0
  158. package/dist/workers/queues.js.map +1 -0
  159. package/package.json +6 -3
  160. package/prisma/migrations/20260506162500_consolidate_run_job_and_unify_job_status/migration.sql +166 -0
  161. package/prisma/schema.prisma +465 -387
  162. package/src/index.ts +12 -1
  163. package/src/lib/config/cors.ts +0 -1
  164. package/src/lib/fileUpload.ts +2 -4
  165. package/src/lib/pusher.ts +7 -1
  166. package/src/models/attendance.ts +15 -0
  167. package/src/models/auth.ts +1 -0
  168. package/src/models/school.ts +0 -14
  169. package/src/models/worksheet.ts +2 -2
  170. package/src/pipelines/aiLabChat.ts +71 -79
  171. package/src/pipelines/aiNewtonChat.ts +47 -31
  172. package/src/pipelines/gradeWorksheet.ts +63 -25
  173. package/src/pipelines/pdfToWorksheet.ts +272 -34
  174. package/src/routers/assignment.ts +74 -3
  175. package/src/routers/school.ts +5 -0
  176. package/src/routers/worksheet.ts +25 -4
  177. package/src/services/announcement.ts +2 -2
  178. package/src/services/assignment.ts +18 -4
  179. package/src/services/attendance.ts +87 -2
  180. package/src/services/labChat.ts +108 -48
  181. package/src/services/message.ts +19 -0
  182. package/src/services/newtonChat.ts +46 -25
  183. package/src/services/school.ts +89 -6
  184. package/src/services/worksheet.ts +87 -28
  185. package/src/utils/generateUsername.ts +108 -0
  186. package/src/utils/inference.ts +209 -1
  187. package/src/workers/connection.ts +37 -0
  188. package/src/workers/index.ts +71 -0
  189. package/src/workers/processors/aiLabChat.ts +65 -0
  190. package/src/workers/processors/aiNewtonChat.ts +75 -0
  191. package/src/workers/processors/createRunJobWorker.ts +164 -0
  192. package/src/workers/processors/gradeWorksheet.ts +16 -0
  193. package/src/workers/processors/pdfToWorksheet.ts +17 -0
  194. package/src/workers/processors/studentProgressChat.ts +28 -0
  195. package/src/workers/queues.ts +174 -0
  196. package/tests/{globalSetup.ts → integration/globalSetup.ts} +4 -4
  197. package/tests/{helpers.ts → integration/helpers.ts} +2 -2
  198. package/tests/{pipelines → integration/pipelines}/pdfToWorksheet.test.ts +55 -43
  199. package/tests/{routers → integration/routers}/attendance.test.ts +61 -2
  200. package/tests/{routers → integration/routers}/auth.test.ts +3 -3
  201. package/tests/{routers → integration/routers}/class.test.ts +3 -17
  202. package/tests/{routers → integration/routers}/conversation.test.ts +1 -1
  203. package/tests/{routers → integration/routers}/event.test.ts +1 -1
  204. package/tests/{routers → integration/routers}/message.test.ts +1 -1
  205. package/tests/{routers → integration/routers}/newtonChat.test.ts +1 -1
  206. package/tests/{routers → integration/routers}/studentProgress.test.ts +1 -1
  207. package/tests/{user.test.ts → integration/routers/user.test.ts} +2 -2
  208. package/tests/integration/routers/worksheet.test.ts +448 -0
  209. package/tests/{setup.ts → integration/setup.ts} +4 -4
  210. package/tests/{lib → unit/lib}/cors.test.ts +1 -1
  211. package/tests/{middleware → unit/middleware}/security.test.ts +1 -1
  212. package/tests/{pipelines → unit/pipelines}/aiLabChat.test.ts +1 -1
  213. package/tests/{lib → unit/pipelines}/aiLabChatContract.test.ts +1 -1
  214. package/tests/{utils/aiLabChatPrompt.test.ts → unit/pipelines/labChatPrompt.test.ts} +2 -2
  215. package/tests/{utils → unit/services}/studentProgress.test.ts +5 -5
  216. package/tests/{utils → unit/utils}/aiUser.test.ts +1 -1
  217. package/tests/{utils → unit/utils}/generateInviteCode.test.ts +1 -1
  218. package/tests/{utils → unit/utils}/inference.test.ts +6 -6
  219. package/tests/{utils → unit/utils}/logger.test.ts +1 -1
  220. package/tests/{utils → unit/utils}/prismaErrorHandler.test.ts +1 -1
  221. package/tests/{utils → unit/utils}/prismaWrapper.test.ts +1 -1
  222. package/vitest.config.ts +11 -3
  223. package/vitest.unit.config.ts +10 -10
  224. package/tests/routers/worksheet.test.ts +0 -194
  225. package/tests/startup.test.ts +0 -5
  226. package/tests/worksheet.test.ts +0 -181
  227. /package/tests/{routers → integration/routers}/agenda.test.ts +0 -0
  228. /package/tests/{routers → integration/routers}/announcement.test.ts +0 -0
  229. /package/tests/{routers → integration/routers}/assignment.test.ts +0 -0
  230. /package/tests/{routers → integration/routers}/comment.test.ts +0 -0
  231. /package/tests/{routers → integration/routers}/folder.test.ts +0 -0
  232. /package/tests/{routers → integration/routers}/labChat.test.ts +0 -0
  233. /package/tests/{routers → integration/routers}/marketing.test.ts +0 -0
  234. /package/tests/{routers → integration/routers}/notification.test.ts +0 -0
  235. /package/tests/{routers → integration/routers}/search.test.ts +0 -0
  236. /package/tests/{routers → integration/routers}/section.test.ts +0 -0
  237. /package/tests/{server → unit/middleware}/rateLimit.test.ts +0 -0
@@ -1,18 +1,19 @@
1
1
  /**
2
- * `pdfToWorksheet` integration-style tests: real DB, mocked inference.
3
- * CI does not call the model, and the PDF URL is passed through directly.
2
+ * `pdfToWorksheet` integration-style tests: real DB, mocked OpenAI.
3
+ *
4
+ * We intercept the `responses.parse` method on the shared openAIClient
5
+ * instance instead of `vi.mock`ing the inference module: setupFiles loads
6
+ * the inference module (via appRouter) before any test file's vi.mock can
7
+ * register, so module mocks don't intercept those cached imports.
4
8
  */
5
9
  import { describe, test, expect, vi, beforeAll, beforeEach, afterEach } from "vitest";
6
10
  import { user1Caller } from "../setup";
7
- import { prisma } from "../../src/lib/prisma";
8
- import { logger } from "../../src/utils/logger";
11
+ import { prisma } from "../../../src/lib/prisma";
12
+ import { logger } from "../../../src/utils/logger";
13
+ import { openAIClient } from "../../../src/utils/inference.js";
14
+ import { pdfToWorksheet } from "../../../src/pipelines/pdfToWorksheet.js";
9
15
 
10
- vi.mock("../../src/utils/inference.js", () => ({
11
- inference: vi.fn(),
12
- }));
13
-
14
- import { inference } from "../../src/utils/inference.js";
15
- import { pdfToWorksheet } from "../../src/pipelines/pdfToWorksheet.js";
16
+ const mockedResponsesParse = vi.spyOn(openAIClient.responses, "parse");
16
17
 
17
18
  /** Same PDF path represented as a signed Google Cloud Storage URL. */
18
19
  export const CONV_TEST_PDF_GCS_URL =
@@ -68,7 +69,7 @@ describe("pdfToWorksheet", () => {
68
69
  });
69
70
 
70
71
  beforeEach(() => {
71
- vi.mocked(inference).mockReset();
72
+ mockedResponsesParse.mockReset();
72
73
  });
73
74
 
74
75
  afterEach(async () => {
@@ -79,7 +80,7 @@ describe("pdfToWorksheet", () => {
79
80
 
80
81
  test("passes the PDF URL directly to inference and persists a worksheet with 8 questions", async () => {
81
82
  const draft = draftWithEightQuestions();
82
- vi.mocked(inference).mockResolvedValue(draft);
83
+ mockedResponsesParse.mockResolvedValue({ output_parsed: draft } as any);
83
84
 
84
85
  const worksheet = await pdfToWorksheet(CONV_TEST_PDF_GCS_URL, classId);
85
86
  expect(worksheet).not.toBeNull();
@@ -87,6 +88,9 @@ describe("pdfToWorksheet", () => {
87
88
 
88
89
  expect(worksheet!.name).toBe(draft.title);
89
90
  expect(worksheet!.classId).toBe(classId);
91
+ expect(worksheet!.sourceUrl).toBe(
92
+ "https://storage.googleapis.com/studious-test-bucket/worksheets/conv-test.pdf"
93
+ );
90
94
 
91
95
  const questions = await prisma.worksheetQuestion.findMany({
92
96
  where: { worksheetId: worksheet!.id },
@@ -99,27 +103,17 @@ describe("pdfToWorksheet", () => {
99
103
  expect(questions.map((q) => q.order)).toEqual([1, 2, 3, 4, 5, 6, 7, 8]);
100
104
  expect(questions.map((q) => q.points)).toEqual([1, 2, 3, 4, 5, 6, 7, 8]);
101
105
 
102
- expect(inference).toHaveBeenCalledTimes(1);
103
- const input = vi.mocked(inference).mock.calls[0][0];
104
- expect(Array.isArray(input)).toBe(true);
106
+ expect(mockedResponsesParse).toHaveBeenCalledTimes(1);
107
+ const callArgs = mockedResponsesParse.mock.calls[0][0] as { input: unknown };
105
108
  type InputPart = { type?: string; file_url?: string; text?: string };
106
- const content = (input as { role: string; content: InputPart[] }[])[0].content;
109
+ const input = callArgs.input as { role: string; content: InputPart[] }[];
110
+ expect(Array.isArray(input)).toBe(true);
111
+ const content = input[0].content;
107
112
  const filePart = content.find((p) => p.type === "input_file");
108
113
  expect(filePart).toEqual({
109
114
  type: "input_file",
110
115
  file_url: CONV_TEST_PDF_GCS_URL,
111
116
  });
112
- expect(vi.mocked(inference)).toHaveBeenCalledWith(
113
- expect.any(Array),
114
- expect.anything(),
115
- expect.objectContaining({
116
- reason: "pdf_to_worksheet_draft",
117
- metadata: expect.objectContaining({
118
- classId,
119
- filename: "conv-test.pdf",
120
- }),
121
- })
122
- );
123
117
  });
124
118
 
125
119
  test("rejects non-Google Cloud URLs and only logs a sanitized path", async () => {
@@ -133,23 +127,25 @@ describe("pdfToWorksheet", () => {
133
127
  "pdfToWorksheet: rejected unsupported PDF URL",
134
128
  { sanitizedUrl: "https://drive.google.com/file/d/1BfotteM60Ax0krHzE6rFN1Df8S8kT3kb/view" }
135
129
  );
136
- expect(inference).not.toHaveBeenCalled();
130
+ expect(mockedResponsesParse).not.toHaveBeenCalled();
137
131
  warnSpy.mockRestore();
138
132
  });
139
133
 
140
134
  test("rounds persisted integer fields from model output", async () => {
141
- vi.mocked(inference).mockResolvedValue({
142
- title: "Rounded worksheet",
143
- questions: [{
144
- type: "SHORT_ANSWER",
145
- question: "Rounded question",
146
- answer: "Rounded answer",
147
- options: [],
148
- markScheme: [{ id: "step-1", points: 0.6, description: "Half step" }],
149
- points: 1.7,
150
- order: 1.2,
151
- }],
152
- } as Awaited<ReturnType<typeof inference>>);
135
+ mockedResponsesParse.mockResolvedValue({
136
+ output_parsed: {
137
+ title: "Rounded worksheet",
138
+ questions: [{
139
+ type: "SHORT_ANSWER",
140
+ question: "Rounded question",
141
+ answer: "Rounded answer",
142
+ options: [],
143
+ markScheme: [{ id: "step-1", points: 0.6, description: "Half step" }],
144
+ points: 1.7,
145
+ order: 1.2,
146
+ }],
147
+ },
148
+ } as any);
153
149
 
154
150
  const worksheet = await pdfToWorksheet(CONV_TEST_PDF_GCS_URL, classId);
155
151
  expect(worksheet).not.toBeNull();
@@ -165,12 +161,28 @@ describe("pdfToWorksheet", () => {
165
161
  });
166
162
 
167
163
  test("returns null when inference yields no parsed output", async () => {
168
- vi.mocked(inference).mockResolvedValue(
169
- null as unknown as Awaited<ReturnType<typeof inference>>
170
- );
164
+ mockedResponsesParse.mockResolvedValue({ output_parsed: null } as any);
171
165
 
172
166
  const out = await pdfToWorksheet(CONV_TEST_PDF_GCS_URL, classId);
173
167
  expect(out).toBeNull();
174
168
  });
175
169
 
170
+ test("reuses an existing worksheet for the same PDF source", async () => {
171
+ mockedResponsesParse.mockResolvedValue({
172
+ output_parsed: draftWithEightQuestions("Idempotent worksheet"),
173
+ } as any);
174
+
175
+ const first = await pdfToWorksheet(CONV_TEST_PDF_GCS_URL, classId);
176
+ expect(first).not.toBeNull();
177
+ createdWorksheetIds.push(first!.id);
178
+
179
+ const second = await pdfToWorksheet(
180
+ "https://storage.googleapis.com/studious-test-bucket/worksheets/conv-test.pdf?X-Goog-Signature=retry-signature",
181
+ classId
182
+ );
183
+
184
+ expect(second?.id).toBe(first!.id);
185
+ expect(mockedResponsesParse).toHaveBeenCalledTimes(1);
186
+ });
187
+
176
188
  });
@@ -1,7 +1,7 @@
1
1
  import { test, expect, describe, beforeEach } from 'vitest';
2
2
  import { user1Caller, user2Caller } from '../setup';
3
3
  import { expectUnauth } from '../helpers';
4
- import { prisma } from '../../src/lib/prisma';
4
+ import { prisma } from '../../../src/lib/prisma';
5
5
 
6
6
  describe('Attendance Router', () => {
7
7
  let testClass: any;
@@ -117,6 +117,66 @@ describe('Attendance Router', () => {
117
117
  expect(updatedRecord?.late.some((l: any) => l.id === student2.id)).toBe(true);
118
118
  });
119
119
 
120
+ test('should notify students newly marked late or absent', async () => {
121
+ await prisma.notification.deleteMany({
122
+ where: { receiverId: student2.id },
123
+ });
124
+
125
+ await user1Caller.attendance.update({
126
+ classId: testClass.id,
127
+ eventId: testEvent.event.id,
128
+ attendance: {
129
+ present: [{ id: student1.id, username: student1.username }],
130
+ late: [{ id: student2.id, username: student2.username }],
131
+ absent: [],
132
+ },
133
+ });
134
+
135
+ let notifications = await user2Caller.notification.list();
136
+ expect(
137
+ notifications.some(
138
+ (notification: any) =>
139
+ notification.title === `Marked late in ${testClass.name}` &&
140
+ notification.content.includes(testEvent.event.name) &&
141
+ notification.content.includes(`/class/${testClass.id}/attendance/${testEvent.event.id}`)
142
+ )
143
+ ).toBe(true);
144
+
145
+ await user1Caller.attendance.update({
146
+ classId: testClass.id,
147
+ eventId: testEvent.event.id,
148
+ attendance: {
149
+ present: [{ id: student1.id, username: student1.username }],
150
+ late: [{ id: student2.id, username: student2.username }],
151
+ absent: [],
152
+ },
153
+ });
154
+
155
+ notifications = await user2Caller.notification.list();
156
+ expect(
157
+ notifications.filter(
158
+ (notification: any) => notification.title === `Marked late in ${testClass.name}`
159
+ )
160
+ ).toHaveLength(1);
161
+
162
+ await user1Caller.attendance.update({
163
+ classId: testClass.id,
164
+ eventId: testEvent.event.id,
165
+ attendance: {
166
+ present: [{ id: student1.id, username: student1.username }],
167
+ late: [],
168
+ absent: [{ id: student2.id, username: student2.username }],
169
+ },
170
+ });
171
+
172
+ notifications = await user2Caller.notification.list();
173
+ expect(
174
+ notifications.some(
175
+ (notification: any) => notification.title === `Marked absent in ${testClass.name}`
176
+ )
177
+ ).toBe(true);
178
+ });
179
+
120
180
  test('should update attendance without event', async () => {
121
181
  const result = await user1Caller.attendance.update({
122
182
  classId: testClass.id,
@@ -157,4 +217,3 @@ describe('Attendance Router', () => {
157
217
  })));
158
218
  });
159
219
  });
160
-
@@ -3,9 +3,9 @@
3
3
  import { randomUUID } from 'crypto';
4
4
  import { SessionType } from '@prisma/client';
5
5
  import { test, expect, describe } from 'vitest';
6
- import { appRouter } from '../../src/routers/_app';
7
- import { createTRPCContext } from '../../src/trpc';
8
- import { prisma } from '../../src/lib/prisma';
6
+ import { appRouter } from '../../../src/routers/_app';
7
+ import { createTRPCContext } from '../../../src/trpc';
8
+ import { prisma } from '../../../src/lib/prisma';
9
9
  import {
10
10
  caller,
11
11
  login1,
@@ -1,10 +1,10 @@
1
1
  import { test, expect, describe, beforeEach, afterEach, vi } from 'vitest';
2
2
  import { expectForbidden, expectRejects, expectUnauth } from '../helpers';
3
- import { prisma } from '../../src/lib/prisma';
3
+ import { prisma } from '../../../src/lib/prisma';
4
4
  import { caller, user1Caller, user2Caller, user3Caller } from '../setup';
5
5
 
6
- vi.mock('../../src/lib/googleCloudStorage.js', async (importOriginal) => {
7
- const actual = await importOriginal<typeof import('../../src/lib/googleCloudStorage.js')>();
6
+ vi.mock('../../../src/lib/googleCloudStorage.js', async (importOriginal) => {
7
+ const actual = await importOriginal<typeof import('../../../src/lib/googleCloudStorage.js')>();
8
8
  return {
9
9
  ...actual,
10
10
  copyFile: vi.fn().mockResolvedValue(undefined),
@@ -340,19 +340,5 @@ describe('Class Router', () => {
340
340
  expect(rootFolder.name).toBe('Class Files');
341
341
  });
342
342
 
343
-
344
- test('non-teacher cannot import class', async () => {
345
- await user1Caller.folder.getRootFolder({ classId: testClass.id });
346
- const exported = await user1Caller.class.exportClass({ classId: testClass.id });
347
- const newClassId = `import-forbidden-${Date.now()}`;
348
-
349
- await expect(
350
- user3Caller.class.importClass({
351
- classId: newClassId,
352
- year: new Date().getFullYear(),
353
- classData: exported,
354
- })
355
- ).rejects.toThrow();
356
- });
357
343
  });
358
344
  });
@@ -1,5 +1,5 @@
1
1
  import { test, expect, describe, beforeAll } from 'vitest';
2
- import { prisma } from '../../src/lib/prisma';
2
+ import { prisma } from '../../../src/lib/prisma';
3
3
  import { user1Caller, user2Caller, user3Caller } from '../setup';
4
4
  import { expectUnauth } from '../helpers';
5
5
 
@@ -1,7 +1,7 @@
1
1
  import { test, expect, describe, beforeEach } from 'vitest';
2
2
  import { user1Caller, user2Caller } from '../setup';
3
3
  import { expectRejects, expectUnauth } from '../helpers';
4
- import { prisma } from '../../src/lib/prisma';
4
+ import { prisma } from '../../../src/lib/prisma';
5
5
  import { startOfWeek } from 'date-fns';
6
6
 
7
7
  describe('Event Router', () => {
@@ -1,6 +1,6 @@
1
1
  import { test, expect, describe, beforeAll } from 'vitest';
2
2
  import { user1Caller, user2Caller, user3Caller } from '../setup';
3
- import { prisma } from '../../src/lib/prisma';
3
+ import { prisma } from '../../../src/lib/prisma';
4
4
 
5
5
  const expectTRPCError = async (promise: Promise<unknown>, code: string) => {
6
6
  await expect(promise).rejects.toMatchObject({ code });
@@ -1,5 +1,5 @@
1
1
  import { beforeAll, describe, expect, test } from 'vitest';
2
- import { prisma } from '../../src/lib/prisma';
2
+ import { prisma } from '../../../src/lib/prisma';
3
3
  import { user1Caller, user2Caller } from '../setup';
4
4
 
5
5
  describe('Newton Chat Router', () => {
@@ -1,5 +1,5 @@
1
1
  import { test, expect, describe, beforeEach, vi } from "vitest";
2
- import { openAIClient } from "../../src/utils/inference.js";
2
+ import { openAIClient } from "../../../src/utils/inference.js";
3
3
  import { user1Caller, user2Caller, user3Caller } from "../setup";
4
4
 
5
5
  const mockedCompletionCreate = vi.spyOn(
@@ -1,6 +1,6 @@
1
1
  import { test, expect, describe, beforeEach } from 'vitest';
2
- import { user1Caller, user2Caller } from './setup';
3
- import { expectUnauth } from './helpers';
2
+ import { user1Caller, user2Caller } from '../setup';
3
+ import { expectUnauth } from '../helpers';
4
4
 
5
5
  describe('User Router', () => {
6
6
  describe('getProfile', () => {