test(nix): drive the installed binary through the shared tier (task 0038)
All checks were successful
CI / test (pull_request) Successful in 53s
CI / test (push) Successful in 54s

The Nix build now runs the installed-binary facet of the packaging tier
against the binary it has just produced, via an `installCheckPhase` that
sets `GITEA_AXI_INSTALLED_BIN` to `$out/bin/gitea-axi` and runs the new
`test:installed` script. Running after `fixupPhase` means the binary
under test is the wrapped one an operator actually gets, and naming it is
all the phase does — the assertions stay in the shared tier, so the npm
and Nix installation paths cannot drift apart in what they guarantee.

`npmInstallHook` prunes dev dependencies out of the build tree before the
check runs, so `preInstall` snapshots `node_modules` with `cp -al` and the
check restores it by copying, leaving the snapshot intact for a replayed
phase.

Two defects fixed in passing, both surfaced by review:

`test:installed` pins a test file by path while the packaging runner sets
`passWithNoTests: true`, so moving that file would have taken the build
green having asserted nothing — the same silently-inert trap `doCheck`
sprang in task 0037. The script now passes `--passWithNoTests=false`.

`checkPhase`'s vitest left a timestamped run cache under
`node_modules/.vite`, which `npmInstallHook` copied into `$out`, shipping
a stray cache and making the derivation non-reproducible. It is now
removed before the install phase; `nix build --rebuild` passes.
This commit was merged in pull request #47.
This commit is contained in:
2026-07-19 23:23:01 -04:00
parent 5a2874d11c
commit 82cdad4ce0
6 changed files with 131 additions and 5 deletions

View File

@@ -15,8 +15,57 @@ The check reuses the seam from the parameterized tier; it does not introduce a s
## Acceptance criteria
- [ ] The Nix build drives the installed binary through the shared installed-binary tier after installation.
- [ ] A binary installed without its executable bit fails the build.
- [ ] A bundled Agent Skill installed at the wrong location relative to the built output fails the build.
- [ ] The post-install phase adds no assertions of its own beyond pointing the shared tier at the installed binary.
- [ ] `nix build` still succeeds end to end on a clean checkout.
- [x] The Nix build drives the installed binary through the shared installed-binary tier after installation.
- [x] A binary installed without its executable bit fails the build.
- [x] A bundled Agent Skill installed at the wrong location relative to the built output fails the build.
- [x] The post-install phase adds no assertions of its own beyond pointing the shared tier at the installed binary.
- [x] `nix build` still succeeds end to end on a clean checkout.
## Implementation Notes
### Where the check hangs
`installCheckPhase`, not `postInstall`.
It runs after `fixupPhase`, which is where `wrapProgram` has already done its work — so the binary the tier drives is the wrapped one an operator actually gets, not the bare entrypoint.
The phase sets `GITEA_AXI_INSTALLED_BIN=$out/bin/gitea-axi` and runs `npm run test:installed`, a new script that runs the installed-binary facet alone under the packaging runner configuration.
Naming the binary is all it does; the assertions stay in the tier, satisfying the fourth criterion by construction.
`vitest.packaging.config.ts` had to join the source allowlist in `package.nix` — exactly the Gotcha task 0037 recorded, hit on the first build.
### Dev dependencies are gone by install time
`npmInstallHook` runs `npm prune --omit=dev` against the build tree's `node_modules` during `installPhase`, so vitest no longer exists when `installCheckPhase` runs.
`preInstall` snapshots the tree with `cp -al` first — hardlinks, so it costs neither time nor space, and the prune's deletions do not follow through to the copy — and the check restores it.
Restoring copies rather than moves, so a replayed phase (`--keep-failed` debugging) does not consume the only surviving snapshot.
Recorded as a Gotcha, since the failure is disconnected from its cause.
### Criterion 2 holds, but by a different mechanism than the task assumed
The task motivates the executable-bit guard with "the compiler does not set the executable bit that npm would otherwise set from the manifest's `bin` entry".
That is true of the npm path and *not* of the Nix path: `nodejsInstallExecutables` installs each `bin` entry as a generated wrapper invoking `node <path>`, not as a symlink to the entrypoint.
So `chmod -x` on `dist/main.js` changes nothing — verified, the build stayed green — while `chmod -x` on `$out/bin/gitea-axi` fails all three tests with `EACCES`.
The criterion as written is therefore satisfied, and was demonstrated by probe, but the bit it protects under Nix is one `makeWrapper` always sets.
The assertion earns its keep on the npm caller, where the failure the task describes is real.
This is an argument for the shared tier rather than against it: neither installation path gets to pick which guarantees it feels like offering.
Both mechanism and probe results are recorded as a Gotcha.
The Skill-location criterion is the one doing real work under Nix — moving the installed `skills` directory aside fails the `setup` test and the build.
### Two defects found and fixed in passing
Both were surfaced by the review pass rather than planned.
`test:installed` pins a test file by path, and the packaging runner sets `passWithNoTests: true`.
Renaming or moving that file would have made vitest match nothing, exit 0, and take `nix build` green having asserted nothing — the same silently-inert trap that `doCheck` sprang in task 0037.
The script now passes `--passWithNoTests=false`.
`nix build --rebuild` reported the derivation "may not be deterministic".
The cause predates this task: `checkPhase`'s vitest leaves a run cache at `node_modules/.vite/…/results.json` recording durations and timestamps, and `npmInstallHook` copies `node_modules` into `$out` wholesale — so every build shipped a stray cache in the closure and no two outputs matched.
`checkPhase` now removes it, and `--rebuild` passes.
Strictly this belonged to 0037, but this task adds a second vitest run over the same surface, and the fix is one line in a file already being edited.
### Scope note
`.gitignore` gains `result` / `result-*`.
That is 0037's flake output rather than this task's, but the symlink appears the moment anyone runs `nix build` without `--no-link` and was already showing up as untracked.