diff --git a/apps/desktop/src/main/__tests__/app-shell-session-settings-actions.test.ts b/apps/desktop/src/main/__tests__/app-shell-session-settings-actions.test.ts index 6889c68786..287f5f5d74 100644 --- a/apps/desktop/src/main/__tests__/app-shell-session-settings-actions.test.ts +++ b/apps/desktop/src/main/__tests__/app-shell-session-settings-actions.test.ts @@ -71,6 +71,7 @@ function createHarness(options: { const permissionCalls: string[] = []; const thinkingCalls: string[] = []; const errors: string[] = []; + const errorDescriptions: Array = []; const errorTargets: Array<{ sessionId: string } | undefined> = []; const successes: Array<{ title: string; description?: string }> = []; const newTaskPermissionModes: string[] = []; @@ -121,8 +122,9 @@ function createHarness(options: { }, toastApi: { success: (title, description) => successes.push({ title, description }), - error: (title, _description, _details, target) => { + error: (title, description, _details, target) => { errors.push(title); + errorDescriptions.push(description); errorTargets.push(target); }, confirm: options.confirm ?? (async () => true), @@ -133,6 +135,7 @@ function createHarness(options: { actions, activeIdRef, errors, + errorDescriptions, errorTargets, modelCalls, modelResult, @@ -360,4 +363,19 @@ describe('AppShell session settings actions', () => { harness.modelResult.resolve(session('session-a')); await modelChange; }); + + it('points a failed account-and-model switch at credential recovery', async () => { + const harness = createHarness(); + + const modelChange = harness.actions.setSessionModel({ + llmConnectionId: 'connection-1', + llmConnectionSlug: 'e2e', + model: 'claude-opus', + }); + harness.modelResult.reject(new Error('fixture failure')); + await modelChange; + + assert.match(harness.errorDescriptions[0] ?? '', /设置 · 模型/); + assert.match(harness.errorDescriptions[0] ?? '', /登录或 API Key/); + }); }); diff --git a/apps/desktop/src/main/__tests__/chat-recovery-notice.test.ts b/apps/desktop/src/main/__tests__/chat-recovery-notice.test.ts new file mode 100644 index 0000000000..50fb540aea --- /dev/null +++ b/apps/desktop/src/main/__tests__/chat-recovery-notice.test.ts @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { createElement } from 'react'; +import { renderToStaticMarkup } from 'react-dom/server'; +import { ChatRecoveryNotice } from '../../renderer/chat-recovery-notice.js'; +import { SessionHealthRecoveryNotice } from '../../renderer/chat-recovery-notice.js'; + +test('composer recovery notices share one wrapper and secondary disabled action', () => { + const markup = renderToStaticMarkup(createElement(ChatRecoveryNotice, { + status: 'error', + title: 'Choose a model connection', + description: 'Choose the connection and model to use.', + actionLabel: 'Choose connection and model', + actionDisabled: true, + onAction: () => undefined, + })); + + assert.match(markup, /class="maka-chat-recovery-notice"/); + assert.match(markup, /maka-chat-recovery-notice-alert/); + assert.match(markup, /Choose a model connection/); + assert.match(markup, /Choose the connection and model to use\./); + assert.match(markup, /Choose connection and model/); + assert.match(markup, /disabled=""/); + assert.match(markup, /secondary/); +}); + +test('composer recovery notices omit the action when no handler exists', () => { + const markup = renderToStaticMarkup(createElement(ChatRecoveryNotice, { + status: 'warning', + title: 'Workspace unavailable', + actionLabel: 'Open Settings', + })); + + assert.doesNotMatch(markup, /