From 69c33526f42bbf17d06f8a61f6e132495539d9de Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Tue, 6 Oct 2026 11:58:13 +0200 Subject: [PATCH] fix(qa): run the container tests on a writable copy and add node to the test image Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 2 +- architecture/qa.md | 9 ++++----- docker/Dockerfile.test | 2 ++ scripts/qa.mjs | 4 +++- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9dab88b..339c0dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ยท versioned wi ## [Unreleased] ### Added -- **QA: Python tests in a throwaway container** (`npm run qa -- --only pyunit --container`): runs the unit tests on production's Python 3.11 in a no-network, read-only container built from the app image (`docker/Dockerfile.test`, adds only `httpx2`); opt-in, SKIPPED when docker or the image is missing. +- **QA: Python tests in a throwaway container** (`npm run qa -- --only pyunit --container`): runs the unit tests on production's Python 3.11 in a no-network, read-only container built from the app image on a writable throwaway copy of the read-only mounted repo (`docker/Dockerfile.test` adds only `httpx2` and `nodejs`); opt-in, SKIPPED when docker or the image is missing. - **Sonar triage `--drain`:** works through open agent issues one at a time, fires the multica webhook, waits for the agent's answer on GitHub and resends once if nothing arrives; stops on a stalled issue. - **Sonar triage can fire a multica webhook autopilot** after each GitHub issue (URL from `~/.config/multica-webhook` or `MULTICA_WEBHOOK_URL`, passed to curl on stdin only); the multica CLI stays as fallback. ### Changed diff --git a/architecture/qa.md b/architecture/qa.md index d9106e1..3a1464a 100644 --- a/architecture/qa.md +++ b/architecture/qa.md @@ -62,9 +62,8 @@ Why: production runs Python 3.11 with the dependencies installed from `requireme Setup (once, local only, nothing pushed): `docker build -f docker/Dockerfile.test -t tts-voice-creator-test:local docker`. `docker/Dockerfile.test` is `FROM` the app image plus `httpx2` (needed by `starlette.testclient`, not shipped in production). Without docker or the image the stage reports SKIPPED with the build command. -Container: `docker run --rm --network none --read-only --tmpfs /tmp:rw,exec --tmpfs /home/app:rw`, `HOME`/`CONFIG_DIR`/`TMPDIR` under `/tmp`, repo mounted read-only at `/work`, user `app`, entrypoint `python3`. +Container: `docker run --rm --network none --read-only --tmpfs /tmp:rw,exec --tmpfs /home/app:rw`, `HOME`/`CONFIG_DIR`/`TMPDIR` under `/tmp`, user `app`. The repo is mounted read-only at `/src`; the tests run in a writable throwaway copy at `/tmp/work` (tar copy without `.git`, `node_modules`, `config`, `exports`, `logs`, `attachments`, `.multica`, the nested project dir; `config/regression_snapshots` is added back because a test reads it). Some tests write to the tree (e.g. `os.utime`), so the original is never written. -Known differences from the host run (1086 tests, all OK on host; container: 2 errors, 3 skipped): -- `test_asset_versions...build_id_changes_when_a_script_changes`: the test calls `os.utime` on a repo file; fails on the read-only mount (the mount is deliberate). -- `test_character_identity...browser_and_server_compute_identical_fingerprints`: needs `node`, which the app image does not contain. -Neither is a Python 3.11 vs 3.12 behaviour difference; the other tests pass on 3.11. Coverage (`--sonar`) still uses the host interpreter. +The test image also has `nodejs` (Debian package, aarch64) for the browser/server fingerprint test, and `httpx2`. + +Result: 1086 tests, OK, 3 skipped in the container (host: 1086 OK). Skips: `test_guided_installer.RunCommandTest` (3 tests), reason 'tkinter unavailable' (the image has no tkinter). No Python 3.11 vs 3.12 behaviour differences found. Coverage (`--sonar`) still uses the host interpreter. diff --git a/docker/Dockerfile.test b/docker/Dockerfile.test index a5ae842..1167ce7 100644 --- a/docker/Dockerfile.test +++ b/docker/Dockerfile.test @@ -4,5 +4,7 @@ FROM tts-voice-creator-clone-and-design-2:latest USER root # starlette.testclient (fastapi TestClient) needs an HTTP client package that production does not ship. +# node: the browser/server fingerprint test shells out to it (distro package, matches the image's arch). +RUN apt-get update && apt-get install -y --no-install-recommends nodejs && rm -rf /var/lib/apt/lists/* RUN pip install --no-cache-dir httpx2 USER app diff --git a/scripts/qa.mjs b/scripts/qa.mjs index 97148c7..939a99e 100644 --- a/scripts/qa.mjs +++ b/scripts/qa.mjs @@ -35,7 +35,9 @@ const PY = (() => { const TEST_IMAGE = 'tts-voice-creator-test:local'; const containerArgs = (...cmd) => ['run', '--rm', '--network', 'none', '--read-only', '--tmpfs', '/tmp:rw,exec', '--tmpfs', '/home/app:rw', '-e', 'HOME=/tmp/home', '-e', 'CONFIG_DIR=/tmp/cfg', '-e', 'TMPDIR=/tmp', '-e', 'PYTHONDONTWRITEBYTECODE=1', - '-v', `${root}:/work:ro`, '-w', '/work', '--entrypoint', 'python3', TEST_IMAGE, ...cmd]; + '-v', `${root}:/src:ro`, '--entrypoint', 'sh', TEST_IMAGE, '-c', COPY_AND_RUN, 'sh', ...cmd]; +// Repo is mounted read-only at /src; tests run in a writable throwaway copy (some tests touch files) without the big/irrelevant dirs. +const COPY_AND_RUN = 'mkdir /tmp/work && cd /src && tar -c --exclude=./node_modules --exclude=./.git --exclude=./tts-voice-creator-clone-and-design-2 --exclude=./.multica --exclude=./exports --exclude=./logs --exclude=./config --exclude=./test-results --exclude=./.scannerwork --exclude=./attachments . | tar -x -C /tmp/work && mkdir -p /tmp/work/config && cp -a /src/config/regression_snapshots /tmp/work/config/ && cd /tmp/work && exec python3 "$@"'; const containerReady = () => { if (!have('docker')) return 'docker not available'; if (sh('docker', ['image', 'inspect', TEST_IMAGE]).status) return `image ${TEST_IMAGE} missing; build: docker build -f docker/Dockerfile.test -t ${TEST_IMAGE} docker`;