项目文件夹

文件
wehub-resource-sync 070959e133
landing-page-staging / Deploy landing page to staging (push) Has been skipped
landing-page-ci / Validate landing page (push) Failing after 4s
visual-baseline / Capture visual baselines (push) Has been cancelled
bake-plugin-previews / Bake plugin previews (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:00:47 +08:00

4929 行
168 KiB
TypeScript

// @vitest-environment jsdom
import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { OpenDesignHostUpdaterStatusSnapshot } from '@open-design/host';
import { installMockOpenDesignHost } from '@open-design/host/testing';
import { en } from '../../src/i18n/locales/en';
function optionNames(container: HTMLElement): string[] {
return within(container).getAllByRole('option').map((option) => {
const labelledBy = option.getAttribute('aria-labelledby');
if (!labelledBy) return option.textContent?.trim() ?? '';
return labelledBy
.split(/\s+/)
.map((id) => document.getElementById(id)?.textContent?.trim() ?? '')
.filter(Boolean)
.join(' ');
});
}
const {
playSoundMock,
requestNotificationPermissionMock,
showCompletionNotificationMock,
notificationPermissionMock,
fetchCodexPetsMock,
syncCommunityPetsMock,
fetchSkillsMock,
fetchDesignSystemsMock,
fetchSkillMock,
fetchDesignSystemMock,
importLocalDesignSystemMock,
importGitHubDesignSystemMock,
fetchProviderModelsMock,
fetchLatestGithubReleaseInfoMock,
openExternalUrlMock,
analyticsTrackMock,
} = vi.hoisted(() => ({
playSoundMock: vi.fn(),
requestNotificationPermissionMock: vi.fn(),
showCompletionNotificationMock: vi.fn(),
notificationPermissionMock: vi.fn(),
fetchCodexPetsMock: vi.fn(),
syncCommunityPetsMock: vi.fn(),
fetchSkillsMock: vi.fn(),
fetchDesignSystemsMock: vi.fn(),
fetchSkillMock: vi.fn(),
fetchDesignSystemMock: vi.fn(),
importLocalDesignSystemMock: vi.fn(),
importGitHubDesignSystemMock: vi.fn(),
fetchProviderModelsMock: vi.fn(),
fetchLatestGithubReleaseInfoMock: vi.fn(),
openExternalUrlMock: vi.fn(),
analyticsTrackMock: vi.fn(),
}));
vi.mock('../../src/utils/notifications', async () => {
const actual = await vi.importActual<typeof import('../../src/utils/notifications')>(
'../../src/utils/notifications',
);
return {
...actual,
playSound: playSoundMock,
requestNotificationPermission: requestNotificationPermissionMock,
showCompletionNotification: showCompletionNotificationMock,
notificationPermission: notificationPermissionMock,
};
});
vi.mock('../../src/providers/registry', async () => {
const actual = await vi.importActual<typeof import('../../src/providers/registry')>(
'../../src/providers/registry',
);
return {
...actual,
fetchCodexPets: fetchCodexPetsMock,
syncCommunityPets: syncCommunityPetsMock,
fetchSkills: fetchSkillsMock,
fetchDesignSystems: fetchDesignSystemsMock,
fetchSkill: fetchSkillMock,
fetchDesignSystem: fetchDesignSystemMock,
importLocalDesignSystem: importLocalDesignSystemMock,
importGitHubDesignSystem: importGitHubDesignSystemMock,
fetchLatestGithubReleaseInfo: fetchLatestGithubReleaseInfoMock,
openExternalUrl: openExternalUrlMock,
codexPetSpritesheetUrl: (pet: { spritesheetUrl: string }) => pet.spritesheetUrl,
};
});
vi.mock('../../src/providers/provider-models', () => ({
fetchProviderModels: fetchProviderModelsMock,
}));
vi.mock('../../src/analytics/provider', () => ({
useAnalytics: () => ({
track: analyticsTrackMock,
setConsent: () => undefined,
setIdentity: () => undefined,
setConfigureGlobals: () => undefined,
anonymousId: 'test-anonymous',
sessionId: 'test-session',
newRequestId: () => 'test-request',
}),
}));
import { SettingsDialog } from '../../src/components/SettingsDialog';
import { IntegrationsView } from '../../src/components/IntegrationsView';
import type { AgentRefreshOptions, SettingsSection } from '../../src/components/SettingsDialog';
import { reconcileAmrModelChoice } from '../../src/components/SettingsDialog';
import { reconcileAmrProfileEnv } from '../../src/components/SettingsDialog';
import { I18nProvider } from '../../src/i18n';
import { LOCALES } from '../../src/i18n/types';
import { MAX_MAX_TOKENS, MIN_MAX_TOKENS } from '../../src/state/maxTokens';
import type { AgentInfo, AppConfig, AppVersionInfo } from '../../src/types';
const baseConfig: AppConfig = {
mode: 'api',
apiKey: '',
apiProtocol: 'anthropic',
apiVersion: '',
baseUrl: 'https://api.anthropic.com',
model: 'claude-sonnet-4-5',
apiProviderBaseUrl: 'https://api.anthropic.com',
apiProtocolConfigs: {},
agentId: null,
skillId: null,
designSystemId: null,
onboardingCompleted: true,
mediaProviders: {},
agentModels: {},
agentCliEnv: {},
};
const availableAgents: AgentInfo[] = [
{
id: 'codex',
name: 'Codex CLI',
bin: 'codex',
available: true,
version: '0.80.0',
models: [{ id: 'default', label: 'Default' }],
},
];
const amrAgent: AgentInfo = {
id: 'amr',
name: 'AMR (vela)',
bin: 'amr',
available: true,
version: '1.0.0',
models: [{ id: 'default', label: 'Default' }],
supportsCustomModel: false,
};
type OnRefreshAgents = (
options?: AgentRefreshOptions,
) => void | AgentInfo[] | Promise<void | AgentInfo[]>;
const sampleBundledPets = [
{
id: 'dario',
displayName: 'Dario',
description: 'A tiny frustrated companion.',
spritesheetUrl: '/api/codex-pets/dario.webp',
spritesheetExt: 'webp',
hatchedAt: 1710000000000,
bundled: true,
},
{
id: 'nyako',
displayName: 'Nyako',
description: 'A warm companion.',
spritesheetUrl: '/api/codex-pets/nyako.webp',
spritesheetExt: 'webp',
hatchedAt: 1710000001000,
bundled: true,
},
];
const sampleCommunityPets = [
{
id: 'jade',
displayName: 'Jade',
description: 'A cheerful explorer.',
spritesheetUrl: '/api/codex-pets/jade.webp',
spritesheetExt: 'webp',
hatchedAt: 1710000010000,
},
{
id: 'voidling',
displayName: 'Voidling',
description: 'A tiny grim companion.',
spritesheetUrl: '/api/codex-pets/voidling.webp',
spritesheetExt: 'webp',
hatchedAt: 1710000020000,
},
];
const sampleSkills = [
{
id: 'blog-post',
name: 'blog-post',
description: 'A long-form article / blog post.',
mode: 'prototype',
previewType: 'HTML',
},
{
id: 'dashboard',
name: 'dashboard',
description: 'Admin / analytics dashboard.',
mode: 'prototype',
previewType: 'HTML',
},
{
id: 'sales-deck',
name: 'sales-deck',
description: 'A narrative sales presentation.',
mode: 'deck',
previewType: 'PPTX',
},
];
const sampleDesignSystems = [
{
id: 'neutral-modern',
title: 'Neutral Modern',
summary: 'Calm editorial neutrals.',
category: 'Default',
swatches: ['#111827', '#f5f5f4'],
},
{
id: 'signal-green',
title: 'Signal Green',
summary: 'Brighter utility system.',
category: 'Experimental',
swatches: ['#14532d', '#86efac'],
},
];
let restoreOpenDesignHost: (() => void) | null = null;
function updateStatus(
overrides: Partial<OpenDesignHostUpdaterStatusSnapshot> = {},
): OpenDesignHostUpdaterStatusSnapshot {
return {
arch: 'arm64',
capabilities: {
canApplyInPlace: false,
canDownload: true,
canOpenInstaller: true,
requiresManualInstall: true,
},
channel: 'beta',
currentVersion: '1.2.3-beta.3',
enabled: true,
mode: 'package-launcher',
platform: 'darwin',
state: 'idle',
supported: true,
...overrides,
};
}
function renderSettingsDialog(
initial: Partial<AppConfig> = {},
options: {
agents?: AgentInfo[];
daemonLive?: boolean;
onRefreshAgents?: OnRefreshAgents;
initialSection?: SettingsSection;
appVersionInfo?: AppVersionInfo | null;
welcome?: boolean;
} = {},
) {
const onPersist = vi.fn();
const onPersistComposioKey = vi.fn();
const onClose = vi.fn();
const onRefreshAgents = options.onRefreshAgents ?? vi.fn<OnRefreshAgents>();
const view = render(
<SettingsDialog
initial={{ ...baseConfig, ...initial }}
agents={options.agents ?? availableAgents}
daemonLive={options.daemonLive ?? true}
appVersionInfo={options.appVersionInfo ?? null}
initialSection={options.initialSection ?? 'execution'}
welcome={options.welcome}
onPersist={onPersist}
onPersistComposioKey={onPersistComposioKey}
onClose={onClose}
onRefreshAgents={onRefreshAgents}
/>,
);
return { onPersist, onPersistComposioKey, onClose, onRefreshAgents, ...view };
}
function renderIntegrationsView(
initial: Partial<AppConfig> = {},
options: {
initialTab?: 'mcp' | 'connectors' | 'skills' | 'use-everywhere';
} = {},
) {
const onConfigPersist = vi.fn();
const onPersistComposioKey = vi.fn();
const view = render(
<IntegrationsView
config={{ ...baseConfig, ...initial }}
initialTab={options.initialTab ?? 'mcp'}
onConfigPersist={onConfigPersist}
onPersistComposioKey={onPersistComposioKey}
/>,
);
return { onConfigPersist, onPersistComposioKey, ...view };
}
function renderLanguageSettingsDialog(initialLocale: Parameters<typeof I18nProvider>[0]['initial'] = 'en') {
const onPersist = vi.fn();
const onClose = vi.fn();
render(
<I18nProvider initial={initialLocale}>
<SettingsDialog
initial={baseConfig}
agents={availableAgents}
daemonLive={true}
appVersionInfo={null}
initialSection="language"
onPersist={onPersist}
onPersistComposioKey={vi.fn()}
onClose={onClose}
onRefreshAgents={vi.fn()}
/>
</I18nProvider>,
);
return { onPersist, onClose };
}
async function waitForPersist(
onPersist: ReturnType<typeof vi.fn>,
expectedConfig: unknown,
expectedOptions: { forceMediaProviderSync?: boolean } = { forceMediaProviderSync: false },
) {
await waitFor(() => {
expect(onPersist).toHaveBeenCalledWith(
expectedConfig,
expect.objectContaining(expectedOptions),
);
});
}
function openGatewayPresetPopover() {
fireEvent.click(screen.getByRole('combobox', { name: 'Gateway preset' }));
return screen.getByTestId('settings-byok-provider-preset-popover');
}
function selectGatewayPreset(label: string) {
const popover = openGatewayPresetPopover();
fireEvent.click(within(popover).getByRole('option', { name: label }));
}
function deferred<T>() {
let resolve!: (value: T | PromiseLike<T>) => void;
let reject!: (reason?: unknown) => void;
const promise = new Promise<T>((res, rej) => {
resolve = res;
reject = rej;
});
return { promise, resolve, reject };
}
beforeEach(() => {
playSoundMock.mockReset();
requestNotificationPermissionMock.mockReset();
showCompletionNotificationMock.mockReset();
notificationPermissionMock.mockReset();
fetchCodexPetsMock.mockReset();
syncCommunityPetsMock.mockReset();
fetchSkillsMock.mockReset();
fetchDesignSystemsMock.mockReset();
fetchSkillMock.mockReset();
fetchDesignSystemMock.mockReset();
importLocalDesignSystemMock.mockReset();
importGitHubDesignSystemMock.mockReset();
fetchProviderModelsMock.mockReset();
openExternalUrlMock.mockReset();
analyticsTrackMock.mockReset();
notificationPermissionMock.mockReturnValue('default');
requestNotificationPermissionMock.mockResolvedValue('granted');
showCompletionNotificationMock.mockResolvedValue('shown');
fetchCodexPetsMock.mockResolvedValue({
pets: [],
rootDir: '/Users/test/.codex/pets',
});
syncCommunityPetsMock.mockResolvedValue({
wrote: 0,
skipped: 0,
failed: 0,
total: 0,
rootDir: '/Users/test/.codex/pets',
errors: [],
});
fetchSkillsMock.mockResolvedValue(sampleSkills);
fetchDesignSystemsMock.mockResolvedValue(sampleDesignSystems);
fetchSkillMock.mockImplementation(async (id: string) => ({
id,
body: `skill body for ${id}`,
}));
fetchDesignSystemMock.mockImplementation(async (id: string) => ({
id,
body: `design system body for ${id}`,
}));
fetchLatestGithubReleaseInfoMock.mockReset();
fetchLatestGithubReleaseInfoMock.mockResolvedValue(null);
openExternalUrlMock.mockResolvedValue(true);
importLocalDesignSystemMock.mockResolvedValue({
designSystem: {
id: 'imported-system',
title: 'Imported System',
summary: 'A newly imported system.',
category: 'Imported',
swatches: ['#0f766e', '#ccfbf1'],
},
});
importGitHubDesignSystemMock.mockResolvedValue({
designSystem: {
id: 'github-system',
title: 'GitHub System',
summary: 'A GitHub imported system.',
category: 'Imported',
swatches: ['#1d4ed8', '#bfdbfe'],
},
});
fetchProviderModelsMock.mockResolvedValue({
ok: true,
kind: 'success',
latencyMs: 1,
models: [],
});
});
afterEach(() => {
restoreOpenDesignHost?.();
restoreOpenDesignHost = null;
});
describe('SettingsDialog privacy settings interactions', () => {
afterEach(() => {
cleanup();
vi.unstubAllGlobals();
});
it('preserves a pending privacy choice when an unrelated parent config update arrives', async () => {
const randomUUID = vi.fn(() => 'inst-new');
vi.stubGlobal('crypto', { randomUUID });
const initial: AppConfig = {
...baseConfig,
mode: 'daemon',
agentId: null,
installationId: null,
privacyDecisionAt: 1778244000000,
telemetry: { metrics: false, content: false, artifactManifest: true },
};
const view = renderSettingsDialog(initial, { initialSection: 'privacy' });
fireEvent.click(screen.getByRole('button', { name: 'Share' }));
await waitFor(() => {
expect((screen.getByLabelText('Anonymous ID') as HTMLInputElement).value).toBe('inst-new');
});
view.rerender(
<SettingsDialog
initial={{ ...initial, agentId: 'codex' }}
agents={availableAgents}
daemonLive={true}
appVersionInfo={null}
initialSection="privacy"
onPersist={view.onPersist}
onPersistComposioKey={view.onPersistComposioKey}
onClose={view.onClose}
onRefreshAgents={view.onRefreshAgents}
/>,
);
expect(screen.getByRole('button', { name: 'Share' }).getAttribute('aria-pressed'))
.toBe('true');
expect((screen.getByLabelText('Anonymous ID') as HTMLInputElement).value).toBe('inst-new');
expect(screen.getByRole('button', { name: /Anonymous metrics/ }).getAttribute('aria-pressed'))
.toBe('true');
expect(screen.getByRole('button', { name: /Conversation and tool content/ }).getAttribute('aria-pressed'))
.toBe('true');
});
});
describe('SettingsDialog execution settings BYOK interactions', () => {
afterEach(() => {
cleanup();
vi.unstubAllGlobals();
});
it('collapses the settings sidebar and toggles fullscreen from dialog chrome', () => {
const { container } = renderSettingsDialog();
const dialog = screen.getByRole('dialog');
const sidebar = container.querySelector('#settings-sidebar');
expect(dialog.classList.contains('settings-sidebar-collapsed')).toBe(false);
expect(sidebar?.getAttribute('aria-hidden')).toBeNull();
fireEvent.click(screen.getByRole('button', { name: 'Collapse settings sidebar' }));
expect(dialog.classList.contains('settings-sidebar-collapsed')).toBe(true);
expect(sidebar?.getAttribute('aria-hidden')).toBe('true');
expect(
screen
.getByRole('button', { name: 'Expand settings sidebar' })
.getAttribute('aria-pressed'),
).toBe('true');
fireEvent.click(screen.getByRole('button', { name: 'Expand settings sidebar' }));
expect(dialog.classList.contains('settings-sidebar-collapsed')).toBe(false);
expect(sidebar?.getAttribute('aria-hidden')).toBeNull();
fireEvent.click(screen.getByRole('button', { name: 'Fullscreen' }));
expect(dialog.classList.contains('settings-fullscreen')).toBe(true);
expect(screen.getByRole('button', { name: 'Exit fullscreen' })).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: 'Exit fullscreen' }));
expect(dialog.classList.contains('settings-fullscreen')).toBe(false);
});
it('renders BYOK provider preset tabs and toggles API key visibility', () => {
renderSettingsDialog();
expect(screen.getByRole('tablist', { name: en['settings.protocolAria'] })).toBeTruthy();
expect(screen.getByRole('tab', { name: 'Anthropic' }).getAttribute('aria-selected')).toBe('true');
expect(screen.getByRole('tab', { name: 'OpenAI' })).toBeTruthy();
expect(screen.getByRole('tab', { name: 'Azure OpenAI' })).toBeTruthy();
expect(screen.getByRole('tab', { name: 'Google Gemini' })).toBeTruthy();
expect(screen.getByRole('tab', { name: 'Ollama Cloud' })).toBeTruthy();
expect(screen.getByRole('tab', { name: 'SenseAudio' })).toBeTruthy();
expect(screen.getByRole('tab', { name: 'AIHubMix' })).toBeTruthy();
expect(screen.queryByRole('tab', { name: 'AWS Bedrock' })).toBeNull();
expect(screen.getByLabelText('Gateway preset')).toBeTruthy();
expect(screen.getByLabelText('Model')).toBeTruthy();
const baseUrlInput = screen.getByLabelText('Base URL') as HTMLInputElement;
expect(baseUrlInput.value).toBe('https://api.anthropic.com');
expect(baseUrlInput.readOnly).toBe(true);
expect(screen.getByText('Default endpoint. Usually no need to change this.')).toBeTruthy();
const memoryModelDetails = screen
.getAllByText('Memory model')
.find((node) => node.closest('summary'))
?.closest('details');
expect(memoryModelDetails?.hasAttribute('open')).toBe(false);
expect(within(memoryModelDetails!).queryByLabelText('Base URL')).toBeNull();
expect(
screen
.getByRole('link', { name: 'Get key ↗' })
.getAttribute('href'),
).toBe('https://console.anthropic.com/settings/keys');
const apiKeyInput = screen.getByLabelText('API key') as HTMLInputElement;
expect(apiKeyInput.type).toBe('password');
fireEvent.click(screen.getByRole('button', { name: 'Show' }));
expect(apiKeyInput.type).toBe('text');
fireEvent.click(screen.getByRole('button', { name: 'Hide' }));
expect(apiKeyInput.type).toBe('password');
fireEvent.click(screen.getByRole('tab', { name: 'OpenAI' }));
expect(screen.getByLabelText('Gateway preset')).toBeTruthy();
expect((screen.getByLabelText('Base URL') as HTMLInputElement).value).toBe(
'https://api.openai.com/v1',
);
expect(
screen
.getByRole('link', { name: 'Get key ↗' })
.getAttribute('href'),
).toBe('https://platform.openai.com/api-keys');
});
it('isolates API key draft and visibility by BYOK provider preset', () => {
renderSettingsDialog({
apiProtocol: 'openai',
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-4o',
apiProviderBaseUrl: 'https://api.openai.com/v1',
});
const apiKeyInput = screen.getByLabelText('API key') as HTMLInputElement;
fireEvent.change(apiKeyInput, { target: { value: 'sk-openai-provider' } });
fireEvent.click(screen.getByRole('button', { name: 'Show' }));
expect(apiKeyInput.value).toBe('sk-openai-provider');
expect(apiKeyInput.type).toBe('text');
fireEvent.click(screen.getByRole('tab', { name: 'DeepSeek' }));
expect(apiKeyInput.value).toBe('');
expect(apiKeyInput.type).toBe('password');
expect((screen.getByLabelText('Base URL') as HTMLInputElement).value).toBe(
'https://api.deepseek.com',
);
fireEvent.change(apiKeyInput, { target: { value: 'sk-deepseek-provider' } });
fireEvent.click(screen.getByRole('tab', { name: 'OpenAI' }));
expect(apiKeyInput.value).toBe('sk-openai-provider');
expect(apiKeyInput.type).toBe('text');
fireEvent.click(screen.getByRole('tab', { name: 'DeepSeek' }));
expect(apiKeyInput.value).toBe('sk-deepseek-provider');
expect(apiKeyInput.type).toBe('password');
});
it('shows configured status from provider-scoped drafts for same-protocol presets', () => {
renderSettingsDialog({
apiProtocol: 'openai',
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-4o',
apiProviderBaseUrl: 'https://api.openai.com/v1',
});
const apiKeyInput = screen.getByLabelText('API key') as HTMLInputElement;
fireEvent.change(apiKeyInput, { target: { value: 'sk-openai-provider' } });
fireEvent.click(screen.getByRole('tab', { name: 'DeepSeek' }));
fireEvent.change(apiKeyInput, { target: { value: 'sk-deepseek-provider' } });
expect(screen.getByRole('tab', { name: 'OpenAI' }).getAttribute('title')).toBe(
'OpenAI - Configured',
);
expect(screen.getByRole('tab', { name: 'DeepSeek' }).getAttribute('title')).toBe(
'DeepSeek - Configured',
);
});
it('persists provider-scoped BYOK drafts across Settings reopen', async () => {
const first = renderSettingsDialog({
apiProtocol: 'openai',
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-4o',
apiProviderBaseUrl: 'https://api.openai.com/v1',
});
const apiKeyInput = screen.getByLabelText('API key') as HTMLInputElement;
fireEvent.change(apiKeyInput, { target: { value: 'sk-openai-provider' } });
fireEvent.click(screen.getByRole('tab', { name: 'DeepSeek' }));
await waitForPersist(
first.onPersist,
expect.objectContaining({
apiProviderBaseUrl: 'https://api.deepseek.com',
baseUrl: 'https://api.deepseek.com',
byokProviderConfigDrafts: expect.objectContaining({
'openai:https://api.openai.com/v1': expect.objectContaining({
apiConfig: expect.objectContaining({
apiKey: 'sk-openai-provider',
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-4o',
}),
}),
}),
}),
{},
);
const persistedConfig = first.onPersist.mock.calls.at(-1)?.[0] as AppConfig;
first.unmount();
renderSettingsDialog(persistedConfig);
expect((screen.getByLabelText('Base URL') as HTMLInputElement).value).toBe(
'https://api.deepseek.com',
);
fireEvent.click(screen.getByRole('tab', { name: 'OpenAI' }));
expect((screen.getByLabelText('API key') as HTMLInputElement).value).toBe(
'sk-openai-provider',
);
expect((screen.getByLabelText('Base URL') as HTMLInputElement).value).toBe(
'https://api.openai.com/v1',
);
expect(screen.getByRole('combobox', { name: 'Model' }).textContent).toContain('gpt-4o');
});
it('keeps BYOK file-editing limits discoverable from the provider heading (issue #1106)', () => {
// Regression cover: switching from Local CLI to BYOK previously gave no
// signal that file-editing tools (`Read`/`Write`/`Edit`) are absent on the
// API path. Users typed "continue adjusting the design" expecting edits
// and got an HTML monologue back. The notice now sits behind a heading
// info icon so it stays discoverable without competing with setup fields.
renderSettingsDialog();
const trigger = screen.getByTestId('settings-byok-no-file-tools-trigger');
expect(trigger).toBeTruthy();
const notice = screen.getByRole('tooltip');
expect(notice.textContent).toContain("BYOK can't read, write, or edit project files");
expect(notice.textContent).toContain('Local CLI');
fireEvent.click(screen.getByRole('tab', { name: 'OpenAI' }));
expect(screen.getByTestId('settings-byok-no-file-tools-trigger')).toBeTruthy();
fireEvent.click(screen.getByRole('tab', { name: 'Google Gemini' }));
expect(screen.getByTestId('settings-byok-no-file-tools-trigger')).toBeTruthy();
});
it('hides the BYOK no-file-tools notice when Local CLI mode is selected', () => {
renderSettingsDialog({ mode: 'daemon' });
expect(screen.queryByTestId('settings-byok-no-file-tools-notice')).toBeNull();
});
it('only persists Max tokens overrides within the supported BYOK range', async () => {
const { onPersist } = renderSettingsDialog({ apiKey: 'sk-test' });
const maxTokensInput = screen.getByRole('spinbutton', { name: /Max tokens/ }) as HTMLInputElement;
expect(maxTokensInput.min).toBe(String(MIN_MAX_TOKENS));
expect(maxTokensInput.max).toBe(String(MAX_MAX_TOKENS));
expect(maxTokensInput.step).toBe('1');
fireEvent.change(maxTokensInput, { target: { value: String(MIN_MAX_TOKENS - 1) } });
await waitFor(() => {
const latestConfig = onPersist.mock.calls.at(-1)?.[0] as AppConfig | undefined;
expect(latestConfig?.maxTokens).toBeUndefined();
});
expect(
onPersist.mock.calls.some(([config]) => (config as AppConfig).maxTokens === MIN_MAX_TOKENS - 1),
).toBe(false);
expect(maxTokensInput.value).toBe(String(MIN_MAX_TOKENS - 1));
fireEvent.blur(maxTokensInput);
expect(maxTokensInput.value).toBe('');
fireEvent.change(maxTokensInput, { target: { value: '64000' } });
await waitFor(() => {
const latestConfig = onPersist.mock.calls.at(-1)?.[0] as AppConfig | undefined;
expect(latestConfig?.maxTokens).toBe(64000);
});
fireEvent.change(maxTokensInput, { target: { value: String(MAX_MAX_TOKENS + 1) } });
await waitFor(() => {
const latestConfig = onPersist.mock.calls.at(-1)?.[0] as AppConfig | undefined;
expect(latestConfig?.maxTokens).toBeUndefined();
});
expect(
onPersist.mock.calls.some(([config]) => (config as AppConfig).maxTokens === MAX_MAX_TOKENS + 1),
).toBe(false);
fireEvent.change(maxTokensInput, { target: { value: '' } });
await waitFor(() => {
const latestConfig = onPersist.mock.calls.at(-1)?.[0] as AppConfig | undefined;
expect(latestConfig?.maxTokens).toBeUndefined();
});
});
it('lets Anthropic and Google users customize the default base URL', () => {
renderSettingsDialog();
expect((screen.getByLabelText('Base URL') as HTMLInputElement).readOnly).toBe(true);
fireEvent.click(screen.getByRole('button', { name: 'Customize' }));
expect((screen.getByLabelText('Base URL') as HTMLInputElement).readOnly).toBe(false);
cleanup();
renderSettingsDialog();
fireEvent.click(screen.getByRole('tab', { name: 'Google Gemini' }));
expect((screen.getByLabelText('Base URL') as HTMLInputElement).value).toBe(
'https://generativelanguage.googleapis.com',
);
expect((screen.getByLabelText('Base URL') as HTMLInputElement).readOnly).toBe(true);
});
it('updates model and base URL when quick fill provider changes', () => {
renderSettingsDialog({ apiProtocol: 'openai', baseUrl: 'https://api.openai.com/v1', model: 'gpt-4o', apiProviderBaseUrl: 'https://api.openai.com/v1' });
fireEvent.click(screen.getByRole('tab', { name: 'OpenAI' }));
selectGatewayPreset('DeepSeek — OpenAI');
expect(screen.getByRole('combobox', { name: 'Model' }).textContent).toContain('deepseek-chat');
expect((screen.getByLabelText('Base URL') as HTMLInputElement).value).toBe('https://api.deepseek.com');
});
it('keeps Anthropic-compatible gateway presets selectable', () => {
renderSettingsDialog();
const providerPopover = openGatewayPresetPopover();
expect(optionNames(providerPopover)).toEqual(
expect.arrayContaining([
'Custom provider',
'Anthropic (Claude)',
'DeepSeek — Anthropic',
'MiniMax — Anthropic',
'MiMo (Xiaomi) — Anthropic',
]),
);
fireEvent.click(within(providerPopover).getByRole('option', { name: 'DeepSeek — Anthropic' }));
expect(screen.getByRole('combobox', { name: 'Model' }).textContent).toContain('deepseek-chat');
expect((screen.getByLabelText('Base URL') as HTMLInputElement).value).toBe(
'https://api.deepseek.com/anthropic',
);
});
it('treats a manually edited base URL as a custom provider', () => {
renderSettingsDialog({ apiProtocol: 'openai', baseUrl: 'https://api.openai.com/v1', model: 'gpt-4o', apiProviderBaseUrl: 'https://api.openai.com/v1' });
fireEvent.click(screen.getByRole('tab', { name: 'OpenAI' }));
expect(screen.getByRole('combobox', { name: 'Gateway preset' }).textContent).toContain('OpenAI');
fireEvent.change(screen.getByLabelText('Base URL'), {
target: { value: 'https://my-proxy.example.com/v1' },
});
expect(screen.getByRole('combobox', { name: 'Gateway preset' }).textContent).toContain('Custom provider');
expect((screen.getByLabelText('Base URL') as HTMLInputElement).value).toBe(
'https://my-proxy.example.com/v1',
);
});
it('offers managed and self-hosted Ollama presets with editable base URLs', () => {
renderSettingsDialog();
fireEvent.click(screen.getByRole('tab', { name: 'Ollama Cloud' }));
const providerPopover = openGatewayPresetPopover();
expect(optionNames(providerPopover)).toEqual([
'Custom provider',
'Ollama Cloud (managed)',
'Ollama Self-hosted (local)',
]);
fireEvent.click(screen.getByRole('combobox', { name: 'Gateway preset' }));
expect((screen.getByLabelText('Base URL') as HTMLInputElement).readOnly).toBe(false);
selectGatewayPreset('Ollama Self-hosted (local)');
expect((screen.getByLabelText('Base URL') as HTMLInputElement).value).toBe(
'http://localhost:11434',
);
expect(screen.queryByRole('link', { name: /Get key/i })).toBeNull();
expect(screen.getByRole('button', { name: 'Test' })).toBeTruthy();
});
it('saves and auto-tests the self-hosted Ollama preset without an API key', async () => {
const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
const url = input.toString();
if (url === '/api/memory') {
return new Response(
JSON.stringify({ enabled: true, memories: [], extraction: null }),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
expect(url).toBe('/api/test/connection');
expect(JSON.parse(String(init?.body))).toMatchObject({
mode: 'provider',
protocol: 'ollama',
apiKey: '',
baseUrl: 'http://localhost:11434',
model: 'gemma3:4b',
});
return new Response(
JSON.stringify({
ok: true,
kind: 'ok',
latencyMs: 28,
model: 'gemma3:4b',
sample: 'pong',
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
});
vi.stubGlobal('fetch', fetchMock);
const { onPersist } = renderSettingsDialog();
fireEvent.click(screen.getByRole('tab', { name: 'Ollama Cloud' }));
selectGatewayPreset('Ollama Self-hosted (local)');
await waitForPersist(
onPersist,
expect.objectContaining({
apiProtocol: 'ollama',
apiKey: '',
baseUrl: 'http://localhost:11434',
model: 'gemma3:4b',
apiProviderBaseUrl: 'http://localhost:11434',
}),
{},
);
await waitFor(() => {
expect(screen.getByText(/Connected\. Replied in 28 ms/)).toBeTruthy();
});
const testConnectionCalls = fetchMock.mock.calls.filter(
([input]) => input.toString() === '/api/test/connection',
);
expect(testConnectionCalls).toHaveLength(1);
});
it('keeps protocol drafts isolated without leaking API keys between tabs', () => {
renderSettingsDialog({ apiKey: 'anthropic-key' });
const apiKeyInput = screen.getByLabelText('API key') as HTMLInputElement;
expect(apiKeyInput.value).toBe('anthropic-key');
fireEvent.click(screen.getByRole('tab', { name: 'OpenAI' }));
expect((screen.getByLabelText('API key') as HTMLInputElement).value).toBe('');
fireEvent.change(screen.getByLabelText('API key'), {
target: { value: 'openai-key' },
});
fireEvent.click(screen.getByRole('tab', { name: 'Anthropic' }));
expect((screen.getByLabelText('API key') as HTMLInputElement).value).toBe('anthropic-key');
fireEvent.click(screen.getByRole('tab', { name: 'OpenAI' }));
expect((screen.getByLabelText('API key') as HTMLInputElement).value).toBe('openai-key');
});
it('autosaves BYOK edits once required fields are valid', async () => {
const { onPersist } = renderSettingsDialog();
fireEvent.change(screen.getByLabelText('API key'), {
target: { value: 'sk-test' },
});
fireEvent.click(screen.getByRole('button', { name: 'Customize' }));
// A non-http scheme is still rejected client-side. (An internal-IP URL is
// no longer rejected here — it is syntactically valid and the daemon owns
// the allowlist decision; see #3225.)
fireEvent.change(screen.getByLabelText('Base URL'), {
target: { value: 'ftp://api.example.com' },
});
expect(screen.getByRole('alert').textContent).toContain(
'Use a public http:// or https:// URL.',
);
fireEvent.change(screen.getByLabelText('Base URL'), {
target: { value: 'http://localhost:11434/v1' },
});
await waitForPersist(
onPersist,
expect.objectContaining({
mode: 'api',
apiProtocol: 'anthropic',
apiKey: 'sk-test',
baseUrl: 'http://localhost:11434/v1',
model: 'claude-sonnet-4-5',
apiProviderBaseUrl: null,
}),
{},
);
});
it('surfaces autosave progress, success, and failure states in the modal chrome', async () => {
const first = renderSettingsDialog();
fireEvent.change(screen.getByLabelText('API key'), {
target: { value: 'sk-saved' },
});
await waitFor(() => {
expect(screen.getByText('Saving…')).toBeTruthy();
});
await waitFor(() => {
expect(screen.getByText('All changes saved')).toBeTruthy();
});
expect(first.onPersist).toHaveBeenCalledWith(
expect.objectContaining({ apiKey: 'sk-saved' }),
expect.any(Object),
);
cleanup();
const second = renderSettingsDialog();
second.onPersist.mockRejectedValueOnce(new Error('daemon offline'));
fireEvent.change(screen.getByLabelText('API key'), {
target: { value: 'sk-error' },
});
await waitFor(() => {
expect(screen.getByText('Saving…')).toBeTruthy();
});
await waitFor(() => {
expect(screen.getByText(/Couldn’t save changes/i)).toBeTruthy();
});
});
it('closes BYOK via the close button or backdrop', () => {
const first = renderSettingsDialog();
fireEvent.change(screen.getByLabelText('API key'), {
target: { value: 'sk-unsaved' },
});
fireEvent.click(first.container.querySelector('.settings-close') as HTMLElement);
expect(first.onClose).toHaveBeenCalledTimes(1);
cleanup();
const second = renderSettingsDialog();
fireEvent.change(screen.getByLabelText('API key'), {
target: { value: 'sk-unsaved-2' },
});
fireEvent.click(document.querySelector('.modal-backdrop') as HTMLElement);
expect(second.onClose).toHaveBeenCalledTimes(1);
});
it('shows Azure-specific fields and autosaves an Azure config', async () => {
const { onPersist } = renderSettingsDialog();
fireEvent.click(screen.getByRole('tab', { name: 'Azure OpenAI' }));
expect(screen.getByRole('heading', { name: 'Azure OpenAI' })).toBeTruthy();
expect(screen.getByLabelText('Deployment name')).toBeTruthy();
expect(screen.getByLabelText('API version')).toBeTruthy();
expect((screen.getByLabelText('Base URL') as HTMLInputElement).placeholder).toBe(
'Paste Azure endpoint URL',
);
expect(
screen.getByText('Find this in Azure portal → your resource → Endpoint.'),
).toBeTruthy();
fireEvent.change(screen.getByLabelText('API key'), {
target: { value: 'azure-key' },
});
fireEvent.change(screen.getByLabelText('Deployment name'), {
target: { value: '__custom__' },
});
fireEvent.change(screen.getByLabelText('Custom deployment name'), {
target: { value: 'deployment-one' },
});
fireEvent.change(screen.getByLabelText('Base URL'), {
target: { value: 'https://example.openai.azure.com' },
});
fireEvent.change(screen.getByLabelText('API version'), {
target: { value: '2024-10-21' },
});
await waitFor(() => {
expect(screen.getByText('Ready to test')).toBeTruthy();
});
await waitForPersist(
onPersist,
expect.objectContaining({
mode: 'api',
apiProtocol: 'azure',
apiKey: 'azure-key',
model: 'deployment-one',
baseUrl: 'https://example.openai.azure.com',
apiVersion: '2024-10-21',
apiProviderBaseUrl: null,
}),
{},
);
});
it('does not fetch provider models while the API key edit is still uncommitted', async () => {
renderSettingsDialog({
apiProtocol: 'openai',
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-4o',
apiProviderBaseUrl: 'https://api.openai.com/v1',
});
fireEvent.click(screen.getByRole('tab', { name: 'OpenAI' }));
fireEvent.change(screen.getByLabelText('API key'), {
target: { value: 'sk-partial' },
});
await new Promise((resolve) => window.setTimeout(resolve, 350));
expect(fetchProviderModelsMock).not.toHaveBeenCalled();
});
it('loads provider models automatically only after required fields are committed', async () => {
fetchProviderModelsMock.mockResolvedValueOnce({
ok: true,
kind: 'success',
latencyMs: 12,
models: [{ id: 'gpt-account', label: 'Account Model' }],
});
renderSettingsDialog({
apiProtocol: 'openai',
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-4o',
apiProviderBaseUrl: 'https://api.openai.com/v1',
});
fireEvent.click(screen.getByRole('tab', { name: 'OpenAI' }));
expect(screen.queryByRole('button', { name: 'Fetch models' })).toBeNull();
expect(fetchProviderModelsMock).not.toHaveBeenCalled();
fireEvent.change(screen.getByLabelText('API key'), {
target: { value: 'sk-openai' },
});
fireEvent.blur(screen.getByLabelText('API key'));
expect(await screen.findByText(/✓ Loaded \d+ models(?: from your account)?\./)).toBeTruthy();
expect(fetchProviderModelsMock).toHaveBeenCalledWith(
expect.objectContaining({
protocol: 'openai',
baseUrl: 'https://api.openai.com/v1',
apiKey: 'sk-openai',
}),
expect.any(AbortSignal),
);
const modelPicker = screen.getByRole('combobox', { name: 'Model' });
fireEvent.click(modelPicker);
const modelPopover = screen.getByTestId('settings-byok-model-popover');
expect(optionNames(modelPopover)).toEqual(expect.arrayContaining([
'Account Model (gpt-account) · From your account',
'gpt-4o · Suggested',
'Custom (type below)…',
]));
expect(analyticsTrackMock).toHaveBeenCalledWith(
'settings_byok_models_fetch_result',
expect.objectContaining({
page_name: 'settings',
area: 'configure_execution_mode_byok',
provider_id: 'openai',
result: 'success',
trigger: 'auto',
source: 'network',
model_count: 1,
}),
undefined,
);
fireEvent.click(screen.getByRole('tab', { name: 'Azure OpenAI' }));
expect(screen.queryByRole('button', { name: 'Fetch models' })).toBeNull();
expect(screen.getByText(/Azure deployments can’t be fetched automatically/)).toBeTruthy();
fireEvent.click(screen.getByRole('tab', { name: 'Ollama Cloud' }));
expect(screen.queryByRole('button', { name: 'Fetch models' })).toBeNull();
expect(screen.getByText('Model discovery is not available for this protocol.')).toBeTruthy();
fetchProviderModelsMock.mockClear();
fireEvent.click(screen.getByRole('tab', { name: '小米 MiMo' }));
await new Promise((resolve) => window.setTimeout(resolve, 350));
expect(fetchProviderModelsMock).not.toHaveBeenCalled();
expect(screen.queryByRole('button', { name: 'Fetch models' })).toBeNull();
expect(screen.getByText('Model discovery is not available for this protocol.')).toBeTruthy();
expect(screen.getByRole('combobox', { name: 'Model' }).textContent).toContain('mimo-v2.5-pro');
});
it('auto-loads provider models after a pasted dirty key is cleaned on blur', async () => {
fetchProviderModelsMock.mockResolvedValueOnce({
ok: true,
kind: 'success',
latencyMs: 12,
models: [{ id: 'gpt-account', label: 'Account Model' }],
});
renderSettingsDialog({
apiProtocol: 'openai',
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-4o',
apiProviderBaseUrl: 'https://api.openai.com/v1',
});
fireEvent.click(screen.getByRole('tab', { name: 'OpenAI' }));
expect(fetchProviderModelsMock).not.toHaveBeenCalled();
// Paste a key with a leading zero-width char + trailing newline/tab.
fireEvent.change(screen.getByLabelText('API key'), {
target: { value: '\u200Bsk-openai\n\t' },
});
fireEvent.blur(screen.getByLabelText('API key'));
// If onByokKeyCommit committed the dirty-key cache key, the auto-fetch
// effect would bail on providerModelsCommittedKey !== providerModelsKey
// and this text would never appear.
expect(await screen.findByText(/✓ Loaded \d+ models(?: from your account)?\./)).toBeTruthy();
expect(fetchProviderModelsMock).toHaveBeenCalledWith(
expect.objectContaining({
protocol: 'openai',
baseUrl: 'https://api.openai.com/v1',
apiKey: 'sk-openai',
}),
expect.any(AbortSignal),
);
});
it('defaults to an account model when discovery replaces a provider preset', async () => {
fetchProviderModelsMock.mockResolvedValueOnce({
ok: true,
kind: 'success',
latencyMs: 12,
models: [{ id: 'account-ready-model', label: 'Account Ready' }],
});
const { onPersist } = renderSettingsDialog({
apiProtocol: 'openai',
apiKey: 'sk-openai',
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-4o',
apiProviderBaseUrl: 'https://api.openai.com/v1',
});
fireEvent.click(screen.getByRole('tab', { name: 'OpenAI' }));
expect(await screen.findByText(/✓ Loaded \d+ models(?: from your account)?\./)).toBeTruthy();
expect(screen.getByRole('combobox', { name: 'Model' }).textContent).toContain(
'Account Ready (account-ready-model) · From your account',
);
await waitForPersist(
onPersist,
expect.objectContaining({
apiProtocol: 'openai',
model: 'account-ready-model',
}),
{},
);
});
it('does not carry a generic preset model into a fresh custom provider before discovery', async () => {
fetchProviderModelsMock.mockResolvedValueOnce({
ok: true,
kind: 'success',
latencyMs: 12,
models: [{ id: 'endpoint-model', label: 'Endpoint Model' }],
});
const { onPersist } = renderSettingsDialog({
apiProtocol: 'openai',
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-4o',
apiProviderBaseUrl: 'https://api.openai.com/v1',
});
fireEvent.click(screen.getByRole('tab', { name: 'Custom provider' }));
expect((screen.getByLabelText('Custom model id') as HTMLInputElement).value).toBe('');
fireEvent.change(screen.getByLabelText('Base URL'), {
target: { value: 'https://custom.example.com/v1' },
});
fireEvent.change(screen.getByLabelText('API key'), {
target: { value: 'sk-custom' },
});
fireEvent.blur(screen.getByLabelText('API key'));
expect(await screen.findByText(/✓ Loaded \d+ models(?: from your account)?\./)).toBeTruthy();
expect(screen.getByRole('combobox', { name: 'Model' }).textContent).toContain(
'Endpoint Model (endpoint-model) · From your account',
);
await waitForPersist(
onPersist,
expect.objectContaining({
apiProviderBaseUrl: null,
baseUrl: 'https://custom.example.com/v1',
model: 'endpoint-model',
}),
{},
);
});
it('restores fetched model discovery state when switching back to a provider', async () => {
fetchProviderModelsMock.mockResolvedValueOnce({
ok: true,
kind: 'success',
latencyMs: 12,
models: [{ id: 'account-ready-model', label: 'Account Ready' }],
});
renderSettingsDialog({
apiProtocol: 'openai',
apiKey: 'sk-openai',
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-4o',
apiProviderBaseUrl: 'https://api.openai.com/v1',
});
fireEvent.click(screen.getByRole('tab', { name: 'OpenAI' }));
expect(await screen.findByText(/✓ Loaded \d+ models(?: from your account)?\./)).toBeTruthy();
fireEvent.click(screen.getByRole('tab', { name: 'DeepSeek' }));
expect(screen.queryByText(/✓ Loaded \d+ models(?: from your account)?\./)).toBeNull();
fireEvent.click(screen.getByRole('tab', { name: 'OpenAI' }));
expect(screen.getByText(/✓ Loaded \d+ models(?: from your account)?\./)).toBeTruthy();
expect(fetchProviderModelsMock).toHaveBeenCalledTimes(1);
});
it('keeps a suggested model the user explicitly re-picked after discovery resolves', async () => {
fetchProviderModelsMock.mockResolvedValueOnce({
ok: true,
kind: 'success',
latencyMs: 12,
models: [{ id: 'account-ready-model', label: 'Account Ready' }],
});
const { onPersist } = renderSettingsDialog({
apiProtocol: 'openai',
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-4o',
apiProviderBaseUrl: 'https://api.openai.com/v1',
});
fireEvent.click(screen.getByRole('tab', { name: 'OpenAI' }));
expect(fetchProviderModelsMock).not.toHaveBeenCalled();
// The user deliberately re-picks the suggested model that equals the
// provider preset id before discovery runs.
fireEvent.click(screen.getByRole('combobox', { name: 'Model' }));
const modelPopover = screen.getByTestId('settings-byok-model-popover');
fireEvent.click(within(modelPopover).getByRole('option', { name: 'gpt-4o' }));
// Supplying the key triggers account-model discovery.
fireEvent.change(screen.getByLabelText('API key'), {
target: { value: 'sk-openai' },
});
fireEvent.blur(screen.getByLabelText('API key'));
expect(await screen.findByText(/✓ Loaded \d+ models(?: from your account)?\./)).toBeTruthy();
// The explicit pick must survive discovery — no silent rewrite to the
// first fetched account model.
const modelCombobox = screen.getByRole('combobox', { name: 'Model' });
expect(modelCombobox.textContent).toContain('gpt-4o');
expect(modelCombobox.textContent).not.toContain('account-ready-model');
await waitForPersist(
onPersist,
expect.objectContaining({
apiProtocol: 'openai',
model: 'gpt-4o',
}),
{},
);
});
it('does not show a BYOK Test button or nag when the API key is still missing', () => {
renderSettingsDialog({
apiProtocol: 'anthropic',
apiKey: '',
baseUrl: 'https://api.anthropic.com',
model: 'claude-sonnet-4-5',
});
expect(screen.queryByRole('button', { name: 'Test' })).toBeNull();
fireEvent.blur(screen.getByLabelText('API key'));
expect(screen.queryByText('Fill API key to test the connection.')).toBeNull();
});
it('auto-tests a saved complete BYOK config when Settings opens', async () => {
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
const url = input.toString();
if (url === '/api/memory') {
return new Response(
JSON.stringify({ enabled: true, memories: [], extraction: null }),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
expect(url).toBe('/api/test/connection');
return new Response(
JSON.stringify({
ok: true,
kind: 'ok',
latencyMs: 21,
model: 'claude-sonnet-4-5',
sample: 'pong',
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
});
vi.stubGlobal('fetch', fetchMock);
renderSettingsDialog({ apiKey: 'sk-ant-test-provider' });
expect(await screen.findByText(/Connected\. Replied in 21 ms/)).toBeTruthy();
const testConnectionCalls = fetchMock.mock.calls.filter(
([input]) => input.toString() === '/api/test/connection',
);
expect(testConnectionCalls).toHaveLength(1);
});
it('auto-tests BYOK after required fields become locally valid', async () => {
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
const url = input.toString();
if (url === '/api/memory') {
return new Response(
JSON.stringify({ enabled: true, memories: [], extraction: null }),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
expect(url).toBe('/api/test/connection');
return new Response(
JSON.stringify({
ok: true,
kind: 'ok',
latencyMs: 14,
model: 'claude-sonnet-4-5',
sample: 'pong',
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
});
vi.stubGlobal('fetch', fetchMock);
renderSettingsDialog({ apiKey: '' });
fireEvent.change(screen.getByLabelText('API key'), {
target: { value: 'sk-ant-test-provider' },
});
expect(await screen.findByText(/Connected\. Replied in 14 ms/)).toBeTruthy();
const testConnectionCalls = fetchMock.mock.calls.filter(
([input]) => input.toString() === '/api/test/connection',
);
expect(testConnectionCalls).toHaveLength(1);
});
it('filters long BYOK model lists after provider discovery succeeds without hiding the current selection', async () => {
fetchProviderModelsMock.mockResolvedValueOnce({
ok: true,
kind: 'success',
latencyMs: 12,
models: [
{ id: 'gpt-4.1-mini', label: 'gpt-4.1-mini' },
{ id: 'gpt-4.1', label: 'gpt-4.1' },
{ id: 'gpt-5.5', label: 'gpt-5.5' },
{ id: 'o4-mini', label: 'o4-mini' },
{ id: 'o3', label: 'o3' },
{ id: 'o1', label: 'o1' },
{ id: 'gpt-4o', label: 'gpt-4o' },
{ id: 'gpt-4o-mini', label: 'gpt-4o-mini' },
],
});
renderSettingsDialog({
apiProtocol: 'openai',
apiKey: 'sk-openai',
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-4.1-mini',
apiProviderBaseUrl: 'https://api.openai.com/v1',
});
fireEvent.click(screen.getByRole('tab', { name: 'OpenAI' }));
expect(await screen.findByText(/✓ Loaded \d+ models(?: from your account)?\./)).toBeTruthy();
const modelPicker = screen.getByRole('combobox', { name: 'Model' });
fireEvent.click(modelPicker);
const modelPopover = screen.getByTestId('settings-byok-model-popover');
const searchInput = within(modelPopover).getByTestId('settings-byok-model-search') as HTMLInputElement;
fireEvent.change(searchInput, { target: { value: '5.5' } });
expect(optionNames(modelPopover)).toEqual([
'gpt-4.1-mini · From your account',
'gpt-5.5 · From your account',
'Custom (type below)…',
]);
});
it('reports the account model count when account models are combined with provider suggestions', async () => {
fetchProviderModelsMock.mockResolvedValueOnce({
ok: true,
kind: 'success',
latencyMs: 12,
models: [
{ id: 'glm-4.6', label: 'glm-4.6' },
{ id: 'account-glm-1', label: 'Account GLM 1' },
{ id: 'account-glm-2', label: 'Account GLM 2' },
{ id: 'account-glm-3', label: 'Account GLM 3' },
{ id: 'account-glm-4', label: 'Account GLM 4' },
{ id: 'account-glm-5', label: 'Account GLM 5' },
{ id: 'account-glm-6', label: 'Account GLM 6' },
{ id: 'account-glm-7', label: 'Account GLM 7' },
],
});
renderSettingsDialog({
apiProtocol: 'openai',
apiKey: 'sk-zhipu',
baseUrl: 'https://open.bigmodel.cn/api/paas/v4',
model: 'glm-4.6',
apiProviderBaseUrl: 'https://open.bigmodel.cn/api/paas/v4',
});
fireEvent.click(screen.getByRole('tab', { name: '智谱' }));
expect(await screen.findByText('✓ Loaded 8 models from your account.')).toBeTruthy();
fireEvent.click(screen.getByRole('combobox', { name: 'Model' }));
const modelPopover = screen.getByTestId('settings-byok-model-popover');
expect(within(modelPopover).getByRole('option', { name: 'glm-4-plus · Suggested' })).toBeTruthy();
expect(within(modelPopover).getByRole('option', { name: 'glm-4-air · Suggested' })).toBeTruthy();
});
it('fetches provider models, merges them into the picker, and preserves a custom current model', async () => {
fetchProviderModelsMock.mockResolvedValueOnce({
ok: true,
kind: 'success',
latencyMs: 12,
models: [
{ id: 'remote-alpha', label: 'Remote Alpha' },
{ id: 'gpt-4o', label: 'gpt-4o' },
],
});
renderSettingsDialog({
apiProtocol: 'openai',
apiKey: 'sk-openai',
baseUrl: 'https://api.openai.com/v1',
model: 'custom-still-here',
apiProviderBaseUrl: 'https://api.openai.com/v1',
});
fireEvent.click(screen.getByRole('tab', { name: 'OpenAI' }));
expect((screen.getByLabelText('Custom model id') as HTMLInputElement).value).toBe('custom-still-here');
expect(await screen.findByText(/✓ Loaded \d+ models(?: from your account)?\./)).toBeTruthy();
expect(fetchProviderModelsMock).toHaveBeenCalledWith(
expect.objectContaining({
protocol: 'openai',
baseUrl: 'https://api.openai.com/v1',
apiKey: 'sk-openai',
}),
expect.any(AbortSignal),
);
const modelPicker = screen.getByRole('combobox', { name: 'Model' });
fireEvent.click(modelPicker);
const modelPopover = screen.getByTestId('settings-byok-model-popover');
expect(optionNames(modelPopover)).toEqual(expect.arrayContaining([
'Remote Alpha (remote-alpha) · From your account',
'gpt-4o · From your account',
'Custom (type below)…',
]));
expect((screen.getByLabelText('Custom model id') as HTMLInputElement).value).toBe('custom-still-here');
});
it('clears stale fetched-model status when provider fields change', async () => {
fetchProviderModelsMock.mockResolvedValueOnce({
ok: true,
kind: 'success',
latencyMs: 12,
models: [{ id: 'remote-alpha', label: 'Remote Alpha' }],
});
renderSettingsDialog({
apiProtocol: 'openai',
apiKey: 'sk-openai',
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-4o',
apiProviderBaseUrl: 'https://api.openai.com/v1',
});
fireEvent.click(screen.getByRole('tab', { name: 'OpenAI' }));
expect(await screen.findByText(/✓ Loaded \d+ models(?: from your account)?\./)).toBeTruthy();
fireEvent.change(screen.getByLabelText('Base URL'), {
target: { value: 'https://proxy.example.com/v1' },
});
await waitFor(() => {
expect(screen.queryByText(/✓ Loaded \d+ models(?: from your account)?\./)).toBeNull();
});
});
it('renders automatic provider auth failures under the API key field', async () => {
fetchProviderModelsMock.mockResolvedValueOnce({
ok: false,
kind: 'auth_failed',
latencyMs: 12,
status: 401,
detail: 'bad key',
});
renderSettingsDialog({
apiProtocol: 'openai',
apiKey: 'sk-openai',
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-4o',
apiProviderBaseUrl: 'https://api.openai.com/v1',
});
fireEvent.click(screen.getByRole('tab', { name: 'OpenAI' }));
expect(await screen.findByText('Invalid API key.')).toBeTruthy();
expect(screen.queryByText('Ready to test')).toBeNull();
expect(screen.getByRole('button', { name: 'Test' })).toBeTruthy();
});
it('renders non-auth provider model discovery failures explicitly', async () => {
fetchProviderModelsMock.mockResolvedValueOnce({
ok: false,
kind: 'upstream_unavailable',
latencyMs: 12,
status: 503,
detail: 'provider unavailable',
});
renderSettingsDialog({
apiProtocol: 'openai',
apiKey: 'sk-openai',
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-4o',
apiProviderBaseUrl: 'https://api.openai.com/v1',
});
fireEvent.click(screen.getByRole('tab', { name: 'OpenAI' }));
expect(await screen.findByText('Could not fetch models: provider unavailable')).toBeTruthy();
});
it('supports custom model entry in BYOK mode', async () => {
const { onPersist } = renderSettingsDialog({ apiProtocol: 'openai', baseUrl: 'https://api.openai.com/v1', model: 'gpt-4o', apiProviderBaseUrl: 'https://api.openai.com/v1' });
fireEvent.click(screen.getByRole('tab', { name: 'OpenAI' }));
fireEvent.change(screen.getByLabelText('API key'), {
target: { value: 'sk-openai' },
});
fireEvent.click(screen.getByRole('combobox', { name: 'Model' }));
const modelPopover = screen.getByTestId('settings-byok-model-popover');
fireEvent.click(within(modelPopover).getByRole('option', { name: 'Custom (type below)…' }));
const customModelInput = screen.getByLabelText('Custom model id') as HTMLInputElement;
expect(customModelInput).toBeTruthy();
fireEvent.change(customModelInput, {
target: { value: 'gpt-4.1-custom' },
});
await waitForPersist(
onPersist,
expect.objectContaining({
apiProtocol: 'openai',
apiKey: 'sk-openai',
model: 'gpt-4.1-custom',
baseUrl: 'https://api.openai.com/v1',
}),
{},
);
});
it('runs the BYOK connection test manually only after required fields are present', async () => {
const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
const url = input.toString();
// MemoryModelInline mounts inside the BYOK section and reads the
// current extraction override from /api/memory on mount. Swallow
// it here so the assertion below only counts the test-connection
// POST the user actually triggered.
if (url === '/api/memory') {
return new Response(
JSON.stringify({ enabled: true, memories: [], extraction: null }),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
expect(url).toBe('/api/test/connection');
expect(JSON.parse(String(init?.body))).toMatchObject({
mode: 'provider',
protocol: 'anthropic',
apiKey: 'sk-ant-test-provider',
baseUrl: 'https://api.anthropic.com',
model: 'claude-sonnet-4-5',
});
return new Response(
JSON.stringify({
ok: true,
kind: 'ok',
latencyMs: 42,
model: 'claude-sonnet-4-5',
sample: 'pong',
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
});
vi.stubGlobal('fetch', fetchMock);
renderSettingsDialog({ apiKey: 'sk-ant-test-provider' });
expect(screen.getByRole('button', { name: 'Test' })).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: 'Test' }));
await waitFor(() => {
expect(screen.getByText('Testing connection…')).toBeTruthy();
});
await waitFor(() => {
expect(screen.getByText(/Connected\. Replied in 42 ms/)).toBeTruthy();
});
const testConnectionCalls = fetchMock.mock.calls.filter(
([input]) => input.toString() === '/api/test/connection',
);
expect(testConnectionCalls).toHaveLength(1);
});
it('shows provider upstream detail for failed BYOK connection tests', async () => {
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
const url = input.toString();
if (url === '/api/memory') {
return new Response(
JSON.stringify({ enabled: true, memories: [], extraction: null }),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
expect(url).toBe('/api/test/connection');
return new Response(
JSON.stringify({
ok: false,
kind: 'upstream_unavailable',
latencyMs: 42,
status: 400,
detail:
'The selected NVIDIA model instance is currently unavailable at the provider. Try a different model or retry later.',
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
});
vi.stubGlobal('fetch', fetchMock);
renderSettingsDialog({ apiKey: 'sk-ant-test-provider' });
fireEvent.click(screen.getByRole('button', { name: 'Test' }));
expect(
await screen.findByText(
/Provider returned 400\. Try again in a moment\. The selected NVIDIA model instance is currently unavailable/,
),
).toBeTruthy();
});
it('blocks an obvious OpenAI key in the Anthropic tab before testing', async () => {
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
if (input.toString() === '/api/memory') {
return new Response(
JSON.stringify({ enabled: true, memories: [], extraction: null }),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
throw new Error(`unexpected request: ${input.toString()}`);
});
vi.stubGlobal('fetch', fetchMock);
renderSettingsDialog({ apiKey: 'sk-openai-provider' });
expect(screen.getByRole('alert').textContent).toContain('Invalid API key.');
expect(screen.queryByText('Ready to test')).toBeNull();
fireEvent.click(screen.getByRole('button', { name: 'Test' }));
expect(screen.getByRole('alert').textContent).toContain('Invalid API key.');
expect(screen.queryByText('Ready to test')).toBeNull();
await waitFor(() => {
const testConnectionCalls = fetchMock.mock.calls.filter(
([input]) => input.toString() === '/api/test/connection',
);
expect(testConnectionCalls).toHaveLength(0);
});
expect(analyticsTrackMock).toHaveBeenCalledWith(
'settings_byok_test_result',
expect.objectContaining({
page_name: 'settings',
area: 'execution_model',
provider_id: 'anthropic',
result: 'failed',
error_code: 'api_key_wrong_protocol',
error_kind: 'api_key_wrong_protocol',
field_missing: 'none',
config_key_changed: false,
success_after_action: false,
duration_ms: 0,
}),
undefined,
);
});
it('shows API key and Base URL errors together for mistyped first-party URLs', async () => {
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
if (input.toString() === '/api/memory') {
return new Response(
JSON.stringify({ enabled: true, memories: [], extraction: null }),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
throw new Error(`unexpected request: ${input.toString()}`);
});
vi.stubGlobal('fetch', fetchMock);
renderSettingsDialog({
apiKey: 'sk-openai-provider',
baseUrl: 'https://api.anthropic.comsssss',
apiProviderBaseUrl: null,
});
const apiKeyField = screen.getByLabelText('API key').closest('label');
const baseUrlField = screen.getByLabelText('Base URL').closest('label');
expect(apiKeyField).toBeTruthy();
expect(baseUrlField).toBeTruthy();
expect(
within(apiKeyField as HTMLElement).getByText('Invalid API key.'),
).toBeTruthy();
expect(
within(baseUrlField as HTMLElement).getByText('Base URL is invalid or unreachable.'),
).toBeTruthy();
expect(screen.queryByText('Ready to test')).toBeNull();
await new Promise((resolve) => window.setTimeout(resolve, 600));
const testConnectionCalls = fetchMock.mock.calls.filter(
([input]) => input.toString() === '/api/test/connection',
);
expect(testConnectionCalls).toHaveLength(0);
});
it('blocks mistyped first-party URLs with a valid key before auto-testing', async () => {
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
if (input.toString() === '/api/memory') {
return new Response(
JSON.stringify({ enabled: true, memories: [], extraction: null }),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
throw new Error(`unexpected request: ${input.toString()}`);
});
vi.stubGlobal('fetch', fetchMock);
renderSettingsDialog({
apiKey: 'sk-ant-test-provider',
baseUrl: 'https://api.anthropic.comx',
apiProviderBaseUrl: null,
});
const apiKeyField = screen.getByLabelText('API key').closest('label');
const baseUrlField = screen.getByLabelText('Base URL').closest('label');
expect(apiKeyField).toBeTruthy();
expect(baseUrlField).toBeTruthy();
expect(
within(apiKeyField as HTMLElement).queryByText('Invalid API key.'),
).toBeNull();
expect(
within(baseUrlField as HTMLElement).getByText('Base URL is invalid or unreachable.'),
).toBeTruthy();
expect(screen.queryByRole('button', { name: 'Test' })).toBeNull();
expect(screen.queryByText('Ready to test')).toBeNull();
await new Promise((resolve) => window.setTimeout(resolve, 600));
const testConnectionCalls = fetchMock.mock.calls.filter(
([input]) => input.toString() === '/api/test/connection',
);
expect(testConnectionCalls).toHaveLength(0);
expect(fetchProviderModelsMock).not.toHaveBeenCalled();
});
it('sends a cleaned API key when the pasted value has trailing newline/zero-width characters', async () => {
let sentApiKey: unknown;
const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
const url = input.toString();
if (url === '/api/memory') {
return new Response(
JSON.stringify({ enabled: true, memories: [], extraction: null }),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
expect(url).toBe('/api/test/connection');
sentApiKey = JSON.parse(String(init?.body)).apiKey;
return new Response(
JSON.stringify({
ok: true,
kind: 'ok',
latencyMs: 7,
model: 'claude-sonnet-4-5',
sample: 'pong',
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
});
vi.stubGlobal('fetch', fetchMock);
// Leading zero-width char + trailing newline/tab — the exact shape a
// pasted key picks up from a docs code block or a terminal copy.
renderSettingsDialog({ apiKey: '\u200Bsk-ant-test-provider\n\t' });
fireEvent.blur(screen.getByLabelText('API key'));
// The auto-test must survive the apiKey-cleanup re-render: if it fired in
// the blur tick it would be dropped by the stale-revision guard and the
// success state would never reach the UI.
await waitFor(() => {
expect(screen.getByText(/Connected\. Replied in 7 ms/)).toBeTruthy();
});
const testConnectionCalls = fetchMock.mock.calls.filter(
([input]) => input.toString() === '/api/test/connection',
);
expect(testConnectionCalls).toHaveLength(1);
// The malformed value must never reach the wire.
expect(sentApiKey).toBe('sk-ant-test-provider');
});
it('shows a BYOK API key cleaned notice after blur cleanup', async () => {
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
const url = input.toString();
if (url === '/api/memory') {
return new Response(
JSON.stringify({ enabled: true, memories: [], extraction: null }),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
expect(url).toBe('/api/test/connection');
return new Response(
JSON.stringify({
ok: true,
kind: 'ok',
latencyMs: 7,
model: 'claude-sonnet-4-5',
sample: 'pong',
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
});
vi.stubGlobal('fetch', fetchMock);
renderSettingsDialog();
const apiKeyInput = screen.getByLabelText('API key') as HTMLInputElement;
fireEvent.change(apiKeyInput, {
target: { value: ' \u200Bsk-ant-test-provider\n' },
});
fireEvent.blur(apiKeyInput);
await waitFor(() => {
expect(apiKeyInput.value).toBe('sk-ant-test-provider');
});
expect(screen.getByText(en['settings.apiKeyCleaned'])).toBeTruthy();
const testConnectionCalls = fetchMock.mock.calls.filter(
([input]) => input.toString() === '/api/test/connection',
);
expect(testConnectionCalls).toHaveLength(1);
});
it('lets users retry a failed BYOK connection test without editing the API key', async () => {
let attempt = 0;
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
const url = input.toString();
if (url === '/api/memory') {
return new Response(
JSON.stringify({ enabled: true, memories: [], extraction: null }),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
attempt += 1;
return new Response(
JSON.stringify(
attempt === 1
? {
ok: false,
kind: 'timeout',
latencyMs: 30000,
model: 'claude-sonnet-4-5',
}
: {
ok: true,
kind: 'ok',
latencyMs: 18,
model: 'claude-sonnet-4-5',
sample: 'pong',
},
),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
});
vi.stubGlobal('fetch', fetchMock);
renderSettingsDialog({ apiKey: 'sk-ant-test-provider' });
fireEvent.click(screen.getByRole('button', { name: 'Test' }));
expect(await screen.findByRole('button', { name: 'Retry test' })).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: 'Retry test' }));
expect(await screen.findByText(/Connected\. Replied in 18 ms/)).toBeTruthy();
const testConnectionCalls = fetchMock.mock.calls.filter(
([input]) => input.toString() === '/api/test/connection',
);
expect(testConnectionCalls).toHaveLength(2);
});
it('marks a successful BYOK test after a config edit as success after action', async () => {
const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
const url = input.toString();
if (url === '/api/memory') {
return new Response(
JSON.stringify({ enabled: true, memories: [], extraction: null }),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
expect(url).toBe('/api/test/connection');
const body = JSON.parse(String(init?.body)) as { apiKey?: string };
return new Response(
JSON.stringify(
body.apiKey === 'sk-ant-fixed'
? {
ok: true,
kind: 'ok',
latencyMs: 20,
model: 'claude-sonnet-4-5',
sample: 'pong',
}
: {
ok: false,
kind: 'auth_failed',
latencyMs: 18,
model: 'claude-sonnet-4-5',
},
),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
});
vi.stubGlobal('fetch', fetchMock);
renderSettingsDialog({ apiKey: 'sk-ant-stale' });
fireEvent.click(screen.getByRole('button', { name: 'Test' }));
expect(await screen.findByRole('button', { name: 'Retry test' })).toBeTruthy();
fireEvent.change(screen.getByLabelText('API key'), {
target: { value: 'sk-ant-fixed' },
});
fireEvent.click(screen.getByRole('button', { name: 'Test' }));
expect(await screen.findByText(/Connected\. Replied in 20 ms/)).toBeTruthy();
expect(analyticsTrackMock).toHaveBeenCalledWith(
'settings_byok_test_result',
expect.objectContaining({
page_name: 'settings',
area: 'execution_model',
provider_id: 'anthropic',
result: 'success',
field_missing: 'none',
config_key_changed: true,
success_after_action: true,
}),
undefined,
);
});
it('renders invalid Base URL test failures on the Base URL field', async () => {
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
const url = input.toString();
if (url === '/api/memory') {
return new Response(
JSON.stringify({ enabled: true, memories: [], extraction: null }),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
expect(url).toBe('/api/test/connection');
return new Response(
JSON.stringify({
ok: false,
kind: 'invalid_base_url',
latencyMs: 12,
model: 'claude-sonnet-4-5',
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
});
vi.stubGlobal('fetch', fetchMock);
renderSettingsDialog({ apiKey: 'sk-ant-test-provider' });
expect(await screen.findByText('Base URL is invalid or unreachable.')).toBeTruthy();
const baseUrlField = screen.getByLabelText('Base URL').closest('label');
expect(baseUrlField).toBeTruthy();
expect(
within(baseUrlField as HTMLElement).getByText('Base URL is invalid or unreachable.'),
).toBeTruthy();
expect(screen.getAllByText('Base URL is invalid or unreachable.')).toHaveLength(1);
expect(screen.getByRole('button', { name: 'Retry test' })).toBeTruthy();
});
it('renders auth failed test failures on the API key field', async () => {
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
const url = input.toString();
if (url === '/api/memory') {
return new Response(
JSON.stringify({ enabled: true, memories: [], extraction: null }),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
expect(url).toBe('/api/test/connection');
return new Response(
JSON.stringify({
ok: false,
kind: 'auth_failed',
latencyMs: 12,
model: 'claude-sonnet-4-5',
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
});
vi.stubGlobal('fetch', fetchMock);
renderSettingsDialog({ apiKey: 'sk-ant-test-provider' });
expect(await screen.findByText('Invalid API key.')).toBeTruthy();
const apiKeyField = screen.getByLabelText('API key').closest('label');
expect(apiKeyField).toBeTruthy();
expect(
within(apiKeyField as HTMLElement).getByText('Invalid API key.'),
).toBeTruthy();
expect(screen.getAllByText('Invalid API key.')).toHaveLength(1);
expect(screen.queryByText('Authentication failed. Check your API key.')).toBeNull();
expect(screen.queryByText('Ready to test')).toBeNull();
expect(screen.getByRole('button', { name: 'Retry test' })).toBeTruthy();
});
it('focuses the model field when the BYOK test returns model not found', async () => {
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
const url = input.toString();
if (url === '/api/memory') {
return new Response(
JSON.stringify({ enabled: true, memories: [], extraction: null }),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
expect(url).toBe('/api/test/connection');
return new Response(
JSON.stringify({
ok: false,
kind: 'not_found_model',
latencyMs: 18,
model: 'missing-model',
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
});
vi.stubGlobal('fetch', fetchMock);
renderSettingsDialog({
apiProtocol: 'openai',
apiKey: 'sk-openai',
baseUrl: 'https://api.openai.com/v1',
model: 'missing-model',
apiProviderBaseUrl: 'https://api.openai.com/v1',
});
fireEvent.click(screen.getByRole('tab', { name: 'OpenAI' }));
fireEvent.click(screen.getByRole('button', { name: 'Test' }));
expect(await screen.findByText("Model 'missing-model' not found on this endpoint.")).toBeTruthy();
await waitFor(() => {
expect(document.activeElement).toBe(screen.getByLabelText('Custom model id'));
});
});
});
describe('SettingsDialog execution settings Local CLI interactions', () => {
afterEach(() => {
cleanup();
vi.unstubAllGlobals();
});
it('pins Open Design to the top of the installed CLI list', () => {
const claudeAgent: AgentInfo = {
id: 'claude',
name: 'Claude Code',
bin: 'claude',
available: true,
version: '2.1.196',
models: [{ id: 'default', label: 'Default' }],
};
vi.stubGlobal('fetch', vi.fn(async (input: RequestInfo | URL) => {
const url = input.toString();
if (url === '/api/memory') {
return new Response(
JSON.stringify({ enabled: true, memories: [], extraction: null }),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
if (url === '/api/integrations/vela/status') {
return new Response(
JSON.stringify({
loggedIn: false,
loginInFlight: false,
profile: 'local',
user: null,
configPath: '/Users/test/.amr/config.json',
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
return new Response('{}', { status: 200 });
}));
renderSettingsDialog(
{ mode: 'daemon', agentId: 'codex' },
{ agents: [availableAgents[0]!, claudeAgent, amrAgent] },
);
fireEvent.click(screen.getByRole('tab', { name: /Local CLI.*3 installed/i }));
expect(
screen
.getAllByTestId(/^settings-agent-card-/)
.map((card) => card.getAttribute('data-testid')),
).toEqual([
'settings-agent-card-amr',
'settings-agent-card-codex',
'settings-agent-card-claude',
]);
});
it('lets users switch to Local CLI, select an installed agent, and autosave', async () => {
const installed = availableAgents[0]!;
const unavailable: AgentInfo = {
id: 'kimi',
name: 'Kimi CLI',
bin: 'kimi',
available: false,
version: null,
models: [],
installUrl: 'https://github.com/MoonshotAI/kimi-cli',
docsUrl: 'https://www.kimi.com/code/docs/en/kimi-cli/guides/getting-started.html?aff=open-design',
};
const { onPersist } = renderSettingsDialog(
{ mode: 'daemon', agentId: null },
{ agents: [installed, unavailable] },
);
const localCliTab = screen.getByRole('tab', { name: /Local CLI.*1 installed/i });
fireEvent.click(localCliTab);
expect(screen.getByText('Your CLIs (1)')).toBeTruthy();
const installGroupSummary = screen.getByText('Available to install (1)');
expect(installGroupSummary.closest('details')?.hasAttribute('open')).toBe(false);
const codexCard = screen.getByRole('button', { name: /Codex CLI/i }) as HTMLButtonElement;
fireEvent.click(installGroupSummary);
const kimiGroup = screen.getByRole('group', { name: /Kimi CLI/i });
expect(within(kimiGroup).getByText('Moonshot Kimi CLI')).toBeTruthy();
expect(
(within(kimiGroup).getByRole('link', { name: en['settings.agentInstall.install'] }) as HTMLAnchorElement).getAttribute('href'),
).toBe(
'https://github.com/MoonshotAI/kimi-cli',
);
expect(
screen.getByText(en['settings.agentInstall.stepAuth']),
).toBeTruthy();
expect(
screen.getByText(en['settings.agentInstall.stepSelect']),
).toBeTruthy();
expect(screen.getByText(en['settings.agentInstall.pathHint'])).toBeTruthy();
fireEvent.click(codexCard);
const selectedCard = codexCard.closest('.agent-card') as HTMLElement;
expect(
within(selectedCard).getByRole('combobox', {
name: en['settings.modelPicker'],
}),
).toBeTruthy();
expect(
selectedCard.compareDocumentPosition(installGroupSummary) &
Node.DOCUMENT_POSITION_FOLLOWING,
).toBeTruthy();
await waitForPersist(
onPersist,
expect.objectContaining({
mode: 'daemon',
agentId: 'codex',
}),
{},
);
});
it('filters long Local CLI model lists in Settings without hiding the current selection', () => {
renderSettingsDialog(
{ mode: 'daemon', agentId: 'codex', agentModels: { codex: { model: 'gpt-4.1-mini' } } },
{
agents: [
{
...availableAgents[0]!,
modelsSource: 'live',
models: [
{ id: 'default', label: 'Default' },
{ id: 'gpt-4.1-mini', label: 'gpt-4.1-mini' },
{ id: 'gpt-4.1', label: 'gpt-4.1' },
{ id: 'gpt-5.5', label: 'gpt-5.5' },
{ id: 'o4-mini', label: 'o4-mini' },
{ id: 'o3', label: 'o3' },
{ id: 'o1', label: 'o1' },
{ id: 'gpt-4o', label: 'gpt-4o' },
{ id: 'gpt-4o-mini', label: 'gpt-4o-mini' },
],
},
],
},
);
fireEvent.click(screen.getByRole('tab', { name: /Local CLI/i }));
const codexCard = screen.getByRole('button', { name: /Codex CLI/i });
fireEvent.click(codexCard);
const modelPicker = screen.getByRole('combobox', {
name: en['settings.modelPicker'],
});
fireEvent.click(modelPicker);
const searchInput = screen.getByTestId('settings-agent-model-search-codex') as HTMLInputElement;
fireEvent.change(searchInput, { target: { value: '5.5' } });
const modelPopover = screen.getByTestId('settings-agent-model-popover-codex');
expect(optionNames(modelPopover)).toEqual(['gpt-4.1-mini', 'gpt-5.5', 'Custom (type below)…']);
});
it('labels live CLI model metadata in the model picker', () => {
renderSettingsDialog(
{ mode: 'daemon', agentId: 'codex' },
{
agents: [
{
...availableAgents[0]!,
modelsSource: 'live',
models: [
{ id: 'default', label: 'Default' },
{ id: 'gpt-6-codex', label: 'GPT-6 Codex' },
],
},
],
},
);
fireEvent.click(screen.getByRole('tab', { name: /Local CLI/i }));
expect(screen.getByText('Live from CLI')).toBeTruthy();
expect(
screen.getByText(/Model list comes from this CLI/i),
).toBeTruthy();
});
it('labels fallback CLI model metadata in the model picker', () => {
renderSettingsDialog(
{ mode: 'daemon', agentId: 'codex' },
{
agents: [
{
...availableAgents[0]!,
modelsSource: 'fallback',
},
],
},
);
fireEvent.click(screen.getByRole('tab', { name: /Local CLI/i }));
expect(screen.getByText('Built-in list')).toBeTruthy();
expect(
screen.getByText(/Showing built-in defaults/i),
).toBeTruthy();
});
it('uses the existing Settings card picker for AMR without exposing custom stale models', () => {
renderSettingsDialog(
{
mode: 'daemon',
agentId: 'amr',
agentModels: { amr: { model: 'gpt-5.4-mini', reasoning: 'default' } },
},
{
agents: [
{
...amrAgent,
modelsSource: 'live',
models: [
{ id: 'glm-5', label: 'GLM 5' },
{ id: 'glm-5.1', label: 'GLM 5.1' },
],
},
],
},
);
fireEvent.click(screen.getByRole('tab', { name: /Local CLI/i }));
fireEvent.click(screen.getByRole('button', { name: /^Open Design\b/ }));
const modelPickers = screen.getAllByRole('combobox', {
name: en['settings.modelPicker'],
});
expect(modelPickers).toHaveLength(1);
expect(modelPickers[0]?.textContent).toContain('GLM 5');
fireEvent.click(modelPickers[0]!);
const modelPopover = screen.getByTestId('settings-agent-model-popover-amr');
expect(optionNames(modelPopover)).toEqual(['GLM 5', 'GLM 5.1']);
expect(screen.queryByLabelText(en['settings.modelCustomLabel'])).toBeNull();
});
it('shows an empty state when no local CLI agents are detected', () => {
renderSettingsDialog(
{ mode: 'daemon', agentId: null },
{ agents: [] },
);
fireEvent.click(screen.getByRole('tab', { name: /Local CLI.*0 installed/i }));
expect(screen.getByText(/No agents detected yet/i)).toBeTruthy();
});
it('labels the memory model default with the selected Local CLI', async () => {
const agents: AgentInfo[] = [
...availableAgents,
{
id: 'claude',
name: 'Claude Code',
bin: 'claude',
available: true,
version: '1.2.3',
models: [{ id: 'default', label: 'Default (CLI config)' }],
},
];
vi.stubGlobal('fetch', vi.fn(async (input: RequestInfo | URL) => {
const url = input.toString();
if (url === '/api/memory') {
return new Response(
JSON.stringify({ enabled: true, memories: [], extraction: null }),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
return new Response(JSON.stringify({}), { status: 404 });
}));
renderSettingsDialog(
{ mode: 'daemon', agentId: 'claude' },
{ agents },
);
fireEvent.click(screen.getByRole('tab', { name: /Local CLI.*2 installed/i }));
const memoryModel = await screen.findByRole('combobox', { name: 'Memory model' });
expect(memoryModel.textContent).toBe('Same as chat (Claude Code)');
expect(screen.getByText(/anthropic is only the fallback provider family/i)).toBeTruthy();
});
it('shows rescan loading, avoids duplicate rescans, and renders the success notice', async () => {
const nextAgents: AgentInfo[] = [
availableAgents[0]!,
{
id: 'claude',
name: 'Claude Code',
bin: 'claude',
available: true,
version: '1.2.3',
models: [{ id: 'default', label: 'Default' }],
},
];
const pending = deferred<AgentInfo[]>();
const onRefreshAgents = vi.fn(() => pending.promise);
renderSettingsDialog(
{ mode: 'daemon', agentId: 'codex' },
{ agents: availableAgents, onRefreshAgents },
);
fireEvent.click(screen.getByRole('tab', { name: /Local CLI.*1 installed/i }));
const rescanButton = screen.getByRole('button', { name: /Rescan|Scanning/i }) as HTMLButtonElement;
fireEvent.click(rescanButton);
expect(onRefreshAgents).toHaveBeenCalledTimes(1);
expect(onRefreshAgents).toHaveBeenCalledWith({
throwOnError: true,
agentCliEnv: {},
});
expect(rescanButton.disabled).toBe(true);
expect(screen.getByText('Scanning...')).toBeTruthy();
fireEvent.click(rescanButton);
expect(onRefreshAgents).toHaveBeenCalledTimes(1);
pending.resolve(nextAgents);
await waitFor(() => {
expect(screen.getByText('Scan complete. 2 available.')).toBeTruthy();
expect((screen.getByRole('button', { name: /Rescan/i }) as HTMLButtonElement).disabled).toBe(false);
});
});
it('renders an error notice when rescan fails', async () => {
const onRefreshAgents = vi.fn(async () => {
throw new Error('boom');
});
renderSettingsDialog(
{ mode: 'daemon', agentId: 'codex' },
{ agents: availableAgents, onRefreshAgents },
);
fireEvent.click(screen.getByRole('tab', { name: /Local CLI.*1 installed/i }));
fireEvent.click(screen.getByRole('button', { name: /Rescan/i }));
await waitFor(() => {
expect(screen.getByText('Scan failed. Check the daemon and try again.')).toBeTruthy();
});
});
it('renders diagnostics and fix actions on installed agents with auth failures', async () => {
const docsUrl = 'https://docs.example.com/codex-auth';
const authMissingAgent: AgentInfo = {
...availableAgents[0]!,
authStatus: 'missing',
authMessage: 'Codex CLI is installed but not authenticated.',
docsUrl,
diagnostics: [
{
reason: 'auth-missing',
severity: 'error',
message: 'Codex CLI is installed but not authenticated.',
fixActions: [{ kind: 'openDocs' }, { kind: 'rescan' }],
},
],
};
const onRefreshAgents = vi.fn(async () => [authMissingAgent]);
renderSettingsDialog(
{ mode: 'daemon', agentId: 'codex' },
{ agents: [authMissingAgent], onRefreshAgents },
);
fireEvent.click(screen.getByRole('tab', { name: /Local CLI.*1 installed/i }));
const codexCard = screen.getByRole('button', { name: /Codex CLI/i })
.closest('.agent-card') as HTMLElement;
expect(
within(codexCard).getByText('Codex CLI is installed but not authenticated.'),
).toBeTruthy();
fireEvent.click(within(codexCard).getByRole('button', {
name: en['settings.agentInstall.docs'],
}));
expect(openExternalUrlMock).toHaveBeenCalledWith(docsUrl);
fireEvent.click(within(codexCard).getByRole('button', {
name: en['settings.rescan'],
}));
await waitFor(() => {
expect(onRefreshAgents).toHaveBeenCalledWith({
throwOnError: true,
agentCliEnv: {},
});
});
});
it('rescans automatically when returning after opening an install link', async () => {
const unavailable: AgentInfo = {
id: 'kimi',
name: 'Kimi CLI',
bin: 'kimi',
available: false,
version: null,
models: [],
installUrl: 'https://github.com/MoonshotAI/kimi-cli',
};
const onRefreshAgents = vi.fn(async () => availableAgents);
renderSettingsDialog(
{ mode: 'daemon', agentId: 'codex' },
{ agents: [availableAgents[0]!, unavailable], onRefreshAgents },
);
fireEvent.click(screen.getByRole('tab', { name: /Local CLI.*1 installed/i }));
fireEvent.click(screen.getByText('Available to install (1)'));
fireEvent.click(screen.getByRole('link', { name: en['settings.agentInstall.install'] }));
expect(onRefreshAgents).not.toHaveBeenCalled();
document.dispatchEvent(new Event('visibilitychange'));
await waitFor(() => {
expect(onRefreshAgents).toHaveBeenCalledWith({
throwOnError: true,
agentCliEnv: {},
});
});
});
it('autosaves CLI env overrides from the execution form', async () => {
const { onPersist } = renderSettingsDialog(
{ mode: 'daemon', agentId: 'codex' },
{ agents: availableAgents },
);
expect(
screen.getByLabelText('Codex/OpenAI CLI API key (CODEX_API_KEY)'),
).toBeTruthy();
expect(
screen.getByLabelText('Codex/OpenAI CLI API key (OPENAI_API_KEY)'),
).toBeTruthy();
fireEvent.change(screen.getByLabelText('Codex home'), {
target: { value: ' ~/.codex-team ' },
});
await waitForPersist(
onPersist,
expect.objectContaining({
mode: 'daemon',
agentId: 'codex',
agentCliEnv: {
codex: { CODEX_HOME: '~/.codex-team' },
},
}),
{},
);
});
it('disables Local CLI mode when the daemon is offline', () => {
renderSettingsDialog(
{ mode: 'api' },
{ agents: availableAgents, daemonLive: false },
);
const localCliTab = screen.getByRole('tab', { name: /Local CLI.*daemon offline/i }) as HTMLButtonElement;
expect(localCliTab.disabled).toBe(true);
expect(localCliTab.getAttribute('title')).toBe('Daemon is not running');
expect(screen.getByRole('tab', { name: /BYOK.*API provider/i }).getAttribute('aria-selected')).toBe('true');
});
it('renders a Local CLI connection test for selected installed agents', () => {
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
const url = input.toString();
if (url === '/api/memory') {
return new Response(
JSON.stringify({ enabled: true, memories: [], extraction: null }),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
throw new Error(`Unexpected fetch: ${url}`);
});
vi.stubGlobal('fetch', fetchMock);
renderSettingsDialog(
{ mode: 'daemon', agentId: 'codex' },
{ agents: availableAgents },
);
fireEvent.click(screen.getByRole('tab', { name: /Local CLI.*1 installed/i }));
expect(screen.getByRole('button', { name: /Codex CLI/i })).toBeTruthy();
expect(screen.getByRole('button', { name: 'Test' })).toBeTruthy();
});
it('renders the AMR local agent without vela branding and with the Local CLI test action', async () => {
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
const url = input.toString();
if (url === '/api/memory') {
return new Response(
JSON.stringify({ enabled: true, memories: [], extraction: null }),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
if (url === '/api/integrations/vela/status') {
return new Response(
JSON.stringify({
loggedIn: false,
profile: 'default',
user: null,
configPath: '/Users/test/.amr/config.json',
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
throw new Error(`Unexpected fetch: ${url}`);
});
vi.stubGlobal('fetch', fetchMock);
renderSettingsDialog(
{ mode: 'daemon', agentId: 'amr' },
{ agents: [amrAgent] },
);
fireEvent.click(screen.getByRole('tab', { name: /Local CLI.*1 installed/i }));
expect(screen.getByRole('button', { name: /^Open Design\b/ })).toBeTruthy();
expect(screen.queryByText('1.0.0')).toBeNull();
expect(screen.queryByText(/AMR \(vela\)/i)).toBeNull();
expect(screen.queryByText(/vela/i)).toBeNull();
expect(screen.queryByText(/Not signed in/i)).toBeNull();
expect(screen.getByText('Official')).toBeTruthy();
expect(screen.queryByText('Lower cost')).toBeNull();
expect(screen.getByText('Many models')).toBeTruthy();
expect(screen.queryByText('Limited bonus: +100%')).toBeNull();
expect(await screen.findByRole('button', { name: 'Authorize' })).toBeTruthy();
expect(screen.queryByRole('button', { name: 'Test' })).toBeNull();
});
it('only shows the AMR authorization action after selecting the AMR card', async () => {
globalThis.fetch = vi.fn(async (input: RequestInfo | URL) => {
const url = input.toString();
if (url === '/api/memory') {
return new Response(
JSON.stringify({ enabled: true, memories: [], extraction: null }),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
if (url === '/api/integrations/vela/status') {
return new Response(
JSON.stringify({ loggedIn: false, profile: 'local', user: null }),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
throw new Error(`Unexpected fetch: ${url}`);
}) as typeof fetch;
const codexAgent = availableAgents.find((agent) => agent.id === 'codex');
expect(codexAgent).toBeTruthy();
if (!codexAgent) throw new Error('missing codex test agent');
renderSettingsDialog(
{ mode: 'daemon', agentId: codexAgent.id },
{ agents: [amrAgent, codexAgent] },
);
fireEvent.click(screen.getByRole('tab', { name: /Local CLI.*2 installed/i }));
expect(screen.getByRole('button', { name: /^Open Design\b/ })).toBeTruthy();
expect(screen.queryByRole('button', { name: 'Authorize' })).toBeNull();
fireEvent.click(screen.getByRole('button', { name: /^Open Design\b/ }));
expect(await screen.findByRole('button', { name: 'Authorize' })).toBeTruthy();
});
it('reveals AMR cancel only while hovering the active card during sign-in', async () => {
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
const url = input.toString();
if (url === '/api/memory') {
return new Response(
JSON.stringify({ enabled: true, memories: [], extraction: null }),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
if (url === '/api/integrations/vela/status') {
return new Response(
JSON.stringify({
loggedIn: false,
loginInFlight: true,
profile: 'local',
user: null,
configPath: '/Users/test/.amr/config.json',
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
throw new Error(`Unexpected fetch: ${url}`);
});
vi.stubGlobal('fetch', fetchMock);
renderSettingsDialog(
{ mode: 'daemon', agentId: 'amr' },
{ agents: [amrAgent] },
);
fireEvent.click(screen.getByRole('tab', { name: /Local CLI.*1 installed/i }));
const amrCardButton = screen.getByRole('button', { name: /^Open Design\b/ });
const amrCard = amrCardButton.closest('.agent-card') as HTMLElement;
expect(amrCard).toBeTruthy();
expect(await screen.findByText('Signing in…')).toBeTruthy();
expect(screen.queryByRole('button', { name: 'Cancel' })).toBeNull();
fireEvent.mouseEnter(amrCard);
expect(await screen.findByRole('button', { name: 'Cancel' })).toBeTruthy();
fireEvent.mouseLeave(amrCard);
await waitFor(() => {
expect(screen.queryByRole('button', { name: 'Cancel' })).toBeNull();
});
});
it('cancels an in-flight AMR sign-in and returns to Authorize after a brief canceled state', async () => {
let statusStage: 'pending' | 'signed-out' = 'pending';
const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
const url = input.toString();
if (url === '/api/memory') {
return new Response(
JSON.stringify({ enabled: true, memories: [], extraction: null }),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
if (url === '/api/integrations/vela/status') {
return new Response(
JSON.stringify(
statusStage === 'pending'
? {
loggedIn: false,
loginInFlight: true,
profile: 'local',
user: null,
configPath: '/Users/test/.amr/config.json',
}
: {
loggedIn: false,
loginInFlight: false,
profile: 'local',
user: null,
configPath: '/Users/test/.amr/config.json',
},
),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
if (url === '/api/integrations/vela/login/cancel' && init?.method === 'POST') {
statusStage = 'signed-out';
return new Response(JSON.stringify({ canceled: true }), {
status: 200,
headers: { 'content-type': 'application/json' },
});
}
throw new Error(`Unexpected fetch: ${url}`);
});
vi.stubGlobal('fetch', fetchMock);
renderSettingsDialog(
{ mode: 'daemon', agentId: 'amr' },
{ agents: [amrAgent] },
);
fireEvent.click(screen.getByRole('tab', { name: /Local CLI.*1 installed/i }));
const amrCard = screen.getByRole('button', { name: /^Open Design\b/ }).closest('.agent-card') as HTMLElement;
expect(await screen.findByText('Signing in…')).toBeTruthy();
fireEvent.mouseEnter(amrCard);
fireEvent.click(await screen.findByRole('button', { name: 'Cancel' }));
expect(await screen.findByText('Canceled')).toBeTruthy();
expect(fetchMock).toHaveBeenCalledWith('/api/integrations/vela/login/cancel', { method: 'POST' });
await waitFor(() => {
expect(screen.getByRole('button', { name: 'Authorize' })).toBeTruthy();
}, { timeout: 3000 });
expect(screen.queryByText('Canceled')).toBeNull();
});
// Regression for the race called out on #3158 by both codex-connector and
// looper: the daemon's `cancelVelaLogin()` only SIGTERMs the vela child
// and keeps it in `activeLoginProcs` until it actually exits, so a
// `/api/integrations/vela/status` read right after a successful cancel
// can legally still report `loginInFlight: true`. If the AmrLoginPill
// listener self-refreshed on the local cancel path it would bounce back
// into `Signing in…` polling and surface the timeout/error path even
// though the user already canceled.
it('does not bounce back to Signing in… when daemon /status still reports loginInFlight after a local cancel (#3158)', async () => {
let cancelReceived = false;
const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
const url = input.toString();
if (url === '/api/memory') {
return new Response(
JSON.stringify({ enabled: true, memories: [], extraction: null }),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
if (url === '/api/integrations/vela/status') {
// Keep reporting in-flight even *after* the cancel API succeeds —
// this is the SIGTERM-to-exit window where the daemon hasn't reaped
// the vela child yet.
return new Response(
JSON.stringify({
loggedIn: false,
loginInFlight: true,
profile: 'local',
user: null,
configPath: '/Users/test/.amr/config.json',
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
if (url === '/api/integrations/vela/login/cancel' && init?.method === 'POST') {
cancelReceived = true;
return new Response(JSON.stringify({ canceled: true }), {
status: 200,
headers: { 'content-type': 'application/json' },
});
}
throw new Error(`Unexpected fetch: ${url}`);
});
vi.stubGlobal('fetch', fetchMock);
renderSettingsDialog(
{ mode: 'daemon', agentId: 'amr' },
{ agents: [amrAgent] },
);
fireEvent.click(screen.getByRole('tab', { name: /Local CLI.*1 installed/i }));
const amrCard = screen.getByRole('button', { name: /^Open Design\b/ }).closest('.agent-card') as HTMLElement;
expect(await screen.findByText('Signing in…')).toBeTruthy();
fireEvent.mouseEnter(amrCard);
fireEvent.click(await screen.findByRole('button', { name: 'Cancel' }));
expect(await screen.findByText('Canceled')).toBeTruthy();
expect(cancelReceived).toBe(true);
// Give the listener event handler — plus any rogue polling tick — a
// generous window to misfire. Under the buggy code path the pill would
// call /status again, see loginInFlight:true, setPending('login'), and
// restart polling, flipping the UI back to 'Signing in…'.
await new Promise((resolve) => setTimeout(resolve, 400));
expect(screen.queryByText('Signing in…')).toBeNull();
// Eventually the Canceled UI window times out and Authorize re-appears.
await waitFor(() => {
expect(screen.getByRole('button', { name: 'Authorize' })).toBeTruthy();
}, { timeout: 3000 });
// And still no bounce back to Signing in…
expect(screen.queryByText('Signing in…')).toBeNull();
});
it('reconciles late AMR browser completion to Signed in after local cancel', async () => {
let statusStage: 'pending' | 'signed-out' | 'signed-in' = 'pending';
const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
const url = input.toString();
if (url === '/api/memory') {
return new Response(
JSON.stringify({ enabled: true, memories: [], extraction: null }),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
if (url === '/api/integrations/vela/status') {
const body =
statusStage === 'pending'
? {
loggedIn: false,
loginInFlight: true,
profile: 'local',
user: null,
configPath: '/Users/test/.amr/config.json',
}
: statusStage === 'signed-in'
? {
loggedIn: true,
loginInFlight: false,
profile: 'local',
user: { id: 'user-1', email: 'late@example.com' },
configPath: '/Users/test/.amr/config.json',
}
: {
loggedIn: false,
loginInFlight: false,
profile: 'local',
user: null,
configPath: '/Users/test/.amr/config.json',
};
return new Response(JSON.stringify(body), {
status: 200,
headers: { 'content-type': 'application/json' },
});
}
if (url === '/api/integrations/vela/login/cancel' && init?.method === 'POST') {
statusStage = 'signed-out';
return new Response(JSON.stringify({ canceled: true }), {
status: 200,
headers: { 'content-type': 'application/json' },
});
}
throw new Error(`Unexpected fetch: ${url}`);
});
vi.stubGlobal('fetch', fetchMock);
renderSettingsDialog(
{ mode: 'daemon', agentId: 'amr' },
{ agents: [amrAgent] },
);
fireEvent.click(screen.getByRole('tab', { name: /Local CLI.*1 installed/i }));
const amrCard = screen.getByRole('button', { name: /^Open Design\b/ }).closest('.agent-card') as HTMLElement;
expect(await screen.findByText('Signing in…')).toBeTruthy();
fireEvent.mouseEnter(amrCard);
fireEvent.click(await screen.findByRole('button', { name: 'Cancel' }));
expect(await screen.findByText('Canceled')).toBeTruthy();
statusStage = 'signed-in';
window.dispatchEvent(
new CustomEvent('od:amr-login-status-change', {
detail: { reason: 'status-changed' },
}),
);
await waitFor(() => {
expect(screen.getByRole('button', { name: 'Sign out' })).toBeTruthy();
expect(screen.getByText('late@example.com')).toBeTruthy();
});
});
it('renders the signed-in AMR account state inside Settings without leaking vela branding', async () => {
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
const url = input.toString();
if (url === '/api/memory') {
return new Response(
JSON.stringify({ enabled: true, memories: [], extraction: null }),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
if (url === '/api/integrations/vela/status') {
return new Response(
JSON.stringify({
loggedIn: true,
profile: 'local',
user: {
id: 'user-1',
email: 'signed-in@example.com',
name: 'Signed In User',
},
account: { plan: 'pro' },
configPath: '/Users/test/.amr/config.json',
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
throw new Error(`Unexpected fetch: ${url}`);
});
vi.stubGlobal('fetch', fetchMock);
renderSettingsDialog(
{ mode: 'daemon', agentId: 'amr' },
{ agents: [amrAgent] },
);
fireEvent.click(screen.getByRole('tab', { name: /Local CLI.*1 installed/i }));
expect(await screen.findByRole('button', { name: 'Sign out' })).toBeTruthy();
expect(screen.getByRole('button', { name: /^Open Design\b/ })).toBeTruthy();
expect(screen.getByRole('button', { name: /Plan pro/ })).toBeTruthy();
expect(screen.getByText('signed-in@example.com')).toBeTruthy();
expect(screen.queryByText(/AMR \(vela\)/i)).toBeNull();
expect(screen.queryByText(/^vela$/i)).toBeNull();
});
it('keeps the AMR plan badge on the account row outside the clipped benefits row', async () => {
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
const url = input.toString();
if (url === '/api/memory') {
return new Response(
JSON.stringify({ enabled: true, memories: [], extraction: null }),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
if (url === '/api/integrations/vela/status') {
return new Response(
JSON.stringify({
loggedIn: true,
profile: 'local',
user: {
id: 'user-1',
email: 'signed-in@example.com',
name: 'Signed In User',
},
account: { plan: 'pro' },
configPath: '/Users/test/.amr/config.json',
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
throw new Error(`Unexpected fetch: ${url}`);
});
vi.stubGlobal('fetch', fetchMock);
renderSettingsDialog(
{ mode: 'daemon', agentId: 'amr' },
{ agents: [amrAgent] },
);
fireEvent.click(screen.getByRole('tab', { name: /Local CLI.*1 installed/i }));
const amrCardButton = await screen.findByRole('button', { name: /Plan pro/ });
const planBadge = within(amrCardButton).getByText('pro');
const benefits = amrCardButton.querySelector('.agent-card-benefits');
const planSlot = planBadge.closest('.agent-card-plan-badge-slot');
const accountRow = planBadge.closest('.agent-card-amr-email');
expect(benefits).toBeTruthy();
expect(planSlot).toBeTruthy();
expect(accountRow).toBeTruthy();
expect(accountRow?.textContent).toContain('signed-in@example.com');
expect(planSlot?.getAttribute('aria-hidden')).toBe('true');
expect(benefits?.contains(planBadge)).toBe(false);
});
it('loads the Settings AMR wallet fallback balance without a manual card refresh button', async () => {
let walletCalls = 0;
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
const url = input.toString();
if (url === '/api/memory') {
return new Response(
JSON.stringify({ enabled: true, memories: [], extraction: null }),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
if (url === '/api/integrations/vela/status') {
return new Response(
JSON.stringify({
loggedIn: true,
profile: 'local',
user: {
id: 'user-1',
email: 'signed-in@example.com',
name: 'Signed In User',
},
configPath: '/Users/test/.amr/config.json',
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
if (url === '/api/integrations/vela/wallet' || url === '/api/integrations/vela/wallet?refresh=1') {
walletCalls += 1;
return new Response(
JSON.stringify({
status: 'available',
profile: 'local',
user: { id: 'user-1', email: 'signed-in@example.com' },
balanceUsd: '1.0000',
updatedAt: '2026-06-29T08:00:00.000Z',
fetchedAt: '2026-06-29T08:00:01.000Z',
stale: false,
source: 'vela_api',
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
throw new Error(`Unexpected fetch: ${url}`);
});
vi.stubGlobal('fetch', fetchMock);
renderSettingsDialog(
{ mode: 'daemon', agentId: 'amr' },
{ agents: [amrAgent] },
);
fireEvent.click(screen.getByRole('tab', { name: /Local CLI.*1 installed/i }));
expect(await screen.findByText('$1.00')).toBeTruthy();
expect(screen.queryByRole('button', { name: 'Refresh AMR wallet balance' })).toBeNull();
expect(walletCalls).toBe(1);
expect(fetchMock).toHaveBeenCalledWith('/api/integrations/vela/wallet', {
cache: 'no-store',
});
});
it('renders env-backed AMR login inside Settings without fabricating account details', async () => {
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
const url = input.toString();
if (url === '/api/memory') {
return new Response(
JSON.stringify({ enabled: true, memories: [], extraction: null }),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
if (url === '/api/integrations/vela/status') {
return new Response(
JSON.stringify({
loggedIn: true,
profile: 'local',
user: null,
configPath: '/Users/test/.amr/config.json',
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
throw new Error(`Unexpected fetch: ${url}`);
});
vi.stubGlobal('fetch', fetchMock);
renderSettingsDialog(
{ mode: 'daemon', agentId: 'amr' },
{ agents: [amrAgent] },
);
fireEvent.click(screen.getByRole('tab', { name: /Local CLI.*1 installed/i }));
expect(await screen.findByRole('button', { name: 'Sign out' })).toBeTruthy();
expect(screen.getByRole('button', { name: /^Open Design\b/ })).toBeTruthy();
expect(screen.queryByText(/@/i)).toBeNull();
expect(screen.queryByText(/AMR \(vela\)/i)).toBeNull();
});
it('does not keep a stale signed-in AMR state after a later Settings reopen reads loggedOut', async () => {
let statusCalls = 0;
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
const url = input.toString();
if (url === '/api/memory') {
return new Response(
JSON.stringify({ enabled: true, memories: [], extraction: null }),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
if (url === '/api/integrations/vela/status') {
statusCalls += 1;
return new Response(
JSON.stringify(
statusCalls === 1
? {
loggedIn: true,
profile: 'local',
user: { id: 'user-1', email: 'signed-in@example.com' },
configPath: '/Users/test/.amr/config.json',
}
: {
loggedIn: false,
profile: 'local',
user: null,
configPath: '/Users/test/.amr/config.json',
},
),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
throw new Error(`Unexpected fetch: ${url}`);
});
vi.stubGlobal('fetch', fetchMock);
const first = renderSettingsDialog(
{ mode: 'daemon', agentId: 'amr' },
{ agents: [amrAgent] },
);
fireEvent.click(screen.getByRole('tab', { name: /Local CLI.*1 installed/i }));
expect(await screen.findByRole('button', { name: 'Sign out' })).toBeTruthy();
first.unmount();
const second = renderSettingsDialog(
{ mode: 'daemon', agentId: 'amr' },
{ agents: [amrAgent] },
);
fireEvent.click(screen.getByRole('tab', { name: /Local CLI.*1 installed/i }));
expect(await screen.findByRole('button', { name: 'Authorize' })).toBeTruthy();
expect(screen.queryByRole('button', { name: 'Sign out' })).toBeNull();
second.unmount();
});
it('keeps AMR selected in Settings after local logout instead of silently switching agents', async () => {
let statusCalls = 0;
const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
const url = input.toString();
if (url === '/api/memory') {
return new Response(
JSON.stringify({ enabled: true, memories: [], extraction: null }),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
if (url === '/api/integrations/vela/status') {
statusCalls += 1;
return new Response(
JSON.stringify({
loggedIn: statusCalls === 1,
profile: 'local',
user: statusCalls === 1 ? { id: 'user-1', email: 'signed-in@example.com' } : null,
configPath: '/Users/test/.amr/config.json',
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
}
if (url === '/api/integrations/vela/logout' && init?.method === 'POST') {
return new Response(JSON.stringify({ ok: true }), {
status: 200,
headers: { 'content-type': 'application/json' },
});
}
throw new Error(`Unexpected fetch: ${url}`);
});
vi.stubGlobal('fetch', fetchMock);
const { onPersist } = renderSettingsDialog(
{ mode: 'daemon', agentId: 'amr' },
{ agents: [amrAgent] },
);
fireEvent.click(screen.getByRole('tab', { name: /Local CLI.*1 installed/i }));
expect(await screen.findByRole('button', { name: 'Sign out' })).toBeTruthy();
expect(screen.getByRole('button', { name: /^Open Design\b/ })).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: 'Sign out' }));
expect(await screen.findByRole('button', { name: 'Authorize' })).toBeTruthy();
expect(screen.getByRole('button', { name: /^Open Design\b/ })).toBeTruthy();
expect(
onPersist.mock.calls.some(
([nextConfig]) =>
typeof nextConfig === 'object' &&
nextConfig !== null &&
'agentId' in (nextConfig as Record<string, unknown>) &&
(nextConfig as Record<string, unknown>).agentId !== 'amr',
),
).toBe(false);
});
});
describe('SettingsDialog media providers interactions', () => {
afterEach(() => {
cleanup();
});
it('sorts configured providers ahead of unconfigured ones and shows configured badges', () => {
renderSettingsDialog(
{
mode: 'daemon',
agentId: 'codex',
mediaProviders: {
openai: { apiKey: 'sk-media', baseUrl: 'https://custom.openai.example/v1' },
minimax: { apiKey: 'mini-key', baseUrl: 'https://api.minimaxi.chat/v1' },
},
},
{ initialSection: 'media' },
);
const names = Array.from(document.querySelectorAll('.media-provider-name')).map((node) =>
node.textContent?.trim(),
);
expect(names.slice(0, 2)).toEqual(['MiniMax', 'OpenAI']);
});
it('renders non-integrated providers in the coming-soon section without input fields', () => {
renderSettingsDialog(
{ mode: 'daemon', agentId: 'codex' },
{ initialSection: 'media' },
);
// Non-integrated providers (e.g. Fal.ai, Black Forest Labs) are shown in
// a separate "Coming soon" disclosure without editable inputs.
expect(screen.queryByLabelText('Black Forest Labs API key')).toBeNull();
expect(screen.queryByLabelText('Black Forest Labs Base URL')).toBeNull();
expect(document.querySelector('.media-provider-coming-soon')).toBeTruthy();
expect(screen.getByText('ComfyUI')).toBeTruthy();
});
it('renders ElevenLabs as an integrated media provider with enabled inputs', () => {
renderSettingsDialog(
{ mode: 'daemon', agentId: 'codex' },
{ initialSection: 'media' },
);
const apiKeyInput = screen.getByLabelText('ElevenLabs API key') as HTMLInputElement;
const baseUrlInput = screen.getByLabelText('ElevenLabs Base URL') as HTMLInputElement;
expect(apiKeyInput.disabled).toBe(false);
expect(baseUrlInput.disabled).toBe(false);
});
it('clears an existing provider config and removes it from the persisted payload', async () => {
const { onPersist } = renderSettingsDialog(
{
mode: 'daemon',
agentId: 'codex',
mediaProviders: {
openai: { apiKey: 'sk-media', baseUrl: 'https://custom.openai.example/v1' },
},
},
{ initialSection: 'media' },
);
// Issue #737 added a window.confirm guard on the Clear button so a
// stray click cannot wipe a saved API key. Auto-accept the prompt
// here so the test still exercises the cleared-payload path.
const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true);
const clearButtons = screen.getAllByRole('button', { name: 'Clear' });
fireEvent.click(clearButtons[0]!);
expect(confirmSpy).toHaveBeenCalledTimes(1);
expect((screen.getByLabelText('OpenAI API key') as HTMLInputElement).value).toBe('');
expect((screen.getByLabelText('OpenAI Base URL') as HTMLInputElement).value).toBe('');
await waitForPersist(
onPersist,
expect.objectContaining({
mediaProviders: {},
}),
{ forceMediaProviderSync: true },
);
confirmSpy.mockRestore();
});
it('cancels Clear when the confirmation is dismissed (issue #737)', () => {
const { onPersist } = renderSettingsDialog(
{
mode: 'daemon',
agentId: 'codex',
mediaProviders: {
openai: { apiKey: 'sk-media', baseUrl: 'https://custom.openai.example/v1' },
},
},
{ initialSection: 'media' },
);
const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(false);
const clearButtons = screen.getAllByRole('button', { name: 'Clear' });
fireEvent.click(clearButtons[0]!);
expect(confirmSpy).toHaveBeenCalledTimes(1);
// Saved key + base URL must stay intact when the user dismisses
// the confirmation; without this guard a fat-fingered click on
// Clear would silently wipe the key. Autosave should never fire
// because nothing changed.
expect((screen.getByLabelText('OpenAI API key') as HTMLInputElement).value).toBe('sk-media');
expect((screen.getByLabelText('OpenAI Base URL') as HTMLInputElement).value).toBe(
'https://custom.openai.example/v1',
);
expect(onPersist).not.toHaveBeenCalled();
confirmSpy.mockRestore();
});
it('supports persisting provider API key and base URL edits', async () => {
const { onPersist } = renderSettingsDialog(
{ mode: 'daemon', agentId: 'codex' },
{ initialSection: 'media' },
);
fireEvent.change(screen.getByLabelText('FishAudio API key'), {
target: { value: 'fish-key' },
});
fireEvent.change(screen.getByLabelText('FishAudio Base URL'), {
target: { value: 'https://fish.example.com' },
});
await waitForPersist(
onPersist,
expect.objectContaining({
mediaProviders: expect.objectContaining({
fishaudio: {
apiKey: 'fish-key',
baseUrl: 'https://fish.example.com',
model: '',
},
}),
}),
{ forceMediaProviderSync: true },
);
});
it('re-masks a replacement media provider API key until reveal is used again', () => {
renderSettingsDialog(
{
mode: 'daemon',
agentId: 'codex',
mediaProviders: {
openai: { apiKey: 'sk-media', baseUrl: 'https://api.openai.com/v1' },
},
},
{ initialSection: 'media' },
);
const apiKeyInput = screen.getByLabelText('OpenAI API key') as HTMLInputElement;
expect(apiKeyInput.type).toBe('password');
fireEvent.click(screen.getByRole('button', { name: 'OpenAI Show key' }));
expect(apiKeyInput.type).toBe('text');
// Issue #737 added a window.confirm guard on Clear; jsdom's
// unimplemented confirm() returns undefined, which would cancel
// the clear and leave this test asserting the wrong reveal state.
const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true);
fireEvent.click(screen.getAllByRole('button', { name: 'Clear' })[0]!);
expect(apiKeyInput.type).toBe('password');
fireEvent.change(apiKeyInput, { target: { value: 'sk-replacement' } });
expect(apiKeyInput.type).toBe('password');
fireEvent.click(screen.getByRole('button', { name: 'OpenAI Show key' }));
expect(apiKeyInput.type).toBe('text');
confirmSpy.mockRestore();
});
it('supports providers with a custom model override field', async () => {
const { onPersist } = renderSettingsDialog(
{ mode: 'daemon', agentId: 'codex' },
{ initialSection: 'media' },
);
fireEvent.change(screen.getByLabelText('Nano Banana API key'), {
target: { value: 'banana-key' },
});
fireEvent.change(screen.getByLabelText('Nano Banana Base URL'), {
target: { value: 'https://gateway.example.com' },
});
fireEvent.change(screen.getByLabelText('Nano Banana model'), {
target: { value: 'gemini-3.1-flash-image-preview' },
});
await waitForPersist(
onPersist,
expect.objectContaining({
mediaProviders: expect.objectContaining({
nanobanana: {
apiKey: 'banana-key',
baseUrl: 'https://gateway.example.com',
model: 'gemini-3.1-flash-image-preview',
},
}),
}),
{ forceMediaProviderSync: true },
);
});
it('catches unmount flush failures for pending media-provider autosaves', async () => {
const rejection = new Error('daemon unavailable');
const handleUnhandledRejection = vi.fn((event: PromiseRejectionEvent) => {
event.preventDefault();
});
window.addEventListener('unhandledrejection', handleUnhandledRejection);
try {
const { onPersist, unmount } = renderSettingsDialog(
{ mode: 'daemon', agentId: 'codex' },
{ initialSection: 'media' },
);
onPersist.mockRejectedValueOnce(rejection);
fireEvent.change(screen.getByLabelText('OpenAI API key'), {
target: { value: 'sk-unmount-media' },
});
await waitFor(() => {
expect(screen.getByText('Saving…')).toBeTruthy();
});
unmount();
await Promise.resolve();
await Promise.resolve();
expect(onPersist).toHaveBeenCalledWith(
expect.objectContaining({
mediaProviders: expect.objectContaining({
openai: expect.objectContaining({ apiKey: 'sk-unmount-media' }),
}),
}),
expect.objectContaining({ forceMediaProviderSync: true }),
);
expect(handleUnhandledRejection).not.toHaveBeenCalled();
} finally {
window.removeEventListener('unhandledrejection', handleUnhandledRejection);
}
});
it('closes media settings via the close button or backdrop', () => {
const first = renderSettingsDialog(
{ mode: 'daemon', agentId: 'codex' },
{ initialSection: 'media' },
);
fireEvent.change(screen.getByLabelText('OpenAI API key'), {
target: { value: 'sk-unsaved-media' },
});
fireEvent.click(first.container.querySelector('.settings-close') as HTMLElement);
expect(first.onClose).toHaveBeenCalledTimes(1);
cleanup();
const second = renderSettingsDialog(
{ mode: 'daemon', agentId: 'codex' },
{ initialSection: 'media' },
);
fireEvent.change(screen.getByLabelText('OpenAI API key'), {
target: { value: 'sk-unsaved-media-2' },
});
fireEvent.click(document.querySelector('.modal-backdrop') as HTMLElement);
expect(second.onClose).toHaveBeenCalledTimes(1);
});
});
describe('SettingsDialog connectors interactions', () => {
afterEach(() => {
cleanup();
});
it('renders a saved Composio key state with masked tail and replacement guidance', () => {
renderSettingsDialog(
{
mode: 'daemon',
agentId: 'codex',
composio: {
apiKey: '',
apiKeyConfigured: true,
apiKeyTail: 'uQEg',
},
},
{ initialSection: 'composio' },
);
expect(screen.getAllByRole('heading', { name: 'Connectors' }).length).toBeGreaterThan(0);
expect(screen.getByText('Saved · ••••uQEg')).toBeTruthy();
expect((screen.getByPlaceholderText('Paste a new key to replace the saved one') as HTMLInputElement).value).toBe('');
expect(screen.getByText(/your key is saved in the local daemon/i)).toBeTruthy();
expect((screen.getByRole('button', { name: 'Clear' }) as HTMLButtonElement).disabled).toBe(false);
const getApiKeyLink = screen.getByRole('link', { name: /Get API Key/i }) as HTMLAnchorElement;
expect(getApiKeyLink.href).toBe('https://app.composio.dev/');
});
it('supports replacing a saved Composio key and saving the pending edit', async () => {
const { onPersistComposioKey } = renderSettingsDialog(
{
mode: 'daemon',
agentId: 'codex',
composio: {
apiKey: '',
apiKeyConfigured: true,
apiKeyTail: 'uQEg',
},
},
{ initialSection: 'composio' },
);
fireEvent.change(screen.getByPlaceholderText('Paste a new key to replace the saved one'), {
target: { value: 'cmp_replacement_secret' },
});
fireEvent.click(screen.getByRole('button', { name: 'Save key' }));
await waitFor(() => {
expect(onPersistComposioKey).toHaveBeenCalledWith({
apiKey: 'cmp_replacement_secret',
apiKeyConfigured: true,
apiKeyTail: 'uQEg',
});
});
});
it('clears a saved Composio key from the payload', async () => {
const { onPersistComposioKey } = renderSettingsDialog(
{
mode: 'daemon',
agentId: 'codex',
composio: {
apiKey: '',
apiKeyConfigured: true,
apiKeyTail: 'uQEg',
},
},
{ initialSection: 'composio' },
);
fireEvent.click(screen.getByRole('button', { name: 'Clear' }));
fireEvent.click(screen.getByRole('button', { name: /continue/i }));
await waitFor(() => {
expect((screen.getByRole('button', { name: /hold on|disconnect/i }) as HTMLButtonElement).disabled).toBe(false);
});
fireEvent.click(screen.getByRole('button', { name: /hold on|disconnect/i }));
await waitFor(() => {
expect(onPersistComposioKey).toHaveBeenCalledWith({
apiKey: '',
apiKeyConfigured: false,
apiKeyTail: '',
});
});
expect(screen.getByText(/keys are stored locally and never shared/i)).toBeTruthy();
});
it('closes Composio settings via the close button or backdrop', () => {
const first = renderSettingsDialog(
{
mode: 'daemon',
agentId: 'codex',
composio: {
apiKey: '',
apiKeyConfigured: true,
apiKeyTail: 'uQEg',
},
},
{ initialSection: 'composio' },
);
fireEvent.change(screen.getByPlaceholderText('Paste a new key to replace the saved one'), {
target: { value: 'cmp_unsaved_secret' },
});
fireEvent.click(first.container.querySelector('.settings-close') as HTMLElement);
expect(first.onClose).toHaveBeenCalledTimes(1);
cleanup();
const second = renderSettingsDialog(
{
mode: 'daemon',
agentId: 'codex',
composio: {
apiKey: '',
apiKeyConfigured: true,
apiKeyTail: 'uQEg',
},
},
{ initialSection: 'composio' },
);
fireEvent.change(screen.getByPlaceholderText('Paste a new key to replace the saved one'), {
target: { value: 'cmp_unsaved_secret_2' },
});
fireEvent.click(document.querySelector('.modal-backdrop') as HTMLElement);
expect(second.onClose).toHaveBeenCalledTimes(1);
});
});
describe('SettingsDialog MCP server interactions', () => {
const installInfo = {
command: '/Applications/Open Design.app/Contents/Resources/open-design/bin/node',
args: [
'/Applications/Open Design.app/Contents/Resources/app/node_modules/@open-design/daemon/dist/cli.js',
'mcp',
'--daemon-url',
'http://127.0.0.1:51706',
],
daemonUrl: 'http://127.0.0.1:51706',
platform: 'darwin',
cliExists: true,
nodeExists: true,
buildHint: null,
};
let fetchMock: ReturnType<typeof vi.fn>;
let writeTextMock: ReturnType<typeof vi.fn>;
let originalClipboard: PropertyDescriptor | undefined;
beforeEach(() => {
fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: async () => installInfo,
});
vi.stubGlobal('fetch', fetchMock);
originalClipboard = Object.getOwnPropertyDescriptor(navigator, 'clipboard');
writeTextMock = vi.fn().mockResolvedValue(undefined);
Object.defineProperty(navigator, 'clipboard', {
configurable: true,
value: {
writeText: writeTextMock,
},
});
});
afterEach(() => {
cleanup();
vi.unstubAllGlobals();
if (originalClipboard) {
Object.defineProperty(navigator, 'clipboard', originalClipboard);
} else {
delete (navigator as { clipboard?: Clipboard }).clipboard;
}
vi.clearAllMocks();
});
it('renders the default Claude Code install snippet after fetching daemon install info', async () => {
renderSettingsDialog(
{ mode: 'daemon', agentId: 'codex' },
{ initialSection: 'integrations' },
);
await waitFor(() => {
expect(fetchMock).toHaveBeenCalledWith('/api/mcp/install-info');
});
expect(screen.getByText(/Run this in your terminal/i)).toBeTruthy();
await waitFor(() => {
expect(screen.getByText(/claude mcp add-json --scope user open-design/i)).toBeTruthy();
});
expect(screen.getByText(/Restart your client to pick up the new server/i)).toBeTruthy();
expect(screen.getByText(/Open Design must be running for MCP tool calls to succeed/i)).toBeTruthy();
});
it('switches client instructions and snippet content when a different MCP client is selected', async () => {
renderSettingsDialog(
{ mode: 'daemon', agentId: 'codex' },
{ initialSection: 'integrations' },
);
await waitFor(() => {
expect(screen.getByText(/claude mcp add-json --scope user open-design/i)).toBeTruthy();
});
fireEvent.click(screen.getByRole('button', { name: /Claude Code/i }));
fireEvent.click(screen.getByRole('option', { name: /Codex/i }));
await waitFor(() => {
expect(screen.getByText(/Append this table to ~\/\.codex\/config\.toml/i)).toBeTruthy();
});
expect(screen.getByText(/\[mcp_servers\.open-design\]/i)).toBeTruthy();
// Scope to the picker trigger ("Codex" + the TOML method chip) so
// we don't collide with the new one-click "Install in Codex" /
// "Remove from Codex" button on the same panel.
fireEvent.click(screen.getByRole('button', { name: /Codex.*TOML/i }));
fireEvent.click(screen.getByRole('option', { name: /Cursor/i }));
await waitFor(() => {
expect(screen.getByRole('button', { name: /Install in Cursor/i })).toBeTruthy();
});
expect(screen.getByText(/merge this JSON into ~\/\.cursor\/mcp\.json/i)).toBeTruthy();
expect(screen.getByText(/"mcpServers"/i)).toBeTruthy();
});
it('copies the currently selected MCP snippet to the clipboard', async () => {
renderSettingsDialog(
{ mode: 'daemon', agentId: 'codex' },
{ initialSection: 'integrations' },
);
await waitFor(() => {
expect(screen.getByText(/claude mcp add-json --scope user open-design/i)).toBeTruthy();
});
fireEvent.click(screen.getByRole('button', { name: 'Copy MCP configuration snippet' }));
await waitFor(() => {
expect(writeTextMock).toHaveBeenCalledWith(
expect.stringContaining("claude mcp add-json --scope user open-design"),
);
});
expect(screen.getByText('Copied')).toBeTruthy();
});
it('shows a daemon error state when install paths cannot be resolved', async () => {
fetchMock.mockRejectedValueOnce(new Error('network down'));
renderSettingsDialog(
{ mode: 'daemon', agentId: 'codex' },
{ initialSection: 'integrations' },
);
await waitFor(() => {
const errorCard = document.querySelector('.empty-card');
expect(errorCard?.textContent).toContain('reach the local daemon to resolve install paths');
});
expect(screen.getByText(/# resolving paths failed, see the error above/i)).toBeTruthy();
});
});
describe('SettingsDialog language interactions', () => {
afterEach(() => {
cleanup();
window.localStorage.removeItem('open-design:locale');
document.documentElement.removeAttribute('lang');
document.documentElement.removeAttribute('dir');
});
it('shows every locale as a tile and marks the current locale as selected', async () => {
renderLanguageSettingsDialog('en');
const tiles = await screen.findAllByRole('radio');
expect(tiles).toHaveLength(LOCALES.length);
expect(screen.getByRole('radio', { name: /English/i }).getAttribute('aria-checked')).toBe('true');
expect(screen.getByRole('radio', { name: /简体中文/i }).getAttribute('aria-checked')).toBe('false');
});
it('switches locale immediately and updates localStorage', async () => {
renderLanguageSettingsDialog('en');
fireEvent.click(screen.getByRole('radio', { name: /简体中文/i }));
expect(screen.getByRole('radio', { name: /简体中文/i }).getAttribute('aria-checked')).toBe('true');
expect(window.localStorage.getItem('open-design:locale')).toBe('zh-CN');
expect(document.documentElement.getAttribute('lang')).toBe('zh-CN');
expect(document.documentElement.getAttribute('dir')).toBe('ltr');
});
it('sets rtl direction for rtl locales', async () => {
renderLanguageSettingsDialog('en');
fireEvent.click(screen.getByRole('radio', { name: /فارسی/i }));
expect(window.localStorage.getItem('open-design:locale')).toBe('fa');
expect(document.documentElement.getAttribute('lang')).toBe('fa');
expect(document.documentElement.getAttribute('dir')).toBe('rtl');
});
it('does not route language changes through autosave and closing does not revert an applied locale', async () => {
const { onPersist, onClose } = renderLanguageSettingsDialog('en');
fireEvent.click(screen.getByRole('radio', { name: /Deutsch/i }));
expect(window.localStorage.getItem('open-design:locale')).toBe('de');
expect(document.documentElement.getAttribute('lang')).toBe('de');
fireEvent.click(screen.getByTitle(/close|schließen/i));
expect(onPersist).not.toHaveBeenCalled();
expect(onClose).toHaveBeenCalledTimes(1);
expect(window.localStorage.getItem('open-design:locale')).toBe('de');
expect(document.documentElement.getAttribute('lang')).toBe('de');
expect(document.documentElement.getAttribute('dir')).toBe('ltr');
});
});
describe('SettingsDialog notifications interactions', () => {
afterEach(() => {
cleanup();
});
it('renders notifications offline by default and only reveals sound pickers when enabled', () => {
renderSettingsDialog(
{ mode: 'daemon', agentId: 'codex' },
{ initialSection: 'notifications' },
);
expect(screen.getByRole('group', { name: 'Completion sound' })).toBeTruthy();
expect(screen.getAllByRole('button', { name: 'offline' })[0]?.getAttribute('aria-pressed')).toBe('false');
expect(screen.queryByRole('group', { name: 'Success sound' })).toBeNull();
expect(screen.queryByRole('group', { name: 'Failure sound' })).toBeNull();
fireEvent.click(screen.getAllByRole('button', { name: 'offline' })[0] as HTMLButtonElement);
expect(playSoundMock).toHaveBeenCalledWith('ding');
expect(screen.getByRole('group', { name: 'Success sound' })).toBeTruthy();
expect(screen.getByRole('group', { name: 'Failure sound' })).toBeTruthy();
});
it('updates completion success and failure sounds and autosaves the edited notification config', async () => {
const { onPersist } = renderSettingsDialog(
{
mode: 'daemon',
agentId: 'codex',
notifications: {
soundEnabled: true,
successSoundId: 'chime',
failureSoundId: 'two-tone-down',
desktopEnabled: false,
},
},
{ initialSection: 'notifications' },
);
fireEvent.click(screen.getByRole('button', { name: 'Pluck' }));
fireEvent.click(screen.getByRole('button', { name: 'Thud' }));
expect(playSoundMock).toHaveBeenNthCalledWith(1, 'pluck');
expect(playSoundMock).toHaveBeenNthCalledWith(2, 'thud');
await waitForPersist(
onPersist,
expect.objectContaining({
notifications: {
soundEnabled: true,
successSoundId: 'pluck',
failureSoundId: 'thud',
desktopEnabled: false,
},
}),
{},
);
});
it('enables desktop notifications after permission is granted and sends a test notification', async () => {
notificationPermissionMock.mockReturnValueOnce('default').mockReturnValue('granted');
requestNotificationPermissionMock.mockResolvedValue('granted');
showCompletionNotificationMock.mockResolvedValue('shown');
renderSettingsDialog(
{ mode: 'daemon', agentId: 'codex' },
{ initialSection: 'notifications' },
);
const desktopToggle = screen.getAllByRole('button', { name: 'offline' })[1] as HTMLButtonElement;
fireEvent.click(desktopToggle);
await waitFor(() => {
expect(requestNotificationPermissionMock).toHaveBeenCalledTimes(1);
});
expect(screen.getByRole('button', { name: 'active' }).getAttribute('aria-pressed')).toBe('true');
fireEvent.click(screen.getByRole('button', { name: 'Send test' }));
await waitFor(() => {
expect(showCompletionNotificationMock).toHaveBeenCalledWith(
expect.objectContaining({ status: 'succeeded' }),
);
});
expect(screen.getByText(/Test notification sent/i)).toBeTruthy();
});
it('shows a blocked hint and keeps desktop notifications disabled when permission is denied', async () => {
notificationPermissionMock.mockReturnValueOnce('default').mockReturnValue('denied');
requestNotificationPermissionMock.mockResolvedValue('denied');
renderSettingsDialog(
{ mode: 'daemon', agentId: 'codex' },
{ initialSection: 'notifications' },
);
const desktopToggle = screen.getAllByRole('button', { name: 'offline' })[1] as HTMLButtonElement;
fireEvent.click(desktopToggle);
await waitFor(() => {
expect(requestNotificationPermissionMock).toHaveBeenCalledTimes(1);
});
expect(screen.getByText(/Notifications blocked by the browser/i)).toBeTruthy();
expect(screen.queryByRole('button', { name: 'Send test' })).toBeNull();
});
it('closes notification settings via the close button or backdrop', () => {
const first = renderSettingsDialog(
{ mode: 'daemon', agentId: 'codex' },
{ initialSection: 'notifications' },
);
fireEvent.click(screen.getAllByRole('button', { name: 'offline' })[0] as HTMLButtonElement);
fireEvent.click(first.container.querySelector('.settings-close') as HTMLElement);
expect(first.onClose).toHaveBeenCalledTimes(1);
cleanup();
const second = renderSettingsDialog(
{ mode: 'daemon', agentId: 'codex' },
{ initialSection: 'notifications' },
);
fireEvent.click(screen.getAllByRole('button', { name: 'offline' })[0] as HTMLButtonElement);
fireEvent.click(document.querySelector('.modal-backdrop') as HTMLElement);
expect(second.onClose).toHaveBeenCalledTimes(1);
});
});
describe('SettingsDialog appearance interactions', () => {
afterEach(() => {
cleanup();
document.documentElement.removeAttribute('data-theme');
document.documentElement.style.removeProperty('--accent');
document.documentElement.style.removeProperty('--accent-strong');
document.documentElement.style.removeProperty('--accent-soft');
document.documentElement.style.removeProperty('--accent-tint');
document.documentElement.style.removeProperty('--accent-hover');
});
it('treats System as the selected appearance mode when theme is unset or system', () => {
renderSettingsDialog(
{ theme: 'system' },
{ initialSection: 'appearance' },
);
expect(screen.getByRole('button', { name: 'System' }).getAttribute('aria-pressed')).toBe('true');
expect(screen.getByRole('button', { name: 'Light' }).getAttribute('aria-pressed')).toBe('false');
expect(screen.getByRole('button', { name: 'Dark' }).getAttribute('aria-pressed')).toBe('false');
});
it('applies the first accent color as the default appearance color', () => {
renderSettingsDialog(
{ theme: 'system' },
{ initialSection: 'appearance' },
);
expect(screen.getByRole('radio', { name: 'Default accent color' }).getAttribute('aria-checked')).toBe('true');
expect(document.documentElement.style.getPropertyValue('--accent')).toBe('#c96442');
});
it('live previews explicit themes and removes the explicit document theme when switching back to System', () => {
renderSettingsDialog(
{ theme: 'dark' },
{ initialSection: 'appearance' },
);
expect(document.documentElement.getAttribute('data-theme')).toBe('dark');
fireEvent.click(screen.getByRole('button', { name: 'Light' }));
expect(document.documentElement.getAttribute('data-theme')).toBe('light');
fireEvent.click(screen.getByRole('button', { name: 'System' }));
expect(document.documentElement.hasAttribute('data-theme')).toBe(false);
});
it('reverts an unsaved appearance preview back to the saved theme when the dialog closes', () => {
const first = renderSettingsDialog(
{ theme: 'dark' },
{ initialSection: 'appearance' },
);
expect(document.documentElement.getAttribute('data-theme')).toBe('dark');
fireEvent.click(screen.getByRole('button', { name: 'Light' }));
expect(document.documentElement.getAttribute('data-theme')).toBe('light');
fireEvent.click(first.container.querySelector('.settings-close') as HTMLElement);
expect(first.onClose).toHaveBeenCalledTimes(1);
first.unmount();
expect(document.documentElement.getAttribute('data-theme')).toBe('dark');
});
it('persists System mode explicitly and preserves accent variables without an explicit document theme', async () => {
const { onPersist } = renderSettingsDialog(
{ mode: 'daemon', agentId: 'codex', theme: 'dark', accentColor: '#2563eb' },
{ initialSection: 'appearance' },
);
fireEvent.click(screen.getByRole('button', { name: 'System' }));
expect(document.documentElement.hasAttribute('data-theme')).toBe(false);
expect(document.documentElement.style.getPropertyValue('--accent')).toBe('#2563eb');
await waitForPersist(
onPersist,
expect.objectContaining({
theme: 'system',
accentColor: '#2563eb',
}),
{},
);
});
it('reconciles the open settings draft when the parent agent CLI env changes', async () => {
const view = renderSettingsDialog(
{
mode: 'daemon',
agentId: 'amr',
theme: 'dark',
agentModels: {
amr: {
model: 'prod-only-model',
reasoning: 'default',
},
},
agentCliEnv: {
codex: { CODEX_BIN: '/tmp/codex-dev' },
amr: {
OPEN_DESIGN_AMR_PROFILE: 'prod',
AMR_API_BASE_URL: 'https://draft.example.test',
},
},
},
{ initialSection: 'appearance', agents: [amrAgent, ...availableAgents] },
);
view.rerender(
<SettingsDialog
initial={{
...baseConfig,
mode: 'daemon',
agentId: 'amr',
theme: 'dark',
agentCliEnv: {
amr: {
OPEN_DESIGN_AMR_PROFILE: 'local',
AMR_API_BASE_URL: 'https://daemon.example.test',
},
},
}}
agents={[amrAgent, ...availableAgents]}
daemonLive={true}
appVersionInfo={null}
initialSection="appearance"
onPersist={view.onPersist}
onPersistComposioKey={view.onPersistComposioKey}
onClose={view.onClose}
onRefreshAgents={view.onRefreshAgents}
/>,
);
fireEvent.click(screen.getByRole('button', { name: 'Light' }));
await waitForPersist(
view.onPersist,
expect.objectContaining({
theme: 'light',
agentModels: {},
agentCliEnv: {
codex: { CODEX_BIN: '/tmp/codex-dev' },
amr: {
OPEN_DESIGN_AMR_PROFILE: 'local',
AMR_API_BASE_URL: 'https://draft.example.test',
},
},
}),
{},
);
});
it('clears a stale AMR draft model when the external profile changes and the draft still matches the previous config', () => {
expect(
reconcileAmrModelChoice(
{
amr: {
model: 'prod-only-model',
reasoning: 'default',
},
},
{
...baseConfig,
agentModels: {
amr: {
model: 'prod-only-model',
reasoning: 'default',
},
},
agentCliEnv: {
amr: {
OPEN_DESIGN_AMR_PROFILE: 'prod',
},
},
},
{
...baseConfig,
agentModels: {},
agentCliEnv: {
amr: {
OPEN_DESIGN_AMR_PROFILE: 'local',
},
},
},
),
).toEqual({});
});
it('preserves unrelated draft env entries when reconciling the AMR profile', () => {
expect(
reconcileAmrProfileEnv(
{
codex: { CODEX_BIN: '/tmp/codex-dev' },
amr: {
OPEN_DESIGN_AMR_PROFILE: 'prod',
AMR_API_BASE_URL: 'https://draft.example.test',
},
},
{
amr: {
OPEN_DESIGN_AMR_PROFILE: 'local',
AMR_API_BASE_URL: 'https://daemon.example.test',
},
},
),
).toEqual({
codex: { CODEX_BIN: '/tmp/codex-dev' },
amr: {
OPEN_DESIGN_AMR_PROFILE: 'local',
AMR_API_BASE_URL: 'https://draft.example.test',
},
});
});
it('switches back to the default accent color and persists it explicitly', async () => {
const { onPersist } = renderSettingsDialog(
{ mode: 'daemon', agentId: 'codex', theme: 'light', accentColor: '#2563eb' },
{ initialSection: 'appearance' },
);
fireEvent.click(screen.getByRole('radio', { name: 'Default accent color' }));
expect(document.documentElement.style.getPropertyValue('--accent')).toBe('#c96442');
await waitForPersist(
onPersist,
expect.objectContaining({
accentColor: '#c96442',
}),
{},
);
});
it('keeps an autosaved accent color applied after the dialog closes', async () => {
const view = renderSettingsDialog(
{ mode: 'daemon', agentId: 'codex', theme: 'light', accentColor: '#2563eb' },
{ initialSection: 'appearance' },
);
fireEvent.click(screen.getByRole('radio', { name: '#059669' }));
await waitForPersist(
view.onPersist,
expect.objectContaining({
accentColor: '#059669',
}),
{},
);
fireEvent.click(view.container.querySelector('.settings-close') as HTMLElement);
expect(view.onClose).toHaveBeenCalledTimes(1);
view.unmount();
expect(document.documentElement.style.getPropertyValue('--accent')).toBe('#059669');
});
it('live previews and autosaves preset and custom accent colors', async () => {
const { onPersist } = renderSettingsDialog(
{ mode: 'daemon', agentId: 'codex', theme: 'light' },
{ initialSection: 'appearance' },
);
fireEvent.click(screen.getByRole('radio', { name: '#059669' }));
expect(document.documentElement.style.getPropertyValue('--accent')).toBe('#059669');
await waitForPersist(
onPersist,
expect.objectContaining({
accentColor: '#059669',
}),
{},
);
fireEvent.change(screen.getByLabelText('Custom color'), {
target: { value: '#123456' },
});
expect(document.documentElement.style.getPropertyValue('--accent')).toBe('#123456');
await waitForPersist(
onPersist,
expect.objectContaining({
accentColor: '#123456',
}),
{},
);
});
it('localizes the accent color controls in Chinese', () => {
render(
<I18nProvider initial="zh-CN">
<SettingsDialog
initial={{ ...baseConfig, theme: 'light' }}
agents={availableAgents}
daemonLive={true}
appVersionInfo={null}
initialSection="appearance"
onPersist={vi.fn()}
onPersistComposioKey={vi.fn()}
onClose={vi.fn()}
onRefreshAgents={vi.fn()}
/>
</I18nProvider>,
);
expect(screen.getByText('主题色')).toBeTruthy();
expect(screen.getByRole('radiogroup', { name: '主题色' })).toBeTruthy();
expect(screen.getByRole('radio', { name: '默认主题色' })).toBeTruthy();
expect(screen.getByLabelText('自定义颜色')).toBeTruthy();
});
});
describe('SettingsDialog pets interactions', () => {
const clipboardDescriptor = Object.getOwnPropertyDescriptor(window.navigator, 'clipboard');
afterEach(() => {
if (clipboardDescriptor) {
Object.defineProperty(window.navigator, 'clipboard', clipboardDescriptor);
} else {
Reflect.deleteProperty(window.navigator, 'clipboard');
}
cleanup();
});
it('renders bundled pets by default and exposes community pets in a separate tab', async () => {
fetchCodexPetsMock.mockResolvedValue({
pets: [...sampleBundledPets, ...sampleCommunityPets],
rootDir: '/Users/test/.codex/pets',
});
renderSettingsDialog(
{ mode: 'daemon', agentId: 'codex' },
{ initialSection: 'pet' },
);
expect((screen.getByRole('button', { name: 'Show pet' }) as HTMLButtonElement).disabled).toBe(false);
await waitFor(() => {
expect(screen.getByText('Dario')).toBeTruthy();
expect(screen.getByText('Nyako')).toBeTruthy();
});
expect(screen.queryByText('Jade')).toBeNull();
fireEvent.click(screen.getByRole('tab', { name: 'Community' }));
expect(screen.getByText('Recently hatched')).toBeTruthy();
expect(screen.getByRole('button', { name: 'Download community pets' })).toBeTruthy();
expect(screen.getByRole('button', { name: 'Refresh' })).toBeTruthy();
expect(screen.getByText('Jade')).toBeTruthy();
expect(screen.getByText('Voidling')).toBeTruthy();
});
it('supports editing and persisting a custom pet', async () => {
const { onPersist } = renderSettingsDialog(
{ mode: 'daemon', agentId: 'codex' },
{ initialSection: 'pet' },
);
fireEvent.click(screen.getByRole('tab', { name: 'Custom' }));
fireEvent.change(screen.getByDisplayValue('Buddy'), {
target: { value: 'Scout' },
});
fireEvent.change(screen.getByDisplayValue('🦄'), {
target: { value: '🤖' },
});
fireEvent.change(screen.getByDisplayValue('Hi! I am here whenever you need me.'), {
target: { value: 'Hi there, builder.' },
});
fireEvent.click(document.querySelector('.pet-swatch[title="#2348b8"]') as HTMLElement);
expect(screen.getAllByText('Scout').length).toBeGreaterThan(0);
expect(screen.getByText('Hi there, builder.')).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: 'Use my pet' }));
await waitForPersist(
onPersist,
expect.objectContaining({
pet: expect.objectContaining({
adopted: true,
enabled: true,
petId: 'custom',
custom: expect.objectContaining({
name: 'Scout',
glyph: '🤖',
greeting: 'Hi there, builder.',
accent: '#2348b8',
}),
}),
}),
{},
);
});
it('toggles an adopted pet between tucked and awake states', async () => {
const { onPersist } = renderSettingsDialog(
{
mode: 'daemon',
agentId: 'codex',
pet: {
adopted: true,
enabled: true,
petId: 'custom',
custom: {
name: 'Buddy',
glyph: '🦄',
accent: '#c96442',
greeting: 'Hi! I am here whenever you need me.',
},
},
},
{ initialSection: 'pet' },
);
const toggle = screen.getByRole('button', { name: 'Hide pet' });
fireEvent.click(toggle);
expect(screen.getByRole('button', { name: 'Show pet' })).toBeTruthy();
expect(screen.getByText('Hide pet')).toBeTruthy();
await waitForPersist(
onPersist,
expect.objectContaining({
pet: expect.objectContaining({
adopted: true,
enabled: false,
}),
}),
{},
);
});
it('refreshes and syncs community pets with inline status feedback', async () => {
fetchCodexPetsMock.mockResolvedValue({
pets: sampleCommunityPets,
rootDir: '/Users/test/.codex/pets',
});
syncCommunityPetsMock.mockResolvedValue({
wrote: 2,
skipped: 1,
failed: 0,
total: 5,
rootDir: '/Users/test/.codex/pets',
errors: [],
});
renderSettingsDialog(
{ mode: 'daemon', agentId: 'codex' },
{ initialSection: 'pet' },
);
fireEvent.click(screen.getByRole('tab', { name: 'Community' }));
await waitFor(() => {
expect(fetchCodexPetsMock).toHaveBeenCalledTimes(1);
});
fireEvent.click(screen.getByRole('button', { name: 'Refresh' }));
await waitFor(() => {
expect(fetchCodexPetsMock).toHaveBeenCalledTimes(2);
});
fireEvent.click(screen.getByRole('button', { name: 'Download community pets' }));
await waitFor(() => {
expect(syncCommunityPetsMock).toHaveBeenCalledTimes(1);
expect(fetchCodexPetsMock).toHaveBeenCalledTimes(3);
expect(screen.getByText('Synced 2 new pets (5 total).')).toBeTruthy();
});
});
it('copies the hatch prompt with the current concept', async () => {
const writeText = vi.fn().mockResolvedValue(undefined);
Object.defineProperty(window.navigator, 'clipboard', {
configurable: true,
value: { writeText },
});
renderSettingsDialog(
{ mode: 'daemon', agentId: 'codex' },
{ initialSection: 'pet' },
);
fireEvent.click(screen.getByRole('tab', { name: 'Community' }));
fireEvent.change(screen.getByLabelText('Pet concept (optional)'), {
target: { value: 'a tiny pixel-art bee in a cozy sweater' },
});
fireEvent.click(screen.getByRole('button', { name: 'Copy prompt' }));
await waitFor(() => {
expect(writeText).toHaveBeenCalledWith(
expect.stringContaining('Concept: a tiny pixel-art bee in a cozy sweater.'),
);
expect(writeText).toHaveBeenCalledWith(
expect.stringContaining('Use the @hatch-pet skill end-to-end:'),
);
expect(screen.getByRole('button', { name: 'Copied!' })).toBeTruthy();
});
});
});
describe('IntegrationsView skills tab', () => {
afterEach(() => {
cleanup();
});
it('lists functional skills and filters them by mode + search', async () => {
renderIntegrationsView(
{ mode: 'daemon', agentId: 'codex' },
{ initialTab: 'skills' },
);
await waitFor(() => {
expect(screen.getByText('blog-post')).toBeTruthy();
expect(screen.getByText('sales-deck')).toBeTruthy();
});
fireEvent.change(screen.getByRole('combobox', { name: 'Type' }), {
target: { value: 'deck' },
});
expect(screen.queryByText('blog-post')).toBeNull();
expect(screen.getByText('sales-deck')).toBeTruthy();
fireEvent.change(screen.getByPlaceholderText('Search...'), {
target: { value: 'sales' },
});
expect(screen.getByText('sales-deck')).toBeTruthy();
expect(screen.queryByText('dashboard')).toBeNull();
});
it('opens a skill detail panel and persists disabled skills from toggle switches', async () => {
const { onConfigPersist } = renderIntegrationsView(
{ mode: 'daemon', agentId: 'codex' },
{ initialTab: 'skills' },
);
await waitFor(() => {
expect(screen.getByText('blog-post')).toBeTruthy();
});
fireEvent.click(screen.getByText('blog-post'));
await waitFor(() => {
expect(fetchSkillMock).toHaveBeenCalledWith('blog-post');
expect(screen.getByText('skill body for blog-post')).toBeTruthy();
});
const toggles = screen.getAllByTitle('Toggle');
fireEvent.click(toggles[0] as HTMLElement);
await waitFor(() => {
expect(onConfigPersist).toHaveBeenCalledWith(
expect.objectContaining({
disabledSkills: ['blog-post'],
}),
);
});
});
it('shows an empty state when search matches nothing', async () => {
renderIntegrationsView(
{ mode: 'daemon', agentId: 'codex' },
{ initialTab: 'skills' },
);
await waitFor(() => {
expect(screen.getByText('blog-post')).toBeTruthy();
});
fireEvent.change(screen.getByPlaceholderText('Search...'), {
target: { value: 'zzz-no-match' },
});
expect(screen.getByText('No items match your search.')).toBeTruthy();
});
});
describe('SettingsDialog design systems section', () => {
afterEach(() => {
cleanup();
});
it('lists design systems and persists disabled selections from toggle switches', async () => {
const { onPersist } = renderSettingsDialog(
{ mode: 'daemon', agentId: 'codex' },
{ initialSection: 'designSystems' },
);
await waitFor(() => {
expect(screen.getByText('Neutral Modern')).toBeTruthy();
expect(screen.getByText('Signal Green')).toBeTruthy();
});
fireEvent.change(screen.getByLabelText('Category'), {
target: { value: 'Experimental' },
});
expect(screen.queryByText('Neutral Modern')).toBeNull();
expect(screen.getByText('Signal Green')).toBeTruthy();
fireEvent.click(screen.getByText('Signal Green'));
await waitFor(() => {
expect(fetchDesignSystemMock).toHaveBeenCalledWith('signal-green');
expect(screen.getByText('design system body for signal-green')).toBeTruthy();
});
fireEvent.click(screen.getAllByLabelText('Show in home gallery')[0] as HTMLElement);
await waitForPersist(
onPersist,
expect.objectContaining({
disabledDesignSystems: ['signal-green'],
}),
{},
);
});
it('shows an imported design system from the hidden-only import CTA', async () => {
renderSettingsDialog(
{
mode: 'daemon',
agentId: 'codex',
disabledDesignSystems: ['neutral-modern'],
},
{ initialSection: 'designSystems' },
);
await waitFor(() => {
expect(screen.getByText('Neutral Modern')).toBeTruthy();
expect(screen.getByText('Signal Green')).toBeTruthy();
});
fireEvent.click(screen.getByRole('button', { name: 'Show hidden' }));
expect(screen.getByText('Neutral Modern')).toBeTruthy();
expect(screen.queryByText('Signal Green')).toBeNull();
fireEvent.click(screen.getByRole('button', { name: 'Add design system' }));
fireEvent.change(screen.getByPlaceholderText('/path/to/project'), {
target: { value: '/tmp/imported-system' },
});
fireEvent.click(screen.getByRole('button', { name: 'Import from project' }));
await waitFor(() => {
expect(importLocalDesignSystemMock).toHaveBeenCalledWith({
baseDir: '/tmp/imported-system',
importMode: 'hybrid',
craftApplies: [],
});
expect(screen.getByText('Imported Imported System')).toBeTruthy();
});
fireEvent.click(screen.getByRole('button', { name: 'View imported design system' }));
await waitFor(() => {
expect(screen.getByText('Imported System')).toBeTruthy();
});
expect(screen.queryByText('No items match your search.')).toBeNull();
});
});
describe('SettingsDialog about interactions', () => {
afterEach(() => {
cleanup();
});
it('renders app version and runtime details when version info is available', () => {
renderSettingsDialog(
{ mode: 'daemon', agentId: 'codex' },
{
initialSection: 'about',
appVersionInfo: {
version: '0.4.1',
channel: 'beta',
packaged: true,
platform: 'darwin',
arch: 'arm64',
},
},
);
expect(screen.getByText('Version')).toBeTruthy();
expect(screen.getByText('0.4.1')).toBeTruthy();
expect(screen.getByText('Channel')).toBeTruthy();
expect(screen.getByText('beta')).toBeTruthy();
expect(screen.getByText('Runtime')).toBeTruthy();
expect(screen.getByText('Packaged app')).toBeTruthy();
expect(screen.getByText('Platform')).toBeTruthy();
expect(screen.getByText('darwin')).toBeTruthy();
expect(screen.getByText('Architecture')).toBeTruthy();
expect(screen.getByText('arm64')).toBeTruthy();
});
it('renders the unavailable fallback when app version info is missing', () => {
renderSettingsDialog(
{ mode: 'daemon', agentId: 'codex' },
{ initialSection: 'about', appVersionInfo: null },
);
expect(
screen.getByText(/Version details are unavailable while the daemon is offline\./i),
).toBeTruthy();
});
it('does not create dirty state on the about page', () => {
const first = renderSettingsDialog(
{ mode: 'daemon', agentId: 'codex' },
{
initialSection: 'about',
appVersionInfo: {
version: '0.4.1',
channel: 'beta',
packaged: false,
platform: 'linux',
arch: 'x64',
},
},
);
fireEvent.click(first.container.querySelector('.settings-close') as HTMLElement);
expect(first.onClose).toHaveBeenCalledTimes(1);
cleanup();
const second = renderSettingsDialog(
{ mode: 'daemon', agentId: 'codex' },
{
initialSection: 'about',
appVersionInfo: {
version: '0.4.1',
channel: 'beta',
packaged: false,
platform: 'linux',
arch: 'x64',
},
},
);
fireEvent.click(document.querySelector('.modal-backdrop') as HTMLElement);
expect(second.onClose).toHaveBeenCalledTimes(1);
});
it('shows development builds as unsupported for in-app updates', () => {
renderSettingsDialog(
{ mode: 'daemon', agentId: 'codex' },
{
initialSection: 'about',
appVersionInfo: {
version: '0.4.1',
channel: 'beta',
packaged: false,
platform: 'darwin',
arch: 'arm64',
},
},
);
expect(screen.getByText(en['settings.updateStatusDevelopment'])).toBeTruthy();
expect(screen.queryByRole('button', { name: en['settings.installLatest'] })).toBeNull();
expect(screen.queryByRole('combobox')).toBeNull();
fireEvent.click(screen.getByRole('button', { name: en['settings.updateViewReleases'] }));
expect(openExternalUrlMock).toHaveBeenCalledWith('https://github.com/nexu-io/open-design/releases');
});
it('downloads an available packaged update from the about page', async () => {
const available = updateStatus({
availableVersion: '1.2.3-beta.4',
state: 'available',
});
const downloaded = updateStatus({
artifact: {
name: 'Open Design Beta.dmg',
platformKey: 'macAppleSilicon',
type: 'dmg',
url: 'https://fixture.test/Open Design Beta.dmg',
},
availableVersion: '1.2.3-beta.4',
downloadPath: '/tmp/open-design-updater/Open Design Beta.dmg',
state: 'downloaded',
});
const download = vi.fn(async () => downloaded);
restoreOpenDesignHost = installMockOpenDesignHost({
host: {
updater: {
download,
status: vi.fn(async () => available),
},
},
});
renderSettingsDialog(
{ mode: 'daemon', agentId: 'codex' },
{
initialSection: 'about',
appVersionInfo: {
version: '1.2.3-beta.3',
channel: 'beta',
packaged: true,
platform: 'darwin',
arch: 'arm64',
},
},
);
expect(
await screen.findByText('New version 1.2.3-beta.4 found. Preparing download.'),
).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: en['updater.download'] }));
await waitFor(() => {
expect(download).toHaveBeenCalledWith({ payload: { source: 'settings-about' } });
});
expect(screen.getByText('Version 1.2.3-beta.4 is ready to install.')).toBeTruthy();
});
it('installs a downloaded payload update from the about page', async () => {
const payloadReady = updateStatus({
artifact: {
name: 'open-design-1.2.3-beta.4-mac-arm64-payload.zip',
platformKey: 'mac',
type: 'payload',
url: 'https://fixture.test/open-design-1.2.3-beta.4-mac-arm64-payload.zip',
},
availableVersion: '1.2.3-beta.4',
capabilities: {
canApplyInPlace: true,
canDownload: true,
canOpenInstaller: false,
requiresManualInstall: false,
},
downloadPath: '/tmp/open-design-updater/open-design-1.2.3-beta.4-mac-arm64-payload.zip',
state: 'downloaded',
});
const installing = updateStatus({
...payloadReady,
state: 'installing',
});
const install = vi.fn(async () => installing);
const quit = vi.fn(async () => ({ ok: true as const }));
restoreOpenDesignHost = installMockOpenDesignHost({
host: {
updater: {
install,
quit,
status: vi.fn(async () => payloadReady),
},
},
});
renderSettingsDialog(
{ mode: 'daemon', agentId: 'codex' },
{
initialSection: 'about',
appVersionInfo: {
version: '1.2.3-beta.3',
channel: 'beta',
packaged: true,
platform: 'darwin',
arch: 'arm64',
},
},
);
expect(await screen.findByText('Version 1.2.3-beta.4 is ready to install.')).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: en['updater.installRestart'] }));
await waitFor(() => {
expect(install).toHaveBeenCalledWith({ payload: { source: 'settings-about' } });
});
await waitFor(() => {
expect(quit).toHaveBeenCalledWith({ payload: { source: 'settings-about' } });
});
});
it('keeps a quit retry action when update install succeeds but quit fails', async () => {
const payloadReady = updateStatus({
artifact: {
name: 'open-design-1.2.3-beta.4-mac-arm64-payload.zip',
platformKey: 'mac',
type: 'payload',
url: 'https://fixture.test/open-design-1.2.3-beta.4-mac-arm64-payload.zip',
},
availableVersion: '1.2.3-beta.4',
capabilities: {
canApplyInPlace: true,
canDownload: true,
canOpenInstaller: false,
requiresManualInstall: false,
},
downloadPath: '/tmp/open-design-updater/open-design-1.2.3-beta.4-mac-arm64-payload.zip',
state: 'downloaded',
});
const installed = updateStatus({
...payloadReady,
installResult: {
dryRun: true,
openedAt: '2026-05-19T00:00:00.000Z',
path: '/tmp/open-design-updater/open-design-1.2.3-beta.4-mac-arm64-payload.zip',
},
});
const install = vi.fn(async () => installed);
const quit = vi.fn(async () => ({
ok: false as const,
reason: 'desktop quit is not available',
}));
restoreOpenDesignHost = installMockOpenDesignHost({
host: {
updater: {
install,
quit,
status: vi.fn(async () => payloadReady),
},
},
});
renderSettingsDialog(
{ mode: 'daemon', agentId: 'codex' },
{
initialSection: 'about',
appVersionInfo: {
version: '1.2.3-beta.3',
channel: 'beta',
packaged: true,
platform: 'darwin',
arch: 'arm64',
},
},
);
fireEvent.click(await screen.findByRole('button', { name: en['updater.installRestart'] }));
await waitFor(() => {
expect(install).toHaveBeenCalledWith({ payload: { source: 'settings-about' } });
});
await waitFor(() => {
expect(quit).toHaveBeenCalledTimes(1);
});
expect(screen.getByRole('button', { name: en['updater.quitButton'] })).toBeTruthy();
expect(screen.getAllByText(en['settings.updateQuitFailed']).length).toBeGreaterThan(0);
fireEvent.click(screen.getByRole('button', { name: en['updater.quitButton'] }));
await waitFor(() => {
expect(quit).toHaveBeenCalledTimes(2);
});
expect(install).toHaveBeenCalledTimes(1);
});
});