import { completeSetup, expect, test } from './fixtures/electron'; const TEST_PROVIDER_ID = 'moonshot-e2e'; const TEST_PROVIDER_LABEL = 'Moonshot E2E'; async function seedTestProvider(page: Parameters[0]): Promise { await page.evaluate(async ({ providerId, providerLabel }) => { const now = new Date().toISOString(); await window.electron.ipcRenderer.invoke('provider:save', { id: providerId, name: providerLabel, type: 'moonshot', baseUrl: 'https://api.moonshot.cn/v1', model: 'kimi-k2.6', enabled: true, createdAt: now, updatedAt: now, }); }, { providerId: TEST_PROVIDER_ID, providerLabel: TEST_PROVIDER_LABEL }); } test.describe('ClawX provider lifecycle', () => { test('shows a saved provider and removes it cleanly after deletion', async ({ page }) => { await completeSetup(page); await seedTestProvider(page); await page.getByTestId('sidebar-nav-models').click(); await expect(page.getByTestId('providers-settings')).toBeVisible(); await expect(page.getByTestId(`provider-card-${TEST_PROVIDER_ID}`)).toContainText(TEST_PROVIDER_LABEL); await page.getByTestId(`provider-card-${TEST_PROVIDER_ID}`).hover(); await page.getByTestId(`provider-delete-${TEST_PROVIDER_ID}`).click(); await expect(page.getByTestId(`provider-card-${TEST_PROVIDER_ID}`)).toHaveCount(0); await expect(page.getByText(TEST_PROVIDER_LABEL)).toHaveCount(0); }); test('does not redisplay a deleted provider after relaunch', async ({ electronApp, launchElectronApp, page }) => { await completeSetup(page); await seedTestProvider(page); await page.getByTestId('sidebar-nav-models').click(); await expect(page.getByTestId(`provider-card-${TEST_PROVIDER_ID}`)).toContainText(TEST_PROVIDER_LABEL); await page.getByTestId(`provider-card-${TEST_PROVIDER_ID}`).hover(); await page.getByTestId(`provider-delete-${TEST_PROVIDER_ID}`).click(); await expect(page.getByTestId(`provider-card-${TEST_PROVIDER_ID}`)).toHaveCount(0); await electronApp.close(); const relaunchedApp = await launchElectronApp(); try { const relaunchedPage = await relaunchedApp.firstWindow(); await relaunchedPage.waitForLoadState('domcontentloaded'); await expect(relaunchedPage.getByTestId('main-layout')).toBeVisible(); await relaunchedPage.getByTestId('sidebar-nav-models').click(); await expect(relaunchedPage.getByTestId('providers-settings')).toBeVisible(); await expect(relaunchedPage.getByTestId(`provider-card-${TEST_PROVIDER_ID}`)).toHaveCount(0); await expect(relaunchedPage.getByText(TEST_PROVIDER_LABEL)).toHaveCount(0); } finally { await relaunchedApp.close(); } }); test('shows OpenAI OAuth and API key auth mode toggle in add-provider dialog', async ({ page }) => { await completeSetup(page); await page.getByTestId('sidebar-nav-models').click(); await expect(page.getByTestId('providers-settings')).toBeVisible(); await page.getByTestId('providers-add-button').click(); await expect(page.getByTestId('add-provider-dialog')).toBeVisible(); await page.getByTestId('add-provider-type-openai').click(); await expect(page.getByRole('button', { name: 'OAuth Login' })).toBeVisible(); await expect(page.getByRole('button', { name: 'API Key' })).toBeVisible(); await page.getByRole('button', { name: 'OAuth Login' }).click(); await expect(page.getByRole('button', { name: 'Login with Browser' })).toBeVisible(); await expect(page.getByTestId('add-provider-api-key-input')).toHaveCount(0); }); test('trims whitespace before validating and saving a custom provider key', async ({ electronApp, page }) => { await completeSetup(page); await electronApp.evaluate(async ({ app: _app }) => { const { ipcMain } = process.mainModule!.require('electron') as typeof import('electron'); let accounts: Array> = []; let keyInfo: Array<{ accountId: string; hasKey: boolean; keyMasked: string | null }> = []; let statuses: Array> = []; let defaultAccountId: string | null = null; const respond = (json: unknown, status = 200) => ({ ok: true, data: { status, ok: status >= 200 && status < 300, json, }, }); ipcMain.removeHandler('hostapi:fetch'); ipcMain.handle('hostapi:fetch', async (_event: unknown, request: { path?: string; method?: string; body?: string | null }) => { const path = request?.path ?? ''; const method = request?.method ?? 'GET'; const body = request?.body ? JSON.parse(request.body) : null; // New account-based endpoints (preferred path). if (path === '/api/provider-accounts' && method === 'GET') return respond(accounts); if (path === '/api/provider-accounts/key-info' && method === 'GET') return respond(keyInfo); if (path === '/api/provider-vendors' && method === 'GET') return respond([]); if (path === '/api/provider-accounts/default' && method === 'GET') return respond({ accountId: defaultAccountId }); if (path === '/api/provider-accounts/validate' && method === 'POST') { if (body?.apiKey !== 'sk-lm-test') { return respond({ valid: false, error: `unexpected key: ${String(body?.apiKey)}` }, 400); } return respond({ valid: true }); } if (path === '/api/provider-accounts' && method === 'POST') { accounts = [body.account]; keyInfo = [{ accountId: body.account.id, hasKey: Boolean(body.apiKey), keyMasked: body.apiKey ? 'sk-***' : null, }]; // Keep statuses populated for any consumer still on the legacy path. statuses = [{ id: body.account.id, name: body.account.label, type: body.account.vendorId, baseUrl: body.account.baseUrl, model: body.account.model, enabled: body.account.enabled, createdAt: body.account.createdAt, updatedAt: body.account.updatedAt, hasKey: Boolean(body.apiKey), keyMasked: body.apiKey ? 'sk-***' : null, }]; return respond({ success: true }); } if (path === '/api/provider-accounts/default' && method === 'PUT') { defaultAccountId = body?.accountId ?? null; return respond({ success: true }); } // ── Legacy compatibility shims ───────────────────────────── // Older renderer builds still reach for these. Keeping them // wired up here exercises the backward-compat path in the // route layer (it returns the same data, just without the // newer key-info payload structure). if (path === '/api/providers' && method === 'GET') return respond(statuses); if (path === '/api/providers/validate' && method === 'POST') { if (body?.apiKey !== 'sk-lm-test') { return respond({ valid: false, error: `unexpected key: ${String(body?.apiKey)}` }, 400); } return respond({ valid: true }); } return respond({}); }); }); await page.getByTestId('sidebar-nav-models').click(); await expect(page.getByTestId('providers-settings')).toBeVisible(); await page.getByTestId('providers-add-button').click(); await expect(page.getByTestId('add-provider-dialog')).toBeVisible(); await page.getByTestId('add-provider-type-custom').click(); await page.getByTestId('add-provider-name-input').fill('LM Studio Local'); await page.getByTestId('add-provider-api-key-input').fill(' sk-lm-test \n'); await page.getByTestId('add-provider-base-url-input').fill('http://127.0.0.1:1234/v1'); await page.getByTestId('add-provider-model-id-input').fill('local-model'); await page.getByTestId('add-provider-submit-button').click(); await expect(page.getByTestId('provider-card-custom')).toContainText('LM Studio Local'); }); test('edit form validates the new API key inline before saving (single button)', async ({ electronApp, page }) => { await completeSetup(page); await electronApp.evaluate(async ({ app: _app }) => { const { ipcMain } = process.mainModule!.require('electron') as typeof import('electron'); const provider = { id: 'moonshot-edit', vendorId: 'moonshot', label: 'Moonshot Edit', authMode: 'api_key', baseUrl: 'https://api.moonshot.cn/v1', model: 'kimi-k2.6', enabled: true, isDefault: false, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; let storedKey = 'sk-existing'; let keyInfo = [{ accountId: provider.id, hasKey: true, keyMasked: 'sk-***' }]; const respond = (json: unknown, status = 200) => ({ ok: true, data: { status, ok: status >= 200 && status < 300, json, }, }); ipcMain.removeHandler('hostapi:fetch'); ipcMain.handle('hostapi:fetch', async (_event: unknown, request: { path?: string; method?: string; body?: string | null }) => { const path = request?.path ?? ''; const method = request?.method ?? 'GET'; const body = request?.body ? JSON.parse(request.body) : null; if (path === '/api/provider-accounts' && method === 'GET') return respond([provider]); if (path === '/api/provider-accounts/key-info' && method === 'GET') return respond(keyInfo); if (path === '/api/provider-vendors' && method === 'GET') return respond([]); if (path === '/api/provider-accounts/default' && method === 'GET') return respond({ accountId: provider.id }); if (path === '/api/provider-accounts/validate' && method === 'POST') { if (body?.apiKey === 'sk-good') { return respond({ valid: true }); } return respond({ valid: false, error: 'Invalid API key' }, 400); } if (path.startsWith('/api/provider-accounts/') && method === 'PUT') { if (body?.apiKey) storedKey = body.apiKey; keyInfo = [{ accountId: provider.id, hasKey: Boolean(storedKey), keyMasked: 'sk-***' }]; return respond({ success: true }); } if (path === '/api/providers' && method === 'GET') return respond([provider]); return respond({}); }); }); await page.getByTestId('sidebar-nav-models').click(); await expect(page.getByTestId('providers-settings')).toBeVisible(); await expect(page.getByTestId('provider-card-moonshot-edit')).toBeVisible(); await page.getByTestId('provider-card-moonshot-edit').hover(); await page.getByTestId('provider-edit-moonshot-edit').click(); await page.getByTestId('provider-edit-key-input-moonshot-edit').fill('sk-bad'); await page.getByTestId('provider-edit-save-moonshot-edit').click(); await expect(page.getByTestId('provider-edit-validation-error-moonshot-edit')).toContainText('Invalid API key'); await page.getByTestId('provider-edit-key-input-moonshot-edit').fill('sk-good'); await expect(page.getByTestId('provider-edit-validation-error-moonshot-edit')).toHaveCount(0); await page.getByTestId('provider-edit-save-moonshot-edit').click(); await expect(page.getByTestId('provider-edit-save-moonshot-edit')).toHaveCount(0); }); });