test: add frontend quality gate
This commit is contained in:
parent
c79a2efaab
commit
a714ea0273
@ -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
|
||||||
|
|||||||
3
TODO.md
3
TODO.md
@ -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.
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
36
scripts/check-frontend.mjs
Normal file
36
scripts/check-frontend.mjs
Normal 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.`);
|
||||||
Loading…
Reference in New Issue
Block a user