7fd5fcd9af
- Bring-your-own-key assistant config in a dedicated persisted store (src/stores/assistantStore.ts); partialize drops the key from localStorage when 'remember' is off. Key lives in localStorage only, so it's auto-excluded from Dexie backups. - Provider-agnostic LLM client (src/lib/llm) over fetch, no SDK: Anthropic Messages + OpenAI-compatible Chat Completions (covers OpenAI/OpenRouter/ Ollama/LM Studio via custom base URL). Structured JSON output parsed + Zod-validated; timeouts; typed error kinds; key never leaked in errors. - Settings 'Assistant (AI)' section with provider/model/baseUrl/key, remember + enabled toggles, and Test connection. - CSP connect-src widened to any https + localhost for the BYO endpoint. 10 client unit tests; settings e2e asserts config persists and the key is never present in an exported backup. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
53 lines
2.1 KiB
TypeScript
53 lines
2.1 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
|
import fs from 'node:fs';
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto('/');
|
|
await page.evaluate(async () => {
|
|
indexedDB.deleteDatabase('ttrpg-manager');
|
|
localStorage.clear();
|
|
});
|
|
await page.reload();
|
|
});
|
|
|
|
test('command palette navigates', async ({ page }) => {
|
|
await page.keyboard.press('Control+k');
|
|
const input = page.getByLabel('Command search');
|
|
await expect(input).toBeVisible();
|
|
await input.fill('Dice');
|
|
await input.press('Enter');
|
|
await expect(page.getByRole('heading', { name: 'Dice' })).toBeVisible();
|
|
});
|
|
|
|
test('settings loads the sample campaign', async ({ page }) => {
|
|
await page.getByLabel('Settings').click();
|
|
await expect(page.getByRole('heading', { name: 'Settings' })).toBeVisible();
|
|
await page.getByRole('button', { name: 'Load sample campaign' }).click();
|
|
await expect(page.getByRole('heading', { name: 'Sample: Lost Mine' })).toBeVisible();
|
|
// sample seeded characters (use the nav link, not the dashboard card)
|
|
await page.getByLabel('Primary').getByRole('link', { name: 'Characters' }).click();
|
|
await expect(page.getByText('Lia the Brave')).toBeVisible();
|
|
});
|
|
|
|
test('assistant config persists and the key is never exported', async ({ page }) => {
|
|
const SECRET = 'sk-test-DO-NOT-EXPORT-789';
|
|
await page.getByLabel('Settings').click();
|
|
await page.getByLabel('Enable AI assistant').check();
|
|
await page.getByLabel('API key', { exact: true }).fill(SECRET);
|
|
await page.getByLabel('Model').fill('claude-opus-4-8');
|
|
|
|
// Persists across reload (rememberKey defaults on)
|
|
await page.reload();
|
|
await page.getByLabel('Settings').click();
|
|
await expect(page.getByLabel('Enable AI assistant')).toBeChecked();
|
|
await expect(page.getByLabel('API key', { exact: true })).toHaveValue(SECRET);
|
|
|
|
// The backup export (Dexie only) must not contain the localStorage-held key
|
|
const downloadPromise = page.waitForEvent('download');
|
|
await page.getByRole('button', { name: 'Export backup' }).click();
|
|
const download = await downloadPromise;
|
|
const path = await download.path();
|
|
const content = fs.readFileSync(path, 'utf8');
|
|
expect(content).not.toContain(SECRET);
|
|
});
|