Skip to content

Contributing

git clone uv sync
createdb abilian_cdn_test
scripts/garage-dev.sh start        # a local S3
make test

The suite needs PostgreSQL and something that speaks S3. Both are skipped with a message saying how to start them when they are not there, rather than failing with a connection error.

The checks

make lint      # ruff, ty, pyrefly, mypy
make test      # the three tiers
make docs      # this site, with --strict

make lint runs four type checkers because they disagree, and the disagreements have been worth reading. pre-commit runs the same ruff as the dev dependency group — when those two drifted apart they disagreed about which suppression syntax was valid, and make lint passed on exactly what the commit hook rejected.

The test tiers

what it needs what it is for
tests/a_unit nothing one function, called directly
tests/b_integration PostgreSQL, S3 components together, through a test transport
tests/c_e2e a running server the real thing over real HTTP

The tree mirrors the source packages, so tests/a_unit/s3/ holds the unit tests for src/abilian_cdn/s3/. tepyd checks that mirroring and the balance between tiers.

The integration tier is the bulk of it, deliberately. Every bug this service has shipped and caught was a disagreement with a real client — a chunk decoder mishandling a frame-closing CRLF, a canonical path including the query string, PutObject answering null, rclone zeroing a file on copy — and none of them was reachable from a pure function.

Write the cheap test where a cheap test can answer the question. Anything that does not truly need a database, a socket or a browser belongs in a_unit, where it runs in milliseconds and names the failure precisely.

Conventions

  • Everything is checked by four type checkers. Annotate accordingly.
  • No # noqa without a reason next to it. If a rule fires often, the structure is usually what is wrong.
  • Migrations are generated, then read. Autogenerate proposes NOT NULL with no backfill; every one of those has needed a server_default and a follow-up ALTER to drop it.
  • Tests are named as sentences. test_a_disabled_user_cannot_sign_in, not test_login_2.
  • Comments say why. What the code does is in the code.

The writing style guide covers the prose in this site and in the README.

One application per test session

The fixtures build one Litestar application for the whole run, and give each test a fresh client and lifespan over it. Registering a route handler into an application makes the handler — a module-level object — hold that application, so one application per test is one application retained per test: the suite reached 4 GB and could not exit.

If you add a fixture that builds its own application, it is for a genuinely different configuration, and it should say which in a comment.

The specification

notes/01-specs.md is the functional specification: what each milestone is, what was built differently and why. Read it before changing anything structural — several decisions there look arbitrary until you find the paragraph explaining what went wrong the other way.