Skip to content

fix(gate): add nix to the pytests check β€” 10 handle tests could not run in the sandbox - #290

Merged
ZacxDev merged 1 commit into
mainfrom
fix/pytests-gate-needs-nix
Aug 2, 2026
Merged

fix(gate): add nix to the pytests check β€” 10 handle tests could not run in the sandbox#290
ZacxDev merged 1 commit into
mainfrom
fix/pytests-gate-needs-nix

Conversation

@ZacxDev

@ZacxDev ZacxDev commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

nix flake check is green end to end with this. Third and last of the three regressions #276 shipped.

The bug

scripts/tests/test_opencode_config.py's nix_eval() shells out to nix-instantiate --eval to pin the generated handle values from nix/agent-handles.nix, and calls pytest.fail() β€” not skip β€” when the binary is absent. That is deliberate and correct:

"a skip here is how a wrong kubeconfig path ships"

nix was not in the pytests check's nativeBuildInputs, so all 10 test_handles_resolve_to_the_exact_expected_paths[...] cases failed in the nix sandbox and passed on every dev host.

It was invisible until #289 β€” it hid behind the gate being red for an unrelated reason. Its sibling is the exact complement: session_insight's test_patterns_cover_bash_guard fails only where ~/.claude/hooks/bash-guard.py is deployed, and skips in the sandbox. So #276's three regressions each concealed the others; that lesson is in #287.

Measured, not predicted

I argued in review that agent-handles.nix is { home }: {…} with no nixpkgs import and no fetch, so --eval is a pure evaluation needing no daemon, network or store realisation. That was a prediction. This is the check's own output:

collected passed failed
scripts/tests before 959 949 10
scripts/tests after 959 959 0
TOTAL collected=4353  passed=4351  skipped=2  failed=0   (floor: 2850)
---- skips (pinned: 2) ----   β†’ observed 2, reconciled
RESULT: PASS

nodetests:  files=14  tests=468  pass=468  fail=0  skipped=0   (floor: 450)
nix flake check β†’ all checks passed

Identical collected count with failures to zero β€” the 10 pass, they did not merely stop running. That was the distinction asked for, and a count is the only thing that shows it: a target that is accepted but not executed looks the same from outside.

The diagnosis was confirmed by a positive control first, on a dev host: running that file with PATH stripped to python alone reproduced exactly 10 failed, 455 passed, naming the same 10 tests.

Cost, stated as deliberately as the nodejs paragraph above it

  • +30 store paths, 22 of them the nix closure β€” nix-{util,store,expr,main,flake,fetchers,cmd} including -dev outputs, plus boost and libarchive.
  • A nixpkgs bump that moves nix now invalidates this check's build cache β€” the same trade already accepted for nodejs.

Bought: 10 tests that structurally cannot run without it, pinning that the handles every agent shell exports ($DEVRC, $HOMELAB, $KC_*, …) resolve to the exact expected paths.

Rejected alternative β€” DEVHOST_TARGETS. It keeps the closure small, but weakens the pin to "runs only where someone remembers to run it", which is precisely the failure mode this whole area keeps hitting. Preserving fail-not-skip was the point.

Also

nix-instantiate added to REQUIRED_TOOLS, so a missing binary becomes one named precondition failure rather than 10 unexplained assertion failures deep in the run β€” that guard's entire purpose. It binds the pre-push tier too, which is safe here: both hosts are NixOS.

Scope

Branched off 88eb6d0, not stacked on anything β€” it touches flake.nix where #289 touched run-tests.sh. Deliberate: stacking buys nothing for orthogonal changes and carries the gh pr merge --delete-branch hazard that auto-closes a child PR GitHub then refuses to reopen.

Process note

The wrapper around this run reported exit 0 for the pipeline while the meaningful status lived in the log β€” $? read the trailing echo. Every number above is read from the build log, not from an exit status. Same class as the rc=$? case documented in #287.

πŸ€– Generated with Claude Code

…un in the sandbox

scripts/tests/test_opencode_config.py's `nix_eval()` shells out to
`nix-instantiate --eval` to pin the GENERATED handle values from
nix/agent-handles.nix, and calls pytest.fail() β€” NOT skip β€” when the
binary is absent, deliberately: 'a skip here is how a wrong kubeconfig
path ships'. `nix` was not in the check's nativeBuildInputs, so all 10
test_handles_resolve_to_the_exact_expected_paths[...] cases failed in the
nix sandbox and passed on every dev host.

This was INVISIBLE until #289: it hid behind the gate being red for an
unrelated reason. Its sibling defect is the exact complement β€”
session_insight's test_patterns_cover_bash_guard fails only where
~/.claude/hooks/bash-guard.py is DEPLOYED and skips in the sandbox. Both
came from #276.

MEASURED, not predicted (the pure-eval argument was a prediction; this is
the check's own output):

  scripts/tests before   collected=959 passed=949 failed=10
  scripts/tests after    collected=959 passed=959 failed=0
  TOTAL                  collected=4353 passed=4351 skipped=2 failed=0
  skips                  pinned 2, observed 2
  nodetests              files=14 tests=468 pass=468 fail=0
  nix flake check        all checks passed

Identical collected count with failures to zero β€” the 10 PASS rather than
merely stopping to fail. The 10-failure diagnosis was confirmed first by a
positive control on a dev host: stripping PATH to python alone reproduced
exactly '10 failed, 455 passed' in that file.

COST, stated as deliberately as the nodejs one above it: +30 store paths,
22 of them the nix closure (nix-{util,store,expr,main,flake,fetchers,cmd}
incl. -dev outputs, plus boost and libarchive), and a nixpkgs bump moving
`nix` now invalidates this check's cache β€” the same trade already
accepted for nodejs. Bought: 10 tests that structurally cannot run
without it, pinning that the handles every agent shell exports resolve to
exact paths. Rejected DEVHOST_TARGETS as the alternative: it keeps the
closure small but weakens the pin to 'runs only where someone remembers
to run it', which is the failure mode this whole area keeps hitting.

Also adds nix-instantiate to REQUIRED_TOOLS so a missing binary is ONE
named precondition failure instead of 10 unexplained assertion failures
deep in the run β€” that guard's entire purpose.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ZacxDev

ZacxDev commented Aug 2, 2026

Copy link
Copy Markdown
Contributor Author

Also green on the merged tree with #291

Beyond this PR's own green (collected=4353 passed=4351 skipped=2 failed=0, scripts/tests 959/959), I built an integration branch off main merging this PR + #291 and ran the gate there:

PASS  scripts/tests                                  (collected=959 passed=959 skipped=0)
PASS  scripts/session-analysis/session_insight/tests (collected=57  passed=57  skipped=0)
TOTAL collected=4353  passed=4352  skipped=1  failed=0   (floor: 2850)
RESULT: PASS   |   nix flake check -> all checks passed

Both PRs touch scripts/run-tests.sh β€” this one adds nix-instantiate to REQUIRED_TOOLS, #291 removes a conditional EXPECTED_SKIPS entry. Different regions, ort auto-merged, and I read the merged result to confirm it is semantically coherent rather than just textually clean.

So the two are safe to land in either order as a pair; on current main, #291 alone would leave the sandbox red for this PR's reason, so this one should land first.

@ZacxDev
ZacxDev merged commit 8cf6748 into main Aug 2, 2026
@ZacxDev
ZacxDev deleted the fix/pytests-gate-needs-nix branch August 2, 2026 22:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant