diff --git a/architecture/qa.md b/architecture/qa.md index 57ec601..67806ac 100644 --- a/architecture/qa.md +++ b/architecture/qa.md @@ -51,3 +51,6 @@ Playwright `baseURL` defaults to the live container (`http://localhost:7890`, ov - `check:frontend`: duplicate top-level `say` in `static/js/voice-archetypes.js` and `static/js/magazine.js`. - `test_epub_export.EndpointTests.test_downloads_a_valid_epub`. - Secret scan: tracked `scratch/test_openai.py` and `scratch/test_google_imagen.py` contain key-like literals; rotate if real and remove from history/tracking. + +## Python interpreter +`scripts/qa.mjs` runs Python through the first interpreter that can `import fastapi`: `$PYTHON`, `python3` on PATH, `~/miniforge3/bin/python3`, `/usr/bin/python3`. Agents started by the multica daemon often see `/usr/bin/python3` first (no fastapi), which made their unit tests fail on import; `npm run qa` now works there too. diff --git a/architecture/sonar-refactorer.md b/architecture/sonar-refactorer.md index d42d567..07a310f 100644 --- a/architecture/sonar-refactorer.md +++ b/architecture/sonar-refactorer.md @@ -29,7 +29,7 @@ One GitHub issue per function (one Sonar location), not per rule: a complexity r ## sonar-refactorer instructions (runbook) Goal: reduce the cognitive complexity of the ONE function named in the webhook payload (`body`, `github_issue_url`) to 15 or below WITHOUT changing behaviour. -Context: private repo TTS Voice Creator. Read CLAUDE.md and PROJECT_MAP.md first. Python tests use `unittest` with an isolated HOME/CONFIG_DIR (see `isolatedPyEnv` in `scripts/qa.mjs`); JS checks use `node scripts/check-frontend.mjs`; browser (Playwright) specs run against the live app and cannot see worktree edits, so say which checks you did not run. Specs and tests mock the API and import from `tests/_guard.js`. +Context: private repo TTS Voice Creator. Read CLAUDE.md and PROJECT_MAP.md first. Python tests use `unittest` with an isolated HOME/CONFIG_DIR (see `isolatedPyEnv` in `scripts/qa.mjs`); JS checks use `node scripts/check-frontend.mjs`; browser (Playwright) specs run against the live app and cannot see worktree edits, so say which checks you did not run. Python: `/usr/bin/python3` has no fastapi; use `/home/sparky/miniforge3/bin/python3` (or `npm run qa -- --only pyunit`, which picks the right interpreter) and never report a failure that comes from a missing module as a test failure. Specs and tests mock the API and import from `tests/_guard.js`. Steps: 1. Find the function by name (line numbers in Sonar are often stale). Read it and all its callers (`grep` the whole repo; JS files share globals). diff --git a/architecture/sonar-triage.md b/architecture/sonar-triage.md index ef8194d..d22b26d 100644 --- a/architecture/sonar-triage.md +++ b/architecture/sonar-triage.md @@ -56,7 +56,7 @@ npm run sonar:triage -- --apply --fix-small Local fix commits: `git reset --hard ` / `git revert`. Issues: close the GitHub issue and the multica issue. Nothing is pushed by the script. ## sonar-fixer agent instructions -Fix only the given Sonar issue. Read CLAUDE.md and PROJECT_MAP.md first. Run `node scripts/check-frontend.mjs` and Python `unittest` with an isolated HOME; tests mock the API and import from tests/_guard.js. Browser (Playwright) specs run against the live app and cannot see worktree edits: say which checks you did not run. One local Conventional Commit `fix(sonar): ...`; never push; never touch secrets; if a design decision is needed, stop and comment. +Fix only the given Sonar issue. Read CLAUDE.md and PROJECT_MAP.md first. Run `node scripts/check-frontend.mjs` and Python `unittest` with an isolated HOME; tests mock the API and import from tests/_guard.js. Browser (Playwright) specs run against the live app and cannot see worktree edits: say which checks you did not run. Python: `/usr/bin/python3` has no fastapi; use `/home/sparky/miniforge3/bin/python3` (or `npm run qa -- --only pyunit`, which picks the right interpreter) and never report a failure that comes from a missing module as a test failure. One local Conventional Commit `fix(sonar): ...`; never push; never touch secrets; if a design decision is needed, stop and comment. When done: 1. Comment on the multica issue: commit SHA, files changed, what you verified, checks NOT run, findings skipped as false positives (with reason). diff --git a/scripts/qa.mjs b/scripts/qa.mjs index 5122fde..fbc1f47 100644 --- a/scripts/qa.mjs +++ b/scripts/qa.mjs @@ -23,7 +23,14 @@ const isolatedPyEnv = () => { const tmp = join(dir, 'tmp'); mkdirSync(tmp); return { env: { ...process.env, HOME: dir, CONFIG_DIR: dir, TMPDIR: tmp }, cleanup: () => rmSync(dir, { recursive: true, force: true }) }; }; -const pyMod = (m) => sh('python3', ['-m', m, '--version']).status === 0; +// The project's Python needs fastapi etc. Agents (multica daemon) can see /usr/bin/python3 first, which has none of it, +// so pick the first interpreter that imports fastapi: $PYTHON, python3 on PATH, ~/miniforge3, /usr/bin. +const PY = (() => { + const home = process.env.HOME || ''; + const cands = [process.env.PYTHON, 'python3', home && `${home}/miniforge3/bin/python3`, '/usr/bin/python3'].filter(Boolean); + return cands.find((c) => sh(c, ['-c', 'import fastapi']).status === 0) || 'python3'; +})(); +const pyMod = (m) => sh(PY, ['-m', m, '--version']).status === 0; const tail = (r, n = 15) => ((r.stdout || '') + (r.stderr || '')).trim().split('\n').slice(-n).join('\n'); // A stage returns {status:'PASS'|'FAIL'|'SKIPPED', note} const pass = (note = '') => ({ status: 'PASS', note }); @@ -46,15 +53,15 @@ const stages = [ } let r = sh('npm', ['run', '--silent', 'check:frontend']); if (r.status) return result(r, 'check:frontend'); - r = sh('python3', ['-m', 'compileall', '-q', 'core', 'routes', 'scripts', 'server.py', 'ttsvc_cli.py']); + r = sh(PY, ['-m', 'compileall', '-q', 'core', 'routes', 'scripts', 'server.py', 'ttsvc_cli.py']); if (r.status) return result(r, 'compileall'); - if (pyMod('ruff')) { r = sh('python3', ['-m', 'ruff', 'check', 'core', 'routes', 'scripts', 'server.py']); if (r.status) return result(r, 'ruff'); return pass('with ruff'); } + if (pyMod('ruff')) { r = sh(PY, ['-m', 'ruff', 'check', 'core', 'routes', 'scripts', 'server.py']); if (r.status) return result(r, 'ruff'); return pass('with ruff'); } return pass('ruff not installed, skipped'); }], ['types', 'type check', () => skip('no type checker configured')], ['pyunit', 'python unittest', () => { const { env, cleanup } = isolatedPyEnv(); - try { return result(inherit('python3', ['-m', 'unittest', 'discover', '-s', 'tests', '-p', 'test_*.py'], { env }), 'see unittest output above'); } + try { return result(inherit(PY, ['-m', 'unittest', 'discover', '-s', 'tests', '-p', 'test_*.py'], { env }), 'see unittest output above'); } finally { cleanup(); } }], ['nodeunit', 'node unit tests', () => result(inherit('npm', ['run', '--silent', 'test:unit']))], @@ -75,7 +82,7 @@ const stages = [ else if (/ENOTFOUND|EAI_AGAIN|ECONNREFUSED|ETIMEDOUT|network|audit endpoint/i.test(out) || a.error) notes.push('npm audit SKIPPED (offline)'); else { bad = true; console.log(tail(a, 20)); notes.push('npm audit FOUND high+'); } if (existsSync(join(root, 'requirements.txt')) && pyMod('pip_audit')) { - const p = sh('python3', ['-m', 'pip_audit', '-r', 'requirements.txt', '--progress-spinner', 'off'], { timeout: 120000 }); + const p = sh(PY, ['-m', 'pip_audit', '-r', 'requirements.txt', '--progress-spinner', 'off'], { timeout: 120000 }); const po = (p.stdout || '') + (p.stderr || ''); if (p.status === 0) notes.push('pip-audit ok'); else if (/connection|resolve|network|timed out|ConnectionError/i.test(po) || p.error) notes.push('pip-audit SKIPPED (offline)'); @@ -105,11 +112,11 @@ const stages = [ if (!tok || !host) return skip('set SONAR_HOST_URL and SONAR_TOKEN in the shell or CI secrets'); if (!have('sonar-scanner')) return skip('tool missing: sonar-scanner'); // Python coverage for new-code gate (optional: only if `coverage` is installed). JS coverage is not collected (see architecture/qa.md). - if (sh('python3', ['-c', 'import coverage']).status === 0) { + if (sh(PY, ['-c', 'import coverage']).status === 0) { const { env, cleanup } = isolatedPyEnv(); try { - const c = inherit('python3', ['-m', 'coverage', 'run', '-m', 'unittest', 'discover', '-s', 'tests', '-p', 'test_*.py'], { env }); - if (c.status === 0) sh('python3', ['-m', 'coverage', 'xml', '-o', 'coverage.xml']); + const c = inherit(PY, ['-m', 'coverage', 'run', '-m', 'unittest', 'discover', '-s', 'tests', '-p', 'test_*.py'], { env }); + if (c.status === 0) sh(PY, ['-m', 'coverage', 'xml', '-o', 'coverage.xml']); } finally { cleanup(); } } else console.log('coverage: SKIPPED (module missing; `pip install coverage` to report Python coverage to Sonar)'); const r = sh('sonar-scanner', ['-Dsonar.qualitygate.wait=true', '-Dsonar.qualitygate.timeout=300'], { stdio: ['ignore', 'pipe', 'pipe'] });