From 998e85efab9f4f0311d4657649013debe373ae07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Mon, 1 Jun 2026 00:36:11 +0800 Subject: [PATCH] fix: localize synthetic business errors Synthetic API errors were carrying only English display text, so desktop chat could not distinguish our compatibility guidance from raw upstream failures and recovery logic depended on those English strings. Add stable business error codes for media, PDF, request-size, prompt-length, and auto-mode errors. Desktop chat now resolves those codes through locale keys and suppresses the stale English fallback for known business errors, while normalizeMessagesForAPI still supports legacy text matching for old transcripts. Constraint: Raw upstream provider details must remain available for diagnostics but should not drive localized business UX. Rejected: Localize src/services/api/errors.ts strings directly | that would make persisted transcript text and media recovery depend on the active UI language. Confidence: high Scope-risk: moderate Directive: Add new user-facing synthetic API errors with businessErrorCode values before localizing their display text. Tested: bun test src/services/api/errors.test.ts tests/mediaRecoveryAndEstimation.test.ts src/server/__tests__/ws-memory-events.test.ts Tested: cd desktop && bun run test -- run src/stores/chatStore.test.ts src/components/chat/MessageList.test.tsx Tested: bun run check:server Tested: bun run check:desktop Not-tested: Manual desktop screenshot smoke for the rendered Chinese error card --- .../src/components/chat/MessageList.test.tsx | 31 +++++++++++++++++ desktop/src/components/chat/MessageList.tsx | 14 ++++++-- desktop/src/i18n/locales/en.ts | 11 +++++++ desktop/src/i18n/locales/zh.ts | 11 +++++++ desktop/src/stores/chatStore.test.ts | 26 +++++++++++++++ desktop/src/stores/chatStore.ts | 12 ++++++- desktop/src/types/chat.ts | 4 +-- src/constants/businessErrors.ts | 24 ++++++++++++++ src/query.ts | 3 ++ src/server/__tests__/ws-memory-events.test.ts | 20 +++++++++++ src/server/ws/events.ts | 2 +- src/server/ws/handler.ts | 3 ++ src/services/api/errors.test.ts | 2 ++ src/services/api/errors.ts | 11 +++++++ src/utils/messages.ts | 33 ++++++++++++++++--- tests/mediaRecoveryAndEstimation.test.ts | 27 +++++++++++++++ 16 files changed, 223 insertions(+), 11 deletions(-) create mode 100644 src/constants/businessErrors.ts diff --git a/desktop/src/components/chat/MessageList.test.tsx b/desktop/src/components/chat/MessageList.test.tsx index fef77b26..5823c4fc 100644 --- a/desktop/src/components/chat/MessageList.test.tsx +++ b/desktop/src/components/chat/MessageList.test.tsx @@ -3637,4 +3637,35 @@ describe('MessageList nested tool calls', () => { ), ).toBeTruthy() }) + + it('renders business API errors in the active locale without raw English fallback', () => { + useSettingsStore.setState({ locale: 'zh' }) + useChatStore.setState({ + sessions: { + [ACTIVE_TAB]: makeSessionState({ + messages: [ + { + id: 'error-1', + type: 'error', + code: 'invalid_request', + businessErrorCode: 'image_unsupported', + message: + 'This model does not support images. Continue with text, or switch to a vision-capable model and send the image again.', + timestamp: 1, + }, + ], + }), + }, + }) + + render() + + expect(screen.getByText('错误:')).toBeTruthy() + expect( + screen.getByText( + '当前模型不支持图片。请继续使用文字,或切换到支持视觉的模型后重新发送图片。', + ), + ).toBeTruthy() + expect(screen.queryByText(/This model does not support images/)).toBeNull() + }) }) diff --git a/desktop/src/components/chat/MessageList.tsx b/desktop/src/components/chat/MessageList.tsx index db0ef1f0..722b8773 100644 --- a/desktop/src/components/chat/MessageList.tsx +++ b/desktop/src/components/chat/MessageList.tsx @@ -2015,16 +2015,26 @@ export const MessageBlock = memo(function MessageBlock({ /> ) case 'error': { + const businessErrorKey = message.businessErrorCode + ? `businessError.${message.businessErrorCode}` as TranslationKey + : null + const businessErrorText = businessErrorKey ? t(businessErrorKey) : null const errorKey = message.code ? `error.${message.code}` as TranslationKey : null const errorText = errorKey ? t(errorKey) : null - const displayMessage = (errorText && errorText !== errorKey) ? errorText : message.message + const displayMessage = + businessErrorText && businessErrorText !== businessErrorKey + ? businessErrorText + : (errorText && errorText !== errorKey) + ? errorText + : message.message const showRawDetail = + !message.businessErrorCode && Boolean(message.message) && message.message.trim() !== '' && message.message !== displayMessage return (
- Error: {displayMessage} + {t('common.error')}: {displayMessage} {showRawDetail && (
{message.message} diff --git a/desktop/src/i18n/locales/en.ts b/desktop/src/i18n/locales/en.ts index bf497e40..86f13257 100644 --- a/desktop/src/i18n/locales/en.ts +++ b/desktop/src/i18n/locales/en.ts @@ -14,6 +14,7 @@ export const en = { 'common.enable': 'Enable', 'common.disable': 'Disable', 'common.active': 'ACTIVE', + 'common.error': 'Error', 'common.copyFailed': 'Copy failed.', // ─── Sidebar ────────────────────────────────────── @@ -1627,6 +1628,16 @@ export const en = { 'error.NOT_FOUND': 'Resource not found.', 'error.INTERNAL_ERROR': 'Internal server error.', + // ─── Business Errors ────────────────────────────────────── + 'businessError.pdf_too_large': 'The PDF is too large for the selected model. Convert it to text or use a smaller PDF.', + 'businessError.pdf_password_protected': 'The PDF is password protected. Unlock or convert it before sending it again.', + 'businessError.pdf_invalid': 'The PDF file is not valid. Convert it to text or send a different file.', + 'businessError.image_too_large': 'The image is too large for the selected model. Resize it or send a smaller image.', + 'businessError.image_unsupported': 'This model does not support images. Continue with text, or switch to a vision-capable model and send the image again.', + 'businessError.request_too_large': 'The request is too large for the selected model. Remove large files or retry with a smaller message.', + 'businessError.prompt_too_long': 'The prompt is too long for the selected model. Compact the conversation or retry with less context.', + 'businessError.auto_mode_unavailable': 'Auto mode is unavailable for your current plan.', + // ─── Server Status Verbs ────────────────────────────────────── 'serverVerb.Thinking': 'Thinking', 'serverVerb.Compacting conversation': 'Compacting context', diff --git a/desktop/src/i18n/locales/zh.ts b/desktop/src/i18n/locales/zh.ts index c91d3038..58216bfd 100644 --- a/desktop/src/i18n/locales/zh.ts +++ b/desktop/src/i18n/locales/zh.ts @@ -16,6 +16,7 @@ export const zh: Record = { 'common.enable': '启用', 'common.disable': '禁用', 'common.active': '已激活', + 'common.error': '错误', 'common.copyFailed': '复制失败。', // ─── Sidebar ────────────────────────────────────── @@ -1629,6 +1630,16 @@ export const zh: Record = { 'error.NOT_FOUND': '资源未找到。', 'error.INTERNAL_ERROR': '内部服务器错误。', + // ─── 业务错误 ────────────────────────────────────── + 'businessError.pdf_too_large': '这个 PDF 超出了当前模型可处理的大小。请先转成文本,或换一个更小的 PDF。', + 'businessError.pdf_password_protected': '这个 PDF 有密码保护。请先解锁或转换后再发送。', + 'businessError.pdf_invalid': '这个 PDF 文件无效。请先转成文本,或换一个文件发送。', + 'businessError.image_too_large': '这张图片超出了当前模型可处理的大小。请压缩图片,或换一张更小的图片。', + 'businessError.image_unsupported': '当前模型不支持图片。请继续使用文字,或切换到支持视觉的模型后重新发送图片。', + 'businessError.request_too_large': '这次请求超出了当前模型可处理的大小。请移除大文件,或缩短消息后重试。', + 'businessError.prompt_too_long': '当前上下文超出了模型限制。请先压缩会话,或减少上下文后重试。', + 'businessError.auto_mode_unavailable': '当前套餐不支持自动模式。', + // ─── Server Status Verbs ────────────────────────────────────── 'serverVerb.Thinking': '思考中', 'serverVerb.Compacting conversation': '正在压缩上下文', diff --git a/desktop/src/stores/chatStore.test.ts b/desktop/src/stores/chatStore.test.ts index 688f3cb9..5106d8fd 100644 --- a/desktop/src/stores/chatStore.test.ts +++ b/desktop/src/stores/chatStore.test.ts @@ -2411,6 +2411,32 @@ describe('chatStore history mapping', () => { expect(updateTabStatusMock).toHaveBeenLastCalledWith(TEST_SESSION_ID, 'error') }) + it('preserves business error codes from server error messages', () => { + useChatStore.setState({ + sessions: { + [TEST_SESSION_ID]: makeSession({ + messages: [], + chatState: 'streaming', + }), + }, + }) + + useChatStore.getState().handleServerMessage(TEST_SESSION_ID, { + type: 'error', + message: 'This model does not support images.', + code: 'invalid_request', + businessErrorCode: 'image_unsupported', + }) + + const session = useChatStore.getState().sessions[TEST_SESSION_ID] + expect(session?.messages[session.messages.length - 1]).toMatchObject({ + type: 'error', + message: 'This model does not support images.', + code: 'invalid_request', + businessErrorCode: 'image_unsupported', + }) + }) + it('removes the transient compacting card when compacting status ends without a boundary', () => { useChatStore.setState({ sessions: { diff --git a/desktop/src/stores/chatStore.ts b/desktop/src/stores/chatStore.ts index dadcb835..30a2650e 100644 --- a/desktop/src/stores/chatStore.ts +++ b/desktop/src/stores/chatStore.ts @@ -1649,7 +1649,17 @@ export const useChatStore = create((set, get) => ({ newMessages = appendAssistantTextMessage(newMessages, pendingText, Date.now()) } newMessages = dropTailCompactingCompactSummary(newMessages) - newMessages = [...newMessages, { id: nextId(), type: 'error', message: msg.message, code: msg.code, timestamp: Date.now() }] + newMessages = [ + ...newMessages, + { + id: nextId(), + type: 'error', + message: msg.message, + code: msg.code, + ...(msg.businessErrorCode ? { businessErrorCode: msg.businessErrorCode } : {}), + timestamp: Date.now(), + }, + ] return { messages: newMessages, chatState: 'idle', diff --git a/desktop/src/types/chat.ts b/desktop/src/types/chat.ts index 4819499d..bc0ce247 100644 --- a/desktop/src/types/chat.ts +++ b/desktop/src/types/chat.ts @@ -84,7 +84,7 @@ export type ServerMessage = errorType?: string errorMessage?: string } - | { type: 'error'; message: string; code: string; retryable?: boolean } + | { type: 'error'; message: string; code: string; retryable?: boolean; businessErrorCode?: string } | { type: 'system_notification'; subtype: string; message?: string; data?: unknown } | { type: 'pong' } | { type: 'team_update'; teamName: string; members: TeamMemberStatus[] } @@ -290,5 +290,5 @@ export type UIMessage = description?: string timestamp: number } - | { id: string; type: 'error'; message: string; code: string; timestamp: number } + | { id: string; type: 'error'; message: string; code: string; businessErrorCode?: string; timestamp: number } | { id: string; type: 'task_summary'; tasks: TaskSummaryItem[]; timestamp: number } diff --git a/src/constants/businessErrors.ts b/src/constants/businessErrors.ts new file mode 100644 index 00000000..542f1a6f --- /dev/null +++ b/src/constants/businessErrors.ts @@ -0,0 +1,24 @@ +export const BUSINESS_ERROR_CODES = { + PDF_TOO_LARGE: 'pdf_too_large', + PDF_PASSWORD_PROTECTED: 'pdf_password_protected', + PDF_INVALID: 'pdf_invalid', + IMAGE_TOO_LARGE: 'image_too_large', + IMAGE_UNSUPPORTED: 'image_unsupported', + REQUEST_TOO_LARGE: 'request_too_large', + PROMPT_TOO_LONG: 'prompt_too_long', + AUTO_MODE_UNAVAILABLE: 'auto_mode_unavailable', +} as const + +export type BusinessErrorCode = + (typeof BUSINESS_ERROR_CODES)[keyof typeof BUSINESS_ERROR_CODES] + +export const BUSINESS_ERROR_MEDIA_BLOCK_TYPES: Partial< + Record +> = { + [BUSINESS_ERROR_CODES.PDF_TOO_LARGE]: ['document'], + [BUSINESS_ERROR_CODES.PDF_PASSWORD_PROTECTED]: ['document'], + [BUSINESS_ERROR_CODES.PDF_INVALID]: ['document'], + [BUSINESS_ERROR_CODES.IMAGE_TOO_LARGE]: ['image'], + [BUSINESS_ERROR_CODES.IMAGE_UNSUPPORTED]: ['image'], + [BUSINESS_ERROR_CODES.REQUEST_TOO_LARGE]: ['document', 'image'], +} diff --git a/src/query.ts b/src/query.ts index ccd17a0f..ef1251f2 100644 --- a/src/query.ts +++ b/src/query.ts @@ -23,6 +23,7 @@ import { logEvent, type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, } from 'src/services/analytics/index.js' +import { BUSINESS_ERROR_CODES } from './constants/businessErrors.js' import { ImageSizeError } from './utils/imageValidation.js' import { ImageResizeError } from './utils/imageResizer.js' import { findToolByName, type ToolUseContext } from './Tool.js' @@ -643,6 +644,7 @@ async function* queryLoop( yield createAssistantAPIErrorMessage({ content: PROMPT_TOO_LONG_ERROR_MESSAGE, error: 'invalid_request', + businessErrorCode: BUSINESS_ERROR_CODES.PROMPT_TOO_LONG, }) return { reason: 'blocking_limit' } } @@ -974,6 +976,7 @@ async function* queryLoop( ) { yield createAssistantAPIErrorMessage({ content: error.message, + businessErrorCode: BUSINESS_ERROR_CODES.IMAGE_TOO_LARGE, }) return { reason: 'image_error' } } diff --git a/src/server/__tests__/ws-memory-events.test.ts b/src/server/__tests__/ws-memory-events.test.ts index 0b9cac46..334a9449 100644 --- a/src/server/__tests__/ws-memory-events.test.ts +++ b/src/server/__tests__/ws-memory-events.test.ts @@ -6,6 +6,26 @@ import { import { parseSlashCommand } from '../../utils/slashCommandParsing.js' describe('WebSocket memory events', () => { + it('forwards assistant business error codes to the desktop client', () => { + expect(translateCliMessage({ + type: 'assistant', + error: 'invalid_request', + isApiErrorMessage: true, + businessErrorCode: 'image_unsupported', + message: { + role: 'assistant', + content: [{ type: 'text', text: 'This model does not support images.' }], + }, + }, 'session-1')).toEqual([ + { + type: 'error', + message: 'This model does not support images.', + code: 'invalid_request', + businessErrorCode: 'image_unsupported', + }, + ]) + }) + it('forwards CLI memory_saved system messages to the desktop client', () => { const messages = translateCliMessage( { diff --git a/src/server/ws/events.ts b/src/server/ws/events.ts index d729763d..aba96873 100644 --- a/src/server/ws/events.ts +++ b/src/server/ws/events.ts @@ -72,7 +72,7 @@ export type ServerMessage = errorType?: string errorMessage?: string } - | { type: 'error'; message: string; code: string; retryable?: boolean } + | { type: 'error'; message: string; code: string; retryable?: boolean; businessErrorCode?: string } | { type: 'system_notification'; subtype: string; message?: string; data?: unknown } | { type: 'pong' } | { type: 'team_update'; teamName: string; members: TeamMemberStatus[] } diff --git a/src/server/ws/handler.ts b/src/server/ws/handler.ts index 37f1bc90..2c51c768 100644 --- a/src/server/ws/handler.ts +++ b/src/server/ws/handler.ts @@ -1070,6 +1070,9 @@ export function translateCliMessage(cliMsg: any, sessionId: string): ServerMessa type: 'error', message, code, + ...(typeof cliMsg.businessErrorCode === 'string' + ? { businessErrorCode: cliMsg.businessErrorCode } + : {}), }] } diff --git a/src/services/api/errors.test.ts b/src/services/api/errors.test.ts index 0d49bb11..dbf5ed02 100644 --- a/src/services/api/errors.test.ts +++ b/src/services/api/errors.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from 'bun:test' +import { BUSINESS_ERROR_CODES } from '../../constants/businessErrors.js' import { getAssistantMessageFromError, getImageUnsupportedErrorMessage, @@ -29,6 +30,7 @@ describe('image unsupported API errors', () => { ) expect(msg.isApiErrorMessage).toBe(true) + expect(msg.businessErrorCode).toBe(BUSINESS_ERROR_CODES.IMAGE_UNSUPPORTED) expect(msg.errorDetails).toBe('This model does not support image blocks') expect(msg.message.content[0]).toMatchObject({ type: 'text', diff --git a/src/services/api/errors.ts b/src/services/api/errors.ts index 1b8708eb..bb482455 100644 --- a/src/services/api/errors.ts +++ b/src/services/api/errors.ts @@ -8,6 +8,7 @@ import type { BetaStopReason, } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs' import { AFK_MODE_BETA_HEADER } from 'src/constants/betas.js' +import { BUSINESS_ERROR_CODES } from 'src/constants/businessErrors.js' import type { SDKAssistantMessageError } from 'src/entrypoints/agentSdkTypes.js' import type { AssistantMessage, @@ -495,6 +496,7 @@ export function getAssistantMessageFromError( if (error instanceof ImageSizeError || error instanceof ImageResizeError) { return createAssistantAPIErrorMessage({ content: getImageTooLargeErrorMessage(), + businessErrorCode: BUSINESS_ERROR_CODES.IMAGE_TOO_LARGE, }) } @@ -511,6 +513,7 @@ export function getAssistantMessageFromError( content: getImageUnsupportedErrorMessage(), error: 'invalid_request', errorDetails: error.message, + businessErrorCode: BUSINESS_ERROR_CODES.IMAGE_UNSUPPORTED, }) } @@ -633,6 +636,7 @@ export function getAssistantMessageFromError( content: PROMPT_TOO_LONG_ERROR_MESSAGE, error: 'invalid_request', errorDetails: error.message, + businessErrorCode: BUSINESS_ERROR_CODES.PROMPT_TOO_LONG, }) } @@ -645,6 +649,7 @@ export function getAssistantMessageFromError( content: getPdfTooLargeErrorMessage(), error: 'invalid_request', errorDetails: error.message, + businessErrorCode: BUSINESS_ERROR_CODES.PDF_TOO_LARGE, }) } @@ -656,6 +661,7 @@ export function getAssistantMessageFromError( return createAssistantAPIErrorMessage({ content: getPdfPasswordProtectedErrorMessage(), error: 'invalid_request', + businessErrorCode: BUSINESS_ERROR_CODES.PDF_PASSWORD_PROTECTED, }) } @@ -669,6 +675,7 @@ export function getAssistantMessageFromError( return createAssistantAPIErrorMessage({ content: getPdfInvalidErrorMessage(), error: 'invalid_request', + businessErrorCode: BUSINESS_ERROR_CODES.PDF_INVALID, }) } @@ -682,6 +689,7 @@ export function getAssistantMessageFromError( return createAssistantAPIErrorMessage({ content: getImageTooLargeErrorMessage(), errorDetails: error.message, + businessErrorCode: BUSINESS_ERROR_CODES.IMAGE_TOO_LARGE, }) } @@ -698,6 +706,7 @@ export function getAssistantMessageFromError( : 'An image in the conversation exceeds the dimension limit for many-image requests (2000px). Run /compact to remove old images from context, or start a new session.', error: 'invalid_request', errorDetails: error.message, + businessErrorCode: BUSINESS_ERROR_CODES.IMAGE_TOO_LARGE, }) } @@ -714,6 +723,7 @@ export function getAssistantMessageFromError( return createAssistantAPIErrorMessage({ content: 'Auto mode is unavailable for your plan', error: 'invalid_request', + businessErrorCode: BUSINESS_ERROR_CODES.AUTO_MODE_UNAVAILABLE, }) } @@ -723,6 +733,7 @@ export function getAssistantMessageFromError( return createAssistantAPIErrorMessage({ content: getRequestTooLargeErrorMessage(), error: 'invalid_request', + businessErrorCode: BUSINESS_ERROR_CODES.REQUEST_TOO_LARGE, }) } diff --git a/src/utils/messages.ts b/src/utils/messages.ts index 8ea0e002..266b21d4 100644 --- a/src/utils/messages.ts +++ b/src/utils/messages.ts @@ -24,6 +24,10 @@ import type { AgentId } from 'src/types/ids.js' import { companionIntroText } from '../buddy/prompt.js' import { NO_CONTENT_MESSAGE } from '../constants/messages.js' import { OUTPUT_STYLE_CONFIG } from '../constants/outputStyles.js' +import { + type BusinessErrorCode, + BUSINESS_ERROR_MEDIA_BLOCK_TYPES, +} from '../constants/businessErrors.js' import { isAutoMemoryEnabled } from '../memdir/paths.js' import { checkStatsigFeatureGate_CACHED_MAY_BE_STALE, @@ -359,6 +363,7 @@ function baseCreateAssistantMessage({ apiError, error, errorDetails, + businessErrorCode, isVirtual, usage = { input_tokens: 0, @@ -381,6 +386,7 @@ function baseCreateAssistantMessage({ apiError?: AssistantMessage['apiError'] error?: SDKAssistantMessageError errorDetails?: string + businessErrorCode?: BusinessErrorCode isVirtual?: true usage?: Usage }): AssistantMessage { @@ -404,6 +410,7 @@ function baseCreateAssistantMessage({ apiError, error, errorDetails, + businessErrorCode, isApiErrorMessage, isVirtual, } @@ -438,11 +445,13 @@ export function createAssistantAPIErrorMessage({ apiError, error, errorDetails, + businessErrorCode, }: { content: string apiError?: AssistantMessage['apiError'] error?: SDKAssistantMessageError errorDetails?: string + businessErrorCode?: BusinessErrorCode }): AssistantMessage { return baseCreateAssistantMessage({ content: [ @@ -455,6 +464,7 @@ export function createAssistantAPIErrorMessage({ apiError, error, errorDetails, + businessErrorCode, }) } @@ -769,6 +779,9 @@ export function normalizeMessages(messages: Message[]): NormalizedMessage[] { uuid, error: message.error, isApiErrorMessage: message.isApiErrorMessage, + apiError: message.apiError, + errorDetails: message.errorDetails, + businessErrorCode: message.businessErrorCode, advisorModel: message.advisorModel, } as NormalizedAssistantMessage }) @@ -2001,7 +2014,9 @@ export function normalizeMessagesForAPI( m => !((m.type === 'user' || m.type === 'assistant') && m.isVirtual), ) - // Build a map from error text → which block types to strip from the preceding user message. + // Build a fallback map from legacy error text → which block types to strip + // from the preceding user message. New synthetic errors use stable + // businessErrorCode values so translated display text cannot break recovery. const errorToBlockTypes: Record> = { [getPdfTooLargeErrorMessage()]: new Set(['document']), [getPdfPasswordProtectedErrorMessage()]: new Set(['document']), @@ -2019,16 +2034,24 @@ export function normalizeMessagesForAPI( if (!isSyntheticApiErrorMessage(msg)) { continue } - // Determine which error this is + let blockTypesToStrip: Set | undefined + const blockTypesFromCode = + typeof msg.businessErrorCode === 'string' + ? BUSINESS_ERROR_MEDIA_BLOCK_TYPES[msg.businessErrorCode as BusinessErrorCode] + : undefined + if (blockTypesFromCode) { + blockTypesToStrip = new Set(blockTypesFromCode) + } + + // Determine which legacy text error this is. const errorText = Array.isArray(msg.message.content) && msg.message.content[0]?.type === 'text' ? msg.message.content[0].text : undefined - if (!errorText) { - continue + if (!blockTypesToStrip && errorText) { + blockTypesToStrip = errorToBlockTypes[errorText] } - const blockTypesToStrip = errorToBlockTypes[errorText] if (!blockTypesToStrip) { continue } diff --git a/tests/mediaRecoveryAndEstimation.test.ts b/tests/mediaRecoveryAndEstimation.test.ts index ecc36f5e..4b75c102 100644 --- a/tests/mediaRecoveryAndEstimation.test.ts +++ b/tests/mediaRecoveryAndEstimation.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from 'bun:test' +import { BUSINESS_ERROR_CODES } from '../src/constants/businessErrors.js' import { getImageUnsupportedErrorMessage } from '../src/services/api/errors.js' import { roughTokenCountEstimationForAPIRequest } from '../src/services/tokenEstimation.js' import type { UserMessage } from '../src/types/message.js' @@ -43,6 +44,32 @@ describe('media error recovery', () => { expect(serialized).toContain('describe this screenshot') expect(serialized).toContain('continue with text only') }) + + test('strips images using stable business error codes', () => { + const imageUser = createUserMessage({ + content: [ + { type: 'text', text: 'describe this screenshot' }, + imageBlock('base64-image-payload'), + ], + uuid: '00000000-0000-4000-8000-000000000003', + }) + const unsupported = createAssistantAPIErrorMessage({ + content: 'localized display text', + error: 'invalid_request', + businessErrorCode: BUSINESS_ERROR_CODES.IMAGE_UNSUPPORTED, + }) + const nextUser = createUserMessage({ + content: 'continue with text only', + uuid: '00000000-0000-4000-8000-000000000004', + }) + + const normalized = normalizeMessagesForAPI([imageUser, unsupported, nextUser]) + const serialized = JSON.stringify(normalized) + + expect(serialized).not.toContain('base64-image-payload') + expect(serialized).toContain('describe this screenshot') + expect(serialized).toContain('continue with text only') + }) }) describe('media context estimation', () => {