CI: run backoffice test suite on push/PR, document local test setup

Adds .gitea/workflows/test-backoffice.yml (paths-filtered to
backoffice/app/** and backoffice/db/**, self-hosted homelab runner,
plain git clone matching deploy-backoffice.yml's conventions) so a
regression no longer needs a human to notice it.

The psycopg[binary]==3.2.1 pin turned out to be correct, not stale: it
only ships wheels through cp312, so it resolves cleanly under the
project's python:3.12-slim target but not under newer interpreters.
Documented in requirements.txt and docs/agents/testing.md, along with
the fact that the suite spins up its own throwaway postgres:16-alpine
container (tests/conftest.py) with no manual DB setup required.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-03 17:34:43 +02:00
parent 528a13ca7c
commit 3a20cb5d2b
4 changed files with 139 additions and 0 deletions
+65
View File
@@ -0,0 +1,65 @@
name: Test backoffice (smb-crm)
# Automatic, unlike deploy-backoffice.yml: this workflow never touches the
# live host, only spins up a throwaway postgres:16-alpine container via the
# test suite itself (see backoffice/app/tests/conftest.py), so it's safe to
# run on every push/PR. Scoped to the paths that can actually affect these
# tests so unrelated changes elsewhere (n8n/, deploy/, docs/, etc.) don't
# trigger a run.
on:
push:
paths:
- "backoffice/app/**"
- "backoffice/db/**"
pull_request:
paths:
- "backoffice/app/**"
- "backoffice/db/**"
jobs:
test:
runs-on: [self-hosted, homelab]
steps:
# Plain git + fetch-by-sha instead of actions/checkout@v4 (Node.js
# based; this runner deliberately has no Node toolchain) and instead
# of deploy-backoffice.yml's branch-name clone (that workflow is
# workflow_dispatch-only with a branch/tag input; push/pull_request
# give us a commit SHA via github.sha, which needs a fetch-by-SHA, not
# a branch checkout, to be exact for PRs).
- name: Checkout
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
GIT_SHA: ${{ github.sha }}
run: |
set -euo pipefail
find . -mindepth 1 -delete
git init -q
git remote add origin "http://${DEPLOY_TOKEN}@172.24.0.2:3000/BPPP/smb-online.git"
git fetch --depth 1 origin "$GIT_SHA"
git checkout -q FETCH_HEAD
git remote set-url origin http://172.24.0.2:3000/BPPP/smb-online.git
# This runner is bare-metal (no guaranteed python3.12 binary), but it
# already has docker access (see deploy-backoffice.yml), so the venv is
# built inside a python:3.12-slim container — the same base image
# backoffice/app/Dockerfile ships on, so "3.12 venv" here means exactly
# what runs in production, with zero assumptions about the host's own
# Python. --network host is required: tests/conftest.py itself runs
# `docker run -p 127.0.0.1:<port>:5432 postgres:16-alpine` as a sibling
# container (via the mounted docker socket), and the test process must
# see that published port on the same network namespace it lands in.
- name: Install deps and run backoffice test suite (python:3.12-slim)
run: |
set -euo pipefail
docker run --rm --network host \
-v "$PWD":/repo \
-v /var/run/docker.sock:/var/run/docker.sock \
-w /repo \
-e HOME=/tmp \
python:3.12-slim bash -euxc '
apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq docker.io >/dev/null
python3 -m venv .venv
.venv/bin/pip install -q --upgrade pip
.venv/bin/pip install -q -r backoffice/app/requirements-dev.txt
.venv/bin/python -m pytest backoffice/app/tests -q
'
+7
View File
@@ -20,3 +20,10 @@ with `/mattpocock-skills:triage`. See `docs/agents/triage-labels.md`.
Single-context: one `CONTEXT.md` + `docs/adr/` at the repo root (created lazily when terms or Single-context: one `CONTEXT.md` + `docs/adr/` at the repo root (created lazily when terms or
decisions get resolved). See `docs/agents/domain.md`. decisions get resolved). See `docs/agents/domain.md`.
### Testing
Backoffice tests need only Docker (a throwaway Postgres container is started for you) and a
Python 3.12 venv — the `psycopg[binary]` pin has version-specific wheel availability, so use
3.12, not whatever `python3` defaults to. Runs automatically in Gitea Actions on push/PR. See
`docs/agents/testing.md`.
+6
View File
@@ -1,4 +1,10 @@
Flask==3.0.3 Flask==3.0.3
# psycopg-binary 3.2.1 only publishes prebuilt wheels up to cp312 (see
# https://pypi.org/project/psycopg-binary/3.2.1/#files) — installing this
# under Python 3.13+ falls back to a source build (needs libpq headers) or
# just fails to resolve. Match the python:3.12-slim runtime everywhere
# (venv, CI, prod image) and this pin installs from wheel with no build
# toolchain needed. See docs/agents/testing.md.
psycopg[binary]==3.2.1 psycopg[binary]==3.2.1
waitress==3.0.0 waitress==3.0.0
PyJWT[crypto]==2.9.0 PyJWT[crypto]==2.9.0
+61
View File
@@ -0,0 +1,61 @@
# Testing: backoffice (smb-crm)
## Running the suite locally
```bash
python3.12 -m venv .venv # must be 3.12 — see "psycopg pin" below
.venv/bin/pip install -r backoffice/app/requirements-dev.txt
cd backoffice/app && ../../.venv/bin/python -m pytest tests/ -q
```
No `python3.12` on your machine? Run it in a container instead — this is what
CI does (see `.gitea/workflows/test-backoffice.yml`):
```bash
docker run --rm --network host -v "$PWD":/repo -v /var/run/docker.sock:/var/run/docker.sock \
-w /repo -e HOME=/tmp python:3.12-slim bash -c '
apt-get update -qq && apt-get install -y -qq docker.io >/dev/null
python3 -m venv .venv && .venv/bin/pip install -q -r backoffice/app/requirements-dev.txt
.venv/bin/python -m pytest backoffice/app/tests -q'
```
`--network host` matters: `tests/conftest.py` runs `docker run -p 127.0.0.1:<port>:5432`
as a sibling container via the mounted socket, and the test process needs to see that
published port in the same network namespace.
## Docker is the only prerequisite
`backoffice/app/tests/conftest.py` starts its own **throwaway `postgres:16-alpine`
container** at collection time (a `docker run` in module scope, bound to a random free
local port), applies `backoffice/db/init.sql` against it, and stops it via `atexit` when
the process exits. There is:
- no `DATABASE_URL` to set by hand — the fixture sets `os.environ["DATABASE_URL"]` itself
- no shared/persistent test database to seed, migrate, or worry about polluting between runs
- nothing to tear down manually — a crashed run leaves an orphaned `smb-booking-test-db-*`
container behind (`docker ps -a | grep smb-booking-test-db` to find and `docker rm -f` it),
but the container name is randomized per run so it never collides with a live one.
Per module docstring, this is deliberate (issue #14's testing decision): real Postgres, no
mocking, so the booking `EXCLUDE` constraint and tenancy filters get exercised for real.
## The `psycopg[binary]==3.2.1` pin: why it can fail to install, and the fix
`backoffice/app/requirements.txt` pins `psycopg[binary]==3.2.1`. If `pip install` reports
no matching distribution, **check your Python version first** — this is not a stale pin.
`psycopg-binary` 3.2.1 only ships prebuilt wheels for CPython 3.83.12
(confirmed against <https://pypi.org/project/psycopg-binary/3.2.1/#files>). Under Python
3.13+ there is no wheel, so pip either falls back to a from-source build (needs libpq dev
headers you probably don't have) or fails outright with "No matching distribution."
The project's production image is `python:3.12-slim` (see `backoffice/app/Dockerfile` and
the `tzdata` comment in `requirements.txt`), so the fix is to **match that Python version
everywhere** — local venv, CI, prod — rather than bump the pin. Use `python3.12`, or the
`python:3.12-slim` container recipe above if your machine doesn't have 3.12 installed.
## CI
This suite runs automatically on every push/PR touching `backoffice/app/**` or
`backoffice/db/**`, via `.gitea/workflows/test-backoffice.yml`. A local run before
pushing is a fast pre-check, not the only gate.