@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.
- package/dist/index.js +15 -3
- package/dist/index.js.map +1 -1
- package/dist/lib/config/cors.d.ts.map +1 -1
- package/dist/lib/config/cors.js +2 -3
- package/dist/lib/config/cors.js.map +1 -1
- package/dist/lib/fileUpload.d.ts.map +1 -1
- package/dist/lib/fileUpload.js +4 -5
- package/dist/lib/fileUpload.js.map +1 -1
- package/dist/lib/pusher.d.ts +7 -1
- package/dist/lib/pusher.d.ts.map +1 -1
- package/dist/lib/pusher.js +9 -3
- package/dist/lib/pusher.js.map +1 -1
- package/dist/models/attendance.d.ts +16 -7
- package/dist/models/attendance.d.ts.map +1 -1
- package/dist/models/attendance.js +17 -2
- package/dist/models/attendance.js.map +1 -1
- package/dist/models/auth.d.ts +1 -0
- package/dist/models/auth.d.ts.map +1 -1
- package/dist/models/auth.js +3 -2
- package/dist/models/auth.js.map +1 -1
- package/dist/models/comment.d.ts +0 -5
- package/dist/models/comment.d.ts.map +1 -1
- package/dist/models/conversation.d.ts +17 -16
- package/dist/models/conversation.d.ts.map +1 -1
- package/dist/models/labChat.d.ts +2 -1
- package/dist/models/labChat.d.ts.map +1 -1
- package/dist/models/message.d.ts +6 -3
- package/dist/models/message.d.ts.map +1 -1
- package/dist/models/school.d.ts +0 -22
- package/dist/models/school.d.ts.map +1 -1
- package/dist/models/school.js +2 -14
- package/dist/models/school.js.map +1 -1
- package/dist/models/worksheet.d.ts +76 -19
- package/dist/models/worksheet.d.ts.map +1 -1
- package/dist/models/worksheet.js +2 -2
- package/dist/models/worksheet.js.map +1 -1
- package/dist/pipelines/aiLabChat.d.ts +20 -9
- package/dist/pipelines/aiLabChat.d.ts.map +1 -1
- package/dist/pipelines/aiLabChat.js +63 -75
- package/dist/pipelines/aiLabChat.js.map +1 -1
- package/dist/pipelines/aiNewtonChat.d.ts +13 -4
- package/dist/pipelines/aiNewtonChat.d.ts.map +1 -1
- package/dist/pipelines/aiNewtonChat.js +39 -27
- package/dist/pipelines/aiNewtonChat.js.map +1 -1
- package/dist/pipelines/gradeWorksheet.d.ts +5 -3
- package/dist/pipelines/gradeWorksheet.d.ts.map +1 -1
- package/dist/pipelines/gradeWorksheet.js +60 -24
- package/dist/pipelines/gradeWorksheet.js.map +1 -1
- package/dist/pipelines/pdfToWorksheet.d.ts +34 -2
- package/dist/pipelines/pdfToWorksheet.d.ts.map +1 -1
- package/dist/pipelines/pdfToWorksheet.js +205 -32
- package/dist/pipelines/pdfToWorksheet.js.map +1 -1
- package/dist/routers/_app.d.ts +106 -48
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/announcement.d.ts +0 -4
- package/dist/routers/announcement.d.ts.map +1 -1
- package/dist/routers/assignment.d.ts +0 -8
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/assignment.js +64 -4
- package/dist/routers/assignment.js.map +1 -1
- package/dist/routers/auth.d.ts +1 -0
- package/dist/routers/auth.d.ts.map +1 -1
- package/dist/routers/comment.d.ts +0 -3
- package/dist/routers/comment.d.ts.map +1 -1
- package/dist/routers/conversation.d.ts +2 -1
- package/dist/routers/conversation.d.ts.map +1 -1
- package/dist/routers/labChat.d.ts +3 -1
- package/dist/routers/labChat.d.ts.map +1 -1
- package/dist/routers/message.d.ts +3 -0
- package/dist/routers/message.d.ts.map +1 -1
- package/dist/routers/newtonChat.d.ts +1 -0
- package/dist/routers/newtonChat.d.ts.map +1 -1
- package/dist/routers/school.d.ts +6 -0
- package/dist/routers/school.d.ts.map +1 -1
- package/dist/routers/school.js +7 -2
- package/dist/routers/school.js.map +1 -1
- package/dist/routers/worksheet.d.ts +37 -7
- package/dist/routers/worksheet.d.ts.map +1 -1
- package/dist/routers/worksheet.js +16 -6
- package/dist/routers/worksheet.js.map +1 -1
- package/dist/services/announcement.js +4 -4
- package/dist/services/announcement.js.map +1 -1
- package/dist/services/assignment.d.ts.map +1 -1
- package/dist/services/assignment.js +16 -6
- package/dist/services/assignment.js.map +1 -1
- package/dist/services/attendance.d.ts.map +1 -1
- package/dist/services/attendance.js +68 -4
- package/dist/services/attendance.js.map +1 -1
- package/dist/services/auth.d.ts +1 -0
- package/dist/services/auth.d.ts.map +1 -1
- package/dist/services/comment.d.ts +0 -3
- package/dist/services/comment.d.ts.map +1 -1
- package/dist/services/conversation.d.ts +2 -1
- package/dist/services/conversation.d.ts.map +1 -1
- package/dist/services/labChat.d.ts +10 -2
- package/dist/services/labChat.d.ts.map +1 -1
- package/dist/services/labChat.js +103 -44
- package/dist/services/labChat.js.map +1 -1
- package/dist/services/message.d.ts +3 -0
- package/dist/services/message.d.ts.map +1 -1
- package/dist/services/message.js +20 -2
- package/dist/services/message.js.map +1 -1
- package/dist/services/newtonChat.d.ts +1 -0
- package/dist/services/newtonChat.d.ts.map +1 -1
- package/dist/services/newtonChat.js +40 -17
- package/dist/services/newtonChat.js.map +1 -1
- package/dist/services/school.d.ts +6 -0
- package/dist/services/school.d.ts.map +1 -1
- package/dist/services/school.js +71 -6
- package/dist/services/school.js.map +1 -1
- package/dist/services/worksheet.d.ts +48 -11
- package/dist/services/worksheet.d.ts.map +1 -1
- package/dist/services/worksheet.js +73 -29
- package/dist/services/worksheet.js.map +1 -1
- package/dist/utils/generateUsername.d.ts +8 -0
- package/dist/utils/generateUsername.d.ts.map +1 -0
- package/dist/utils/generateUsername.js +103 -0
- package/dist/utils/generateUsername.js.map +1 -0
- package/dist/utils/inference.d.ts +50 -1
- package/dist/utils/inference.d.ts.map +1 -1
- package/dist/utils/inference.js +167 -2
- package/dist/utils/inference.js.map +1 -1
- package/dist/workers/connection.d.ts +3 -0
- package/dist/workers/connection.d.ts.map +1 -0
- package/dist/workers/connection.js +33 -0
- package/dist/workers/connection.js.map +1 -0
- package/dist/workers/index.d.ts +10 -0
- package/dist/workers/index.d.ts.map +1 -0
- package/dist/workers/index.js +63 -0
- package/dist/workers/index.js.map +1 -0
- package/dist/workers/processors/aiLabChat.d.ts +7 -0
- package/dist/workers/processors/aiLabChat.d.ts.map +1 -0
- package/dist/workers/processors/aiLabChat.js +52 -0
- package/dist/workers/processors/aiLabChat.js.map +1 -0
- package/dist/workers/processors/aiNewtonChat.d.ts +7 -0
- package/dist/workers/processors/aiNewtonChat.d.ts.map +1 -0
- package/dist/workers/processors/aiNewtonChat.js +57 -0
- package/dist/workers/processors/aiNewtonChat.js.map +1 -0
- package/dist/workers/processors/createRunJobWorker.d.ts +46 -0
- package/dist/workers/processors/createRunJobWorker.d.ts.map +1 -0
- package/dist/workers/processors/createRunJobWorker.js +118 -0
- package/dist/workers/processors/createRunJobWorker.js.map +1 -0
- package/dist/workers/processors/gradeWorksheet.d.ts +6 -0
- package/dist/workers/processors/gradeWorksheet.d.ts.map +1 -0
- package/dist/workers/processors/gradeWorksheet.js +19 -0
- package/dist/workers/processors/gradeWorksheet.js.map +1 -0
- package/dist/workers/processors/pdfToWorksheet.d.ts +8 -0
- package/dist/workers/processors/pdfToWorksheet.d.ts.map +1 -0
- package/dist/workers/processors/pdfToWorksheet.js +20 -0
- package/dist/workers/processors/pdfToWorksheet.js.map +1 -0
- package/dist/workers/processors/studentProgressChat.d.ts +8 -0
- package/dist/workers/processors/studentProgressChat.d.ts.map +1 -0
- package/dist/workers/processors/studentProgressChat.js +27 -0
- package/dist/workers/processors/studentProgressChat.js.map +1 -0
- package/dist/workers/queues.d.ts +85 -0
- package/dist/workers/queues.d.ts.map +1 -0
- package/dist/workers/queues.js +119 -0
- package/dist/workers/queues.js.map +1 -0
- package/package.json +6 -3
- package/prisma/migrations/20260506162500_consolidate_run_job_and_unify_job_status/migration.sql +166 -0
- package/prisma/schema.prisma +465 -387
- package/src/index.ts +12 -1
- package/src/lib/config/cors.ts +0 -1
- package/src/lib/fileUpload.ts +2 -4
- package/src/lib/pusher.ts +7 -1
- package/src/models/attendance.ts +15 -0
- package/src/models/auth.ts +1 -0
- package/src/models/school.ts +0 -14
- package/src/models/worksheet.ts +2 -2
- package/src/pipelines/aiLabChat.ts +71 -79
- package/src/pipelines/aiNewtonChat.ts +47 -31
- package/src/pipelines/gradeWorksheet.ts +63 -25
- package/src/pipelines/pdfToWorksheet.ts +272 -34
- package/src/routers/assignment.ts +74 -3
- package/src/routers/school.ts +5 -0
- package/src/routers/worksheet.ts +25 -4
- package/src/services/announcement.ts +2 -2
- package/src/services/assignment.ts +18 -4
- package/src/services/attendance.ts +87 -2
- package/src/services/labChat.ts +108 -48
- package/src/services/message.ts +19 -0
- package/src/services/newtonChat.ts +46 -25
- package/src/services/school.ts +89 -6
- package/src/services/worksheet.ts +87 -28
- package/src/utils/generateUsername.ts +108 -0
- package/src/utils/inference.ts +209 -1
- package/src/workers/connection.ts +37 -0
- package/src/workers/index.ts +71 -0
- package/src/workers/processors/aiLabChat.ts +65 -0
- package/src/workers/processors/aiNewtonChat.ts +75 -0
- package/src/workers/processors/createRunJobWorker.ts +164 -0
- package/src/workers/processors/gradeWorksheet.ts +16 -0
- package/src/workers/processors/pdfToWorksheet.ts +17 -0
- package/src/workers/processors/studentProgressChat.ts +28 -0
- package/src/workers/queues.ts +174 -0
- package/tests/{globalSetup.ts → integration/globalSetup.ts} +4 -4
- package/tests/{helpers.ts → integration/helpers.ts} +2 -2
- package/tests/{pipelines → integration/pipelines}/pdfToWorksheet.test.ts +55 -43
- package/tests/{routers → integration/routers}/attendance.test.ts +61 -2
- package/tests/{routers → integration/routers}/auth.test.ts +3 -3
- package/tests/{routers → integration/routers}/class.test.ts +3 -17
- package/tests/{routers → integration/routers}/conversation.test.ts +1 -1
- package/tests/{routers → integration/routers}/event.test.ts +1 -1
- package/tests/{routers → integration/routers}/message.test.ts +1 -1
- package/tests/{routers → integration/routers}/newtonChat.test.ts +1 -1
- package/tests/{routers → integration/routers}/studentProgress.test.ts +1 -1
- package/tests/{user.test.ts → integration/routers/user.test.ts} +2 -2
- package/tests/integration/routers/worksheet.test.ts +448 -0
- package/tests/{setup.ts → integration/setup.ts} +4 -4
- package/tests/{lib → unit/lib}/cors.test.ts +1 -1
- package/tests/{middleware → unit/middleware}/security.test.ts +1 -1
- package/tests/{pipelines → unit/pipelines}/aiLabChat.test.ts +1 -1
- package/tests/{lib → unit/pipelines}/aiLabChatContract.test.ts +1 -1
- package/tests/{utils/aiLabChatPrompt.test.ts → unit/pipelines/labChatPrompt.test.ts} +2 -2
- package/tests/{utils → unit/services}/studentProgress.test.ts +5 -5
- package/tests/{utils → unit/utils}/aiUser.test.ts +1 -1
- package/tests/{utils → unit/utils}/generateInviteCode.test.ts +1 -1
- package/tests/{utils → unit/utils}/inference.test.ts +6 -6
- package/tests/{utils → unit/utils}/logger.test.ts +1 -1
- package/tests/{utils → unit/utils}/prismaErrorHandler.test.ts +1 -1
- package/tests/{utils → unit/utils}/prismaWrapper.test.ts +1 -1
- package/vitest.config.ts +11 -3
- package/vitest.unit.config.ts +10 -10
- package/tests/routers/worksheet.test.ts +0 -194
- package/tests/startup.test.ts +0 -5
- package/tests/worksheet.test.ts +0 -181
- /package/tests/{routers → integration/routers}/agenda.test.ts +0 -0
- /package/tests/{routers → integration/routers}/announcement.test.ts +0 -0
- /package/tests/{routers → integration/routers}/assignment.test.ts +0 -0
- /package/tests/{routers → integration/routers}/comment.test.ts +0 -0
- /package/tests/{routers → integration/routers}/folder.test.ts +0 -0
- /package/tests/{routers → integration/routers}/labChat.test.ts +0 -0
- /package/tests/{routers → integration/routers}/marketing.test.ts +0 -0
- /package/tests/{routers → integration/routers}/notification.test.ts +0 -0
- /package/tests/{routers → integration/routers}/search.test.ts +0 -0
- /package/tests/{routers → integration/routers}/section.test.ts +0 -0
- /package/tests/{server → unit/middleware}/rateLimit.test.ts +0 -0
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeAll, 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", () => {
|
|
9
|
+
let crudClass: any;
|
|
10
|
+
const createdWorksheetIds = new Set<string>();
|
|
11
|
+
|
|
12
|
+
beforeAll(async () => {
|
|
13
|
+
crudClass = await user1Caller.class.create({
|
|
14
|
+
name: "Worksheet Test Class",
|
|
15
|
+
subject: "Physics",
|
|
16
|
+
section: "12th Grade",
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(async () => {
|
|
21
|
+
for (const worksheetId of Array.from(createdWorksheetIds)) {
|
|
22
|
+
await user1Caller.worksheet.deleteWorksheet({ worksheetId }).catch(() => undefined);
|
|
23
|
+
createdWorksheetIds.delete(worksheetId);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterAll(async () => {
|
|
28
|
+
if (!crudClass?.id) return;
|
|
29
|
+
|
|
30
|
+
await user1Caller.class.delete({
|
|
31
|
+
classId: crudClass.id,
|
|
32
|
+
id: crudClass.id,
|
|
33
|
+
}).catch(() => undefined);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const createCrudWorksheet = async (name = "Chapter 1 Quiz") => {
|
|
37
|
+
const worksheet = await user1Caller.worksheet.create({
|
|
38
|
+
classId: crudClass.id,
|
|
39
|
+
name,
|
|
40
|
+
});
|
|
41
|
+
createdWorksheetIds.add(worksheet.id);
|
|
42
|
+
return worksheet;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const addShortAnswerQuestion = (worksheetId: string) =>
|
|
46
|
+
user1Caller.worksheet.addQuestion({
|
|
47
|
+
worksheetId,
|
|
48
|
+
question: "What is Newton's second law?",
|
|
49
|
+
answer: "F = ma",
|
|
50
|
+
points: 5,
|
|
51
|
+
type: "SHORT_ANSWER",
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const addMultipleChoiceQuestion = (worksheetId: string) =>
|
|
55
|
+
user1Caller.worksheet.addQuestion({
|
|
56
|
+
worksheetId,
|
|
57
|
+
question: "What is the SI unit of force?",
|
|
58
|
+
answer: "Newton",
|
|
59
|
+
points: 3,
|
|
60
|
+
type: "MULTIPLE_CHOICE",
|
|
61
|
+
options: [
|
|
62
|
+
{ id: "a", text: "Joule", isCorrect: false },
|
|
63
|
+
{ id: "b", text: "Newton", isCorrect: true },
|
|
64
|
+
{ id: "c", text: "Watt", isCorrect: false },
|
|
65
|
+
{ id: "d", text: "Pascal", isCorrect: false },
|
|
66
|
+
],
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("create", () => {
|
|
70
|
+
test("should create a worksheet", async () => {
|
|
71
|
+
const worksheet = await createCrudWorksheet();
|
|
72
|
+
|
|
73
|
+
expect(worksheet).toBeDefined();
|
|
74
|
+
expect(worksheet.name).toBe("Chapter 1 Quiz");
|
|
75
|
+
expect(worksheet.classId).toBe(crudClass.id);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("listWorksheets", () => {
|
|
80
|
+
test("should list worksheets for a class", async () => {
|
|
81
|
+
const worksheet = await createCrudWorksheet();
|
|
82
|
+
|
|
83
|
+
const worksheets = await user1Caller.worksheet.listWorksheets({
|
|
84
|
+
classId: crudClass.id,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(Array.isArray(worksheets)).toBe(true);
|
|
88
|
+
expect(worksheets.length).toBeGreaterThanOrEqual(1);
|
|
89
|
+
expect(worksheets.some((w) => w.id === worksheet.id)).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("getWorksheet", () => {
|
|
94
|
+
test("should get a worksheet by ID", async () => {
|
|
95
|
+
const created = await createCrudWorksheet();
|
|
96
|
+
|
|
97
|
+
const worksheet = await user1Caller.worksheet.getWorksheet({
|
|
98
|
+
worksheetId: created.id,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
expect(worksheet).toBeDefined();
|
|
102
|
+
expect(worksheet.id).toBe(created.id);
|
|
103
|
+
expect(worksheet.name).toBe("Chapter 1 Quiz");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("should fail for non-existent worksheet", async () => {
|
|
107
|
+
await expect(
|
|
108
|
+
user1Caller.worksheet.getWorksheet({ worksheetId: "nonexistent" }),
|
|
109
|
+
).rejects.toThrow();
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("updateWorksheet", () => {
|
|
114
|
+
test("should update worksheet name", async () => {
|
|
115
|
+
const worksheet = await createCrudWorksheet();
|
|
116
|
+
|
|
117
|
+
const updated = await user1Caller.worksheet.updateWorksheet({
|
|
118
|
+
worksheetId: worksheet.id,
|
|
119
|
+
name: "Chapter 1 Test",
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
expect(updated.name).toBe("Chapter 1 Test");
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("addQuestion", () => {
|
|
127
|
+
test("should add a short answer question", async () => {
|
|
128
|
+
const worksheet = await createCrudWorksheet();
|
|
129
|
+
const question = await addShortAnswerQuestion(worksheet.id);
|
|
130
|
+
|
|
131
|
+
expect(question).toBeDefined();
|
|
132
|
+
expect(question.question).toBe("What is Newton's second law?");
|
|
133
|
+
expect(question.answer).toBe("F = ma");
|
|
134
|
+
expect(question.points).toBe(5);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("should add a multiple choice question", async () => {
|
|
138
|
+
const worksheet = await createCrudWorksheet();
|
|
139
|
+
const question = await addMultipleChoiceQuestion(worksheet.id);
|
|
140
|
+
|
|
141
|
+
expect(question).toBeDefined();
|
|
142
|
+
expect(question.type).toBe("MULTIPLE_CHOICE");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("should fail for non-existent worksheet", async () => {
|
|
146
|
+
await expect(
|
|
147
|
+
user1Caller.worksheet.addQuestion({
|
|
148
|
+
worksheetId: "nonexistent",
|
|
149
|
+
question: "Test",
|
|
150
|
+
answer: "Test",
|
|
151
|
+
type: "SHORT_ANSWER",
|
|
152
|
+
}),
|
|
153
|
+
).rejects.toThrow();
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe("updateQuestion", () => {
|
|
158
|
+
test("should update a question", async () => {
|
|
159
|
+
const worksheet = await createCrudWorksheet();
|
|
160
|
+
const question = await addShortAnswerQuestion(worksheet.id);
|
|
161
|
+
|
|
162
|
+
const updated = await user1Caller.worksheet.updateQuestion({
|
|
163
|
+
worksheetId: worksheet.id,
|
|
164
|
+
questionId: question.id,
|
|
165
|
+
question: "State Newton's second law of motion.",
|
|
166
|
+
points: 10,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
expect(updated.question).toBe("State Newton's second law of motion.");
|
|
170
|
+
expect(updated.points).toBe(10);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe("reorderQuestions", () => {
|
|
175
|
+
test("should reorder questions", async () => {
|
|
176
|
+
const worksheet = await createCrudWorksheet();
|
|
177
|
+
const question = await addShortAnswerQuestion(worksheet.id);
|
|
178
|
+
const secondQuestion = await addMultipleChoiceQuestion(worksheet.id);
|
|
179
|
+
|
|
180
|
+
const result = await user1Caller.worksheet.reorderQuestions({
|
|
181
|
+
worksheetId: worksheet.id,
|
|
182
|
+
movedId: secondQuestion.id,
|
|
183
|
+
position: "before",
|
|
184
|
+
targetId: question.id,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
expect(Array.isArray(result)).toBe(true);
|
|
188
|
+
expect(result.length).toBe(2);
|
|
189
|
+
expect(result.map((r) => r.id)).toEqual([
|
|
190
|
+
secondQuestion.id,
|
|
191
|
+
question.id,
|
|
192
|
+
]);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("should reject moving a question relative to itself", async () => {
|
|
196
|
+
const worksheet = await createCrudWorksheet();
|
|
197
|
+
const question = await addShortAnswerQuestion(worksheet.id);
|
|
198
|
+
|
|
199
|
+
await expectTRPCError(
|
|
200
|
+
user1Caller.worksheet.reorderQuestions({
|
|
201
|
+
worksheetId: worksheet.id,
|
|
202
|
+
movedId: question.id,
|
|
203
|
+
position: "before",
|
|
204
|
+
targetId: question.id,
|
|
205
|
+
}),
|
|
206
|
+
"BAD_REQUEST",
|
|
207
|
+
);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe("deleteQuestion", () => {
|
|
212
|
+
test("should delete a question", async () => {
|
|
213
|
+
const worksheet = await createCrudWorksheet();
|
|
214
|
+
await addShortAnswerQuestion(worksheet.id);
|
|
215
|
+
const secondQuestion = await addMultipleChoiceQuestion(worksheet.id);
|
|
216
|
+
|
|
217
|
+
const result = await user1Caller.worksheet.deleteQuestion({
|
|
218
|
+
worksheetId: worksheet.id,
|
|
219
|
+
questionId: secondQuestion.id,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
expect(result.id).toBe(secondQuestion.id);
|
|
223
|
+
|
|
224
|
+
const updatedWorksheet = await user1Caller.worksheet.getWorksheet({
|
|
225
|
+
worksheetId: worksheet.id,
|
|
226
|
+
});
|
|
227
|
+
expect(updatedWorksheet.questions.length).toBe(1);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe("deleteWorksheet", () => {
|
|
232
|
+
test("should delete a worksheet", async () => {
|
|
233
|
+
const toDelete = await createCrudWorksheet("Temporary Worksheet");
|
|
234
|
+
|
|
235
|
+
const result = await user1Caller.worksheet.deleteWorksheet({
|
|
236
|
+
worksheetId: toDelete.id,
|
|
237
|
+
});
|
|
238
|
+
createdWorksheetIds.delete(toDelete.id);
|
|
239
|
+
|
|
240
|
+
expect(result.id).toBe(toDelete.id);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe("Worksheet Router Security", () => {
|
|
246
|
+
let testClass: any;
|
|
247
|
+
let worksheet: any;
|
|
248
|
+
let question: any;
|
|
249
|
+
let multipleChoiceQuestion: any;
|
|
250
|
+
let submission: any;
|
|
251
|
+
let worksheetResponse: any;
|
|
252
|
+
let progressId: string;
|
|
253
|
+
|
|
254
|
+
beforeEach(async () => {
|
|
255
|
+
testClass = await user1Caller.class.create({
|
|
256
|
+
name: `Worksheet Security ${Date.now()}`,
|
|
257
|
+
subject: "Physics",
|
|
258
|
+
section: "12th Grade",
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const user2Profile = await user2Caller.user.getProfile();
|
|
262
|
+
await user1Caller.class.addStudent({
|
|
263
|
+
classId: testClass.id,
|
|
264
|
+
studentId: user2Profile.id,
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
worksheet = await user1Caller.worksheet.create({
|
|
268
|
+
classId: testClass.id,
|
|
269
|
+
name: "Security Worksheet",
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
question = await user1Caller.worksheet.addQuestion({
|
|
273
|
+
worksheetId: worksheet.id,
|
|
274
|
+
question: "What is force?",
|
|
275
|
+
answer: "Mass times acceleration",
|
|
276
|
+
points: 5,
|
|
277
|
+
type: "SHORT_ANSWER",
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
multipleChoiceQuestion = await user1Caller.worksheet.addQuestion({
|
|
281
|
+
worksheetId: worksheet.id,
|
|
282
|
+
question: "What is the SI unit of force?",
|
|
283
|
+
answer: "newton",
|
|
284
|
+
points: 1,
|
|
285
|
+
type: "MULTIPLE_CHOICE",
|
|
286
|
+
options: [
|
|
287
|
+
{ id: "a", text: "Joule", isCorrect: false },
|
|
288
|
+
{ id: "b", text: "Newton", isCorrect: true },
|
|
289
|
+
],
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const dueDate = new Date();
|
|
293
|
+
dueDate.setDate(dueDate.getDate() + 7);
|
|
294
|
+
|
|
295
|
+
const assignment = await user1Caller.assignment.create({
|
|
296
|
+
classId: testClass.id,
|
|
297
|
+
title: "Worksheet Assignment",
|
|
298
|
+
instructions: "Complete the worksheet.",
|
|
299
|
+
dueDate: dueDate.toISOString(),
|
|
300
|
+
acceptWorksheet: true,
|
|
301
|
+
gradeWithAI: true,
|
|
302
|
+
worksheetIds: [worksheet.id],
|
|
303
|
+
type: "HOMEWORK",
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
submission = await user2Caller.assignment.getSubmission({
|
|
307
|
+
classId: testClass.id,
|
|
308
|
+
assignmentId: assignment.id,
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
worksheetResponse = await user2Caller.worksheet.getWorksheetSubmission({
|
|
312
|
+
worksheetId: worksheet.id,
|
|
313
|
+
submissionId: submission.id,
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
const answered = await user2Caller.worksheet.answerQuestion({
|
|
317
|
+
worksheetResponseId: worksheetResponse.id,
|
|
318
|
+
questionId: question.id,
|
|
319
|
+
response: "Mass times acceleration",
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
progressId = answered.responses[0].id;
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
afterEach(async () => {
|
|
326
|
+
if (!testClass?.id) return;
|
|
327
|
+
|
|
328
|
+
await user1Caller.class.delete({
|
|
329
|
+
classId: testClass.id,
|
|
330
|
+
id: testClass.id,
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
testClass = undefined;
|
|
334
|
+
worksheet = undefined;
|
|
335
|
+
question = undefined;
|
|
336
|
+
multipleChoiceQuestion = undefined;
|
|
337
|
+
submission = undefined;
|
|
338
|
+
worksheetResponse = undefined;
|
|
339
|
+
progressId = "";
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test("redacts worksheet answers and correctness metadata for students", async () => {
|
|
343
|
+
const studentWorksheet = await user2Caller.worksheet.getWorksheet({
|
|
344
|
+
worksheetId: worksheet.id,
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
const shortAnswerQuestion = studentWorksheet.questions.find(
|
|
348
|
+
(item: any) => item.id === question.id,
|
|
349
|
+
);
|
|
350
|
+
expect(shortAnswerQuestion.answer).toBe("");
|
|
351
|
+
expect(shortAnswerQuestion.markScheme).toBeNull();
|
|
352
|
+
|
|
353
|
+
const studentMcq = studentWorksheet.questions.find(
|
|
354
|
+
(item: any) => item.id === multipleChoiceQuestion.id,
|
|
355
|
+
);
|
|
356
|
+
expect(studentMcq.answer).toBe("");
|
|
357
|
+
expect(studentMcq.options).toEqual([
|
|
358
|
+
{ id: "a", text: "Joule" },
|
|
359
|
+
{ id: "b", text: "Newton" },
|
|
360
|
+
]);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test("blocks cross-class worksheet reads by id", async () => {
|
|
364
|
+
await expectTRPCError(
|
|
365
|
+
user3Caller.worksheet.getWorksheet({ worksheetId: worksheet.id }),
|
|
366
|
+
"FORBIDDEN",
|
|
367
|
+
);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test("blocks cross-class worksheet submission reads by id", async () => {
|
|
371
|
+
await expectTRPCError(
|
|
372
|
+
user3Caller.worksheet.getWorksheetSubmission({
|
|
373
|
+
worksheetId: worksheet.id,
|
|
374
|
+
submissionId: submission.id,
|
|
375
|
+
}),
|
|
376
|
+
"FORBIDDEN",
|
|
377
|
+
);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
test("blocks cross-class worksheet answer mutations", async () => {
|
|
381
|
+
await expectTRPCError(
|
|
382
|
+
user3Caller.worksheet.answerQuestion({
|
|
383
|
+
worksheetResponseId: worksheetResponse.id,
|
|
384
|
+
questionId: question.id,
|
|
385
|
+
response: "Unauthorized answer",
|
|
386
|
+
}),
|
|
387
|
+
"FORBIDDEN",
|
|
388
|
+
);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
test("blocks students from teacher worksheet mutations", async () => {
|
|
392
|
+
await expectTRPCError(
|
|
393
|
+
user2Caller.worksheet.updateWorksheet({
|
|
394
|
+
worksheetId: worksheet.id,
|
|
395
|
+
name: "Student update",
|
|
396
|
+
}),
|
|
397
|
+
"FORBIDDEN",
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
await expectTRPCError(
|
|
401
|
+
user2Caller.worksheet.addQuestion({
|
|
402
|
+
worksheetId: worksheet.id,
|
|
403
|
+
question: "Student-created question",
|
|
404
|
+
answer: "Nope",
|
|
405
|
+
type: "SHORT_ANSWER",
|
|
406
|
+
}),
|
|
407
|
+
"FORBIDDEN",
|
|
408
|
+
);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
test("blocks students from grading and feedback actions", async () => {
|
|
412
|
+
await expectTRPCError(
|
|
413
|
+
user2Caller.worksheet.gradeAnswer({
|
|
414
|
+
questionId: question.id,
|
|
415
|
+
responseId: progressId,
|
|
416
|
+
studentWorksheetResponseId: worksheetResponse.id,
|
|
417
|
+
isCorrect: true,
|
|
418
|
+
feedback: "Nice work",
|
|
419
|
+
points: 5,
|
|
420
|
+
}),
|
|
421
|
+
"FORBIDDEN",
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
await expectTRPCError(
|
|
425
|
+
user2Caller.worksheet.addComment({
|
|
426
|
+
responseId: progressId,
|
|
427
|
+
comment: "Teacher feedback",
|
|
428
|
+
}),
|
|
429
|
+
"FORBIDDEN",
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
await expectTRPCError(
|
|
433
|
+
user2Caller.worksheet.cancelGrading({
|
|
434
|
+
worksheetResponseId: worksheetResponse.id,
|
|
435
|
+
progressId,
|
|
436
|
+
}),
|
|
437
|
+
"FORBIDDEN",
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
await expectTRPCError(
|
|
441
|
+
user2Caller.worksheet.regradeQuestion({
|
|
442
|
+
worksheetResponseId: worksheetResponse.id,
|
|
443
|
+
progressId,
|
|
444
|
+
}),
|
|
445
|
+
"FORBIDDEN",
|
|
446
|
+
);
|
|
447
|
+
});
|
|
448
|
+
});
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { execSync } from 'child_process';
|
|
2
2
|
import { config } from 'dotenv';
|
|
3
3
|
import { resolve } from 'path';
|
|
4
|
-
import { prisma } from '
|
|
4
|
+
import { prisma } from '../../src/lib/prisma';
|
|
5
5
|
import { beforeAll, beforeEach, afterAll, afterEach } from 'vitest';
|
|
6
|
-
import { logger } from '
|
|
7
|
-
import { appRouter } from '
|
|
8
|
-
import { createTRPCContext } from '
|
|
6
|
+
import { logger } from '../../src/utils/logger';
|
|
7
|
+
import { appRouter } from '../../src/routers/_app';
|
|
8
|
+
import { createTRPCContext } from '../../src/trpc';
|
|
9
9
|
import { Session, SessionType } from '@prisma/client';
|
|
10
10
|
|
|
11
11
|
// Load test environment variables
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
isValidCorsOriginPatternList,
|
|
5
5
|
parseCorsOriginPatterns,
|
|
6
6
|
parseCorsOrigins,
|
|
7
|
-
} from '
|
|
7
|
+
} from '../../../src/lib/config/cors.js';
|
|
8
8
|
|
|
9
9
|
describe('CORS configuration', () => {
|
|
10
10
|
test('parses comma-separated origins while trimming empty entries', () => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from "vitest";
|
|
2
2
|
|
|
3
|
-
import { sliceMessagesThroughAnchor } from "
|
|
3
|
+
import { sliceMessagesThroughAnchor } from "../../../src/pipelines/aiLabChat.js";
|
|
4
4
|
|
|
5
5
|
describe("sliceMessagesThroughAnchor", () => {
|
|
6
6
|
const messages = [
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from "vitest";
|
|
2
|
-
import { labChatArrayFieldInstructions, labChatResponseFormat, labChatResponseSchema } from "
|
|
2
|
+
import { labChatArrayFieldInstructions, labChatResponseFormat, labChatResponseSchema } from "../../../src/pipelines/aiLabChatContract.js";
|
|
3
3
|
|
|
4
4
|
describe("aiLabChat contract", () => {
|
|
5
5
|
test("defaults worksheet and section collections to empty arrays", () => {
|
|
@@ -3,8 +3,8 @@ import { describe, expect, test } from "vitest";
|
|
|
3
3
|
import {
|
|
4
4
|
buildLabChatResponseMessages,
|
|
5
5
|
buildLabChatSystemPrompt,
|
|
6
|
-
} from "
|
|
7
|
-
import { labChatArrayFieldInstructions, labChatResponseFormat } from "
|
|
6
|
+
} from "../../../src/pipelines/labChatPrompt";
|
|
7
|
+
import { labChatArrayFieldInstructions, labChatResponseFormat } from "../../../src/pipelines/aiLabChatContract";
|
|
8
8
|
|
|
9
9
|
describe("buildLabChatSystemPrompt", () => {
|
|
10
10
|
test("requires arrays for worksheet and section structured output fields", () => {
|
|
@@ -11,19 +11,19 @@ const { prismaMocks, isTeacherInClass, inference, logger } = vi.hoisted(() => ({
|
|
|
11
11
|
logger: { error: vi.fn() },
|
|
12
12
|
}));
|
|
13
13
|
|
|
14
|
-
vi.mock("
|
|
14
|
+
vi.mock("../../../src/lib/prisma.js", () => ({
|
|
15
15
|
prisma: prismaMocks,
|
|
16
16
|
}));
|
|
17
17
|
|
|
18
|
-
vi.mock("
|
|
18
|
+
vi.mock("../../../src/models/class.js", () => ({
|
|
19
19
|
isTeacherInClass,
|
|
20
20
|
}));
|
|
21
21
|
|
|
22
|
-
vi.mock("
|
|
22
|
+
vi.mock("../../../src/utils/inference.js", () => ({
|
|
23
23
|
inference,
|
|
24
24
|
}));
|
|
25
25
|
|
|
26
|
-
vi.mock("
|
|
26
|
+
vi.mock("../../../src/utils/logger.js", () => ({
|
|
27
27
|
logger,
|
|
28
28
|
}));
|
|
29
29
|
|
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
dismissStudentRecommendation,
|
|
33
33
|
getClassProgressRecommendations,
|
|
34
34
|
getStudentProgressRecommendations,
|
|
35
|
-
} from "
|
|
35
|
+
} from "../../../src/services/studentProgress.js";
|
|
36
36
|
|
|
37
37
|
function createSubmission(overrides: Partial<any> = {}) {
|
|
38
38
|
return {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, test, expect } from 'vitest';
|
|
2
|
-
import { generateInviteCode } from '
|
|
2
|
+
import { generateInviteCode } from '../../../src/utils/generateInviteCode.js';
|
|
3
3
|
|
|
4
4
|
describe('generateInviteCode', () => {
|
|
5
5
|
test('returns a string of length 5', () => {
|
|
@@ -47,27 +47,27 @@ vi.mock('openai/helpers/zod', () => ({
|
|
|
47
47
|
zodTextFormat: vi.fn((schema, name) => ({ schema, name })),
|
|
48
48
|
}));
|
|
49
49
|
|
|
50
|
-
vi.mock('
|
|
50
|
+
vi.mock('../../../src/lib/prisma.js', () => ({
|
|
51
51
|
prisma: prismaMocks,
|
|
52
52
|
}));
|
|
53
53
|
|
|
54
|
-
vi.mock('
|
|
54
|
+
vi.mock('../../../src/utils/logger.js', () => ({
|
|
55
55
|
logger,
|
|
56
56
|
}));
|
|
57
57
|
|
|
58
|
-
vi.mock('
|
|
58
|
+
vi.mock('../../../src/lib/pusher.js', () => ({
|
|
59
59
|
chatChannel: vi.fn(),
|
|
60
60
|
pusher: {
|
|
61
61
|
trigger: vi.fn(),
|
|
62
62
|
},
|
|
63
63
|
}));
|
|
64
64
|
|
|
65
|
-
vi.mock('
|
|
65
|
+
vi.mock('../../../src/utils/aiUser.js', () => ({
|
|
66
66
|
ensureAIUserExists: vi.fn(),
|
|
67
67
|
getAIUserId: vi.fn(() => 'AI_ASSISTANT'),
|
|
68
68
|
}));
|
|
69
69
|
|
|
70
|
-
vi.mock('
|
|
70
|
+
vi.mock('../../../src/lib/config/env.js', () => ({
|
|
71
71
|
env: {
|
|
72
72
|
INFERENCE_API_KEY: 'test-key',
|
|
73
73
|
INFERENCE_API_BASE_URL: 'http://localhost:11434/v1',
|
|
@@ -75,7 +75,7 @@ vi.mock('../../src/lib/config/env.js', () => ({
|
|
|
75
75
|
},
|
|
76
76
|
}));
|
|
77
77
|
|
|
78
|
-
import { estimateTokenCount, inference } from '
|
|
78
|
+
import { estimateTokenCount, inference } from '../../../src/utils/inference.js';
|
|
79
79
|
|
|
80
80
|
describe('inference', () => {
|
|
81
81
|
beforeEach(() => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { logger } from '
|
|
2
|
+
import { logger } from '../../../src/utils/logger.js';
|
|
3
3
|
|
|
4
4
|
describe('Logger', () => {
|
|
5
5
|
let consoleSpy: { log: ReturnType<typeof vi.spyOn>; warn: ReturnType<typeof vi.spyOn>; error: ReturnType<typeof vi.spyOn>; debug: ReturnType<typeof vi.spyOn> };
|
|
@@ -2,7 +2,7 @@ import { describe, test, expect } from 'vitest';
|
|
|
2
2
|
import {
|
|
3
3
|
handlePrismaError,
|
|
4
4
|
getFieldDisplayName,
|
|
5
|
-
} from '
|
|
5
|
+
} from '../../../src/utils/prismaErrorHandler.js';
|
|
6
6
|
import {
|
|
7
7
|
PrismaClientKnownRequestError,
|
|
8
8
|
PrismaClientValidationError,
|
|
@@ -2,7 +2,7 @@ import { describe, test, expect, vi } from 'vitest';
|
|
|
2
2
|
import {
|
|
3
3
|
withPrismaErrorHandling,
|
|
4
4
|
prismaWrapper,
|
|
5
|
-
} from '
|
|
5
|
+
} from '../../../src/utils/prismaWrapper.js';
|
|
6
6
|
import { TRPCError } from '@trpc/server';
|
|
7
7
|
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';
|
|
8
8
|
|
package/vitest.config.ts
CHANGED
|
@@ -5,15 +5,23 @@ import { resolve } from 'path';
|
|
|
5
5
|
// Load test environment variables before anything else
|
|
6
6
|
config({ path: resolve(process.cwd(), '.env.test'), override: true });
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Integration test config — real Postgres via globalSetup, shared tRPC callers
|
|
10
|
+
* via setupFiles. Tests that mock external services (e.g. OpenAI) at the module
|
|
11
|
+
* boundary are still integration tests if they import setup.ts; they live here.
|
|
12
|
+
*
|
|
13
|
+
* Use: npm test (default) or npm run test:run.
|
|
14
|
+
* For pure unit tests, use: npm run test:unit.
|
|
15
|
+
*/
|
|
8
16
|
export default defineConfig({
|
|
9
17
|
test: {
|
|
10
18
|
globals: true,
|
|
11
19
|
environment: 'node',
|
|
12
20
|
hookTimeout: 60000, // 60 seconds
|
|
13
21
|
testTimeout: 30000, // 30 seconds per test
|
|
14
|
-
include: ['tests/**/*.test.ts'],
|
|
15
|
-
globalSetup: ['./tests/globalSetup.ts'],
|
|
16
|
-
setupFiles: ['./tests/setup.ts'],
|
|
22
|
+
include: ['tests/integration/**/*.test.ts'],
|
|
23
|
+
globalSetup: ['./tests/integration/globalSetup.ts'],
|
|
24
|
+
setupFiles: ['./tests/integration/setup.ts'],
|
|
17
25
|
sequence: {
|
|
18
26
|
concurrent: false,
|
|
19
27
|
},
|