Skip to content

Commit 61a7710

Browse files
committed
Add tests for agent file attachments
1 parent 8cf8da2 commit 61a7710

2 files changed

Lines changed: 162 additions & 1 deletion

File tree

apps/sim/executor/handlers/agent/agent-handler.test.ts

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,20 @@ import { BlockType, isMcpTool } from '@/executor/constants'
55
import { AgentBlockHandler } from '@/executor/handlers/agent/agent-handler'
66
import type { ExecutionContext, StreamingExecution } from '@/executor/types'
77
import { executeProviderRequest } from '@/providers'
8-
import { getProviderFromModel, transformBlockTool } from '@/providers/utils'
8+
import {
9+
getProviderFromModel,
10+
supportsFileAttachments,
11+
transformBlockTool,
12+
} from '@/providers/utils'
913
import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types'
1014
import { executeTool } from '@/tools'
1115

1216
process.env.NEXT_PUBLIC_APP_URL = 'http://localhost:3000'
1317

18+
const { mockReadUserFileContent } = vi.hoisted(() => ({
19+
mockReadUserFileContent: vi.fn(),
20+
}))
21+
1422
vi.mock('@/lib/core/config/feature-flags', () => ({
1523
isHosted: false,
1624
isProd: false,
@@ -26,6 +34,7 @@ vi.mock('@/lib/core/config/feature-flags', () => ({
2634

2735
vi.mock('@/providers/utils', () => ({
2836
getProviderFromModel: vi.fn().mockReturnValue('mock-provider'),
37+
supportsFileAttachments: vi.fn().mockReturnValue(false),
2938
transformBlockTool: vi.fn(),
3039
getBaseModelProviders: vi.fn().mockReturnValue({ openai: {}, anthropic: {} }),
3140
getApiKey: vi.fn().mockReturnValue('mock-api-key'),
@@ -45,6 +54,10 @@ vi.mock('@/providers/utils', () => ({
4554
}),
4655
}))
4756

57+
vi.mock('@/lib/execution/payloads/materialization.server', () => ({
58+
readUserFileContent: mockReadUserFileContent,
59+
}))
60+
4861
vi.mock('@/blocks', () => ({
4962
getAllBlocks: vi.fn().mockReturnValue([]),
5063
}))
@@ -113,6 +126,7 @@ setupGlobalFetchMock()
113126
const mockGetAllBlocks = getAllBlocks as Mock
114127
const mockExecuteTool = executeTool as Mock
115128
const mockGetProviderFromModel = getProviderFromModel as Mock
129+
const mockSupportsFileAttachments = supportsFileAttachments as Mock
116130
const mockTransformBlockTool = transformBlockTool as Mock
117131
const mockFetch = global.fetch as unknown as Mock
118132
const mockExecuteProviderRequest = executeProviderRequest as Mock
@@ -164,6 +178,8 @@ describe('AgentBlockHandler', () => {
164178
} as SerializedWorkflow,
165179
}
166180
mockGetProviderFromModel.mockReturnValue('mock-provider')
181+
mockSupportsFileAttachments.mockReturnValue(false)
182+
mockReadUserFileContent.mockResolvedValue('ZmlsZQ==')
167183

168184
mockExecuteProviderRequest.mockResolvedValue({
169185
content: 'Mocked response content',
@@ -1121,6 +1137,113 @@ describe('AgentBlockHandler', () => {
11211137
expect(requestBody.messages[6].content).toBe('Continue our conversation.')
11221138
})
11231139

1140+
it('should pass files messages as provider file attachments when the model supports files', async () => {
1141+
const inputs = {
1142+
model: 'gpt-4o',
1143+
messages: [
1144+
{ role: 'user' as const, content: 'Summarize the attached file.' },
1145+
{
1146+
role: 'files' as const,
1147+
files: [
1148+
{
1149+
name: 'brief.pdf',
1150+
path: '/api/files/serve/test-workspace/brief.pdf',
1151+
key: 'workspace/test-workspace/brief.pdf',
1152+
size: 128,
1153+
type: 'application/pdf',
1154+
},
1155+
],
1156+
},
1157+
],
1158+
apiKey: 'test-api-key',
1159+
}
1160+
1161+
mockGetProviderFromModel.mockReturnValue('openai')
1162+
mockSupportsFileAttachments.mockReturnValue(true)
1163+
mockReadUserFileContent.mockResolvedValue('cGRm')
1164+
1165+
await handler.execute(
1166+
{
1167+
...mockContext,
1168+
userId: 'test-user',
1169+
workspaceId: 'test-workspace',
1170+
executionId: 'test-execution',
1171+
},
1172+
mockBlock,
1173+
inputs
1174+
)
1175+
1176+
const requestBody = mockExecuteProviderRequest.mock.calls[0][1]
1177+
1178+
expect(requestBody.messages).toEqual([
1179+
{ role: 'user', content: 'Summarize the attached file.' },
1180+
])
1181+
expect(requestBody.fileAttachments).toEqual([
1182+
{
1183+
name: 'brief.pdf',
1184+
type: 'application/pdf',
1185+
base64: 'cGRm',
1186+
},
1187+
])
1188+
expect(mockReadUserFileContent).toHaveBeenCalledWith(
1189+
expect.objectContaining({
1190+
name: 'brief.pdf',
1191+
key: 'workspace/test-workspace/brief.pdf',
1192+
type: 'application/pdf',
1193+
}),
1194+
expect.objectContaining({
1195+
userId: 'test-user',
1196+
workspaceId: 'test-workspace',
1197+
executionId: 'test-execution',
1198+
encoding: 'base64',
1199+
})
1200+
)
1201+
})
1202+
1203+
it('should ignore files messages when the selected model does not support files', async () => {
1204+
const inputs = {
1205+
model: 'deepseek-chat',
1206+
messages: [
1207+
{ role: 'user' as const, content: 'Summarize the attached file.' },
1208+
{
1209+
role: 'files' as const,
1210+
files: [
1211+
{
1212+
name: 'brief.pdf',
1213+
path: '/api/files/serve/test-workspace/brief.pdf',
1214+
key: 'workspace/test-workspace/brief.pdf',
1215+
size: 128,
1216+
type: 'application/pdf',
1217+
},
1218+
],
1219+
},
1220+
],
1221+
apiKey: 'test-api-key',
1222+
}
1223+
1224+
mockGetProviderFromModel.mockReturnValue('deepseek')
1225+
mockSupportsFileAttachments.mockReturnValue(false)
1226+
1227+
await handler.execute(
1228+
{
1229+
...mockContext,
1230+
userId: 'test-user',
1231+
workspaceId: 'test-workspace',
1232+
executionId: 'test-execution',
1233+
},
1234+
mockBlock,
1235+
inputs
1236+
)
1237+
1238+
const requestBody = mockExecuteProviderRequest.mock.calls[0][1]
1239+
1240+
expect(requestBody.messages).toEqual([
1241+
{ role: 'user', content: 'Summarize the attached file.' },
1242+
])
1243+
expect(requestBody.fileAttachments).toBeUndefined()
1244+
expect(mockReadUserFileContent).not.toHaveBeenCalled()
1245+
})
1246+
11241247
it('should preserve multiple system messages when no explicit systemPrompt is provided', async () => {
11251248
const inputs = {
11261249
model: 'gpt-4o',
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { buildResponsesInputFromMessages } from '@/providers/openai/utils'
3+
import type { ProviderFileAttachment } from '@/providers/types'
4+
5+
describe('buildResponsesInputFromMessages', () => {
6+
it('attaches files to the latest user message', () => {
7+
const fileAttachments: ProviderFileAttachment[] = [
8+
{
9+
name: 'brief.pdf',
10+
type: 'application/pdf',
11+
base64: 'cGRm',
12+
},
13+
]
14+
15+
const input = buildResponsesInputFromMessages(
16+
[
17+
{ role: 'user', content: 'Earlier request' },
18+
{ role: 'assistant', content: 'Earlier response' },
19+
{ role: 'user', content: 'Use this file now' },
20+
],
21+
fileAttachments
22+
)
23+
24+
expect(input[0]).toEqual({ role: 'user', content: 'Earlier request' })
25+
expect(input[1]).toEqual({ role: 'assistant', content: 'Earlier response' })
26+
expect(input[2]).toEqual({
27+
role: 'user',
28+
content: [
29+
{ type: 'input_text', text: 'Use this file now' },
30+
{
31+
type: 'input_file',
32+
file_data: 'data:application/pdf;base64,cGRm',
33+
filename: 'brief.pdf',
34+
},
35+
],
36+
})
37+
})
38+
})

0 commit comments

Comments
 (0)