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
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
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
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
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
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
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
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
The template checks were grepping an empty argument list. SETUP_SOURCES was
initialised as SETUP_SOURCES=("${SETUP_SOURCES[@]}") — a self-reference to an
array that did not exist yet — so it started empty, never contained
$SETUP_SCRIPT, and the lib-discovery loop ran dirname on nothing.
CI reported "setup_midnight_shutdown.sh should install the D-Bus monitor
helper". Reproduced locally before changing anything, so the fix is verified
against the real failure.
Seeding it with ("$SETUP_SCRIPT") restores the intent: search the entry
script together with lib/ms_*.sh, since the monitor template moved into
lib/ms_monitor.sh during the split.
test_pacman_wrapper_security.sh and test_hosts_guard_pacman_integration.sh
re-checked at the same time and both pass, so the Shell tests set is green
again before the file-length gate goes in.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV
Shell tests has been red since fbe9134c. Three assertions across two
files grep pacman_wrapper.sh for functions the split moved into the flat
pw_*.sh libs: pacman_hooks_manage_guard_lib and
should_use_wrapper_guard_lib_fallback (pw_guard.sh),
verify_policy_integrity and the two -U helpers (pw_policy.sh), and
auto_remove_virtualbox_vms plus VBoxManage (pw_cleanup.sh).
Each grep now searches the entry script and its libs together, via a
pw_*.sh glob rather than a named lib, so a future re-partition does not
break them again. The sed extraction in the security test reads
pw_policy.sh directly, since it runs those two functions in isolation.
Deliberately left narrow: the two assert_order checks still target the
entry script alone. They assert the order of two call sites, and both
calls are still there -- widening them would turn a positional check
into a meaningless one. The 'CRITICAL near verify_policy_integrity' and
auto_remove_virtualbox_vms call-site greps were also left alone after
confirming they still match in the entry script.
Both files now exit 0. Of the two other suites still failing locally,
neither is from this commit: test_security_hardening's remaining failure
is this machine's unwrapped signal-desktop/discord and passes in CI, and
test_shutdown_timer_monitor is failing on the uncommitted
setup_midnight_shutdown split in the working tree.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The block_compulsive_opening split moved launch_with_timer into
lib/cco_wrapper.sh, and tests 17 and 18 grep the entry script alone. CI went
red on "launch_with_timer function exists — Not found at expected location":
a false failure about code that is present and working, one file over.
Both assertions are about a feature existing, so they now search the entry
script together with lib/cco_*.sh. That restores what they were always
checking and does not go stale on the next split.
The lib list is built with a loop and an -f test rather than a bare glob, so
an unmatched pattern cannot land in the array as a literal and no shellcheck
suppression is needed.
Verified by running the suite: both checks PASS, 18 passed. The one remaining
local failure is test 19, which inspects /usr/bin on this machine — signal
and discord are not currently wrapped here and beeper is not installed. That
is machine state, predates this change, and is not what CI reported.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV
Regression from this session's earlier commit (327d48b): the test still
referenced pacman-hooks/*.sh and install_pacman_hooks.sh, which that commit
archived to testsAndMisc-archive. shell-tests.yml runs this test directly
(not scoped by pre-commit's --files), so it went red on CI without being
caught locally before the push.
Dropped the assertions tied to the archived files; kept and extended the
ones checking pacman_wrapper.sh's guard-lib fallback logic, which was
already fully guard-lib-native and unaffected by the archive.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
The last Python file over the cap:
_transcribe_media.py 143 _try_import, both duration probes,
get_media_duration, ffmpeg transcode, cleanup
_transcribe_cluster.py 105 _resample_linear, _kmeans_cosine and its
iteration loop
_transcribe_diarize.py 164 the pipeline: audio loading, the classifier,
embedding extraction, diarize_segments
The graph is a tree: _transcribe_media imports nothing local, _cluster takes
only _try_import from it, _diarize imports from both. All 13 top-level
definitions verified AST-identical to HEAD, and an AST scan confirms the
three module constants travelled to the modules that use them.
transcribe_fw.py now imports get_media_duration from _transcribe_media
rather than through _transcribe_diarize. Ruff removed that re-export as
unused and broke the import; running the program is what caught it, and
naming the owning module is the correct fix under the no-barrels rule.
Verified: transcribe_fw.py /nonexistent.wav still exits 2 and --help exits
0; 63 tests pass at 100% line and branch on all three modules; 338 pass in
the directory. Over-cap files 33 -> 32, and no Python file is over the cap
any more - the remaining 32 are 29 shell, 2 kotlin, 1 dart.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
The largest untested Python file left in the repo, at 356 lines against the
cap. Tests land first so the split that follows is verified against tests
that predate the seam.
63 tests: duration probing through both ffmpeg paths, the transcode fallback
and temp-file cleanup, audio loading's three outcomes, linear resampling,
cosine k-means, the ECAPA classifier loader, per-segment embedding
extraction, and diarize_segments end to end.
numpy is real here, not a double. It is a declared dependency, and the core
of this file is clustering maths - assertions against a mocked numpy would
prove nothing about whether two speakers actually get two labels. The
_try_import seam passes numpy through while stubbing torch and speechbrain,
so the tests still run in CI with neither installed. The clustering tests
assert real properties: two opposed groups separate, magnitude is ignored
because the metric is cosine, and a fixed seed reproduces.
The last three coverage gaps were _load_audio's ffmpeg fallback in both
outcomes, the minimum-slice widening, and the zero-iteration k-means branch.
Split across six test modules after two intermediate arrangements went over
the cap, and one move silently broke a test by separating it from its
doubles - caught by the test count holding at 63 through every rearrangement.
338 tests pass in the directory, up from 275 by exactly the 63 added.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
_transcribe_progress.py 91 _run_progress_loop, _format_progress_line,
and the two constants only they use
transcribe_fw.py 236 argument parsing, model loading, diarized
output writing, main
All 8 top-level definitions verified AST-identical to HEAD, and an AST scan
confirms neither module references a name it does not bind - which is what
caught that the two progress constants and the hhmmss import had to travel
with the loop. transcribe_fw.py no longer imports time.
The progress tests now patch _transcribe_progress, including its sys.stderr
and time references, since that is where the loop resolves them.
_resolve_device_and_compute stayed behind, so its test still points at
transcribe_fw.
Verified: --help prints the full usage and a missing input returns exit 2,
the code transcribe.sh branches on; 32 tests pass at 100% line and branch on
both modules; 275 pass in the directory. Over-cap files 34 -> 33.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
transcribe_fw.py is 302 lines against the cap and was untested. Tests land
first, in their own commit, so the split that follows is verified against
tests that predate the seam.
32 tests: argument parsing and its FW_* environment defaults, device and
compute resolution, the progress line, the transcription loop, model
loading, diarized output writing, and main()'s three exit paths.
faster_whisper is reached only through _try_import, so patching that seam
covers every path without the package installed. _format_progress_line is
tested directly rather than through the loop - it is pure, so the percentage
clamp, the ETA, and the suppression of an ETA longer than a day are each
assertable without a model.
The last coverage gap was _try_import itself, which every other test
patches; it is now exercised directly in both directions.
Split across three test files because a single pipeline module reached 321
lines - over the cap this work exists to clear. 275 tests pass in the
directory, up from 243 by exactly the 32 added.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
Split along the natural seam now that the file is fully covered:
_transcribe_probes.py 177 _try_import, the four dependency probes,
generate_sine_wav, prepare_model, test_cuda
transcribe_helpers.py 142 the eight _handle_* functions and main()
All 18 top-level definitions verified AST-identical to HEAD, and an AST
scan confirms neither module references a name it does not bind.
transcribe.sh is untouched - it invokes tools/transcribe_helpers.py by
path, and that path still exists with the same name and executable bit.
The CLI tests' patch targets did not move either, because main() dispatches
to names imported into its own namespace.
Verified by running every subcommand and reading the exit status directly,
since those codes are the contract transcribe.sh branches on:
python-version 0, check-faster-whisper 7, check-ctranslate2 1,
check-diarization 0, deps-installed 0; generate-wav writes the same
96044-byte WAV as before the split. 41 tests pass at 100% line and branch
on both modules; 243 pass in the directory. Over-cap files 35 -> 34.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
transcribe_helpers.py is 289 lines against the 250-line cap and was
completely untested. Tests land first, in their own commit, so the split
that follows is verified against tests that predate the seam.
Left at its current path rather than moved under python_pkg/: transcribe.sh
invokes it by path inside a dedicated ML venv, and transcribe_fw.py imports
two siblings by bare name, so the closure is five files (1236 lines), not
three. Tests go in linux_configuration/tests/ instead, whose conftest
already puts standalone script directories on sys.path - the same
arrangement the usage_report work used, and still collected by CI.
The ML dependencies are never installed in CI and the module reaches them
only through _try_import, so patching that one seam covers every probe in
both directions. An AST scan at every nesting level confirmed there is no
other path to them: the only direct numpy imports sit under TYPE_CHECKING.
41 tests, 100% line and branch. The last gap was the dispatch guard at
line 284, unreachable through the CLI because argparse's choices list and
the dispatch dict carry the same eight commands; covering it by admitting a
ninth choice also documents why the guard is there.
Split across four test files because a single module hit 291 lines - over
the cap this work exists to clear.
Verified: python-version prints 3.14, check-faster-whisper exits 7 with no
ML deps present, generate-wav writes a 96044-byte WAV; 243 tests pass in the
directory, up from 202 by exactly the 41 added.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
Documented as "kept for backwards import compatibility", it had zero
production callers - only its own two tests. Removed the function, those
two tests, and the pathlib.Path import that only its signature used.
_SEC_PER_DAY stays: two live callers remain at _usage_report_run.py:135
and :181, so only the function was dead, not the constant.
Verified: usage_report.py --help exits 0 (the unit tests stub subprocesses,
so only a real run proves the import graph), 202 tests pass, and
_usage_report_run.py holds 100% line+branch coverage after removing both
the code and the tests that covered it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
The entry point now holds only the parser, mode dispatch and main. Three
modules take one concern each: _usage_report_logs (where the logs are,
the state file, per-day segment planning), _usage_report_emit (preflight,
stdout, clipboard), _usage_report_run (the two run paths and the
aggregation they drive).
Verified by the 100%-coverage tests from the previous commit. An AST
comparison of all 22 top-level defs and classes reports 22 identical, 0
mismatches.
All four modules hold 100% line+branch. 204 tests before and after. Both
CLI modes were run against the machine's real logs, which is the check
the unit tests cannot make: they stub the collaborators, so only a real
run proves the new import graph resolves.
Two lessons, recorded because the next target is a 498-line file:
This cut was too big. Two names had to be relocated after their first
placement created an import cycle, and four modules' worth of monkeypatch
targets needed re-pointing. A smaller first slice would have been cheaper.
Patching a moved function must target the module whose namespace resolves
it - the caller's, not the one it was moved to. Getting that wrong on
_run_since made main() call the real function, which read the actual atop
logs and hung the suite rather than failing.
Three import bugs (Path and _fmt_h left under TYPE_CHECKING but used at
runtime, dataclass missing where _Aggregates landed) were caught
immediately by the coverage from the previous commit.
Over-cap 38 -> 37.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
25 tests in four files, no production change - the module is
byte-identical to HEAD. Lands before the split for the usual reason.
Covers the CLI surface (every default, every flag, main's dispatch), both
run paths including the shared empty-window-means-a-full-day fallback,
_run_since's no-data path and both state-advance branches, the
clipboard/emit side (tool selection, a failing tool, no tool at all),
_preflight's three outcomes, _aggregate_segments with a gap day and with
a present-but-empty log, and _log_descriptions.
Also covers _compute_window, which turns out to be dead: a shim
documented as "kept for backwards import compatibility" with zero callers
anywhere in the repo. Tested rather than deleted here, since removing it
is a behaviour change that does not belong in a coverage commit - but it
is a deletion candidate.
185 stmts / 44 branches, 0 missed. Directory: 179 -> 204 passing.
No suppressions: mypy rejected a `list.append(...) or 0` lambda, replaced
with a real closure; ten ruff S108 findings on hardcoded /tmp paths became
/nonexistent sentinels, which is more accurate anyway since those paths
are never opened.
Written as two files of 273 and 282 lines, then split to four under the
cap before committing - the DropZone lesson from earlier today.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
Lifts the atop subprocess machinery into _usage_report_atop.py: running
atop, streaming its parseable output, building/caching the atop_agg C
helper, and reading its TSV. What stays is record parsing, epoch windows
and per-day merging.
Verified by the 100%-coverage tests from the previous commit. An AST
comparison of all 16 functions against HEAD reports 16 identical, 0
mismatches.
_usage_report_render.py imported _run from the parsing module; it now
imports from the module that owns it. That is the same re-export barrel
the last split shipped and had to fix in a follow-up - caught in the same
commit this time. usage_report.py needed no edit at all.
Both modules hold 100% line+branch (94/28 and 130/34, 0 missed). 179
tests before and after; the five new test files collect 8+6+4+14+5 = 37,
which is last commit's 36 plus the one re-homed out of
test_usage_report_modes.py, where it referenced a constant that had
moved out from under it.
The two test files added last commit were themselves over cap (373, 272)
so they are split five ways here - the DropZone lesson from earlier
today, where a lint fix silently added an over-cap file.
Ran the program: a real report from today's logs, through the native
atop_agg path, which is precisely the code this commit moved.
Over-cap 39 -> 38.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
36 tests, no production change - the module is byte-identical to HEAD.
Lands separately from the split that follows, because tests written in
the same commit as a split are written against the post-split shape.
This one mattered more than the last: the uncovered 61% was almost
exactly the code a split would extract - the subprocess plumbing and the
native atop_agg path. Splitting that first would have proved nothing.
_atop_agg_binary had no tests at all; all five outcomes are now covered
(fresh cache, no compiler, make raising, make exiting 0 with no binary,
successful build + chmod). The fresh-cache test asserts by installing a
subprocess.run that fails if called. Every subprocess is faked - no test
spawns atop, make or cc.
Also covers _run's two error paths, _iter_atop_lines with and without a
stdout pipe, the native N/W TSV rows including malformed ones,
_parse_name's six paths, _parse_prm, _window_from_epochs' median
interval, _fold_pid_aggregates, and aggregate_atop end to end.
219 stmts / 62 branches, 0 missed. Directory: 143 -> 179 passing. No
suppressions - two mypy exit-return findings were fixed by typing the
fakes' __exit__ as None rather than silencing them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
Verified by the 100%-coverage tests from the previous commit, written
before this seam existed.
_cpu_table/_dedupe_ram/_ram_table/_gpu_table move to
_usage_report_tables.py with the two constants they own. _md_escape and
_fmt_h move to _usage_report_format.py: both the tables module and the
render module need them, so a third module is what avoids a cycle.
Import sites name the owning module; no re-export barrel.
usage_report.py needed no edit - it imports _fmt_h and _render_report
from the render module, and both names still resolve there.
Evidence the move was verbatim: an AST comparison of all 10 functions
against HEAD reports 10 identical, 0 mismatches. That matters because I
retyped rather than cut-and-pasted, and did briefly introduce a literal
× where the original had × - ruff's RUF001 caught it.
Coverage stays 100% line+branch on all three (15/55/54 stmts, 4/10/14
branches, 0 missed). Test count 43 before and after, summed across the
five files it now lives in - the check that matters for a test split,
since a file outside the runner's glob is silently never collected.
Directory total 143 passed, unchanged.
Ran the real program: a full report from today's atop + pmon logs, every
section rendering through the new boundaries including the RAM sibling
dedupe and its (+20 more) truncation.
Over-cap 41 -> 39. No suppressions; a shared test-helper module was tried
and abandoned because name-tests-test would have needed an exclusion.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
43 tests, no production change - the module is byte-identical to HEAD.
This lands separately from the split that follows because a split
verified by tests written in the same commit proves nothing: the tests
would have been written against the post-split shape.
Covers every function, and specifically the branches a happy-path test
misses: zero-length window and cpu_count() -> None in _cpu_table,
non-positive RSS and the CPU/pid tie-break in _dedupe_ram, the sibling
truncation boundary in _ram_table, zero total_samples in _gpu_table,
OSError and unparseable MemTotal in _host_profile, and the single-sample
unknown-interval path in _methodology_section and _render_report.
_host_profile is the only impure function; its /proc reads go through a
monkeypatched Path.open and nvidia-smi is stubbed at the _run seam, so
no test touches the disk or forks.
117 stmts / 28 branches, 0 missed. linux_configuration/tests: 100 -> 143
passing. No suppressions: two ANN401s were removed by typing the helper
kwargs explicitly rather than silencing them, and a mypy ignore went away
with a fallback branch no caller reached.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
Split three ways on the file's own section banners, which already grouped the
tests by concern:
test_usage_report_merge.py (145) aggregate and window merging, PID carry-over
test_usage_report_since.py (186) pmon filtering, atop bounds, state, planning
test_usage_report_modes.py (178) start resolution, dispatch, PRC HZ, native
6 + 13 + 11 = 30, the pre-split count -- the check that matters, since a file
that failed to import would collect nothing and still report success. Also
verified a negated assertion still fails, so none of the three is vacuous.
Only the test file is split. The usage_report SOURCE files stay over cap on
purpose: measured coverage is 21% for _usage_report_render.py, 39% for
_usage_report_parsing.py and 47% for usage_report.py, so a split there would
move code no test runs and pass either way.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
The 316-line installer was 66 over the 250-line cap. The managed-copy helpers,
the /usr/local/bin install step, the integrity + drift manifest, and the
immutability/LeechBlock/VirtualBox payloads move into a sibling lib/, leaving a
128-line entry.
What stays in the entry is chosen by what the harness can execute, not by
taste. `--bind-abs /usr/bin` removes the directory bwrap needs to exec
anything, so the trace stops at `ln -sf ... /usr/bin/pacman` with exit 1 on
EPERM. That line onwards, plus the `if [ ! -L /usr/bin/pacman ]` backup branch
whose guard is false on this machine, therefore never run under trace — moving
them would have shipped code no diff could check. They stay put.
Everything else is proven: the before/after traces are identical, including all
16 generated-file hashes. pacman_wrapper at 548fc2b53ee4cea6 is the one that
matters most — it is post-`sed -i`, so it catches a reorder of the rewrite
relative to the copy.
The path definitions stay in the entry too, for a linter reason rather than a
runtime one: a definitions-only lib assigns without referencing, which is
SC2034 on 36 variables, and suppressions are not allowed. Libs that only
reference globals are fine. `export` would have silenced it while leaking 40
variables into every child process, which the trace cannot see.
Eight assertions across two test libs grep the installer's source text; five
needed widening to search entry+lib/. TEST 9's `chattr +i` grep caught the move
as a genuine failure first, which is what prompted enumerating the rest. No
assertion was weakened.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
The 290-line installer was 40 over the 250-line cap. Its five banner-marked
steps move into a sibling lib/, leaving a 53-line entry that owns log()/die(),
the globals that cross between steps, and the call order.
Proven by execution, not inspection: meta/scripts/trace_shell_split.sh
--prefix was captured at HEAD in place (tracing from a detached worktree
changes REPO_DIR, which is interpolated into a generated file, and fakes a
diff), then re-run after the split. The traces are identical -- same exit
status, same ordered stubbed calls, and all five generated-file hashes
unchanged, including usage-report-catchup.sh at sha=9f4803ab157c5527.
REPO_DIR is computed once in the entry and read as a global rather than
recomputed inside a lib one directory deeper; recomputing it would have
pointed the generated catch-up script at the wrong repo while still exiting 0.
test_usage_monitoring_installer_efficiency.sh greps the installer's source
text for the pmon heredoc, so it follows that heredoc to lib/nvidia_pmon.sh.
Its extraction anchor also had to become whitespace-insensitive: the
pre-commit hook runs `shfmt -w` on staged shell files and rewrites
`cat > "x" << 'SCRIPT'` to `cat >"x" <<'SCRIPT'`, which silently broke
extraction. All five property assertions are unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
test_pacman_wrapper_security.sh (287) and test_i3blocks_efficiency.sh
(345). Both keep their filenames -- shell-tests.yml runs them by name --
and both were verified by diffing a full run against the pre-split code:
byte-identical output, exit 0 either side.
The i3blocks setup half stays in the entry on purpose. Its five fake
$BIN_DIR binaries are written with heredocs, and a seam inside a heredoc
yields a lib that will not parse -- the worst failure this effort has
produced so far. Only the two heredoc-free regions moved.
Both i3blocks lib functions end in an explicit `return 0`. Their last
statement is a `|| fail ...`, which as a function tail under set -e
becomes the return value and would abort a run in which every check
passed. `fail` exits on its own, so reaching the end means success.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two files under the cap.
test_security_hardening.sh (423) keeps its filename -- shell-tests.yml
runs the suite by name -- and its five sections move into three sourced
libs. They are top-level blocks, not functions, so each is wrapped and
called in the original order; the counters and LIVE_SECTION stay in the
entry, which sourcing shares. Verified by diffing a full run against the
same suite from a detached worktree at HEAD: byte-identical output, same
exit status, still 18/1/5 on this host. The one failure is pre-existing.
Linting a lib alone turns a bare LIVE_SECTION=1 into SC2034, since its
only reader is test_result in the entry. A live_section() setter makes
the write a real reference, so no suppression was needed.
The mutation-driver split was already in the tree from the previous
session and did not run: the operators had moved to
_session_autopsy_mutations.py but the driver still named Mutation
without importing it, so mypy and pylint both reported undefined-variable.
Restored the import; the script now reports all 12 tests killed.
block_compulsive_opening.sh (705) is deliberately untouched and now says
so in the contract. install_all copies the running script to
/usr/local/bin, so an entry sourcing lib/ would resolve SCRIPT_DIR there,
find nothing, and leave beeper, signal-desktop and discord unlaunchable
with a failing pacman hook. Splitting it means teaching the installer to
copy the libs -- a refactor, not a line-count split.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
412 lines -> 135, with the nine functions in lib/music_detect.sh (133),
lib/music_monitor.sh (91) and lib/music_status.sh (85).
find_music_services and kill_music_services stay together: the first
writes MUSIC_FOUND_PROCESS/MUSIC_FOUND_WINDOW and the second reads them,
so splitting between them would thread mutable state across files.
The test suite caught what the static checks could not. The harness
copies the script into a temp worktree to stub common.sh, and copied
exactly one file -- so after the split every source line failed and the
script died before any behaviour under test ran. Teaching it to copy
lib/ too is a harness fix under decision 8: no assertion changed, the
same five checks run, exit 0 before and after.
bash -n also earned its keep: the first splice dropped notify_user's
closing brace, and shellcheck reported that only as SC1094 "parsing of
sourced file failed" against the entry.
One rename, not a suppression: monitor_loop's local `music_services`
became `active_services` after shellcheck flagged SC2153 against the
MUSIC_SERVICES config array now visible in the same file.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
is_blocked_package_name compared raw pacman -U <file>.pkg.tar.zst
arguments against whitelist entries, which never matched since yay
always installs AUR packages this way. Added strip_pkgfile_suffix()
to derive the bare package name first, plus a regression test.
Also swaps the browser whitelist entries to ungoogled-chromium
(Chromium 151 dropped MV2, breaking uBlock Origin's full/classic
build).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015yfDYbaw1D1WhVMqWhsSDA
The claude_usage i3blocks tests failed on CI while passing locally.
archlinux:base-devel ships no jq, and claude_usage.sh parses its cache
with it, so the block degraded to its "no data" path and every
percentage assertion failed. That degradation is correct at runtime —
the bar should not break because a tool is missing — but it made a
missing dependency look like a logic bug.
Install jq alongside git and strace, and probe for it at the top of the
section so the failure names its own cause. The probe runs jq rather
than calling `command -v`: a jq that exists but cannot execute produces
exactly the same "no data" output as an absent one, and `command -v`
passes in that case — verified by stubbing a jq that exits 127.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YQLwdquDCNmWtvgi3Lk72J
Add a [claude_usage] block showing the 5h and 7d rate-limit windows. It
reads ~/.claude/cache/limit-state/*.json, which Claude Code's statusLine
hook already writes to disk, so the data costs nothing to display: the
script only runs jq against a local file and makes no API call. Limits
are account-wide while the cache is sharded one file per project, so it
takes the newest file and ignores cwd. Percentages arrive as floats and
are truncated; a stale cache is labelled rather than shown as current,
and a rolled-over window reads "?%" instead of a misleading value.
Replace Font Awesome private-use-area icons with plain Unicode. Both
fonts named in the bar's pango string ("System San Francisco Display",
"FontAwesome") are not installed, so PUA codepoints fell through to
whatever font claimed them: ethernet rendered as a star, wifi as
Cyrillic, gpu as Hangul, disk as a chess pawn. Emoji resolve through
fontconfig consistently, which the pre-existing volume block already
relied on, and need no change to ~/.config/i3/config.
Remove the bluetooth, battery, wifi, activitywatch, warp and
network_monitor blocks: this desktop has no BT adapter, no battery, no
wireless interface, and no warp-cli. This also removes a rendering bug,
since activitywatch_status.sh and warp_status.sh were interval=persist
yet emitted three lines per update, leaking their colour line into the
bar as literal "#FF0000" text.
Fix ethernet reporting "down" on a live wired link:
find_ethernet_interface returned the first non-loopback interface, which
with docker installed is a br-* bridge (permanently down, and sorted
before enp*). It now requires a real /sys/class/net/*/device and prefers
an interface that is up.
Tests drop the assertions and fixtures for the removed blocks and add
claude_usage coverage (fresh, warn, critical, stale, rolled-over, null
reset, unparsable, missing dir), a guard that removed blocks cannot
return, a repo-wide check that no block script uses a PUA codepoint, and
an ethernet physical-NIC regression check. The fork budget is asserted
against a populated cache directory, which caught a stat call inside the
scan loop that cost one fork per cached project (170 -> 2 execs).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YQLwdquDCNmWtvgi3Lk72J
phone_focus_mode is built for a rooted phone: the hosts layer is a read-only
bind mount over /system/etc/hosts, chosen so that even root-in-a-terminal
cannot overwrite it. None of that ports to a stock locked device. This splits
out the parts that are device-independent so one policy can drive both.
python_pkg/focus_policy parses config.sh rather than copying it, so the shell
config stays the source of truth and the two cannot drift. It rejects the
shipped REDACTED_* coordinate placeholder instead of defaulting to (0,0),
which would move "home" to the Atlantic and quietly disable the location gate.
blocklist.py converts the hosts file to domains for a DoT resolver or a local
VpnService. Three properties of the real 194k-line file make the obvious parser
wrong, and all three are handled: generate_hosts_file.sh *comments out*
deliberate unblocks rather than deleting them, so stripping '#' re-blocks
facebook/messenger/linkedin/4chan; the StevenBlack header carries loopback and
IPv6 housekeeping that would blackhole localhost; and ~1060 upstream annotation
tokens ('#[adware.zeno]', '"spam') render as address=/"spam/, which makes
dnsmasq refuse to start -- taking DNS down rather than blocking anything.
Verified with dnsmasq --test over all 185681 generated domains.
strip_workout_hosts.py had the same class of bug on the live rooted phone:
exact set membership meant the allowlisted googlevideo.com never matched the
dynamic r*---sn-*.googlevideo.com hosts YouTube actually streams from, so the
workout unblock was only partly working. Now suffix-matched (17 lines stripped,
was 14). The three new tests were checked against the original code in a
throwaway worktree; two of them fail there, so they pin the fix rather than
passing vacuously.
focus_owner is a device-owner app that deliberately does nothing yet. It ships
clearDeviceOwnerApp() implemented and tested first, because dpm
remove-active-admin does not work on a device owner: an app that locks the
device down before proving it can let go leaves a soft-brick recoverable only
through fastboot on a locked bootloader. Built, installed and launched on the
Pixel 6a; dumpsys afterwards confirms Device Owner Type -1 and no user
restrictions, i.e. the phone is unchanged.
Measured while scoping this, and not yet reflected in any enforcement code:
on Android 16 both pm suspend and pm disable-user are cleared by a reboot, so
shell-level app blocking is per-session only. setApplicationHidden is assumed
to persist under Device Owner but is untested.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AwepyL5Lxs5QKbKkMpxxxL
The shellcheck hook ran with --severity=warning, so info- and style-level
findings never failed a commit. 222 had accumulated across 94 of 244 shell
files. They were not all style noise -- six were bugs:
- Five scripts in single_use/misc/testsAndMisc-bash/ sourced
$SCRIPT_DIR/../../lib/common.sh, one directory too shallow. Nothing exists
at that path, so all five failed at runtime.
- setup_personal_website.sh and setup_kcd2_dice_solver.sh called usage()
(which ends in `exit 0`) and then `exit 1` for an unknown command. The
exit 1 was unreachable, so a typo'd subcommand reported SUCCESS.
- Six i3blocks scripts had a `# shellcheck source=` directive missing the
scripts/periodic_background/ path segment. The stale path had silently
disabled source-following for all of them.
- diagnose_arch_performance.sh word-split a newline-joined file list; when
the find came back empty, awk got no file operands, fell through to
reading stdin, and the diagnostic hung.
- lib/common.sh's handle_arg_help_or_unknown ended in an unreachable
`return 0` while documenting "Returns: 0 if handled" -- a contract no code
path could satisfy.
- deploy.sh's SCRIPT_DIR is reassigned by config.sh, which it sources at top
level, and config.sh can repoint it at the on-device path
/data/local/tmp/focus_mode. Renamed to DEPLOY_DIR.
The SC1091 bulk (81 findings) was resolved rather than silenced: .shellcheckrc
sets external-sources + source-path=SCRIPTDIR so sourced files are actually
followed and analysed. That is what surfaced the i3blocks and deploy.sh bugs.
17 `# shellcheck disable=` directives remain, each with a written reason, and
only where the finding is provably wrong: injection-test payloads that must not
expand, literal source text being grepped for, POSIX sh scripts on the phone
where arrays do not exist, and source paths that only exist at runtime.
Everything else was fixed in code.
Verified in a CLEAN worktree, not just my own tree. config.sh sources the
gitignored config_secrets.sh, which exists locally but not in a fresh checkout;
a working-tree-only check passed while ci_mirror.sh -- which lints HEAD in a
clean worktree, exactly as CI does -- failed on it.
The gate is verified to bite: a file whose only finding is SC2016 (info) fails
the hook, while the old --severity=warning gate exits 0 on it.
third_party/ is excluded -- vendored upstream, matching the prettier hook.
Not fixed, reported: check_polling_antipatterns.sh carries a pre-existing
SC2034 disable that hides a real bug -- its `patterns` array is never used, so
the checker detects nothing. Confirmed unchanged by this commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The gate caught this: test_session_autopsy_hooks reads the DEPLOYED hook under
~/.claude, so repointing that hook to kuhyx/mcp-servers broke an assertion still
expecting `python_pkg.session_autopsy`. Exactly what a deployment-path test is
for — a test against a stale in-repo copy would have stayed green.
The replacement is stronger than what it replaces: it asserts both the new module
path AND that "python_pkg" is absent, so regressing to the old location fails
rather than passing quietly. The mutation checker confirms all 12 tests are still
killed by at least one mutation.
Also adds the evidence/contract artifacts for the extraction. They were written
for 627b9e7 but never committed: that commit was pure deletions, and the
ai-evidence-contract hook does not treat deletions as code changes, so it passed
without them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
a83b5ab took the analyzer package to 100% branch coverage but stopped at the
package boundary. Its only production entry points are two shell hooks in
~/.claude/hooks, and those were untracked, unlinted and untested -- the same
"untracked meant unlinted" shape that commit's own message describes, one
directory over. Both run on every session on this machine.
Both hooks end in an unconditional exit 0 by design: an autopsy failure must
never surface into the session lifecycle. That makes exit status a constant, so
a test asserting it passes against a hook whose body has been deleted. Every
assertion here is on a side effect instead. For SessionEnd a setsid shim on PATH
records the launch argv rather than spawning an ingest, so the tests can pin the
command, transcript path, --quiet, PYTHONPATH and timeout on the happy path, and
pin no-launch-at-all for a missing key, an empty path, a nonexistent file and
malformed JSON.
SessionStart asserts stderr as well as stdout. Under set -u, (( count > 0 )) on
a non-numeric count raises an arithmetic error to stderr while stdout stays
silent, so a stdout-only assertion would miss a hook injecting bash noise into
every session start. That gap was found by mutation, not by inspection.
A pytest file rather than a test_*.sh, deliberately: only .py under
linux_configuration/tests is collected by pytest_changed_packages.py, so this
lands in the local pre-commit gate, whereas the .sh tests there are gated by the
shell-tests workflow alone. It imports nothing from python_pkg, so the
100%-on-python_pkg gate is untouched -- 977 pass at 100% branch.
Verified by a per-guard mutation matrix rather than one whole-body gutting:
gutting alone kills only the three positive tests and would have falsely branded
the nine negative ones vacuous. Dropping each guard in turn, widening > 0 to
>= 0, and announcing unconditionally kills all twelve. The matrix also showed
that the start hook's [[ ! -r "$STATE" ]] guard is redundant -- deleting it kills
nothing, since jq's stderr is suppressed and an empty count already fails the
arithmetic. Left in place as defence in depth; the hooks are unmodified.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Test 10 only knew the retired shutdown-schedule-guard.path, so it
reported a skip while guard-file@shutdown-schedule.path was actively
protecting /etc/shutdown-schedule.conf. That understated the real
protection in the same way tests 2, 3 and 5 did before the last
commit.
It now accepts the guard-lib unit and passes. The
disabled-is-intentional branch stays for hosts running neither unit.
Host: 19 passed / 0 failed / 5 skipped. Container: exit 0.
The suite reported four failures on this host and none of them meant
protection was missing.
Tests 2, 3 and 5 checked hosts-guard.path, hosts-bind-mount.service
and nsswitch-guard.path. The 2026-07-17 guard-lib refactor replaced
those with templated units — guard-file@hosts.path,
guard-bind-mount@hosts.service, guard-file@nsswitch.path — which are
all active. That is also why the legacy units stop a second after
boot: they are superseded, and the old nsswitch guard retriggered
itself into systemd's start limit fighting the new watcher. Each check
now accepts either generation, and the bind-mount check also accepts
the read-only mount on /etc/hosts, which is the real invariant.
Test 10 now treats a *disabled* shutdown-schedule-guard as intended
rather than broken: the poweroff flow it protected was replaced by
night-lockdown-enter.sh. An enabled-but-inactive unit still fails.
That check also had a shell bug — `systemctl is-enabled` prints the
state and exits non-zero when disabled, so `|| echo disabled`
appended a second line and never matched.
Verified protection is genuinely intact rather than assuming it:
/etc/hosts is mounted ro,noatime and both it and /etc/nsswitch.conf
carry the immutable flag. Host now reports 18/0/6. A container with
faked host markers still reports 10 failures, so the suite has not
been softened into always-green.
No system state was changed.
The last three exclusions are gone.
test_i3blocks_efficiency.sh only ever needed strace and ptrace. It went
unverified earlier because Docker's bridge network has no DNS on this
machine, so pacman could not install strace; --network=host works, and
the test passes. The job installs strace and adds SYS_PTRACE.
test_hosts_guard_pacman_integration.sh asserted three symbols the
guard-lib refactor renamed (pacman_hooks_manage_hosts_guard ->
pacman_hooks_manage_guard_lib, and similar). The feature was intact;
only the assertions were stale.
test_security_hardening.sh now separates live-host checks from
repository checks. Off-host the live ones skip, so it is green in a
container while still running its repository checks — and on the real
host it still fails loudly when a guard unit is down.
That last part needed care: the first detection attempt used
`systemctl is-system-running`, which exits non-zero on a degraded
system — precisely the state a failed guard unit produces. It masked
all four real failures. Detection now tests for /run/systemd/system
plus the installed guard payload, which unit health does not affect.
Verified: all 8 arch tests pass in a container; the same
security-hardening file still reports 4 failures on this host.
Adds an arch-tests job on archlinux:base-devel so the pacman wrapper,
makepkg wrapper and hosts-guard tests finally run somewhere. They need
an Arch userland and root, which a disposable container provides.
Four more tests still referenced pre-reorganisation paths and could
never find their targets; repairing them took the container set from
two passing to five. All five were verified with --network=none, so
the job installs nothing beyond git (base-devel ships no git, and
actions/checkout needs it).
Also removes the silent-failure mode behind the earlier
music_parallelism confusion. The test now derives its temp-worktree
path from the script's real position instead of hardcoding the depth,
checks up front that ../../lib/common.sh resolves to the stub, and has
the stub write a marker asserted after every run. Previously a
reorganisation made the script source the real
/usr/local/lib/common.sh, so the test measured the live system and
failed as though the daemon were broken.
Three tests stay out, each for a different reason, documented in the
workflow: test_i3blocks_efficiency.sh needs strace and could not be
verified locally (containers here have no DNS);
test_hosts_guard_pacman_integration.sh fails a real assertion worth
investigating; test_security_hardening.sh asserts live host state and
can never pass in a fresh container.
Both tests excluded from CI last commit turned out to be stale rather
than catching regressions. Neither covered script needed changing.
test_music_parallelism.sh built its temp worktree at
scripts/digital_wellbeing/, but music_parallelism.sh resolves its
library as $SCRIPT_DIR/../../lib/common.sh — two levels up, correct
for the real scripts/periodic_background/digital_wellbeing/ location.
From the shallower path that resolved outside the worktree, so the
stubbed common.sh was never sourced and the script fell through to the
real /usr/local/lib/common.sh on the machine. The test was exercising
the live library and ignoring its own MOCK_FOCUS_ACTIVE, which is why
idle mode looked like it waited 15s instead of 30s. The daemon was
correct all along.
test_usage_monitoring_installer_efficiency.sh demanded a literal
'sleep 60'. The logger template no longer polls: it sleeps once until
midnight via seconds_until_next_day, then blocks on wait for the pmon
child — strictly fewer wakeups than the assertion required. Replaced
with assertions on that property so a return to polling still fails.
Allowlist extended from four tests to six.
The shell tests under linux_configuration/tests/ never ran anywhere:
no workflow executed them, so they rotted silently. Three referenced
script paths that moved under scripts/periodic_background/ in an
earlier reorganisation, and test_android_guardian_service.sh created
its temp worktree at a different path from the one it copied into, so
its cp could never succeed.
The new workflow runs the bats suites plus an explicit allowlist of
side-effect-free tests. Roughly eight others drive pacman, /etc,
systemd or adb and need root plus an Arch container, so they stay out.
Repairing the paths uncovered two genuine failures, deliberately left
out of the allowlist because they look like real regressions in the
scripts they cover rather than stale assertions:
- test_music_parallelism.sh expects idle mode to wait
IDLE_CHECK_INTERVAL (30s); the daemon waits 15s, so it wakes twice
as often as intended when idle.
- test_usage_monitoring_installer_efficiency.sh expects the logger
template to sleep between day-rollover checks.
Both need a fix in the covered scripts, not in the tests.
Diagnosing "Steam downloads are slow" kept pointing at the ISP because
every averaged measurement showed a steady ~170 Mbps. Sampling at 1s
instead of 5s showed that plateau does not exist: the link alternates
between ~450 Mbps bursts and multi-second stalls. Effective rate is
burst x duty_cycle, and only the second factor was wrong.
steam-download-duty.sh reports those two factors separately, which is
what the Steam UI cannot do. line-speed-probe.sh separates burst from
sustained throughput, because a single stream tops out near 180 Mbps on
this line and reads convincingly as ISP shaping when it is not — a
many-connection swarm reaches 986 Mbps with zero retransmits.
Arithmetic is extracted into pure helpers with main() behind a
BASH_SOURCE guard so the bats suite can source them without starting a
measurement. install.sh installs bats and links both scripts onto PATH.
Note: no CI workflow runs .sh/.bats tests, including the four shell
tests that predate this one, so the suite is manual-only for now.
Steam/browser mutual-exclusion daemon was unusable in its current state.
Removed all code, install tooling, and every reference across
install_core_system.sh, check_and_enable_services.sh, the security
hardening test suite, and CLAUDE.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Vendored makepkg's run_pacman() waits on /var/lib/pacman/db.lck existence
with no timeout BEFORE invoking pacman, so an orphaned lock hangs
`makepkg -i` forever ("Pacman is currently in use, please wait...") and
the pacman wrapper's stale-lock cleanup never gets a turn. Wrap makepkg
(mirroring the pacman-wrapper pattern) to clear an orphaned lock up front.
- Extract stale-lock logic into shared pacman_lock_lib.sh; pacman_wrapper
sources it after integrity verification and it is in the SHA256 manifest
- makepkg_wrapper: pre-flight clear for -i/--install; bypass on FAKEROOTKEY
and non-install; FAIL OPEN if the lib is missing (never brick makepkg)
- Cross-user pacman_process_running() pgrep guard: unprivileged makepkg can
detect a root pacman its own fuser cannot see (avoids removing a live lock)
- Upgrade survival: rewrap hook (Install/Upgrade only) + check_makepkg_wrapper
drift verifier + both wrappers wired into the hourly periodic driver
- Fix reorg path bugs: stale ~/linux-configuration periodic path,
$CONFIG_DIR/hosts -> scripts/periodic_background/hosts, test WRAPPER_DIR;
refresh .orig only when target isn't already our symlink (no exec loop)
- Tests: test_makepkg_wrapper.sh (18) incl. hook Operation validity; add
lib-integrity coverage to test_pacman_wrapper_security.sh (also path-fixed)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JsCs1GUyiinA7bg68YvAh1
Add a self-expiring override system so specific late-night live events
(2026 World Cup matches) can be watched without the auto-shutdown killing
the PC, without weakening the permanent schedule ratchet:
- shutdown-override-manager.sh (add/list/remove): add is friction-gated
(typed confirmation phrase + 30s delay, max 12h, max 3 windows) and
writes absolute-epoch windows to /etc/shutdown-schedule-overrides.conf.
- day-specific-shutdown-check.sh now skips the shutdown decision inside an
active window (pruning expired entries) and gained a DRY_RUN guard around
rtcwake/poweroff/hibernate for safe testing.
i3blocks shutdown_countdown.sh: show the exact next-shutdown clock time
(not a relative countdown), show the overridden end time in green when an
override is active or will rescue the upcoming shutdown, and make the
skip-dates check fork-free (builtin read loop) so the hot path spawns no
external processes.
install.sh: deploy i3blocks by symlinking each repo file into
~/.config/i3blocks instead of cp -r (the source of silent drift), cd to
the script dir so it runs from anywhere, and chmod +x only *.sh so the
config file stays 0644.
Repair the previously-dead i3blocks regression suite (stale path), update
the shutdown assertion to the new absolute-time output, add hermetic
override coverage, and keep the <=1-exec fork-count guarantee.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmwYH5RfThZGkVD2jE61Da
setup_dns_blocker.sh turns this PC into a DNS server that serves the
identical /etc/hosts blocklist to the whole LAN with no per-device app,
by feeding generate_hosts_file.sh output into dnsmasq via addn-hosts
(same StevenBlack variant + custom entries + unblocks). Adds status/
refresh subcommands, a daily refresh timer, Restart=always, and logging
to /var/log/dns-blocker/.
Because the ISP router cannot advertise a custom DNS server, a dhcp/
dhcp-off mode makes the PC the LAN DHCP server (static IP via nmcli with
a gateway-ping self-check and safe, always-online rollback), handing out
itself as the DNS server so every device is blocked automatically.
setup_wireguard_ssh.sh gains an allow-dns subcommand that opens DNS (53,
LAN saddr) and DHCP (67) in the single-owner nftables ruleset. The DHCP
accept rule is placed before 'ct state invalid drop' because the
0.0.0.0->255.255.255.255 DHCP broadcast is classified INVALID and was
otherwise dropped. Firewall detection now reads the actual loaded ruleset
(a ruleset can be loaded while nftables.service reads inactive) and avoids
a grep -q/pipefail SIGPIPE false-negative.
Verified end-to-end: Pixel-6a leased 192.168.1.82 with DNS 192.168.1.43;
youtube.com blocked, legit sites resolve. Regression tests added.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018pqSnrLt8QoWup7n2Ymk77
Split diet_guard/_gatelock.py, wake_alarm/_alarm.py, and the
usage_report.py/_usage_report_parsing.py pair into focused
sub-modules so every Python file is <= 500 lines, satisfying
test_file_length.py. Install python-kasa into .venv (declared in
requirements but missing after the 3.13->3.14 venv upgrade),
fixing 8 failing smart_plug tests and restoring 100% coverage.
Also includes prior in-progress work from the working tree: the
wake_alarm Progress/View/Hardware field-grouping refactor,
brother_printer query module + tests, diet_guard foodbank/state/cli
updates, new shared coerce/logging_setup helpers, morning_routine
orchestrator tweaks, dwm window-manager config, gaming scripts, and
misc maintenance/digital-wellbeing script updates.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>