@studious-lms/server 1.14.0 → 1.15.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 (196) 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/lib/inferDocumentFormat.d.ts +10 -0
  5. package/dist/lib/inferDocumentFormat.d.ts.map +1 -0
  6. package/dist/lib/inferDocumentFormat.js +48 -0
  7. package/dist/lib/inferDocumentFormat.js.map +1 -0
  8. package/dist/lib/legal.d.ts +3 -0
  9. package/dist/lib/legal.d.ts.map +1 -0
  10. package/dist/lib/legal.js +6 -0
  11. package/dist/lib/legal.js.map +1 -0
  12. package/dist/models/announcement.d.ts +47 -45
  13. package/dist/models/announcement.d.ts.map +1 -1
  14. package/dist/models/assignment.d.ts +75 -75
  15. package/dist/models/auth.d.ts +8 -0
  16. package/dist/models/auth.d.ts.map +1 -1
  17. package/dist/models/auth.js +4 -2
  18. package/dist/models/auth.js.map +1 -1
  19. package/dist/models/class.d.ts +13 -13
  20. package/dist/models/comment.d.ts +6 -6
  21. package/dist/models/conversation.d.ts +23 -23
  22. package/dist/models/event.d.ts +20 -18
  23. package/dist/models/event.d.ts.map +1 -1
  24. package/dist/models/file.d.ts +33 -9
  25. package/dist/models/file.d.ts.map +1 -1
  26. package/dist/models/labChat.d.ts +4 -4
  27. package/dist/models/marketing.d.ts +2 -15
  28. package/dist/models/marketing.d.ts.map +1 -1
  29. package/dist/models/marketing.js +12 -4
  30. package/dist/models/marketing.js.map +1 -1
  31. package/dist/models/message.d.ts +13 -13
  32. package/dist/models/newtonChat.d.ts +3 -3
  33. package/dist/models/notification.d.ts +4 -4
  34. package/dist/models/worksheet.d.ts +121 -121
  35. package/dist/pipelines/aiLabChat.d.ts +7 -1
  36. package/dist/pipelines/aiLabChat.d.ts.map +1 -1
  37. package/dist/pipelines/aiLabChat.js +153 -67
  38. package/dist/pipelines/aiLabChat.js.map +1 -1
  39. package/dist/pipelines/aiLabChatContract.d.ts +34 -195
  40. package/dist/pipelines/aiLabChatContract.d.ts.map +1 -1
  41. package/dist/pipelines/aiLabChatContract.js +9 -19
  42. package/dist/pipelines/aiLabChatContract.js.map +1 -1
  43. package/dist/pipelines/gradeWorksheet.d.ts +6 -6
  44. package/dist/pipelines/labChatPrompt.d.ts +8 -3
  45. package/dist/pipelines/labChatPrompt.d.ts.map +1 -1
  46. package/dist/pipelines/labChatPrompt.js +52 -14
  47. package/dist/pipelines/labChatPrompt.js.map +1 -1
  48. package/dist/pipelines/pdfToWorksheet.d.ts +1 -1
  49. package/dist/routers/_app.d.ts +750 -546
  50. package/dist/routers/_app.d.ts.map +1 -1
  51. package/dist/routers/announcement.d.ts +40 -40
  52. package/dist/routers/assignment.d.ts +131 -121
  53. package/dist/routers/assignment.d.ts.map +1 -1
  54. package/dist/routers/auth.d.ts +2 -0
  55. package/dist/routers/auth.d.ts.map +1 -1
  56. package/dist/routers/auth.js +15 -2
  57. package/dist/routers/auth.js.map +1 -1
  58. package/dist/routers/class.d.ts +12 -8
  59. package/dist/routers/class.d.ts.map +1 -1
  60. package/dist/routers/comment.d.ts +5 -5
  61. package/dist/routers/conversation.d.ts +6 -6
  62. package/dist/routers/discussion.d.ts +3 -3
  63. package/dist/routers/embedding.d.ts +6 -2
  64. package/dist/routers/embedding.d.ts.map +1 -1
  65. package/dist/routers/embedding.js +44 -36
  66. package/dist/routers/embedding.js.map +1 -1
  67. package/dist/routers/event.d.ts +20 -18
  68. package/dist/routers/event.d.ts.map +1 -1
  69. package/dist/routers/file.d.ts +2 -2
  70. package/dist/routers/labChat.d.ts +3 -3
  71. package/dist/routers/marketing.d.ts +0 -13
  72. package/dist/routers/marketing.d.ts.map +1 -1
  73. package/dist/routers/notifications.d.ts +4 -4
  74. package/dist/routers/school.d.ts +4 -0
  75. package/dist/routers/school.d.ts.map +1 -1
  76. package/dist/routers/search.d.ts +2 -2
  77. package/dist/routers/user.d.ts +89 -0
  78. package/dist/routers/user.d.ts.map +1 -1
  79. package/dist/routers/user.js +19 -2
  80. package/dist/routers/user.js.map +1 -1
  81. package/dist/routers/worksheet.d.ts +44 -44
  82. package/dist/routes/aiStream.d.ts.map +1 -1
  83. package/dist/routes/aiStream.js +41 -11
  84. package/dist/routes/aiStream.js.map +1 -1
  85. package/dist/seedDatabase.d.ts +3 -1
  86. package/dist/seedDatabase.d.ts.map +1 -1
  87. package/dist/services/aiChatStream.d.ts +20 -0
  88. package/dist/services/aiChatStream.d.ts.map +1 -1
  89. package/dist/services/aiChatStream.js +110 -13
  90. package/dist/services/aiChatStream.js.map +1 -1
  91. package/dist/services/aiModifierTools.d.ts +0 -16
  92. package/dist/services/aiModifierTools.d.ts.map +1 -1
  93. package/dist/services/aiModifierTools.js +112 -3
  94. package/dist/services/aiModifierTools.js.map +1 -1
  95. package/dist/services/aiProposalTools.d.ts.map +1 -1
  96. package/dist/services/aiProposalTools.js +25 -25
  97. package/dist/services/aiProposalTools.js.map +1 -1
  98. package/dist/services/aiTools.js +3 -3
  99. package/dist/services/aiTools.js.map +1 -1
  100. package/dist/services/announcement.d.ts +34 -34
  101. package/dist/services/assignment.d.ts +121 -111
  102. package/dist/services/assignment.d.ts.map +1 -1
  103. package/dist/services/assignment.js +4 -2
  104. package/dist/services/assignment.js.map +1 -1
  105. package/dist/services/auth.d.ts +2 -0
  106. package/dist/services/auth.d.ts.map +1 -1
  107. package/dist/services/auth.js +13 -4
  108. package/dist/services/auth.js.map +1 -1
  109. package/dist/services/class.d.ts +10 -6
  110. package/dist/services/class.d.ts.map +1 -1
  111. package/dist/services/class.js +7 -3
  112. package/dist/services/class.js.map +1 -1
  113. package/dist/services/comment.d.ts +5 -5
  114. package/dist/services/conversation.d.ts +6 -6
  115. package/dist/services/discussion.d.ts.map +1 -1
  116. package/dist/services/discussion.js +9 -2
  117. package/dist/services/discussion.js.map +1 -1
  118. package/dist/services/embedding.d.ts +19 -1
  119. package/dist/services/embedding.d.ts.map +1 -1
  120. package/dist/services/embedding.js +154 -8
  121. package/dist/services/embedding.js.map +1 -1
  122. package/dist/services/event.d.ts +20 -18
  123. package/dist/services/event.d.ts.map +1 -1
  124. package/dist/services/file.d.ts +4 -4
  125. package/dist/services/folder.d.ts.map +1 -1
  126. package/dist/services/folder.js +32 -2
  127. package/dist/services/folder.js.map +1 -1
  128. package/dist/services/labChat.d.ts +3 -3
  129. package/dist/services/marketing.d.ts +1 -14
  130. package/dist/services/marketing.d.ts.map +1 -1
  131. package/dist/services/marketing.js +5 -5
  132. package/dist/services/marketing.js.map +1 -1
  133. package/dist/services/notification.d.ts +4 -4
  134. package/dist/services/openaiDocumentGen.d.ts +37 -0
  135. package/dist/services/openaiDocumentGen.d.ts.map +1 -0
  136. package/dist/services/openaiDocumentGen.js +262 -0
  137. package/dist/services/openaiDocumentGen.js.map +1 -0
  138. package/dist/services/school.d.ts +4 -0
  139. package/dist/services/school.d.ts.map +1 -1
  140. package/dist/services/search.d.ts +1 -1
  141. package/dist/services/search.d.ts.map +1 -1
  142. package/dist/services/search.js +27 -2
  143. package/dist/services/search.js.map +1 -1
  144. package/dist/services/userPrivacy.d.ts +86 -0
  145. package/dist/services/userPrivacy.d.ts.map +1 -0
  146. package/dist/services/userPrivacy.js +184 -0
  147. package/dist/services/userPrivacy.js.map +1 -0
  148. package/dist/services/worksheet.d.ts +66 -49
  149. package/dist/services/worksheet.d.ts.map +1 -1
  150. package/dist/services/worksheet.js +48 -3
  151. package/dist/services/worksheet.js.map +1 -1
  152. package/dist/utils/email.d.ts.map +1 -1
  153. package/dist/utils/email.js +7 -3
  154. package/dist/utils/email.js.map +1 -1
  155. package/dist/workers/processors/embedContent.d.ts.map +1 -1
  156. package/dist/workers/processors/embedContent.js +16 -25
  157. package/dist/workers/processors/embedContent.js.map +1 -1
  158. package/package.json +1 -1
  159. package/prisma/migrations/20260610140000_expand_embedding_source_types/migration.sql +4 -0
  160. package/prisma/migrations/20260611120000_add_user_terms_acceptance/migration.sql +3 -0
  161. package/prisma/schema.prisma +6 -0
  162. package/src/instrument.ts +1 -1
  163. package/src/lib/inferDocumentFormat.ts +58 -0
  164. package/src/lib/legal.ts +2 -0
  165. package/src/models/auth.ts +4 -0
  166. package/src/models/marketing.ts +10 -2
  167. package/src/pipelines/aiLabChat.ts +198 -74
  168. package/src/pipelines/aiLabChatContract.ts +7 -17
  169. package/src/pipelines/labChatPrompt.ts +64 -14
  170. package/src/routers/auth.ts +13 -0
  171. package/src/routers/embedding.ts +44 -39
  172. package/src/routers/user.ts +26 -0
  173. package/src/routes/aiStream.ts +47 -9
  174. package/src/services/aiChatStream.ts +133 -11
  175. package/src/services/aiModifierTools.ts +154 -1
  176. package/src/services/aiProposalTools.ts +27 -25
  177. package/src/services/aiTools.ts +1 -1
  178. package/src/services/assignment.ts +3 -0
  179. package/src/services/auth.ts +14 -2
  180. package/src/services/class.ts +7 -1
  181. package/src/services/discussion.ts +13 -0
  182. package/src/services/embedding.ts +167 -6
  183. package/src/services/folder.ts +31 -0
  184. package/src/services/marketing.ts +3 -3
  185. package/src/services/openaiDocumentGen.ts +319 -0
  186. package/src/services/search.ts +25 -1
  187. package/src/services/userPrivacy.ts +196 -0
  188. package/src/services/worksheet.ts +68 -1
  189. package/src/utils/email.ts +5 -1
  190. package/src/workers/processors/embedContent.ts +18 -22
  191. package/tests/helpers/registerInput.ts +20 -0
  192. package/tests/integration/globalSetup.ts +10 -18
  193. package/tests/integration/routers/auth.test.ts +13 -18
  194. package/tests/unit/lib/inferDocumentFormat.test.ts +33 -0
  195. package/tests/unit/services/aiChatStream.test.ts +92 -0
  196. package/tests/unit/services/openaiDocumentGen.test.ts +65 -0
@@ -1,12 +1,17 @@
1
1
  /**
2
2
  * Background worker for embedding content into pgvector.
3
- * Handles indexing of files, assignments, worksheets, and announcements.
3
+ * Handles indexing of files, assignments, worksheets, announcements,
4
+ * discussions, submissions, and syllabi.
4
5
  */
5
6
  import { createRunJobWorker } from './createRunJobWorker.js';
6
7
  import {
7
8
  indexAssignment,
8
9
  indexWorksheet,
9
10
  indexAnnouncement,
11
+ indexDiscussion,
12
+ indexSubmission,
13
+ indexSyllabus,
14
+ indexFile,
10
15
  indexContent,
11
16
  } from '../../services/embedding.js';
12
17
  import { prisma } from '../../lib/prisma.js';
@@ -35,30 +40,21 @@ export const embedContentWorker = createRunJobWorker({
35
40
  await indexAnnouncement(classId, sourceId);
36
41
  break;
37
42
 
38
- case 'FILE': {
39
- // For files, fetch the file metadata and content
40
- const file = await prisma.file.findUnique({
41
- where: { id: sourceId },
42
- select: { name: true, path: true, type: true },
43
- });
43
+ case 'FILE':
44
+ await indexFile(classId, sourceId);
45
+ break;
44
46
 
45
- if (!file) {
46
- logger.warn('File not found for embedding', { sourceId });
47
- return { skipped: true, reason: 'file_not_found' };
48
- }
47
+ case 'DISCUSSION':
48
+ await indexDiscussion(classId, sourceId);
49
+ break;
49
50
 
50
- // For now, index the filename and any text-based content.
51
- // PDF extraction would be added here with pdf-parse.
52
- const text = `File: ${file.name}\nType: ${file.type}\nPath: ${file.path}`;
53
- await indexContent({
54
- classId,
55
- sourceType: 'FILE',
56
- sourceId,
57
- text,
58
- metadata: { name: file.name, type: file.type },
59
- });
51
+ case 'SUBMISSION':
52
+ await indexSubmission(classId, sourceId);
53
+ break;
54
+
55
+ case 'SYLLABUS':
56
+ await indexSyllabus(sourceId);
60
57
  break;
61
- }
62
58
 
63
59
  case 'SECTION': {
64
60
  const section = await prisma.section.findUnique({
@@ -0,0 +1,20 @@
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
+ }
@@ -10,6 +10,7 @@ import { createTRPCContext } from '../../src/trpc';
10
10
  import { clearDatabase } from '../../src/seedDatabase';
11
11
  import { execSync } from 'child_process';
12
12
  import { SessionType } from '@prisma/client';
13
+ import { registerInput } from '../helpers/registerInput.js';
13
14
 
14
15
  async function runSetup() {
15
16
  const ctx = await createTRPCContext({
@@ -20,26 +21,17 @@ async function runSetup() {
20
21
 
21
22
  execSync('npm run test:pre');
22
23
 
23
- const user1 = await caller.auth.register({
24
- username: 'testuser1',
25
- email: 'test@studious.sh',
26
- password: 'password_is_1234',
27
- confirmPassword: 'password_is_1234',
28
- });
24
+ const user1 = await caller.auth.register(
25
+ registerInput({ username: 'testuser1', email: 'test@studious.sh' }),
26
+ );
29
27
 
30
- const user2 = await caller.auth.register({
31
- username: 'testuser2',
32
- email: 'test2@studious.sh',
33
- password: 'password_is_1234',
34
- confirmPassword: 'password_is_1234',
35
- });
28
+ const user2 = await caller.auth.register(
29
+ registerInput({ username: 'testuser2', email: 'test2@studious.sh' }),
30
+ );
36
31
 
37
- const user3 = await caller.auth.register({
38
- username: 'testuser3',
39
- email: 'test3@studious.sh',
40
- password: 'password_is_1234',
41
- confirmPassword: 'password_is_1234',
42
- });
32
+ const user3 = await caller.auth.register(
33
+ registerInput({ username: 'testuser3', email: 'test3@studious.sh' }),
34
+ );
43
35
 
44
36
  const session1 = await prisma.session.findFirst({
45
37
  where: { userId: user1.user.id, type: SessionType.EMAIL_VERIFICATION },
@@ -6,6 +6,7 @@ import { test, expect, describe } from 'vitest';
6
6
  import { appRouter } from '../../../src/routers/_app';
7
7
  import { createTRPCContext } from '../../../src/trpc';
8
8
  import { prisma } from '../../../src/lib/prisma';
9
+ import { registerInput } from '../../helpers/registerInput.js';
9
10
  import {
10
11
  caller,
11
12
  login1,
@@ -64,12 +65,9 @@ describe('Auth Router', () => {
64
65
  test('check fails when unauthenticated', () => expectUnauth((c) => c.auth.check()));
65
66
 
66
67
  test('resendVerificationEmail succeeds for existing user', async () => {
67
- await caller.auth.register({
68
- username: 'resendtest',
69
- email: 'resend@studious.sh',
70
- password: 'password_is_1234',
71
- confirmPassword: 'password_is_1234',
72
- });
68
+ await caller.auth.register(
69
+ registerInput({ username: 'resendtest', email: 'resend@studious.sh' }),
70
+ );
73
71
  const result = await caller.auth.resendVerificationEmail({
74
72
  email: 'resend@studious.sh',
75
73
  });
@@ -227,23 +225,20 @@ describe('Auth Router', () => {
227
225
 
228
226
  test('register fails when username already exists', async () => {
229
227
  await expect(
230
- caller.auth.register({
231
- username: 'testuser1',
232
- email: 'newemail@studious.sh',
233
- password: 'password_is_1234',
234
- confirmPassword: 'password_is_1234',
235
- })
228
+ caller.auth.register(
229
+ registerInput({
230
+ username: 'testuser1',
231
+ email: 'newemail@studious.sh',
232
+ }),
233
+ )
236
234
  ).rejects.toThrow();
237
235
  });
238
236
 
239
237
  test('register fails when email already exists', async () => {
240
238
  await expect(
241
- caller.auth.register({
242
- username: 'newuser',
243
- email: 'test@studious.sh',
244
- password: 'password_is_1234',
245
- confirmPassword: 'password_is_1234',
246
- })
239
+ caller.auth.register(
240
+ registerInput({ username: 'newuser', email: 'test@studious.sh' }),
241
+ )
247
242
  ).rejects.toThrow();
248
243
  });
249
244
 
@@ -0,0 +1,33 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { inferDocumentFormat } from '../../../src/lib/inferDocumentFormat.js';
3
+
4
+ describe('inferDocumentFormat', () => {
5
+ it('respects explicit non-pdf format', () => {
6
+ expect(inferDocumentFormat('Title', 'body', 'xlsx')).toBe('xlsx');
7
+ });
8
+
9
+ it('overrides model pdf when title says presentation', () => {
10
+ expect(
11
+ inferDocumentFormat(
12
+ 'Volume of Revolution Presentation',
13
+ 'Slide 1: intro',
14
+ 'pdf',
15
+ ),
16
+ ).toBe('pptx');
17
+ });
18
+
19
+ it('uses teacher message for format when model picked pdf', () => {
20
+ expect(
21
+ inferDocumentFormat(
22
+ 'Calculus Unit 3',
23
+ 'Content here',
24
+ 'pdf',
25
+ 'Can you make a PowerPoint for this lesson?',
26
+ ),
27
+ ).toBe('pptx');
28
+ });
29
+
30
+ it('defaults to pdf for generic handouts', () => {
31
+ expect(inferDocumentFormat('Reading Guide', 'Chapter summary', 'pdf')).toBe('pdf');
32
+ });
33
+ });
@@ -0,0 +1,92 @@
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
+ });
@@ -0,0 +1,65 @@
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
+ });