Commit Graph

236 Commits

Author SHA1 Message Date
a1f8044a4a Extract system-maintenance, the isolated subsystem
github.com/kuhyx/system-maintenance, cloned to ~/system-maintenance

First of the four live subsystems, and the one with no cross-unit source
edges, so it establishes the pattern for the harder three. Passed in
vmbox first -- the installer ran from an extracted-layout copy in the
guest and detected its distro without any monorepo path -- then on the
host.

Its 21 Python and shell test files lived in linux_configuration/tests/,
reaching bin/ through a shared conftest. Twenty moved with the code and
got their own conftest; the shared one stays, since it serves several
script directories that are not going anywhere.

test_shutdown_timer_monitor.sh stays here. It is a pair test: it checks
that digital_wellbeing's installer template and the monitor it installs
still agree, so it belongs to whichever half owns the installer. It now
reads the monitor from ~/system-maintenance and SKIPs when that checkout
is absent.

An intermediate version of that test fell back to /usr/local/bin, which
HUNG -- the installed copy is stale and differs from source, so the test
was comparing an installer template against a binary it never generated.
Falling back to it would have been worse than skipping, so the fallback
is gone and the comment says why.

Also removed test_hosts_file_monitor.sh from shell-tests.yml's explicit
list: it left with the subsystem, and CI would have failed on the missing
file rather than quietly skipping it.

Verified: 136 tests pass in the new repo, 202 still pass here, the pair
test passes against the checkout and skips without it, and both the shell
coverage gate and shellcheck are clean. Depth 294 -> 267.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-23 17:55:29 +02:00
6b5fe8e7a7 Extract mtk_root, with the libraries that lived outside it
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
github.com/kuhyx/mtk-root

Unlike the app extractions, mtk_root could not travel alone: its four
libraries and three of its four test suites sat in scripts/lib/, one
directory up from the stages that source them. Taking only the directory
would have orphaned eight files here and left the new repo unable to
source anything, so all twelve moved in one filter-repo pass. The
mtk_root/ + lib/ sibling layout is preserved, so every
`source "$SCRIPT_DIR/../lib/..."` resolves with no call site edited.

Two things the new repo needed that the monorepo provided implicitly:

  lib/tests/run_all.sh  -- the copy here is shared with the non-mtk suite
                           and stays. Without a new one, shell-tests.yml's
                           glob over */lib/tests/run_all.sh would have
                           stopped covering these suites on a green build.
  .shellcheckrc         -- external-sources + source-path=SCRIPTDIR, which
                           is what lets shellcheck run with no severity
                           filter and still pass on the ../lib/ sources.

mtk_harness.sh stays here despite the name: test_common_datetime.sh
sources it. Removing it broke that suite outright, which running the
suite caught immediately. Its header now describes what it actually
serves; the misleading name is kept on purpose, since renaming would
churn its one consumer for nothing.

Verified: 84 tests pass in the new repo with shellcheck clean, 55 still
pass here, and check_shell_coverage.sh --all exits 0 with the four
mtk_root allowlist entries gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-23 17:39:27 +02:00
a0459b99a0 Extract reverse_survivors, kcd2_dice_solver and focus_owner
Three self-contained apps move to their own public repos with full
history (8, 14 and 30 commits, each matching the monorepo count):

  github.com/kuhyx/reverse-survivors
  github.com/kuhyx/kcd2-dice-solver   (cloned to ~/kcd2-dice-solver)
  github.com/kuhyx/focus-owner

Each gained a CI workflow. Only reverse_survivors had one here; the other
two were never gated by anything, which is part of why they no longer
belong in a repo whose CI is shared.

What stayed: setup_kcd2_dice_solver.sh, which deploys the solver rather
than being part of it. It now builds from ~/kcd2-dice-solver, and the
post-commit hook it generates drops its `git diff -- kcd2_dice_solver/`
filter -- in a standalone repo every commit is solver code, so the old
filter could never match again.

focus_owner's two references to python_pkg.focus_policy now name the
phone-focus-mode repo, where that generator is headed.

Running the live kcd2 unit -- rather than reading it -- found three real
breakages that no amount of grepping would have: pnpm ignoring the
package.json "pnpm" field, pnpm refusing to purge node_modules without a
TTY, and the Docker container still bind-mounting the deleted monorepo
path. All three are fixed; the unit exits 0 and
https://dice.kuhy.duckdns.org/ serves the freshly built bundle.

Depth violations 417 -> 303.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-23 17:34:02 +02:00
ec5d3aba34 Archive billsplit and everything coupled to it
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
The coupled case the previous archive commit deferred. billsplit is not
just a directory: a coverage package exists only to gate it, app_icons
holds a registry entry pointing into it, a CI workflow builds it, and
.binary-allowlist / .gitignore carry a paired set of rules for its
launcher icons. Removing the directory alone would have failed the 100%
coverage gate and left three configs naming a missing path.

So all of it goes in one commit:

  billsplit/                        73 files
  python_pkg/billsplit_coverage/     5 files, gated billsplit only
  .github/workflows/billsplit-ci.yml
  the app_icons AppIcon(key="billsplit") entry
  the .binary-allowlist patterns and their .gitignore ! overrides
  the .prettierignore fixture line

billsplit/ and billsplit_coverage/ were extracted in a single filter-repo
pass (8 commits, matching the monorepo) and pushed to
github.com/kuhyx/testsAndMisc-archive, where the coverage package joins
the archive's existing python_pkg/ tree. Presence was confirmed through
the GitHub API before anything was deleted here.

.binary-allowlist is now empty of patterns -- billsplit's icons were its
only entries. The policy header stays and records that no exception is in
force, and check_no_binaries.sh still exits 0 on a comment-only file.

Verified: 2095 tests pass at 100% coverage, down from 2114 by exactly the
19 billsplit_coverage tests. Depth violations 451 -> 417.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-23 17:17:22 +02:00
ea281134b6 Archive bucket_catch and poker-stakes to testsAndMisc-archive
Two self-contained apps, neither referenced by any other code, systemd
unit or symlink -- which is why they go first in the restructure.

Full history preserved via git filter-repo and merged into
github.com/kuhyx/testsAndMisc-archive at their original repo-relative
paths, matching how C/, CPP/, horatio/ and the rest already sit there.
Archived commit counts match the monorepo exactly: bucket_catch 10,
poker-stakes 2.

poker-stakes-ci.yml goes with the app it builds, so no workflow is left
pointing at a missing path. bucket_catch had no workflow.

Verified before deleting: file counts match (64 and 65), diff -r against
the working tree is clean, and the GitHub API lists both units in the
archive. Depth violations drop 556 -> 451.

billsplit is deliberately NOT part of this commit: it is coupled to
python_pkg/billsplit_coverage and an app_icons registry entry that the
100% coverage gate requires be deleted in the same commit as the app.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-23 17:12:54 +02:00
46c8f065e8 Wire the shell ratchet into pre-commit, and test the coverage tooling
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
2026-08-23 15:51:29 +02:00
5ba2cbd476 Dedupe endurain imports against postgres when the API cannot answer
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Endurain API keys are upload-only by design: the scope allow-list is
frozenset({"activities:upload"}), enforced in both validate_api_key_scopes
and the create schema, and the activities read route is mounted with
validate_access_token rather than validate_access_token_or_api_key. A key
therefore fails at authentication before any scope check, so the HTTP
dedupe check added in 325d5a8b could never succeed and every pass fell
back to the ledger alone. Verified against v0.19.0; upstream documents no
plan to widen the allow-list.

Read the start times out of postgres instead. The container publishes no
host port, so this goes through docker exec -- no new dependency and no
port opened just to read four rows. The HTTP path is still tried first so
the supported route resumes working on its own if the allow-list ever
grows.

None and [] are kept strictly apart: a dead container, timeout or non-zero
exit means "could not ask" and returns to the ledger, while an empty result
set is a real answer. Conflating them would re-upload everything on any
docker hiccup, which is the bug the dedupe check exists to prevent.

Proven with an empty ledger: the known-duplicate TCX was skipped and the
activity count held at 4, and the same file WAS imported with
ENDURAIN_NO_DB=1, so the check is what blocks it. The duplicate that
negative control created was removed afterwards.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GbYEnfGnf1988cdFdGebot
2026-08-23 15:23:08 +02:00
8ab15446fd Make the push gate check the push, and make its pytest stage real
`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
2026-08-23 15:12:23 +02:00
325d5a8baa Stop the endurain importer duplicating RunnerUp's own uploads
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
RunnerUp can now upload to Endurain directly as well as through the WebDAV
inbox, and the two paths cannot see each other: the same run landed twice
(activities 14 and 15, identical start time, distances differing only by
tcx/gpx rounding). The ledger only knows what this importer handled, so it
could not catch it.

Add a third dedupe check that asks Endurain what it already holds, queried
once per run. Endurain exposes no query-by-start-time endpoint, so a page of
recent activities is fetched and compared client-side, with a +/-120s
tolerance: the file for activity 15 embeds 21:51:04Z while the server stored
21:51:05Z, so exact equality misses the very duplicate that motivated this.
A failed or unauthorised query returns None, never an empty list, so it can
never be read as "nothing matches" and upload on the strength of it.

Export filenames are parsed as LOCAL wall-clock time. RunnerUp_2026-08-14-
00-58-45_Running is 2026-08-13 22:58:45 UTC; reading it as UTC would move a
late-evening run onto the wrong day and defeat the comparison.

Also fix the adb fallback, which broke the moment a second device attached:
phone_attached() returned true if ANY device was in the 'device' state, then
every bare adb call failed with "more than one device/emulator" as a warning
on a path nobody watches. The serial is now resolved once and threaded into
every call as -s. Ambiguity is settled by configuration, never by guessing --
several devices and no ENDURAIN_ADB_SERIAL means the fallback is skipped.

The remote check stays inert until the API key is granted activities:read;
until then it falls back to the ledger, which is the previous behaviour.

85 tests, 100% branch coverage on all four modules, no suppressions. The
oversized test modules are split by concern to stay under the 250-line cap.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GbYEnfGnf1988cdFdGebot
2026-08-23 14:36:03 +02:00
b399fab56d Take the fixes/lib suite from 110s to 37s, same 645 assertions
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Three changes, none of which touch what a single test asserts:

thorium_repairs.sh's `sleep 4` becomes `sleep ${THORIUM_STARTUP_WAIT:-4}`
and the harness sets 0.2s. Three cases were each paying four real
seconds to watch a stubbed browser stay up; the stub sleeps 30s, so the
`kill -0` check is unchanged. That file: 17.0s -> 5.6s.

_t_hide was the rest of it. It walks PATH and creates a symlink per
executable -- ~13.5k here, 4.6s a build -- and the suite has ~16
distinct tool sets. Now the full farm is built ONCE and `cp -al`
hard-links it per set, with only the hidden names deleted from the copy.
The cache also moved out of $TEST_TMPDIR, which is per-file, into a
run-scoped dir run_all.sh creates and removes, so it survives between
files.

110s -> 98s (sleep) -> 85s (shared cache) -> 37s (hard-linked farm).

645 OK, 0 FAIL throughout. The cases that only pass when a tool is
genuinely unfindable -- install_pyroveil's missing-dependency arms,
fix_aggressive without sqlite3 -- still pass, which is what proves the
farm is still hiding rather than quietly returning a full PATH.

lib_test_core.sh hit exactly 250 lines, so _t_hide and the farm move to
lib_test_path.sh (194 + 67).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 14:00:13 +02:00
cb1e8c53c9 Test nvidia_config.sh to 100%; the 8-lib campaign is done (85 -> 84)
76/76 lines through run_all.sh at --min 100, exit 0. 45 assertions over
15 cases across all four functions.

XORG_CONF and XORG_CONF_D are plain globals ASSIGNED inside
configure_xorg, so the default had to move to the assignment itself --
exporting a value from the harness alone would have been overwritten on
entry. PROFILE_FILE and USER_HOME take the ordinary `${VAR:-default}`
form. The real /etc/X11/xorg.conf.d/20-nvidia.conf keeps its Jun 2025
mtime and ~/pyroveil does not exist after the suite runs.

configure_xorg's RenderAccel value is asserted to be "true" AND asserted
never to be "false": the lib's own comment records that "false" forces
software rendering and pins Xorg above 30% CPU, so that is a regression
worth a dedicated negative assertion.

install_pyroveil's prompt could not be asserted on its text -- bash
writes a `read -p` prompt straight to the terminal, so it never reaches
captured output. The branches are distinguished by the non-interactive
arm's "Auto-installing" line instead, which is observable.

bt_audio.sh re-measured at 127/127 with the new files present.

That is all eight libs from the original handoff: arch_sysctl (banked as
unmeasurable and quarantined), arch_cpu, arch_hardware, thorium_repairs,
hosts_guard_migrate, hosts_guard_rollback, ubuntu_perf_fixes and
nvidia_config. Allowlist 91 -> 84.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 13:14:37 +02:00
558883f281 feat(endurain): self-hosted run viewer fed by RunnerUp WebDAV exports
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
2026-08-23 13:09:18 +02:00
ad55d1b6a1 Test ubuntu_perf_fixes.sh to 100%, take it off the allowlist (86 -> 85)
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
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>
2026-08-23 12:38:40 +02:00
0201d5460a Clear both hosts-guard libs; teach the instrument done <redirect> (88 -> 86)
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
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>
2026-08-23 11:50:13 +02:00
0e9629e110 Test thorium_repairs.sh to 100%, lift the gate timeout (89 -> 88)
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
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>
2026-08-23 11:05:56 +02:00
88ba911641 Test arch_hardware.sh to 100%, fix its dead journal vacuum (90 -> 89)
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
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>
2026-08-22 21:46:42 +02:00
b4b6908d69 Test arch_cpu.sh to 100% and take it off the allowlist (91 -> 90)
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
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>
2026-08-22 21:14:57 +02:00
7c14cfca61 Test arch_sysctl.sh and open the optimize_arch_desktop lib family
23 assertions over 8 cases covering both tweak_vm_sysctl and
tweak_network_sysctl: the skip path, the write-when-the-drop-in-is-
missing path that a naive "nothing to do" check gets wrong, the
value-differs path, an unreadable-sysctl path, and the tcp_bbr guard.

arch_desktop_harness.sh is the third family harness, supplying the
globals and apply_tweak that live in optimize_arch_desktop.sh rather
than in any lib. arch_cpu.sh and arch_hardware.sh will reuse it.

SYSCTL_DROPIN_DIR defaults to the real /etc/sysctl.d, matching the
seam ubuntu_perf_more.sh and arch_perf_fixes.sh already carry; the
suite runs un-jailed in ci_mirror.sh and CI, so a bind mount would
protect nothing. Both real drop-ins keep their Mar 2 mtimes.

_t_run lands in lib_test_core.sh because `out="$(func)"` forks a
subshell and kcov never registers a lib whose first execution happens
inside one -- measured as 0 lines via $( ) against 54 lines via a
direct call.

arch_sysctl.sh STAYS on the coverage allowlist: it still reports "kcov
instrumented no lines" through the jail, while ubuntu_perf_more.sh
measures 32/32 in the same worktree and the same invocation. The tests
are real and green; the coverage number is not yet obtainable, and the
exemption may only come off once it is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 20:54:47 +02:00
fa7b05da74 Test arch_perf_probes.sh to 100%: allowlist 93 -> 92
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>
2026-08-22 19:04:50 +02:00
ffcf99f102 Test arch_perf_report.sh to 98.33% and fix its dead journal-size check
Fifth of the 16 untested fixes/lib libs and the first of the arch-performance
group, which gets its own harness here. 38 assertions across two files. The
lib STAYS on the allowlist at 59/60 = 98.33%, measured twice.

The second dead branch this campaign has found, fixed here. Lines 36 and 65
matched on:

    ([0-9]+\.?[0-9]*)\ (G|M)

a REQUIRED space before the unit. journalctl actually prints "Archived and
active journals take up 305.5M in the file system." with no separator --
verified against the real binary on this machine. So check_journal_size could
never report a large journal and apply_safe_fixes could never vacuum one:
both were unreachable. The space is now optional, and tests cover the spaced
and unspaced forms so neither can rot.

NOTE: this makes a previously-dead DESTRUCTIVE action live. With
--apply-safe-fixes and a journal over a gigabyte, the entry script will now
really run `journalctl --vacuum-size=300M`. That is the flag's documented
purpose and it stays behind the flag, but it has never actually executed
before today.

Line 20, the one uncovered line, is `} >>"$REPORT_FILE"` -- the closing brace
of a redirected group. A minimal probe writes both inner lines to the file
while bash attributes no statement to the brace, and the report assertions
independently prove the group runs, since they read the text it writes. Same
defect (a) class as bt_audio's `} |`.

That form now caps four libs: bt_audio.sh, arch_perf_report.sh, and
arch_perf_probes.sh and nvidia_config.sh which are still untested. So the
continuation_lines() fix is worth landing after all -- as its own commit,
next, so a denominator change can be reverted without taking these tests
with it.

_t_hide and _t_full_path move from bt_harness.sh into lib_test_core.sh: they
are generic PATH plumbing and the arch harness needs them for the
"nvidia-smi is not installed" branch. All twelve existing suites re-run green
after the move.

One trap worth recording: apply_safe_fixes appends to the ACTIONS array, so
it must be called directly rather than inside "$(...)" -- a command
substitution runs it in a subshell where the appends die with it, and the
assertions read an empty array.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 18:06:57 +02:00
a5b6663c02 Test bt_audio.sh to 96.97% and fix its wpctl health check
Fourth of the 16 untested fixes/lib libs, the largest at 214 lines, and the
last of the bluetooth family. 47 assertions across three files. The lib
STAYS on the allowlist: 128/132 = 96.97%, twice with the same line set.

The bug the tests found, fixed here. Line 21 read:

    if timeout 3 _run_as_user wpctl status &>/dev/null; then

`timeout` is a binary and `_run_as_user` is a shell function that is never
exported, so this always failed with rc=127 -- "timeout: failed to run
command '_run_as_user'". check_pipewire_health therefore reported PipeWire
hung on EVERY run and restarted a healthy audio stack every time
fix_bluetooth.sh ran. The healthy branch was unreachable, which is why the
test for it could not be written without first noticing. Now
`_run_as_user timeout 3 wpctl status`: same bound, inside the sudo'd command.

Why 96.97% and not 100%. An earlier run of this suite read 99/99 = 100.00%,
and that number was wrong -- it measured the file before pre-commit's
`shfmt -w` reformatted it. shfmt rewrites the `{ ...; } \` + `| bluetoothctl`
one-liners into multi-line blocks ending in `} |`, which raises the
denominator 99 -> 132. Confirmed unavoidable by running shfmt against HEAD's
own copy: it rewrites there too, so reverting the reformat is not stable.

The four uncovered lines (90, 113, 137, 157) are each that `} |` -- the pipe
operator joining a block to bluetoothctl, not a statement. They do execute:
a `{ echo a; echo b; } | consumer` block writes every marker to a file even
though bash's xtrace never reports the left-hand side, because a pipe's LHS
runs in a subshell whose xtrace is not captured. kcov instruments them and
cannot attribute them; defect (a) in docs/kcov-under-report.md, third flavour.

Scope, measured: the continuation-pipe form appears ONLY in bt_adapter.sh
(pre-format) and bt_audio.sh (post-shfmt). None of the remaining twelve
untested libs carries either, so this does not block the campaign and the
detector fix stays off the critical path.

Also worth 90.91% -> 96.97% on its own: the bluetoothctl stubs now drain
stdin before exiting. Real bluetoothctl consumes its whole input; a stub
that read one line and exited closed the pipe early, so the writing block
took SIGPIPE partway through and its later lines never ran.

NOTE for whoever edits this file next: bt_audio.sh is now exactly 250 lines,
at the cap. The next change to it needs a split, not a comment trim.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 17:52:24 +02:00
434cb7587e Test bt_adapter.sh to 98.55%; line 97 is unreachable by construction
Third of the 16 untested fixes/lib libs, and the first that does NOT come
off the allowlist. 68/69 = 98.55%, up from 45/69. The remaining line is
not a missing test:

  PS4='+TRACE:${BASH_SOURCE##*/}:${LINENO} ' bash -x

shows bash attributing the whole multi-line dbus-send assignment in
_services_resolved to line 100, and never emitting line 97 at all. Line 97
is the OPENING line of a backslash-continued statement; kcov instruments
it, bash never attributes a statement to it. That is defect (a) in
docs/kcov-under-report.md -- a wrong denominator -- and no test can move it.

continuation_lines() in shell_coverage_lines.py already excludes such lines,
but only detects them by tracking QUOTE state, so a line ending in an
unquoted backslash slips through. Fixing that is deliberately left out of
this commit: it changes the denominator for every measured lib, and if it
regresses one of the libs now gated with no exemption, reverting a mixed
commit would throw away these tests too.

Scope of the problem, measured rather than guessed -- backslash
continuations are confined to the bluetooth family: bt_audio.sh 10,
bt_adapter.sh 7, bt_pairing.sh 3, pacman_hook_stall_summary.sh 2, and ZERO
in the other 12 remaining libs. So this caps bt_audio.sh next and then stops
mattering.

Also replaces the harness's _t_only_path with _t_hide. _t_only_path dropped
PATH to $FAKE_BIN alone, which hid grep, head and sort as well as the tool
under test -- so the "usbreset is not installed" case died in a cascade of
"command not found" and passed for the wrong reason instead of exercising
the fallback. _t_hide rebuilds PATH from symlinks to every real executable
except the named ones; verified directly that command -v usbreset is then
empty while grep and head still resolve.

bt_report.sh (37/37) and bt_pairing.sh (65/65) re-measured at --min 100,
both exit 0, so neither cleared lib regressed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 17:10:15 +02:00
0e9cc0ce95 Test bt_pairing.sh to 100%: allowlist 97 -> 96
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
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>
2026-08-22 16:47:40 +02:00
11aea86920 Land the token_audit standing-cost checkup (116 tests, 100% branch coverage)
Answers "what is billed on every turn regardless of use" by measurement
instead of recall, which is the part earlier audits kept getting wrong.

A surface is reported dead only when unused in BOTH windows. One earlier
audit's seven-day dead list included six servers that had been used within
thirty days, so a single window is not evidence. Shares are weighted with
cache-read weight 0.1, so a cached surface is not billed as though it were
re-read in full each turn.

Verified by running it, not just by tests: the report renders over 44,311
turns, putting rules/ at 4,907 tokens (1.77%), MEMORY.md at 4,312 (1.55%),
memories/ at 3,758 (1.35%), skill descriptions at 3,279 (1.18%) and CLAUDE.md
at 2,057 (0.74%), with per-MCP 7d/30d call counts behind each verdict.

The files were found untracked in the tree from an earlier session; every
module is already under the 250-line cap, so nothing needed splitting, and
the package was already at 100% branch coverage, so nothing needed new tests.
This lands the instrument only -- no surface is unregistered on its say-so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 14:33:03 +02:00
81191f40e6 Fix both kcov defects: dwm_config 47.95% -> 98.48%, rpi_nc_install 11.36% -> 96.59%
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>
2026-08-22 14:24:25 +02:00
0091608b08 Make the gate read jail_args and refuse a suite that dies partway
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
2026-08-22 13:36:54 +02:00
b91be3099d Clear unity_handler.sh: 100%, off the allowlist, 105 -> 104
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
2026-08-22 13:20:54 +02:00
ac1cf57656 Give testsAndMisc-bash/lib a harness; cover transcribe_deps and _pkgmgr
The directory held five allowlisted libs and no tests. It now has a harness,
a runner, and suites for two libs: transcribe_deps.sh (the fourth
hardest-first target, 48/52 = 92.31%) and transcribe_pkgmgr.sh (49/50 =
98.00%). Both stay on the allowlist. 15 + 18 assertions, 0 failures.

No jail is needed here and no jail_args marker is created: every branch turns
on `command -v <manager>` and every install goes through sudo, both
intercepted by a PATH stub dir. Verified the runner exits 0 when invoked
bare, which is how both gates call it.

The harness REPLACES PATH rather than prepending. This host really has
pacman, and a prepended stub dir cannot HIDE a binary, so every "manager not
installed" case would have asserted nothing. `bash` and `env` are in the
private coreutils dir for a non-obvious reason: every stub's
`#!/usr/bin/env bash` shebang resolves through that restricted PATH, and
without them each stub died with "env: 'bash': No such file or directory"
while looking exactly like a coverage gap.

Assertions are on recorded ARGV rather than on the call happening, because
which packages a branch installs is what differs between distros --
pacman gets python-virtualenv where apt gets python3-venv.

Three assertions aimed at the libsndfile branch were REMOVED rather than left
green-but-vacuous: /usr/lib/libsndfile.so exists on this host, so
need_libsndfile is legitimately 0 and lines 19/52/53/55 are unreachable
without a jail that hides the library. The limitation is written into the
suite instead of asserted around.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016sxxsDHWgdRhmdmzNMA6XM
2026-08-22 13:14:02 +02:00
170f843481 Make the jailed lib/tests gate able to fail, without breaking measurement
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
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
2026-08-22 12:16:18 +02:00
61ff5eacac Cover nc_php.sh (98.82%) and teach the jail to measure foreign platforms
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
2026-08-22 11:30:32 +02:00
bca70b67b5 Shell coverage: convert the ratchet to a measured 100% bar
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
2026-08-22 11:20:58 +02:00
d42d8ff20d Shrink the shell-coverage allowlist 115 -> 106: cover scripts/lib
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
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>
2026-08-21 23:52:09 +02:00
1d7271f023 Phase 2: enforce the 250-line cap and stop pushing red
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
2026-08-21 16:41:31 +02:00
d72f03ea93 setup_midnight_shutdown: split into lib/ — the cap is now zero
1734 -> 109 lines across eight libs plus one payload file. This was the last
file over the 250-line cap; check_file_length.sh --all now reports "all
checked files are within 250 lines".

This file needed a different proof from the others. Its product is scripts
generated into /usr/local/bin via heredocs, so "did the functions move
verbatim" is not the question that matters — "does it still emit the same
bytes" is. Every heredoc body was hashed before the split: 11 of 12 are
byte-identical afterwards, and the twelfth (the 224-line config-unlock body)
now lives in lib/payloads/shutdown-config-unlock.sh.in at hash
1a5c67cfd0244101, exactly matching what it replaced. No new bodies appeared.

The "~39 top-level functions" this file is documented as having are not real.
log(), now_epoch(), cmd_add() and friends sit at column 0 because they are
inside the generated scripts. The brace-matching extractor keeps each heredoc
with its owning generator, so they moved as part of it. That is also why
verify_shell_split.sh reports a bare "-log": it cannot tell a heredoc-internal
definition from a top-level one. log() is not lost — it is line 17 of the
payload, which is where it always ran from.

$_MS_PAYLOAD_DIR is defined in the same file as its use, directly above it,
following the _NL_PAYLOAD_DIR lesson from earlier in this campaign.

--help is byte-identical, shellcheck is clean with no suppression added.
`setup` itself is not executed — it installs systemd units, makes the config
immutable and arms a shutdown timer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV
2026-08-21 16:27:54 +02:00
fbe9134c22 pacman_wrapper: split into flat libs under the 250-line cap
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
929 -> 217 lines across five pw_*.sh libraries. This file IS /usr/bin/pacman
here (the symlink points at the deployed copy), so the design constraint was
not readability but that no failure mode may leave the machine unable to
install packages.

Fail OPEN, unlike the other splits in this campaign. A missing lib warns on
stderr and execs real pacman unwrapped. Failing closed would mean a missing
lib leaves no way to install anything, including the missing lib. This is the
same choice the existing pacman_lock_lib.sh block already makes, and it is
verified rather than asserted: with pw_guard.sh removed from a scratch-dir
copy, the wrapper warns, execs, and returns real `-Qi bash` output at rc=0.

The libs load at the very top, before the fast path, because the fast path
calls needs_unlock() to decide whether to exec straight through, and that
function now lives in pw_guard.sh.

They are deployed flat beside the wrapper — the shape pacman_lock_lib.sh and
heavy_job_lock.sh already use in /usr/local/bin — as required copies, and are
added to the integrity manifest. Tamper detection was round-tripped in a fake
root: 8 manifest entries verify clean, and appending one line to
pw_challenge.sh is detected and the transaction denied.

Verified by execution, not just textually: real `-Qi bash` and `-Q` through
the split wrapper are byte-identical to the pre-split baseline, same 2824
packages. verify_shell_split.sh reports all 14 top-level functions moved
verbatim.

The deployed copy is deliberately NOT updated by this commit and still hashes
548fc2b5... exactly as before the work started; live pacman is unaffected.
A live `pacman -S` remains unverified — the Bash tool deadlocks on db.lck and
a failed experiment costs the package manager.

Cap is now down to one file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV
2026-08-21 16:19:26 +02:00
79a49846b9 setup_night_lockdown: define _NL_PAYLOAD_DIR where it is used
The split referenced $_NL_PAYLOAD_DIR from both install functions but
defined it nowhere, so `setup_night_lockdown.sh setup` would have died
with an unbound variable partway through installing the lockdown. My
earlier scratch verification set the variable by hand, which is exactly
what hid the bug: the emitted artifacts hashed correctly while the real
entry path could not have reached them.

It is now defined in each lib that uses it, eight lines above the use,
via `: "${_NL_PAYLOAD_DIR:=...}"` so an explicit override still wins. A
definition placed in the entry script was silently stripped by an
autoformat pass three times during this work, and a half-run lockdown
installer is not an acceptable failure mode.

Also drops two zero-byte payload files left behind by the .sh -> .sh.in
rename.

Re-verified without presetting anything: install_enter_script and
install_unlock_script emit mode-0755 files hashing 4baaf405... and
eafce28d..., byte-identical to the live /usr/local/bin copies. help and
an unknown command match their baselines; status differs only by the
05:00 countdown ticking 13h to 12h between runs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 16:02:59 +02:00
f6bfdff76a setup_night_lockdown: split into lib/ under the 250-line cap
918 -> 138 lines, across five libs plus two data-file payloads.

The two generated scripts became data files rather than heredocs.
ENTER_EOF alone was 279 lines, so any lib containing it would have been
over the cap however the rest was arranged. Both heredocs were quoted,
so their bodies were already literal text with no build-time
interpolation, and copying them verbatim is exactly equivalent.

The payloads are named *.sh.in on purpose. The pre-commit hook globs
staged files by '\.(sh|bash|zsh)$', runs shfmt -w on them and re-stages
the result -- and shfmt does reformat these bodies. That would have
changed their bytes and destroyed the property this split is verified
by. The .in extension keeps them out of every .sh-globbing hook without
a suppression or an exclude regex.

cat + chmod 0755 became a single `install -m 0755`, closing the window
the original left between creating the file and making it executable.

The load-bearing check: running install_enter_script and
install_unlock_script against a scratch directory produces mode-0755
files whose sha256 sums are identical to the copies already deployed in
/usr/local/bin on this machine. The split provably emits the exact bytes
that are running today, and it touched no system file -- sha256sum -c on
both deployed artifacts passes unchanged.

help, status and an unknown command are byte-identical to their
pre-split baselines. A line-multiset check leaves exactly six lines
unaccounted for: the two cat/heredoc openers, the two terminators and
the two chmod calls that install -m now performs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 15:59:35 +02:00
72e1b6a8b1 block_compulsive_opening: test the allow/block decision
21 assertions for cco_state.sh, the lib that decides whether a launch is
permitted. Coverage measured: 62/66 lines = 93.94%. The only uncovered lines
are kill_app's pkill/pkill -9 pair, which must never run in a test on a
machine whose apps are live. cco_wrapper/cco_install/cco_report stay at 0%:
every function in them moves system binaries, writes /etc/pacman.d/hooks or
kills processes.

The assertion that matters most is the one in the negative direction: a
PREVIOUS hour's stamp must NOT keep blocking. If that decision inverted, a
legitimately-closed app could never be reopened, and the failure would look
like the tool working. Both directions are pinned, plus the empty-stamp case
and stale suspend-marker cleanup.

Also generalises run_all.sh from test_leechblock_*.sh to test_*.sh. This
directory now holds suites for more than one entry script, and a prefix glob
silently skips every suite added after it — including inside the CI discovery
step added in fbec4656, which runs this file.

The harness asserts its own fixture is complete rather than letting a typo in
a global surface as a confusing unbound-variable from inside the code under
test. That check is also what makes the fixture's globals legible to
shellcheck, which cannot see them being read across the source boundary — a
real check that happens to remove SC2034, not a suppression.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV
2026-08-21 15:53:54 +02:00
f5edebae4a block_compulsive_opening: split into lib/ under the 250-line cap
705 -> 182 lines, with state, the launch gate, install/uninstall and
reporting moved into cco_state.sh, cco_wrapper.sh, cco_install.sh and
cco_report.sh.

The entry script keeps its exact deployed path and stays a real file
there, not a symlink: the app wrappers already installed on disk
hard-code /usr/local/bin/block-compulsive-opening.sh, and the pacman
hook invokes it. _CCO_LIB_DIR probes for lib/ beside the script and
falls back to the script's own directory, so the repo tree and the flat
/usr/local/bin deploy layout both work; both branches were exercised.

install_all deploys the libs and uninstall_all removes them in this same
commit, so there is never a deployed copy that is split but missing its
libs -- the failure that had to be repaired for install_leechblock.

The libs deploy flat into /usr/local/bin rather than into
/usr/local/share/digital_wellbeing/lib, because install_leechblock_payload
does an rm -rf mirror of that directory and would delete them.

Verified beyond the usual gates: a line-multiset check reports zero
missing functional lines, and all three heredoc bodies -- the generated
app wrapper, the generated pacman hook and the help text -- are
byte-identical, which verify_shell_split.sh cannot check. status output
matches the still-monolithic deployed copy byte for byte.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 15:51:06 +02:00
63454fa544 install_leechblock: first test suite for the split libs
29 assertions across two files, run via lib/tests/run_all.sh.

Coverage measured, not waived: leechblock_fetch.sh 38.57% (27/70),
leechblock_browsers.sh 36.47% (31/85), config and firefox 0%. The uncovered
lines are exactly the bodies that must never run under test — curl/tar/unzip
downloads, pkill + sudo binary patching, npm install, and writes to
/etc/*/policies.json. Closing the gap needs those externals shimmed.

What the tests actually pin down:
- all three get_latest_tag fallbacks (releases API, tags API, Location
  header), plus JSON-null-is-not-a-tag and the all-sources-exhausted
  non-zero return. A silent failure there installs the wrong version.
- the 8+3 browser tables including every hyphenated key, and the
  command -v gating that decides whether a binary is patched at all.

Also fixes a latent set -u abort: wire_up_browsers interpolates VERSION_DIR,
VERSION and TAG, which only resolve_version sets. Calling it on its own
aborted on an unbound VERSION_DIR; they are now read through locals with
defaults, leaving production output unchanged.

Harness note: PATH must be narrowed to the shim dir for "no browser
installed" to be testable on a machine that has five, but every external the
code under test calls has to be seeded there too — a missing mkdir aborted
the suite under set -e with no stderr, which under kcov looked like a
coverage-tool bug rather than a PATH bug.

The hyphenated array subscripts in the test are quoted for the same reason
they are in the lib: shfmt -w rewrites an unquoted [google-chrome-stable]
into a subtraction expression. Reproduced live in this file during the work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV
2026-08-21 15:39:15 +02:00
1dceca6600 install_leechblock: split into lib/ under the 250-line cap
485 -> 114 lines, with the six install phases moved into four sourced
libs: leechblock_fetch.sh, leechblock_config.sh, leechblock_browsers.sh
and leechblock_firefox.sh.

detect_browsers, replace_browser_in_place and wire_up_browsers stay in
one lib on purpose: detect_browsers writes the BROWSERS/FIREFOXES arrays
that wire_up_browsers reads, and splitting them would create a
write-only cross-file global.

Four justified body changes, all recorded in the evidence:

- Quoted the six hyphenated associative-array keys. shfmt 3.13.1 parses
  an unquoted [google-chrome-stable] as arithmetic and rewrites it to
  [google - chrome - stable], which would break browser detection
  entirely. The pre-commit hook runs shfmt -w on staged files and
  re-stages them, so this corruption would have landed in the commit
  unseen -- this is what caused the previous attempt at this split to be
  reverted. Behaviour-preserving: quoted and unquoted are the same key.

- SCRIPT_DIR is assigned in the entry script rather than inside
  inject_default_config. Left in the lib, ${BASH_SOURCE[0]} resolves to
  lib/, so the defaults JSON lookup would miss and default blocking
  rules would silently never be seeded.

- install_firefox_policy takes ff_found as a parameter instead of
  reading it as a cross-file global, fixing a genuine SC2154 without a
  suppression.

- Added require_cmd unzip. It was used bare under set -Eeuo pipefail, so
  a missing unzip would abort after the version dir was populated but
  before the 'current' symlink was made, leaving a half-installed tree.

verify_shell_split.sh reports additions only -- no -/+ pair -- so
get_latest_tag and replace_browser_in_place moved byte-identically.
--help and unrecognized-flag output are byte-identical to the baseline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 15:32:48 +02:00
47478791d8 hosts/install: split into lib/ under the 250-line cap
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
913 lines down to 150. The ten functions were only 202 lines; the other 712
were a single top-level block, now eight phase functions across six libs. As
in the previous split the bodies were wrapped WITHOUT re-indenting, because
five heredocs use quoted terminators that must stay at column 0 (shfmt
indented them afterwards, which is safe).

The ~320-line custom-blocklist heredoc became custom_entries.hosts. It was
321 of the 370 lines of hosts_write.sh and cannot be split, so that lib could
not otherwise fit the cap; a hosts-format data file is also far easier to
audit and diff than a quoted heredoc.

That move broke a guard, which is the important part of this commit:

  extract_custom_entries_from_script sliced the blocklist out of the script
  between "# Custom blocking entries" and the heredoc terminator. With the
  entries in a data file there is no terminator, so it would have extracted
  NOTHING -- and check_custom_entries_protection, the guard whose entire job
  is refusing to drop blocked domains, would have compared against an empty
  list and passed everything. It now reads the data file, and the extracted
  set is identical: 254 domains, diffed.

A second, pre-existing bug surfaced while testing:

  extract_unblock_entries_from_script matched its OWN marker lines.
  PROTECTED_UNBLOCK_LIST_START and _END are word characters, so the
  '^# [a-zA-Z0-9._-]+$' pattern pulled both into the whitelist as if they
  were domains. Both are in this machine's live state file right now.
  Renaming either marker would have read as the whitelist growing and blocked
  the install outright.

There is no dry-run or status mode, so this file cannot be proven by running
it: an aborted run leaves /etc/hosts mutable with the guard's bind mount down.
The proof is textual instead. Every one of the 8 wrapped phase bodies is
byte-identical to its original line range; a line-multiset check found 0 lines
lost; and an indentation-insensitive comparison against the pre-split file
leaves exactly 8 absent lines, each an intentional edit (the rewritten
extractor, three `readlink -f "$0"` made overridable, the /etc/hosts fallback
made overridable, and the heredoc markers). /etc/hosts is byte-unchanged
across the whole session, and `--help` output is identical.

36 assertions. Both anti-tamper guards at 100% line coverage (75/75 and
49/49), covering both directions of each: removing a blocked domain is
refused and adding one allowed; growing the whitelist is refused and shrinking
it allowed. A same-count swap is refused too, which a count-only check would
have let through.

The install phases themselves are deliberately untested -- running them would
take down the protection they install. That is weaker evidence than the two
previous splits had, and it is inherent to this file rather than a gap.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV
2026-08-21 14:45:04 +02:00
241652cade generate_study_materials: split into lib/ under the 250-line cap
1017 lines down to 148. Six libs: the per-language doc-URL builders (split in
two only for the cap), the lookup/dispatch layer, and one per generation phase.

The hard part was not the ten functions but the 335-line top-level block,
which extract_shell_functions.py cannot move because it only relocates
functions. It is now generate_doc_links / generate_anki_cards /
generate_llm_prompt, wrapped WITHOUT re-indenting the bodies: five heredocs
use quoted terminators, which must stay at column 0.

Proof the split is behaviour-preserving: a golden fixture captured before any
edit, re-checked after every step. All three outputs -- anki_cards.txt,
documentation_links.md, llm_anki_prompt.md -- stay byte-identical by sha256sum,
and stdout matches too.

Four latent defects surfaced while writing the tests, all fixed:

  python_doc_url required a $2 it never reads, and get_doc_url has always
  called it with one argument. That only worked because the entry script runs
  `set -e` without `-u`, so "$2" silently expanded to empty; any caller with
  `set -u` aborted. Defaulted, along with three more positionals documented as
  optional but declared required.

  The two imports loops hardcoded `head -20` while the other seven honoured
  TOP_N, so --top was silently ignored for imports.

  Only one of nine term loops skipped comment lines, so a '#' line in an
  analysis file became a flashcard whose front was a comment.

  Introduced and fixed within this change: the comment filter has to run
  BEFORE `head -n` or comment lines count against the limit, and it needs
  `|| true`, since grep exits 1 when it filters every line and the callers run
  under `set -e`.

The golden fixture cannot show the last three, because it has no comments and
fewer than 20 imports -- which is exactly why they survived. A second fixture
with 25 imports and comment lines, at --top 3, emitted 20 imports and two
comment cards before, and 3 imports with zero leakage after.

166 assertions across 4 test files. 100% line coverage on study_doc_urls.sh
(38/38), study_doc_urls_more.sh (30/30), study_doc_lookup.sh (49/49) and
study_gen_llm.sh (54/54). study_gen_docs.sh 93.10% and study_gen_anki.sh
96.15%, where every remaining line is a `} >>"$FILE"` closing a brace group
whose output a passing test asserts -- the same kcov attribution artifact
recorded for services_hosts.sh.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV
2026-08-21 14:32:08 +02:00
08f3c764b2 check_and_enable_services: a warning must never mask an error
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Eight checks assigned status="warning" unconditionally, so a check running
later in the same function overwrote the status="error" an earlier one had
set. The five other sites in this family already guard the assignment with
`if [[ $status != "error" ]]`, so this was an inconsistency, not a design.

Only half of it was load-bearing, and the two halves fail differently:

  services_units.sh (4 sites) reports through report_and_fix, which repairs
  ONLY on "error". A downgrade there meant no repair ran at all. The is-active
  check is the LAST status assignment in each of the three functions, so it
  could mask any error above it -- in check_periodic_systems, three separate
  is-enabled checks. A timer that was both disabled and inactive, which is the
  ordinary shape of a broken timer, was reported and then left broken.

  services_hosts.sh (4 sites) gates its own repair on `status != "ok"`, so the
  repair still ran. There the bug printed a real error as a yellow [!] warning
  and understated the summary row.

Demonstrated rather than argued, against a simulated broken machine (timer
disabled AND inactive, unit files present):

  without the guards:  status=warning, repair ran 0 times
  with the guards:     status=error,   repair ran 1 time

The seven assertions that previously pinned the broken behaviour now assert
the correct behaviour; they were rewritten first and failed, then the guards
made them pass. Two further cases cover the masking shape directly: a broken
guard-lib instance must still be repaired when a later check warns.

`--status --dry-run` output is still byte-identical to rev 6691cb26, because
this machine has no disabled timer and no missing pacman hook -- the buggy
path is never taken here, which is precisely why it went unnoticed. The A/B
above is the evidence the live diff cannot provide.

222 assertions across 10 files. services_units.sh holds 100% coverage;
services_hosts.sh rises from 86.05% to 93.02%. test_services_hosts.sh needed
splitting twice to stay under the 250-line cap (hosts_fix, resolved), and
stage_hosts_ok moved into the harness so both new files can use it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV
2026-08-21 14:16:01 +02:00
0a69d18390 check_and_enable_services: 100% coverage via a redirectable path prefix
Follow-up to 9085a4e5, which recorded services_units.sh at 84.51% because
its remaining branches sit behind [[ -f /etc/systemd/system/*.service ]] --
absolute root-owned paths that exist on this machine, so the else-branch
never runs and no PATH shim can redirect it.

All 87 such paths across the seven check libs now go through $SYSROOT, read
from SERVICES_ROOT. The prefix is deliberately NOT defaulted. Three commands
in hosts_fix_resolved run outside `run` and so are not stopped by --dry-run
(chattr -i on resolved.conf.d, find -delete over its *.conf, and an append to
resolved.conf); with an empty default, a test that forgot to set SERVICES_ROOT
would really delete this machine's resolved.conf drop-ins. Unset is therefore
a hard error and only an explicitly empty value means the real filesystem.

The harness now owns PATH outright: $FAKE_BIN for shims and staged fake apps,
$REAL_BIN for a curated set of symlinks to genuine coreutils. The machine's
/usr/bin is deliberately absent -- a really-installed discord and chromium
otherwise make the "no target apps present" branches untestable, which is
exactly what happened until these tests caught it.

Coverage, measured against run_all.sh because two test files jointly cover
services_common.sh after the 250-cap split: common 55/55, report 27/27,
units 72/72 (was 84.51%), wrappers 100/100, apps 66/66, browser 79/79 --
seven libs at 100%. services_hosts.sh reads 86.05% and services_hosts_fix.sh
97.30%, and those 14 lines are a kcov attribution artifact rather than a gap.
Falsified rather than assumed: the suite prints "51 passed, 0 failed" when run
under kcov, so no group is skipped, and every supposedly-uncovered line is an
issues+=("...") whose string can only reach stdout through the print loop
below it -- with a passing assertion matching that exact string.

Two falsification checks, both re-run after the rewrite: `--status --dry-run`
as root stays byte-identical to rev 6691cb26 modulo the Date: line, and the
sha256sums of /etc/hosts, /etc/nsswitch.conf and /etc/systemd/resolved.conf
plus the contents of resolved.conf.d are unchanged across a full suite run.
The second one is what makes a missed prefix site detectable instead of a
matter of inspection: the repair paths really do write.

220 assertions across 8 test files. Also fixes a bug in the harness's own
lsattr shim, where printf with a leading-dash format string parsed the dashes
as options and silently produced nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV
2026-08-21 14:08:11 +02:00
9085a4e508 check_and_enable_services: split into lib/ under the 250-line cap
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
1225 lines down to 202, with the 21 functions moved into eight sourced
libraries grouped by subject. The entry script now holds only the
configuration, the argument parsing and the order the checks run in.

check_hosts was the one function that could not move verbatim: at 260 lines
it would have stayed over the cap on its own, so its repair half is now
hosts_fix_nsswitch / hosts_fix_resolved / hosts_repair_all in
services_hosts_fix.sh. Repair order is preserved (resolver config, then the
hosts file, then the guard-lib migration), and hosts_repair_all returns
non-zero under --dry-run so a dry run can no longer promote a status to "ok"
on the strength of repairs that never happened.

SERVICE_STATUS was a write-only global across five libs, which is a real
SC2034 rather than noise: the pre-commit hook runs shellcheck with no -x and
genuinely cannot see the one reader. It now goes through set_service_status /
get_service_status, which also rejects a duplicate write — each check owns
exactly one summary row, so a second write means two checks are fighting over
it. No suppression was added anywhere.

Proof the behaviour is unchanged: `--status --dry-run` run as root against
this machine is byte-identical to the same command from a worktree at the
pre-split revision, modulo the Date: line. verify_shell_split.sh reports
check_hosts as the single expected difference; the other 20 functions hash
identically. 72 assertions across three test files pass, with 100% line
coverage on services_common.sh (54/54) and services_report.sh (27/27).

services_units.sh measures 84.51% (60/71). The 11 uncovered lines are the
else-branches of [[ -f /etc/systemd/system/*.service ]] and
[[ -f /usr/local/bin/*.sh ]] probes — absolute root-owned paths that exist on
this machine and that no PATH shim can redirect. Reaching them needs a
root-prefix rewrite of live enforcement logic, which is not a line-count fix
and would forfeit the verbatim-move proof for everything else.

Found while testing, deliberately NOT fixed here: in all three unit checks
the is-active branch assigns status="warning" unconditionally, overwriting
the status="error" the is-enabled branch set just above it. A timer that is
both disabled and inactive therefore never gets repaired, because
report_and_fix only repairs on "error". It is identical in the pre-split
file, so this split neither caused nor hides it. The tests pin the current
behaviour and name it; fixing it changes enforcement and earns its own commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV
2026-08-21 13:47:05 +02:00
7d7b3c71f9 focus_owner: split EnforcementRunner.kt under the 250-line cap
Extracts four cohesive concerns into standalone files:
LocationAcquisition (fix acquisition), HomeLocationStore (home
read/write), PolicyPinning (VPN/DNS pin + visibility sweep),
SweepablePackages (installed-package enumeration). Public API
unchanged -- EnforcementRunner.best(...) deliberately stays put since
EnforcementRunnerBestTest.kt calls it directly; every other public
method is a one-line delegating wrapper.

Verified with gradlew compileDebugKotlin + test --rerun-tasks
(BUILD SUCCESSFUL, 40 tests / 0 failures, including the 6
EnforcementRunnerBestTest cases unmodified).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-21 13:17:59 +02:00
6efb4baf04 focus_owner: split DevicePolicyBridge.kt under the 250-line cap
Some checks failed
Pre-commit checks / pre-commit (push) Has been cancelled
Shell tests / shell-tests (push) Has been cancelled
Shell tests / arch-tests (push) Has been cancelled
Kotlin has no part-of like Dart, so this extracts three cohesive
method groups into standalone internal helper classes held as
private delegates: DevicePolicyVpnDns (VPN/Private DNS pinning),
DevicePolicyLocation (location permissions), DevicePolicyUninstallGuard
(uninstall blocking). DevicePolicyBridge's public API is unchanged --
every method MainActivity.kt/EnforcementService.kt call directly
still exists as a one-line delegating wrapper.

Verified with gradlew compileDebugKotlin + test --rerun-tasks
(BUILD SUCCESSFUL, 40 tests / 0 failures).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-18 23:56:21 +02:00
327d48badd Archive legacy hosts-guard subsystem: superseded by guard-lib
setup_hosts_guard.sh was NEXT_SESSION_PROMPT.md's next split target, but
live-machine inspection showed it (and 16 supporting files - enforce-*.sh,
6 systemd units, install_pacman_hooks.sh, pacman-hooks/, psychological/,
both READMEs) is fully retired in favor of guard-lib, migrated 2026-07-04.
User confirmed archive over split, then confirmed the larger 17-file scope
once the directory was surveyed. Full history preserved via git filter-repo,
live at github.com/kuhyx/testsAndMisc-archive/tree/main/linux_configuration/scripts/periodic_background/hosts/guard.
plugins/{nsswitch,resolved}-plugin.sh stay - they're guard-lib's live
plugins, referenced by absolute path from /etc/guard-lib/targets/*.conf.

check_and_enable_services.sh's check_hosts() rewritten to check guard-lib
state (guardctl file-guard status) instead of the retired units/scripts it
used to check and self-repair - verified live, now reports the hosts
service ok against the actual migrated machine instead of a false error.
copilot-instructions.md and one install.sh comment updated to match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-18 22:49:54 +02:00
015eb48714 diagnose_pacman_hook_stall.sh: split 493 -> 249 + 6 libs, 100% coverage
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Confirmed live (not dead code) via the dead-code check-in before any code
was touched -- the last two candidates this pattern was tried on were both
archived instead. Kept run_one/cleanup/log_size/main together in the entry
script: a prior session (2527fb3) already proved splitting run_one out
produces a real SC2153/SC2034 pair since PACMAN_PID/LAST_ELAPSED cross the
EXIT trap, not a suppressible false positive.

Fixed a real bug found while writing tests: print_summary's inline awk had
a dead `NR == 0` guard (printf on an empty array still emits one blank
line), reachable via `-n 0`. Refactored mem_available_mb, descendant_pids/
kill_tree, capture_stall's two output blocks, and print_summary's median
calc after confirming empirically (bash -x tracing) that kcov's
xtrace-based coverage can never see a `done <redirect>`, `} >redirect`, or
inline-awk line -- user chose to fix the code over the coverage tool.
descendant_pids specifically needed an explicit-fd process substitution
(not a pipe) to keep its array mutation in the current shell.

Made PACMAN_BIN/PACMAN_LOG/CACHE_DIR/PACMAN_LOCK/LOAD_FLOOR_MB/
LOAD_MIN_FREE_MB env-overridable for testing. Caught and fixed, before it
ran unbounded, a --with-load test that would have dd'd 13-20+ GB into
/dev/shm using this box's real available memory.

56 lib-level + 13 entry-level PATH-shim/subprocess assertions, 0 failed.
The entry test itself grew past 250 lines while adding coverage and had to
be split into a shared harness plus two test files, mid-session.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-18 22:33:57 +02:00