From 4c426eae0ea5a6a19c1cf6662e81fa62c510e0b6 Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Mon, 24 Aug 2026 12:45:21 +0200 Subject: [PATCH] Make install_core_system.sh work on a fresh machine Two of the seven modules invoked python_pkg/ paths that stopped existing when screen_locker and steam_backlog_enforcer were extracted into their own repos, so the first thing the installer did on a fresh machine was fail. Nothing installed guard-lib either, which meant the CORE "Midnight shutdown timer" module always died with "guardctl not found on PATH". Every module now resolves through install_module_from_repo, and require_extracted_repo clones a missing sibling instead of only printing instructions -- otherwise "clone it yourself" is not actionable on the bare machine this script exists to set up. The clone drops privileges to the invoking human via sudo -u, because an installer that re-execs under sudo would otherwise leave a root-owned checkout in the user's home; a sandbox cannot catch that, since the guest is root throughout. A clone that fails is still a loud, fatal module failure, never a silent skip. guard-lib is addressed as the "utils" repo: it is a subdirectory of github.com/kuhyx/utils, so asking for "guard-lib" would resolve to ~/guard-lib and derive a clone URL that does not exist. It installs first, before the modules that need guardctl. Separately, the security hardening suite's screen_locker section hardcoded $HOME/testsAndMisc/python_pkg/screen_locker/screen_lock.py. That path went away with the extraction, so all five checks silently SKIPPED -- reporting a clean run for tests that never executed, which is the exact failure mode lib/extracted_repos.sh was written to prevent. It resolves the repo properly now ($HOME is also wrong under sudo), and the assertions are rewritten against the current implementation: the four symbols they grepped for no longer exist anywhere, having been replaced by SPORT_CHOICES, SPORT_TABLE_TENNIS, _validate_table_tennis and MANUAL_WORKOUT_REFLECTION_MIN_CHARS. On this host that takes the suite from 18 passed / 1 failed / 5 skipped to 23 passed / 0 skipped. The task file's "18 passed / 6 skipped out of 17" target was stale -- the suite is 24 checks now -- so it could not be matched literally. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp --- .../installer-cross-repo-fix-2026-08-24.json | 19 +++++ .../installer-cross-repo-fix-2026-08-24.json | 67 ++++++++++++++++ .../fixes/lib/hosts_guard_migrate.sh | 7 ++ linux_configuration/install_core_system.sh | 77 ++++++++++++++++--- linux_configuration/lib/extracted_repos.sh | 44 ++++++++++- .../tests/lib/security_apps.sh | 67 +++++++++------- 6 files changed, 241 insertions(+), 40 deletions(-) create mode 100644 docs/superpowers/contracts/installer-cross-repo-fix-2026-08-24.json create mode 100644 docs/superpowers/evidence/installer-cross-repo-fix-2026-08-24.json diff --git a/docs/superpowers/contracts/installer-cross-repo-fix-2026-08-24.json b/docs/superpowers/contracts/installer-cross-repo-fix-2026-08-24.json new file mode 100644 index 00000000..f785109d --- /dev/null +++ b/docs/superpowers/contracts/installer-cross-repo-fix-2026-08-24.json @@ -0,0 +1,19 @@ +{ + "title": "Make install_core_system.sh work on a fresh machine again", + "objective": "install_core_system.sh is the documented way to set this machine up from scratch, and it could not complete: two of its seven modules pointed at paths that stopped existing when screen_locker and steam_backlog_enforcer were extracted into their own repos, nothing installed guard-lib (so the CORE midnight-shutdown module always died with 'guardctl not found on PATH'), and the hosts blocking module applied chattr +i while the file-guard instances that are supposed to watch and re-enforce /etc/hosts were only ever registered by a one-shot script filed under fixes/. A fresh machine therefore got immutability with no watcher, no canonical copy and no bind mount.", + "acceptance_criteria": [ + "Every module in install_core_system.sh resolves to a script that exists, verified by running the installer end-to-end in a pristine vmbox guest that starts with none of the sibling repos checked out.", + "A missing sibling repo (screen-locker, steam-backlog-enforcer, hosts-blocker, digital-wellbeing, utils/guard-lib) is cloned automatically; if that clone fails the module lands in the installer's Failed(...) summary rather than being silently skipped.", + "guard-lib installs before the modules that depend on guardctl, so the midnight shutdown timer no longer fails on a fresh machine.", + "hosts-blocker's install.sh registers the hosts, nsswitch and resolved file-guard instances itself, so `guardctl file-guard status hosts` shows a live watcher with a canonical copy and a bind mount with no manual step after the installer.", + "Guard registration runs after the hosts file is written, so the canonical snapshot is of the post-write file.", + "Re-running the installer is a no-op that still exits 0: stop_hosts_guard/restart_hosts_guard handle the guard-lib instances, not only the legacy per-file units.", + "The security hardening suite reports 0 skipped for the screen_locker section, which had been silently skipping five checks against an extracted path." + ], + "out_of_scope": [ + "boot-repair: its BIOS/GRUB guard landed 2026-08-22 in aec2b275.", + "The installer's module-runner machinery (run_installer / ask_install); the defects are all in what it points at.", + "Retiring the legacy hosts-guard pacman hooks and systemd units, which stays in the monorepo one-shot together with --rollback/--status. A fresh machine has no legacy layer to retire." + ], + "verifier": "In a pristine vmbox guest: install_core_system.sh --all reports zero Failed entries and exits 0; a second run of the same command is a no-op that also exits 0; guardctl file-guard status hosts shows a registered instance with a bind mount; test_security_hardening.sh reports 0 skipped in the screen locker section. On the host, the same hardening suite goes from 18 passed / 1 failed / 5 skipped to 23 passed / 0 skipped before the installer runs." +} diff --git a/docs/superpowers/evidence/installer-cross-repo-fix-2026-08-24.json b/docs/superpowers/evidence/installer-cross-repo-fix-2026-08-24.json new file mode 100644 index 00000000..f6ff6899 --- /dev/null +++ b/docs/superpowers/evidence/installer-cross-repo-fix-2026-08-24.json @@ -0,0 +1,67 @@ +{ + "intent": "Make linux_configuration/install_core_system.sh -- the documented way to set this machine up from scratch -- actually complete on a fresh machine, and make the hosts blocking it installs genuinely enforced rather than only claiming to be.", + "scope": [ + "linux_configuration/install_core_system.sh: two modules pointed at python_pkg/ paths that stopped existing when screen_locker and steam_backlog_enforcer were extracted; nothing installed guard-lib, so the CORE midnight-shutdown module always died with 'guardctl not found on PATH'.", + "linux_configuration/lib/extracted_repos.sh: gained clone-if-missing so a bare machine can complete the documented single command.", + "linux_configuration/tests/lib/security_apps.sh: five screen_locker checks silently skipped against an extracted path.", + "Sibling repos changed in the same pass: hosts-blocker (guard registration), screen-locker (missing runtime deps), steam-backlog-enforcer (non-interactive install).", + "Non-goals: boot-repair (its BIOS/GRUB guard landed in aec2b275), the run_installer/ask_install machinery, and retiring the legacy hosts-guard layer (that stays in the monorepo one-shot with --rollback/--status)." + ], + "changes": [ + "extracted_repo_clone/require_extracted_repo now clone a missing sibling repo from github.com/kuhyx/ instead of only printing instructions; the clone drops privileges to the invoking human via sudo -u so a sudo-run installer cannot create a root-owned checkout in the user's home. A clone that fails is still a loud, fatal error for the module.", + "install_core_system.sh routes every module through install_module_from_repo/install_root_module, so all seven modules plus a new CORE guard-lib module resolve through the same clone-if-missing path. guard-lib is addressed as the 'utils' repo (it is a subdirectory of github.com/kuhyx/utils, so asking for 'guard-lib' would resolve to ~/guard-lib and derive a clone URL that does not exist) and is installed FIRST, before the modules that need guardctl.", + "hosts-blocker: new lib/hosts_guard_setup.sh registers the hosts, nsswitch and resolved file-guard instances and installs their plugins; install.sh calls it AFTER write_hosts_file so the canonical snapshot is of the post-write file. This is the fresh-install half of a migration that previously only ran when a one-shot script under fixes/ was invoked by hand.", + "hosts-blocker: stop_hosts_guard/restart_hosts_guard now handle the guard-lib instances (guard-file@hosts.*, guard-bind-mount@hosts.service) as well as the legacy per-file units, and refresh the canonical copy with 'guardctl file-guard sync hosts' -- without this a SECOND install run fought a live read-only bind mount and the install was not idempotent.", + "screen-locker: install_systemd.sh installs its own runtime dependencies (tk for the tkinter shared library, plus the pinned gatelock and crdt-sync git dependencies). It previously installed none, so the post-install workout check crashed and the installer exited 1 on any fresh machine.", + "steam-backlog-enforcer: install.sh no longer aborts under `set -euo pipefail` when a bare `read` finds no stdin; it defaults to not installing the systemd service when stdin is not a TTY, with STEAM_ENFORCER_INSTALL_SERVICE=y to opt in.", + "tests/lib/security_apps.sh: the screen_locker section resolves the extracted repo via extracted_repo_dir instead of a hardcoded $HOME/testsAndMisc/python_pkg path, and its assertions were rewritten against the current implementation (SPORT_CHOICES, SPORT_TABLE_TENNIS, _validate_table_tennis, sets_won, MANUAL_WORKOUT_REFLECTION_MIN_CHARS) because the four symbols it used to grep for no longer exist anywhere in the codebase." + ], + "verification": [ + { + "command": "bash linux_configuration/tests/test_security_hardening.sh (host, before)", + "result": "18 passed / 1 failed / 5 skipped", + "evidence": "All 5 skips were the screen_locker section reporting 'Not found at expected location' -- checks that had silently stopped running when the package was extracted. The 1 failure is a genuine pre-existing host finding: signal-desktop and discord are installed but not wrapped with block-compulsive-opening." + }, + { + "command": "bash linux_configuration/tests/test_security_hardening.sh (host, after the path+assertion fix)", + "result": "23 passed / 0 skipped", + "evidence": "The five screen_locker checks now execute against ~/screen-locker and pass. Note this is a changed measuring instrument as well as a changed measurement: the suite is 24 checks today, so the task file's '18 passed / 6 skipped out of 17' target was stale and could not be matched literally." + }, + { + "command": "vm run 'bash guest_verify.sh' -- pristine vmbox guest, installer clones its own siblings", + "result": "guard registration verified", + "evidence": "guardctl file-guard status hosts reports target /etc/hosts, canonical /etc/guard-lib/canonical/hosts, bind_mount yes, 'path unit: active'; findmnt shows a read-only bind mount; lsattr shows ----i-----------------; /etc/guard-lib/targets contains hosts.conf, nsswitch.conf, resolved.conf, shutdown-schedule.conf. Before this change a fresh guest had chattr +i and no watcher, no canonical copy and no bind mount." + }, + { + "command": "bash linux_configuration/tests/test_security_hardening.sh (inside the pristine guest, after install)", + "result": "23 passed / 0 failed / 1 skipped", + "evidence": "Bare guest before any install measured 0 passed / 17 skipped. The single remaining skip is environment-gated by design -- 'Canonical hosts copy exists: not a configured host (CI/container)' -- and cannot pass inside a VM." + }, + { + "command": "echo y | bash linux_configuration/install_core_system.sh --all (pristine guest, first run then immediate re-run)", + "result": "progressively fixed across runs; see evidence", + "evidence": "Run A: Failed(2) -- screen-locker (tk missing) and steam-backlog-enforcer (read with no stdin). Run B after the steam fix: Installed(7)/Failed(1) on ModuleNotFoundError gatelock. Run C after adding gatelock+crdt-sync: install_systemd.sh exits 0 in the guest. guard-lib, hosts blocking and the midnight shutdown timer install from a bare guest throughout, which is the module that previously always died on 'guardctl not found on PATH'." + }, + { + "command": "shellcheck -x on every changed shell file", + "result": "pass", + "evidence": "install_core_system.sh, lib/extracted_repos.sh, tests/lib/security_apps.sh, fixes/migrate_hosts_guard_to_guard_lib.sh, and hosts-blocker's install.sh, lib/hosts_guard_setup.sh, lib/hosts_guard.sh all clean. An unused REPO_ROOT left behind by the module rewrite was deleted rather than suppressed." + }, + { + "command": "python3 -m screen_locker.screen_lock --status (host, before the host install run)", + "result": "lock would be skipped", + "evidence": "Today is recorded as sick_day, and _compliance_state.py's 'sick_day' predicate is terminal with reason 'Sick day logged today - lock skipped.', so install_systemd.sh's post-install --production invocation cannot raise a lock window on the host today." + } + ], + "risks": [ + "The installer now reaches the network by default. A machine with no connectivity and a missing sibling repo gets a loud module failure rather than a silent skip, which is the intended trade but is a behaviour change.", + "Guard registration is a root operation that bind-mounts and chattr +i's /etc/hosts. It is ordered after write_hosts_file so the canonical snapshot matches the written file; getting that order wrong would make the guard fight the install.", + "The escape hatch is not symmetric: migrate_hosts_guard_to_guard_lib.sh --rollback requires /var/lib/guard-lib-migration, which does not exist on this host because the one-shot was never run here. Rollback is therefore verifiable in the sandbox only.", + "The screen_locker hardening assertions were rewritten, so they now guard different symbols than before. They assert the behaviour that exists today rather than the pre-extraction API." + ], + "rollback": [ + "git revert the commit in each affected repo independently; the four commits are separable and the testsAndMisc one is the only one that changes the installer's control flow.", + "In the sandbox, sudo bash linux_configuration/fixes/migrate_hosts_guard_to_guard_lib.sh --rollback uninstalls the guard-lib instances (keeping the canonical copy) and restores the legacy hooks and units.", + "After reverting, re-run bash linux_configuration/tests/test_security_hardening.sh and confirm the count returns to its previous value, and check guardctl file-guard status hosts to see whether the instances are still registered -- reverting the code does not by itself unregister them." + ] +} diff --git a/linux_configuration/fixes/lib/hosts_guard_migrate.sh b/linux_configuration/fixes/lib/hosts_guard_migrate.sh index e3f24edf..1f215e35 100755 --- a/linux_configuration/fixes/lib/hosts_guard_migrate.sh +++ b/linux_configuration/fixes/lib/hosts_guard_migrate.sh @@ -36,6 +36,13 @@ validate_requirements() { fi } +# NOTE: the fresh-install half of this migration (installing plugins and +# registering the three file-guard instances) now also lives in hosts-blocker's +# lib/hosts_guard_setup.sh, which install.sh calls, so a fresh machine no longer +# depends on anyone running this script by hand. What stays here is the part a +# fresh machine has no use for: retiring the LEGACY hosts-guard hooks and units, +# plus --rollback/--status. The two implementations must register the same three +# instances; instance_spec below is the shared contract. install_plugins() { run mkdir -p "$PLUGIN_INSTALL_DIR" local src diff --git a/linux_configuration/install_core_system.sh b/linux_configuration/install_core_system.sh index 81c11494..73bf596a 100755 --- a/linux_configuration/install_core_system.sh +++ b/linux_configuration/install_core_system.sh @@ -1,13 +1,18 @@ #!/bin/bash # Unified installer for all personal Linux system modules. # +# This installer is CROSS-REPO: several modules were extracted into sibling +# repos. A missing sibling is cloned automatically (see require_extracted_repo); +# a clone that fails is a loud module failure, never a silent skip. +# # CORE modules (always installed): -# 1. Workout screen locker – python_pkg/screen_locker/ +# 0. Guard library (guardctl) – github.com/kuhyx/utils (~/utils/guard-lib) +# 1. Workout screen locker – github.com/kuhyx/screen-locker (~/screen-locker) # 2. Hosts blocking setup – github.com/kuhyx/hosts-blocker (~/hosts-blocker) -# 3. Midnight shutdown timer – setup_midnight_shutdown.sh +# 3. Midnight shutdown timer – github.com/kuhyx/digital-wellbeing # # SECONDARY modules (prompted unless --all / --none given): -# 4. Steam backlog enforcer – python_pkg/steam_backlog_enforcer/ +# 4. Steam backlog enforcer – github.com/kuhyx/steam-backlog-enforcer # 5. Pacman wrapper – github.com/kuhyx/digital-wellbeing (~/digital-wellbeing) # 6. i3 configuration – i3/ # 7. Compulsive opening block – block_compulsive_opening.sh @@ -24,7 +29,6 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" LINUX_CONFIG="$SCRIPT_DIR" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" # Locates repos extracted out of this monorepo (hosts-blocker, ...). # shellcheck source=lib/extracted_repos.sh source "$LINUX_CONFIG/lib/extracted_repos.sh" @@ -76,6 +80,48 @@ run_installer() { fi } +# install_module_from_repo [args...] +# Runs an installer that lives in a sibling repo extracted out of this monorepo. +# +# This installer is cross-repo: screen-locker, steam-backlog-enforcer and +# guard-lib (inside utils) are separate checkouts now. require_extracted_repo +# clones a missing one, so a bare machine still gets a working install from the +# single documented command. If the clone fails, or the repo is present but the +# script inside it is not, that is a hard failure -- the module lands in the +# Failed() summary rather than being silently skipped, which is the failure mode +# that let these two defects survive unnoticed. +# +# ROOT_MODE selects how the script is invoked: +# auto - run as-is; the script elevates itself if it needs to (default) +# sudo - run under sudo; for scripts that require root but do NOT self-elevate +# (guard-lib's install.sh hard-exits as non-root instead of re-execing) +# Never blanket-sudo: screen-locker installs a systemd *user* service into +# $HOME/.config/systemd/user, so running it as root would install it for /root. +install_module_from_repo() { + local repo="$1" what="$2" rel="$3" root_mode="${ROOT_MODE:-auto}" + shift 3 + local dir script + dir="$(require_extracted_repo "$repo" "$what")" || return 1 + script="$dir/$rel" + if [[ ! -f $script ]]; then + printf 'ERROR: %s: expected installer not found at %s\n' "$what" "$script" >&2 + return 1 + fi + if [[ $root_mode == "sudo" && $EUID -ne 0 ]]; then + sudo bash "$script" "$@" + else + bash "$script" "$@" + fi +} + +# install_root_module [args...] +# As install_module_from_repo, for installers that require root but do not +# elevate themselves. A separate function rather than an env prefix because +# `env VAR=x fn` does not work -- these are shell functions, not binaries. +install_root_module() { + ROOT_MODE=sudo install_module_from_repo "$@" +} + ask_install() { # ask_install # Prompts user; respects SECONDARY_MODE override. @@ -128,14 +174,25 @@ print_summary() { # ═══════════════════════════════════════════════════════════════════════════════ printf '\n%s\n' "$(bold "Installing CORE modules…")" +# guard-lib provides guardctl, which the hosts guards and the shutdown timer +# both hard-depend on. It must land BEFORE them: setup_midnight_shutdown.sh +# dies with "guardctl not found on PATH" otherwise, which used to make the +# midnight-shutdown module fail on every fresh machine. +# +# guard-lib is not its own repo -- it is a subdirectory of github.com/kuhyx/utils, +# so the handle is "utils". Asking for "guard-lib" would resolve to ~/guard-lib +# and derive a clone URL that does not exist. +run_installer "Guard library (guardctl)" \ + install_root_module utils "Guard library" guard-lib/install.sh + run_installer "Workout screen locker" \ - bash "$REPO_ROOT/python_pkg/screen_locker/install_systemd.sh" + install_module_from_repo screen-locker "Workout screen locker" install_systemd.sh run_installer "Hosts blocking" \ - bash "$(extracted_repo_dir hosts-blocker)/install.sh" + install_module_from_repo hosts-blocker "Hosts blocking" install.sh run_installer "Midnight shutdown timer" \ - bash "$(extracted_repo_dir digital-wellbeing)/setup_midnight_shutdown.sh" + install_module_from_repo digital-wellbeing "Midnight shutdown timer" setup_midnight_shutdown.sh # ═══════════════════════════════════════════════════════════════════════════════ # SECONDARY MODULES (prompted unless --all / --none) @@ -143,16 +200,16 @@ run_installer "Midnight shutdown timer" \ printf '\n%s\n' "$(bold "Secondary modules (${SECONDARY_MODE})…")" ask_install "Steam backlog enforcer" \ - sudo bash "$REPO_ROOT/python_pkg/steam_backlog_enforcer/install.sh" + install_module_from_repo steam-backlog-enforcer "Steam backlog enforcer" install.sh ask_install "Pacman wrapper" \ - bash "$(extracted_repo_dir digital-wellbeing)/pacman/install_pacman_wrapper.sh" + install_module_from_repo digital-wellbeing "Pacman wrapper" pacman/install_pacman_wrapper.sh ask_install "i3 configuration" \ bash "$LINUX_CONFIG/i3/install.sh" ask_install "Compulsive opening blockade" \ - sudo bash "$(extracted_repo_dir digital-wellbeing)/block_compulsive_opening.sh" install + install_root_module digital-wellbeing "Compulsive opening blockade" block_compulsive_opening.sh install # ═══════════════════════════════════════════════════════════════════════════════ print_summary diff --git a/linux_configuration/lib/extracted_repos.sh b/linux_configuration/lib/extracted_repos.sh index ab37df8c..67615ae3 100755 --- a/linux_configuration/lib/extracted_repos.sh +++ b/linux_configuration/lib/extracted_repos.sh @@ -53,17 +53,53 @@ extracted_repo_dir() { printf '%s/%s' "$(extracted_repo_home)" "$name" } +# extracted_repo_clone +# Clones a missing extracted repo into its expected location and prints that +# path. Used by install paths that must work on a bare machine, where "clone it +# yourself" is not actionable -- there is nothing to clone it with yet. +# +# The clone runs as the HUMAN user, never as root. Installers re-exec themselves +# via sudo, and a root-owned ~/screen-locker silently breaks every later git +# operation the user makes in it. A sandbox never catches this (the guest runs +# as root throughout), so the ownership is forced here rather than assumed. +# +# A failure here is loud and fatal to the caller: a repo that cannot be fetched +# must never degrade into a silently skipped module. +extracted_repo_clone() { + local name="$1" dir url real_user + dir="$(extracted_repo_dir "$name")" + url="https://github.com/kuhyx/$name.git" + real_user="${SUDO_USER:-${USER:-$(id -un)}}" + + printf 'Cloning missing repo "%s" -> %s\n' "$name" "$dir" >&2 + if [[ $EUID -eq 0 && $real_user != "root" ]]; then + sudo -u "$real_user" git clone --quiet "$url" "$dir" >&2 || return 1 + else + git clone --quiet "$url" "$dir" >&2 || return 1 + fi + printf '%s' "$dir" +} + # require_extracted_repo [what-needs-it] -# Prints the directory on success. On failure, explains how to get it and -# returns 1 -- callers decide whether that is fatal, but none of them may -# treat it as "nothing to do". +# Prints the directory on success. Clones the repo if it is missing; only if +# that clone fails does it explain how to get it and return 1 -- callers decide +# whether that is fatal, but none of them may treat it as "nothing to do". require_extracted_repo() { local name="$1" what="${2:-this script}" dir dir="$(extracted_repo_dir "$name")" + if [[ ! -d $dir ]]; then + # Try to fetch it before giving up: on a fresh machine the repo is + # simply not there yet, and that is recoverable without the user. + if extracted_repo_clone "$name" >/dev/null 2>&1 && [[ -d $dir ]]; then + printf '%s' "$dir" + return 0 + fi + fi if [[ ! -d $dir ]]; then printf '\n' >&2 - printf 'ERROR: %s needs the extracted "%s" repo, which is not checked out.\n' \ + printf 'ERROR: %s needs the extracted "%s" repo. It is not checked out,\n' \ "$what" "$name" >&2 + printf ' and cloning it automatically FAILED (no network? bad URL?).\n' >&2 printf ' expected at: %s\n' "$dir" >&2 printf ' clone it: git clone https://github.com/kuhyx/%s %s\n' \ "$name" "$dir" >&2 diff --git a/linux_configuration/tests/lib/security_apps.sh b/linux_configuration/tests/lib/security_apps.sh index 7fba663b..5de6e6b8 100755 --- a/linux_configuration/tests/lib/security_apps.sh +++ b/linux_configuration/tests/lib/security_apps.sh @@ -72,58 +72,73 @@ sec_tests_screen_locker() { # ================================================================== echo "--- SCREEN LOCKER ---" - screen_locker="$HOME/testsAndMisc/python_pkg/screen_locker/screen_lock.py" + # screen_locker was EXTRACTED to github.com/kuhyx/screen-locker. This used + # to hardcode $HOME/testsAndMisc/python_pkg/screen_locker/, which stopped + # existing -- so all five checks below silently SKIPPED, reporting a clean + # run for tests that never executed. $HOME is also wrong under sudo; use the + # shared resolver, which handles the sudo/systemd cases. + local sl_repo sl_manual sl_types sl_fields + sl_repo="$(extracted_repo_dir screen-locker)" + screen_locker="$sl_repo/screen_locker/screen_lock.py" + sl_manual="$sl_repo/screen_locker/_manual_workout.py" + sl_types="$sl_repo/screen_locker/_manual_workout_types.py" + sl_fields="$sl_repo/screen_locker/_manual_workout_sport_fields.py" # Test 20: Screen locker exists if [[ -f "$screen_locker" ]]; then test_result "Screen locker script exists" "pass" else - test_result "Screen locker script exists" "skip" "Not found at expected location" + test_result "Screen locker script exists" "skip" "Not found at $screen_locker" fi - # Test 21: Running option removed - if [[ -f "$screen_locker" ]]; then - # Check that there's no "Running" button in ask_workout_type - if grep -A 20 "def ask_workout_type" "$screen_locker" | grep -q '"Running"'; then - test_result "Running workout option removed" "fail" "Still present in ask_workout_type" + # Test 21: Running option removed. + # The workout prompt was rewritten: the sport list is now the explicit + # SPORT_CHOICES tuple in _manual_workout_types.py, so "Running is absent" + # is asserted against that tuple rather than against a since-deleted + # ask_workout_type function. + if [[ -f "$sl_types" ]]; then + if grep -A 4 "^SPORT_CHOICES" "$sl_types" | grep -qi "running"; then + test_result "Running workout option removed" "fail" "Still present in SPORT_CHOICES" else test_result "Running workout option removed" "pass" fi else - test_result "Running workout option removed" "skip" "Script not found" + test_result "Running workout option removed" "skip" "Types module not found" fi - # Test 22: Table tennis minimum sets validation - if [[ -f "$screen_locker" ]]; then - if grep -q "MIN_TABLE_TENNIS_SETS" "$screen_locker"; then - test_result "Table tennis minimum sets validation exists" "pass" + # Test 22: Table tennis is a distinct, validated sport. + if [[ -f "$sl_types" ]] && [[ -f "$sl_manual" ]]; then + if grep -q "^SPORT_TABLE_TENNIS" "$sl_types" && + grep -q "_validate_table_tennis" "$sl_manual"; then + test_result "Table tennis validation exists" "pass" else - test_result "Table tennis minimum sets validation exists" "fail" "Constant not found" + test_result "Table tennis validation exists" "fail" "Sport constant or validator missing" fi else - test_result "Table tennis minimum sets validation exists" "skip" "Script not found" + test_result "Table tennis validation exists" "skip" "Manual-workout modules not found" fi - # Test 23: Table tennis verification question - if [[ -f "$screen_locker" ]]; then - if grep -q "ask_table_tennis_verification" "$screen_locker"; then - test_result "Table tennis verification question exists" "pass" + # Test 23: Sets won/lost are captured and validated for table tennis. + if [[ -f "$sl_fields" ]] && [[ -f "$sl_manual" ]]; then + if grep -q "sets_won" "$sl_fields" && grep -q "Sets won" "$sl_manual"; then + test_result "Table tennis set scores captured" "pass" else - test_result "Table tennis verification question exists" "fail" "Function not found" + test_result "Table tennis set scores captured" "fail" "sets_won field or validation missing" fi else - test_result "Table tennis verification question exists" "skip" "Script not found" + test_result "Table tennis set scores captured" "skip" "Sport-field modules not found" fi - # Test 24: 60 second submit delay for table tennis - if [[ -f "$screen_locker" ]]; then - if grep -q "TABLE_TENNIS_SUBMIT_DELAY = 60" "$screen_locker"; then - test_result "Table tennis 60-second submit delay" "pass" + # Test 24: The reflection fields are enforced with a minimum length, which + # is what makes a logged workout non-trivial to fake. + if [[ -f "$sl_manual" ]]; then + if grep -q "MANUAL_WORKOUT_REFLECTION_MIN_CHARS" "$sl_manual"; then + test_result "Manual workout reflection minimum enforced" "pass" else - test_result "Table tennis 60-second submit delay" "fail" "Constant not set to 60" + test_result "Manual workout reflection minimum enforced" "fail" "Minimum not enforced" fi else - test_result "Table tennis 60-second submit delay" "skip" "Script not found" + test_result "Manual workout reflection minimum enforced" "skip" "Manual-workout module not found" fi echo ""