fix(qa): run the container tests on a writable copy and add node to the test image

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
mARTin-B78 2026-10-06 11:58:13 +02:00
parent 456b64f5b7
commit 69c33526f4
4 changed files with 10 additions and 7 deletions

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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`;