fix(gate): unbreak the pytests gate β red on main since 2026-08-04, 4 causes - #356
Merged
Conversation
β¦ain since 2026-08-04 The authoritative gate has been RED on main for two days and ~a dozen PRs merged through it. Measured against committed main: TOTAL collected=6160 passed=6129 skipped=3 failed=28 RESULT: FAIL THREE independent causes, none of which is fixed by silencing a test. A. 27 x FileNotFoundError: '/usr/bin/env' (93caa3a, 2026-08-04) scripts/tests/test_rig_control.py and test_monitor_blackout.py exec [<env-path>, "bash", <script>] as an argv. The nix build sandbox has no /usr/bin/env, so subprocess raises before the script is ever reached β and their mock stubs carried the same interpreter in a shebang, which would have failed next. The interpreter is now resolved ONCE via shutil.which() to an absolute path (not a bare "bash": several tests override PATH with a stub-only directory, and PATH lookup uses the env= passed to subprocess), and every stub goes through testlib.mockbin.write_exec, which owns the shebang. B. 1 x AssertionError in scripts/collector/keylog/tests/test_espanso_detect.py 4c3e913 pruned the :date snippet from nix/home.nix without updating the positive control that pinned it. The :date clause is NOT deleted β that would defeat the control. It is re-pointed at :sshwn (added in #134, untouched since), whose search_terms list is longer and therefore a stricter test of the list-splitting regex. The pinned values are read off nix/home.nix, never off the scraper's output. C. 2 x UNPINNED SKIP (#332, 2026-08-04 β the first breakage of the day) test_skill_audit.py added two regression pins against the live datapacket-talos skill corpus, a private clone that cannot exist in the sandbox. Both skip there, neither was pinned, and GUARD 2 failed the run independently of any failing test. Pinned in EXPECTED_SKIPS on exactly the is_dir() predicate the tests use, so on a host WITH the clone they still run and nothing is pinned. Flagged in-place as the weak shape it is. ROOT CAUSE OF (A) β the guard that should have caught it test_runtime_shebangs.py exists to make this class impossible, and it was green for the whole two days. It saw ONE shape: a shebang directly behind a quote, i.e. a one-line stub body. Both new files carry the hazard in two shapes it could not see: * a shebang opening its own line inside a multi-line textwrap.dedent body; * the interpreter path as a plain argv element, not a shebang at all. testlib.shebang_scan.line_is_offender now enumerates all three, each with its own positive control. Verified red on main's version of both files and green after. The scanner's own needle is assembled from character codes for the third shape too β spelling it literally made the scanner its own first offender (observed). VERIFICATION main : collected=6160 passed=6129 skipped=3 failed=28 RESULT: FAIL branch : see PR body for the counted numbers on both tiers. No test skipped, xfailed, deleted or weakened; collected count does not drop. Mutation-checked: breaking the espanso record regex and breaking the search_terms regex each turn the re-pointed control RED. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Exposed only once the FileNotFoundError above it was fixed and this suite could actually RUN in the nix sandbox. monitor-blackout.sh resolves ddcutil at line 20 β before it dispatches any subcommand β and exits 1 with "ddcutil not found on PATH" when it is absent. test_status_no_timer stubbed systemctl and nothing else, so it asserted `returncode == 0` against a script that had already bailed. On a workbench dev host it passed because a REAL ddcutil is on PATH; the sandbox has none. The module docstring claims "All OFFLINE: no ddcutil, no systemctl" β it was not. Adding the stub makes that claim true. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The gate has been RED on
mainsince 2026-08-04nix build .#checks.x86_64-linux.pytestsis the authoritative merge gate, and roughly adozen PRs merged through it while it was failing. A permanently-red gate is worse than no
gate β two agents and Zach each had to hand-diff failing-test-name sets to prove their own
work wasn't the cause.
Nothing here is skipped, xfailed, deleted or weakened. The collected count goes UP.
Counted verdicts (never an exit code)
maincollected=6160 passed=6129 skipped=3 failed=28RESULT: FAILcollected=6162 passed=6159 skipped=3 failed=0(floor 5600)RESULT: PASSsuites=3 files=30 tests=1024 pass=1024 fail=0 skipped=0(floor 970)RESULT: PASSReporting trap, for whoever reads this next:
test_bash_guard.pyprints its ownRESULT: all good, which lands before the runner's real verdict. A naivegrep "RESULT:"returns two subsystems interleaved, hook first. The numbers above comefrom the runner's own final
TOTAL/RESULTlines, cross-checked against the per-suiteSUMMARYblock.Failing-test name set, before -> after
Before (28, all gone after):
scripts/tests/test_monitor_blackout.pyβ 11:test_no_args_blackout_default_duration,test_restore_reads_state_and_restores,test_restore_uses_default_brightness_when_no_state,test_status_no_timer,test_get_brightness_returns_value,test_fade_blackout_saves_state,test_fade_calls_setvcp_multiple_steps,test_fade_restore_reads_state,test_blackout_custom_duration,test_blackout_fails_when_no_monitor,test_restore_with_corrupt_statescripts/tests/test_rig_control.pyβ 16:test_unknown_subcommand_exits_2,test_status_awake_when_no_state,test_status_sleeping_when_statefile_says_sleeping,test_sleep_writes_state_file,test_sleep_calls_rgb_off_and_blackout,test_wake_writes_state_file,test_rgb_on_subcommand,test_rgb_off_subcommand,test_blackout_subcommand,test_restore_subcommand,test_sleep_then_status_shows_sleeping,test_wake_then_status_shows_awake,test_gui_exits_nonzero_when_yad_missing,test_state_dir_created_on_status,test_status_with_corrupt_state,test_status_with_empty_statescripts/collector/keylog/tests/test_espanso_detect.pyβ 1:test_live_scraper_observes_the_real_configAfter: empty.
Added (net +2 collected):
test_positive_control_the_scan_finds_a_bare_shebang_line,test_positive_control_the_scan_finds_an_argv_element.Four causes β the fourth only became visible after the first was fixed
The first branch build came back
collected=6162 passed=6158 skipped=3 failed=1. Thesurvivor is a genuine defect the
FileNotFoundErrorhad been hiding:test_status_no_timerstubbed
systemctland nothing else, whilemonitor-blackout.shresolvesddcutilatline 20 β before it dispatches any subcommand β and exits 1 with
ddcutil not found on PATH. The test assertedreturncode == 0against a script that had already bailed. Itpassed on a workbench dev host only because a real
ddcutilis onPATHthere. Themodule docstring says "All OFFLINE: no ddcutil, no systemctl"; that is now true.
A β 27 x
FileNotFoundErroron theenvpath (93caa3a, 2026-08-04).Both new suites exec
[<env-path>, "bash", <script>]as an argv. The nix build sandbox hasno such file, so
subprocessraises before the script is reached β and their mock stubscarried the same interpreter in a shebang, which would have failed next. Fixed by resolving
the interpreter once via
shutil.which()to an absolute path, and routing every stubthrough
testlib.mockbin.write_exec, which owns the shebang.Not a bare
"bash"argv, deliberately: several of these tests overridePATHwith astub-only directory, and
subprocessdoes its PATH lookup in theenv=you pass it β abare
"bash"would make the interpreter itself unfindable.B β 1 x
AssertionError(4c3e913). That commit pruned the:datesnippet fromnix/home.nixwithout updating the positive control that pinned it. The:dateclause isnot deleted β it exists so a scraper regex that silently matched nothing cannot make
the three guards below it vacuously true. It is re-pointed at
:sshwn(added in #134,untouched since), whose
search_termslist is longer and therefore a stricter test ofthe list-splitting regex. Pinned values were read off
nix/home.nix, never off thescraper's output.
Mutation-checked, both red at the re-pointed assertion: breaking
_NIX_RECgivesscraper found only 0 snippets; breaking_NIX_TERMSfails thesearch_termspin.C β 2 unpinned skips (#332, 2026-08-04 15:44 β actually the first breakage of the
day, and not in the original bug report).
test_skill_audit.pyadded two regression pinsagainst the live datapacket-talos skill corpus, a private clone that cannot exist in the
sandbox. Both skip there, neither was pinned, and GUARD 2 failed the run independently
of any failing test β
failed=0alone would not have turned this gate green.Pinned in
EXPECTED_SKIPSon exactly theis_dir()predicate the tests themselves use,so on a host with the clone the tests still run and nothing is pinned. See "could not
fix" below β this one is honest accounting, not a repair.
Root cause of A: the guard that existed to prevent it was green throughout
scripts/tests/test_runtime_shebangs.pyis a repo-wide structural guard whose entirepurpose is making this class impossible. It watched
93caa3aland and stayed green fortwo days, because it could see one shape: a shebang directly behind a quote, i.e. a
one-line stub body. Both new files carry the hazard in two shapes it could not see:
textwrap.dedentbody;actually produced all 27 failures.
testlib.shebang_scan.line_is_offendernow enumerates all three, each with its ownpositive control. Measured against
main's copies of the two files:Red at
main, green at HEAD β and the 0-vs-6 is what shows the widening is doing thework rather than a coincidence.
One allowlist entry added:
test_guard_core.py's parametrize list of wrapper-binarystrings, which are fed to
gc.evaluate()as text and never resolved.Self-match note: spelling the new needle as a quoted literal made the scanner its own
first offender (observed, then fixed) β it is assembled from character codes like the
others, and
test_this_guards_source_does_not_match_itselfcovers it.What I could NOT fix
Cause C is accounted for, not repaired. Those two
test_skill_audit.pyassertions arekeyed to an out-of-repo absolute path, which is precisely the shape
run-tests.sh's own"REMOVED, deliberately" comment argues against a few lines below the entry I added: a
check keyed to a foreign clone is structurally unobservable in the tier that gates merges.
Making them hermetic would mean vendoring another (private) repo's
SKILL.mdfiles intothis public repo, which is worse. So they are pinned β conditionally, on the test's own
predicate, with the weakness flagged in place. If that check ever needs re-pointing, point
it at something tracked in this repo rather than widening the pin.
Not in scope, but worth a follow-up:
test_rig_control.pyandtest_monitor_blackout.pyare weak beyond the shebang problem β several tests are
if path.exists(): assert ...orbare
assert True, so they pass whether or not the script did anything. They now run;they do not yet prove much.
π€ Generated with Claude Code