Files

286 lines
9.6 KiB
TypeScript
Raw Permalink Normal View History

import { expect, test } from '@playwright/test';
import type { Page } from '@playwright/test';
const STORAGE_KEY = 'open-design:config';
async function openExecutionSettings(
page: Page,
config: Record<string, unknown>,
) {
await page.addInitScript(
({ key, value }) => {
window.localStorage.setItem(key, JSON.stringify(value));
},
{ key: STORAGE_KEY, value: config },
);
await page.route('**/api/health', async (route) => {
await route.fulfill({ status: 503, body: 'offline' });
});
await page.goto('/');
await page.getByTitle('Configure execution mode').click();
await expect(page.getByRole('dialog')).toBeVisible();
}
2026-05-08 14:27:46 +08:00
async function readSavedConfig(page: Page) {
return page.evaluate((key) => {
const raw = window.localStorage.getItem(key);
return raw ? JSON.parse(raw) : null;
}, STORAGE_KEY);
}
async function openExecutionSettingsWithAgents(
page: Page,
config: Record<string, unknown>,
agents: Array<{
id: string;
name: string;
bin: string;
available: boolean;
version?: string | null;
models?: Array<{ id: string; label: string }>;
}>,
) {
await page.addInitScript(
({ key, value }) => {
window.localStorage.setItem(key, JSON.stringify(value));
},
{ key: STORAGE_KEY, value: config },
);
await page.route('**/api/health', async (route) => {
await route.fulfill({ status: 200, contentType: 'application/json', body: '{"ok":true}' });
});
await page.route('**/api/agents', async (route) => {
await route.fulfill({ json: { agents } });
});
await page.goto('/');
await page.getByTitle('Configure execution mode').click();
await expect(page.getByRole('dialog')).toBeVisible();
}
test('legacy known OpenAI provider switches to the matching Anthropic preset', async ({ page }) => {
await openExecutionSettings(page, {
mode: 'api',
apiKey: 'sk-test',
baseUrl: 'https://api.deepseek.com',
model: 'deepseek-chat',
agentId: null,
skillId: null,
designSystemId: null,
onboardingCompleted: true,
mediaProviders: {},
agentModels: {},
});
2026-05-08 14:27:46 +08:00
const dialog = page.getByRole('dialog');
const protocolTabs = dialog.getByRole('tablist', { name: 'API protocol' });
const openAiTab = protocolTabs.getByRole('tab', { name: 'OpenAI', exact: true });
const anthropicTab = protocolTabs.getByRole('tab', { name: 'Anthropic', exact: true });
2026-05-08 14:27:46 +08:00
const baseUrlInput = dialog.getByLabel('Base URL');
const modelSelect = dialog.getByLabel('Model');
await expect(openAiTab).toHaveAttribute('aria-selected', 'true');
2026-05-08 14:27:46 +08:00
await expect(dialog.getByRole('heading', { name: 'OpenAI API' })).toBeVisible();
await expect(baseUrlInput).toHaveValue('https://api.deepseek.com');
await expect(modelSelect).toHaveValue('deepseek-chat');
await anthropicTab.click();
await expect(anthropicTab).toHaveAttribute('aria-selected', 'true');
2026-05-08 14:27:46 +08:00
await expect(dialog.getByRole('heading', { name: 'Anthropic API' })).toBeVisible();
await expect(baseUrlInput).toHaveValue('https://api.deepseek.com/anthropic');
await expect(modelSelect).toHaveValue('deepseek-chat');
});
test('legacy custom provider preserves custom baseUrl and model when switching protocols', async ({ page }) => {
await openExecutionSettings(page, {
mode: 'api',
apiKey: 'sk-test',
baseUrl: 'https://my-proxy.example.com/v1',
model: 'my-custom-model',
agentId: null,
skillId: null,
designSystemId: null,
onboardingCompleted: true,
mediaProviders: {},
agentModels: {},
});
2026-05-08 14:27:46 +08:00
const dialog = page.getByRole('dialog');
const protocolTabs = dialog.getByRole('tablist', { name: 'API protocol' });
const openAiTab = protocolTabs.getByRole('tab', { name: 'OpenAI', exact: true });
const anthropicTab = protocolTabs.getByRole('tab', { name: 'Anthropic', exact: true });
2026-05-08 14:27:46 +08:00
const baseUrlInput = dialog.getByLabel('Base URL');
const customModelInput = dialog.getByLabel(/Custom model id/i);
await expect(openAiTab).toHaveAttribute('aria-selected', 'true');
2026-05-08 14:27:46 +08:00
await expect(dialog.getByRole('heading', { name: 'OpenAI API' })).toBeVisible();
await expect(baseUrlInput).toHaveValue('https://my-proxy.example.com/v1');
await expect(customModelInput).toHaveValue('my-custom-model');
await anthropicTab.click();
await expect(anthropicTab).toHaveAttribute('aria-selected', 'true');
2026-05-08 14:27:46 +08:00
await expect(dialog.getByRole('heading', { name: 'Anthropic API' })).toBeVisible();
await expect(baseUrlInput).toHaveValue('https://my-proxy.example.com/v1');
await expect(customModelInput).toHaveValue('my-custom-model');
});
test('BYOK quick fill provider updates fields and saved settings persist after closing and reopening', async ({ page }) => {
await openExecutionSettings(page, {
mode: 'api',
apiKey: '',
apiProtocol: 'openai',
apiVersion: '',
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-4o',
apiProviderBaseUrl: 'https://api.openai.com/v1',
agentId: null,
skillId: null,
designSystemId: null,
onboardingCompleted: true,
mediaProviders: {},
agentModels: {},
agentCliEnv: {},
});
2026-05-08 14:27:46 +08:00
const dialog = page.getByRole('dialog');
2026-05-08 14:27:46 +08:00
await dialog.getByRole('tab', { name: 'OpenAI', exact: true }).click();
await dialog.getByLabel('Quick fill provider').selectOption('1');
await expect(dialog.getByLabel('Model')).toHaveValue('deepseek-chat');
await expect(dialog.getByLabel('Base URL')).toHaveValue('https://api.deepseek.com');
await dialog.getByRole('button', { name: 'Show' }).click();
const apiKeyInput = dialog.getByLabel('API key');
await expect(apiKeyInput).toHaveAttribute('type', 'text');
await apiKeyInput.fill('sk-openai-test');
2026-05-08 14:27:46 +08:00
await expect
.poll(async () => readSavedConfig(page))
.toMatchObject({
mode: 'api',
apiProtocol: 'openai',
apiKey: 'sk-openai-test',
baseUrl: 'https://api.deepseek.com',
model: 'deepseek-chat',
apiProviderBaseUrl: 'https://api.deepseek.com',
});
await dialog.getByRole('button', { name: 'Close', exact: true }).click();
await expect(page.getByRole('dialog')).toHaveCount(0);
2026-05-08 14:27:46 +08:00
const savedConfig = await readSavedConfig(page);
expect(savedConfig).toMatchObject({
mode: 'api',
apiProtocol: 'openai',
apiKey: 'sk-openai-test',
baseUrl: 'https://api.deepseek.com',
model: 'deepseek-chat',
apiProviderBaseUrl: 'https://api.deepseek.com',
});
await page.getByTitle('Configure execution mode').click();
await expect(page.getByRole('dialog')).toBeVisible();
2026-05-08 14:27:46 +08:00
const reopenedDialog = page.getByRole('dialog');
await expect(reopenedDialog.getByRole('tab', { name: 'OpenAI', exact: true })).toHaveAttribute('aria-selected', 'true');
await expect(reopenedDialog.getByLabel('Quick fill provider')).toHaveValue('1');
await expect(reopenedDialog.getByLabel('Model')).toHaveValue('deepseek-chat');
await expect(reopenedDialog.getByLabel('Base URL')).toHaveValue('https://api.deepseek.com');
await expect(reopenedDialog.getByLabel('API key')).toHaveValue('sk-openai-test');
});
test('BYOK save stays disabled until required fields are valid', async ({ page }) => {
await openExecutionSettings(page, {
mode: 'api',
apiKey: '',
apiProtocol: 'anthropic',
apiVersion: '',
baseUrl: 'https://api.anthropic.com',
model: 'claude-sonnet-4-5',
apiProviderBaseUrl: 'https://api.anthropic.com',
agentId: null,
skillId: null,
designSystemId: null,
onboardingCompleted: true,
mediaProviders: {},
agentModels: {},
agentCliEnv: {},
});
2026-05-08 14:27:46 +08:00
const dialog = page.getByRole('dialog');
const closeButton = dialog.getByRole('button', { name: 'Close', exact: true });
await expect(closeButton).toBeEnabled();
2026-05-08 14:27:46 +08:00
await dialog.getByLabel('API key').fill('sk-ant-test');
await expect.poll(async () => readSavedConfig(page)).toMatchObject({ apiKey: 'sk-ant-test' });
2026-05-08 14:27:46 +08:00
await dialog.getByLabel('Base URL').fill('http://10.0.0.5:11434/v1');
await expect(dialog.locator('#settings-base-url-error')).toContainText('valid public');
2026-05-08 14:27:46 +08:00
await dialog.getByLabel('Base URL').fill('http://localhost:11434/v1');
await expect.poll(async () => readSavedConfig(page)).toMatchObject({
apiKey: 'sk-ant-test',
baseUrl: 'http://localhost:11434/v1',
});
});
test('saving Local CLI updates the entry status pill with the selected agent', async ({ page }) => {
await openExecutionSettingsWithAgents(
page,
{
mode: 'api',
apiKey: 'sk-openai-test',
apiProtocol: 'openai',
apiVersion: '',
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-4o',
apiProviderBaseUrl: 'https://api.openai.com/v1',
agentId: null,
skillId: null,
designSystemId: null,
onboardingCompleted: true,
mediaProviders: {},
agentModels: {},
agentCliEnv: {},
},
[
{
id: 'codex',
name: 'Codex CLI',
bin: 'codex',
available: true,
version: '0.80.0',
models: [{ id: 'default', label: 'Default' }],
},
{
id: 'gemini',
name: 'Gemini CLI',
bin: 'gemini',
available: false,
version: null,
models: [],
},
],
);
2026-05-08 14:27:46 +08:00
const dialog = page.getByRole('dialog');
await dialog.getByRole('tab', { name: /Local CLI.*1 installed/i }).click();
await dialog.getByRole('button', { name: /Codex CLI/i }).click();
await expect.poll(async () => readSavedConfig(page)).toMatchObject({
mode: 'daemon',
agentId: 'codex',
});
await dialog.getByRole('button', { name: 'Close', exact: true }).click();
await expect(page.getByRole('dialog')).toHaveCount(0);
const executionPill = page.getByTitle('Configure execution mode');
await expect(executionPill).toContainText('Local CLI');
await expect(executionPill).toContainText('Codex CLI');
await expect(executionPill).toContainText('0.80.0');
});