From fd804dfcc0ae400bc9e4d7008e11438a248e548b Mon Sep 17 00:00:00 2001 From: Joel Date: Wed, 29 Jul 2026 17:37:18 +0800 Subject: [PATCH] fix(web): show API errors when agent publishing fails (#39756) --- .../use-agent-configure-sync.spec.tsx | 41 +++++++++++++++++-- .../configure/use-agent-configure-sync.ts | 18 +++++++- 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/web/features/agent-v2/agent-detail/configure/__tests__/use-agent-configure-sync.spec.tsx b/web/features/agent-v2/agent-detail/configure/__tests__/use-agent-configure-sync.spec.tsx index 7a24d7e501f..bb44c924166 100644 --- a/web/features/agent-v2/agent-detail/configure/__tests__/use-agent-configure-sync.spec.tsx +++ b/web/features/agent-v2/agent-detail/configure/__tests__/use-agent-configure-sync.spec.tsx @@ -975,11 +975,45 @@ describe('useAgentConfigureSync', () => { }) }) - await expect(result.current.publishDraft()).rejects.toThrow( - 'Failed to save agent composer draft.', - ) + await expect(result.current.publishDraft()).rejects.toThrow('save failed') expect(publishAgentMutationFn).not.toHaveBeenCalled() + expect(toastMock.error).toHaveBeenCalledWith('save failed') + }) + + it('should show the API error message when publishing fails', async () => { + const responseError = new Response( + JSON.stringify({ + code: 'invalid_model_credentials', + message: 'Model credential validation failed', + status: 400, + }), + { + headers: { + 'Content-Type': 'application/json', + }, + status: 400, + statusText: 'Bad Request', + }, + ) + publishAgentMutationFn.mockRejectedValueOnce(responseError) + const { result } = renderUseAgentConfigureSync({ + currentModel: configuredModel, + }) + + await expect(result.current.publishDraft()).rejects.toBe(responseError) + + expect(toastMock.error).toHaveBeenCalledWith('Model credential validation failed') + }) + + it('should show the default error when publish rejection has no message', async () => { + publishAgentMutationFn.mockRejectedValueOnce({ code: 'publish_failed' }) + const { result } = renderUseAgentConfigureSync({ + currentModel: configuredModel, + }) + + await expect(result.current.publishDraft()).rejects.toEqual({ code: 'publish_failed' }) + expect(toastMock.error).toHaveBeenCalledWith('common.api.actionFailed') }) @@ -1249,6 +1283,7 @@ describe('useAgentConfigureSync', () => { expect(result.current.isPublishing).toBe(false) expect(toastMock.error).toHaveBeenCalledTimes(1) + expect(toastMock.error).toHaveBeenCalledWith('publish failed') expect(composerPutMutationFn).toHaveBeenCalledTimes(2) expect(composerPutMutationFn).toHaveBeenLastCalledWith( expect.objectContaining({ diff --git a/web/features/agent-v2/agent-detail/configure/use-agent-configure-sync.ts b/web/features/agent-v2/agent-detail/configure/use-agent-configure-sync.ts index 6e77988a1f0..0148668c358 100644 --- a/web/features/agent-v2/agent-detail/configure/use-agent-configure-sync.ts +++ b/web/features/agent-v2/agent-detail/configure/use-agent-configure-sync.ts @@ -135,9 +135,10 @@ export function useAgentConfigureSync({ agent_soul: configSnapshot, }, }) - } catch { + } catch (error) { // Autosave is silent and keeps the local draft intact; explicit commands must stop at this boundary. if (!silent) { + if (publish) throw error throw new Error('Failed to save agent composer draft.') } @@ -378,7 +379,20 @@ export function useAgentConfigureSync({ }) toast.success(tCommon(($) => $['api.actionSuccess'])) } catch (error) { - toast.error(tCommon(($) => $['api.actionFailed'])) + let errorData: unknown = error + if (error instanceof Response) { + try { + errorData = await error.clone().json() + } catch {} + } + toast.error( + errorData && + typeof errorData === 'object' && + 'message' in errorData && + typeof errorData.message === 'string' + ? errorData.message + : tCommon(($) => $['api.actionFailed']), + ) throw error } finally { publishInFlightRef.current = false