Skip to content

fix(gate): unbreak the pytests gate β€” red on main since 2026-08-04, 4 causes - #356

Merged
ZacxDev merged 2 commits into
mainfrom
fix/unbreak-pytests-gate
Aug 6, 2026
Merged

fix(gate): unbreak the pytests gate β€” red on main since 2026-08-04, 4 causes#356
ZacxDev merged 2 commits into
mainfrom
fix/unbreak-pytests-gate

Conversation

@ZacxDev

@ZacxDev ZacxDev commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

The gate has been RED on main since 2026-08-04

nix build .#checks.x86_64-linux.pytests is the authoritative merge gate, and roughly a
dozen 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)

tier ref counts verdict
pytests main collected=6160 passed=6129 skipped=3 failed=28 RESULT: FAIL
pytests this branch collected=6162 passed=6159 skipped=3 failed=0 (floor 5600) RESULT: PASS
nodetests this branch suites=3 files=30 tests=1024 pass=1024 fail=0 skipped=0 (floor 970) RESULT: PASS

Reporting trap, for whoever reads this next: test_bash_guard.py prints its own
RESULT: all good, which lands before the runner's real verdict. A naive
grep "RESULT:" returns two subsystems interleaved, hook first. The numbers above come
from the runner's own final TOTAL/RESULT lines, cross-checked against the per-suite
SUMMARY block.

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_state
  • scripts/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_state
  • scripts/collector/keylog/tests/test_espanso_detect.py β€” 1:
    test_live_scraper_observes_the_real_config

After: 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. The
survivor is a genuine defect the FileNotFoundError had been hiding: test_status_no_timer
stubbed systemctl and nothing else, while monitor-blackout.sh resolves ddcutil at
line 20 β€” before it dispatches any subcommand β€” and exits 1 with ddcutil not found on PATH. The test asserted returncode == 0 against a script that had already bailed. It
passed on a workbench dev host only because a real ddcutil is on PATH there. The
module docstring says "All OFFLINE: no ddcutil, no systemctl"; that is now true.

A β€” 27 x FileNotFoundError on the env path (93caa3a, 2026-08-04).
Both new suites exec [<env-path>, "bash", <script>] as an argv. The nix build sandbox has
no such file, so subprocess raises before the script is reached β€” and their mock stubs
carried 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 stub
through testlib.mockbin.write_exec, which owns the shebang.

Not a bare "bash" argv, deliberately: several of these tests override PATH with a
stub-only directory, and subprocess does its PATH lookup in the env= you pass it β€” a
bare "bash" would make the interpreter itself unfindable.

B β€” 1 x AssertionError (4c3e913). That commit pruned the :date snippet from
nix/home.nix without updating the positive control that pinned it. The :date clause is
not 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_terms list is longer and therefore a stricter test of
the list-splitting regex. Pinned values were read off nix/home.nix, never off the
scraper's output.

Mutation-checked, both red at the re-pointed assertion: breaking _NIX_REC gives
scraper found only 0 snippets; breaking _NIX_TERMS fails the search_terms pin.

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.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
β€” failed=0 alone would not have turned this gate green.

Pinned in EXPECTED_SKIPS on exactly the is_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.py is a repo-wide structural guard whose entire
purpose is making this class impossible. It watched 93caa3a land and stayed green for
two 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:

  1. a shebang opening its own line inside a multi-line textwrap.dedent body;
  2. the interpreter path as a plain argv element β€” not a shebang at all, and the shape that
    actually produced all 27 failures.

testlib.shebang_scan.line_is_offender now enumerates all three, each with its own
positive control. Measured against main's copies of the two files:

WIDENED scan hits on main's files: 6
   test_monitor_blackout.py:26  (argv element)
   test_monitor_blackout.py:39  (bare shebang line)
   test_monitor_blackout.py:55  (bare shebang line)
   test_monitor_blackout.py:221 (bare shebang line)
   test_rig_control.py:27       (argv element)
   test_rig_control.py:37       (bare shebang line)
OLD one-shape scan hits on the same files: 0 []

Red at main, green at HEAD β€” and the 0-vs-6 is what shows the widening is doing the
work rather than a coincidence.

One allowlist entry added: test_guard_core.py's parametrize list of wrapper-binary
strings, 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_itself covers it.

What I could NOT fix

Cause C is accounted for, not repaired. Those two test_skill_audit.py assertions are
keyed 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.md files into
this 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.py and test_monitor_blackout.py
are weak beyond the shebang problem β€” several tests are if path.exists(): assert ... or
bare 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

ZacxDev and others added 2 commits August 6, 2026 12:26
…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>
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