@studious-lms/server 1.15.0 → 1.16.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 (113) hide show
  1. package/.github/workflows/deploy.yml +1 -1
  2. package/dist/instrument.js +3 -3
  3. package/dist/instrument.js.map +1 -1
  4. package/dist/models/announcement.d.ts +0 -2
  5. package/dist/models/announcement.d.ts.map +1 -1
  6. package/dist/models/auth.d.ts +0 -8
  7. package/dist/models/auth.d.ts.map +1 -1
  8. package/dist/models/auth.js +2 -4
  9. package/dist/models/auth.js.map +1 -1
  10. package/dist/models/event.d.ts +0 -2
  11. package/dist/models/event.d.ts.map +1 -1
  12. package/dist/models/file.d.ts +0 -24
  13. package/dist/models/file.d.ts.map +1 -1
  14. package/dist/models/marketing.d.ts +15 -2
  15. package/dist/models/marketing.d.ts.map +1 -1
  16. package/dist/models/marketing.js +4 -12
  17. package/dist/models/marketing.js.map +1 -1
  18. package/dist/pipelines/aiLabChat.d.ts.map +1 -1
  19. package/dist/pipelines/aiLabChat.js +26 -34
  20. package/dist/pipelines/aiLabChat.js.map +1 -1
  21. package/dist/pipelines/gradeWorksheet.d.ts.map +1 -1
  22. package/dist/pipelines/gradeWorksheet.js +75 -36
  23. package/dist/pipelines/gradeWorksheet.js.map +1 -1
  24. package/dist/pipelines/labChatPrompt.d.ts.map +1 -1
  25. package/dist/pipelines/labChatPrompt.js +3 -6
  26. package/dist/pipelines/labChatPrompt.js.map +1 -1
  27. package/dist/routers/_app.d.ts +26 -222
  28. package/dist/routers/_app.d.ts.map +1 -1
  29. package/dist/routers/assignment.d.ts +0 -10
  30. package/dist/routers/assignment.d.ts.map +1 -1
  31. package/dist/routers/auth.d.ts +0 -2
  32. package/dist/routers/auth.d.ts.map +1 -1
  33. package/dist/routers/auth.js +2 -15
  34. package/dist/routers/auth.js.map +1 -1
  35. package/dist/routers/class.d.ts +0 -4
  36. package/dist/routers/class.d.ts.map +1 -1
  37. package/dist/routers/event.d.ts +0 -2
  38. package/dist/routers/event.d.ts.map +1 -1
  39. package/dist/routers/marketing.d.ts +13 -0
  40. package/dist/routers/marketing.d.ts.map +1 -1
  41. package/dist/routers/school.d.ts +0 -4
  42. package/dist/routers/school.d.ts.map +1 -1
  43. package/dist/routers/user.d.ts +0 -89
  44. package/dist/routers/user.d.ts.map +1 -1
  45. package/dist/routers/user.js +2 -19
  46. package/dist/routers/user.js.map +1 -1
  47. package/dist/seedDatabase.d.ts +0 -2
  48. package/dist/seedDatabase.d.ts.map +1 -1
  49. package/dist/services/aiChatStream.d.ts +0 -2
  50. package/dist/services/aiChatStream.d.ts.map +1 -1
  51. package/dist/services/aiChatStream.js +4 -31
  52. package/dist/services/aiChatStream.js.map +1 -1
  53. package/dist/services/aiProposalTools.d.ts.map +1 -1
  54. package/dist/services/aiProposalTools.js +7 -7
  55. package/dist/services/aiProposalTools.js.map +1 -1
  56. package/dist/services/aiTools.js +3 -3
  57. package/dist/services/aiTools.js.map +1 -1
  58. package/dist/services/assignment.d.ts +0 -10
  59. package/dist/services/assignment.d.ts.map +1 -1
  60. package/dist/services/auth.d.ts +0 -2
  61. package/dist/services/auth.d.ts.map +1 -1
  62. package/dist/services/auth.js +4 -13
  63. package/dist/services/auth.js.map +1 -1
  64. package/dist/services/class.d.ts +0 -4
  65. package/dist/services/class.d.ts.map +1 -1
  66. package/dist/services/event.d.ts +0 -2
  67. package/dist/services/event.d.ts.map +1 -1
  68. package/dist/services/marketing.d.ts +14 -1
  69. package/dist/services/marketing.d.ts.map +1 -1
  70. package/dist/services/marketing.js +5 -5
  71. package/dist/services/marketing.js.map +1 -1
  72. package/dist/services/openaiDocumentGen.d.ts +0 -12
  73. package/dist/services/openaiDocumentGen.d.ts.map +1 -1
  74. package/dist/services/openaiDocumentGen.js +63 -130
  75. package/dist/services/openaiDocumentGen.js.map +1 -1
  76. package/dist/services/school.d.ts +0 -4
  77. package/dist/services/school.d.ts.map +1 -1
  78. package/dist/utils/email.d.ts.map +1 -1
  79. package/dist/utils/email.js +3 -7
  80. package/dist/utils/email.js.map +1 -1
  81. package/package.json +1 -1
  82. package/prisma/schema.prisma +0 -3
  83. package/src/instrument.ts +1 -1
  84. package/src/models/auth.ts +0 -4
  85. package/src/models/marketing.ts +2 -10
  86. package/src/pipelines/aiLabChat.ts +28 -41
  87. package/src/pipelines/gradeWorksheet.ts +95 -38
  88. package/src/pipelines/labChatPrompt.ts +1 -4
  89. package/src/routers/auth.ts +0 -13
  90. package/src/routers/user.ts +0 -26
  91. package/src/services/aiChatStream.ts +3 -30
  92. package/src/services/aiProposalTools.ts +5 -6
  93. package/src/services/aiTools.ts +1 -1
  94. package/src/services/auth.ts +2 -14
  95. package/src/services/marketing.ts +3 -3
  96. package/src/services/openaiDocumentGen.ts +64 -146
  97. package/src/utils/email.ts +1 -5
  98. package/tests/integration/globalSetup.ts +18 -10
  99. package/tests/integration/routers/auth.test.ts +18 -13
  100. package/dist/lib/legal.d.ts +0 -3
  101. package/dist/lib/legal.d.ts.map +0 -1
  102. package/dist/lib/legal.js +0 -6
  103. package/dist/lib/legal.js.map +0 -1
  104. package/dist/services/userPrivacy.d.ts +0 -86
  105. package/dist/services/userPrivacy.d.ts.map +0 -1
  106. package/dist/services/userPrivacy.js +0 -184
  107. package/dist/services/userPrivacy.js.map +0 -1
  108. package/prisma/migrations/20260611120000_add_user_terms_acceptance/migration.sql +0 -3
  109. package/src/lib/legal.ts +0 -2
  110. package/src/services/userPrivacy.ts +0 -196
  111. package/tests/helpers/registerInput.ts +0 -20
  112. package/tests/unit/services/aiChatStream.test.ts +0 -92
  113. package/tests/unit/services/openaiDocumentGen.test.ts +0 -65
@@ -1,184 +0,0 @@
1
- /**
2
- * User privacy service — data export and account erasure (anonymization).
3
- */
4
-
5
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="7184e3ea-1921-5a91-936e-6fa9a92f8a03")}catch(e){}}();
6
- import { TRPCError } from "@trpc/server";
7
- import { randomBytes } from "node:crypto";
8
- import { hash } from "bcryptjs";
9
- import { prisma } from "../lib/prisma.js";
10
- import { deleteSessionsByUserId } from "../models/auth.js";
11
- async function assertAccountCanBeErased(userId) {
12
- const user = await prisma.user.findUnique({
13
- where: { id: userId },
14
- select: {
15
- id: true,
16
- teacherIn: {
17
- select: {
18
- id: true,
19
- name: true,
20
- _count: { select: { students: true } },
21
- },
22
- },
23
- _count: {
24
- select: {
25
- assignments: true,
26
- createdLabChats: true,
27
- },
28
- },
29
- },
30
- });
31
- if (!user) {
32
- throw new TRPCError({ code: "NOT_FOUND", message: "User not found" });
33
- }
34
- const classesWithStudents = user.teacherIn.filter((cls) => cls._count.students > 0);
35
- if (classesWithStudents.length > 0) {
36
- throw new TRPCError({
37
- code: "PRECONDITION_FAILED",
38
- message: "Delete or transfer your classes before deleting your account.",
39
- });
40
- }
41
- if (user._count.assignments > 0) {
42
- throw new TRPCError({
43
- code: "PRECONDITION_FAILED",
44
- message: "Remove or reassign your assignments before deleting your account.",
45
- });
46
- }
47
- if (user._count.createdLabChats > 0) {
48
- throw new TRPCError({
49
- code: "PRECONDITION_FAILED",
50
- message: "Remove your AI lab chats before deleting your account.",
51
- });
52
- }
53
- }
54
- /** Export a portable snapshot of the authenticated user's personal data. */
55
- export async function exportUserData(userId) {
56
- const user = await prisma.user.findUnique({
57
- where: { id: userId },
58
- select: {
59
- id: true,
60
- username: true,
61
- email: true,
62
- verified: true,
63
- termsAcceptedAt: true,
64
- termsVersion: true,
65
- profile: true,
66
- teacherIn: {
67
- select: { id: true, name: true, subject: true, section: true },
68
- },
69
- studentIn: {
70
- select: { id: true, name: true, subject: true, section: true },
71
- },
72
- schoolMemberships: {
73
- select: {
74
- role: true,
75
- status: true,
76
- school: { select: { id: true, name: true } },
77
- },
78
- },
79
- submissions: {
80
- select: {
81
- id: true,
82
- createdAt: true,
83
- submittedAt: true,
84
- gradeReceived: true,
85
- extendedResponse: true,
86
- assignment: { select: { id: true, title: true, classId: true } },
87
- },
88
- },
89
- events: {
90
- select: {
91
- id: true,
92
- name: true,
93
- startTime: true,
94
- endTime: true,
95
- location: true,
96
- remarks: true,
97
- },
98
- },
99
- sentMessages: {
100
- select: {
101
- id: true,
102
- content: true,
103
- createdAt: true,
104
- conversationId: true,
105
- },
106
- take: 500,
107
- orderBy: { createdAt: "desc" },
108
- },
109
- notificationsReceived: {
110
- select: {
111
- id: true,
112
- title: true,
113
- content: true,
114
- read: true,
115
- createdAt: true,
116
- },
117
- take: 200,
118
- orderBy: { createdAt: "desc" },
119
- },
120
- },
121
- });
122
- if (!user) {
123
- throw new TRPCError({ code: "NOT_FOUND", message: "User not found" });
124
- }
125
- return {
126
- exportedAt: new Date().toISOString(),
127
- user,
128
- };
129
- }
130
- /**
131
- * Erase personal account data by anonymizing the user record and removing
132
- * sessions and memberships. Educational records tied to classes (e.g.
133
- * submissions) remain with an anonymized identity where required.
134
- */
135
- export async function deleteUserAccount(userId) {
136
- await assertAccountCanBeErased(userId);
137
- await deleteSessionsByUserId(userId);
138
- const anonUsername = `deleted_${userId.slice(0, 8)}`;
139
- const anonEmail = `deleted+${userId}@studious.invalid`;
140
- const randomPassword = await hash(randomBytes(32).toString("hex"), 10);
141
- await prisma.$transaction([
142
- prisma.conversationMember.deleteMany({ where: { userId } }),
143
- prisma.notification.deleteMany({
144
- where: { OR: [{ senderId: userId }, { receiverId: userId }] },
145
- }),
146
- prisma.schoolMembership.deleteMany({ where: { userId } }),
147
- prisma.permission.deleteMany({
148
- where: { OR: [{ userId }, { grantedById: userId }] },
149
- }),
150
- prisma.user.update({
151
- where: { id: userId },
152
- data: {
153
- teacherIn: { set: [] },
154
- studentIn: { set: [] },
155
- departments: { set: [] },
156
- tags: { set: [] },
157
- specificAssignments: { set: [] },
158
- presentAttendance: { set: [] },
159
- lateAttendance: { set: [] },
160
- absentAttendance: { set: [] },
161
- schoolId: null,
162
- username: anonUsername,
163
- email: anonEmail,
164
- password: randomPassword,
165
- verified: false,
166
- termsAcceptedAt: null,
167
- termsVersion: null,
168
- profile: {
169
- update: {
170
- displayName: null,
171
- bio: null,
172
- location: null,
173
- website: null,
174
- profilePicture: null,
175
- profilePictureThumbnail: null,
176
- },
177
- },
178
- },
179
- }),
180
- ]);
181
- return { success: true };
182
- }
183
- //# sourceMappingURL=userPrivacy.js.map
184
- //# debugId=7184e3ea-1921-5a91-936e-6fa9a92f8a03
@@ -1 +0,0 @@
1
- {"version":3,"file":"userPrivacy.js","sources":["services/userPrivacy.ts"],"sourceRoot":"/","sourcesContent":["/**\n * User privacy service — data export and account erasure (anonymization).\n */\nimport { TRPCError } from \"@trpc/server\";\nimport { randomBytes } from \"node:crypto\";\nimport { hash } from \"bcryptjs\";\nimport { prisma } from \"../lib/prisma.js\";\nimport { deleteSessionsByUserId } from \"../models/auth.js\";\n\nasync function assertAccountCanBeErased(userId: string) {\n const user = await prisma.user.findUnique({\n where: { id: userId },\n select: {\n id: true,\n teacherIn: {\n select: {\n id: true,\n name: true,\n _count: { select: { students: true } },\n },\n },\n _count: {\n select: {\n assignments: true,\n createdLabChats: true,\n },\n },\n },\n });\n\n if (!user) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"User not found\" });\n }\n\n const classesWithStudents = user.teacherIn.filter(\n (cls) => cls._count.students > 0,\n );\n if (classesWithStudents.length > 0) {\n throw new TRPCError({\n code: \"PRECONDITION_FAILED\",\n message:\n \"Delete or transfer your classes before deleting your account.\",\n });\n }\n\n if (user._count.assignments > 0) {\n throw new TRPCError({\n code: \"PRECONDITION_FAILED\",\n message:\n \"Remove or reassign your assignments before deleting your account.\",\n });\n }\n\n if (user._count.createdLabChats > 0) {\n throw new TRPCError({\n code: \"PRECONDITION_FAILED\",\n message: \"Remove your AI lab chats before deleting your account.\",\n });\n }\n}\n\n/** Export a portable snapshot of the authenticated user's personal data. */\nexport async function exportUserData(userId: string) {\n const user = await prisma.user.findUnique({\n where: { id: userId },\n select: {\n id: true,\n username: true,\n email: true,\n verified: true,\n termsAcceptedAt: true,\n termsVersion: true,\n profile: true,\n teacherIn: {\n select: { id: true, name: true, subject: true, section: true },\n },\n studentIn: {\n select: { id: true, name: true, subject: true, section: true },\n },\n schoolMemberships: {\n select: {\n role: true,\n status: true,\n school: { select: { id: true, name: true } },\n },\n },\n submissions: {\n select: {\n id: true,\n createdAt: true,\n submittedAt: true,\n gradeReceived: true,\n extendedResponse: true,\n assignment: { select: { id: true, title: true, classId: true } },\n },\n },\n events: {\n select: {\n id: true,\n name: true,\n startTime: true,\n endTime: true,\n location: true,\n remarks: true,\n },\n },\n sentMessages: {\n select: {\n id: true,\n content: true,\n createdAt: true,\n conversationId: true,\n },\n take: 500,\n orderBy: { createdAt: \"desc\" },\n },\n notificationsReceived: {\n select: {\n id: true,\n title: true,\n content: true,\n read: true,\n createdAt: true,\n },\n take: 200,\n orderBy: { createdAt: \"desc\" },\n },\n },\n });\n\n if (!user) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"User not found\" });\n }\n\n return {\n exportedAt: new Date().toISOString(),\n user,\n };\n}\n\n/**\n * Erase personal account data by anonymizing the user record and removing\n * sessions and memberships. Educational records tied to classes (e.g.\n * submissions) remain with an anonymized identity where required.\n */\nexport async function deleteUserAccount(userId: string) {\n await assertAccountCanBeErased(userId);\n await deleteSessionsByUserId(userId);\n\n const anonUsername = `deleted_${userId.slice(0, 8)}`;\n const anonEmail = `deleted+${userId}@studious.invalid`;\n const randomPassword = await hash(randomBytes(32).toString(\"hex\"), 10);\n\n await prisma.$transaction([\n prisma.conversationMember.deleteMany({ where: { userId } }),\n prisma.notification.deleteMany({\n where: { OR: [{ senderId: userId }, { receiverId: userId }] },\n }),\n prisma.schoolMembership.deleteMany({ where: { userId } }),\n prisma.permission.deleteMany({\n where: { OR: [{ userId }, { grantedById: userId }] },\n }),\n prisma.user.update({\n where: { id: userId },\n data: {\n teacherIn: { set: [] },\n studentIn: { set: [] },\n departments: { set: [] },\n tags: { set: [] },\n specificAssignments: { set: [] },\n presentAttendance: { set: [] },\n lateAttendance: { set: [] },\n absentAttendance: { set: [] },\n schoolId: null,\n username: anonUsername,\n email: anonEmail,\n password: randomPassword,\n verified: false,\n termsAcceptedAt: null,\n termsVersion: null,\n profile: {\n update: {\n displayName: null,\n bio: null,\n location: null,\n website: null,\n profilePicture: null,\n profilePictureThumbnail: null,\n },\n },\n },\n }),\n ]);\n\n return { success: true };\n}\n"],"names":[],"mappings":"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","debug_id":"7184e3ea-1921-5a91-936e-6fa9a92f8a03"}
@@ -1,3 +0,0 @@
1
- -- AlterTable
2
- ALTER TABLE "User" ADD COLUMN "termsAcceptedAt" TIMESTAMP(3),
3
- ADD COLUMN "termsVersion" TEXT;
package/src/lib/legal.ts DELETED
@@ -1,2 +0,0 @@
1
- /** Must match the "Last updated" month in the client privacy/terms copy. */
2
- export const CURRENT_TERMS_VERSION = "2025-11";
@@ -1,196 +0,0 @@
1
- /**
2
- * User privacy service — data export and account erasure (anonymization).
3
- */
4
- import { TRPCError } from "@trpc/server";
5
- import { randomBytes } from "node:crypto";
6
- import { hash } from "bcryptjs";
7
- import { prisma } from "../lib/prisma.js";
8
- import { deleteSessionsByUserId } from "../models/auth.js";
9
-
10
- async function assertAccountCanBeErased(userId: string) {
11
- const user = await prisma.user.findUnique({
12
- where: { id: userId },
13
- select: {
14
- id: true,
15
- teacherIn: {
16
- select: {
17
- id: true,
18
- name: true,
19
- _count: { select: { students: true } },
20
- },
21
- },
22
- _count: {
23
- select: {
24
- assignments: true,
25
- createdLabChats: true,
26
- },
27
- },
28
- },
29
- });
30
-
31
- if (!user) {
32
- throw new TRPCError({ code: "NOT_FOUND", message: "User not found" });
33
- }
34
-
35
- const classesWithStudents = user.teacherIn.filter(
36
- (cls) => cls._count.students > 0,
37
- );
38
- if (classesWithStudents.length > 0) {
39
- throw new TRPCError({
40
- code: "PRECONDITION_FAILED",
41
- message:
42
- "Delete or transfer your classes before deleting your account.",
43
- });
44
- }
45
-
46
- if (user._count.assignments > 0) {
47
- throw new TRPCError({
48
- code: "PRECONDITION_FAILED",
49
- message:
50
- "Remove or reassign your assignments before deleting your account.",
51
- });
52
- }
53
-
54
- if (user._count.createdLabChats > 0) {
55
- throw new TRPCError({
56
- code: "PRECONDITION_FAILED",
57
- message: "Remove your AI lab chats before deleting your account.",
58
- });
59
- }
60
- }
61
-
62
- /** Export a portable snapshot of the authenticated user's personal data. */
63
- export async function exportUserData(userId: string) {
64
- const user = await prisma.user.findUnique({
65
- where: { id: userId },
66
- select: {
67
- id: true,
68
- username: true,
69
- email: true,
70
- verified: true,
71
- termsAcceptedAt: true,
72
- termsVersion: true,
73
- profile: true,
74
- teacherIn: {
75
- select: { id: true, name: true, subject: true, section: true },
76
- },
77
- studentIn: {
78
- select: { id: true, name: true, subject: true, section: true },
79
- },
80
- schoolMemberships: {
81
- select: {
82
- role: true,
83
- status: true,
84
- school: { select: { id: true, name: true } },
85
- },
86
- },
87
- submissions: {
88
- select: {
89
- id: true,
90
- createdAt: true,
91
- submittedAt: true,
92
- gradeReceived: true,
93
- extendedResponse: true,
94
- assignment: { select: { id: true, title: true, classId: true } },
95
- },
96
- },
97
- events: {
98
- select: {
99
- id: true,
100
- name: true,
101
- startTime: true,
102
- endTime: true,
103
- location: true,
104
- remarks: true,
105
- },
106
- },
107
- sentMessages: {
108
- select: {
109
- id: true,
110
- content: true,
111
- createdAt: true,
112
- conversationId: true,
113
- },
114
- take: 500,
115
- orderBy: { createdAt: "desc" },
116
- },
117
- notificationsReceived: {
118
- select: {
119
- id: true,
120
- title: true,
121
- content: true,
122
- read: true,
123
- createdAt: true,
124
- },
125
- take: 200,
126
- orderBy: { createdAt: "desc" },
127
- },
128
- },
129
- });
130
-
131
- if (!user) {
132
- throw new TRPCError({ code: "NOT_FOUND", message: "User not found" });
133
- }
134
-
135
- return {
136
- exportedAt: new Date().toISOString(),
137
- user,
138
- };
139
- }
140
-
141
- /**
142
- * Erase personal account data by anonymizing the user record and removing
143
- * sessions and memberships. Educational records tied to classes (e.g.
144
- * submissions) remain with an anonymized identity where required.
145
- */
146
- export async function deleteUserAccount(userId: string) {
147
- await assertAccountCanBeErased(userId);
148
- await deleteSessionsByUserId(userId);
149
-
150
- const anonUsername = `deleted_${userId.slice(0, 8)}`;
151
- const anonEmail = `deleted+${userId}@studious.invalid`;
152
- const randomPassword = await hash(randomBytes(32).toString("hex"), 10);
153
-
154
- await prisma.$transaction([
155
- prisma.conversationMember.deleteMany({ where: { userId } }),
156
- prisma.notification.deleteMany({
157
- where: { OR: [{ senderId: userId }, { receiverId: userId }] },
158
- }),
159
- prisma.schoolMembership.deleteMany({ where: { userId } }),
160
- prisma.permission.deleteMany({
161
- where: { OR: [{ userId }, { grantedById: userId }] },
162
- }),
163
- prisma.user.update({
164
- where: { id: userId },
165
- data: {
166
- teacherIn: { set: [] },
167
- studentIn: { set: [] },
168
- departments: { set: [] },
169
- tags: { set: [] },
170
- specificAssignments: { set: [] },
171
- presentAttendance: { set: [] },
172
- lateAttendance: { set: [] },
173
- absentAttendance: { set: [] },
174
- schoolId: null,
175
- username: anonUsername,
176
- email: anonEmail,
177
- password: randomPassword,
178
- verified: false,
179
- termsAcceptedAt: null,
180
- termsVersion: null,
181
- profile: {
182
- update: {
183
- displayName: null,
184
- bio: null,
185
- location: null,
186
- website: null,
187
- profilePicture: null,
188
- profilePictureThumbnail: null,
189
- },
190
- },
191
- },
192
- }),
193
- ]);
194
-
195
- return { success: true };
196
- }
@@ -1,20 +0,0 @@
1
- import { CURRENT_TERMS_VERSION } from "../../src/lib/legal.js";
2
-
3
- export function registerInput(
4
- overrides: {
5
- username: string;
6
- email: string;
7
- password?: string;
8
- confirmPassword?: string;
9
- },
10
- ) {
11
- const password = overrides.password ?? "password_is_1234";
12
- return {
13
- username: overrides.username,
14
- email: overrides.email,
15
- password,
16
- confirmPassword: overrides.confirmPassword ?? password,
17
- agreeToTerms: true as const,
18
- termsVersion: CURRENT_TERMS_VERSION,
19
- };
20
- }
@@ -1,92 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import {
3
- enrichStreamPartsWithGeneratedFiles,
4
- finalizeDeferredToolParts,
5
- } from '../../../src/services/aiChatStream.js';
6
-
7
- describe('enrichStreamPartsWithGeneratedFiles', () => {
8
- it('links generated files to generate_document tool parts by title', () => {
9
- const parts = enrichStreamPartsWithGeneratedFiles(
10
- [
11
- { type: 'text', text: 'Here is your worksheet.' },
12
- {
13
- type: 'tool',
14
- toolCallId: 'tc-1',
15
- toolName: 'generate_document',
16
- title: 'Algebra Review',
17
- status: 'completed',
18
- },
19
- ],
20
- [{ title: 'Algebra Review', fileId: 'file-1', fileName: 'Algebra_Review.pdf' }],
21
- );
22
-
23
- expect(parts[1]).toMatchObject({
24
- type: 'tool',
25
- fileId: 'file-1',
26
- fileName: 'Algebra_Review.pdf',
27
- });
28
- });
29
-
30
- it('falls back to order when titles do not match', () => {
31
- const parts = enrichStreamPartsWithGeneratedFiles(
32
- [
33
- {
34
- type: 'tool',
35
- toolCallId: 'tc-1',
36
- toolName: 'generate_document',
37
- status: 'completed',
38
- },
39
- ],
40
- [{ title: 'Doc A', fileId: 'file-a', fileName: 'doc_a.pdf' }],
41
- );
42
-
43
- expect(parts[0]).toMatchObject({ fileId: 'file-a' });
44
- });
45
-
46
- it('marks linked generate_document tools completed', () => {
47
- const parts = enrichStreamPartsWithGeneratedFiles(
48
- [
49
- {
50
- type: 'tool',
51
- toolCallId: 'tc-1',
52
- toolName: 'generate_document',
53
- title: 'Slides',
54
- status: 'executing',
55
- },
56
- ],
57
- [{ title: 'Slides', fileId: 'file-1', fileName: 'slides.pptx' }],
58
- );
59
-
60
- expect(parts[0]).toMatchObject({ status: 'completed', fileId: 'file-1' });
61
- });
62
- });
63
-
64
- describe('finalizeDeferredToolParts', () => {
65
- it('marks generate_document without a file as failed', () => {
66
- const parts = finalizeDeferredToolParts([
67
- {
68
- type: 'tool',
69
- toolCallId: 'tc-1',
70
- toolName: 'generate_document',
71
- title: 'Missing',
72
- status: 'executing',
73
- },
74
- ]);
75
-
76
- expect(parts[0]).toMatchObject({ status: 'failed' });
77
- });
78
-
79
- it('leaves other tools unchanged', () => {
80
- const parts = finalizeDeferredToolParts([
81
- {
82
- type: 'tool',
83
- toolCallId: 'tc-1',
84
- toolName: 'propose_assignment',
85
- title: 'HW',
86
- status: 'completed',
87
- },
88
- ]);
89
-
90
- expect(parts[0]).toMatchObject({ status: 'completed' });
91
- });
92
- });
@@ -1,65 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import {
3
- extractContainerGeneratedFiles,
4
- mimeTypeForFormat,
5
- normalizeDocumentFormat,
6
- } from '../../../src/services/openaiDocumentGen.js';
7
- import type { Response } from 'openai/resources/responses/responses.js';
8
-
9
- describe('openaiDocumentGen', () => {
10
- it('normalizes unknown formats to pdf', () => {
11
- expect(normalizeDocumentFormat('docx')).toBe('docx');
12
- expect(normalizeDocumentFormat('.PDF')).toBe('pdf');
13
- expect(normalizeDocumentFormat('bogus')).toBe('pdf');
14
- });
15
-
16
- it('maps formats to mime types', () => {
17
- expect(mimeTypeForFormat('pdf')).toBe('application/pdf');
18
- expect(mimeTypeForFormat('xlsx')).toBe(
19
- 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
20
- );
21
- expect(mimeTypeForFormat('png')).toBe('image/png');
22
- });
23
-
24
- it('collects container_file_citation annotations for the requested extension', () => {
25
- const response = {
26
- output: [
27
- {
28
- type: 'message',
29
- content: [
30
- {
31
- type: 'output_text',
32
- text: 'Here is your spreadsheet.',
33
- annotations: [
34
- {
35
- type: 'container_file_citation',
36
- container_id: 'cntr_1',
37
- file_id: 'cfile_1',
38
- filename: 'Grades.xlsx',
39
- start_index: 0,
40
- end_index: 0,
41
- },
42
- {
43
- type: 'container_file_citation',
44
- container_id: 'cntr_1',
45
- file_id: 'cfile_2',
46
- filename: 'Notes.pdf',
47
- start_index: 0,
48
- end_index: 0,
49
- },
50
- ],
51
- },
52
- ],
53
- },
54
- ],
55
- } as unknown as Response;
56
-
57
- expect(extractContainerGeneratedFiles(response, 'xlsx')).toEqual([
58
- {
59
- containerId: 'cntr_1',
60
- fileId: 'cfile_1',
61
- filename: 'Grades.xlsx',
62
- },
63
- ]);
64
- });
65
- });