The chat said 47 passed. CI said the browser was missing. Those two facts can both be true.
You ran end-to-end tests through a coding agent on a remote Linux box. The runner printed green. Then CI started from an empty image and died on the first page.goto. The failure was not a flake. The box was warm.
A lived-in home directory is a cache. Playwright, Cypress, Puppeteer, npm, and pip all drop large binaries under $HOME. An agent that types npx playwright test will reuse last week's Chromium. Your CI will not.
The log line that actually matters
Read the CI log before you rerun anything. Look for a path, not a vibe.
browserType.launch: Executable doesn't exist at
/home/runner/.cache/ms-playwright/chromium-XXXX/chrome-linux/chrome
Looks like Playwright was just installed or updated.
Please run the following command to download new browsers:
npx playwright install
That message is normal Playwright behavior. Browsers live outside node_modules unless you relocate them. The current browsers guide is here: https://playwright.dev/docs/browsers
Your agent transcript never showed that block. Why would it? The executable was already on disk.
What a lived-in $HOME stores
You are not debugging a test. You are debugging a filesystem.
On Linux these directories are the usual suspects:
~/.cache/ms-playwright # Playwright browsers
~/.cache/puppeteer # Chrome for Testing
~/.cache/Cypress # Cypress binary
~/.npm # packed tarballs, not your lockfile
~/.cache/pip # wheels from some earlier python
npx playwright test does not mean npx playwright install ran. npm test does not mean the npm cache was empty. A reused remote box is closer to a shared workstation than to CI.
When you need a scratch kernel for messy browser downloads, a throwaway Linux server is useful. MonkeyCode offers free model access and a free server option. Disclosure: This article was prepared as part of MonkeyCode's product outreach. The product does not change the rule. If $HOME is dirty, the green bar is not evidence.
Prove the cache, not the transcript
Do not argue with the model. Print the machine.
Label the following as a proposed check. Run it on the box before you believe any e2e summary.
#!/usr/bin/env bash
# Proposed fingerprint. Unexecuted until you run it.
set -euo pipefail
echo "== identity =="
uname -a
id
echo "HOME=$HOME"
echo "PWD=$PWD"
echo "== browsers already on disk =="
du -sh \
"${PLAYWRIGHT_BROWSERS_PATH:-$HOME/.cache/ms-playwright}" \
"$HOME/.cache/puppeteer" \
"$HOME/.cache/Cypress" \
2>/dev/null || true
ls -1 "${PLAYWRIGHT_BROWSERS_PATH:-$HOME/.cache/ms-playwright}" 2>/dev/null | head || echo "no ms-playwright dir"
echo "== env that relocates browsers =="
env | grep -E 'PLAYWRIGHT_|PUPPETEER_|CYPRESS_|npm_config_cache|PIP_CACHE' || echo "no relocating env"
echo "== what the project claims =="
if [[ -f package.json ]]; then
node -e 'const p=require("./package.json"); console.log("pw", (p.devDependencies||{})["@playwright/test"]||(p.dependencies||{})["@playwright/test"]||"missing")'
fi
echo "== install is not test =="
command -v npx >/dev/null && npx playwright --version || echo "playwright cli missing"
npx playwright install --dry-run 2>/dev/null | tail -20 || true
If du prints hundreds of megabytes and install --dry-run wants almost nothing, the previous occupant of $HOME bought that green bar.
Then ask Git a second question. Did the agent pin an absolute cache path that only exists on the box?
git grep -n "PLAYWRIGHT_BROWSERS_PATH\|PUPPETEER_CACHE_DIR\|CYPRESS_CACHE_FOLDER" -- ':!node_modules' || true
git grep -n "npx playwright install\|playwright install --with-deps" -- .github .gitlab-ci.yml Makefile package.json Dockerfile 2>/dev/null || true
ls -a | grep -E '^\.env' || true
A line like PLAYWRIGHT_BROWSERS_PATH=/home/box/.cache/ms-playwright in .env is a second trap. The box can resolve it. CI cannot. Missing playwright install in CI plus a warm box is the whole bug.
The artifact: a cold-home harness
You need one directory that is not $HOME. You need the test command to fail if browsers are absent. Chat will not give you that. A wrapper will.
Save this proposed script as scripts/e2e-cold-home.sh. It is a method, not a benchmark. Do not point it at production data.
#!/usr/bin/env bash
# Proposed cold-home harness for Playwright-style e2e.
# Label: unexecuted. chmod +x and run on a throwaway box.
set -euo pipefail
ROOT="$(git rev-parse --show-toplevel)"
STAMP="${E2E_COLD_HOME:-$ROOT/.cold-home}"
REPORT="$ROOT/.cold-home-report.txt"
rm -rf "$STAMP"
mkdir -p "$STAMP/tmp" "$STAMP/cache" "$STAMP/npm" "$STAMP/config"
export HOME="$STAMP"
export XDG_CACHE_HOME="$STAMP/cache"
export XDG_CONFIG_HOME="$STAMP/config"
export TMPDIR="$STAMP/tmp"
export npm_config_cache="$STAMP/npm"
export PLAYWRIGHT_BROWSERS_PATH="$STAMP/pw-browsers"
unset PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD || true
{
echo "cold HOME=$HOME"
echo "pw path=$PLAYWRIGHT_BROWSERS_PATH"
echo "user=$(id -u) host=$(hostname)"
date -u +"%Y-%m-%dT%H:%M:%SZ"
} | tee "$REPORT"
cd "$ROOT"
if [[ ! -f package-lock.json && ! -f pnpm-lock.yaml && ! -f yarn.lock ]]; then
echo "FAIL: no lockfile. a cold HOME cannot save a floating graph." | tee -a "$REPORT"
exit 3
fi
if [[ ! -d node_modules ]]; then
echo "node_modules missing; install inside the cold HOME" | tee -a "$REPORT"
npm ci
fi
echo "== expect install to fetch browsers ==" | tee -a "$REPORT"
npx playwright install --with-deps chromium | tee -a "$REPORT"
if [[ ! -d "$PLAYWRIGHT_BROWSERS_PATH" ]]; then
echo "FAIL: browsers path still empty after install" | tee -a "$REPORT"
exit 2
fi
echo "== run tests against the cold tree ==" | tee -a "$REPORT"
npx playwright test "$@"
echo "COLD_OK" | tee -a "$REPORT"
Two properties matter. A new HOME means Chromium cannot be a leftover. The report file sits in the worktree, so git status can see it. Chat cannot hide a missing COLD_OK line if you look at the file.
Ignore the browser blob. Add this to .gitignore before the agent helpfully commits 300MB:
.cold-home/
.cold-home-report.txt
playwright-report/
test-results/
Cypress uses CYPRESS_CACHE_FOLDER. Puppeteer uses PUPPETEER_CACHE_DIR. Same idea. Different env names.
Six steps that keep a scratch box honest
Use a remote box for messy browser downloads. Keep those downloads off your laptop. Do not keep them in a shared $HOME either.
- Clone a branch. Do not use
mainas a scratch worktree. - Run the fingerprint script. If
~/.cache/ms-playwrightalready exists, treat the next test run as contaminated. - Run
scripts/e2e-cold-home.sh. Let it fail. A failure here is data. - Only after
COLD_OKappears, copy the CI snippet that callsnpx playwright install --with-depsbeforenpx playwright test. - Delete
.cold-home/or keep it gitignored. Never commit the browser blob. - Re-run the fingerprint in CI, or on a second machine. The second machine is the witness.
A free model can type those commands. You still read COLD_OK. If the model summarizes all tests passed and the report file is missing, the summary is fiction.
Decision table
Keep this next to the script. No timing claims. Just a gate.
| Observation | What it means | What you do |
|---|---|---|
du shows a large ~/.cache/ms-playwright before install |
Warm box | Ignore the agent's first green run |
playwright install --dry-run wants 0 bytes |
Cache already satisfied | Force a cold HOME and rerun |
| Tests pass in chat, CI missing executable | Install step absent in CI | Add playwright install --with-deps
|
.env pins an absolute /home/... cache path |
Box-only path leak | Delete it; use a relative path or CI env |
Agent sets PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
|
Speed hack | Revert unless CI vendors browsers |
.cold-home/ shows up in git status
|
Harness residue | gitignore it; do not commit browsers |
If row one and row three are both true, you do not have a flake. You have two environments.
Wire the same order into CI
The harness is for the box. CI needs the same order of operations. An empty job image already is a cold HOME, so you do not need the fake $HOME trick there. You do need the install step.
Proposed order. Plug in the checkout and Node setup actions your repo already pins. This fragment is unexecuted until you paste it.
# proposed order of steps, not a full workflow
steps:
- checkout
- setup node # use the major tags your repo already pins
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: npx playwright test
env:
CI: true
Notice what is not in that fragment. There is no silent reuse of a developer cache. npm tarball caching is acceptable. Chromium caching is optional and dangerous if the key is wrong. If you cache ~/.cache/ms-playwright, key it on the Playwright version from the lockfile. An unkeyed cache is how this bug returns.
GitLab CI is the same order: npm ci, install browsers, then test. Do not let an agent collapse those three lines into npm test.
Limitations
This harness does not prove visual correctness. It only proves the browser was fetched in the same session as the tests.
It will not help if your tests skip themselves when Chromium is missing. Search for test.skip around executablePath failures. An agent will add those skips to keep the bar green.
It does not pin a browser revision for you. Playwright already pins a revision to the @playwright/test version. If the agent upgrades the package and you cache browsers by a handwritten key, you can still get a mismatch. Key caches on the lockfile, or do not cache browsers at all.
Linux is not macOS. A cold Linux HOME still will not exercise WebKit the way a Darwin laptop does. If you ship Safari behavior, this method is incomplete on purpose.
The script uses npm ci. If you do not have a lockfile, stop and fix that first. A cold HOME cannot save a floating graph.
Who should skip this
Skip it if you vendor browsers in the image and never call playwright install on the box. Skip it if you do not run browser tests at all.
Skip it if every e2e job is already a fresh container with --with-deps in the Dockerfile. You already have the cold HOME. The agent is the weak point only when it runs on a reused user account.
Do not use a throwaway box as a substitute for matrix CI. Headless Chromium on Linux is not Firefox on Windows. The harness answers one question: did this run fetch its own browser? That is the whole scope.
Closing
So did the suite pass, or did $HOME pass? Open the cache directory before you open the transcript.
A scratch server is a good place to find out, as long as you empty HOME first. Start with the harness, not with another npx playwright test. Trust the COLD_OK line. Nothing else.
Top comments (0)