test: add frontend quality gate

This commit is contained in:
mARTin-B78 2026-09-04 09:26:01 +02:00
parent c79a2efaab
commit a714ea0273
5 changed files with 43 additions and 3 deletions

View File

@ -42,6 +42,8 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
secrets, audio, and image payloads are excluded from persistence. secrets, audio, and image payloads are excluded from persistence.
- **Bounded Durable Job Retention.** Completed background-job metadata is pruned oldest-first at a fixed limit so - **Bounded Durable Job Retention.** Completed background-job metadata is pruned oldest-first at a fixed limit so
restart-safe status tracking cannot become an unbounded database growth path. restart-safe status tracking cannot become an unbounded database growth path.
- **Frontend Quality Gate.** Added `npm run check:frontend` to validate every manifest module for syntax errors,
duplicate manifest entries, and top-level declaration collisions before bundling.
- **STT Cancellation Safety.** Queued transcription cancels before consuming the single STT slot, and timed-out - **STT Cancellation Safety.** Queued transcription cancels before consuming the single STT slot, and timed-out
requests keep the device slot held until the underlying worker actually exits. requests keep the device slot held until the underlying worker actually exits.
- **Credential Boundary Review.** Provider credentials are now documented as server-only; only the explicitly - **Credential Boundary Review.** Provider credentials are now documented as server-only; only the explicitly

View File

@ -104,7 +104,8 @@ the first block before investing in architecture or polish.
- [ ] Replace broad globals with namespaces such as `window.TTVC.audiobook`. - [ ] Replace broad globals with namespaces such as `window.TTVC.audiobook`.
- [x] Generate the feature-module list for `loader.js` and `scripts/minify.mjs` from one shared manifest; - [x] Generate the feature-module list for `loader.js` and `scripts/minify.mjs` from one shared manifest;
larger responsibility splits remain. larger responsibility splits remain.
- [ ] Add linting and duplicate-function checks. - [x] Add a dependency-free frontend syntax and top-level declaration collision gate (`npm run check:frontend`);
a full style/lint policy remains a follow-up.
- [ ] Extract Rehearser voice matching and voice design. - [ ] Extract Rehearser voice matching and voice design.
- [ ] Extract Audiobook parsing and casting. - [ ] Extract Audiobook parsing and casting.
- [ ] Split `style.css` by feature. - [ ] Split `style.css` by feature.

View File

@ -1 +1 @@
1.23.0 1.23.1

View File

@ -8,7 +8,8 @@
"test:headed": "playwright test --headed", "test:headed": "playwright test --headed",
"test:ui": "playwright test --ui", "test:ui": "playwright test --ui",
"install:browsers": "playwright install chromium", "install:browsers": "playwright install chromium",
"minify": "node scripts/minify.mjs" "minify": "node scripts/minify.mjs",
"check:frontend": "node scripts/check-frontend.mjs"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.48.0", "@playwright/test": "^1.48.0",

View File

@ -0,0 +1,36 @@
// Fast, dependency-free frontend gate. The application deliberately shares one
// global scope, so duplicate top-level declarations are a runtime collision.
// Nested callbacks are ignored; vendor/PDF bundles are outside the manifest.
import { execFileSync } from 'node:child_process';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
const root = join(fileURLToPath(new URL('.', import.meta.url)), '..');
const modules = JSON.parse(readFileSync(join(root, 'static', 'module-manifest.json'), 'utf8'));
const seenModules = new Set();
const declarations = new Map();
const declarationPattern = /^(?:async\s+)?(?:function|class|const|let|var)\s+([A-Za-z_$][\w$]*)/gm;
for (const name of modules) {
if (seenModules.has(name)) throw new Error(`duplicate module in manifest: ${name}`);
seenModules.add(name);
const file = join(root, 'static', 'js', name + '.js');
try {
execFileSync(process.execPath, ['--check', file], { stdio: 'pipe' });
} catch (error) {
throw new Error(`JavaScript syntax check failed for ${file}: ${error.stderr?.toString().trim() || error.message}`);
}
const source = readFileSync(file, 'utf8');
for (const match of source.matchAll(declarationPattern)) {
const files = declarations.get(match[1]) || [];
files.push(file);
declarations.set(match[1], files);
}
}
const collisions = [...declarations.entries()].filter(([, files]) => files.length > 1);
if (collisions.length) {
throw new Error('duplicate top-level declarations:\n' + collisions.map(([name, files]) => ` ${name}: ${files.join(', ')}`).join('\n'));
}
console.log(`Frontend gate passed: ${modules.length} modules, no syntax errors or top-level declaration collisions.`);