mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
* Add global onboarding flow * Remove AMR from onboarding variant * Add onboarding role question
272 lines
7.6 KiB
TypeScript
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: '',
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
});
|
|
});
|