diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e23c17..d841645 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,8 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ยท versioned wi headers so browser and intermediary caches cannot retain the client-visible access-key handoff. - **Navigation Race Protection.** Rehearser phase requests now survive late feature-module initialization, and overlapping character-library refreshes cannot overwrite a newer table/card view. +- **Constrained Browser Smoke Coverage.** Added a 4 GB `navigator.deviceMemory` regression that verifies shared + batch work is capped at two concurrent operations. - **API Boundary Hardening.** External API authentication is enabled by default, request and mutation limits are enforced, and settings responses redact provider credentials for non-browser callers. - **Server-side Provider Credentials.** Provider keys are now masked even for same-origin Settings responses, are diff --git a/TODO.md b/TODO.md index 358a7b2..6325935 100644 --- a/TODO.md +++ b/TODO.md @@ -60,7 +60,8 @@ the first block before investing in architecture or polish. - [x] **M โ€” Add explicit cache invalidation and eviction.** Fish.audio search responses, persisted synthesized audio, image-generation job mappings, and browser object URLs have bounded cleanup. - [ ] **M โ€” Add constrained-device validation.** Test with Chrome CPU/memory throttling, - a 4 GB VM/container, and measure browser, Docker and temporary-disk usage. + a 4 GB VM/container, and measure browser, Docker and temporary-disk usage. Automated browser emulation now proves + the 4 GB shared-pool cap; physical VM/container and resource measurements remain. - [ ] **L โ€” Add background jobs and bounded queues.** Give voice design, transcription, casting and image generation IDs, progress, cancellation, timeout, retry and idempotency support. diff --git a/static/js/utils.js b/static/js/utils.js index 2f525a7..eb5bd64 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -367,6 +367,7 @@ async function runPool(items, worker, concurrency = 4, onProgress) { } await Promise.all(Array.from({ length: Math.min(effectiveConcurrency, arr.length) }, run)); } +window.runPool = runPool; async function copyText(text) { try { await navigator.clipboard.writeText(text); diff --git a/tests/constrained.spec.js b/tests/constrained.spec.js new file mode 100644 index 0000000..f503a70 --- /dev/null +++ b/tests/constrained.spec.js @@ -0,0 +1,22 @@ +// @ts-check +// Lightweight constrained-device regression: the browser's 4 GB profile must +// reduce shared batch concurrency before multiple audio/image operations run. +const { test, expect } = require('@playwright/test'); + +test('4 GB browser profile caps shared batch concurrency at two', async ({ page }) => { + await page.goto('/', { waitUntil: 'domcontentloaded' }); + await page.waitForFunction(() => typeof window.runPool === 'function', null, { timeout: 15_000 }); + const maxActive = await page.evaluate(async () => { + Object.defineProperty(navigator, 'deviceMemory', { configurable: true, value: 4 }); + let active = 0; + let max = 0; + await window.runPool(Array.from({ length: 8 }, (_, i) => i), async () => { + active += 1; + max = Math.max(max, active); + await new Promise(resolve => setTimeout(resolve, 15)); + active -= 1; + }, 4); + return max; + }); + expect(maxActive).toBeLessThanOrEqual(2); +});