testsAndMisc/NEXT_SESSION_PROMPT.md
Krzysztof kuhy Rudnicki 3338033cf2 Refresh the ratchet handoff after the first allowlist batch
115 -> 106 everywhere the count appears, 4 -> 5 suites, and the
"chipping away" section rewritten around what the scripts/lib batch
actually measured: the per-directory cost, the two isolation traps
(source-time $HOME writes, readonly double-source), and the reason to
mutation-test before reseeding rather than after.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 23:58:35 +02:00

16 KiB

Next session: Phase 3 — finish wiring the shell-coverage ratchet

Paste this whole file into a fresh Claude session opened in ~/testsAndMisc. It is self-contained. Do not go looking for the previous session's context.

Phases 1 and 2 are DONE. The 250-line cap is at zero and enforced; the CI gates are wired.

Phase 3 is BUILT but NOT ENFORCED (commit 3278bebd). The three scoping questions below were answered without the user, because the session that built it was non-interactive — a grilling round would have ended the turn with nothing delivered. The user can still veto any of these cheaply, since the gate is not yet in the hook chain:

  • Q1 Scope → ratchet. 106 pre-existing libraries are exempt via meta/shell-coverage-allowlist.txt; any library not on that list must be covered, so a new one cannot enter untested. Unrelated commits are never blocked (verified). Exemption is static — editing an allowlisted library is allowed and does not demand tests. An entry leaves the list only when its directory gains a suite.
  • Q2 Bar → presence, not a percentage — but NOT because a percentage is impossible. An earlier session on 2026-08-21 reached 100% (38/38) on setup_night_lockdown.sh with no source changes and no suppressions, using a user-namespace jail, with containment verified by canary. The generic runner then got 75% (57/76) on pacman_wrapper.sh, because each subject needs its own mount analysis. So a numeric bar is reachable but costs bespoke per-file work — presence is the floor that can land today, and a percentage can layer on top later. See docs/superpowers/evidence/phase3-namespace-jail-feasibility-2026-08-21.json and meta/scripts/shell_coverage.sh. Both jail subjects were entry scripts, which Q3 puts out of scope, so nothing is yet measured for the 198 lib/ files.
  • Q3 Which files → .sh under a lib/ dir, excluding lib/tests/ and lib/payloads/. Entry scripts are orchestration whose bodies are untestable.

The one thing left to do

meta/scripts/check_shell_coverage.sh works standalone but is not in .pre-commit-config.yaml. That edit has now been denied twice as a sensitive file (2026-08-21, two separate sessions), so it is not an oversight and not worth a third agent attempt — the user must paste it. Do not try to route around the denial with sed/heredoc/python; the restriction is deliberate.

Paste this block directly after the file-length-cap hook (which ends at always_run: true, immediately before - id: ci-baseline-green).

Indent every line below by 6 spaces so - id: lines up with the neighbouring - id: file-length-cap and - id: ci-baseline-green. The block is shown unindented because prettier reformats fenced code and strips the leading whitespace; it is a repo: local hook list, so the indentation is load-bearing and YAML will reject a flush-left paste.

- id: shell-coverage-ratchet
  name: Shell libraries must have a test suite
  # Per-file (files:, NOT --all) is what makes this a ratchet rather than
  # a repo-wide gate: the 106 libraries that predate it stay exempt via
  # meta/shell-coverage-allowlist.txt, so unrelated commits never block,
  # but a NEW library cannot enter without a tests/run_all.sh beside it.
  # Filenames must be passed — with pass_filenames: false the script
  # would receive no arguments, iterate nothing, and pass forever.
  entry: bash meta/scripts/check_shell_coverage.sh
  language: system
  files: \.sh$

Or let sed do the indenting — copy the block above into /tmp/hook.yaml, then sed 's/^/ /' /tmp/hook.yaml and paste that output.

Do NOT copy pass_filenames: false from the neighbouring file-length-cap hook. That hook uses it because it runs --all; this one is driven by files:. With pass_filenames: false the script gets zero arguments, report_uncovered "$@" iterates nothing, and the gate exits 0 forever — a dead gate that looks green in every test you would naturally run.

Verification already done (2026-08-21) — do not redo

The script's own behaviour is measured; only the YAML wiring is outstanding.

Case Command Result
Unrelated entry script must not block check_shell_coverage.sh meta/scripts/check_file_length.sh exit 0
New untested lib must block canary linux_configuration/scripts/lib/_ratchet_canary_tmp.sh exit 1, names the needed tests/run_all.sh
Batch, all covered 3 covered files in one call exit 0
Batch, 3rd of 4 uncovered canary in the middle of a batch exit 1
A lib/tests/ harness must not block check_shell_coverage.sh <a lib/tests/*.sh> exit 0 — script self-filters

That last row is why the block needs no exclude:. files: \.sh$ does match lib/tests/ and lib/payloads/ files, but the script filters them itself, so the gate cannot block its own test suites.

The batch cases matter because pre-commit passes many filenames per invocation; a loop that only inspected $1 would have passed silently.

After pasting, confirm the wiring end-to-end:

pre-commit run shell-coverage-ratchet --all-files   # expect pass (106 exempt)
grep -n "shell-coverage-ratchet" .pre-commit-config.yaml

The grep is not redundant — an autoformat pass silently stripped load-bearing lines three times during Phase 1. Check after the commit lands, not before.

Do not run --seed after adding an untested library — it would rewrite the allowlist and silently exempt it. The list is shrink-only, and --seed now enforces that by exiting 1 rather than adding an entry.

Chipping away at the allowlist — 115 → 106, one batch done

198 in-scope libraries: 106 exempt, 92 covered. scripts/lib (9) was cleared on 2026-08-21 (commit d42d8ff2); the remaining concentration is single_use/features/lib (45), single_use/fixes/lib (22), phone_focus_mode/lib (9). Adding one tests/run_all.sh to a directory enforces every file in it at once, so expect the work to arrive in batches rather than one file at a time.

What the first batch cost, as a calibration point: 139 assertions across four suites for nine libraries, and the pattern held — the decision logic was richly testable, the effecting code was not touched at all. The mtk toolkit was unusually cheap because mtk_common.sh already had a deliberate MTK_ROOT_FIXTURE seam; a directory without one will cost more.

Mutation-test before you reseed. is_covered() only checks that tests/run_all.sh exists, so an empty file removes entries from the allowlist while testing nothing, and no downstream gate can tell. Mutate each rule the suite claims to protect and require the suite to go red. One of the four mutations here initially survived: dropping 10#$ from is_hour_in_range makes bash abort with "value too great for base", a non-zero exit that an exit-status-only assertion cannot distinguish from an honest out-of-range result. Assert on stderr when a mutation's failure mode is an arithmetic or syntax abort.

Two isolation traps found in scripts/lib, both likely elsewhere: android.sh runs ensure_dir "$ANDROID_WORK_DIR" at source time, so merely sourcing it writes under $HOME — redirect HOME to a tmpdir first. mtk_common.sh declares readonly patterns and sources three siblings at its bottom, so sourcing it twice in one shell aborts under set -e; run one suite per process, which run_all.sh already does by invoking "$t" directly.

Anything time-, host- or device-dependent becomes a push-blocking flake, since these suites run in shell-tests.yml and on every pre-push via ci-mirror. Shim the clock, point MTK_ROOT_CACHE at a tmpdir, and assert /proc/uptime readers on shape rather than value.

--seed is shrink-only and now enforces it — it exits 1 rather than adding an entry, so you cannot exempt a new library by reseeding. It reads tracked files only, so stage before trusting its output.

Phase 4 (later): a percentage bar on top of the ratchet

The namespace-jail evidence above is the load-bearing input. The technique works and is contained; what is unmeasured is whether it generalises to lib/ files rather than entry scripts, and what per-file mount analysis costs at scale. The pacman_wrapper.sh result names the dominant failure mode: masking /usr/local/bin makes the wrapper take its "libraries missing → exec pacman unwrapped" escape hatch at lines 44-45 and exit before doing any real work, so coverage collapses from 75% to 29%. Measure a handful of lib/ files through jail_run.sh before proposing any number.

What is already true (verify, do not redo)

bash meta/scripts/check_file_length.sh --all   # "all checked files are within 250 lines"
bash meta/scripts/check_ci_green.sh            # exit 0 while main is green
gh run list --limit 5
  • Every file is under 250 lines. The five deployment-trap files were split: install_leechblock.sh (485→119), block_compulsive_opening.sh (705→182), setup_night_lockdown.sh (918→127), pacman_wrapper.sh (929→217), setup_midnight_shutdown.sh (1734→109), plus _glyph_art.py (253→208).
  • file-length-cap runs check_file_length.sh --all on every commit.
  • ci-baseline-green refuses to commit onto a red main (fails closed on red, warns and passes when it cannot tell). Bypass: CI_GREEN_SKIP=1.
  • ci-mirror on pre-push now also runs every */lib/tests/run_all.sh and the side-effect-free half of shell-tests.yml.
  • shell-tests.yml discovers and runs every */lib/tests/run_all.sh. Four such suites existed before and none of them ran in CI until this was added.

Measuring the scale — use git ls-files, not find

git ls-files '*.sh' | wc -l                          # 560 tracked
git ls-files '*/lib/tests/run_all.sh'                # 5 suites
bash meta/scripts/check_shell_coverage.sh --all      # 106 uncovered, 106 exempt

A bare find reports 565 .sh files because it walks .venv/, .ci-mirror-venv/ and node_modules/. The earlier "roughly 487" figure in this file came from an unfiltered walk of a different tree state. Seeding an allowlist from an unfiltered find would bake vendored venv scripts into a tracked file, and check_file_length.sh --all passing would not reveal it, because that script carries its own separate exclusion list.

What Phase 1 measured — why the bar is presence, not a percentage

Coverage of the new libs, measured with bash meta/scripts/shell_coverage.sh <lib/tests/run_all.sh> <lib-basename> 0:

lib coverage what is uncovered
cco_state.sh 93.94% (62/66) only kill_app's two pkill lines
leechblock_fetch.sh 38.57% (27/70) download_extension: curl/tar/unzip
leechblock_browsers.sh 36.47% (31/85) replace_browser_in_place: sudo binary patching
cco_wrapper/install/report, leechblock_config/firefox 0% every function pkills, sudo-writes or installs

The pattern: the decision logic reaches ~90%+, the effecting code reaches 0%. A 100% bar means shimming sudo, pkill, systemctl, curl and npm for every installer in the repo. That is the real cost to put in front of the user.

The harness that works — extend it, do not clone it

linux_configuration/scripts/periodic_background/digital_wellbeing/lib/tests/ holds leechblock_harness.sh and cco_harness.sh plus a run_all.sh whose glob is test_*.sh. One harness per directory, not per file — jscpd fails above 2% duplication and five near-identical harnesses is exactly how that trips.

Two patterns worth copying:

  • _t_isolate_path narrows PATH to a shim dir. It is the only way to make command -v jq or command -v chromium fail on a machine that has them. Every external the code under test calls must be seeded into that dir — a missing mkdir aborted the suite under set -e with no stderr, which under kcov looked like a coverage-tool bug for twenty minutes.
  • A fixture self-check. cco_harness.sh asserts its own globals are populated and calls notify for real. This is not linter appeasement: it is what turns "a typo in a global name" from a silent skipped assertion into a loud failure, and it happens to satisfy SC2034 honestly.

Traps that cost real time in Phase 1 — all still live

  • shfmt -w corrupts [hyphenated-keys] in associative arrays, turning ${BROWSERS[google-chrome-stable]} into a subtraction expression. Quote the subscripts. Reproduced live in a test file during this campaign. Canary: grep -nE '\[[a-z0-9]+ - ' <file> must be empty after every shfmt and every pre-commit run.
  • A comment line starting with # shellcheck is parsed as a directive and fails with SC1072/SC1073. Write "the linter", not "shellcheck", at the start of a comment line.
  • verify_shell_split.sh cannot see heredocs. In setup_midnight_shutdown.sh it reported a bare -log, because log() sits at column 0 inside a generated script. For any file that emits scripts, the real check is hashing the emitted content, not the function list.
  • New sourced libs need a shebang AND the exec bit, staged with git add --chmod=+x. A plain git add afterwards resets it. Payload files under lib/payloads/ are 100755 too — the shebang hook rejects 100644.
  • gh and git push race. Several pushes failed with "cannot lock ref"; git fetch then re-push. Not a divergence.

Known-broken, NOT caused by the campaign

Python tests has been failing since 2026-08-17 — before any of this work. The cause is pip hitting error: resolution-too-deep while installing meta/requirements.txt, on commits that touch no Python at all. It needs dependency pinning, and it is why python-tests.yml is deliberately not in check_ci_green.sh's required list. Worth fixing on its own; do not fold it into Phase 3.

Rules that will bite you

  • No suppressions, ever. No # noqa, no shellcheck disable, no per-file-ignore without asking every single time.
  • Every commit touching code needs evidence in docs/superpowers/evidence/<slug>-<date>.json, plus a contract in docs/superpowers/contracts/ once ≥4 code files are staged. Put the measured number in it, never a rounded or hoped-for one.
  • A test file broken by your commit is a same-session bug. Three were broken this way in Phase 1 (security_apps.sh, test_shutdown_timer_monitor.sh, and the pacman pair) — each asserted on code a split had moved. Fix the assertion to search the entry script and its libs; do not weaken it.
  • pre-commit run --files <changed> after git add. prettier and ci-mirror run on pre-push. npx prettier --write any .md/.json.
  • Work directly on main. git stash and branch creation are blocked.
  • Verify what actually got committed. Re-grep the file after committing; an autoformat pass stripped a load-bearing variable definition three times in Phase 1, once landing a set -u abort in setup_night_lockdown.sh that had to be fixed in a follow-up commit.