carlos / todomvc-bakeoff Public
fix: suite asks for uncompressed bytes in the Content-Length check; boot-check replaces operator-only carlos vet; scratch rule
Found by the Go pilot: Carloku's edge gzips, so the header is the compressed size; carlos vet needs operator access.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 files changed,
+42
−5
BRIEF.md+12 −3scripts/boot-check.sh+26 −0tests/behaviour/robustness.spec.ts+4 −2
diff --git a/BRIEF.md b/BRIEF.md| index 17148fc..4a0a8cb 100644 |
| --- a/BRIEF.md |
| +++ b/BRIEF.md |
| @@ -124,13 +124,22 @@ carlos instances create --account bfj --app todo-<slug> --host todo-<slug>.bfj.o |
| After that, `make deploy` runs |
| `carlos deploy --account bfj --app todo-<slug> --target linux-arm64 --message "<what changed>" dist/app-linux-arm64`. |
| The version is the git short SHA of your commit. Before the first |
| -deploy, run `carlos vet --boot` on the shipped version as |
| -`references/platform.md` describes. Then run the shared suite against |
| -`https://todo-<slug>.bfj.oncarlos.com`. It must pass there too. |
| +deploy, run `scripts/boot-check.sh dist/app-linux-arm64`. It starts the |
| +arm64 binary under qemu on a unix socket, checks `/healthz` and |
| +`/api/version`, and checks for a clean exit on SIGTERM. (`carlos vet` |
| +needs operator access, which you don't have.) After deploying, run the |
| +shared suite against `https://todo-<slug>.bfj.oncarlos.com`. It must pass |
| +there too. |
| If a `carlos` command refuses or behaves unexpectedly, read its `-h` and |
| `references/platform.md` before trying something else. |
| +## Scratch files |
| + |
| +`$TMPDIR` is shared with other sessions. Make your own directory with |
| +`mktemp -d -p "$TMPDIR"`, keep every scratch file in it, and delete only |
| +that directory. Never delete with a glob in `$TMPDIR`. |
| + |
| ## Git |
| Commit on your branch (`app/<slug>`) as you go, with clear messages |
diff --git a/scripts/boot-check.sh b/scripts/boot-check.sh| new file mode 100755 |
| index 0000000..6677c80 |
| --- /dev/null |
| +++ b/scripts/boot-check.sh |
| @@ -0,0 +1,26 @@ |
| +#!/usr/bin/env bash |
| +# The platform boot check, run locally on the shipped arm64 binary under |
| +# qemu: start it on a unix socket, require 200 from /healthz and |
| +# /api/version, then require a clean exit on SIGTERM. |
| +# scripts/boot-check.sh <dist/app-linux-arm64> |
| +set -euo pipefail |
| +bin=$(cd "$(dirname "$1")" && pwd)/$(basename "$1") |
| +root=${BAKEOFF_TOOLCHAINS:-$HOME/.local/share/bakeoff-toolchains}/arm64root |
| +work=$(mktemp -d -p "${TMPDIR:-$HOME/.cache}") |
| +pid= |
| +trap '[ -n "$pid" ] && kill "$pid" 2>/dev/null; rm -rf "$work"' EXIT |
| +cd "$work" |
| +CARLOS_HOST=boot-check.invalid qemu-aarch64-static -L "$root" "$bin" --socket "$work/app.sock" --db "$work/app.db" > "$work/out.log" 2>&1 & |
| +pid=$! |
| +for _ in $(seq 1 100); do [ -S "$work/app.sock" ] && break; sleep 0.2; done |
| +[ -S "$work/app.sock" ] || { echo "boot-check: no socket after 20 s"; cat "$work/out.log"; exit 1; } |
| +for path in /healthz /api/version; do |
| + code=$(curl -s -o "$work/body" -w '%{http_code}' --unix-socket "$work/app.sock" "http://boot-check.invalid$path") |
| + [ "$code" = 200 ] || { echo "boot-check: $path answered $code"; exit 1; } |
| + echo "boot-check: $path 200 $(head -c 60 "$work/body")" |
| +done |
| +kill -TERM "$pid" |
| +set +e; wait "$pid"; status=$?; set -e |
| +pid= |
| +[ "$status" = 0 ] || { echo "boot-check: exit status $status after SIGTERM"; exit 1; } |
| +echo "boot-check: clean exit on SIGTERM" |
diff --git a/tests/behaviour/robustness.spec.ts b/tests/behaviour/robustness.spec.ts| index 43c4949..a52f24a 100644 |
| --- a/tests/behaviour/robustness.spec.ts |
| +++ b/tests/behaviour/robustness.spec.ts |
| @@ -23,10 +23,12 @@ test('round-trips UTF-8 and sends correct Content-Length', async ({ page }, ti) |
| await addTodo(page, ti, title); |
| await page.reload(); |
| expect(await todos(page).nth(0).locator('label').textContent()).toBe(title); |
| - const res = await page.request.get('/'); |
| + // Ask for the bytes as the app sends them: an edge that compresses |
| + // reports the compressed size, which the decoded body can't match. |
| + const res = await page.request.get('/', { headers: { 'Accept-Encoding': 'identity' } }); |
| const body = await res.body(); |
| const declared = res.headers()['content-length']; |
| - if (declared !== undefined) expect(Number(declared)).toBe(body.length); |
| + if (declared !== undefined && !res.headers()['content-encoding']) expect(Number(declared)).toBe(body.length); |
| expect(body.toString('utf8')).toContain(title); |
| }); |