open-design/apps/web/tests/components/App.mediaProviders.test.tsx
Eli 431a5e2d79
[codex] Add global onboarding flow without AMR (#2272)
* Add global onboarding flow

* Remove AMR from onboarding variant

* Add onboarding role question
2026-05-19 22:00:40 +08:00

272 lines
7.6 KiB
TypeScript

// @vitest-environment jsdom
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { App } from '../../src/App';
import type { AppConfig } from '../../src/types';
import {
fetchComposioConfigFromDaemon,
loadConfig,
mergeDaemonConfig,
saveConfig,
syncConfigToDaemon,
syncMediaProvidersToDaemon,
} from '../../src/state/config';
import {
daemonIsLive,
fetchAgents,
fetchAppVersionInfo,
fetchDesignSystems,
fetchPromptTemplates,
fetchSkills,
} from '../../src/providers/registry';
import { listProjects, listTemplates } from '../../src/state/projects';
const navigateMock = vi.fn();
const useRouteMock = vi.fn(() => ({ kind: 'home' as const, view: 'home' as const }));
vi.mock('../../src/router', () => ({
navigate: (...args: unknown[]) => navigateMock(...args),
useRoute: () => useRouteMock(),
}));
vi.mock('../../src/components/EntryView', () => ({
EntryView: ({ onOpenSettings }: { onOpenSettings: (section?: 'execution' | 'media') => void }) => (
<div>
<button type="button" onClick={() => onOpenSettings('media')}>
Open media settings
</button>
</div>
),
}));
vi.mock('../../src/components/ProjectView', () => ({
ProjectView: () => <div>Project view</div>,
}));
vi.mock('../../src/components/pet/PetOverlay', () => ({
PetOverlay: () => null,
}));
vi.mock('../../src/components/pet/pets', () => ({
migrateCustomPetAtlas: vi.fn().mockResolvedValue(null),
}));
vi.mock('../../src/components/SettingsDialog', () => ({
SettingsDialog: ({
initial,
initialSection,
onPersist,
onClose,
}: {
initial: AppConfig;
initialSection?: string;
onPersist: (next: AppConfig) => void;
onClose: () => void;
}) => (
<div role="dialog" aria-label="Settings dialog">
<div>Section: {initialSection}</div>
<button
type="button"
onClick={() =>
onPersist({
...initial,
mediaProviders: {
openai: {
apiKey: 'media-key',
baseUrl: 'https://api.openai.com/v1',
model: '',
},
},
})
}
>
Save media provider
</button>
<button type="button" onClick={onClose}>
Close settings
</button>
</div>
),
}));
vi.mock('../../src/providers/registry', async () => {
const actual = await vi.importActual<typeof import('../../src/providers/registry')>(
'../../src/providers/registry',
);
return {
...actual,
daemonIsLive: vi.fn(),
fetchAgents: vi.fn(),
fetchAppVersionInfo: vi.fn(),
fetchDesignSystems: vi.fn(),
fetchPromptTemplates: vi.fn(),
fetchSkills: vi.fn(),
};
});
vi.mock('../../src/state/projects', async () => {
const actual = await vi.importActual<typeof import('../../src/state/projects')>(
'../../src/state/projects',
);
return {
...actual,
listProjects: vi.fn(),
listTemplates: vi.fn(),
};
});
vi.mock('../../src/state/config', async () => {
const actual = await vi.importActual<typeof import('../../src/state/config')>(
'../../src/state/config',
);
return {
...actual,
loadConfig: vi.fn(),
mergeDaemonConfig: vi.fn(),
saveConfig: vi.fn(),
syncConfigToDaemon: vi.fn().mockResolvedValue(undefined),
syncMediaProvidersToDaemon: vi.fn().mockResolvedValue(undefined),
fetchComposioConfigFromDaemon: vi.fn().mockResolvedValue(null),
};
});
const mockedDaemonIsLive = vi.mocked(daemonIsLive);
const mockedFetchAgents = vi.mocked(fetchAgents);
const mockedFetchAppVersionInfo = vi.mocked(fetchAppVersionInfo);
const mockedFetchDesignSystems = vi.mocked(fetchDesignSystems);
const mockedFetchPromptTemplates = vi.mocked(fetchPromptTemplates);
const mockedFetchSkills = vi.mocked(fetchSkills);
const mockedListProjects = vi.mocked(listProjects);
const mockedListTemplates = vi.mocked(listTemplates);
const mockedFetchComposioConfigFromDaemon = vi.mocked(fetchComposioConfigFromDaemon);
const mockedLoadConfig = vi.mocked(loadConfig);
const mockedMergeDaemonConfig = vi.mocked(mergeDaemonConfig);
const mockedSaveConfig = vi.mocked(saveConfig);
const mockedSyncConfigToDaemon = vi.mocked(syncConfigToDaemon);
const mockedSyncMediaProvidersToDaemon = vi.mocked(syncMediaProvidersToDaemon);
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: {},
};
describe('App media provider sync flows', () => {
beforeEach(() => {
mockedDaemonIsLive.mockResolvedValue(true);
mockedFetchAgents.mockResolvedValue([]);
mockedFetchSkills.mockResolvedValue([]);
mockedFetchDesignSystems.mockResolvedValue([]);
mockedFetchPromptTemplates.mockResolvedValue([]);
mockedFetchAppVersionInfo.mockResolvedValue(null);
mockedListProjects.mockResolvedValue([]);
mockedListTemplates.mockResolvedValue([]);
mockedFetchComposioConfigFromDaemon.mockResolvedValue(null);
mockedMergeDaemonConfig.mockImplementation((local) => local);
mockedLoadConfig.mockReturnValue({ ...baseConfig });
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue({
ok: true,
json: async () => ({}),
}),
);
});
afterEach(() => {
cleanup();
vi.unstubAllGlobals();
vi.clearAllMocks();
});
it('syncs configured media providers to the daemon during bootstrap when the daemon is live', async () => {
const configuredProviders = {
openai: {
apiKey: 'media-key',
baseUrl: 'https://api.openai.com/v1',
model: '',
},
};
mockedLoadConfig.mockReturnValue({
...baseConfig,
mediaProviders: configuredProviders,
});
render(<App />);
await waitFor(() => {
expect(mockedSyncMediaProvidersToDaemon).toHaveBeenCalledWith(configuredProviders, {
daemonProviders: {},
});
});
});
it('forces a media provider sync when settings are saved', async () => {
mockedLoadConfig.mockReturnValue({
...baseConfig,
onboardingCompleted: true,
privacyDecisionAt: 1778244000000,
});
render(<App />);
fireEvent.click(screen.getByRole('button', { name: 'Open media settings' }));
await waitFor(() => {
expect(screen.getByRole('dialog', { name: 'Settings dialog' })).toBeTruthy();
});
fireEvent.click(screen.getByRole('button', { name: 'Save media provider' }));
await waitFor(() => {
expect(mockedSyncMediaProvidersToDaemon).toHaveBeenCalledWith(
{
openai: {
apiKey: 'media-key',
baseUrl: 'https://api.openai.com/v1',
model: '',
},
},
{ daemonProviders: {}, force: undefined, throwOnError: undefined },
);
});
expect(mockedSaveConfig).toHaveBeenCalledWith(
expect.objectContaining({
onboardingCompleted: true,
mediaProviders: {
openai: {
apiKey: 'media-key',
baseUrl: 'https://api.openai.com/v1',
model: '',
},
},
}),
);
expect(mockedSyncConfigToDaemon).toHaveBeenCalledWith(
expect.objectContaining({
onboardingCompleted: true,
mediaProviders: {
openai: {
apiKey: 'media-key',
baseUrl: 'https://api.openai.com/v1',
model: '',
},
},
}),
);
});
});