lint-once.test.ts misses the path CI actually runs, and its lockfile assertion checks the wrong branch #33

Open
opened 2026-08-10 15:20:43 +02:00 by clawbot · 2 comments
Collaborator

Problem

#29 added test/packaging/lint-once.test.ts to enforce that prettier runs exactly once. The test is the deliverable of that issue — the point was to stop relying on a claim in a PR body. Adversarial review of #31 demonstrated, by mutation, that it does not enforce what its header says it enforces. Both of the first two findings were shown red-to-green: the mutation was applied, the suite stayed green.

1. The walk never reaches script/cibuild

Appending RUN yarn run prettier --check . to Dockerfile after RUN make build left the suite green at 258/258. That gives script/cibuild — the thing .gitea/workflows/check.yml actually executes — two prettier passes: exactly the duplication this test exists to prevent, on the one path where it matters most. The header comment's claim that a duplicate is caught wherever it is added overstates the coverage.

2. The --frozen-lockfile assertion misses the branch the container takes

script/bootstrap has two install sites. The container hits the second one, because node:22-alpine ships yarn. Changing that site to a bare yarn install and leaving the first alone kept the suite green — so the assertion that guarantees lockfile-pinned installs inside the container passes while the container's install has stopped being pinned. A whole-file substring check cannot distinguish the branches.

3. Prettier is counted per line, not per occurrence

RUN yarn run prettier --check . && yarn run prettier --check src counts as one invocation (verified). The same continue also drops any script/make/yarn edge that shares a line with a counted one.

4. Stale comment

test/packaging/entrypoints.test.ts:3 still says make check runs test, lint and fmt-check. #29 made that false.

Definition of done

  • The test walks script/cibuild and the Dockerfile reachability case; the mutation in 1 turns the suite red.
  • The lockfile assertion targets the bootstrap branch the container actually executes, not the file as a whole; the mutation in 2 turns the suite red.
  • Counting is per occurrence, not per line; a two-invocations-on-one-line arrangement is caught.
  • The stale comment in 4 is corrected.
  • Every assertion is mutation-tested: change what it asserts, confirm it fails for its own named reason and not incidentally. An assertion that cannot fail is worse than no assertion, because it reads as a guarantee.
  • make check green; script/cibuild green and shown to have actually executed rather than served from cache.

Not a 1.0.0 blocker, but it should land before anything else relies on this test, since it currently reports a guarantee it does not provide.

## Problem https://git.eeqj.de/sneak/quak/issues/29 added `test/packaging/lint-once.test.ts` to enforce that prettier runs exactly once. The test is the deliverable of that issue — the point was to stop relying on a claim in a PR body. Adversarial review of https://git.eeqj.de/sneak/quak/pulls/31 demonstrated, by mutation, that it does not enforce what its header says it enforces. Both of the first two findings were shown red-to-green: the mutation was applied, the suite stayed green. ### 1. The walk never reaches `script/cibuild` Appending `RUN yarn run prettier --check .` to `Dockerfile` after `RUN make build` left the suite green at 258/258. That gives `script/cibuild` — the thing `.gitea/workflows/check.yml` actually executes — two prettier passes: exactly the duplication this test exists to prevent, on the one path where it matters most. The header comment's claim that a duplicate is caught wherever it is added overstates the coverage. ### 2. The `--frozen-lockfile` assertion misses the branch the container takes `script/bootstrap` has two install sites. The container hits the second one, because `node:22-alpine` ships yarn. Changing that site to a bare `yarn install` and leaving the first alone kept the suite green — so the assertion that guarantees lockfile-pinned installs inside the container passes while the container's install has stopped being pinned. A whole-file substring check cannot distinguish the branches. ### 3. Prettier is counted per line, not per occurrence `RUN yarn run prettier --check . && yarn run prettier --check src` counts as one invocation (verified). The same `continue` also drops any script/make/yarn edge that shares a line with a counted one. ### 4. Stale comment `test/packaging/entrypoints.test.ts:3` still says `make check` runs test, lint and fmt-check. https://git.eeqj.de/sneak/quak/issues/29 made that false. ## Definition of done - The test walks `script/cibuild` and the `Dockerfile` reachability case; the mutation in 1 turns the suite red. - The lockfile assertion targets the bootstrap branch the container actually executes, not the file as a whole; the mutation in 2 turns the suite red. - Counting is per occurrence, not per line; a two-invocations-on-one-line arrangement is caught. - The stale comment in 4 is corrected. - Every assertion is mutation-tested: change what it asserts, confirm it fails for its own named reason and not incidentally. An assertion that cannot fail is worse than no assertion, because it reads as a guarantee. - `make check` green; `script/cibuild` green and shown to have actually executed rather than served from cache. Not a 1.0.0 blocker, but it should land before anything else relies on this test, since it currently reports a guarantee it does not provide.
Author
Collaborator

Implementation plan

Working on next in a fresh clone; one commit, landing on #31.

1. Walk the path CI actually runs. Add a workflow:<path> node kind whose commands are the run: steps of .gitea/workflows/check.yml, and root a walk there. That reaches script/cibuild, which reaches both docker:Dockerfile.lint (via script/lint) and docker:Dockerfile (the bare docker build .). Assertions: the CI path invokes prettier exactly once, and its reached set contains script/cibuild, docker:Dockerfile and docker:Dockerfile.lint. Rooting the walk in the workflow file rather than in a hand-picked entrypoint is what makes "the path CI executes" an assertion rather than an assumption.

2. Lockfile assertion targets the executed branch. Resolve install_js_deps out of script/bootstrap and split it at the missing yarn guard, so the two install sites are separately addressable. node:22-alpine ships yarn, so the container takes the else branch; assert that every yarn install occurrence in that branch carries --frozen-lockfile, and separately that the nvm branch does too. Per occurrence, so a bare yarn install alongside a pinned one cannot hide.

3. Count per occurrence, keep the edges. Replace the per-line boolean plus continue with an occurrence count, and always run edge extraction on the same line. Extract the counter and the edge extractor so both can be asserted directly: yarn run prettier --check . && yarn run prettier --check src counts 2, and a line that both invokes prettier and calls script/lint still yields the script/lint edge.

4. Anti-vacuity. commandsOf throws when a node resolves to no commands (currently a missing package.json script silently yields [""]), plus explicit tests that an unknown make:/yarn:/script/ node throws and that a prettier-free subgraph reports 0, not a silently satisfied expectation.

5. Correct the stale make check comment at test/packaging/entrypoints.test.ts:3.

Verification. Every assertion in the file gets mutation-tested individually, including the ones already there, with the named test recorded for each. The two mutations from the issue body are reproduced first — the Dockerfile prettier append and the bare yarn install at the second bootstrap site — and shown to turn the suite red for their own reason. make check green, and script/cibuild green with per-layer timings and the epoch guard lines shown, so the pass is demonstrably executed rather than cached.

Out of scope, untouched: REPO_POLICIES.md drift and the LINT_EPOCH naming, both #32.

## Implementation plan Working on `next` in a fresh clone; one commit, landing on https://git.eeqj.de/sneak/quak/pulls/31. **1. Walk the path CI actually runs.** Add a `workflow:<path>` node kind whose commands are the `run:` steps of `.gitea/workflows/check.yml`, and root a walk there. That reaches `script/cibuild`, which reaches both `docker:Dockerfile.lint` (via `script/lint`) and `docker:Dockerfile` (the bare `docker build .`). Assertions: the CI path invokes prettier exactly once, and its reached set contains `script/cibuild`, `docker:Dockerfile` and `docker:Dockerfile.lint`. Rooting the walk in the workflow file rather than in a hand-picked entrypoint is what makes "the path CI executes" an assertion rather than an assumption. **2. Lockfile assertion targets the executed branch.** Resolve `install_js_deps` out of `script/bootstrap` and split it at the `missing yarn` guard, so the two install sites are separately addressable. `node:22-alpine` ships yarn, so the container takes the `else` branch; assert that every `yarn install` occurrence *in that branch* carries `--frozen-lockfile`, and separately that the nvm branch does too. Per occurrence, so a bare `yarn install` alongside a pinned one cannot hide. **3. Count per occurrence, keep the edges.** Replace the per-line boolean plus `continue` with an occurrence count, and always run edge extraction on the same line. Extract the counter and the edge extractor so both can be asserted directly: `yarn run prettier --check . && yarn run prettier --check src` counts 2, and a line that both invokes prettier and calls `script/lint` still yields the `script/lint` edge. **4. Anti-vacuity.** `commandsOf` throws when a node resolves to no commands (currently a missing `package.json` script silently yields `[""]`), plus explicit tests that an unknown `make:`/`yarn:`/`script/` node throws and that a prettier-free subgraph reports `0`, not a silently satisfied expectation. **5.** Correct the stale `make check` comment at `test/packaging/entrypoints.test.ts:3`. **Verification.** Every assertion in the file gets mutation-tested individually, including the ones already there, with the named test recorded for each. The two mutations from the issue body are reproduced first — the `Dockerfile` prettier append and the bare `yarn install` at the second bootstrap site — and shown to turn the suite red for their own reason. `make check` green, and `script/cibuild` green with per-layer timings and the epoch guard lines shown, so the pass is demonstrably executed rather than cached. Out of scope, untouched: `REPO_POLICIES.md` drift and the `LINT_EPOCH` naming, both https://git.eeqj.de/sneak/quak/issues/32.
Author
Collaborator

Done in 2bfa11c, pushed to next and carried by #31. The full mutation matrix is in the PR comment there.

1. The walk reaches script/cibuild. A workflow:<path> node kind resolves the run: steps of .gitea/workflows/check.yml, and a second walk is rooted there, so the graph under test is read out of the repo rather than assumed. It reaches script/cibuild, and through it docker:Dockerfile.lint and docker:Dockerfile. All four are asserted reached; the CI build is asserted to invoke prettier exactly once; and the test and build image is asserted to invoke it zero times. Appending RUN yarn run prettier --check . to Dockerfile after RUN make build now fires both of those — before the fix it left the suite green at 258/258, which I reproduced first.

2. The lockfile assertion targets the branch the container executes. install_js_deps is resolved out of script/bootstrap and split at its missing yarn guard. The container takes the else branch, and every yarn install occurrence in it must carry --frozen-lockfile; the nvm branch is asserted separately. Changing the second site to a bare yarn install now fires installs from the lockfile on the branch the container takes and nothing else — and mutating the nvm site instead fires only the other test, which is the cross-check that the two are genuinely distinguished rather than both matching a substring. A companion assertion requires the lint image to reach script/bootstrap at all, so the lockfile claims cannot end up describing a script the container never runs.

3. Counting is per occurrence. countPrettier counts matches on a line, and the continue is gone so edgesOf runs on counted lines too. RUN yarn run prettier --check . && yarn run prettier --check src in Dockerfile.lint now fires five separate count assertions; it scored 1 before.

4. The make check comment at test/packaging/entrypoints.test.ts:3 now says the suite and the lint container.

Vacuity. Checked explicitly rather than assumed, since the node.slice("make:")/NaN draft is the precedent. The yarn: resolver used to return [""] for a missing script — a silent zero of the same family; that and four other ways of reaching nothing (unknown Makefile target, missing script file, node with no commands, unknown node kind) now throw, and each is tested. Renaming the Makefile check: target makes the whole file error with no such Makefile target: check instead of quietly reporting zero invocations. A legitimately prettier-free subgraph (make:clean) is asserted to report 0.

Every assertion mutation-tested, all 35 — the ones I added and the ones already there. Each was changed to assert something else and confirmed to fail for its own named reason, with the failing set matching the mutated set exactly in every run. Nothing survived, so there is no assertion left that reads as a guarantee without being one. All mutations reverted; the tree is byte-identical to the committed one.

Verification. make check green. script/cibuild exit 0 and shown to have executed rather than been served: distinct epoch guard values per build, eslint 2.7s and prettier 4.5s in the lint image, make test 13.8s reporting 279 passed inside the test image, make build verifying the entrypoints. CACHED appears only on the bootstrap and dependency-install layers. Everything went through make targets and script/ entrypoints; no docker builder prune and no cache invalidation beyond the epoch arguments.

Done in `2bfa11c`, pushed to `next` and carried by https://git.eeqj.de/sneak/quak/pulls/31. The full mutation matrix is in the PR comment there. **1. The walk reaches `script/cibuild`.** A `workflow:<path>` node kind resolves the `run:` steps of `.gitea/workflows/check.yml`, and a second walk is rooted there, so the graph under test is read out of the repo rather than assumed. It reaches `script/cibuild`, and through it `docker:Dockerfile.lint` and `docker:Dockerfile`. All four are asserted reached; the CI build is asserted to invoke prettier exactly once; and the test and build image is asserted to invoke it zero times. Appending `RUN yarn run prettier --check .` to `Dockerfile` after `RUN make build` now fires both of those — before the fix it left the suite green at 258/258, which I reproduced first. **2. The lockfile assertion targets the branch the container executes.** `install_js_deps` is resolved out of `script/bootstrap` and split at its `missing yarn` guard. The container takes the `else` branch, and every `yarn install` occurrence in it must carry `--frozen-lockfile`; the nvm branch is asserted separately. Changing the second site to a bare `yarn install` now fires `installs from the lockfile on the branch the container takes` and nothing else — and mutating the *nvm* site instead fires only the other test, which is the cross-check that the two are genuinely distinguished rather than both matching a substring. A companion assertion requires the lint image to reach `script/bootstrap` at all, so the lockfile claims cannot end up describing a script the container never runs. **3. Counting is per occurrence.** `countPrettier` counts matches on a line, and the `continue` is gone so `edgesOf` runs on counted lines too. `RUN yarn run prettier --check . && yarn run prettier --check src` in `Dockerfile.lint` now fires five separate count assertions; it scored 1 before. **4.** The `make check` comment at `test/packaging/entrypoints.test.ts:3` now says the suite and the lint container. **Vacuity.** Checked explicitly rather than assumed, since the `node.slice("make:")`/`NaN` draft is the precedent. The `yarn:` resolver used to return `[""]` for a missing script — a silent zero of the same family; that and four other ways of reaching nothing (unknown Makefile target, missing script file, node with no commands, unknown node kind) now throw, and each is tested. Renaming the Makefile `check:` target makes the whole file error with `no such Makefile target: check` instead of quietly reporting zero invocations. A legitimately prettier-free subgraph (`make:clean`) is asserted to report 0. **Every assertion mutation-tested**, all 35 — the ones I added and the ones already there. Each was changed to assert something else and confirmed to fail for its own named reason, with the failing set matching the mutated set exactly in every run. Nothing survived, so there is no assertion left that reads as a guarantee without being one. All mutations reverted; the tree is byte-identical to the committed one. **Verification.** `make check` green. `script/cibuild` exit 0 and shown to have executed rather than been served: distinct epoch guard values per build, eslint 2.7s and prettier 4.5s in the lint image, `make test` 13.8s reporting 279 passed inside the test image, `make build` verifying the entrypoints. `CACHED` appears only on the bootstrap and dependency-install layers. Everything went through `make` targets and `script/` entrypoints; no `docker builder prune` and no cache invalidation beyond the epoch arguments.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/quak#33