block_compulsive_opening: test the allow/block decision

21 assertions for cco_state.sh, the lib that decides whether a launch is
permitted. Coverage measured: 62/66 lines = 93.94%. The only uncovered lines
are kill_app's pkill/pkill -9 pair, which must never run in a test on a
machine whose apps are live. cco_wrapper/cco_install/cco_report stay at 0%:
every function in them moves system binaries, writes /etc/pacman.d/hooks or
kills processes.

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV
This commit is contained in:
Krzysztof Rudnicki 2026-08-21 15:53:54 +02:00
parent f5edebae4a
commit 72e1b6a8b1
5 changed files with 356 additions and 44 deletions

View File

@ -1,20 +1,21 @@
{
"title": "Split block_compulsive_opening.sh under the 250-line cap",
"objective": "Bring linux_configuration/scripts/periodic_background/digital_wellbeing/block_compulsive_opening.sh (705 lines) under the repo-wide 250-line file cap by moving its state, wrapper, install and reporting concerns into four sourced libs, preserving behaviour exactly and keeping the deployed /usr/local/bin entry path intact so already-installed app wrappers and the pacman hook continue to work.",
"objective": "Bring block_compulsive_opening.sh (705 lines) under the repo-wide 250-line cap by moving its 26 functions into four sourced libs, preserving behaviour exactly, and keeping the deployed /usr/local/bin copy working — it is executed by this machine's beeper/signal/discord launcher wrappers and by a live pacman PostTransaction hook.",
"acceptance_criteria": [
"The entry script and all four lib/cco_*.sh files are each at most 250 lines.",
"A line-multiset check against the pre-split file reports zero missing functional lines.",
"All three heredoc bodies -- the generated app wrapper, the generated pacman hook, and the help text -- are byte-identical to the pre-split versions, since they define deployed artifacts that verify_shell_split.sh cannot inspect.",
"status output is byte-identical to the still-monolithic deployed copy, and both exit 0.",
"The script runs correctly in BOTH layouts: lib/ beside the script (repo) and libs flat beside the script (deployed), and fails closed with exit 1 when a lib is missing.",
"install_all deploys the libs and uninstall_all removes them, in this same commit, so the deployed copy is never split-without-libs.",
"shellcheck passes without -x and shfmt -w is a no-op on all five files, with no suppression added."
"`help`, `status` and an unknown-command run are byte-identical to the pre-split baselines with unchanged exit codes (0, 0, 1).",
"verify_shell_split.sh shows no -/+ pair except install_all and uninstall_all, the two functions deliberately changed to deploy and remove the libs.",
"A line-multiset check accounts for every code line of the 705-line original.",
"The FLAT deploy layout (entry + libs side by side, no lib/ subdir) runs: proven in a scratch directory, since /usr/local/bin cannot be written from a test.",
"A missing lib fails closed with an actionable message rather than loading partially.",
"shellcheck passes without -x and shfmt -w is a no-op on every file, with no suppression added.",
"The new lib/tests suite passes and per-lib coverage is measured and recorded as a real number."
],
"out_of_scope": [
"Shell test coverage for the new libs -- no suite exists for this script; the split is verified behaviourally and textually.",
"Changing the deployed entry path or converting it to a symlink -- installed app wrappers hard-code /usr/local/bin/block-compulsive-opening.sh.",
"Deploying the libs under /usr/local/share/digital_wellbeing/lib, which install_leechblock_payload mirrors with rm -rf.",
"Any change to the app timeout policy, the hour-bucket semantics, or the managed app list."
"Executing install_all / uninstall_all / install_wrapper against the real system: they move /usr/bin binaries and write /etc/pacman.d/hooks.",
"Executing kill_app or launch_with_timer: they pkill running applications.",
"Re-deploying to /usr/local/bin. That is a separate, user-visible step; this commit leaves the working deployed monolith in place.",
"The pre-existing `APPS[$app]: unbound variable` message on an unknown app, confirmed identical pre-split."
],
"verifier": "bash meta/scripts/check_file_length.sh --all for the cap; a python3 line-multiset diff and per-heredoc body diffs against HEAD for content equivalence; status output compared against the pre-split deployed copy; a flat-layout run plus a deleted-lib run for the two _CCO_LIB_DIR branches and the fail-closed guard; shellcheck, shfmt idempotence and bash -n on all five files; pre-commit run --files on the staged set."
"verifier": "bash meta/scripts/check_file_length.sh --all; bash meta/scripts/verify_shell_split.sh HEAD <old> <old> <libs>; diff of help/status/unknown against captured baselines; a flat-layout simulation in a scratch dir including the missing-lib case; shellcheck + shfmt idempotence; lib/tests/run_all.sh and shell_coverage.sh."
}

View File

@ -1,64 +1,73 @@
{
"intent": "Bring block_compulsive_opening.sh (705 lines) under the 250-line cap by moving its state, wrapper, install and reporting logic into four sourced libs, without changing behaviour and without breaking the already-installed app wrappers that hard-code the deployed script's path.",
"intent": "Bring block_compulsive_opening.sh (705 lines) under the 250-line cap by moving its 26 functions into four sourced libs, without breaking the deployed copy that this machine's beeper/signal/discord launchers and a live pacman PostTransaction hook execute on every use.",
"scope": [
"linux_configuration/scripts/periodic_background/digital_wellbeing/block_compulsive_opening.sh: 705 -> 182 lines",
"New: lib/cco_install.sh (238), lib/cco_report.sh (102), lib/cco_state.sh (149), lib/cco_wrapper.sh (120)",
"install_all deploys the libs in the same commit, so there is no window where the deployed copy is split but its libs are missing",
"Non-goal: shell coverage for the new libs; no test suite exists for this script"
"block_compulsive_opening.sh: 705 -> 182 lines",
"New: lib/cco_state.sh (147), lib/cco_wrapper.sh (119), lib/cco_install.sh (238), lib/cco_report.sh (101)",
"New: lib/tests/cco_harness.sh + lib/tests/test_cco_state.sh (21 assertions)",
"Non-goal: executing install/uninstall/kill paths under test — they move system binaries and pkill running apps"
],
"changes": [
"Split into four libs by concern: cco_state.sh (hour-bucket state files), cco_wrapper.sh (the launch gate), cco_install.sh (install/uninstall, the generated app wrapper and the pacman hook), cco_report.sh (status and help).",
"The entry script keeps its exact deployed path, /usr/local/bin/block-compulsive-opening.sh, and stays a real file there rather than a symlink. Already-installed app wrappers on disk hard-code that path, and the pacman hook at 96-restore-pkg-wrappers.hook invokes it; a symlink would also break dirname \"${BASH_SOURCE[0]}\" resolution.",
"_CCO_LIB_DIR probes for lib/ beside the script (repo tree) and falls back to the script's own directory (the flat /usr/local/bin deploy layout). Both branches are exercised below.",
"install_all copies the four libs flat into /usr/local/bin alongside the entry script, and uninstall_all removes them so no orphans are stranded. The copy runs even when the script is already at install_path, so a lib changed upstream still reaches the deployed copy that `rewrap-quiet` executes.",
"A guard before the source lines fails closed with an explicit message when any lib is unreadable, rather than dying on a bare `source` error.",
"Ran shfmt -w on the entry script, which removed a block of ~26 consecutive blank lines. No associative-array keys were affected: unlike install_leechblock.sh, this file's hyphenated keys (\"signal-desktop\") were already quoted, so the shfmt arithmetic-parsing bug does not apply here. Verified by grep after the run."
"Moved all 26 functions into four libs by responsibility: state/timing (cco_state), the launch path (cco_wrapper), install/uninstall + pacman hook (cco_install), status/reset/usage (cco_report). Extracted with meta/scripts/extract_shell_functions.py, which walks brace-by-brace rather than slicing line ranges.",
"DEPLOY SHAPE (user's decision): the libs are deployed FLAT beside the entry script in /usr/local/bin, not in a lib/ subdirectory. This matches the existing precedent — /usr/local/bin/pacman_lock_lib.sh and heavy_job_lock.sh are already deployed and sourced exactly that way. The entry script probes for lib/ first and falls back to its own directory, so the repo layout and the deployed layout both work from one source of truth.",
"JUSTIFIED BODY CHANGE 1 -- install_all now copies the four libs to /usr/local/bin. Without this the deployed entry script would source libs that are not there. It runs even when the script is already at $install_path, because `rewrap-quiet` (the pacman hook) executes the DEPLOYED copy and would otherwise keep sourcing stale libs after an upstream change.",
"JUSTIFIED BODY CHANGE 2 -- uninstall_all removes those four libs, so an uninstall does not strand orphans in /usr/local/bin.",
"The entry script fails CLOSED on a missing lib, with an actionable message naming the re-install command. The pacman wrapper's fail-open pattern is deliberately NOT copied here: a half-loaded blocker that silently stops blocking is the exact failure this tool exists to prevent, and unlike pacman you do not need this script in order to repair it."
],
"verification": [
{
"command": "bash meta/scripts/check_file_length.sh --all",
"result": "pass",
"evidence": "Over-cap list went from 4 files to 3; block_compulsive_opening.sh no longer listed. All five files 182/238/149/120/102 lines, under 250."
"evidence": "Over-cap list went from 4 files to 3. block_compulsive_opening.sh is 182 lines; the largest new lib is cco_install.sh at 238."
},
{
"command": "python3 line-multiset check of entry+4 libs against HEAD's block_compulsive_opening.sh",
"command": "diff of `help`, `status` and an unknown command against pre-split baselines",
"result": "pass",
"evidence": "MISSING functional lines: 0. Every non-comment, non-blank line from the pre-split file is accounted for across the five files."
"evidence": "All three byte-identical, exit codes 0/0/1 unchanged. Re-verified after shfmt. One apparent diff was traced to $0 echoing the invocation path when run from a different cwd, not to a code change."
},
{
"command": "Heredoc body extraction and diff for all three heredocs",
"command": "bash meta/scripts/verify_shell_split.sh HEAD <old> <old> <4 libs>",
"result": "pass",
"evidence": "This is the check verify_shell_split.sh cannot perform, because it cannot see inside a heredoc body. WRAPPER_EOF (5 lines, the generated app wrapper), HOOK_EOF (14 lines, the generated pacman hook) and EOF (26 lines, the help text) are each byte-identical between HEAD and the split. The two deployed artifacts this script generates are therefore provably unchanged."
"evidence": "Exactly two -/+ pairs, install_all and uninstall_all — the two functions deliberately changed above. The other 24 functions moved byte-identically."
},
{
"command": "bash block_compulsive_opening.sh status vs /usr/local/bin/block-compulsive-opening.sh status",
"command": "python3 line-multiset check, original vs entry+libs (comments excluded)",
"result": "pass",
"evidence": "Byte-identical output, both exit 0. The deployed copy is still the pre-split monolith and sha256-identical to HEAD, so this compares the split against the real pre-split behaviour rather than against itself. `help` differs only in the $0 invocation path, as expected."
"evidence": "0 code lines from the 705-line original missing from the split."
},
{
"command": "Flat-layout test: entry script + libs copied into one directory (the deployed shape)",
"command": "flat-layout simulation in a scratch dir (entry + 4 libs, no lib/ subdir)",
"result": "pass",
"evidence": "status runs correctly with exit 0, confirming the _CCO_LIB_DIR fallback branch works in the /usr/local/bin layout, not just the repo tree."
"evidence": "This is the shape install_all writes to /usr/local/bin. `help` and `status` both exit 0 from the flat layout. Removing one lib produces the fail-closed error naming the missing file and the re-install command, rather than a partial load."
},
{
"command": "Missing-lib guard: delete cco_report.sh from the flat layout and re-run",
"command": "wrapper path against an unknown app, with HOME/XDG_STATE_HOME pointed at a tmpdir",
"result": "pass",
"evidence": "Fails closed with 'Error: missing library cco_report.sh in <dir>' and exit code 1 (measured directly, not through a pipeline). A partially-deployed install cannot look successful to the pacman hook."
"evidence": "Rejects cleanly. The `APPS[$app]: unbound variable` message it emits was confirmed PRE-EXISTING by running the identical command against the pre-split original from git — byte-identical output, so not a regression introduced here. The real ~/.local/state/compulsive-block was confirmed untouched afterwards."
},
{
"command": "shellcheck (no -x) + shfmt idempotence + bash -n on all five files",
"command": "lib/tests/run_all.sh",
"result": "pass",
"evidence": "shellcheck rc=0 with no suppressions; shfmt -w is a no-op on all five, so the pre-commit auto-format-and-re-stage cannot alter them; all five parse."
"evidence": "21 assertions for cco_state, 0 failures (50 across the whole directory including the leechblock suites). Covers the allow/block decision in both directions, including the case that matters most: a PREVIOUS hour's stamp must not keep blocking, or an app could never be reopened."
},
{
"command": "bash meta/scripts/shell_coverage.sh <run_all.sh> cco_state.sh 0",
"result": "measured, below 100%",
"evidence": "MEASURED: cco_state.sh 62/66 lines = 93.94%. The only four uncovered lines are kill_app's pkill/pkill -9 pair, which must never execute in a test on this machine. cco_wrapper/cco_install/cco_report are 0%: every function in them moves system binaries, writes /etc/pacman.d/hooks or kills processes."
},
{
"command": "shellcheck (no -x) + shfmt -w idempotence on all 8 files",
"result": "pass",
"evidence": "shellcheck rc=0, no suppressions added. shfmt is a no-op on every file. The hyphenated associative-array keys (signal-desktop, and the APPS/REAL_BINARIES/APP_TIMEOUT_MINUTES/PROCESS_MATCH maps) were already quoted and the corruption canary is clean."
}
],
"risks": [
"The deployed copy at /usr/local/bin/block-compulsive-opening.sh is still the pre-split monolith and keeps working untouched; nothing breaks until someone runs `sudo block-compulsive-opening.sh install`, which now deploys the libs too. Verified that the pacman hook's `rewrap-quiet` path does not call install_all, so the hook cannot trigger a partial deploy.",
"The libs deploy flat into /usr/local/bin rather than into /usr/local/share/digital_wellbeing/lib. That is deliberate: install_leechblock_payload does an `rm -rf` mirror of the latter, which would delete these libs on the next pacman-wrapper install.",
"No test suite exists for this script, so coverage is 0% and the verification above is behavioural and textual rather than test-driven."
"The deployed /usr/local/bin copy is NOT updated by this commit. Until `sudo block_compulsive_opening.sh install` is re-run, the deployed 705-line monolith keeps running — which is safe, because it is self-contained and unchanged. The dangerous ordering would be deploying the new entry script without its libs; the fail-closed guard turns that into a loud error rather than a silently disabled blocker.",
"install_all is not executed by any test (it moves /usr/bin binaries), so its new lib-copy block is verified by the flat-layout simulation rather than by running it. The simulation reproduces exactly what it writes.",
"A pre-existing quirk was confirmed rather than fixed: `wrapper <unknown-app>` prints a raw `APPS[$app]: unbound variable` before its clean error. Identical pre-split; out of scope for a behaviour-preserving split."
],
"rollback": [
"git revert this commit; block_compulsive_opening.sh returns to a single 705-line file and lib/cco_*.sh disappear.",
"If the split had already been deployed, re-run `sudo block-compulsive-opening.sh install` from the reverted tree to restore the monolith and remove the deployed libs.",
"Validate with: block-compulsive-opening.sh status (expect exit 0 and the per-app status table)."
"git revert the commit; block_compulsive_opening.sh returns to a single 705-line file and lib/cco_*.sh disappear.",
"The deployed copy needs no rollback — this commit never touched it.",
"If the split HAD been deployed before rolling back, re-run `sudo block_compulsive_opening.sh install` from the reverted tree and then confirm `ls /usr/local/bin/cco_*.sh` returns nothing and a wrapped app still launches."
]
}

View File

@ -0,0 +1,146 @@
#!/usr/bin/env bash
# lib/tests/cco_harness.sh — shared fixture for the block_compulsive_opening.sh split.
#
# Sourced, not executed. Scope: the hour-key/state logic that decides whether an
# app launch is allowed. Everything that moves system binaries (install_wrapper,
# uninstall_wrapper, install_all) or kills processes (kill_app, launch_with_timer)
# is NOT executed here — this machine's beeper/signal/discord wrappers and a live
# pacman hook depend on those paths, and a test that ran them would rewrite them.
#
# STATE_DIR and LOG_FILE were already variables in the pre-split script, so the
# tests point them at a tmpdir and nothing touches ~/.local/state.
set -euo pipefail
PASS=0
FAIL=0
_t_pass() {
PASS=$((PASS + 1))
printf ' OK: %s\n' "$1"
}
_t_fail() {
FAIL=$((FAIL + 1))
printf ' FAIL: %s\n' "$1"
}
_t_eq() {
local want="$1"
local got="$2"
local what="$3"
if [[ $got == "$want" ]]; then
_t_pass "$what"
else
_t_fail "$what (want '${want}', got '${got}')"
fi
}
_t_has() {
local haystack="$1"
local needle="$2"
local what="$3"
if [[ $haystack == *"$needle"* ]]; then
_t_pass "$what"
else
_t_fail "$what (want a substring '${needle}')"
fi
}
# These helpers return 0/1 to mean allowed/blocked, and which way they fail is
# the whole point — assert on the status explicitly rather than letting `set -e`
# end the run. Called via `if` so a non-zero return does not abort the suite.
_t_true() {
local what="$2"
if ("$1"); then
_t_pass "$what"
else
_t_fail "$what (expected success, got non-zero)"
fi
}
_t_false() {
local what="$2"
if ("$1"); then
_t_fail "$what (expected non-zero, got success)"
else
_t_pass "$what"
fi
}
CCO_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
export CCO_LIB_DIR
# Stand in for the globals the entry script defines before sourcing the libs.
_t_cco_setup() {
TEST_TMPDIR="$(mktemp -d)"
export TEST_TMPDIR
STATE_DIR="$TEST_TMPDIR/state"
LOG_FILE="$STATE_DIR/compulsive-block.log"
mkdir -p "$STATE_DIR"
AUTO_CLOSE_TIMEOUT_MINUTES=10
AUTO_CLOSE_WARNING_MINUTES=2
declare -gA APP_TIMEOUT_MINUTES=(["beeper"]=20 ["signal-desktop"]=20)
declare -gA APPS=(
["beeper"]="$TEST_TMPDIR/bin/beeper"
["signal-desktop"]="$TEST_TMPDIR/bin/signal-desktop"
["discord"]="$TEST_TMPDIR/bin/discord"
)
declare -gA REAL_BINARIES=(
["beeper"]="$TEST_TMPDIR/real/beepertexts"
["signal-desktop"]="$TEST_TMPDIR/real/signal-desktop"
["discord"]="$TEST_TMPDIR/bin/discord.orig"
)
declare -gA PROCESS_MATCH=(["discord"]="$TEST_TMPDIR/home/.config/discord/")
mkdir -p "$TEST_TMPDIR/bin" "$TEST_TMPDIR/real"
# notify() is defined in the entry script, which the tests never source.
notify() { printf 'NOTIFY: %s | %s\n' "$1" "$2" >>"$TEST_TMPDIR/notifications"; }
: >"$TEST_TMPDIR/notifications"
_t_cco_assert_fixture_complete
}
# Every global above is read by the sourced lib, never by this file, so the
# linter reports each one as unused (SC2034) and notify() as uncalled
# (SC2329) — it cannot see across the `source` boundary the tests set up.
# Referencing them here is not linter appeasement: it is a real fixture check.
# A typo in one of these names would otherwise surface as a confusing unbound
# variable from deep inside the code under test, or worse, as a silently
# skipped assertion.
_t_cco_assert_fixture_complete() {
local missing=""
[[ -n $LOG_FILE ]] || missing+=" LOG_FILE"
[[ -n $AUTO_CLOSE_TIMEOUT_MINUTES ]] || missing+=" AUTO_CLOSE_TIMEOUT_MINUTES"
[[ -n $AUTO_CLOSE_WARNING_MINUTES ]] || missing+=" AUTO_CLOSE_WARNING_MINUTES"
[[ ${#APPS[@]} -eq 3 ]] || missing+=" APPS"
[[ ${#REAL_BINARIES[@]} -eq 3 ]] || missing+=" REAL_BINARIES"
[[ ${#APP_TIMEOUT_MINUTES[@]} -eq 2 ]] || missing+=" APP_TIMEOUT_MINUTES"
[[ ${#PROCESS_MATCH[@]} -eq 1 ]] || missing+=" PROCESS_MATCH"
# Call notify() for real rather than probing it with `type -t`: block_app
# routes every blocked launch through it, so a fixture whose notify is
# broken would turn the notification assertions into silent no-ops. The
# probe file is truncated afterwards so this call is not mistaken for one
# made by the code under test.
notify "fixture" "self-check"
[[ -s "$TEST_TMPDIR/notifications" ]] || missing+=" notify"
: >"$TEST_TMPDIR/notifications"
if [[ -n $missing ]]; then
printf 'harness fixture incomplete:%s\n' "$missing" >&2
exit 1
fi
}
_t_cco_teardown() {
[[ -n ${TEST_TMPDIR:-} && -d ${TEST_TMPDIR} ]] && rm -rf "$TEST_TMPDIR"
}
_t_report() {
printf '\n%s: %d passed, %d failed\n' "${1:-tests}" "$PASS" "$FAIL"
[[ $FAIL -eq 0 ]]
}

View File

@ -1,12 +1,17 @@
#!/usr/bin/env bash
# Runs every leechblock lib test. Used directly as the coverage subject:
# Runs every digital_wellbeing lib test. Used directly as the coverage subject:
# meta/scripts/shell_coverage.sh wants the test script itself, not `bash <it>`.
#
# The glob is test_*.sh, NOT one prefix per split: this directory holds suites
# for several entry scripts (leechblock, block_compulsive_opening, ...), and a
# prefix glob silently skips every suite added after it — including in the CI
# discovery step, which runs this file.
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
rc=0
for t in "$HERE"/test_leechblock_*.sh; do
for t in "$HERE"/test_*.sh; do
printf '\n=== %s ===\n' "$(basename "$t")"
# Each suite exits non-zero on failure; keep going so one red suite does not
# hide the state of the others, then fail the run as a whole.

View File

@ -0,0 +1,151 @@
#!/usr/bin/env bash
# Covers lib/cco_state.sh — the logic that decides whether a launch is allowed.
#
# This is the whole point of the tool: was_opened_this_hour deciding wrong in
# either direction is the failure that matters. Too permissive and the blocker
# does nothing; too strict and a legitimately-closed app can never be reopened.
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=cco_harness.sh
. "$HERE/cco_harness.sh"
# shellcheck source=../cco_state.sh
. "$CCO_LIB_DIR/cco_state.sh"
_t_cco_setup
trap _t_cco_teardown EXIT
printf 'get_hour_key: a stable YYYY-MM-DD-HH stamp\n'
hour="$(get_hour_key)"
if [[ $hour =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{2}$ ]]; then
_t_pass "the hour key is YYYY-MM-DD-HH"
else
_t_fail "the hour key is YYYY-MM-DD-HH (got '$hour')"
fi
_t_eq "$hour" "$(get_hour_key)" "two calls in the same hour agree"
printf '\nget_state_file: one file per app, under STATE_DIR\n'
_t_eq "$STATE_DIR/discord.lastopen" "$(get_state_file discord)" "the state path is derived from the app name"
_t_eq "$STATE_DIR/discord.running" "$(get_running_file discord)" "the running-state path is separate from the lastopen path"
printf '\nwas_opened_this_hour: the core allow/block decision\n'
rm -f "$STATE_DIR/discord.lastopen"
if was_opened_this_hour discord; then
_t_fail "a fresh app is not yet opened this hour"
else
_t_pass "a fresh app is not yet opened this hour"
fi
record_opening discord >/dev/null 2>&1
if was_opened_this_hour discord; then
_t_pass "after record_opening the app counts as opened"
else
_t_fail "after record_opening the app counts as opened"
fi
printf '\nwas_opened_this_hour: a stale hour must NOT keep blocking\n'
# The bug that would matter most: if a previous hour's stamp still blocked, an
# app could never be reopened. Write a deliberately old stamp.
echo "2000-01-01-00" >"$STATE_DIR/discord.lastopen"
if was_opened_this_hour discord; then
_t_fail "a previous hour's stamp must not block the current hour"
else
_t_pass "a previous hour's stamp does not block the current hour"
fi
printf '\nwas_opened_this_hour: an unreadable/empty stamp allows the launch\n'
: >"$STATE_DIR/discord.lastopen"
if was_opened_this_hour discord; then
_t_fail "an empty state file must not block"
else
_t_pass "an empty state file does not block"
fi
printf '\nrecord_opening: writes exactly the current hour\n'
rm -f "$STATE_DIR/beeper.lastopen"
record_opening beeper >/dev/null 2>&1
_t_eq "$(get_hour_key)" "$(cat "$STATE_DIR/beeper.lastopen")" "the stamp written is the current hour key"
printf '\nblock_app: notifies the user\n'
: >"$TEST_TMPDIR/notifications"
block_app discord >/dev/null 2>&1
_t_has "$(cat "$TEST_TMPDIR/notifications")" "Blocked" "blocking sends a desktop notification"
printf '\nis_autoclose_suspended: only todays marker counts\n'
rm -f "$STATE_DIR/discord.suspend-autoclose"
if is_autoclose_suspended discord; then
_t_fail "no marker means not suspended"
else
_t_pass "no marker means not suspended"
fi
printf '%(%Y-%m-%d)T' -1 >"$STATE_DIR/discord.suspend-autoclose"
if is_autoclose_suspended discord; then
_t_pass "todays marker suspends auto-close"
else
_t_fail "todays marker suspends auto-close"
fi
echo "1999-12-31" >"$STATE_DIR/discord.suspend-autoclose"
if is_autoclose_suspended discord; then
_t_fail "a stale marker must not suspend auto-close"
else
_t_pass "a stale marker does not suspend auto-close"
fi
if [[ -f "$STATE_DIR/discord.suspend-autoclose" ]]; then
_t_fail "a stale marker is cleaned up on detection"
else
_t_pass "a stale marker is cleaned up on detection"
fi
printf '\nget_real_binary: only reports a binary once the wrapper is installed\n'
rm -f "$TEST_TMPDIR/bin/beeper.orig"
if get_real_binary beeper >/dev/null 2>&1; then
_t_fail "an unwrapped app reports no real binary"
else
_t_pass "an unwrapped app reports no real binary"
fi
: >"$TEST_TMPDIR/bin/beeper.orig"
_t_eq "$TEST_TMPDIR/real/beepertexts" "$(get_real_binary beeper)" "a wrapped app resolves to its real binary"
printf '\nlog_message: appends to the log and echoes to stderr\n'
: >"$LOG_FILE"
log_message "hello from the test" 2>/dev/null
_t_has "$(cat "$LOG_FILE")" "hello from the test" "the message reaches the log file"
printf '\nensure_state_dir: creates STATE_DIR when it is missing\n'
rm -rf "$STATE_DIR"
ensure_state_dir
if [[ -d $STATE_DIR ]]; then
_t_pass "a missing state directory is created"
else
_t_fail "a missing state directory is created"
fi
# Idempotent: the real script calls it on every wrapper launch.
ensure_state_dir
_t_pass "calling it again on an existing directory is harmless"
printf '\ncleanup_stale_running_state: clears state when nothing is running\n'
running="$(get_running_file discord)"
echo "999999 123" >"$running"
# PROCESS_MATCH points at a path no process can match, so this is the
# "app already exited" case.
cleanup_stale_running_state discord 2>/dev/null
if [[ -f $running ]]; then
_t_fail "a running-state file for a dead process is removed"
else
_t_pass "a running-state file for a dead process is removed"
fi
printf '\ncleanup_stale_running_state: no running file is a no-op\n'
# The early return. It must not fall through to the pgrep path, which on a
# machine where the app IS running would otherwise be consulted for nothing.
rm -f "$running"
if (cleanup_stale_running_state discord); then
_t_pass "a missing running-state file returns success without probing"
else
_t_fail "a missing running-state file returns success without probing"
fi
_t_report "test_cco_state"