diff --git a/apps/desktop/src/app/session/hooks/use-prompt-actions.test.tsx b/apps/desktop/src/app/session/hooks/use-prompt-actions.test.tsx index 145a397d87f..96af1e8400e 100644 --- a/apps/desktop/src/app/session/hooks/use-prompt-actions.test.tsx +++ b/apps/desktop/src/app/session/hooks/use-prompt-actions.test.tsx @@ -7,7 +7,7 @@ import { $composerAttachments, type ComposerAttachment } from '@/store/composer' import { $connection, $sessions, setSessions } from '@/store/session' import type { SessionInfo } from '@/types/hermes' -import { usePromptActions } from './use-prompt-actions' +import { uploadComposerAttachment, usePromptActions } from './use-prompt-actions' vi.mock('@/hermes', () => ({ getProfiles: vi.fn(async () => ({ profiles: [] })), @@ -703,3 +703,52 @@ describe('usePromptActions eager attachment upload (drop-time)', () => { }) }) +describe('uploadComposerAttachment remote read failures', () => { + afterEach(() => { + vi.restoreAllMocks() + }) + + it('turns the raw 16MB IPC cap error into a friendly remote-gateway message', async () => { + // electron/hardening.cjs rejects the readFileDataUrl IPC with this exact + // shape when a file exceeds DATA_URL_READ_MAX_BYTES. + Object.defineProperty(window, 'hermesDesktop', { + configurable: true, + value: { + readFileDataUrl: vi.fn(async () => { + throw new Error('File preview failed: file is too large (20971520 bytes; limit 16777216 bytes).') + }) + } + }) + + const requestGateway = vi.fn(async () => ({}) as never) + + await expect( + uploadComposerAttachment( + { id: 'file:big', kind: 'file', label: 'huge.csv', path: '/abs/huge.csv' }, + { remote: true, requestGateway, sessionId: RUNTIME_SESSION_ID } + ) + ).rejects.toThrow('huge.csv is too large to upload to the remote gateway (max 16 MB).') + + // The cap is hit before any gateway round-trip. + expect(requestGateway).not.toHaveBeenCalled() + }) + + it('passes non-cap read errors through unchanged', async () => { + Object.defineProperty(window, 'hermesDesktop', { + configurable: true, + value: { + readFileDataUrl: vi.fn(async () => { + throw new Error('ENOENT: no such file') + }) + } + }) + + await expect( + uploadComposerAttachment( + { id: 'file:gone', kind: 'file', label: 'gone.csv', path: '/abs/gone.csv' }, + { remote: true, requestGateway: vi.fn(async () => ({}) as never), sessionId: RUNTIME_SESSION_ID } + ) + ).rejects.toThrow('ENOENT: no such file') + }) +}) + diff --git a/apps/desktop/src/app/session/hooks/use-prompt-actions.ts b/apps/desktop/src/app/session/hooks/use-prompt-actions.ts index 63e15f05185..167f0d3224f 100644 --- a/apps/desktop/src/app/session/hooks/use-prompt-actions.ts +++ b/apps/desktop/src/app/session/hooks/use-prompt-actions.ts @@ -120,6 +120,27 @@ async function readFileDataUrlForAttach(filePath: string): Promise 0 ? ` (max ${Math.floor(limitBytes / (1024 * 1024))} MB)` : '' + + return new Error(`${label} is too large to upload to the remote gateway${cap}.`) +} + type GatewayRequest = (method: string, params?: Record) => Promise /** @@ -142,7 +163,13 @@ export async function uploadComposerAttachment( let result: ImageAttachResponse if (remote) { - const payload = await readImageForRemoteAttach(path) + let payload: Awaited> + + try { + payload = await readImageForRemoteAttach(path) + } catch (err) { + throw friendlyRemoteAttachError(err, label) + } if (!payload) { throw new Error(`Could not read ${label}`) @@ -176,10 +203,18 @@ export async function uploadComposerAttachment( } // Non-image file. - const dataUrl = remote ? await readFileDataUrlForAttach(path) : null + let dataUrl: string | null = null - if (remote && !dataUrl) { - throw new Error(`Could not read ${label}`) + if (remote) { + try { + dataUrl = await readFileDataUrlForAttach(path) + } catch (err) { + throw friendlyRemoteAttachError(err, label) + } + + if (!dataUrl) { + throw new Error(`Could not read ${label}`) + } } const result = await requestGateway('file.attach', {