3.9 KiB
QA: npm run qa
Developer-facing. Single entry point: scripts/qa.mjs (Node, no dependencies). Every stage is timed; a summary table is printed and the exit code is non-zero if any stage FAILs. SKIPPED never fails the run. Nothing is ever installed automatically.
npm run qa -- --fast # everything except Playwright
npm run qa # full, incl. Playwright (desktop + iphone)
npm run qa -- --only e2e --grep voice-provenance
npm run qa -- --sonar # add the SonarQube stage
npm run qa -- --list
Stages
| id | what | skipped when |
|---|---|---|
| format | formatter check | always for now (no formatter configured) |
| lint | node --check over tracked static/js, scripts, tests mjs; npm run check:frontend; python compileall; ruff if installed |
ruff missing -> noted only |
| types | type check | no type checker configured |
| pyunit | unittest discover -s tests -p 'test_*.py' with a temp HOME/CONFIG_DIR (avoids the /home/app permission error, never touches live data) |
never |
| nodeunit | npm run test:unit |
never |
| e2e | Playwright, projects desktop + iphone; --grep supported |
--fast |
| a11y | axe-core specs | axe-core not installed (recommend npm i -D @axe-core/playwright, add tests/a11y.spec.js) |
| security | npm audit --audit-level=high, pip-audit (if installed + requirements.txt), secret scan (gitleaks if present, else built-in regexes over tracked files) |
audits: network failure -> SKIPPED, not FAIL |
| build | npm run minify, fails if static/dist/main.min.js changed (stale bundle; commit the regenerated file) |
never |
| perf | DOM budget is tests/constrained.spec.js, runs inside e2e |
always (no separate stage) |
| sonar | sonar-scanner with sonar-project.properties |
no --sonar, or SONAR_HOST_URL/SONAR_TOKEN unset, or scanner missing |
Test isolation
Playwright baseURL defaults to the live container (http://localhost:7890, override with BASE_URL). The specs mock every /api/** route (the live server only serves static files and the page), so no live data is written; use a throwaway instance via BASE_URL when in doubt. --fast never touches it. The secret scan prints only file:line pattern-name, never the match.
Isolation and coverage
pyunitand the coverage run each get a freshmkdtempHOME/CONFIG_DIR/TMPDIR (isolatedPyEnvinscripts/qa.mjs), removed afterwards. Stages run strictly sequentially, so e2e and pyunit never overlap.- Sonar stage: if
python3 -c 'import coverage'works, it runscoverage run -m unittest discover ...and writescoverage.xml(read viasonar.python.coverage.reportPaths). Otherwise it prints SKIPPED; install withpip install coverage. - JS coverage is NOT collected. Playwright-driven coverage needs c8/V8 instrumentation of the served bundle (e.g.
page.coverage+v8-to-istanbul->sonar.javascript.lcov.reportPaths). Owner decision pending.
SonarQube / secrets
SONAR_HOST_URL and SONAR_TOKEN are read from the environment only (shell or CI secrets). They are never written to files, argv or output (the scanner reads the token itself; output is redacted). The quality gate is awaited via -Dsonar.qualitygate.wait=true; no separate web API call is made.
CI
.github/workflows/sonarqube.yml already runs compile/unittest/frontend/build steps and the Sonar action (token from ${{ secrets.SONAR_TOKEN }}). An advisory (continue-on-error) npm run qa -- --fast step was added. Make it blocking once the pre-existing failures below are fixed.
Known pre-existing failures (2026-10-04)
check:frontend: duplicate top-levelsayinstatic/js/voice-archetypes.jsandstatic/js/magazine.js.test_epub_export.EndpointTests.test_downloads_a_valid_epub.- Secret scan: tracked
scratch/test_openai.pyandscratch/test_google_imagen.pycontain key-like literals; rotate if real and remove from history/tracking.