test: stabilize browser smoke fixtures

This commit is contained in:
mARTin-B78 2026-09-04 08:30:46 +02:00
parent 3c6245a5f8
commit 2cda7df636
3 changed files with 25 additions and 14 deletions

23
TODO.md
View File

@ -86,10 +86,11 @@ the first block before investing in architecture or polish.
- [x] Establish a Playwright baseline before large refactors (app shell, bundle, i18n, clone tabs, and Rehearser parsing; installed Chromium supported).
- [x] Add explicit Docker memory limits where appropriate; the web container defaults to an 8 GB limit and 2 GB reservation with environment overrides.
- [ ] Limit upload sizes, temporary files, and concurrent jobs.
- [x] Limit upload sizes, temporary files, and concurrent jobs. Upload/request caps, bounded caches, and bounded
TTS/STT/image-generation pools are implemented; durable background-job conversion remains a separate P1 item.
- [x] Use atomic writes for `.meta.json` files.
- [ ] Create automatic backups before bulk deletes, voice moves, and recasts.
- [ ] Add request-size limits, rate limits, and authentication for destructive/API endpoints.
- [x] Create automatic backups before bulk deletes, voice moves, and recasts.
- [x] Add request-size limits, rate limits, and authentication for destructive/API endpoints.
## P1 — Modular frontend architecture
@ -120,21 +121,23 @@ the first block before investing in architecture or polish.
- [ ] Introduce background jobs for voice design, transcription, casting, and image generation.
- [ ] Give every job an ID, progress, cancellation, timeout, retry, and idempotency key.
- [ ] Prevent duplicate generation when a user clicks twice.
- [x] Prevent duplicate generation when a user clicks twice for image, voice-design, and casting submissions.
- [ ] Centralize provider health checks and error handling.
- [ ] Limit concurrency to 1–2 image generations, 1–2 voice generations, and 1 STT request per user. Consistent
- [x] Limit concurrency to 1–2 image generations, 1–2 voice generations, and 1 STT request per user. Consistent
portrait/reference rendering, preview/style/seed-finder synthesis, and the OpenAI-compatible speech proxy are
capped at two active jobs; STT transcription and benchmarks are capped at one active job.
- [ ] Use bounded server queues.
- [x] Use bounded server queues for the implemented synchronous generation paths; durable background-job queues remain
a separate outstanding item above.
## P0 — Security
- [ ] Never expose API keys to frontend JavaScript or logs. Provider keys are masked on read and absent from browser
storage; the deliberate external-client key display and transient entry path still need a final design review.
- [ ] Validate uploaded filenames and paths.
- [ ] Strictly validate custom URLs to prevent SSRF.
- [ ] Add request-size limits, rate limits, and authentication for destructive/API endpoints.
- [ ] Run containers as non-root with dropped capabilities and read-only filesystems where practical.
- [x] Validate uploaded filenames and paths.
- [x] Strictly validate custom URLs to prevent SSRF, with private destinations available only when explicitly allowed
for local provider/container operation.
- [x] Add request-size limits, rate limits, and authentication for destructive/API endpoints.
- [x] Run containers as non-root with dropped capabilities and read-only filesystems where practical.
- [x] Isolate Docker socket access behind a minimal privileged service.
## P0 — Data integrity and recovery

View File

@ -24,6 +24,9 @@ async function navTo(page, section) {
}
test('parses a screenplay and builds the cast', async ({ page }) => {
await page.route('**/api/voices*', route => route.fulfill({
json: [{ id: 'smoke_voice', name: 'Smoke voice', enabled: true }],
}));
await page.goto('/', { waitUntil: 'domcontentloaded' });
await navTo(page, 's-rehearser');
await page.waitForFunction(() => typeof window.navRehearserPhase === 'function', null, { timeout: 15_000 });

View File

@ -47,12 +47,17 @@ test('clone tabs switch and show one panel at a time', async ({ page }) => {
await page.goto('/', { waitUntil: 'domcontentloaded' });
await navTo(page, 's-clone');
await page.waitForFunction(() => typeof window._cloneScheduleAutoSave === 'function', null, { timeout: 15_000 });
await expect(page.locator('.clone-src-tab[data-src="url"]')).toBeVisible({ timeout: 10_000 });
// The loader applies the persisted startup section during its final reveal;
// wait for that initialization to finish before changing sections.
await page.waitForSelector('#app-skeleton', { state: 'detached', timeout: 15_000 });
await page.evaluate(() => window.navTo('s-clone'));
const clone = page.locator('#s-clone');
await expect(clone.locator('.clone-src-tab[data-src="url"]')).toBeVisible({ timeout: 10_000 });
// The app uses a fixed bottom status bar; dispatching the real DOM click
// avoids a viewport-dependent overlap while still exercising the handler.
await page.locator('.clone-src-tab[data-src="url"]').evaluate((el) => el.click());
await expect(page.locator('#clone-src-url')).toBeVisible();
await expect(page.locator('#clone-src-mic')).toBeHidden();
await clone.locator('.clone-src-tab[data-src="url"]').evaluate((el) => el.click());
await expect(clone.locator('#clone-src-url')).toBeVisible();
await expect(clone.locator('#clone-src-mic')).toBeHidden();
});
test('PWA manifest and service worker are served', async ({ page, request }) => {