testsAndMisc/phone_focus_mode/lib/tests/daemon_cases_state.sh
Krzysztof kuhy Rudnicki 7dc3c94160
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
config.sh: split 571 -> 250, with a fingerprint that actually works
The brief's prescribed safety net for this file DOES NOT WORK, and that is
the main finding here. It specifies asdict + json.dumps(default=str) +
sha256 against baseline 1624750c. Four policy fields are frozenset, which
json cannot encode, so default=str stringifies them in SET ITERATION ORDER
-- random per process. Three consecutive runs gave three different digests;
pinning PYTHONHASHSEED=0 gave one. The recipe can never match a recorded
baseline, so it would have flagged every split as a regression regardless
of correctness.

The repo already ships a deterministic serializer. Before trusting it I
checked it was not a weaker proxy: policy_to_json emits all 16 dataclass
fields plus 6 more, so it is strictly stronger than asdict. Baseline
83e05e82, unchanged through all six extractions.

Two audits ran BEFORE any extraction:

  - the loader regex-scans config.sh's TEXT ALONE, so the fifteen names it
    reads are pinned here. Moving one does not error -- it silently yields
    an empty set, and for WHITELIST that reads as "hide everything".
  - extractions are variable-scoped, never line-range. A line-range attempt
    cut through the WHITELIST string, leaving an unterminated quote; 15
    variables vanished from the environment WHILE THE FINGERPRINT STILL
    MATCHED, because the loader's regex tolerated the mangled file. That is
    why the environment diff exists alongside the hash.

The environment check earned its keep twice: it also caught STATE_DIR being
left behind the source line, which expanded every path against an empty
prefix (/status.json rather than the real path) with the policy hash green
throughout. 79 variables, byte-identical either side, and every parsed list
compared entry-by-entry: 29/16/46/1/1/2/15, all matching.

Rationale prose moves to docs/policy-lists.md with pointers left behind,
including the blocks inside the list strings -- the loader and the shell
both ignore '#' lines there.

All five siblings are in both deploy lists. deploy.sh still sources the
chain from the checkout, run_phone.sh --help exits 0, 379 shell assertions
pass across nine suites, and focus_policy's 137 pytest cases pass.

config.sh is at EXACTLY 250 with no headroom; 184 of those lines are
loader-pinned lists that cannot move. Over-cap count 18 -> 17.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-17 17:17:02 +02:00

185 lines
7.0 KiB
Bash
Executable File

#!/usr/bin/env bash
# lib/tests/daemon_cases_state.sh — assertions for daemon_state.sh: the state
# files derived from config, the cached default intent handlers, and the JSON
# status snapshot the companion app reads.
#
# Sourced by test_daemon_libs.sh after daemon_libs_harness.sh, which owns the
# stubs, the subjects and the PASS/FAIL counters this file adds to.
set -euo pipefail
# `settings` is only reached from refresh_default_handlers; stubbed here
# rather than in the harness because these are its only callers.
cat >"${RUN}/bin/settings" <<'STUB'
#!/usr/bin/env bash
printf 'settings %s\n' "$*" >>"${DEV}/calls.log"
key="${*: -1}"
[[ -f "${DEV}/setting_${key}" ]] && cat "${DEV}/setting_${key}"
exit 0
STUB
chmod +x "${RUN}/bin/settings"
# --- build_whitelist_file ---------------------------------------------------
_reset_dev
WHITELIST="com.one
com.two
# a comment
com.three "
build_whitelist_file
parsed="$(cat "${STATE_DIR}/whitelist.txt")"
_t_eq "com.one com.two com.three" "$(printf '%s' "${parsed}" | tr '\n' ' ' | sed 's/ $//')" \
"whitelist drops comments and blanks and trims whitespace"
# The floor warning exists because a stray quote in config.sh silently
# truncates the WHITELIST string; a short list must be loud, not silent.
case "$(_log)" in
*"WARN: whitelist suspiciously small"*) _t_pass "a short whitelist warns loudly" ;;
*) _t_fail "a whitelist under the floor must warn" ;;
esac
case "$(_log)" in
*"Whitelist parsed: 3 entries"*) _t_pass "whitelist logs the parsed entry count" ;;
*) _t_fail "whitelist should log its entry count" ;;
esac
# A healthy list must not cry wolf, or the warning stops meaning anything.
_reset_dev
WHITELIST="$(for i in $(seq 1 40); do printf 'com.app%02d\n' "$i"; done)"
build_whitelist_file
if _log | grep -q "WARN: whitelist suspiciously small"; then
_t_fail "a 40-entry whitelist must not warn"
else
_t_pass "a healthy whitelist does not warn"
fi
# Both sides checked at once: the parsed file must hold every seeded entry,
# and reading WHITELIST back gives it a reader in this file (SC2034).
_t_eq "40-40" "$(wc -l <"${STATE_DIR}/whitelist.txt")-$(printf '%s\n' "${WHITELIST}" | wc -l)" \
"the whitelist keeps every one of the 40 seeded entries"
# --- build_night_whitelist_file --------------------------------------------
_reset_dev
NIGHT_WHITELIST="com.night.one
# comment
com.night.two"
build_night_whitelist_file
_t_eq "com.night.one com.night.two" \
"$(tr '\n' ' ' <"${STATE_DIR}/night_whitelist.txt" | sed 's/ $//')" \
"night whitelist parses like the day list"
case "$(_log)" in
*"WARN: night whitelist suspiciously small"*) _t_pass "a short night whitelist warns" ;;
*) _t_fail "a night whitelist under its floor must warn" ;;
esac
# The two lists have different floors (30 and 10), so a list that is fine for
# one must not be judged by the other's threshold.
_reset_dev
NIGHT_WHITELIST="$(for i in $(seq 1 12); do printf 'com.night%02d\n' "$i"; done)"
build_night_whitelist_file
_t_eq "12" "$(printf '%s\n' "${NIGHT_WHITELIST}" | wc -l)" "12 night entries were seeded"
if _log | grep -q "WARN: night whitelist"; then
_t_fail "12 night entries is above the night floor and must not warn"
else
_t_pass "the night whitelist uses its own lower floor"
fi
# --- build_sysprotect_file / build_blocked_sys_file ------------------------
_reset_dev
SYSTEM_NEVER_DISABLE="com.android.
com.google.android.gms"
build_sysprotect_file
_t_eq "2" "$(wc -l <"${STATE_DIR}/sysprotect.txt")" "sysprotect writes every prefix"
BLOCKED_SYSTEM_APPS="com.android.browser
# not this one
com.android.chrome"
build_blocked_sys_file
_t_eq "com.android.browser com.android.chrome" \
"$(tr '\n' ' ' <"${STATE_DIR}/blocked_sys.txt" | sed 's/ $//')" \
"blocked-system list drops comments"
# Read the two seeds back so each has a reader in this file (SC2034); the
# subject is what actually consumes them.
_t_eq "2-3" "$(printf '%s\n' "${SYSTEM_NEVER_DISABLE}" | wc -l)-$(printf '%s\n' "${BLOCKED_SYSTEM_APPS}" | wc -l)" \
"the seeded sysprotect and blocked-system lists held 2 and 3 lines"
# --- refresh_default_handlers ----------------------------------------------
_reset_dev
printf 'Activity Resolver Table:\ncom.launcher.home/.MainActivity\n' >"${DEV}/cmd_out"
printf 'com.dialer.app\n' >"${DEV}/setting_get"
printf 'com.sms.app\n' >"${DEV}/setting_sms_default_application"
printf 'com.ime.app/.Service\n' >"${DEV}/setting_default_input_method"
refresh_default_handlers
handlers="$(cat "${STATE_DIR}/default_handlers.txt")"
case "${handlers}" in
*com.launcher.home*) _t_pass "default handlers include the launcher" ;;
*) _t_fail "default handlers must include the launcher" ;;
esac
# The IME is the load-bearing one: pm disable-user on the active keyboard
# survives a reboot, so a 1am reboot would leave no way to type a recovery
# command. It has to be protected day and night.
case "${handlers}" in
*com.ime.app*) _t_pass "default handlers include the active keyboard" ;;
*) _t_fail "the active IME must be protected or a reboot locks you out" ;;
esac
# Only the package part of the IME component is usable as a package name.
if grep -q "com.ime.app/" "${STATE_DIR}/default_handlers.txt"; then
_t_fail "the IME component suffix leaked into the package list"
else
_t_pass "the IME is recorded as a package, not a component"
fi
# A literal "null" from the settings provider is an absent value, not a
# package, and must never be written as one.
_reset_dev
printf 'Activity Resolver Table:\ncom.launcher.home/.MainActivity\n' >"${DEV}/cmd_out"
printf 'null\n' >"${DEV}/setting_sms_default_application"
printf 'null\n' >"${DEV}/setting_default_input_method"
refresh_default_handlers
if grep -qx "null" "${STATE_DIR}/default_handlers.txt"; then
_t_fail "a literal null was recorded as a default handler"
else
_t_pass "a null settings value is not recorded as a handler"
fi
if [[ -f "${STATE_DIR}/default_handlers.txt.tmp" ]]; then
_t_fail "refresh left its temp file behind"
else
_t_pass "refresh cleans up its temp file"
fi
# The browser is tracked separately, because during curfew the whole point is
# to disable browsers — the default-handler guard must not resurrect them.
if [[ -f "${STATE_DIR}/default_browser.txt" ]]; then
_t_pass "the default browser is tracked in its own file"
else
_t_fail "the default browser must be tracked separately"
fi
# --- is_default_handler -----------------------------------------------------
_reset_dev
printf 'com.launcher.home\ncom.ime.app\n' >"${STATE_DIR}/default_handlers.txt"
if is_default_handler "com.launcher.home"; then
_t_pass "a listed package is recognised as a default handler"
else
_t_fail "a listed package must be recognised"
fi
if ! is_default_handler "com.other.app"; then
_t_pass "an unlisted package is not a default handler"
else
_t_fail "an unlisted package must not be recognised"
fi
# Exact match only: a prefix would protect far more than intended.
if ! is_default_handler "com.launcher"; then
_t_pass "default-handler matching is exact, not by prefix"
else
_t_fail "a prefix must not match a default handler"
fi