Files
ServerKit/frontend/scripts/settings-ui-browser.mjs

237 lines
18 KiB
JavaScript

// Real settings components and SCSS; synthetic APIs never contact a provider.
import assert from 'node:assert/strict';
import { mkdir } from 'node:fs/promises';
import { createServer } from 'vite';
import { chromium } from 'playwright';
const server = await createServer({ server: { host: '127.0.0.1', port: 0, open: false } });
let browser;
try {
await server.listen();
await mkdir('test-results', { recursive: true });
browser = await chromium.launch({ headless: true });
for (const theme of ['light', 'dark']) {
const page = await browser.newPage({ viewport: { width: 1440, height: 1100 } });
const errors = [];
page.on('pageerror', (error) => errors.push(error.message));
await page.addInitScript((value) => {
localStorage.setItem('access_token', 'synthetic-browser-fixture');
document.addEventListener('DOMContentLoaded', () => { document.documentElement.dataset.theme = value; });
}, theme);
let enabled = false;
let rejectToggle = false;
let savedConnection;
let savedPreferences;
let sentChat;
let management = {
profiles: { utility: null, standard: null, advanced: null }, routing_enabled: false, strategy: 'balanced', pool: [],
fallback_enabled: false, fallbacks: [], budget_policy: 'hard_stop', max_tokens: 0, max_output_tokens: 2048,
max_tool_rounds: 6, max_tool_result_length: 4000, max_history_messages: 40, temperature: null, reasoning_effort: null,
monthly_limit_usd: 0, user_monthly_limit_usd: 0, workspace_monthly_limit_usd: 0, reservation_usd: 0.5,
};
const usage = { run_id: 'run-1', model: 'openai_compatible/team/cheap', connection_id: 'connection-1', profile: 'utility', workflow: 'summarize',
reason: 'Selected Utility task model', cost: 0.02, total_tokens: 120, cost_source: 'estimated', call_count: 1, errors: 0, duration_ms: 1200,
budget: { cost_remaining: 0.48, tokens_remaining: null },
attempts: [{ connection_id: 'connection-1', model: 'team/cheap', status: 'success', total_tokens: 120 }] };
let connection = { id: 'connection-1', name: 'Local gateway', provider: 'openai_compatible', model: 'team/default', config: { endpoint: 'http://gateway.test/v1' }, secrets_set: ['api_key'] };
await page.route('**/api/v1/**', async (route) => {
const path = new URL(route.request().url()).pathname;
const method = route.request().method();
let payload = {};
if (path.endsWith('/auth/setup-status')) payload = { needs_setup: false };
else if (path.endsWith('/auth/me')) payload = { user: { id: 1, username: 'operator', email: 'operator@example.test', role: 'admin', created_at: '2026-01-01' } };
else if (path.endsWith('/ai/status')) payload = { enabled, configured: true };
else if (path.endsWith('/ai/management/preview')) payload = { connection_id: connection.id, model: 'team/cheap', reason: 'Approved Utility candidate' };
else if (path.endsWith('/ai/management')) {
if (method === 'PUT') management = route.request().postDataJSON();
payload = management;
} else if (path.endsWith('/ai/usage')) payload = {
totals: { cost: 0.02, tokens: 120, runs: 1, errors: 0, unknown_cost_runs: 0, average_duration_ms: 1200 },
groups: { model: [{ label: usage.model, cost: 0.02, tokens: 120, runs: 1 }], profile: [{ label: 'utility', cost: 0.02, tokens: 120, runs: 1 }] },
runs: [{ id: 'run-1', model: usage.model, profile: 'utility', status: 'success', cost: 0.02, reason: usage.reason, created_at: '2026-09-08T12:00:00Z', usage }],
allowances: [{ scope: 'Panel', remaining: 9.98, limit: 10 }], has_more: false, next_offset: 50,
};
else if (path.endsWith('/ai/chat/stream')) {
sentChat = route.request().postDataJSON();
const events = [['open', { conversation_id: 'chat-1' }], ['run_start', usage], ['text_delta', { text: 'The server is healthy.' }], ['done', { conversation_id: 'chat-1', usage }]];
return route.fulfill({ contentType: 'text/event-stream', body: events.map(([name, data]) => `event: ${name}\ndata: ${JSON.stringify(data)}\n\n`).join('') });
}
else if (path.endsWith('/ai/settings')) {
if (method === 'PUT') {
const body = route.request().postDataJSON();
if ('enabled' in body) {
if (rejectToggle) return route.fulfill({ status: 500, json: { error: 'Synthetic save failure' } });
enabled = body.enabled;
}
}
payload = { enabled, connections: [connection], default_connection_id: connection.id, max_cost_usd: '0.5', pii_redaction: true, injection_detection: true };
} else if (path.endsWith('/ai/providers')) payload = { providers: [{ id: 'openai_compatible', label: 'OpenAI-Compatible', available: true, fields: [
{ name: 'endpoint', label: 'API base URL', type: 'url', required: true },
{ name: 'api_key', label: 'API key', type: 'text', secret: true },
] }] };
else if (path.endsWith('/ai/connections/probe')) payload = { models: ['team/default', 'team/reasoning', 'team/fast'], source: 'live',
model_details: [{ id: 'team/default' }, { id: 'team/reasoning', reasoning: true, tools: true, context_window: 128000, max_output_tokens: 8192, pricing: { input: 1, output: 3 } }, { id: 'team/fast' }] };
else if (path.endsWith('/ai/connections/connection-1')) {
savedConnection = route.request().postDataJSON();
connection = { ...connection, ...savedConnection, secrets_set: ['api_key'] };
payload = connection;
} else if (path.endsWith('/ai/connections')) payload = { connections: [connection], default_connection_id: connection.id };
else if (path.endsWith('/notifications/preferences')) {
if (method === 'PUT') savedPreferences = route.request().postDataJSON();
payload = { enabled: true, channels: ['email', 'slack'], severities: ['critical', 'warning'] };
} else if (path.endsWith('/api-analytics/overview')) payload = { total_requests: 0, avg_response_time_ms: 0, error_rate: 0, success_count: 0 };
else if (path.endsWith('/views')) payload = { views: [] };
await route.fulfill({ json: payload });
});
const visit = async (pane) => page.goto(`${server.resolvedUrls.local[0]}tests/browser/settings-ui.html?pane=${pane}`);
await visit('ai');
await page.locator('#ai-enabled').waitFor();
await page.locator('#ai-max-cost').fill('1.25');
await page.getByRole('button', { name: 'Discover models', exact: true }).click();
await page.getByRole('button', { name: 'Browse models (3)', exact: true }).click();
await page.getByLabel('Capability', { exact: true }).selectOption('reasoning');
await page.getByText('1 USD input / 3 USD output per 1M tokens', { exact: false }).waitFor();
assert.equal(await page.getByRole('option').filter({ hasText: 'team/default' }).count(), 0);
await page.getByPlaceholder('Search models…').fill('reasoning');
await page.getByRole('option', { name: /^team\/reasoning/ }).click();
assert.equal(await page.locator('#ai-model').inputValue(), 'team/reasoning');
await page.getByRole('button', { name: 'Save connection', exact: true }).click();
await page.getByText('Connection saved.', { exact: false }).waitFor();
assert.equal(savedConnection.model, 'team/reasoning');
assert.equal(savedConnection.config.api_key, undefined, 'Saved secret must not be copied to the browser');
assert.equal(await page.locator('#ai-max-cost').inputValue(), '1.25', 'Saving a connection erased unsaved limits');
await page.locator('#ai-enabled').click();
await page.locator('[data-testid="ai-state"][data-ready="true"]').waitFor({ state: 'attached' });
await page.reload();
await page.locator('#ai-enabled[data-state="checked"]').waitFor();
rejectToggle = true;
await page.locator('#ai-enabled').click();
await page.getByRole('alert').filter({ hasText: 'Synthetic save failure' }).waitFor();
assert.equal(await page.locator('#ai-enabled').getAttribute('data-state'), 'checked');
rejectToggle = false;
await page.locator('#ai-enabled').click();
await page.locator('[data-testid="ai-state"][data-enabled="false"][data-ready="false"]').waitFor({ state: 'attached' });
await page.screenshot({ path: `test-results/settings-ai-${theme}.png`, fullPage: true });
await page.getByRole('tab', { name: 'Task models and behavior' }).click();
await page.locator('#ai-profile-utility-connection').selectOption('connection-1');
await page.locator('#ai-profile-utility-model').fill('team/cheap');
await page.getByRole('button', { name: 'Add model candidate' }).first().click();
await page.locator('#ai-pool-0-model').fill('team/cheap');
await page.locator('#ai-tier-0').selectOption('budget');
await page.locator('#ai-routing_enabled').click();
await page.locator('#ai-routing-sample').fill('Summarize server health');
await page.getByRole('button', { name: 'Preview routing' }).click();
await page.getByText('Approved Utility candidate').waitFor();
await page.locator('#ai-max_output_tokens').fill('1024');
await page.getByRole('button', { name: 'Save AI management settings' }).click();
await page.getByText('AI management settings saved.').waitFor();
assert.equal(management.profiles.utility.model, 'team/cheap');
assert.equal(management.routing_enabled, true);
assert.equal(Number(management.max_output_tokens), 1024);
await page.screenshot({ path: `test-results/settings-ai-management-${theme}.png`, fullPage: true });
await page.reload();
await page.getByRole('tab', { name: 'Task models and behavior' }).click();
assert.equal(await page.locator('#ai-profile-utility-model').inputValue(), 'team/cheap');
await page.getByRole('tab', { name: 'AI usage', exact: true }).click();
await page.getByRole('heading', { name: 'Run details' }).waitFor();
await page.getByRole('button', { name: 'Details', exact: true }).click();
await page.getByRole('dialog').getByText('Cost source: estimated').waitFor();
await page.screenshot({ path: `test-results/settings-ai-usage-${theme}.png`, fullPage: true });
await page.keyboard.press('Escape');
enabled = true;
await visit('chat');
await page.locator('#ai-chat-workflow').selectOption('summarize');
await page.locator('#ai-chat-profile').selectOption('utility');
await page.getByRole('textbox', { name: 'Message the assistant' }).fill('Summarize server health');
await page.getByRole('textbox', { name: 'Message the assistant' }).press('Enter');
await page.locator('.sk-ai-message__usage summary').click();
await page.locator('.sk-ai-message__usage summary').getByText('0.02 USD', { exact: false }).waitFor();
await page.getByText('Conversation cost allowance remaining:', { exact: false }).waitFor();
assert.equal(sentChat.workflow, 'summarize');
assert.equal(sentChat.profile, 'utility');
assert(!sentChat.connection_id, 'A default connection override must not bypass task roles');
await page.screenshot({ path: `test-results/ai-chat-usage-${theme}.png`, fullPage: true });
await visit('notifications');
await page.getByRole('heading', { name: 'Notification Channels' }).waitFor();
assert.equal(await page.getByRole('switch', { name: 'Slack', exact: true }).count(), 0);
assert.equal(await page.getByRole('checkbox').count(), 0);
await Promise.all([
page.getByRole('button', { name: 'Send Test Notification' }).waitFor({ state: 'visible' }),
page.getByRole('button', { name: 'Save Preferences' }).waitFor({ state: 'visible' }),
]);
const testButton = await page.getByRole('button', { name: 'Send Test Notification' }).boundingBox();
const saveButton = await page.getByRole('button', { name: 'Save Preferences' }).boundingBox();
const footer = await page.locator('.settings-actions--footer').boundingBox();
assert(saveButton.x - testButton.x - testButton.width >= 12, 'Footer buttons must have a visible gap');
assert(Math.abs(saveButton.y - testButton.y) < 1, 'Desktop footer actions must align');
assert(Math.abs(saveButton.x + saveButton.width - footer.x - footer.width) < 2, 'Save belongs at the right edge');
await page.getByRole('switch', { name: 'Info', exact: true }).click();
await page.getByRole('button', { name: /Save Preferences/i }).click();
await page.getByText('Your notification preferences have been saved').waitFor();
assert(savedPreferences.severities.includes('info'));
assert(!savedPreferences.channels.includes('slack'), 'Personal preferences must not retain the nonfunctional Slack channel');
await page.screenshot({ path: `test-results/settings-notifications-${theme}.png`, fullPage: true });
await page.getByRole('tab', { name: 'Delivery settings' }).click();
await page.getByRole('button', { name: 'Slack', exact: true }).focus();
await page.keyboard.press('Enter');
await page.locator('.channel-config').waitFor();
for (const display of await page.locator('.channel-config .severity-toggle').evaluateAll((nodes) => nodes.map((node) => getComputedStyle(node).display))) assert.equal(display, 'flex', 'Form labels must not break severity toggle layout');
const surfaces = await page.locator('.channel-config').evaluate((node) => [getComputedStyle(node).backgroundColor, getComputedStyle(node.querySelector('input')).backgroundColor]);
assert.notEqual(surfaces[0], surfaces[1], 'Webhook fields disappear into the form surface');
await page.screenshot({ path: `test-results/settings-delivery-${theme}.png`, fullPage: true });
await visit('profile');
await page.locator('.settings-form input').first().waitFor();
assert.notEqual(await page.locator('.settings-form').evaluate((node) => getComputedStyle(node).backgroundColor), 'rgba(0, 0, 0, 0)');
await page.screenshot({ path: `test-results/settings-profile-${theme}.png`, fullPage: true });
await visit('api');
await page.getByRole('button', { name: 'Create Key' }).waitFor();
await page.getByText('No API keys yet.').waitFor();
for (const border of await page.locator('.settings-card > .empty-state').evaluateAll((nodes) => nodes.map((node) => getComputedStyle(node).borderTopWidth))) assert.equal(border, '0px');
const create = await page.getByRole('button', { name: 'Create Key' }).boundingBox();
const webhook = await page.getByRole('button', { name: 'Add Webhook' }).boundingBox();
assert(create.x > 800 && webhook.x > 800, 'Create actions should share the right side of section headers');
await page.screenshot({ path: `test-results/settings-api-${theme}.png`, fullPage: true });
await visit('controls');
await page.locator('#native-field').waitFor();
for (const selector of ['#native-field', '#native-select']) {
const field = await page.locator(selector).evaluate((node) => ({ height: node.getBoundingClientRect().height, bg: getComputedStyle(node).backgroundColor }));
assert(field.height >= 36, 'FormField left a native control unstyled');
assert.notEqual(field.bg, 'rgba(0, 0, 0, 0)');
}
assert((await page.getByRole('checkbox').boundingBox()).width < 32);
const chrome = await page.locator('.fixture-chrome > *').evaluateAll((nodes) => nodes.map((node) => getComputedStyle(node).backgroundColor));
assert.equal(new Set(chrome).size, 1, 'Shell bars must share one background token');
await page.setViewportSize({ width: 390, height: 844 });
for (const pane of ['ai', 'profile', 'notifications', 'api']) {
await visit(pane);
await page.locator('.settings-card, .settings-form').first().waitFor();
assert(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth), `${pane} overflows on mobile`);
if (pane === 'ai') {
for (const tab of ['Task models and behavior', 'AI usage']) {
await page.getByRole('tab', { name: tab, exact: true }).click();
assert(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth), `${tab} overflows on mobile`);
}
await page.screenshot({ path: `test-results/settings-ai-mobile-${theme}.png`, fullPage: true });
}
if (pane === 'notifications') {
await page.getByRole('button', { name: 'Save Preferences' }).waitFor();
await page.getByRole('button', { name: 'Send Test Notification' }).waitFor();
const testAction = await page.getByRole('button', { name: 'Send Test Notification' }).boundingBox();
const saveAction = await page.getByRole('button', { name: 'Save Preferences' }).boundingBox();
assert(testAction && saveAction, 'Both mobile footer buttons must be visible');
assert(saveAction.y - testAction.y - testAction.height >= 12, 'Mobile footer buttons should stack with a gap');
}
}
assert.deepEqual(errors, []);
await page.close();
console.log(`Settings ${theme}: model discovery, secret preservation, enable persistence/failure, notifications, surfaces, shell and mobile passed`);
}
} finally {
await browser?.close();
await server.close();
}