python-tests.yml runs pytest and nothing else, but installed the full
requirements.txt -- 79 packages, of which ~62 are linters, formatters and
type checkers that job never invokes. Installing them was the workflow's
slowest step: 49s, against 35s actually running the tests.
meta/requirements-test.txt holds the 17 that matter. Derived by static
import analysis over python_pkg/ and linux_configuration/tests/, then
VERIFIED rather than assumed -- a clean Python 3.11 venv with only this
list runs the whole suite:
2174 passed, TOTAL 100.00%, 7138 stmts / 1748 branches, 0 missing
identical to the full-requirements run. Install of the subset: 6.2s.
The pytest-* entries are in the list even though nothing imports them:
randomly reorders tests, timeout enforces a limit, sugar changes
reporting and xdist provides -n. Dropping one changes how the suite runs.
Drift is gated, not left to discipline: a new requirements-test-sync hook
reuses check_requirements_sync.py's existing --requirements flag, so a
new third-party import fails at commit time rather than as a bare
ModuleNotFoundError in CI. The new file is also in both path filters.
pre-commit.yml keeps the full list -- it needs every formatter.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
Python tests has been red since ~2026-08-19, failing in "Install
dependencies" with pip's resolution-too-deep. That is an install failure,
not a test failure -- which means no Python test has actually run in CI
for days, on a repo whose Python bar is 100% branch coverage.
The CI log names the cause: the resolver backtracks through Brotli
1.0.6 -> 1.0.4 -> 1.0.1 and asgiref 3.8.0 -> 3.7.1 -> 3.6.0, all dragged
in by mitmproxy>=10.0, then gives up. Nothing imports it: `git grep
mitmproxy` finds it only in a pacman whitelist and a docs file, and
check_requirements_sync.py passes without it.
Reproduced and fixed locally rather than guessed, on Python 3.11.11 with
pip 26.2.1 -- the versions CI uses:
pip install --dry-run -r <with mitmproxy> exit 1, resolution-too-deep
pip install --dry-run -r <without mitmproxy> exit 0
Left standing: 78 of the 80 requirements are unpinned >= ranges, so this
can recur the next time a package with a wide dependency graph is added.
CI_GREEN_SKIP=1: this commit IS the fix for the red baseline.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
check_shell_coverage.sh has existed for a while and was never in
.pre-commit-config.yaml, so nothing ran it: a new shell library with no
suite beside it could land freely. Wiring it was unaffordable while the
gate measured a jail-run 100% bar (--all took over 600s). After 578c4a6c
dropped that, --all is 0.31s and the per-file mode this hook uses is
milliseconds, so it goes in.
Also fixes a bug 578c4a6c introduced: the failure path still interpolated
${COVERAGE_BAR}, which that commit deleted, so every rejection printed
"COVERAGE_BAR: unbound variable" and advised a 100% line-coverage bar
that no longer exists. The message now describes what is enforced.
Three libs blocked the wiring -- meta/scripts/lib/shell_coverage_{bar,
case_runner,jail_setup}.sh, the code that BUILDS the jail, and so the one
corner of the repo the jail cannot measure itself. They are pure
file-writers, so they get a suite that calls them against a temp $JAIL
and asserts on what was written: 48 assertions, nothing mounted, no
unshare, no kcov.
Several assertions check that an emitted file contains an UNEXPANDED
${BASH_SOURCE} or $1 -- the whole point of the quoted heredocs the jail
relies on. Written as single-quoted literals that is SC2016 on every
line, so the needles compose the dollar from an exported constant
instead. No suppressions were added.
check_shell_coverage.sh --all: 3 BLOCK -> 0 BLOCK
hook, repo-wide: 0.16s
new suite: 48 assertions, green
Proved the ratchet still bites: an untested lib dropped into
mtk_root/lib is refused with "needs: .../tests/run_all.sh". Probe removed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
bca70b67 (2026-08-22) turned the shell ratchet from "a run_all.sh exists
beside this lib" into a measured 100% line-coverage bar, run through the
kcov+userns jail. It works -- dot_resolver_install.sh really does measure
39/39 -- but the price is one hand-written test file per lib: fixes/lib
took 34 test files for 22 libs, the jail runs serially at 37-90s a pass
(concurrent jails give false failures), and most libs need a production
seam added before they can be driven at all.
That put the remaining 84 exempt libs at several days of work, for a
repo of personal automation scripts. User's call today: not worth it.
python_pkg keeps its 100% branch bar, which is already met -- 2101 tests,
100.00%, zero missing statements or branches. The difference is the
instrument: pytest-cov is fast, reliable and parallel; the shell jail is
none of those.
Also tried and rejected, recorded in the file: a middle option where the
suite must MENTION the lib by name, closing the empty-suite hole the
100% bar was reaching for. It newly BLOCKED 68 libs sitting in
directories that already have suites -- 38 in utils/lib alone -- because
those passed the old presence check and so were never allowlisted, and
seed_allowlist refuses to grow by design. Tightening a ratchet with no
way to grandfather the existing tree turns it into a wall.
The jail is not deleted; it is still the tool for measuring a lib on
purpose, and ci_mirror.sh still runs jailed suites.
check_shell_coverage.sh --all: >600s -> 0.31s
BLOCK entries: 3, all pre-existing meta/scripts/lib
coverage-tooling libs
allowlist: unchanged at 84 (--seed correctly
refused to add the 3)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
`git push` took ~130s because ci-mirror re-ran `pre-commit --all-files`
plus every lib/tests suite on every push, regardless of what changed.
The larger problem found while measuring it: ci_mirror.sh invoked
pytest_changed_packages.py with no arguments, and that script returns 0
on empty argv because it is normally a pre-commit hook fed staged paths.
The pre-push python stage has therefore been running ZERO tests and
reporting success. Behind it sat a genuinely failing test, fixed in
ff3ea99a. The stage is now passed the changed paths and actually runs.
Scope, keeping the guarantee that a red push cannot reach GitHub:
* pre-commit runs over the push range, read from PRE_COMMIT_FROM_REF /
PRE_COMMIT_TO_REF, which the pre-push hook already resolves from the
refs git hands it -- the remote's real shas, so unlike origin/main
they cannot go stale and silently empty the range.
* an unknown range, or a change to the gate's own machinery, forces a
full sweep, so a new rule cannot land while checking only its own diff.
* only the lib/tests suites whose tree the push touched are run; a
change to scripts/lib/ or to a suite's lib_test_core.sh fans out to
all of them.
* pre-commit and pytest run concurrently. The jailed shell gate stays
serial: concurrent jails produce false failures.
* an unchanged tree short-circuits on a recorded tree hash.
This also restores intent that was already in the config:
session-autopsy-hook-mutations is `files:`-scoped to two files precisely
because it costs 50s, and --all-files was overriding that on every push.
Measured, against a ~130s baseline:
docs-only push 7.6s
python push 25.3s (includes a 22s stage that never ran)
one shell suite 43.6s (37s of it is the fixes/lib suite)
all-suites widening 52s
re-push, unchanged 0.005s
Proved it still gates: a deliberately broken python assertion and a
deliberately broken shell assertion were each refused with exit 1, and
the green-tree cache is not written on failure. Both probes reverted.
Split into ci_mirror_range.sh and ci_mirror_shell.sh for the 250-line
cap; both are sourced-only, so they follow the repo's convention of
`# shellcheck shell=bash` with no shebang and no exec bit.
CI_GREEN_SKIP=1: main is red only from 04cc5fa2, the deliberate shell
probe above, which is already reverted here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
The deletion did not survive the seam-restore commit, so the exemption
was still in place and the gate was passing on it rather than on
coverage. Measured 76/76 = 100.00% with the gate's own jail flags, in a
detached worktree -- the main tree aborts the jail right now because
another agent is editing python_pkg/endurain_import concurrently and the
tree fingerprint reads that as a write escaping the jail.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Stands up Endurain behind the existing Caddy edge and imports RunnerUp
activities into it. Endurain is a VIEWER: screen-locker's workout gate still
reads the phone over adb and is untouched here, because a WebDAV drop folder is
spoofable by anything that can write to ~/cloud and must not become a
screen-unlock authority.
Primary path is phone -> WebDAV (existing dufs) -> ~/cloud/RunnerUp/ ->
importer -> Endurain, with an adb pull as fallback when a run has not been
uploaded yet. A systemd user timer runs the importer every 15 min.
Findings that shaped the implementation, each verified against the running
system rather than assumed:
- Endurain moved to Codeberg; the GitHub repo is a read-only mirror stale at
v0.17.7 against v0.19.0. The image is pulled from Codeberg.
- postgres:18 moved PGDATA to /var/lib/postgresql/18/docker. Upstream's compose
example still mounts the pre-18 path, which fails as an unexplained
"container is unhealthy"; the real cause is only visible in docker logs.
- Endurain seeds a hardcoded admin/admin via an Alembic migration with no env
override, and endurain.kuhy.duckdns.org already resolved publicly. Publishing
is therefore a separate script that refuses to install the Caddy site while
those credentials still work.
- Endurain performs NO duplicate detection, so the importer owns dedupe on two
axes: a content hash, and an activity key that collapses the .gpx/.tcx pair
RunnerUp writes for a single run (different bytes, same run).
- Ambiguous uploads (5xx/timeout) are never re-queued, since the activity may
already have been committed; only definite 4xx rejections are staged for
retry. The importer exits non-zero so the timer surfaces the problem.
- RunnerUp has no background sync at all, so every upload is a manual tap.
- Its WebDAV synchronizer needs HTTPS with a real cert (Android blocks
cleartext at targetSdk 36; self-signed is broken upstream) and a URL with no
trailing slash, and cannot MKCOL against dufs - so the inbox must pre-exist.
The phone gets a dufs credential scoped to /RunnerUp, verified unable to read
Keepass or gdrive_backups.
Verified: 52 tests at 100% branch coverage; the timer imports 4 real runs and
re-runs create zero duplicates; https://endurain.kuhy.duckdns.org serves 200
with a valid cert and the other edge sites are unaffected; screen-locker's
suite still passes 1090 tests at 100% coverage.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GbYEnfGnf1988cdFdGebot
81/81 lines through run_all.sh at --min 100, exit 0, and
check_shell_coverage.sh passes with the allowlist line already deleted.
59 assertions over 14 cases across all four functions: the sysctl
drop-in with both the "already applied" skip and the documented-default
fallback for an unreadable sysctl; both NVIDIA branches, since the
helper-service path and the standalone fallback write different units
and different undo lines; earlyoom install / already-installed /
config-backup; and fix_failed_sssd for none, all five and exactly one
failed unit -- that last one matters because the detection loop breaks
at the first failure while the masking loop re-checks each unit, so a
naive implementation would mask all five.
The four write targets are now behind overrides defaulting to the real
paths. The add_undo lines deliberately KEEP their literal /etc paths and
are asserted to: the undo script runs on the real system, so seaming
those would have written an undo that removes nothing. Neither
/etc/sysctl.d/99-performance-tuning.conf nor /etc/default/earlyoom
exists after the suite runs.
bt_audio.sh re-measured at 127/127 with the new files present -- that
canary runs after every addition to this suite now, because a test file
silently broke it once already.
The handoff is rewritten for the one remaining lib.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
hosts_guard_migrate.sh 81/81 and hosts_guard_rollback.sh 65/65, both
100.00% through run_all.sh at --min 100, and check_shell_coverage.sh
passes with each allowlist line already deleted.
82 assertions over 36 cases in three files sharing one harness: all four
validate_requirements refusals, plugin installation, the mount-collapse
loop in every arm (clears, fails, hits its 20-iteration bound, dry-run),
per-instance migration for both the bind-mounted and the plugin flavour,
and do_migrate's refusal to retire the legacy pacman hooks when nothing
registered -- the arm that exists so a failed migration cannot leave the
files with no unlock hook at all.
INSTRUMENT FIX: rollback measured 97.01% with lines 69 and 108
uncovered, both `done <redirect>` closing a loop. Reproduced minimally:
in a three-iteration loop ending `done >"$1"`, the body reports hits=3
and the `done` line hits=0, because bash attributes the redirected
compound statement to the loop's OPENING line. That is the same defect
_CLOSING_BRACE_OP already handles for `} |`, so _LOOP_END_OP is its
twin. The redirect is required by the pattern: a bare `done` stays in
the denominator, so genuinely uncovered loops are still caught.
Audited as the instrument notes demand -- every newly excluded line
printed with its text -- and it is exactly those two lines across all of
fixes/lib. Acceptance pair unchanged and exact: dot_resolver_install.sh
39/39, rpi_nc_ca.sh 73/73.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
75/75 lines through run_all.sh at --min 100, exit 0, and
check_shell_coverage.sh passes with the allowlist line already deleted
so the pass cannot come from the exemption.
55 assertions over 23 cases: all five profile repairs plus test_thorium,
including every dry-run arm, the partial singleton-lock case, a corrupt
database and the sqlite3-absent path. The lib needed NO production
change -- it already takes every path from $THORIUM_CONFIG_DIR, so
pointing that one variable at the tmpdir confines it completely.
The gate's per-case timeout moves from the jail's 120s default to 600s.
That default is a hang-catcher, not a performance budget, and this suite
has outgrown it: 76s bare at 25 test files, 93s at 27, and kcov roughly
doubles it. The symptom was a lib FAILING the gate while measuring
perfectly -- bt_audio.sh printed "127/127 lines = 100.00%" and then
"warn: case timed out after 120s", which --fail-on-case-error turned
into a non-zero exit. bt_audio.sh and arch_cpu.sh both pass now.
The cost, recorded in the evidence: a genuine hang takes 10 minutes per
pass to surface instead of 2. test_thorium's `read -r -p` is exactly
that shape, so every case here feeds it stdin explicitly.
This also settles the quarantine in b2779059. Re-adding the arch_sysctl
cases still yields "kcov instrumented no lines" for bt_audio at a 600s
timeout with no timeout warning, so runtime was never that file's
problem and the quarantine stands for the reason its header gives.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
94/94 lines through run_all.sh at --min 100, exit 0. 42 assertions over
26 cases across all five functions, including both boot-loader flavours
and the no-boot-loader path.
PRODUCTION BUG, found by writing the tests and confirmed against this
host: tweak_journal's regex required a space before the unit, but
journalctl prints "305.5M in the file system" with no separator. The
size check never matched, so the vacuum was unreachable and a
multi-gigabyte journal would never have been trimmed. This is the third
instance of the same defect -- arch_perf_report.sh:36,65 carried it too
-- so the fix is identical: the separator is now optional. One case
asserts the spaced form still matches, making this a widening.
The mitigation tweak edits the boot loader, so /boot/loader/entries,
/etc/default/grub and grub.cfg are now behind overrides defaulting to
the real paths. This machine's boot configuration is never touched.
_t_hide gets a cache: it symlinks every executable on PATH (13.5k here)
and was rebuilding that farm on every call, which took the suite to 92s
and pushed a jail case past its 120s timeout. Now 77s, and arch_cpu.sh
re-measures at 57/57 so the shared change cost nothing.
The cases are split across two files because a single one was 307 lines
and file-length-cap rejected the first commit attempt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
57/57 lines, measured through run_all.sh and confirmed a second time at
--min 100 so a shortfall would exit non-zero. check_shell_coverage.sh
passes with the exemption removed.
28 assertions over 14 cases: no cpufreq at all, every core already
tuned, the mixed case where only some cores lag, cpupower absent /
unconfigured / already configured, and all three I/O scheduler targets
(none for NVMe, mq-deadline for SATA SSD, bfq for spinning disks).
The sysfs reads and the two /etc write targets are now behind overrides
defaulting to the real paths, so the tests can present hardware this
host does not have -- a rotational disk -- without touching anything.
One assertion initially passed for the wrong reason: _t_hide keeps
$FAKE_BIN on PATH by design, so the harness's own cpupower stub still
satisfied has_cmd and the "not installed" branch never ran. Unstubbing
before hiding is now explicit, and the case fails if that is lost.
This also settles arch_sysctl.sh's diagnosis: it uses this same
harness, so the harness is not why it cannot be measured.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fourth flavour of defect (a) found this session, after multi-line quotes,
`} |` and the backslash-continued assignment. bash reports a multi-line array
assignment at its OPENING line and never emits the element lines or the
closing paren, so kcov instrumenting them inflates the denominator:
+L3 items=('one' 'two') # opened line 3, closed line 6
# lines 4, 5, 6: never emitted
The rule is therefore symmetric with the existing multi-line-quote case: the
opening line is the statement, everything up to the closing paren is data.
_closes_array() finds that paren by recomputing quote state per candidate, so
an element like "a(b)" does not end the literal early.
Measured effect through fixes/lib/tests/run_all.sh:
arch_perf_fixes.sh 93.48% -> 100.00% (43/43)
bt_audio.sh 100.00% -> 100.00% (127/127)
arch_perf_report.sh 100.00% -> 100.00% (59/59)
arch_perf_probes.sh 100.00% -> 100.00% (93/93)
ubuntu_perf_more.sh 100.00% -> 100.00% (32/32)
bt_report.sh 100.00% -> 100.00% (37/37)
bt_pairing.sh 100.00% -> 100.00% (65/65)
One gained, six held, none went DOWN. Acceptance test unchanged:
dot_resolver_install.sh 39/39 and rpi_nc_ca.sh 73/73. arch_perf_fixes.sh
therefore comes off the allowlist in the same commit, confirmed by the real
gate with no exemption.
The fail-open check, since over-detection INFLATES coverage: the old and new
predicates were diffed over every allowlisted lib plus the six cleared ones,
printing each newly excluded line WITH its text. All 91 across 10 libs are
array elements or closing parens -- common.sh's tool tables, packages.sh's
associative arrays, arch_sysctl.sh's sysctl maps, thorium_repairs.sh's four
path lists, ubuntu_perf_fixes.sh's sssd units, and so on. No statement caught.
Tests cover both directions, including three negatives: a single-line
`x=(a b)`, an element containing a parenthesis, and `x=$(echo hi)` which is a
command substitution rather than an array. Module stays at 100% branch
coverage.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Seventh of the 16 untested fixes/lib libs and the first of the ubuntu-perf
pair, which gets its own harness here. 24 assertions; 32/32 = 100.00%
through run_all.sh on the first measurement, confirmed at --min 100 (exit 0),
then cleared with the real gate and no exemption.
Two absolute paths had to move behind env overrides, defaulting to the real
ones: fix_journal writes /etc/systemd/journald.conf.d/size-limit.conf and
run_undo globs /root for undo scripts. Binding those in the jail was rejected
for the same reason as bt_pairing.sh's udev rule -- run_all.sh runs UN-jailed
in ci_mirror.sh and in CI, so a bind mount protects the coverage run and
nothing else. JOURNALD_CONF_DIR and UNDO_DIR follow the UDEV_RULES_DIR
precedent.
Proof the suite is safe where it runs un-jailed: after a bare run_all.sh the
real /etc/systemd/journald.conf.d/size-limit.conf still carries its 2026-03-02
mtime, a genuine artifact of a past production run, and no
undo_ubuntu_performance_*.sh exists in /root.
Worth noting for the rest of the family: run_undo ends in `exit`, so its cases
run inside an explicit subshell. A command substitution already provides one,
so the exit ends only that subshell rather than the whole test file.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sixth of the 16 untested fixes/lib libs, and the first with no dead branch to
fix -- the whole job was test-writing. 43 assertions across two files;
93/93 = 100.00% through run_all.sh, twice, the second at --min 100 (exit 0),
then cleared with the real gate confirming and no exemption.
The one genuine gap was check_memory_pressure's high-swap branch (lines
158-159), which fires only when swap is heavily used WHILE RAM is still
available -- a state this host does not happen to be in. The lib reads the
absolute /proc/meminfo, but every read goes through `awk`, so stubbing awk
supplies controlled figures without touching /proc: the stub answers on the
pattern being matched and execs the real awk for anything else. Both
directions are covered, including RAM genuinely exhausted, which must NOT be
flagged because it is not the stutter signature.
Everything else here is branch coverage of probes whose inputs are external
commands: failed systemd units, ACPI error counts, a CPU-hungry Xorg,
governors all-performance vs one-powersave vs unsupported, thermal
throttling excerpts, the TLP/power-profiles-daemon conflict in all three
combinations, and storage health with and without fstrim/smartctl.
lib_test_core.sh gains _t_stub_cat. "Print this fixture file" is by far the
commonest stub shape, and writing it inline forces every call site to embed
a literal ${LIB_TEST_DEV} that must not expand at write time -- SC2016 on
every use. Passing the fixture NAME as data removes the class outright, and
two earlier bluetooth test files are simplified onto it. All 21 test files
and three harnesses now lint clean with no suppression anywhere.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both were already tested to their reachable ceiling earlier in this session
(a5b6663c, ffcf99f1) and held below 100% only by the closing-brace artifact
that d989cd7e removed. With the instrument corrected they measure:
bt_audio.sh 127/127 = 100.00%
arch_perf_report.sh 59/59 = 100.00%
both at --min 100 with exit 0, and check_shell_coverage.sh returns
"every checked shell library meets the coverage bar" with their allowlist
lines already gone, i.e. with no exemption. bt_report.sh and bt_pairing.sh
were gated in the same run to confirm the two libs cleared earlier did not
regress.
Note that the closing-brace exclusion is now load-bearing: reverting
d989cd7e without also restoring these two lines would turn every subsequent
commit red.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
continuation_lines() excluded the continuation lines of multi-line QUOTED
arguments but nothing else, so a line holding only a group's closing brace
and an operator -- `} |`, `} >>"$REPORT_FILE"` -- stayed in the denominator
while bash attributed no statement to it. That capped four fixes/lib libs
below 100% with no test able to move them.
Why the anchor makes this safe: bash attributes every command INSIDE a group
to its own line, and a pipe's right-hand side to its own line, leaving the
brace-plus-operator line with nothing. Anchoring the pattern on a LEADING
`}` therefore excludes only lines that can hold no statement. A one-liner
`{ echo a; } >file` begins with `{` and is untouched; a bare `}` closing a
function has no operator and is untouched.
Measured effect, all through fixes/lib/tests/run_all.sh:
bt_audio.sh 96.97% -> 100.00% (127/127)
arch_perf_report.sh 98.33% -> 100.00% (59/59)
bt_report.sh 100.00% -> 100.00% (37/37)
bt_pairing.sh 100.00% -> 100.00% (65/65)
bt_adapter.sh 98.55% -> 98.55% (68/69, capped by a different flavour)
Two gained, three held, none went DOWN -- the handoff's regression signal.
That is not luck: _measure computes covered_set = (executed|traced) &
denominator, so for a lib already at 100% the covered set IS the denominator
and removing a subset cannot lower it. The five gated pacman_hook_stall libs
are safe by the same arithmetic.
Acceptance test from the handoff passes unchanged: dot_resolver_install.sh
39/39 and rpi_nc_ca.sh 73/73, both 100.00%.
The fail-open check that actually matters, since over-detection INFLATES
coverage and could promote an untested lib: the new predicate was run over
every lib in the allowlist plus the cleared ones, printing each newly
excluded line WITH its text. All 17 across 10 libs are genuine
brace-plus-operator lines; no ordinary statement was caught.
Tests cover both directions, including the two negative cases (a bare `}`
and a single-line `{ ...; } >file`), keeping the module at 100% branch
coverage. The quote scanner moved into _quote_after() to stay under the
C901 complexity limit rather than suppressing it.
Deliberately NOT fixed here: the backslash-continued ASSIGNMENT flavour that
caps bt_adapter.sh line 97. bash attributes those to the LAST line of the run
but a backslash-continued plain COMMAND to its FIRST, so that predicate needs
a command-vs-assignment distinction. One line in one lib; not worth the
landmine today.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Second of the 16 untested fixes/lib libs. 65/65 = 100.00% through
run_all.sh, twice, the second at --min 100 (exit 0), then cleared from the
allowlist and confirmed by the real gate with no exemption.
Two findings worth keeping:
- udevadm was NOT in the harness's stub list, so `udevadm control
--reload-rules` was reaching the REAL binary during the suite. A
prepended stub dir cannot hide a tool the host actually has. Caught by
asserting on the recorded call rather than by reading the code, which is
the argument for asserting on calls at all.
- _disable_usb_autosuspend writes /etc/udev/rules.d/50-bluetooth-no-
autosuspend.rules. Binding /etc in the jail would have covered it, and
was rejected: run_all.sh runs UN-jailed in ci_mirror.sh and in the
shell-tests workflow, as its own header says, so a bind mount protects
the coverage run and leaves every bare run writing to a real root-owned
file. Instead the lib gained a UDEV_RULES_DIR override, defaulting to the
real path -- the same env-override seam pacman_hook_stall_harness.sh uses
for PACMAN_LOG/CACHE_DIR/PACMAN_LOCK.
Proof the suite is safe where it runs un-jailed: after a bare run_all.sh,
the real rule file still has its 2026-02-21 mtime (it is a genuine artifact
of a production run six months ago) and the probed power/control values are
unchanged. The jail cannot give that check.
fix_usb_autosuspend globs the real /sys/bus/usb/devices/*/ with no override,
so its branches are driven through stubbed lsusb output: ffff:ffff matches
nothing on any machine and reaches the not-found arm, while 046d:c077
(control=on) and 046d:0825 (control=auto) are read-only probes, each guarded
so the case self-skips with a note if the host state changes rather than
silently asserting nothing.
Split across two test files to stay under the 250-line cap; run_all.sh globs
test_*.sh, so splitting costs nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
First of the 16 untested fixes/lib libs. Measured 37/37 = 100.00% through
lib/tests/run_all.sh, twice, the second run at --min 100 (exit 0), then
removed from the allowlist and confirmed with the real gate and no
exemption.
Adds two reusable pieces rather than a per-family harness clone:
- lib_test_core.sh -- PASS/FAIL tally, _t_eq/_t_contains/_t_lacks, a
mktemp -d sandbox wiped on EXIT, a FAKE_BIN stub dir on PATH, and
_t_stub/_t_unstub. _t_unstub runs `hash -r`: deleting a stub file alone
does not drop bash's cached executable path, so the real binary would
never be reached again.
- bt_harness.sh -- sources the core, sources the real scripts/lib/common.sh
for the production log_*/has_cmd helpers (verified inert at source time),
and redefines _btctl and apply_fix verbatim from fix_bluetooth.sh, since
those live in the entry script rather than in any lib. Same trade-off
pacman_hook_stall_harness.sh already makes with log_size.
pacman_hook_stall_harness.sh is deliberately left alone: its six libs are
already off the allowlist, i.e. gated at 100% with no exemption, so
refactoring it onto the new core would risk banked work for no gain. jscpd
reads 1.6% against the hook's own flags, and **/lib/tests/** is excluded
from that scan anyway, so the duplication budget is not at stake.
_t_only_path exists because every tool these libs call (usbreset, pactl,
lsusb, ...) genuinely exists on an Arch desktop: a prepended stub dir
cannot exercise a "not installed" branch, so those cases drop PATH to
FAKE_BIN alone.
The three coverage runs were made in a detached worktree: a second session
was writing to this repo concurrently, and the jail aborts on any
working-tree delta during a run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Each measured 100.00% through fixes/lib/tests/run_all.sh, twice, with the
second run gating at --min 100 so a shortfall would exit non-zero rather than
print a number I might misread:
pacman_hook_stall_capture.sh 59/59
pacman_hook_stall_load.sh 25/25
pacman_hook_stall_summary.sh 18/18
pacman_hook_stall_watch.sh 24/24
pacman_hook_stall_usage.sh 1/1
Two matching runs was the bar because this directory's suites drive
watch_forever timing loops, and a timing-dependent suite can vary its line
set between runs -- dwm_config.sh was observed doing exactly that. These five
did not vary.
pacman_hook_stall_setup.sh stays: 26/27 = 96.30%, uncovered line 7.
usage.sh's denominator of 1 is honest rather than broken. The lib is a single
usage() whose body is one `cat <<EOF` heredoc plus `exit 0`, and heredoc
content is data, not statements. Its tests assert on the rendered text, so
the behaviour is exercised even though only one line is instrumentable.
summary.sh has one off-set line (36). That cannot inflate the figure -- an
off-set line is by construction one the trace SAW EXECUTE -- but it does mean
the lib is measured over a subset of its statements.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The report printed a COUNT of traced lines that kcov never listed as
instrumentable. A count cannot be acted on: deciding whether such a line is a
real statement or a function declaration needs the line number. It now lists
them, the way it already lists uncovered lines.
Reading them across every lib with a harness splits them cleanly:
benign dwm_config.sh:27 (`heal_config() {`), transcribe_deps.sh:9
(one-line function declaration), clean_audio_filters.sh:197,200
(`((running++))`) -- correctly not statements
real aw_autostart.sh 68,70,73,79-81,83-84,87-88,94 -- plain echo, local,
if, `sudo -u ... mkdir`, `cat >` heredoc. kcov lists nothing after
the `{ ... } >>"$file"` command group at lines 61-65.
So kcov's LINE SET is incomplete too, not just its hit counts. This does NOT
inflate anything: a line reaches that list only because the TRACE SAW IT RUN,
so folding all 11 into both halves moves aw_autostart from 80/82 = 97.56% to
91/93 = 97.85% -- up. An off-set line is by construction an executed line, so
it can only ever be a missing COVERED line, never a missing uncovered one.
What it does mean is that such a subject is measured over a subset of its
statements. The percentage is honest about the subset, not the whole file,
which is why the lines are now printed rather than tallied.
Separately: nc_php.sh reports "kcov instrumented no lines" on the PRE-FIX
instrument too -- checked at 078d3463 in a throwaway worktree -- so it is not
a regression from the two-pass change. The 84/85 = 98.82% in earlier notes
does not reproduce and is recorded as unverified.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The instrument, not the tests, was the bottleneck. Coverage is now
(PS4-traced lines | kcov hits) & (kcov line set - continuation lines).
The trace deliberately does NOT supply the denominator. A trace reports only
lines that RAN, so using it for both halves would make every subject 100.00%
and arm a repo-wide gate that checks nothing.
Three measured findings, each of which yields a silently wrong trace:
1. kcov's ptrace and bash's xtrace cannot share a process. Under
SHELLOPTS=xtrace every line kcov recorded as hits="1" comes back hits="0"
-- reproduced A/B on one subject. Hence two passes, two namespaces.
2. SHELLOPTS is readonly inside bash, so `export SHELLOPTS=xtrace` fails and
tracing never turns on; and `set -x` does not reach child processes.
3. Decisive: bash under `unshare --map-root-user` runs PRIVILEGED and
DISCARDS an inherited PS4, falling back to "+ " while still honouring
xtrace. The trace loses its file:line prefix and every subject reads 0%.
Fixed by delivering PS4 and `set -x` through a BASH_ENV file, which also
solves child-process propagation.
A fourth trap, in the report: kcov records filename="dwm_config.sh", a bare
basename. The source path comes from the trace's ${BASH_SOURCE} or a unique
repo search; an ambiguous basename yields no exclusion rather than a guess.
Measured through run_all.sh, nothing down:
dot_resolver_install.sh 39/39 = 100.00% (acceptance, unchanged)
rpi_nc_ca.sh 73/73 = 100.00% (was 72/73 = 98.63%)
dwm_config.sh 65/66 = 98.48% (was 35/73 = 47.95%)
rpi_nc_install.sh 85/88 = 96.59% (was 10/88 = 11.36%)
rpi_nc_ca reaching 100% is the load-bearing check: its missing line 141 was
cross-process contagion, and the trace pass has no kcov in it to contaminate.
dwm_config's remaining line 39 is a `done < <(...)`. Left in the denominator
on purpose -- removing lines inflates coverage, the fail-open direction, so a
second exclusion rule needs per-line justification, not a pattern from one lib.
Both new Python modules hold 100% branch coverage (29 tests).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two defects found in review, both of which made checks PASS more easily, so
neither ever went red.
GATE: is_covered() hardcoded its jail flags and never read the jail_args
marker that ci_mirror.sh and shell-tests.yml both consume. The features/lib
suite needs /usr/local/share and /etc/pki and aborts under set -e without
them -- but the coverage number still printed, because kcov had already
recorded whatever the dead run reached. The gate would have graded 44 libs on
a suite that never finished. It now reads the marker, keeps the historical
defaults for suites without one, fails closed on an empty marker, and passes
--fail-on-case-error.
Negative control: stripping the binds makes the gate exit 1 and restoring
them returns exit 0. Before this commit both cases exited 0 -- the gate could
not fail on an aborting suite.
HASH CACHE: the _t_unstub helper written earlier today for the transcribe
harness was needed in features_harness.sh too. Two assertions were vacuous --
test_dwm_config's "verify still reports when dwm is absent", and
test_rpi_nc_ca's uname removal, where a cached uname left every later branch
running under a faked Darwin. All four `rm -f "$TEST_TMPDIR/bin/` sites in
the repo were audited; the other two are sound (one probes with [[ -f ]], the
other runs in a subshell).
No measured figure moved: dot_resolver_install.sh still 39/39 = 100.00% with
real absence enforced, so its off-allowlist clearance holds.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016sxxsDHWgdRhmdmzNMA6XM
A 29-assertion suite covering all six functions. transcribe_venv.sh measures
53/53 = 100.00% through the directory runner, and check_shell_coverage.sh
passes it with the exemption deleted.
Assertions are on recorded ARGV because which index a pip install points at
IS the behaviour: the OpenNMT cu12 wheel index on the NVIDIA path, the
CPU-only torch index for diarization (forced to avoid mismatched CUDA
builds). "pip was called" cannot see either.
generate_test_audio's four-deep fallback chain is covered by failing each
generator in turn. Every stub materialises the wav, because the chain
advances on `[[ ! -s $tmpwav ]]` -- a record-only stub would silently
exercise the branch after the one under test.
One assertion was wrong and got corrected rather than forced: I claimed a
wholly-failing pip still returns 0, but the final `python "$PY_HELPERS"
deps-installed` carries no `|| log`, so it does not. The suite now asserts
what the subject actually guarantees -- all four warnings emitted, no earlier
failure aborting the run.
SC2016 fixed at the source with a quoted heredoc, not suppressed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016sxxsDHWgdRhmdmzNMA6XM
The first allowlist removal of the campaign this session. unity_handler.sh
measures 32/32 = 100.00% through its directory's runner, and
check_shell_coverage.sh passes it with the exemption DELETED -- the real
gate, not just the measurement.
HARNESS BUG found getting there: bash caches executable locations in a hash
table, so `command -v foo` keeps succeeding after foo is removed from the
stub dir. Every "tool is missing" test was therefore running as a "tool is
present" test, asserting the opposite of its own name while looking
plausible. Reproduced minimally: after rm, STILL FOUND; after `hash -r`,
gone.
A _t_unstub helper now removes the stub and clears the table. The two suites
committed an hour ago had the same defect and are corrected here --
test_transcribe_pkgmgr.sh goes from 15 to 18 passing assertions, so three of
its checks had been vacuous. This is exactly the failure the campaign exists
to catch, found in its own test code.
`basename` joins the restricted PATH because the SUBJECT calls it; the suite
avoids needing `tail` via a parameter expansion instead, since every binary
in that PATH is one more thing a stub's shebang can trip over.
transcribe_deps.sh (92.31%) and transcribe_pkgmgr.sh (98.00%) re-measured
unchanged and stay on the allowlist.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016sxxsDHWgdRhmdmzNMA6XM
The jail recorded each case's exit code but reported only timeouts,
discarding every other non-zero status. A suite with a failing assertion
exited 0 through the push gate -- the same blind spot that let an earlier
session ship 2 failing assertions in the nc_php suite. Found by negative
control, not by reading: a deliberately failing assertion still exited 0.
Propagating that status unconditionally was wrong, and caught before landing:
the jail's own usage example passes a `bogus` case that exits non-zero to
cover an error path, so a clean 5/5 = 100.00% run started failing. That is
the mode the 105-lib campaign runs in.
So the behaviour sits behind --fail-on-case-error, default off, passed only
by ci_mirror.sh and shell-tests.yml. Verified in all four quadrants:
measurement + error case = 0, gate + error case = 1, gate + clean suite = 0,
gate + broken assertion = 1.
Both gates also pass --min 0: with no --measure the jail was measuring
run_all.sh itself against its default --min 100 and failing the push at
54.55%, a coverage verdict neither gate asked for. ci_mirror.sh now prints
the jail's output on failure, so a red gate can be diagnosed.
The added flag pushed the jail over the 250-line cap, so the in-namespace
case runner moved to lib/shell_coverage_case_runner.sh, following the
existing lib/shell_coverage_jail_setup.sh split. All four quadrants were
re-run after the extraction.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016sxxsDHWgdRhmdmzNMA6XM
ci_mirror.sh and shell-tests.yml discovered every */lib/tests/run_all.sh and
invoked it bare. The features/lib suite refuses to run bare -- that guard is
what stops it rewriting the real /etc -- so the pre-push gate blocked all six
pending commits, and CI would have gone red on the same suite the moment they
landed.
A suite now DECLARES its jail requirement with a jail_args file beside its
run_all.sh, holding the --bind/--seed-* flags it needs. Both consumers branch
on the marker. The 105 still-allowlisted libs were scored on root ops and
system writes, so most future suites will need the jail too; naming the path
in two runners would drift, which is the failure ci_mirror.sh's own comments
say it exists to prevent.
Both branches fail closed: a marker present but empty is an error, not a skip.
Verified the extracted flags reproduce the independently measured
dot_resolver_install.sh 39/39 = 100.00%, and that the other five suites still
pass bare.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016sxxsDHWgdRhmdmzNMA6XM
Investigated whether this session caused the 11:33 hard freeze. It did
not, and the evidence is recorded in the evidence artifact:
- No OOM kill, no oom_reaper, no hung-task or soft-lockup message, and
no kernel message AT ALL after 11:20. The log stops mid-line at
11:33:15 -- a hard hang, not a process the kernel was fighting.
- Last commit 11:30:32; last jail trace 11:11:57, 21 minutes earlier.
The session was idle when the machine died.
- Steam wrote a crash minidump at 11:31:10 and at-spi2 disabled an
unresponsive app at 11:32:06, on an RTX 3090. Correlation only -- a
hard hang leaves no log to prove cause.
- Nothing survived the reboot: no stray unshare/kcov, no leftover
tmpdirs, no leaked mounts, and the live /etc and /usr/local/bin
files keep their original dates.
Hardened anyway, because the jail had one genuinely unbounded
behaviour: a case could run forever. Two hangs yesterday came from a
`crontab -` fallback inheriting the caller's stdin.
Each case now runs under `timeout --kill-after=10s` (default 120s,
--timeout to override) with stdin from /dev/null, and a timed-out case
WARNS by name instead of being swallowed -- a silent timeout looks
identical to an unreachable line in the report. Verified: a `sleep 600`
subject now ends in 5s with --timeout 5s.
dot_resolver_install.sh still measures 39/39 = 100.00%.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbD4nco3EgL1UFoZKmKsLE
nc_php.sh was the next-hardest library on the allowlist. It targets a
Debian Raspberry Pi -- it edits /etc/php/<ver>/apache2/php.ini, drives
occ as www-data and installs a crontab -- so none of its write targets
exist on this Arch host and the jail had nothing to mirror.
- --seed-dir / --seed-file create the paths a foreign-platform subject
expects. Verified with a probe: /etc/php/8.2/apache2 exists inside
the jail while /etc/php does not exist outside it.
- A failed bind is now FATAL, and missing mountpoints are created
first. A bind that fails silently is indistinguishable from
unreachable code in the report, which is the misdiagnosis this
runner exists to prevent.
- Mirroring skips unreadable targets (/root as a normal user) rather
than aborting under set -e.
nc_php.sh: 84/85 = 98.82%, 24 assertions. It STAYS on the allowlist,
because 98.82% is below the bar. Line 111 -- the crontab first-run
fallback -- hung the suite on both attempts to cover it, so it is left
uncovered and documented rather than tried a third time.
Two harness hazards found the hard way and written down: a top-level
stub function shadows the subject's own function for every later
assertion (this silently dropped coverage 83.53% -> 57.65% while the
tests still passed), and a stub for a piped-into command must drain
stdin or the writer takes SIGPIPE.
dot_resolver_install.sh still measures 39/39 = 100.00%.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbD4nco3EgL1UFoZKmKsLE
The gate was a presence check -- 'a run_all.sh exists beside this lib' --
which an empty suite that asserts nothing satisfies. It now measures.
100% is reachable with no suppressions and no production source changes:
run the suite inside a user+mount namespace and the sudo-writes, systemd
units and nftables rules execute for real against a throwaway /etc.
- meta/scripts/shell_coverage_jail.sh: the runner, promoted out of a
session scratchpad. Two fixes it needed on the way: bind targets mirror
the real directory tree (an empty bind makes writes fail with ENOENT,
which reads as unreachable code), and sudo gets a pass-through stub
because real sudo cannot setresuid inside a userns.
- It fingerprints git status before and after and fails the run if a
write escapes the jail. Verified with a deliberate escapee.
- dot_resolver_install.sh: 39/39 = 100.00%, 28 assertions. Picked by
scoring every allowlisted lib on root ops, system writes and
destructive calls -- it scored highest in the repo. Allowlist 106 -> 105.
- jscpd was already over its own threshold at 2.45% and would have
rejected every commit staging a .sh file. Excluding vendored .venv/
shell alone gives 1.67%; also excluding lib/tests/ gives 1.47%.
The gate is deliberately NOT wired into the hook chain yet.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbD4nco3EgL1UFoZKmKsLE
Gives linux_configuration/scripts/lib a real test suite, which takes all
nine of its libraries off meta/shell-coverage-allowlist.txt at once —
coverage is per-directory, so this is the highest-ratio first batch.
139 assertions across four suites, all offline: the mtk toolkit is driven
through its existing MTK_ROOT_FIXTURE seam, and adb is shimmed onto a
narrowed PATH. Nothing reads the wall clock, this host's udev rules, or a
real device, because the suite runs in shell-tests.yml and again on every
pre-push via ci-mirror — a host-dependent assertion would be a flake that
blocks unrelated pushes.
The gate only checks that tests/run_all.sh EXISTS, so an empty one would
mark all nine libraries covered while testing nothing. Each of the four
load-bearing rules was therefore mutation-tested and the suite required to
go red: classify_device's && -> ||, discover_partitions' init_boot/boot
ordering, mtk_select_device's refusal to guess between two phones, and
is_hour_in_range's 10#$ octal guard.
That last one initially SURVIVED. Dropping 10#$ makes bash abort with
"value too great for base", a non-zero exit indistinguishable from an
honest out-of-range result, so the status-only assertion passed either
way. It now asserts stderr is empty.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three review findings, all before the hook is wired:
is_shell_lib excluded */lib/tests/* but not */tests/lib/*. Both are test
code; the second nesting made the gate demand tests/lib/tests/run_all.sh
for 10 helper files. It would have fired precisely while editing test
helpers to write the suites this ratchet asks for. Reseeded 125 -> 115.
--seed now refuses (exit 1) when a reseed would ADD an entry, instead of
a header comment asking nicely. It reads tracked files only, so an
unstaged probe seeds normally -- staging it produces the refusal with
the allowlist left byte-identical.
The parent commit's docs claimed an exempt file loses its exemption when
next modified. It does not: check_files exempts allowlisted paths
unconditionally, verified by staging an edit and getting exit 0. Fixed
the text, not the code -- making an edit trip the gate would block
routine work across 115 of 198 libraries, which is the repo-wide gate
this ratchet exists to avoid.
Also corrects the "a percentage is unreachable" rationale: commit
53e80462 reached 100% on an entry script with no suppressions. Presence
is still the right bar, because that took per-file mount analysis.
Measurement-only. No production file changed.
The gating question was whether the campaign's constraints are jointly
satisfiable: 100% line coverage over ALL .sh files with no suppressions.
Phase 1 measured effecting code at 0% under PATH shims, which implied
either forbidden suppressions or source seams in 17+46 files.
They are satisfiable. A user+mount namespace reaches 100% with zero
source changes: --map-root-user makes require_root fall through, and
bind-mounting each write target makes installer writes land in the jail.
setup_night_lockdown.sh went 81.58% (PATH shims) -> 86.84% (userns) ->
100.00% (38/38, all mounts).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV
A shell library under a lib/ directory must now sit beside a
tests/run_all.sh. This is a presence gate, not a coverage percentage:
Phase 1 measured these libs' decision logic at ~90% under test but
their effecting code at 0%, because it sudo-writes, pkills and installs.
A numeric bar would only be reachable by shimming every external for
every installer, or by the suppressions this repo forbids.
Seeded at 125 exempt libraries out of 208 tracked; the other 83 already
have a suite. The allowlist shrinks only — a modified library loses its
exemption and must bring tests.
Verified standalone: blocks a new untested lib (exit 1), and does NOT
block covered libs, exempt libs, or unrelated entry scripts. That last
case is the point of a ratchet over the repo-wide gate.
The .pre-commit-config.yaml entry is deliberately NOT included — the
edit was denied as a sensitive file, so the gate is runnable but not
yet enforced.
Two pre-commit hooks and one pre-push extension, plus the _glyph_art split
that had to land first — the cap gate cannot be wired while any file is over
it, and the lyricanki note-ascender glyph had taken _glyph_art.py to 253.
file-length-cap runs check_file_length.sh --all repo-wide rather than on
staged files, so a file that grows past the cap in some other commit is still
caught. Proven to block, not assumed: with a deliberate 261-line file staged
the hook exits 1 and names it; with the file removed it passes.
ci-baseline-green is the user's option (a): refuse to build on a main that is
already red. It fails closed on a definitively failing baseline and warns-and-
passes on anything it cannot determine — offline, no gh, unauthenticated, no
settled runs — each call capped at 5s. Blocking every commit on network
reachability buys nothing when (b) is the real per-change guarantee.
CI_GREEN_SKIP=1 is the escape for a fix-forward commit. python-tests.yml is
excluded on purpose: it is path-filtered, so a shell-only commit never
triggers it and its "latest run" would produce false reds.
This hook already earned its place. Run against a red main earlier in this
session it refused with "Shell tests (failure)", which is how the broken
SETUP_SOURCES was found instead of being pushed over.
ci_mirror gains the shell-tests workflow's side-effect-free half — every
lib/tests/run_all.sh plus the workflow's own test list, the user's option (b).
That list is parsed out of shell-tests.yml rather than copied: a hardcoded
duplicate drifts the moment someone adds a test, and a mirror that quietly
checks less than CI is worse than no mirror because it reports safe. The
Arch-container half stays unmirrored — it wants root and an Arch userland.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV
The detection regex matched only `name() {`. bash also accepts
`function name() {` and `function name {`, and pacman_wrapper.sh uses the
keyword form for 17 of its 30 functions — so a split of that file would have
left every one of them behind in the entry script while the tool reported
success. Silent, and exactly the kind of half-done split the verifier is
meant to catch afterwards rather than the extractor causing.
Parentheses stay mandatory in the non-keyword branch. Making them optional
everywhere also matches brace groups and control constructs, which would
sweep non-function code into the library — the opposite failure, and worse.
Nine negative tests pin that boundary.
The function name now comes from a named capture group rather than
splitting on "(", which returned "function name" for the keyword form and
would have broken both --functions selection and the verifier's name matching.
Verified against the already-split files first: cco_state.sh still yields 13
functions and leechblock_fetch.sh 3, so the completed splits are unaffected.
pacman_wrapper.sh goes from 13 detected to 31.
First tests for this script: 20 cases covering every accepted spelling,
brace matching across nested bodies, and single-line functions.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV
The largest file in phone_focus_mode, and the last one blocking deploy.sh:
splitting it grows deploy.sh's two hardcoded lists, so those lists are only
final now. All 55 functions moved verbatim.
Three while-read loops were audited before extraction. Two call pm in their
bodies and moved untouched. The third, iter_whitelist_packages, is fed by a
pipe from printf and reads nothing from stdin -- already safe, and now said
so in ctl_usage.sh's header so the distinction is not lost by a later editor
who has only read the warning in daemon_apps.sh.
136 assertions and 22 mutations, all killed. Three survived the first run
and every one was a defect in MY OWN tests, not in the subject:
- cmd_status carries a THIRD copy of the Haversine, and I had asserted
only that it printed "some number". A wrong Earth radius sailed through.
Now the value is checked against a seeded fix ~4.3 km from home.
- the two whitelist filters were each tested with input the OTHER filter
also rejects, so either could be deleted with nothing failing. Fixed
with a punctuation-free undotted token (only the dot check rejects it)
and a dotted token carrying an illegal character (only the charset check
does), making each independently observable.
That last investigation turned up dead code: the `#*)` case arm is provably
equivalent, because `set -- $line` makes a comment's first token "#", which
the dot check already rejects. Removed rather than covered by a test that
cannot fail.
Coverage is NOT 100% and the real numbers are in the evidence: 41.79% to
92.98% across the eight, up from 5-22%. Unlike the daemon split, most of
what remains is genuinely untested -- the per-enforcer status blocks that
print mount state, APK hashes and offload settings, each needing its own
device fixture. The mutations prove the assertions that exist are load-
bearing; they do not prove those branches work.
jscpd checked in a clean worktree at 1.57%: eight files each carrying a
near-identical start/stop/status trio is exactly what that gate is for, so
the tests drive the trios through tables instead of repeating them.
Two drift hazards recorded, not fixed: ctl_is_curfew_now duplicates
daemon_location.sh's is_curfew_now (pinned by an agreement test across every
boundary minute), and cmd_status's awk is the third Haversine.
Over-cap count 17 -> 16. deploy.sh is the last one here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
Location/curfew goes to daemon_location.sh, the derived state files to
daemon_state.sh, the app layer to daemon_apps.sh. All 23 functions moved
verbatim -- nothing restructured, which matters more here than anywhere
else in this session.
focus_daemon.sh has five `while IFS= read -r pkg; do ... done <file` loops
and EVERY body calls pm. Piping any of them in lets pm inherit and drain
the loop's stdin, so it would process one package instead of all of them --
measured elsewhere in this repo as 1 of 54. That audit ran BEFORE the
extraction, not after coverage came back short, and all five moved
untouched with a note above them saying why.
CURRENT_MODE and _CURFEW_TICK_CACHED are each written in both the entry
script and a library, so both stay declared with main, same call as
NET_BUILT in the curfew split.
110 assertions and 33 mutations, all killed. Two survived the first run and
both were defects in my own tests: the whitelist and sysprotect exactness
checks used a SUPERSTRING of a listed entry, which fails under grep -qF and
grep -qxF alike and so proved nothing about which was in use.
Two subjects are NOT at 100%, and the numbers are reported as measured:
daemon_state.sh 67/67 = 100.00%
daemon_location.sh 26/40 = 65.00%
daemon_apps.sh 71/76 = 93.42%
Both shortfalls are kcov attribution failures, shown to be artifacts rather
than gaps. daemon_location.sh's missing lines are all inside the embedded
awk Haversine; mutating the Earth radius and dropping the longitude term
both fail the suite, and an isolated probe shows a one-line awk measuring
100% where the identical multi-line one measures 0%. Collapsing the formula
to satisfy the tool would cost the readability of the only real mathematics
in the repo, so it stays.
daemon_apps.sh's five are the trailing `done <file` of the very loops
described above. That is the same artifact piping fixed in curfew_net.sh,
and it is exactly what must NOT be done here. Redirecting two of them from
/dev/null fails the suite, so the lines run; all three are pinned in the
mutation spec instead.
Over-cap count 19 -> 18.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
Four evidence files committed today cite mutation runs -- 18, 13, 17 and
17 killed -- that lived in a scratch directory. Evidence pointing at a run
nobody can repeat is not evidence, so the lists move into the repo behind a
spec-driven runner.
Mutations are literal find/replace strings in JSON, not sed expressions.
The shell version this replaces escaped each pattern twice and used | as
its field separator, which collided with || and reported six mutations as
SURVIVED when they had never been applied at all -- a false negative in the
one tool whose entire job is catching false negatives.
Two deliberate choices follow from that:
- a no-op (find matched nothing) counts as a FAILURE, not a skip. A stale
spec entry otherwise stops testing its behaviour without saying so.
- a timeout counts as killed: a mutation that hangs the suite has still
been detected by it.
The conversion itself exercised the first rule -- it reported 8 no-ops from
mangled escapes rather than quietly scoring them as killed. Each was fixed
against the real source line.
All four specs now reproduce their committed figures exactly, and the
working tree is byte-clean after every run.
Not wired into pre-commit: it runs the whole suite once per mutation, which
is minutes, not seconds.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
kcov works here once invoked correctly, so shell coverage is measurable
after all. shell_coverage.sh wraps it as a gate; the XML parsing lives in
shell_coverage_report.py per the rule against inlining another language's
logic in a shell script.
Two kcov traps are documented in the wrapper because both fail silently.
It must be handed the test script DIRECTLY: given `bash script.sh` it
instruments the bash binary, finds no shell source, and reports 0/0 at
0.00% -- indistinguishable from "nothing is covered". And per-line detail
exists only in cov.xml; coverage.json carries the percentage alone, so it
cannot say which lines are missing.
First measurement put dns_iptables.sh at 68/74 = 91.89%, with all six
uncovered lines in error and edge branches: chain creation failing, jump
insertion failing, the stale-jump de-dupe log, and the v6 trusted-resolver
ACCEPT. The stub grew failure injection and jump seeding to reach them,
and four new cases take it to 74/74 = 100.00%.
The gate fails closed: deleting the three ensure_chain failure cases drops
it to 93.24%, names lines 23, 24, 37, 40, 41, and exits 1.
Not wired into pre-commit -- kcov is absent on CI, and most shell files
here have no test at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
On a script with no `set -e` anchor -- every #!/system/bin/sh phone
enforcer -- the tool wrote the library, then exited on the missing
anchor, leaving the functions in the entry script as well. The result
defined each moved function twice, lints clean, and passes a
function-body hash check, so nothing downstream catches it. It cost a
split of dns_enforcer.sh this session: 325 lines in, 331 lines out.
Both writes now happen after the anchor search. A missing anchor is a
warning naming the exact line to paste, not an exit, so the move still
completes. Added a shrink guard that refuses to write when the entry
script would not get shorter, and an explicit error when the selected
function set is empty.
Verified both paths: dns_enforcer.sh (no anchor) warns and still drops
171 -> 158 with the functions gone, phone_backup.sh (anchored) drops
333 -> 264 with the source line auto-inserted as before.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
The shell-splitting work has been referring to check_file_length.sh --all
as its per-commit gate. No such script was ever committed: it is absent
from the working tree and from every commit in history, so each split so
far was checked by eye.
Cap is 250 lines, source extensions only (sh/bash/zsh, py, kt/kts, dart,
ts/tsx/js/jsx). Markdown, HTML, JSON, YAML and lockfiles are out of
scope, as are third_party/ and docs/superpowers/.
--all reports exactly 24 files over the cap, independently reproducing
the count the brief carried. That agreement is what confirms the
extension scope is the intended one.
Deliberately NOT wired into .pre-commit-config.yaml: 24 files are still
over, so a hook would block every commit. It becomes a hook once --all
exits 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
clean_audio.sh 419 -> 230 + 204
enforce_vbox_hosts.sh 443 -> 220 + 236
migrate_hosts_guard.sh 444 -> 167 + 162 + 141
migrate_hosts_guard needed two libraries: with all ten work functions moved
the single lib was 295 lines, so migrate and rollback were separated.
The extractor's brace matcher required index > start and so silently skipped
one-line `warn() { ...; }` definitions. Fixed, and regression-checked against
the committed fresh-install/main.sh split byte-for-byte.
The harness earned its keep on a fourth file: adding `export` to libre_translate
to satisfy SC2034 changed two function bodies, and verify_shell_split.sh caught
what lint had accepted. Those exports were reverted, and libre_translate.sh is
parked - what remains after its clean seams both assigns and reads the same
globals, so getting it under the cap needs parse_args restructured rather than
relocated, which is a refactor rather than a verbatim move.
That cost is now written down: a lib must contain the readers of every global
it assigns. The precedent at install_pacman_wrapper.sh:29 is about
definitions-only libs, not about assignments as such, and the pre-commit hook
runs shellcheck with no -x, so a source= directive will not make it follow the
link. This is the shell analogue of the monkeypatch-target trap.
Verified: all 8, 13 and 19 functions verbatim; all four libraries source with
no side effects; shellcheck passes standalone on every file with no
suppression; bash -n passes on all seven. None of the three was executed -
they drive ffmpeg over real media, mutate VirtualBox guest disks, and rewrite
systemd units.
Over-cap files 29 -> 26.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
Two single-use scripts with no deployed copy, so the entry+lib shape is
safe. Each keeps the ordering, the safety checks and the user-facing output;
the configuration writers move to a library beside it.
Both scripts write `set -e # Exit on any error`, so extract_shell_functions.py
now matches the directive rather than the whole line. Regression-checked by
re-deriving the committed fresh-install/main.sh split byte-for-byte.
Verified: all 6 and all 9 top-level functions verbatim via the shfmt
harness; both libraries source with no side effects and define every
function moved into them; bash -n passes on all four.
Neither script was run. nvidia_troubleshoot.sh writes /etc/X11 config and
installs pyroveil; setup_passwordless_system.sh writes sudoers and lightdm
autologin, which is what this machine's login depends on.
Over-cap files 31 -> 29.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
Generalises the method proved on fresh-install/main.sh. Line-range slicing
is the obvious approach and it is wrong: these scripts interleave top-level
commands between their function definitions, so a range slice sweeps them
into the library where they run at source time and out of order. This walks
the file brace-by-brace, lifts only the function blocks, and leaves every
other line where it was.
Verified by re-deriving the already-committed main.sh split in a throwaway
worktree at HEAD~1: both files come out byte-for-byte identical to the
hand-made version that passed the shfmt harness, the structural flow
comparison, and the source-time side-effect check.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
The five Python splits this session were made safe by a throwaway ast
identity check. There is no ast for bash, so meta/scripts/verify_shell_split.sh
does the same job with shfmt -mn (minify: strips comments and indentation,
keeps logic), hashing each top-level function and naming the one that differs.
shfmt is already a REQUIRED_TOOLS binary, so this adds no dependency.
Validated in all three directions before trusting it: an unchanged copy is
IDENTICAL, a copy with blank lines inserted is IDENTICAL, and a one-character
logic change ($1 -> $2) is caught and named. The first mutation attempt used
sed 's/exit 1/exit 2/' on a file with no 'exit 1' and reported IDENTICAL - the
harness was fine, the test was vacuous. That trap is now documented, because a
normalizer that silently passes everything is worse than none.
Also triaged all 29 over-cap shell scripts for deployed copies, which the
trace harness cannot see. The finding that changes the plan: deploy.sh copies
a hardcoded per-file list into /data/local/tmp/focus_mode and never deploys
lib/, so an entry+lib seam would break focus mode on the phone at its first
source - for 8 of the 29, not the 2 the brief named. Those scripts source only
config.sh, which is on the copy list, so a sibling-file seam works provided
deploy.sh's list grows in the same commit.
16 scripts have no deployed copy and are safe to split conventionally.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
meta/scripts/optimize_vscode.py was 498 lines against a 250-line cap and
completely untested, and at that path the repo-wide gate (--cov=python_pkg,
fail_under = 100) could not see it. Moved verbatim to
python_pkg/vscode_optimizer/_optimize.py - git records a pure rename, and
the content is byte-identical to HEAD.
The move and the tests are one commit by necessity: any intermediate state
with the module under python_pkg/ but uncovered turns every pytest run in
the repo red, including ci-mirror on pre-push for unrelated work.
Running it by path no longer works after the move (sys.path[0] would be the
module's own directory), so __main__.py replaces it, following the existing
focus_policy convention rather than a sys.path insert. Nothing invoked the
old path: grep over .sh/.desktop/.service/.yml/.py found zero callers.
98 tests reach 100% line and branch coverage. Patching is confined to real
boundaries - subprocess.run, the module-level Path, builtins.input, sys.argv
- so the forthcoming split has few monkeypatch targets to re-point.
Writing the tests surfaced a pre-existing bug, left unfixed here and pinned
by a strict xfail: _VENDOR_KW's "ati" key is a substring of "VGA compatible
controller", which appears in every lspci display line, so it matches before
"intel" is ever tried and Intel iGPUs are misreported as AMD.
Verified: python3 -m python_pkg.vscode_optimizer --dry-run reproduces the
pre-move 18-change plan and writes nothing; the real settings.json is
untouched; repo-wide pytest is 1846 passed at 100%; over-cap files 37 -> 36.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
All three were found by review and reproduced before fixing. Each let a run
report success while proving nothing -- the shape this harness exists to
prevent.
A reused prefix defeated dropped-write detection entirely. Run 1's files were
still there when run 2's manifest was taken, so the manifest reported writes
that never happened and the fixture pair diffed CLEAN. The doc's own examples
demonstrated that workflow. A non-empty prefix is now refused -- not deleted,
since the path is user-supplied -- and the examples use distinct prefixes.
A missing bwrap was swallowed. trace_prefix_bwrap_argv returned 1, but the
caller reads it through `mapfile < <(...)` and a process substitution's status
is discarded, so runner ended up empty and the run continued with no sandbox
and no uid 0. Same swallow as the grep-status one fixed earlier, one layer up.
bwrap is now probed in validate_requirements, before anything runs.
The absolute-path scan only fired when no --bind-abs was given at all, so
binding one path and missing three produced no warning. It now runs whenever
--prefix is given and reports only what is not already covered. Fixing that
surfaced a second defect: --bind-abs on a FILE aborted with "Can't mkdir
/etc/profile: Not a directory", and binding its parent reintroduced the
wholesale /etc shadowing this change had already measured as harmful ($SUDO_USER
collapsing to /home//, the real /etc/profile unreadable). Files are now bound
file-over-file, seeded with a copy so a script that greps before appending sees
real content.
Also measured rather than argued: the XDG redirect. A new fixture run with a
real XDG_DATA_HOME pointing at a decoy shows all four writes landing in the
prefix and the decoy empty -- the rsync --delete data-loss path, proven closed
without running install_leechblock.sh. And no-prefix output was diffed against
cdf2a55 from a detached worktree: byte-identical, so existing baselines hold.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
Shell redirections are builtins, so no PATH stub can intercept
`cat > /etc/modprobe.d/x.conf`. That left installer scripts unverifiable:
stub cp/chmod/ln and the trace is nearly empty, don't and the run rewrites
the live system.
--prefix redirects HOME and every XDG_* base into a temp tree. XDG_DATA_HOME
is exported explicitly, not left to default off HOME: install_leechblock.sh
reads it directly and then runs `rsync -a --delete` at it.
--bind-abs binds hardcoded absolutes via bwrap --unshare-user --uid 0, leaf
directories only. uid 0 is load-bearing beyond permissions: common.sh's
require_root does `exec sudo "$0"`, and under the sudo stub that truncated
the whole run to one recorded call -- a trace that diffs clean against any
split. Binding /etc wholesale was measured to shadow /etc/passwd, collapse
$SUDO_USER to /home//, and mask a nested bind, so the scanner never emits it.
The trace gains a manifest of every file written, with content hashes. That
section is the point: with it, a dropped write in the fixture pair is a
one-line diff; with it stripped, the two traces are identical, i.e. the old
harness passed that broken split. Hashes normalize the prefix to @PREFIX@
because install_usage_monitoring.sh writes an unquoted heredoc that bakes
$HOME into the artifact.
Verified: ~/.config/systemd/user, ~/.local/bin, ~/.local/share and
/etc/modprobe.d byte-identical before and after; usage-report units still 2.
The three prior regressions are now committed fixtures rather than ad-hoc --
one was silently neutered mid-change when a trailing echo added for SC2034
became the function's last statement and turned exit 1 into exit 0.
Stub generation moved to lib/ because --prefix pushed the harness to 252
lines, over the cap it exists to serve.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
The previous commit broke `./lint_python.sh` from the repo root. That path
is a symlink into meta/, and `dirname` does not resolve symlinks, so
SCRIPT_DIR became the repo root and the new `source "$SCRIPT_DIR/lib/..."`
line pointed at a lib/ that does not exist -- an instant exit under
set -euo pipefail.
The bug was latent before the split: the same unresolved SCRIPT_DIR only
fed PROJECT_ROOT, so a root-path run degraded quietly. Adding a source
line is what turned a wrong value into a hard failure.
It was invisible because every check I ran used `bash meta/lint_python.sh`
-- the one entry point that could not fail. Both paths are now verified:
`./lint_python.sh` exits 0 through the symlink, and the direct invocation
still diffs byte-identical against the pre-split baseline. No CI job, hook
or wrapper calls it, so nothing automated was broken in the interim.
Recorded in docs/shell-split-verification.md as its own trap, with the
commands to enumerate a script's other entry points before splitting it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY