crate e2e #
Real end-to-end tests against a fully local, deterministic atproto network (a PDS, jetstream, Constellation, Slingshot, Caddy for TLS on the test handles) — not the public network, so no indexing lag and no dependency on real accounts.
These exist for what the unit tests structurally can't cover: the real
OAuth wire flow (PAR/PKCE/DPoP against a real PDS), and cross-account
behavior that depends on real Constellation indexing (adoption, the
edit-inbox). Business logic itself is already covered fast and in isolation
by server/test/promotion_test.gleam and friends via injected Deps
stubs — don't duplicate that here.
One-time setup #
make e2e-setup # generates devnet secrets and installs Playwright
Running #
Three things need to be running:
- The devnet:
make e2e-up(pulls immutable images, wipes any previous devnet state for a clean slate, boots fresh, waits for the PDS to be healthy, seeds the fixedalice.test/bob.testaccounts). - crate's own dev server, pointed at the devnet:
SLINGSHOT_URL=http://localhost:6790 \ CONSTELLATION_URL=http://localhost:6789 \ JETSTREAM_URL=http://localhost:8090/subscribe \ UNSAFE_ALLOW_PRIVATE_ENDPOINTS=1 \ ./scripts/dev.shJETSTREAM_URLmatters since the appview-first cutover: the shelf and edit-inbox reads are served from our own index, which the consumer fills from the devnet firehose.UNSAFE_ALLOW_PRIVATE_ENDPOINTSdisables the outbound-endpoint SSRF guard, which otherwise (correctly) rejects the devnet's loopback-http PDS; never set it outside a local devnet. - The test suite:
make e2e(this also does step 1 for you — running it standalone is only useful if you want to poke at the devnet by hand first).
make e2e-down tears the devnet down and wipes its volumes when you're done.
Why fixed test handles, not generated per run #
Caddy's TLS termination and the PDS's network alias are declared per-handle
in devnet/docker-compose.yml and devnet/Caddyfile (atproto's
bidirectional handle verification needs a real, if locally-CA-signed, TLS
endpoint for each handle — see the devnet's own README for why). Adding a
handle means declaring it in both places, not just calling
create-test-accounts.sh with a new name. make e2e-up wipes and reseeds
the same two accounts fresh each run instead, which is enough isolation for
now — revisit if tests start needing more than two accounts at once.
Pinned dependencies #
devnet/images.env is the one lock for the source-built services. It records
the exact Jetstream and Microcosm revisions alongside the immutable GHCR
digests that ordinary development pulls:
ghcr.io/riotbyte/crate-devnet-jetstreamghcr.io/riotbyte/crate-devnet-constellationghcr.io/riotbyte/crate-devnet-slingshot
The normal path never builds Go or Rust locally. make e2e-up refuses to run
unless all three references are full sha256 digests, so it cannot silently
fall back to a mutable tag. The first checkout intentionally contains
UNPUBLISHED placeholders until the trusted publisher has completed the
bootstrap procedure below.
Bootstrap and update procedure #
- Change the relevant source revision in
devnet/images.env. Leave its image value as@sha256:UNPUBLISHED. - Before merging, a
riotbyteorganization owner must set Packages → Default package visibility to public. New GHCR packages are only created by their first push, so requiring descriptions before that push is circular. The first image instead carries public OCI source, exact revision, license labels, and this public build procedure, which is the Constellation AGPL source notice. The owner then sets the trusted CI-onlyDEVNET_GHCR_PREFLIGHT=public-by-default-and-agpl-source-notice-confirmedsecret. The publisher refuses to run without this exact confirmation. Verify the organization setting manually in GitHub before setting it. - Merge that trusted source-pin change to
main. The trusted Tangledpublish-devnet-imagesworkflow builds the revision forlinux/amd64andlinux/arm64, publishes a unique tag derived from its documentedTANGLED_PIPELINE_ID, and prints oneIMAGE_LOCKline per service. Copy those three full@sha256values intodevnet/images.env. Registry tags are only transport handles. The printed manifest digest is the sole consumption identity, and a retry must start a new Tangled pipeline. - Replace the matching
UNPUBLISHEDimage value with the printed full digest in a follow-up change. Runmake e2e-images-verify,make e2e-up, and the relevant E2E suite before merging it.
For the occasional upstream investigation or a source change that has not
been published yet, use make e2e-up-local. It clones the same source
revisions and uses devnet/docker-compose.local-build.yml explicitly; it is
never selected by the normal target.
The GHCR packages must remain public so contributors can pull them without credentials. They carry OCI source, revision, and license labels. Constellation is AGPL-3.0-only, and the public source link, revision label, and these build instructions are the first-publication notice. SBOM and provenance are explicitly disabled until we adopt a pinned verifier that can validate the actual in-toto subjects and predicate types for both platform manifests. Jetstream is MIT or Apache-2.0; Slingshot is MIT or Apache-2.0.