Skip to content

Commit 8a2844c

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
fix(files): preserve valid zero-byte tool outputs
1 parent 044660b commit 8a2844c

2 files changed

Lines changed: 80 additions & 4 deletions

File tree

‎apps/sim/executor/utils/file-tool-processor.test.ts‎

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,4 +109,80 @@ describe('FileToolProcessor', () => {
109109

110110
expect(mockUploadExecutionFile).not.toHaveBeenCalled()
111111
})
112+
113+
it.each([Buffer.alloc(0), '', { type: 'Buffer', data: [] }])(
114+
'stores valid zero-byte inline files as UserFile outputs: %j',
115+
async (data) => {
116+
const storedFile = {
117+
id: 'empty-file',
118+
key: 'workspace/workspace-1/empty-file',
119+
name: 'empty.txt',
120+
size: 0,
121+
type: 'text/plain',
122+
url: '/api/files/serve?key=workspace%2Fworkspace-1%2Fempty-file',
123+
} satisfies UserFile
124+
mockUploadExecutionFile.mockResolvedValue(storedFile)
125+
126+
const result = await FileToolProcessor.processToolOutputs(
127+
{
128+
file: {
129+
name: 'empty.txt',
130+
mimeType: 'text/plain',
131+
data,
132+
url: 'https://example.com/file',
133+
},
134+
},
135+
toolConfig,
136+
executionContext
137+
)
138+
139+
expect(result.file).toEqual(storedFile)
140+
expect(mockUploadExecutionFile).toHaveBeenCalledWith(
141+
expect.objectContaining({ workspaceId: 'workspace-1', executionId: 'execution-1' }),
142+
Buffer.alloc(0),
143+
'empty.txt',
144+
'text/plain',
145+
'user-1'
146+
)
147+
expect(mockDownloadFileFromUrl).not.toHaveBeenCalled()
148+
}
149+
)
150+
151+
it('preserves empty file entries in file-array outputs', async () => {
152+
const result = await FileToolProcessor.processToolOutputs(
153+
{ file: [{ name: 'empty.txt', mimeType: 'text/plain', data: '' }] },
154+
{ ...toolConfig, outputs: { file: { type: 'file[]' } } },
155+
executionContext
156+
)
157+
158+
expect(result.file).toHaveLength(1)
159+
expect(mockUploadExecutionFile.mock.calls[0]?.[1]).toEqual(Buffer.alloc(0))
160+
})
161+
162+
it('stores a successful zero-byte URL download', async () => {
163+
mockDownloadFileFromUrl.mockResolvedValue(Buffer.alloc(0))
164+
165+
await FileToolProcessor.processToolOutputs(
166+
{ file: { name: 'empty.txt', mimeType: 'text/plain', url: 'https://example.com/empty' } },
167+
toolConfig,
168+
executionContext
169+
)
170+
171+
expect(mockUploadExecutionFile.mock.calls[0]?.[1]).toEqual(Buffer.alloc(0))
172+
})
173+
174+
it.each([undefined, null, '!!!', { type: 'Buffer', data: 'invalid' }])(
175+
'does not turn missing or malformed data into an empty file: %j',
176+
async (data) => {
177+
await expect(
178+
FileToolProcessor.processToolOutputs(
179+
{ file: { name: 'invalid.txt', mimeType: 'text/plain', data } },
180+
toolConfig,
181+
executionContext
182+
)
183+
).rejects.toThrow("Failed to process file output 'file'")
184+
185+
expect(mockUploadExecutionFile).not.toHaveBeenCalled()
186+
}
187+
)
112188
})

‎apps/sim/executor/utils/file-tool-processor.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ export class FileToolProcessor {
169169
} else {
170170
throw new Error(`Invalid serialized buffer format for ${data.name}`)
171171
}
172-
} else if (typeof data.data === 'string' && data.data) {
172+
} else if (typeof data.data === 'string') {
173173
let base64Data = data.data
174174

175175
if (base64Data.includes('-') || base64Data.includes('_')) {
@@ -179,6 +179,9 @@ export class FileToolProcessor {
179179
const paddingBytes = base64Data.endsWith('==') ? 2 : base64Data.endsWith('=') ? 1 : 0
180180
assertFileSize(Math.floor((base64Data.length * 3) / 4) - paddingBytes, data.name)
181181
buffer = Buffer.from(base64Data, 'base64')
182+
if (base64Data.length > 0 && buffer.length === 0) {
183+
throw new Error(`File '${data.name}' has invalid base64 data`)
184+
}
182185
}
183186

184187
if (!buffer && data.url) {
@@ -189,9 +192,6 @@ export class FileToolProcessor {
189192
}
190193

191194
if (buffer) {
192-
if (buffer.length === 0) {
193-
throw new Error(`File '${data.name}' has zero bytes`)
194-
}
195195
assertFileSize(buffer.length, data.name)
196196
const storedMetadata = resolveStoredFileMetadata(data.name, data.mimeType, buffer)
197197

0 commit comments

Comments
 (0)