Testing and debugging¶
This page is the operational test and triage playbook for the current stack.
When to use this page¶
Use this page when validating local changes, checking runtime health, or diagnosing user-facing failures across API, telemetry, and monitoring.
Audience¶
- Developers debugging service behaviour.
- Operators triaging local or cluster-like environments.
Test commands¶
Backend¶
uv run --project backend ruff check .
uv run --project backend ty check --project backend
uv run --project backend python -m pytest -q
For a faster local loop, add -m "not slow" to skip the boundary-size
runtime tests; CI always runs them.
For branch coverage, add --cov to the pytest command. The root .coveragerc
measures every backend package; the database, collection and Swarm lanes skip
unless their environment is set.
Backend tests live under backend/tests/, one folder per area: core,
database (schema, migrations, DB sync), gateway, tactic, radar,
proxy, pipelines, simulator (local broker fixtures) and contracts (cross-service, deployment
and documentation contracts). Shared fakes and fixtures live in
backend/tests/support/; test modules never import each other.
Expected result: lint and the type check exit with status 0, and tests
complete without failures. The type check covers the packages listed under
[tool.ty.src] in backend/pyproject.toml; RADAR joins once its lifecycle
attributes are typed.
RADAR production integration¶
The radar job of the CI workflow
runs as listed in Contributing and CI. It tests
this run's production RADAR image (the images job's verified build, or the digest
publish pushed on main), uses Python 3.14 and frozen dependencies,
and selects the TimescaleDB image pinned by the production release manifest.
Its isolated Ubuntu runner initializes Swarm, runs real MQTT/database/task
replacement regressions and cleans labelled fixtures within a 45 minute job.
Production release selection requires the commit's CI main-push run to succeed,
which includes this job, and ci-gate requires it before merging. The dedicated
job requires a nonempty JUnit report with zero skipped cases. Missing integration
prerequisites therefore fail that job; the general backend suite still supports
optional integration skips for local development.
Lanes select their tests by marker, never by file list:
| Marker | Lane | Applied by |
|---|---|---|
postgres |
MQTT collection integration (-m "postgres and not radar_integration") |
Automatically, for every test using disposable_database or member_database |
radar_integration |
RADAR production integration (radar job) |
Module pytestmark |
collection_stack |
MQTT collection integration, broker step | collection_integration/environment.py |
deployment |
Deployment smoke rehearsal | Module pytestmark |
Each lane fails on an empty report or a skipped case
(require_executed_tests.py).
test_suite_layout.py fails when a module reads a lane's environment gate
without carrying that lane's marker, so a new integration test cannot silently
skip everywhere.
To reproduce on a dedicated local Docker engine, initialize Swarm and provide a
disposable PostgreSQL server whose test role can create and drop databases. Set
its host URL in TEST_SCHEMA_DATABASE_URL; tests create uniquely named databases.
The workflow contains the server bootstrap and readiness commands. Then run:
uv sync --project backend --all-packages --all-groups --frozen --python 3.14
export TEST_RADAR_DATABASE_IMAGE="$(python3 .github/scripts/release_image.py postgres)"
export TEST_RADAR_SWARM_IMAGE=off-key-radar-integration:local
export TEST_RADAR_DOCKER=1
docker pull eclipse-mosquitto:2.0
docker build -f backend/Dockerfile -t "$TEST_RADAR_SWARM_IMAGE" \
--build-arg SERVICE_NAME=off-key-mqtt-radar \
--build-arg SERVICE_ROOT=services/mqtt/radar backend
uv run --frozen --project backend python -m pytest -q --tb=short -m radar_integration
The two opt-in variables enable actual Docker lifecycle tests and current-image Swarm tests. These cover reconnect and ACL failures, scoped alarms, signed checkpoint recovery, overload, sensor silence, interrupted creation, concurrent heartbeats/catalogue changes, task shutdown, resource admission and observation time validation. Fixture teardown removes only resources owned by each run.
Frontend¶
Expected result: no lint errors, passing tests, and a successful production build.
The build type-checks the application, Node tooling, unit tests and Playwright
tests before producing the Vite bundle. npm --prefix frontend run test:coverage
writes an HTML coverage report to frontend/coverage/.
Run the browser journeys against a running Compose stack, in two invocations because they need different TACTIC settings:
| Command | Covers | Requires |
|---|---|---|
npm --prefix frontend run test:e2e |
Default lanes | TACTIC_PIPELINE_AUTHORING_ENABLED=false |
npm --prefix frontend run test:e2e:pipelines |
Visual-pipeline lane | TACTIC_PIPELINE_AUTHORING_ENABLED=true and the session auth-setup saved |
The browser lane of the deployment smoke runs both, one after the other. A run
signs in once: the auth-setup project accepts the operator invitation, signs in
and saves the remembered session to frontend/playwright/.auth/user.json, and
every authenticated journey starts from that saved state. The gateway's login
limit of 10 per account a minute therefore stays the production value. The
pipeline invocation reuses the file the first one saved while authoring was still
disabled, and writes to test-results/pipelines, because Playwright empties its
output folder before a run and the first invocation's evidence must stay. The
rehearsal lane runs only npx playwright test --project=auth-setup before its
deployment tests, which take their access token from the same file.
CI browser journeys retain one retry for diagnostic evidence. Any test that passes only on retry is classified as flaky and fails CI.
The member-management browser journeys read invitation links from the local mail
sink. Set MAILPIT_BASE_URL to its reachable HTTP endpoint when using custom ports.
The disposable production Swarm rehearsal publishes its SMTP fixture's API on
port 8025 and supplies MAILPIT_BASE_URL=http://127.0.0.1:8025 in smoke.env.
The restore test reads the invitations of the members it creates from the same
sink and honours the same variable.
Repository pre-commit gate¶
Runtime health checks¶
# The backend image has no wget or curl; use its Python like the healthchecks do.
probe='import sys, urllib.request as u; print(u.urlopen(sys.argv[1]).read().decode())'
docker compose exec mqtt-proxy /app/bin/python -c "$probe" http://localhost:8010/health
docker compose exec mqtt-proxy /app/bin/python -c "$probe" http://localhost:8010/ready
docker compose exec db-sync /app/bin/python -c "$probe" http://localhost:8009/health
docker compose exec db-sync /app/bin/python -c "$probe" http://localhost:8009/ready/schema
A 503 raises HTTPError instead of printing the body; docker compose ps
shows the same health state.
/ready reports an applied collection revision. An empty selection is healthy idle.
Log inspection¶
Tail critical services:
Inspect recent output for one service:
Debug flows by symptom¶
Frontend loads but charger data is empty¶
- Confirm that
API_PROXY_TARGETis reachable from the Vite dev server; built images proxy/apithrough nginx. - Check Gateway liveness at http://localhost:8000/health.
- Check TACTIC readiness at http://localhost:8001/ready.
- Inspect Gateway and TACTIC logs for the upstream error.
Monitoring start fails¶
-
Confirm that the model catalogue is served:
-
List existing services and look for sensors already claimed:
-
Confirm TACTIC readiness.
- Inspect TACTIC logs for validation, Docker API, image, or network failures.
Telemetry is not arriving¶
- Verify that the source publisher is active.
- Check the Sources catalog, selected sensors, matching applied revisions and per-broker connection status.
- Check proxy health and readiness.
- Inspect proxy logs for parsing, authentication, or database-write errors.
- Query the Gateway telemetry endpoint.
Evidence or anomalies are missing¶
- Verify that the monitor workload exists.
- Confirm that data reaches its EMQX topic.
- Allow static calibration or adaptive warm-up to complete.
-
Query evidence and alarm counts:
-
Inspect RADAR logs for model or persistence errors.
Service addresses — local default¶
| Component | Address or port |
|---|---|
| Frontend | 5173 |
| API Gateway | 8000 |
| TACTIC | 8001 |
| DB Sync | Internal 8009 |
| MQTT Proxy health | Internal 8010 |
| PostgreSQL/TimescaleDB | 5432 |
| EMQX MQTT | 1883 |
| Source broker MQTT | 1884, host loopback |
| EMQX dashboard | 18083 |
| Mailpit UI / SMTP | 8025 / 1025 |