open-design/apps/web/tests/components/SettingsDialog.media.test.tsx
Marc Chan a3872b97a9
fix(tools-dev): preserve web origin trust on web start (#2715)
* fix(tools-dev): preserve web origin trust on web start

Restart daemon/web when the trusted web port is missing, and reuse the active web port during repeated starts so run web and start web keep app-config origin checks aligned.

Generated-By: looper 0.0.0-dev (runner=worker, agent=opencode)

* fix(plugins): refresh official registry bundled count

Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode)

* fix(tools-dev): preserve daemon/web reserved ports

Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode)

* fix(tools-dev): preserve daemon reuse on web start

Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode)

* fix(tools-dev): preserve running daemon port on web reuse

Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode)

* fix(tools-dev): reserve explicit web port before daemon allocation

Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode)

* test(web): stabilize media provider reload flash timing

Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode)

* fix(web): restore merged reattach workspace coverage

Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode)

* fix(tools-dev): reserve allocated daemon port

Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode)

* test(e2e): wait for artifact manifest persistence

Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode)
2026-05-23 00:25:43 +08:00

546 lines
17 KiB
TypeScript

// @vitest-environment jsdom
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
import { within } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { SettingsDialog } from '../../src/components/SettingsDialog';
import { DEFAULT_CONFIG } from '../../src/state/config';
import type { AgentInfo, AppConfig } from '../../src/types';
describe('SettingsDialog media providers', () => {
afterEach(() => {
cleanup();
vi.useRealTimers();
});
it('shows saved masked media provider keys like Composio does', () => {
renderDialog({
...DEFAULT_CONFIG,
mediaProviders: {
openai: {
apiKey: '',
apiKeyConfigured: true,
apiKeyTail: '1234',
baseUrl: '',
},
},
});
expect(screen.getByText('Saved · ••••1234')).toBeTruthy();
expect(screen.getByLabelText('OpenAI API key').getAttribute('placeholder')).toBe(
'Paste a new key to replace the saved one',
);
});
it('shows daemon fallback notice and reloads media providers from daemon', async () => {
const reloadMock = vi.fn(async () => ({
openai: {
apiKey: '',
apiKeyConfigured: true,
apiKeyTail: '9876',
baseUrl: 'https://daemon.example/v1',
},
}));
renderDialog(
{
...DEFAULT_CONFIG,
mediaProviders: {},
},
{
mediaProvidersNotice:
'Could not load media provider settings from the local daemon. Using browser-saved settings for now.',
onReloadMediaProviders: reloadMock,
},
);
expect(
screen.getByText(
'Could not load media provider settings from the local daemon. Using browser-saved settings for now.',
),
).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: 'Reload from daemon' }));
await waitFor(() => {
expect(reloadMock).toHaveBeenCalledTimes(1);
expect(screen.getByText('Saved · ••••9876')).toBeTruthy();
expect(screen.getByText('Reloaded media provider settings from the local daemon.')).toBeTruthy();
});
expect((screen.getByLabelText('OpenAI Base URL') as HTMLInputElement).value).toBe(
'https://daemon.example/v1',
);
});
it('shows loading while reloading, then clears the success flash after a short delay', async () => {
vi.useFakeTimers();
const reloadMock = vi.fn(
() =>
new Promise<AppConfig['mediaProviders']>((resolve) => {
setTimeout(() => {
resolve({
openai: {
apiKey: '',
apiKeyConfigured: true,
apiKeyTail: '9876',
baseUrl: 'https://daemon.example/v1',
},
});
}, 50);
}),
);
renderDialog(
{
...DEFAULT_CONFIG,
mediaProviders: {},
},
{
mediaProvidersNotice:
'Could not load media provider settings from the local daemon. Using browser-saved settings for now.',
onReloadMediaProviders: reloadMock,
},
);
const reloadButton = screen.getByRole('button', { name: 'Reload from daemon' });
fireEvent.click(reloadButton);
expect(reloadMock).toHaveBeenCalledTimes(1);
expect((screen.getByRole('button', { name: 'Loading…' }) as HTMLButtonElement).disabled).toBe(true);
await act(async () => {
await vi.advanceTimersByTimeAsync(50);
});
expect(screen.getByText('Reloaded media provider settings from the local daemon.')).toBeTruthy();
expect(screen.getByRole('button', { name: 'Reloaded' })).toBeTruthy();
await act(async () => {
await vi.advanceTimersByTimeAsync(2000);
});
expect(screen.queryByText('Reloaded media provider settings from the local daemon.')).toBeNull();
expect(screen.getByRole('button', { name: 'Reload from daemon' })).toBeTruthy();
});
it('shows a sticky error when reloading media providers from daemon fails', async () => {
const reloadMock = vi.fn(async () => null);
renderDialog(
{
...DEFAULT_CONFIG,
mediaProviders: {},
},
{
mediaProvidersNotice:
'Could not load media provider settings from the local daemon. Using browser-saved settings for now.',
onReloadMediaProviders: reloadMock,
},
);
fireEvent.click(screen.getByRole('button', { name: 'Reload from daemon' }));
await waitFor(() => {
expect(reloadMock).toHaveBeenCalledTimes(1);
expect(
screen.getByText('Could not reload media provider settings from the local daemon.'),
).toBeTruthy();
});
expect(screen.getByRole('button', { name: 'Reload from daemon' })).toBeTruthy();
});
it('refreshes daemon-backed providers while keeping untouched local-only providers when daemon reload returns a partial provider set', async () => {
const reloadMock = vi.fn(async () => ({
openai: {
apiKey: '',
apiKeyConfigured: true,
apiKeyTail: '9876',
baseUrl: 'https://daemon.example/v1',
},
}));
renderDialog(
{
...DEFAULT_CONFIG,
mediaProviders: {
openai: {
apiKey: 'sk-local-openai',
baseUrl: 'https://local-openai.example/v1',
},
fal: {
apiKey: 'sk-local-fal',
baseUrl: 'https://queue.fal.run',
model: 'fal-ai/imagen4/preview',
},
},
},
{
mediaProvidersNotice:
'Could not load media provider settings from the local daemon. Using browser-saved settings for now.',
onReloadMediaProviders: reloadMock,
},
);
fireEvent.click(screen.getByRole('button', { name: 'Reload from daemon' }));
await waitFor(() => {
expect(reloadMock).toHaveBeenCalledTimes(1);
expect(screen.getByText('Reloaded media provider settings from the local daemon.')).toBeTruthy();
});
expect((screen.getByLabelText('OpenAI Base URL') as HTMLInputElement).value).toBe(
'https://daemon.example/v1',
);
expect((screen.getByLabelText('OpenAI API key') as HTMLInputElement).value).toBe('');
expect(screen.getByText('Saved · ••••9876')).toBeTruthy();
// Fal.ai is a non-integrated (coming-soon) provider and no longer has
// editable input fields in the UI; its config is preserved in state via
// mergeDaemonMediaProviders (covered by state/config.test.ts).
});
it('preserves saved media keys when clearing only a non-secret field', async () => {
const onPersist = vi.fn();
renderDialog(
{
...saveableConfig(),
mediaProviders: {
openai: {
apiKey: '',
apiKeyConfigured: true,
apiKeyTail: '1234',
baseUrl: 'https://custom.example/v1',
},
},
},
{ onPersist },
);
fireEvent.change(screen.getByLabelText('OpenAI Base URL'), { target: { value: '' } });
await waitFor(() => {
expect(onPersist).toHaveBeenCalledWith(
expect.objectContaining({
mediaProviders: {
openai: {
apiKey: '',
apiKeyConfigured: true,
apiKeyTail: '1234',
baseUrl: '',
},
},
}),
expect.objectContaining({ forceMediaProviderSync: true }),
);
});
});
it('does not overwrite a local pending media-provider edit when daemon reload returns saved state', async () => {
const reloadMock = vi.fn(async () => ({
openai: {
apiKey: '',
apiKeyConfigured: true,
apiKeyTail: '9876',
baseUrl: 'https://daemon.example/v1',
},
}));
renderDialog(
{
...DEFAULT_CONFIG,
mediaProviders: {
openai: {
apiKey: '',
apiKeyConfigured: true,
apiKeyTail: '1234',
baseUrl: 'https://saved.example/v1',
},
},
},
{
mediaProvidersNotice:
'Could not load media provider settings from the local daemon. Using browser-saved settings for now.',
onReloadMediaProviders: reloadMock,
},
);
fireEvent.change(screen.getByLabelText('OpenAI API key'), {
target: { value: 'sk-local-pending' },
});
fireEvent.change(screen.getByLabelText('OpenAI Base URL'), {
target: { value: 'https://local-pending.example/v1' },
});
fireEvent.click(screen.getByRole('button', { name: 'Reload from daemon' }));
await waitFor(() => {
expect(reloadMock).toHaveBeenCalledTimes(1);
});
expect((screen.getByLabelText('OpenAI API key') as HTMLInputElement).value).toBe('sk-local-pending');
expect((screen.getByLabelText('OpenAI Base URL') as HTMLInputElement).value).toBe(
'https://local-pending.example/v1',
);
});
it('stops preserving a provider on reload after its media autosave succeeds', async () => {
const reloadMock = vi.fn(async () => ({
openai: {
apiKey: '',
apiKeyConfigured: true,
apiKeyTail: '9876',
baseUrl: 'https://daemon.example/v1',
},
}));
const onPersist = vi.fn(async () => undefined);
renderDialog(
{
...saveableConfig(),
mediaProviders: {
openai: {
apiKey: '',
apiKeyConfigured: true,
apiKeyTail: '1234',
baseUrl: 'https://saved.example/v1',
},
},
},
{
onPersist,
onReloadMediaProviders: reloadMock,
},
);
fireEvent.change(screen.getByLabelText('OpenAI API key'), {
target: { value: 'sk-local-saved' },
});
fireEvent.change(screen.getByLabelText('OpenAI Base URL'), {
target: { value: 'https://local-saved.example/v1' },
});
await waitFor(() => {
expect(onPersist).toHaveBeenCalledWith(
expect.objectContaining({
mediaProviders: {
openai: {
apiKey: 'sk-local-saved',
apiKeyConfigured: true,
apiKeyTail: '1234',
baseUrl: 'https://local-saved.example/v1',
},
},
}),
expect.objectContaining({ forceMediaProviderSync: true }),
);
});
fireEvent.click(screen.getByRole('button', { name: 'Reload from daemon' }));
await waitFor(() => {
expect(reloadMock).toHaveBeenCalledTimes(1);
expect(screen.getByText('Reloaded media provider settings from the local daemon.')).toBeTruthy();
});
expect((screen.getByLabelText('OpenAI API key') as HTMLInputElement).value).toBe('');
expect((screen.getByLabelText('OpenAI Base URL') as HTMLInputElement).value).toBe(
'https://daemon.example/v1',
);
expect(screen.getByText('Saved · ••••9876')).toBeTruthy();
});
it('keeps newer pending provider edits during reload when an older media autosave resolves', async () => {
vi.useFakeTimers();
const reloadMock = vi.fn(async () => ({
openai: {
apiKey: '',
apiKeyConfigured: true,
apiKeyTail: '9876',
baseUrl: 'https://daemon-openai.example/v1',
},
nanobanana: {
apiKey: '',
apiKeyConfigured: true,
apiKeyTail: '4444',
baseUrl: 'https://daemon-nanobanana.example/v1',
model: 'gemini-3.1-flash-image-preview',
},
}));
let resolveFirstPersist: (() => void) | null = null;
const firstPersist = new Promise<void>((resolve) => {
resolveFirstPersist = resolve;
});
const onPersist = vi.fn()
.mockImplementationOnce(() => firstPersist)
.mockImplementation(async () => undefined);
renderDialog(
{
...saveableConfig(),
mediaProviders: {
openai: {
apiKey: '',
apiKeyConfigured: true,
apiKeyTail: '1234',
baseUrl: 'https://saved-openai.example/v1',
},
nanobanana: {
apiKey: '',
apiKeyConfigured: true,
apiKeyTail: '5555',
baseUrl: 'https://saved-nanobanana.example/v1',
model: 'gemini-3.1-flash-image-preview',
},
},
},
{
onPersist,
onReloadMediaProviders: reloadMock,
},
);
fireEvent.change(screen.getByLabelText('OpenAI API key'), {
target: { value: 'sk-openai-first-save' },
});
fireEvent.change(screen.getByLabelText('OpenAI Base URL'), {
target: { value: 'https://local-openai.example/v1' },
});
await act(async () => {
await vi.advanceTimersByTimeAsync(400);
});
expect(onPersist).toHaveBeenCalledTimes(1);
fireEvent.change(screen.getByLabelText('Nano Banana API key'), {
target: { value: 'sk-nanobanana-pending' },
});
fireEvent.change(screen.getByLabelText('Nano Banana Base URL'), {
target: { value: 'https://local-nanobanana.example/v1' },
});
await act(async () => {
resolveFirstPersist?.();
await Promise.resolve();
});
await act(async () => {
fireEvent.click(screen.getByRole('button', { name: 'Reload from daemon' }));
await Promise.resolve();
});
expect(reloadMock).toHaveBeenCalledTimes(1);
expect((screen.getByLabelText('Nano Banana API key') as HTMLInputElement).value).toBe(
'sk-nanobanana-pending',
);
expect((screen.getByLabelText('Nano Banana Base URL') as HTMLInputElement).value).toBe(
'https://local-nanobanana.example/v1',
);
});
it('clears saved media keys only through the explicit Clear action', async () => {
const onPersist = vi.fn();
const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true);
renderDialog(
{
...saveableConfig(),
mediaProviders: {
openai: {
apiKey: '',
apiKeyConfigured: true,
apiKeyTail: '1234',
baseUrl: 'https://custom.example/v1',
},
},
},
{ onPersist },
);
const openaiRow = screen.getByText('OpenAI').closest('.media-provider-row') as HTMLElement | null;
if (!openaiRow) throw new Error('Expected OpenAI media provider row');
fireEvent.click(within(openaiRow).getByRole('button', { name: 'Clear' }));
await waitFor(() => {
expect(onPersist).toHaveBeenCalledWith(
expect.objectContaining({ mediaProviders: {} }),
expect.objectContaining({ forceMediaProviderSync: true }),
);
});
expect(confirmSpy).toHaveBeenCalledTimes(1);
confirmSpy.mockRestore();
});
it('clears saved marker state and custom model fields together for custom-model providers', async () => {
const onPersist = vi.fn();
const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true);
renderDialog(
{
...saveableConfig(),
mediaProviders: {
nanobanana: {
apiKey: '',
apiKeyConfigured: true,
apiKeyTail: '5555',
baseUrl: 'https://gateway.example.com',
model: 'gemini-3.1-flash-image-preview',
},
},
},
{ onPersist },
);
const row = screen.getByText('Nano Banana').closest('.media-provider-row') as HTMLElement | null;
if (!row) throw new Error('Expected Nano Banana media provider row');
expect(screen.getByText('Saved · ••••5555')).toBeTruthy();
expect(screen.getByLabelText('Nano Banana API key').getAttribute('placeholder')).toBe(
'Paste a new key to replace the saved one',
);
fireEvent.click(within(row).getByRole('button', { name: 'Clear' }));
await waitFor(() => {
expect(onPersist).toHaveBeenCalledWith(
expect.objectContaining({ mediaProviders: {} }),
expect.objectContaining({ forceMediaProviderSync: true }),
);
});
expect((screen.getByLabelText('Nano Banana model') as HTMLInputElement).value).toBe('');
expect(confirmSpy).toHaveBeenCalledTimes(1);
confirmSpy.mockRestore();
});
});
function renderDialog(
initial: AppConfig,
options?: {
mediaProvidersNotice?: string | null;
onReloadMediaProviders?: () => Promise<AppConfig['mediaProviders'] | null>;
onPersist?: (cfg: AppConfig, options?: { forceMediaProviderSync?: boolean }) => void;
},
) {
return render(
<SettingsDialog
initial={initial}
agents={SAVEABLE_AGENTS}
daemonLive
appVersionInfo={null}
initialSection="media"
onPersist={options?.onPersist ?? vi.fn()}
onPersistComposioKey={vi.fn()}
onClose={vi.fn()}
onRefreshAgents={vi.fn()}
mediaProvidersNotice={options?.mediaProvidersNotice}
onReloadMediaProviders={options?.onReloadMediaProviders}
/>,
);
}
const SAVEABLE_AGENTS: AgentInfo[] = [
{
id: 'codex',
name: 'Codex',
bin: 'codex',
available: true,
},
];
function saveableConfig(): AppConfig {
return {
...DEFAULT_CONFIG,
agentId: 'codex',
};
}