@studious-lms/server 1.16.0 → 1.16.1
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/.github/workflows/deploy.yml +1 -1
- package/dist/instrument.js +3 -3
- package/dist/instrument.js.map +1 -1
- package/dist/lib/chatAttachmentPrompt.d.ts +47 -0
- package/dist/lib/chatAttachmentPrompt.d.ts.map +1 -0
- package/dist/lib/chatAttachmentPrompt.js +190 -0
- package/dist/lib/chatAttachmentPrompt.js.map +1 -0
- package/dist/lib/inferDocumentFormat.js +3 -3
- package/dist/lib/inferDocumentFormat.js.map +1 -1
- package/dist/lib/legal.d.ts +3 -0
- package/dist/lib/legal.d.ts.map +1 -0
- package/dist/lib/legal.js +6 -0
- package/dist/lib/legal.js.map +1 -0
- package/dist/models/announcement.d.ts +2 -0
- package/dist/models/announcement.d.ts.map +1 -1
- package/dist/models/auth.d.ts +8 -0
- package/dist/models/auth.d.ts.map +1 -1
- package/dist/models/auth.js +4 -2
- package/dist/models/auth.js.map +1 -1
- package/dist/models/event.d.ts +2 -0
- package/dist/models/event.d.ts.map +1 -1
- package/dist/models/file.d.ts +24 -0
- package/dist/models/file.d.ts.map +1 -1
- package/dist/models/marketing.d.ts +2 -15
- package/dist/models/marketing.d.ts.map +1 -1
- package/dist/models/marketing.js +12 -4
- package/dist/models/marketing.js.map +1 -1
- package/dist/pipelines/aiLabChat.d.ts.map +1 -1
- package/dist/pipelines/aiLabChat.js +39 -59
- package/dist/pipelines/aiLabChat.js.map +1 -1
- package/dist/pipelines/aiLabChatContract.d.ts +2 -2
- package/dist/pipelines/aiNewtonChat.d.ts +1 -1
- package/dist/pipelines/aiNewtonChat.d.ts.map +1 -1
- package/dist/pipelines/aiNewtonChat.js +20 -19
- package/dist/pipelines/aiNewtonChat.js.map +1 -1
- package/dist/pipelines/gradeWorksheet.d.ts +1 -0
- package/dist/pipelines/gradeWorksheet.d.ts.map +1 -1
- package/dist/pipelines/gradeWorksheet.js +84 -13
- package/dist/pipelines/gradeWorksheet.js.map +1 -1
- package/dist/pipelines/labChatPrompt.d.ts +4 -20
- package/dist/pipelines/labChatPrompt.d.ts.map +1 -1
- package/dist/pipelines/labChatPrompt.js +24 -64
- package/dist/pipelines/labChatPrompt.js.map +1 -1
- package/dist/routers/_app.d.ts +236 -26
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/assignment.d.ts +10 -0
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/auth.d.ts +2 -0
- package/dist/routers/auth.d.ts.map +1 -1
- package/dist/routers/auth.js +15 -2
- package/dist/routers/auth.js.map +1 -1
- package/dist/routers/class.d.ts +4 -0
- package/dist/routers/class.d.ts.map +1 -1
- package/dist/routers/event.d.ts +2 -0
- package/dist/routers/event.d.ts.map +1 -1
- package/dist/routers/marketing.d.ts +0 -13
- package/dist/routers/marketing.d.ts.map +1 -1
- package/dist/routers/school.d.ts +4 -0
- package/dist/routers/school.d.ts.map +1 -1
- package/dist/routers/user.d.ts +89 -0
- package/dist/routers/user.d.ts.map +1 -1
- package/dist/routers/user.js +19 -2
- package/dist/routers/user.js.map +1 -1
- package/dist/routers/worksheet.d.ts +7 -0
- package/dist/routers/worksheet.d.ts.map +1 -1
- package/dist/routers/worksheet.js +8 -3
- package/dist/routers/worksheet.js.map +1 -1
- package/dist/seedDatabase.d.ts +2 -0
- package/dist/seedDatabase.d.ts.map +1 -1
- package/dist/services/aiChatStream.d.ts +2 -0
- package/dist/services/aiChatStream.d.ts.map +1 -1
- package/dist/services/aiChatStream.js +31 -4
- package/dist/services/aiChatStream.js.map +1 -1
- package/dist/services/aiProposalTools.d.ts.map +1 -1
- package/dist/services/aiProposalTools.js +9 -9
- package/dist/services/aiProposalTools.js.map +1 -1
- package/dist/services/aiTools.js +3 -3
- package/dist/services/aiTools.js.map +1 -1
- package/dist/services/assignment.d.ts +10 -0
- package/dist/services/assignment.d.ts.map +1 -1
- package/dist/services/auth.d.ts +2 -0
- package/dist/services/auth.d.ts.map +1 -1
- package/dist/services/auth.js +13 -4
- package/dist/services/auth.js.map +1 -1
- package/dist/services/class.d.ts +4 -0
- package/dist/services/class.d.ts.map +1 -1
- package/dist/services/event.d.ts +2 -0
- package/dist/services/event.d.ts.map +1 -1
- package/dist/services/marketing.d.ts +1 -14
- package/dist/services/marketing.d.ts.map +1 -1
- package/dist/services/marketing.js +5 -5
- package/dist/services/marketing.js.map +1 -1
- package/dist/services/newtonChat.d.ts.map +1 -1
- package/dist/services/newtonChat.js +2 -8
- package/dist/services/newtonChat.js.map +1 -1
- package/dist/services/openaiDocumentGen.d.ts +12 -0
- package/dist/services/openaiDocumentGen.d.ts.map +1 -1
- package/dist/services/openaiDocumentGen.js +173 -65
- package/dist/services/openaiDocumentGen.js.map +1 -1
- package/dist/services/school.d.ts +4 -0
- package/dist/services/school.d.ts.map +1 -1
- package/dist/services/userPrivacy.d.ts +86 -0
- package/dist/services/userPrivacy.d.ts.map +1 -0
- package/dist/services/userPrivacy.js +184 -0
- package/dist/services/userPrivacy.js.map +1 -0
- package/dist/services/worksheet.d.ts +1 -0
- package/dist/services/worksheet.d.ts.map +1 -1
- package/dist/services/worksheet.js +13 -3
- package/dist/services/worksheet.js.map +1 -1
- package/dist/utils/email.d.ts.map +1 -1
- package/dist/utils/email.js +7 -3
- package/dist/utils/email.js.map +1 -1
- package/dist/workers/processors/aiNewtonChat.d.ts.map +1 -1
- package/dist/workers/processors/aiNewtonChat.js +3 -3
- package/dist/workers/processors/aiNewtonChat.js.map +1 -1
- package/package.json +2 -1
- package/prisma/migrations/20260611120000_add_user_terms_acceptance/migration.sql +3 -0
- package/prisma/schema.prisma +3 -0
- package/src/instrument.ts +1 -1
- package/src/lib/chatAttachmentPrompt.ts +270 -0
- package/src/lib/inferDocumentFormat.ts +1 -1
- package/src/lib/legal.ts +2 -0
- package/src/models/auth.ts +4 -0
- package/src/models/marketing.ts +10 -2
- package/src/pipelines/aiLabChat.ts +46 -71
- package/src/pipelines/aiNewtonChat.ts +20 -23
- package/src/pipelines/gradeWorksheet.ts +87 -11
- package/src/pipelines/labChatPrompt.ts +29 -98
- package/src/routers/auth.ts +13 -0
- package/src/routers/user.ts +26 -0
- package/src/routers/worksheet.ts +11 -0
- package/src/services/aiChatStream.ts +30 -3
- package/src/services/aiProposalTools.ts +8 -7
- package/src/services/aiTools.ts +1 -1
- package/src/services/auth.ts +14 -2
- package/src/services/marketing.ts +3 -3
- package/src/services/newtonChat.ts +0 -7
- package/src/services/openaiDocumentGen.ts +190 -66
- package/src/services/userPrivacy.ts +196 -0
- package/src/services/worksheet.ts +20 -1
- package/src/utils/email.ts +5 -1
- package/src/workers/processors/aiNewtonChat.ts +0 -1
- package/tests/helpers/registerInput.ts +20 -0
- package/tests/integration/globalSetup.ts +10 -18
- package/tests/integration/routers/auth.test.ts +13 -18
- package/tests/integration/routers/newtonChat.test.ts +9 -8
- package/tests/unit/lib/chatAttachmentPrompt.test.ts +135 -0
- package/tests/unit/lib/inferDocumentFormat.test.ts +10 -0
- package/tests/unit/pipelines/labChatPrompt.test.ts +6 -6
- package/tests/unit/services/aiChatStream.test.ts +92 -0
- package/tests/unit/services/openaiDocumentGen.test.ts +65 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
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
|
|
|
@@ -50,7 +50,7 @@ describe('Newton Chat Router', () => {
|
|
|
50
50
|
conversationId = tutorConversation.conversationId;
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
-
test('
|
|
53
|
+
test('allows attachment-only Newton chat posts', async () => {
|
|
54
54
|
const [uploadTarget] = await user2Caller.message.getUploadUrls({
|
|
55
55
|
conversationId,
|
|
56
56
|
files: [
|
|
@@ -70,12 +70,13 @@ describe('Newton Chat Router', () => {
|
|
|
70
70
|
},
|
|
71
71
|
});
|
|
72
72
|
|
|
73
|
-
await
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
).
|
|
73
|
+
const message = await user2Caller.newtonChat.postToNewtonChat({
|
|
74
|
+
newtonChatId,
|
|
75
|
+
content: ' ',
|
|
76
|
+
fileIds: [uploadTarget!.id],
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
expect(message.attachments).toHaveLength(1);
|
|
80
|
+
expect(message.attachments[0]?.name).toBe('newton-attachment-only.pdf');
|
|
80
81
|
});
|
|
81
82
|
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
buildUserMessageContentWithAttachments,
|
|
4
|
+
loadChatAttachments,
|
|
5
|
+
} from '../../../src/lib/chatAttachmentPrompt.js';
|
|
6
|
+
|
|
7
|
+
const pdfBytes = new Uint8Array([0x25, 0x50, 0x44, 0x46]);
|
|
8
|
+
const docxText = 'Unit 3 lesson plan content';
|
|
9
|
+
|
|
10
|
+
vi.mock('officeparser', () => ({
|
|
11
|
+
OfficeParser: {
|
|
12
|
+
parseOffice: vi.fn(async () => ({
|
|
13
|
+
toText: () => docxText,
|
|
14
|
+
})),
|
|
15
|
+
},
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
vi.mock('../../../src/lib/googleCloudStorage.js', () => ({
|
|
19
|
+
bucket: {
|
|
20
|
+
file: vi.fn((path: string) => ({
|
|
21
|
+
download: vi.fn(async () => {
|
|
22
|
+
if (path.includes('missing')) {
|
|
23
|
+
throw new Error('not found');
|
|
24
|
+
}
|
|
25
|
+
if (path.endsWith('.docx')) {
|
|
26
|
+
return [Buffer.from('fake-docx-bytes')];
|
|
27
|
+
}
|
|
28
|
+
return [Buffer.from(pdfBytes)];
|
|
29
|
+
}),
|
|
30
|
+
})),
|
|
31
|
+
},
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
describe('buildUserMessageContentWithAttachments', () => {
|
|
35
|
+
it('returns plain text when there are no attachments', () => {
|
|
36
|
+
expect(buildUserMessageContentWithAttachments('Hello', [])).toBe('Hello');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('includes PDFs as inline file parts with bytes', () => {
|
|
40
|
+
const content = buildUserMessageContentWithAttachments('Review this', [
|
|
41
|
+
{
|
|
42
|
+
id: '1',
|
|
43
|
+
name: 'essay.pdf',
|
|
44
|
+
type: 'application/pdf',
|
|
45
|
+
path: 'essay.pdf',
|
|
46
|
+
data: pdfBytes,
|
|
47
|
+
},
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
expect(content).toEqual([
|
|
51
|
+
{ type: 'text', text: 'Review this' },
|
|
52
|
+
{
|
|
53
|
+
type: 'file',
|
|
54
|
+
data: pdfBytes,
|
|
55
|
+
mediaType: 'application/pdf',
|
|
56
|
+
filename: 'essay.pdf',
|
|
57
|
+
},
|
|
58
|
+
]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('includes extracted docx text in the message body', () => {
|
|
62
|
+
const content = buildUserMessageContentWithAttachments('Summarize', [
|
|
63
|
+
{
|
|
64
|
+
id: '2',
|
|
65
|
+
name: 'plan.docx',
|
|
66
|
+
type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
67
|
+
path: 'plan.docx',
|
|
68
|
+
extractedText: docxText,
|
|
69
|
+
},
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
expect(content).toBe(
|
|
73
|
+
`Summarize\n\n[Content of plan.docx]:\n${docxText}`,
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('lists unsupported file types when extraction failed', () => {
|
|
78
|
+
const content = buildUserMessageContentWithAttachments('Here', [
|
|
79
|
+
{
|
|
80
|
+
id: '3',
|
|
81
|
+
name: 'clip.mp4',
|
|
82
|
+
type: 'video/mp4',
|
|
83
|
+
path: 'clip.mp4',
|
|
84
|
+
},
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
expect(content).toBe(
|
|
88
|
+
'Here\n[Attached files (content not extracted): clip.mp4 (video/mp4)]',
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('loadChatAttachments', () => {
|
|
94
|
+
it('loads bytes and extracts office document text', async () => {
|
|
95
|
+
const resolved = await loadChatAttachments([
|
|
96
|
+
{
|
|
97
|
+
id: '1',
|
|
98
|
+
name: 'plan.docx',
|
|
99
|
+
type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
100
|
+
path: 'uploads/plan.docx',
|
|
101
|
+
},
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
expect(resolved[0]?.data).toBeDefined();
|
|
105
|
+
expect(resolved[0]?.extractedText).toBe(docxText);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('loads pdf bytes without extracted text', async () => {
|
|
109
|
+
const resolved = await loadChatAttachments([
|
|
110
|
+
{
|
|
111
|
+
id: '2',
|
|
112
|
+
name: 'scan.pdf',
|
|
113
|
+
type: 'application/pdf',
|
|
114
|
+
path: 'uploads/scan.pdf',
|
|
115
|
+
},
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
expect(resolved[0]?.data).toEqual(pdfBytes);
|
|
119
|
+
expect(resolved[0]?.extractedText).toBeUndefined();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('skips download for video attachments', async () => {
|
|
123
|
+
const resolved = await loadChatAttachments([
|
|
124
|
+
{
|
|
125
|
+
id: '3',
|
|
126
|
+
name: 'clip.mp4',
|
|
127
|
+
type: 'video/mp4',
|
|
128
|
+
path: 'uploads/clip.mp4',
|
|
129
|
+
},
|
|
130
|
+
]);
|
|
131
|
+
|
|
132
|
+
expect(resolved[0]?.data).toBeUndefined();
|
|
133
|
+
expect(resolved[0]?.extractedText).toBeUndefined();
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -30,4 +30,14 @@ describe('inferDocumentFormat', () => {
|
|
|
30
30
|
it('defaults to pdf for generic handouts', () => {
|
|
31
31
|
expect(inferDocumentFormat('Reading Guide', 'Chapter summary', 'pdf')).toBe('pdf');
|
|
32
32
|
});
|
|
33
|
+
|
|
34
|
+
it('overrides pdf when content mentions a flowchart', () => {
|
|
35
|
+
expect(
|
|
36
|
+
inferDocumentFormat(
|
|
37
|
+
'Photosynthesis Overview',
|
|
38
|
+
'Include a flowchart of the light reactions.',
|
|
39
|
+
'pdf',
|
|
40
|
+
),
|
|
41
|
+
).toBe('png');
|
|
42
|
+
});
|
|
33
43
|
});
|
|
@@ -17,8 +17,8 @@ describe("buildLabChatAgentSystemPrompt", () => {
|
|
|
17
17
|
});
|
|
18
18
|
|
|
19
19
|
describe("buildLabChatAgentMessages", () => {
|
|
20
|
-
test("folds the class context into the system prompt, not the message list", () => {
|
|
21
|
-
const { system, messages } = buildLabChatAgentMessages({
|
|
20
|
+
test("folds the class context into the system prompt, not the message list", async () => {
|
|
21
|
+
const { system, messages } = await buildLabChatAgentMessages({
|
|
22
22
|
context: "Topic context",
|
|
23
23
|
classContext: "CLASS: Biology",
|
|
24
24
|
recentMessages: [
|
|
@@ -41,8 +41,8 @@ describe("buildLabChatAgentMessages", () => {
|
|
|
41
41
|
]);
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
-
test("sorts timestamped history into chronological order before prompt assembly", () => {
|
|
45
|
-
const { messages } = buildLabChatAgentMessages({
|
|
44
|
+
test("sorts timestamped history into chronological order before prompt assembly", async () => {
|
|
45
|
+
const { messages } = await buildLabChatAgentMessages({
|
|
46
46
|
context: "Topic context",
|
|
47
47
|
classContext: "CLASS: Biology",
|
|
48
48
|
recentMessages: [
|
|
@@ -76,8 +76,8 @@ describe("buildLabChatAgentMessages", () => {
|
|
|
76
76
|
});
|
|
77
77
|
});
|
|
78
78
|
|
|
79
|
-
test("keeps original order among messages without timestamps", () => {
|
|
80
|
-
const { messages } = buildLabChatAgentMessages({
|
|
79
|
+
test("keeps original order among messages without timestamps", async () => {
|
|
80
|
+
const { messages } = await buildLabChatAgentMessages({
|
|
81
81
|
context: "Topic context",
|
|
82
82
|
classContext: "CLASS: Biology",
|
|
83
83
|
recentMessages: [
|
|
@@ -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
|
+
});
|