705 -> 182 lines, with state, the launch gate, install/uninstall and
reporting moved into cco_state.sh, cco_wrapper.sh, cco_install.sh and
cco_report.sh.
The entry script keeps its exact deployed path and stays a real file
there, not a symlink: the app wrappers already installed on disk
hard-code /usr/local/bin/block-compulsive-opening.sh, and the pacman
hook invokes it. _CCO_LIB_DIR probes for lib/ beside the script and
falls back to the script's own directory, so the repo tree and the flat
/usr/local/bin deploy layout both work; both branches were exercised.
install_all deploys the libs and uninstall_all removes them in this same
commit, so there is never a deployed copy that is split but missing its
libs -- the failure that had to be repaired for install_leechblock.
The libs deploy flat into /usr/local/bin rather than into
/usr/local/share/digital_wellbeing/lib, because install_leechblock_payload
does an rm -rf mirror of that directory and would delete them.
Verified beyond the usual gates: a line-multiset check reports zero
missing functional lines, and all three heredoc bodies -- the generated
app wrapper, the generated pacman hook and the help text -- are
byte-identical, which verify_shell_split.sh cannot check. status output
matches the still-monolithic deployed copy byte for byte.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
29 assertions across two files, run via lib/tests/run_all.sh.
Coverage measured, not waived: leechblock_fetch.sh 38.57% (27/70),
leechblock_browsers.sh 36.47% (31/85), config and firefox 0%. The uncovered
lines are exactly the bodies that must never run under test — curl/tar/unzip
downloads, pkill + sudo binary patching, npm install, and writes to
/etc/*/policies.json. Closing the gap needs those externals shimmed.
What the tests actually pin down:
- all three get_latest_tag fallbacks (releases API, tags API, Location
header), plus JSON-null-is-not-a-tag and the all-sources-exhausted
non-zero return. A silent failure there installs the wrong version.
- the 8+3 browser tables including every hyphenated key, and the
command -v gating that decides whether a binary is patched at all.
Also fixes a latent set -u abort: wire_up_browsers interpolates VERSION_DIR,
VERSION and TAG, which only resolve_version sets. Calling it on its own
aborted on an unbound VERSION_DIR; they are now read through locals with
defaults, leaving production output unchanged.
Harness note: PATH must be narrowed to the shim dir for "no browser
installed" to be testable on a machine that has five, but every external the
code under test calls has to be seeded there too — a missing mkdir aborted
the suite under set -e with no stderr, which under kcov looked like a
coverage-tool bug rather than a PATH bug.
The hyphenated array subscripts in the test are quoted for the same reason
they are in the lib: shfmt -w rewrites an unquoted [google-chrome-stable]
into a subtraction expression. Reproduced live in this file during the work.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV
485 -> 114 lines, with the six install phases moved into four sourced
libs: leechblock_fetch.sh, leechblock_config.sh, leechblock_browsers.sh
and leechblock_firefox.sh.
detect_browsers, replace_browser_in_place and wire_up_browsers stay in
one lib on purpose: detect_browsers writes the BROWSERS/FIREFOXES arrays
that wire_up_browsers reads, and splitting them would create a
write-only cross-file global.
Four justified body changes, all recorded in the evidence:
- Quoted the six hyphenated associative-array keys. shfmt 3.13.1 parses
an unquoted [google-chrome-stable] as arithmetic and rewrites it to
[google - chrome - stable], which would break browser detection
entirely. The pre-commit hook runs shfmt -w on staged files and
re-stages them, so this corruption would have landed in the commit
unseen -- this is what caused the previous attempt at this split to be
reverted. Behaviour-preserving: quoted and unquoted are the same key.
- SCRIPT_DIR is assigned in the entry script rather than inside
inject_default_config. Left in the lib, ${BASH_SOURCE[0]} resolves to
lib/, so the defaults JSON lookup would miss and default blocking
rules would silently never be seeded.
- install_firefox_policy takes ff_found as a parameter instead of
reading it as a cross-file global, fixing a genuine SC2154 without a
suppression.
- Added require_cmd unzip. It was used bare under set -Eeuo pipefail, so
a missing unzip would abort after the version dir was populated but
before the 'current' symlink was made, leaving a half-installed tree.
verify_shell_split.sh reports additions only -- no -/+ pair -- so
get_latest_tag and replace_browser_in_place moved byte-identically.
--help and unrecognized-flag output are byte-identical to the baseline.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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