Pin the e2e relay to a zooid digest (#578)

This commit is contained in:
Coracle-Bot 2026-09-18 18:18:32 +00:00 committed by hodlbod
parent 0776befc17
commit ec835f7cfb
3 changed files with 45 additions and 16 deletions

View file

@ -425,13 +425,17 @@ stranger.
The suite is not run by agents (see CLAUDE.md).
```sh
pnpm exec playwright install # once
docker pull gitea.coracle.social/coracle/zooid:latest # once; the harness never pulls
pnpm test # starts and stops the container itself
pnpm exec playwright install # once
pnpm test # starts and stops the container itself
```
Every test skips when docker is unavailable, rather than failing.
The relay is one pinned zooid, named by digest in `harness/zooid/relay.ts`, and the harness fetches
that image the first time a machine needs it. So a spec asserting relay behaviour runs against the
relay the diff names, and needing a newer zooid is a bump in that file. `ZOOID_IMAGE` overrides the
pin, which is how a spec is tried against a zooid built from a checkout.
A test fails when the app broke while it ran, whatever it asserted: an uncaught exception on any
of its pages, or code of ours the browser refused under the content security policy. A refusal
reaches the console and nothing else, which is how a policy that had rotted past the script it

View file

@ -10,7 +10,9 @@ name: flotilla-e2e-zooid
services:
relay:
image: gitea.coracle.social/coracle/zooid:latest
# Pinned by digest in e2e/harness/zooid/relay.ts, the only thing that brings this container up.
# No default, for the same reason the config mount below has none.
image: ${ZOOID_IMAGE:?staged by e2e/harness/zooid/relay.ts, which starts this container}
# The harness dials this published port and sends a `Host` per virtual relay, which is what the
# dispatcher matches against the `host` in each file under config/.
ports:

View file

@ -20,7 +20,13 @@ import type {PublishOptions} from "./types"
import {connectToZooid, port, requestZooid} from "./transport"
import type {ZooidConnection} from "./transport"
const image = "gitea.coracle.social/coracle/zooid:latest"
// The relay every test runs against, pinned by digest so a spec is checked against one zooid rather
// than whatever this machine last pulled. The tag is there to read; docker resolves the digest, and
// `docker pull` of a newer tag prints the digest to paste beside it. A zooid built from a checkout
// has no published digest, so ZOOID_IMAGE is how an unreleased one is tried.
const image =
process.env.ZOOID_IMAGE ??
"gitea.coracle.social/coracle/zooid:0.2.2@sha256:70ecf2d3ad0ac338d48f14fe8e5ab9964193d2eb30d6b6f7d8972ca179cad513"
// How long after a recreate chromium can still abort what a page has in flight. Every netlink
// event a create-and-destroy produces lands before `compose up --wait` returns — measured at 26 of
@ -63,11 +69,13 @@ const isRelayPortTaken = () =>
socket.once("error", () => resolve(false))
})
// Every invocation carries ZOOID_CONFIG, `down` included. The compose file names it without a
// default, so a call that left it out fails to interpolate rather than quietly mounting something
// else.
// Every invocation carries the config directory and the image, `down` included. The compose file
// names both without a default, so a call that left one out fails to interpolate rather than
// quietly mounting something else or running a relay nothing here chose.
const docker = (...args: string[]) =>
execFileAsync(dockerCommand, args, {env: {...process.env, ZOOID_CONFIG: configDir}})
execFileAsync(dockerCommand, args, {
env: {...process.env, ZOOID_CONFIG: configDir, ZOOID_IMAGE: image},
})
const compose = (...args: string[]) => docker("compose", "-f", composeFile, ...args)
@ -196,11 +204,7 @@ export class Zooid {
)
if (!hasImage) {
throw new Error(
`The zooid image ${image} is not present locally. Fetch it with ` +
`\`${dockerCommand} pull ${image}\`, or build it from a zooid checkout with ` +
`\`${dockerCommand} build -t ${image} .\`.`,
)
await this.fetchImage()
}
if (await isRelayPortTaken()) {
@ -208,8 +212,8 @@ export class Zooid {
`127.0.0.1:${port} is already in use, but the zooid relay container publishes ` +
"exactly that port and the harness talks to whatever answers there. This is a leftover " +
"container from an interrupted run, or another copy of this suite running on the machine " +
`(under any user). Free it before running — \`${dockerCommand} compose -f ` +
"e2e/harness/zooid/docker/compose.yaml down\` clears one this project started.",
`(under any user). Free it before running — \`${dockerCommand} rm -f ` +
"flotilla-e2e-zooid-relay-1` clears the one this project starts.",
)
}
@ -281,6 +285,25 @@ export class Zooid {
}
}
// Fetching unasked is safe for a pinned reference: the pull can only produce the relay this suite
// was written against, and a machine that already has it never gets here.
private fetchImage = async () => {
console.warn(`\nFetching the zooid image this suite is pinned to:\n ${image}\n`)
try {
await docker("pull", image)
} catch (error) {
const {stderr} = Object(error) as {stderr?: string}
throw new Error(
`Could not fetch ${image}: ${stderr?.trim() || String(error)}\n\n` +
`Fetch it by hand with \`${dockerCommand} pull ${image}\`, or point ZOOID_IMAGE at a ` +
"zooid you built yourself.",
{cause: error},
)
}
}
// Whatever the relay wrote before it died. Compose reports only that a container exited, so
// without this a startup failure is a status code and no reason.
private logs = () =>