Commit Graph

276 Commits

Author SHA1 Message Date
f15f00f3fe Re-point paths for the ~ reorganisation (home-tidy)
Also bumps the 16 meta/requirements pins the freshness gate flagged
(anthropic 1.5.0, numpy 2.5.3, pytest-randomly 5, flake8-bugbear 26.9.9,
ruff 0.16.7, pyright 1.1.414, selenium 4.49.0, hypothesis 6.168.0, lxml
6.1.3, pipdeptree 4.2.5, the two types-* stubs) and Flutter 3.47.3 in
meta/.fvmrc. One test still expected the pre-move ~/mcp-servers path.

The pre-commit hook fed .zsh files to shfmt and shellcheck, neither of
which parses zsh (SC2148/SC1071), so no zsh file could be committed since
the hook was tightened on 2026-08-22; those now get `zsh -n` instead. The
hook's own shfmt pass also staged its reformat of 8 shell files.

CI_GREEN_SKIP because main is red on exactly the dependency-freshness
finding this commit fixes, which is the documented use of that flag.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AwtowrowDXx6ccmWVPkCaF
2026-09-11 20:50:19 +02:00
0cea82cab5 feat: the nftables drift check now runs itself at boot
`verify` gated on the two invariants whose breach cut every bridged
container off the internet on 2026-08-29 and again after the 2026-08-30
reboot -- but nothing invoked it, so it reported drift only when a human
thought to ask, and the outage it exists for is exactly the one nobody
thinks to ask about.

nftables-drift.service now runs it after nftables.service, docker.service
and network-online.target at every boot, plus a daily Persistent timer. A
breach is a failed unit in `systemctl --failed`, not a log line: a
self-check that detects a failure has to gate, not warn.

Proven in ~/utils/vmbox across two real reboots -- drifted /etc leaves the
unit failed with the diff and the missing docker0 accept in its journal,
matching /etc leaves it active with no false positive -- then installed and
run green on the host. Not yet observed across a host reboot.

Also bumps the gatelock pin to v0.7.3, which is what turned main's
dependency-freshness job red; CI_GREEN_SKIP is set because this commit is
that fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015d48t4qsQLBJnDwDeo2Nom
2026-08-30 15:49:12 +02:00
89a44b58b2 feat: gate on nftables drift so the docker cut-off cannot come back silently
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
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 forward-chain fix from 334c0cd9 only ever reached the live ruleset by
hand; /etc/nftables.conf kept the bare default-drop chain, so the next
reboot cut every bridged container off again and the Signal bot went deaf
a second time. Nothing compared generated against persisted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 14:56:24 +02:00
2713ae9d66 Put the screen-locker status back on the i3 bar
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
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 GTK tray icon was the only status surface, and it had been commented out
of ~/.config/i3/config since 2026-08-21. With nothing in the bar, the locker
miscounted the week for a fortnight and the machine never said so.

Shows "<count>/<minimum> · <gaming budget>", read from the local status API
(screen-locker-web.service on 127.0.0.1:8770); clicking opens the full UI.

Three things it deliberately will not do quietly:
- a dead status server renders "status server down" in red, or the last known
  value marked (stale) in orange -- never a blank block. That server is what
  the gaming budget reads, so its absence costs two hours;
- a disarmed or unscheduled timer overrides the compliance colour entirely
  and shows "DISARMED" in red. A locker that cannot fire is the worse
  problem and the one that hides;
- clicking only opens the UI. There is no bar action that can clear a lock.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fcwu78wk17FWMHbbfbyicc
2026-08-29 22:55:24 +02:00
334c0cd904 fix: stop the wireguard firewall from silently cutting every container off
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
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
`write_nftables_ruleset` emitted a forward chain with `policy drop` and no
rules at all. That chain runs at the same netfilter hook as Docker's own
DOCKER-USER/DOCKER-FORWARD chains, so it vetoed them: every bridged
container lost outbound networking while the host itself stayed online, so
nothing looked broken.

It cost the Signal bot three silent hours today. signal-cli lost its
connection to Signal at 15:32:50 and looped on reconnect for 2.5h, while
the bot kept answering /healthz with 200 -- `/v1/groups` still served from
local state, so every surface said healthy.

`flush ruleset` also deletes Docker's own tables, and dockerd only rebuilds
them on its next network event, so restore_docker_rules() now restarts
docker right after a successful apply.

Verified in vmbox (Arch guest, docker installed, alpine probe):
BASELINE=reachable -> OLD=blocked -> NEW=reachable, sshd active throughout.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbjfWdAyHbCubac7wJKJZW
2026-08-29 18:34:25 +02:00
ce33c935e4 Run the website's post editor as a loopback unit, no Caddy route
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
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
2026-08-29 17:50:03 +02:00
61abbf8cf9 Force-recreate the website container so config and dist changes apply
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
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
Two failure modes compose cannot see, both of which report a healthy
container while it serves 404s:

  1. The Caddyfile is a bind mount, so its contents are not in the config
     hash compose compares. Editing it changes nothing observable, compose
     says Running, and the new directives never load.
  2. A bind mount resolves once, at start. Replacing dist/ rather than
     emptying it leaves the container on the old unlinked inode: /srv reads
     empty and every request 404s.

Hit both at once on a real deploy today -- the container was Up 6 hours,
Caddy logs clean, /srv empty, curl 404. Reproduced by rm -rf dist (inode
84695030 -> 84695031) and fixed by the patched function in one run.

wait_for_website also stops using curl -sf, which collapses every non-2xx
into the same "did not become reachable". It now reports the status it saw,
because unreachable and answering-404 are different bugs and the message
named only the first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A7BaCSV8FEtcNgVDVsRAJj
2026-08-29 16:35:30 +02:00
516acc243f Serve prerendered blog posts, not the SPA shell, from the website container
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
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 personal-website repo now prerenders each post to
dist/blog/<slug>/index.html. Caddy try_files does not resolve a directory
to its index on its own, so with the single-term directive every post URL
fell through to the SPA shell: right content once React booted, wrong
<title>, and no link preview. Verified against caddy:2.8 in Docker both
ways before and after.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A7BaCSV8FEtcNgVDVsRAJj
2026-08-29 16:19:28 +02:00
adf2e402df Grade against 3.14 too, not just run on it
`requires-python` and the CI pin already said 3.14, but ruff, mypy and pylint
were still grading `meta/` against 3.10 -- three tools judging an interpreter
nothing runs any more, and ruff withholding the pyupgrade fixes that only
become safe at the newer target. `pin_ci_python.py` rewrites these three knobs.

What the stale target was hiding: `datetime.timezone.utc` where `datetime.UTC`
is now correct (UP017), and ruff's stable formatter applying PEP 758 --
`except (A, B):` becomes `except A, B:`. That makes the source 3.14-only at the
syntax level, which is what `requires-python = ">=3.14"` already promised; kuhy
chose it deliberately over holding ruff a release back (2026-08-29).

Also drops a comment in `wsg_grabber/models.py` that the bump makes false: it
explained why the file used ``str, Enum`` instead of ``StrEnum`` under mypy at
3.10, sitting directly above a ``class Verdict(StrEnum)`` that had already
stopped obeying it.

ruff, ruff-format, mypy --strict and pylint all clean at the new target.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfnJmxxsZ534AKk16rC2Gr
2026-08-29 11:50:39 +02:00
924630c029 Run SyncYomi from the local fork build and stop clobbering the Caddy edge
Some checks are pending
md-naming / markdown naming convention (push) Waiting to run
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
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 14:44:40 +02:00
49d82511c3 Adopt the four markdown namespaces and gate them
Renames this repo's markdown onto README.md / CLAUDE*.md / DOCS*.md /
TODO*.md so a future session can tell a live task from a finished
record without reading the file to find out.

TODO files carry the literal "REMOVE ME AFTER FINISH" and are deleted
when the work lands. Enforced by the md-naming pre-commit hook and CI
workflow, both delegating to the shared gate in kuhyx/utils so this
repo's idea of a valid name cannot drift from every other repo's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A47aX5DJwmA3QFn3pkEWi7
2026-08-24 15:55:46 +02:00
782a6320af Let --rollback work on a freshly installed machine
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 previous commit made hosts-blocker's install.sh register the guard-lib
instances itself, which is what finally gives a fresh machine real hosts
enforcement. It also created a class of machine that --rollback refused to
touch: the state dir it demanded, /var/lib/guard-lib-migration, is written by
the one-shot migration, and a machine that never had a legacy layer has no
such state. So --rollback exited 1 while the instances it exists to remove
were live -- every freshly installed machine had no escape hatch at all.

Rollback now proceeds whenever a guard-lib instance is registered, says
plainly that there is no legacy layer to restore, and still refuses when
there is genuinely nothing to roll back.

The second half is worse and was only visible by running it. The existing
re-assertion of chattr +i works by starting hosts-guard.service and
hosts-bind-mount.service, neither of which exists on a fresh machine, so
every systemctl cat skipped and the rollback finished with /etc/hosts holding
no immutable flag and no bind mount -- less protected than before it ran.
It now re-asserts immutability directly when no legacy unit did it.

Verified in a pristine vmbox guest whose guards came from
install_core_system.sh --all: dry-run and real rollback both exit 0, the
three instances are uninstalled, shutdown-schedule (a different feature) is
untouched, and /etc/hosts keeps ----i----------------- and all 178410 lines.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-24 12:57:24 +02:00
4c426eae0e Make install_core_system.sh work on a fresh machine
Some checks failed
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
Python tests / test (push) Has been cancelled
Two of the seven modules invoked python_pkg/ paths that stopped existing when
screen_locker and steam_backlog_enforcer were extracted into their own repos,
so the first thing the installer did on a fresh machine was fail. Nothing
installed guard-lib either, which meant the CORE "Midnight shutdown timer"
module always died with "guardctl not found on PATH".

Every module now resolves through install_module_from_repo, and
require_extracted_repo clones a missing sibling instead of only printing
instructions -- otherwise "clone it yourself" is not actionable on the bare
machine this script exists to set up. The clone drops privileges to the
invoking human via sudo -u, because an installer that re-execs under sudo
would otherwise leave a root-owned checkout in the user's home; a sandbox
cannot catch that, since the guest is root throughout. A clone that fails is
still a loud, fatal module failure, never a silent skip.

guard-lib is addressed as the "utils" repo: it is a subdirectory of
github.com/kuhyx/utils, so asking for "guard-lib" would resolve to
~/guard-lib and derive a clone URL that does not exist. It installs first,
before the modules that need guardctl.

Separately, the security hardening suite's screen_locker section hardcoded
$HOME/testsAndMisc/python_pkg/screen_locker/screen_lock.py. That path went
away with the extraction, so all five checks silently SKIPPED -- reporting a
clean run for tests that never executed, which is the exact failure mode
lib/extracted_repos.sh was written to prevent. It resolves the repo properly
now ($HOME is also wrong under sudo), and the assertions are rewritten against
the current implementation: the four symbols they grepped for no longer exist
anywhere, having been replaced by SPORT_CHOICES, SPORT_TABLE_TENNIS,
_validate_table_tennis and MANUAL_WORKOUT_REFLECTION_MIN_CHARS.

On this host that takes the suite from 18 passed / 1 failed / 5 skipped to
23 passed / 0 skipped. The task file's "18 passed / 6 skipped out of 17"
target was stale -- the suite is 24 checks now -- so it could not be matched
literally.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-24 12:45:21 +02:00
6b828d29aa Extract digital-wellbeing; the directory-depth cap now enforces
The last unit over the cap is now github.com/kuhyx/digital-wellbeing, cloned
at ~/digital-wellbeing, and `--warn` is gone from the directory-depth-cap
hook. `check_directory_depth.sh --all` exits 0 with zero violations, down from
46 at the start of the session. That was the point of the campaign.

History was filtered across all three paths the unit has lived at (63
commits); the current path alone holds 2.

The six suites that test this code lived in linux_configuration/tests/ rather
than beside it, so they were filtered separately (24 commits) and merged in as
a second root. A path filter of the unit alone would have left them here,
testing code that had left the repo -- and their names would still have been
in shell-tests.yml's hardcoded arrays, failing CI on missing files.

linux_configuration/lib/common.sh is vendored into vendor/ rather than trimmed
to the four functions actually used: set_actual_user_vars calls two of the
others, so a trim breaks silently. It is in vendor/ and not lib/ because
test_music_parallelism.sh copies the unit's own lib/ into a scratch worktree
while stubbing the shared lib; in one directory the real common.sh overwrote
the stub, and the suite silently measured the wrong library.

Running the callers caught what shellcheck and grep both missed:
check_and_enable_services.sh died with "DW_REPO: unbound variable" because the
definition sat below its first use. It now runs clean and reports ok for every
extracted-unit check.

Two long-standing live defects fixed as a consequence. The pacman-wrapper
drift manifest embeds ABSOLUTE paths and had named deleted scripts/ paths
since the flatten, so the check reported a permanent false "stale or
tampered"; it now verifies, with unchanged hashes proving the extracted bytes
are identical. And the last two stale live references are gone: 8 -> 0.

The two pre-commit excludes for setup_midnight_shutdown.sh are dropped rather
than moved. lsattr shows nothing in the unit is chattr +i any more, and the
fixer hooks leave the file alone when allowed to run.

Removing 80 shell files also shrank the jscpd denominator enough that the
virtualenv/tqdm clones inside the GENERATED .ci-mirror-venv/ tipped the repo
to 2.09% and blocked this commit, which adds no duplication. That directory is
the same vendored third-party shell the ignore list already excludes as
.venv/, just under another name; jscpd walks the working tree rather than the
index, so being untracked did not spare it. Excluded by name: 1.74%.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-23 20:57:51 +02:00
309f4c8d42 Repair live self-repair broken by the flatten and extractions
Five live mechanisms were silently pointing at paths that no longer exist.
None of them announced it; each degraded to a log line or a missing file.

- hosts-file-monitor.service is ACTIVE and watches /etc/hosts for tampering,
  but its embedded installer path was the pre-flatten scripts/ one. It logged
  "Hosts install script not found" 37 times: it detected tampering it could
  not repair.
- dns-blocklist-refresh.service (enabled, next run 00:01) named a deleted
  single_use/features/ path, as did media-organizer.service under utils/.
- setup_periodic_system.sh read all TEN of its templates from
  periodic_background/system-maintenance/, which has held nothing but
  untracked .pyc and .coverage residue since system-maintenance was
  extracted. Every install of a service, timer or script was reading a file
  that was not there.
- /etc/sudoers.d/hosts-install-no-passwd granted NOPASSWD on the old hosts
  installer path.

Running the fixed dns unit then caught a bug the static checks missed: under
systemd there is no SUDO_USER and `id -un` is root, so the new resolver
returned /root/hosts-blocker and the service died with "Feed generator not
found". extracted_repo_home() now handles the bare-root case by falling back
to the owner of the lib file itself. Verified across all four invocation
modes: as the user, via sudo, as root with SUDO_USER unset, and with an
explicit *_DIR override.

The service now reports "Feed refreshed and dnsmasq reloaded (189038 lines)".
/etc/hosts is untouched throughout -- still 176888 lines, still chattr +i,
still bind-mounted by its guard.

Live artifacts were patched via validated temp files, never in-place: the
sudoers drop-in was checked with `visudo -cf` before install, and every
original is backed up under /var/tmp/*.pre-{extract,fix}.

Stale live references: 8 -> 2. The 2 that remain name digital_wellbeing
paths and can only be fixed when that unit is extracted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-23 20:39:59 +02:00
f72455662c Extract the hosts blocker into its own repo
linux_configuration/periodic_background/hosts/ is now
github.com/kuhyx/hosts-blocker, cloned at ~/hosts-blocker. Clears 5 of the 46
directory-depth violations; the remaining 41 are all digital_wellbeing, so
--warn stays on the depth hook until that unit moves too.

History was filtered across ALL FOUR paths the unit has lived at (hosts/,
linux_configuration/hosts/, linux_configuration/scripts/periodic_background/
hosts/, and the current one). Filtering on the current path alone would have
preserved 1 commit out of 51 -- the flatten moved it recently, so a naive
single --path silently truncates the history to the flatten commit and still
looks fine in `git log | head`.

No source line changed: all 15 files are byte-identical to the copies removed
here. install.sh already resolved its lib/ via BASH_SOURCE.

The six callers now resolve the checkout through a new shared helper,
linux_configuration/lib/extracted_repos.sh, which reads the invoking user's
home from SUDO_USER's passwd entry rather than $HOME -- several of these
scripts re-exec under sudo, where $HOME is /root. A missing checkout fails
loudly with a clone instruction; it is never a silent skip, which is the
failure mode that left screen-locker's self-repair dead for five months after
the 2026-05-15 reorg.

The live blocker is untouched: /etc/hosts keeps its chattr +i, and the guards
load their plugins from /usr/local/share/guard-lib-plugins/ (byte-identical by
md5), not from a repo path.

Verified: 25 tests pass from ~/hosts-blocker; shellcheck clean on the helper
and all six callers; sudo resolution prints /home/kuhy/hosts-blocker;
test_setup_dns_blocker.sh passes; report_live_state.sh --check is byte-identical
before and after, so no new stale references.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-23 20:30:18 +02:00
ac8e098c03 Point the diagnostics bats suite at the flattened path
Shell tests went red on my last push. test_network_diagnostics.bats sourced
its scripts through $REPO_DIR/scripts/diagnostics, a level the flatten
removed, so setup() failed and all 30 tests errored.

Latent rather than new: the previous run was green only because ci-mirror
aborted before the bats stage. Fixing the concurrency OOM let the gate reach
this suite for the first time since the flatten.

Grepped the tree for the removed scripts/ and single_use/ levels; this was
the last one in tracked code.

Also rewrites the two handoff docs around the units that remain.

CI_GREEN_SKIP=1 because this commit is the fix for the red baseline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-23 20:08:40 +02:00
9aeeb005dd Derive the lib prefix in the music_parallelism drift guard
The pre-push gate was failing on a layout that is actually correct. The
script sources ../../lib/common.sh, which is right for where it sits after
the flattening; the test's guard still hardcoded ../lib/common.sh from
before the move, so it reported drift against itself.

The prefix is now read out of the script under test, so the two cannot
desynchronise the next time the tree moves. Verified non-vacuous: rewriting
the script to ../../../lib/common.sh in a throwaway worktree still fails
the guard, and it now names the real prefix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-23 19:53:17 +02:00
7cb23e8a81 Prettier-normalize two docs the pre-push hook was already failing on
Both were non-conforming before this session and block every push, not
just this one, since the hook runs over all files. The diff is entirely
table-separator widths and emphasis markers; no prose changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-23 19:42:18 +02:00
0fb3e3eee2 Cap extensionless scripts, and split the one that was hiding
check_file_length.sh required a dot in the filename, so every
extensionless executable was silently uncapped. is_source_file() now
reads the first two bytes and accepts a shebang, which catches real
scripts while still skipping data files like i3/config.

That immediately surfaced boot_recovery/boot-repair at 756 lines --
three times the largest capped shell file in the repo, uncapped purely
because its name has no dot in it. Split along its own section markers
into lib/{discovery,repair_fs,repair_kernel,modes}.sh, leaving the entry
script at 218.

The report block stays inline on purpose. It is top-level code with exit
calls rather than function definitions, and sourcing it changed control
flow: 30 of 42 tests failed. That is recorded in the lib headers so the
next reader does not retry it.

No vmbox pass for this one, deliberately. boot-repair is UEFI/ESP tooling
and the guest is BIOS/GRUB, so a sandbox green would be actively
misleading -- the vmbox skill says so itself. The evidence is the fixture
suite, which drives the real script against synthetic root trees: 42
passed before, 42 passed after.

verify_shell_split.sh reports 31 of 32 functions byte-identical. The one
difference is clean_shadow_files, where quoting $ESP_MOUNTPOINT fixed a
genuine SC2086 that only became visible once the function left the
monolith -- shellcheck could no longer see the readonly declaration and
stopped assuming the expansion was safe.

lib/tests/run_all.sh delegates to that same suite rather than standing up
a second, weaker harness: these libraries need the entry script's strict
mode and globals, so testing them in isolation would test a fiction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-23 19:13:40 +02:00
08aac8d7d1 Flatten i3-configuration and misc/testsAndMisc-bash
i3 config was filed under periodic_background, which it never was. It is
now linux_configuration/i3/ (config + installer) and
linux_configuration/i3blocks/ (the status scripts), both at depth 2.

misc/testsAndMisc-bash/ was a directory named after the repo it already
lived in, so its contents moved up to misc/. Its stray .gitignore came
along, keeping the media rules working from the new location; its
.vscode/tasks.json did not, because it invoked Bash/transcribe.sh, a path
this repo dropped long ago.

Most of this commit is the references the previous flatten missed. Its
regex skipped anything with a variable prefix, which left 16 files --
including check_and_enable_services.sh and install_core_system.sh, the
orchestrators -- still naming $CONFIG_DIR/scripts/periodic_background/...
It had also rewritten three i3 paths into scripts/i3blocks/, a directory
that has never existed; the i3blocks suites caught that by failing.

13 more symlinks broke on the i3 move. Repointed, and cpu_monitor/volume/
disk were run again to confirm the bar still reports.

Depth 139 -> 104. Roughly 47 of the remainder sit in digital_wellbeing
and hosts, still deferred. The rest encode real structure -- a C project,
Python package directories, artgate's routes/styles, systemd unit dirs --
so flattening them further would destroy information rather than
taxonomy.

Verified: 1798 tests at 100%, both i3blocks suites pass, the shell
coverage gate is clean, and the relocated media rules still ignore what
they should while test_fw.srt stays tracked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-23 19:02:18 +02:00
4f2b0ccdf7 Dissolve linux_configuration/scripts/ and single_use/
Two levels of pure taxonomy carrying no information, and a 2-directory
cap leaves no room for them. Categories now sit directly under
linux_configuration/: features, fixes, utils, misc, fresh-install,
installers, diagnostics, gaming, boot_recovery, periodic_background, lib,
meta, tests. A script's own lib/ and tests/ are exempt from the cap, so
those stay put.

Depth violations 248 -> 139; roughly 40 of the remainder are inside
digital_wellbeing and hosts, deferred until the guard-lib repoint.

The moves are the easy half. What breaks is every path that counted
levels, and three classes of it had to be found by resolving paths rather
than reading diffs:

  - 52 relative `source` lines for lib/common.sh and lib/android.sh. The
    correct prefix differs per file: 40 were ../../ and 12 were ../../../,
    so a blanket replace would have fixed one set and broken the other.
  - REPO_ROOT in 7 fixes/lib/tests harnesses and setup_syncyomi_guard.sh,
    each walking up four levels from a directory now two levels shallower.
    This caused a real regression -- the fixes suite went red with
    "log_warn: command not found" -- caught by running it and comparing
    against the pre-move baseline worktree.
  - conftest.py's sys.path entries, which took the pytest gate down with
    14 collection errors, and the `# shellcheck source=` directives, which
    took shellcheck down with SC1091.

16 symlinks in $HOME broke, including the entire i3blocks bar. All
repointed, and cpu_monitor/memory/disk/time/volume were run to confirm
they emit real output rather than merely resolving.

Verified: 1798 tests pass at 100% coverage, 8 of 9 lib/tests suites pass
(the ninth needs the coverage jail, identically at baseline), pre-commit
is clean, and the two untracked secret configs are still ignored after
their gitignore paths changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-23 18:53:30 +02:00
a5209b6878 Extract phone_focus_mode together with focus_policy
github.com/kuhyx/phone-focus-mode

The two halves go as one repo: focus_policy parses phone_focus_mode's
config.sh into the typed policy that focus-owner consumes, and nothing
else references it. 77 commits of history, plus the two Python suites
from linux_configuration/tests/ and the six mutation fixtures from
meta/scripts/fixtures/, all of which named phone_focus_mode paths.

Running the suites -- not reading them -- turned up a real defect the
extraction would otherwise have inherited. config.sh sources the
untracked config_secrets.sh, and ctl_libs_harness.sh never pinned
HOME_LAT/HOME_LON, so the distance assertions were checking against
whatever real coordinates that file happened to hold. The suite passed on
this machine and could not run anywhere else, and a real home location
was load-bearing in a pass/fail condition. The harness now pins a pair
4300 m south of the coordinate its own case seeds, and config.sh sources
the secrets file only when it exists. 136/136 pass with no secrets file
present.

Before making the repo public I scanned the whole history for the known
home coordinates and for any committed config_secrets.sh: zero hits, and
the file was never tracked. The new repo also gained a .gitignore, since
none came across in the filter-repo pass.

test_magisk_service.sh hangs. It hangs identically in the pre-extraction
baseline worktree and appears in no CI list here, so the new repo's CI
skips it with that reason rather than pretending it passes.

check_python_location.sh loses its phone_focus_mode exception now that
the directory is gone; every tracked .py still passes it. The .gitignore
rules for config_secrets.sh and the keystore STAY -- the working copy is
still on this machine and still holds a real location.

Depth 262 -> 248.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-23 18:32:58 +02:00
fe8629e104 Extract android_guardian; heavy_job_lock goes to its installer
github.com/kuhyx/android-guardian, cloned to ~/android-guardian

periodic_background/utils/ held two unrelated things: the Magisk module
and heavy_job_lock.sh. Only the module was a unit, so the directory is
split rather than moved.

heavy_job_lock.sh goes to digital_wellbeing/, NOT to ~/utils. Measured
rather than assumed: of its consumers only install_pacman_wrapper.sh
reads the repo copy, while pacman_wrapper.sh, makepkg_capped.sh and
meta/scripts/run_with_heavy_lock.sh all read the installed
/usr/local/bin copy. A library belongs with whoever installs it, and
exporting it would have added a cross-repo edge for one caller.

Fixed a bug that predates the extraction: update_android_hosts.sh set
GUARDIAN_MODULE_DIR to "$SCRIPT_DIR/android_guardian", a sibling that
never existed at that path -- packaging could not have worked from this
checkout at all. It now reads ${ANDROID_GUARDIAN_DIR:-~/android-guardian}
and require_guardian_module() gates build_module(), the single funnel
every packaging path goes through, so a missing checkout names the clone
command instead of failing inside a cp four helpers deep.

Verified both directions: with no checkout the guard fires with
instructions; with one, the module builds -- 182331 blocked domains, a
1.1 MB zip. The subsequent adb push failed on device-side storage, which
this commit does not touch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-23 18:00:58 +02:00
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
0b6df7b003 Revert the deliberate gate-probe break
Reverts the temporary broken assertion used to prove the pre-push gate
refuses a red shell suite (it did: exit 1).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-23 15:08:46 +02:00
04cc5fa23e TEMP probe: deliberate shell-test break 2026-08-23 15:06:15 +02:00
76b1d74388 Revert the deliberate gate-probe break
Reverts 58b4cef8, a temporary broken assertion used to verify the
pre-push gate refuses a red shell suite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-23 15:03:29 +02:00
58b4cef85a TEMP: break shell test 2026-08-23 14:58:14 +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
9677da2832 Restore nvidia_config.sh's four test seams
The seams were applied, verified and measured, but never reached the
commit: cb1e8c53 shipped the file with its hardcoded /etc/X11,
/etc/profile and /home/$SUDO_USER paths. ci-mirror caught it by running
the suite from a clean checkout, where the tests read the REAL system
files -- the RenderAccel assertion failed against this machine's actual
xorg config, which says "disabled", and the pyroveil clone path came
back as /home/testuser rather than the tmpdir.

The local run passed only because a stale seamed copy was sitting in the
measurement worktree. Re-applied and grepped back immediately, which is
the documented rule for exactly this failure mode.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 13:33:35 +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
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
b27790596b Quarantine the arch_sysctl cases: they broke bt_audio's 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
check_shell_coverage.sh was RED on main. bt_audio.sh is off the
allowlist at 127/127, and commit 7c14cfca -- which added
test_arch_sysctl.sh -- made kcov stop registering it entirely, so the
gate failed on a lib nothing had touched.

Bisected by measuring bt_audio.sh at three commits: a2b23521 127/127,
afe6ac3a 127/127, 7c14cfca "kcov instrumented no lines". Removing that
one test file restores 127/127 with every other new file still in the
suite (arch_cpu, arch_hardware x2, thorium x2), and arch_cpu.sh
re-measures at 57/57 alongside it.

Both plausible causes inside lib_test_core.sh were ruled out by
reverting each on its own in a throwaway worktree: `export out` and the
_t_hide farm cache each left bt_audio broken while the file was
present. Neutering the file's body while keeping the source line does
NOT reproduce it either, so the trigger is cumulative rather than one
construct, and it is not understood.

The file keeps all 23 assertions and still passes when run directly; it
just leaves the test_*.sh glob run_all.sh iterates. That costs nothing
measurable, because arch_sysctl.sh has never measured through the jail
in the first place -- it reports "instrumented no lines" on its own,
like nc_php.sh -- and it stays on the allowlist.

The header records the bisect and what to re-verify before anyone
un-quarantines it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 10:15:11 +02:00
aec2b275c1 boot-repair: refuse shadow-file cleanup on BIOS systems with no ESP
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
clean_shadow_files() assumed UEFI with an ESP mounted at /boot. On a
BIOS/GRUB system with no ESP, /boot is a plain directory on the root
filesystem holding the only kernel the machine has -- and neither existing
guard catches it: findmnt /boot is empty (GUARD 1 passes) and there is no
EFI/ or loader/ (GUARD 2 passes). boot-repair therefore read the real
vmlinuz-linux and initramfs-linux.img as "orphaned kernel file(s)
shadowing the ESP" and deleted them, leaving the machine unbootable.

Measured 2026-08-22 in a vmbox BIOS/GRUB sandbox, not theorised: after
sudo boot-repair the guest never booted again.

GUARD 3: if /sys/firmware/efi does not exist the system booted via BIOS,
so there is no ESP and nothing in /boot can be a shadow. warn + return 1,
matching the existing guards' tone and exit convention. Overridable with
the existing --esp DEVICE -- naming the ESP by hand asserts the firmware
assumption the guard otherwise checks -- rather than adding a new flag.

The check goes through rpath(), so a --root fixture declares its own
firmware type and the case is testable without a VM. On the live UEFI
machine it resolves to the real /sys path, so behaviour there is unchanged.

Audit of the other BIOS-unsafe paths (none destructive, left alone):
  * mount_esp(): resolve_esp_device fails with no /boot fstab entry, so it
    reports and does not mount. Non-destructive.
  * sync_kernel_image()/rebuild_initramfs(): gated behind boot_is_mounted,
    which is false on BIOS, so they never write.
  * check_fstab_options(): reports "no /boot entry" -- noise on BIOS, but
    it writes nothing.
  * run_preflight(): aborts every kernel upgrade on a BIOS box. Fail-closed
    and non-destructive, so out of scope here.

Tests: three cases in tests/lib/boot_repair_bios_cases.sh (new file; the
existing case lib is at the 250-line cap) -- the guard refuses and explains,
--esp overrides it, and a UEFI fixture still cleans shadows as before.
Verified failing without the guard (38/4) and passing with it (42/0).

Sandbox: BIOS guest keeps both kernel files, says why, and still boots
afterwards (uname -r 7.1.9-arch1-2 after a clean poweroff). shellcheck clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdyMBGCrGVGDWtJ9xe48f3
2026-08-22 23:46:20 +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
afe6ac3a33 Reformat three fixes libs with shfmt ahead of coverage work
shfmt runs on every staged shell file and rewrites drifted libs, which
moves coverage denominators and invalidates measurements taken minutes
earlier. Landing the reformat first makes every number measured from
here the number that survives pre-commit.

thorium_repairs.sh converts from 2-space indent to tabs; the other two
only lose runs of blank lines. `git diff -w` shows no token changed,
shellcheck is clean, and the 20-file suite stays green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 20:22:23 +02:00
0c92a00f0f Test arch_perf_fixes.sh to 93.48% and fix the journal regex a third time
Eighth of the 16 untested fixes/lib libs. 22 assertions; 43/46 = 93.48%. The
lib STAYS on the allowlist -- see the artifact below.

Third instance of the same dead branch. Line 16 matched on
`([0-9]+\.?[0-9]*)\ G`, a required space before the unit, while journalctl
prints "4.2G" with no separator, so the vacuum could never run. Fixed to an
optional space, as in arch_perf_report.sh.

This one differs from that earlier fix and the difference was confirmed with
the user before landing: arch_perf_report.sh's vacuum sits behind
--apply-safe-fixes, but THIS one runs whenever fix_arch_performance.sh runs.
So the fix makes an unflagged destructive action live -- the script will now
really vacuum the journal to 300M once it passes a gigabyte. That is the
function's evident intent, since its next act is to write a permanent 300M
cap, and the user approved it explicitly.

Three absolute paths moved behind overrides defaulting to the real ones:
JOURNALD_CONF_DIR, SYSTEMD_UNIT_DIR (fix_media_organizer writes
/etc/systemd/system/media-organizer.service) and ORGANIZE_SCRIPT_CANDIDATES.
Verified afterwards that the real unit file still carries its 2026-07-25
mtime and the journald drop-in its 2026-03-02 one, i.e. a bare un-jailed
run_all.sh touched neither.

The three uncovered lines (68-70) are the element lines of a multi-line array
literal. `PS4` + `set -x` on a minimal case shows bash reporting the whole
assignment at its OPENING line and never emitting the elements or the closing
paren, so they are data, not statements -- a fourth flavour of defect (a),
after multi-line quotes, `} |` and the backslash-continued assignment. The
array is provably used: the loop on the next line iterates it and the
media-organizer assertions pass.

Worth fixing, unlike the bt_adapter one-liner: the form appears in three of
the remaining untested libs (thorium_repairs.sh has four) and in nine
allowlisted libs repo-wide. That lands as its own commit next.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 19:34:12 +02:00
99a9804d75 Test ubuntu_perf_more.sh to 100%: allowlist 93 -> 92
Seventh of the 16 untested fixes/lib libs and the first of the ubuntu-perf
pair, which gets its own harness here. 24 assertions; 32/32 = 100.00%
through run_all.sh on the first measurement, confirmed at --min 100 (exit 0),
then cleared with the real gate and no exemption.

Two absolute paths had to move behind env overrides, defaulting to the real
ones: fix_journal writes /etc/systemd/journald.conf.d/size-limit.conf and
run_undo globs /root for undo scripts. Binding those in the jail was rejected
for the same reason as bt_pairing.sh's udev rule -- run_all.sh runs UN-jailed
in ci_mirror.sh and in CI, so a bind mount protects the coverage run and
nothing else. JOURNALD_CONF_DIR and UNDO_DIR follow the UDEV_RULES_DIR
precedent.

Proof the suite is safe where it runs un-jailed: after a bare run_all.sh the
real /etc/systemd/journald.conf.d/size-limit.conf still carries its 2026-03-02
mtime, a genuine artifact of a past production run, and no
undo_ubuntu_performance_*.sh exists in /root.

Worth noting for the rest of the family: run_undo ends in `exit`, so its cases
run inside an explicit subshell. A command substitution already provides one,
so the exit ends only that subshell rather than the whole test file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 19:25:21 +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