From 87ff0b77cdc1d8a66a063cfe471dab78d1966dc4 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Thu, 20 Aug 2026 00:17:09 -0700 Subject: feat(post-rebuild-check): flag a stopped idle daemon and a read-only remote Both are states where the machine looks finished and isn't, which is the whole point of this script. Check 7 asks whether hypridle is running. Nothing else notices when it isn't. Idle lock and suspend stop happening, and the laptop runs until its battery is gone. That's how velox reset its RTC on 2026-08-19, which is what dropped it into check 6's clock and DNS deadlock. The check asks whether the daemon is alive rather than why it might not be, so a crash and a stale caffeine surface alike. It's gated on hypridle being installed, since only Hyprland machines get it. Check 8 asks whether the working repos can push. The installer clones them from the read-only https endpoint. That's right for someone installing archsetup with no key on my server, and wrong for my own machines. Nothing about the tree shows it. velox's dotfiles remote sat that way for four days and announced itself as a 403. Only my own read-only endpoint is flagged. An https remote elsewhere may push fine through a credential helper, and guessing about hosts this machine doesn't own would stand noise in front of real findings. --- scripts/post-rebuild-check | 124 ++++++++++-- .../post-rebuild-check/test_post_rebuild_check.py | 213 +++++++++++++++++++-- 2 files changed, 315 insertions(+), 22 deletions(-) diff --git a/scripts/post-rebuild-check b/scripts/post-rebuild-check index 24e99a4..2013b26 100755 --- a/scripts/post-rebuild-check +++ b/scripts/post-rebuild-check @@ -1,6 +1,6 @@ #!/bin/sh # SPDX-License-Identifier: GPL-3.0-or-later -# post-rebuild-check - the five checks a rebuilt machine actually needs. +# post-rebuild-check - the eight checks a rebuilt machine actually needs. # # A rebuilt machine looks finished and isn't. Five gaps surfaced on velox # within two days of the 2026-08-13 reinstall, and three of them LOOKED @@ -25,6 +25,14 @@ # DoT/DNSSEC validation this machine's DNS runs on, so nothing # resolves -- including the NTP pool that would fix the clock; velox # deadlocked exactly this way 2026-08-19 and needed a second device) +# 7. hypridle installed but not running (nothing then triggers idle lock +# or suspend, so a laptop runs until its battery is gone -- which is +# how velox reset the RTC that caused check 6's deadlock in the first +# place; a caffeine remembered from an earlier boot is the known cause) +# 8. a working repo cloned from the read-only https endpoint (correct +# for a stranger with no key on the server, wrong for this machine, +# which finds out at the first push with a 403 -- velox's dotfiles +# remote sat that way for four days after its rebuild) # # The .gitignore rule in check 4 is what scopes it: a tooling path is only # expected where the project's own .gitignore names it, so a project that @@ -58,6 +66,10 @@ # the special value MISSING = no NTP daemon active # PRC_CHRONY_CONF path to chrony.conf (a fixture, under test) -- the # confdir it names is what decides which drop-ins count +# PRC_IDLE_DAEMON pgrep output for hypridle; "" = installed but not +# running, the special value MISSING = not installed +# PRC_REPO_REMOTES newline list of "path origin-url"; an empty URL +# means origin could not be read # PRC_SYSTEMCTL path to the systemctl binary (a fake, under test) # PRC_SYSTEMCTL_TIMEOUT seconds to allow each systemctl call (default 5) # @@ -69,10 +81,11 @@ usage() { cat <<'EOF' post-rebuild-check - verify a rebuilt machine is actually finished -Runs the six checks that caught velox's 2026-08 reinstall gaps: failed +Runs the eight checks that caught velox's 2026-08 reinstall gaps: failed units, present-but-inert user units, orphaned *.example configs, missing -per-project tooling state, the signal-cli registration, and whether time -sync can recover from a wrong clock without DNS. +per-project tooling state, the signal-cli registration, whether time sync +can recover from a wrong clock without DNS, whether anything still +triggers idle lock and suspend, and whether the working repos can push. Usage: post-rebuild-check [--help] @@ -94,6 +107,7 @@ CHECK_FINDINGS=0 FINDING_LINES="" signal_missing="" ntp_missing="" +idle_absent="" # Every systemctl call is bounded. A wedged user manager spins and answers # nothing -- seen live on velox 2026-08-17, where `is-enabled`, `cat`, and @@ -190,7 +204,7 @@ while IFS= read -r line; do unit=${line#*:} finding "$scope unit failed: $unit" done < "$STAGE" -report "check 1/6: failed units" +report "check 1/8: failed units" # --- 2. user unit files present but not enabled --------------------------- @@ -280,7 +294,7 @@ while read -r name state; do esac finding "unit file present but not enabled: $name ($state)" done < "$STAGE" -report "check 2/6: unit files" +report "check 2/8: unit files" # --- 3. *.example files whose real sibling is missing --------------------- @@ -325,7 +339,7 @@ while IFS= read -r root; do [ -e "${ex%.example}" ] || finding "example without its real file: $ex" done < "$WORK/examples" done < "$WORK/roots" -report "check 3/6: local files" +report "check 3/8: local files" # --- 4. gitignore-mode projects missing their tooling --------------------- @@ -379,7 +393,7 @@ todo.org todo\.org inbox inbox EOF done < "$WORK/projects" -report "check 4/6: project tooling" +report "check 4/8: project tooling" # --- 5. signal-cli registration ------------------------------------------- @@ -406,7 +420,7 @@ if [ "$signal_missing" = 1 ]; then elif [ -z "$signal_missing" ] && [ -z "$accounts" ]; then finding "no signal account registered — agent-text relays into this machine, so paging breaks for the whole fleet" fi -report "check 5/6: signal registration" +report "check 5/8: signal registration" # --- 6. NTP can recover a wrong clock without DNS ------------------------- # @@ -483,7 +497,95 @@ else finding "every NTP source is named by hostname — a wrong clock breaks DNS, so nothing can resolve them and the clock stays wrong" fi fi -report "check 6/6: NTP bootstrap" +report "check 6/8: NTP bootstrap" + +# --- 7. the idle daemon survives session start ---------------------------- +# +# A laptop that never sleeps has no symptom until the battery is gone, so +# nothing surfaces this without being asked. On velox 2026-08-19 hypridle +# started cleanly at 15:29:48 and `settings restore` killed it six seconds +# later, replaying a caffeine stored in an earlier boot. The machine ran +# 11h40m fully awake on battery, died when it flattened, and reset its RTC -- +# which took DNS down with it, the same deadlock check 6 exists for. The +# desktop looked correct throughout. +# +# Behavioural on purpose: this asks whether the daemon is alive, not why it +# might not be, so a stale caffeine, a crash, and a broken config all surface +# the same way. Gated on hypridle being installed, because that is what marks +# a machine as using it -- archsetup installs it only for Hyprland, so a +# headless or dwm box would otherwise report a finding on every run. + +if [ -n "${PRC_IDLE_DAEMON+set}" ]; then + idle_pids=$PRC_IDLE_DAEMON + if [ "$idle_pids" = "MISSING" ]; then + idle_pids="" + idle_absent=1 + fi +elif command -v hypridle >/dev/null 2>&1; then + # pgrep exits non-zero with no match, which is the not-running case rather + # than a probe failure, so the || keeps `set -e`-style callers out of it. + idle_pids=$(pgrep -x hypridle 2>/dev/null) || idle_pids="" +else + idle_pids="" + idle_absent=1 +fi + +if [ "$idle_absent" = 1 ]; then + : # hypridle is not part of this machine -- nothing to check +elif [ -z "$idle_pids" ]; then + finding "hypridle is installed but not running — nothing triggers idle lock or suspend, so this machine stays awake until its battery is gone; a caffeine remembered from an earlier boot is the known cause" +fi +report "check 7/8: idle daemon" + +# --- 8. working repos cloned from the read-only endpoint ------------------ +# +# archsetup clones the user's own archsetup and dotfiles from +# https://git.cjennings.net/..., which serves anonymous clones and refuses +# pushes. That default is correct for a stranger installing archsetup -- they +# have no key on the server -- and wrong for this machine, which has to push. +# ARCHSETUP_REPO / DOTFILES_REPO override it, but only where they are +# configured: a curl|bash install, or a rebuild from a stock ISO, takes the +# default straight back. +# +# Nothing about the tree shows it. The clone is complete and ordinary, and the +# machine finds out at the first push, with a 403 -- which is how velox's +# dotfiles remote was found on 2026-08-17, four days after its rebuild, by +# which time the same rebuild's shallow clone had already answered a +# credential-history question wrongly. +# +# Only the read-only endpoint is flagged. An https remote elsewhere may be +# perfectly pushable through a credential helper, and guessing about hosts +# this machine does not own would stand noise in front of the real findings. + +if [ -n "${PRC_REPO_REMOTES+set}" ]; then + repo_remotes=$PRC_REPO_REMOTES +else + repo_remotes="" + for repo in "$HOME/code/archsetup" "$HOME/.dotfiles"; do + # -e not -d: a worktree or submodule .git is a file naming the gitdir. + [ -e "$repo/.git" ] || continue + # A repo with no origin still gets a line, with an empty URL, so the + # loop below reports it rather than skipping it into a silent pass. + repo_url=$(git -C "$repo" remote get-url origin 2>/dev/null) + repo_remotes="${repo_remotes}${repo} ${repo_url} +" + done +fi + +stage "$repo_remotes" +while IFS= read -r repo_line; do + [ -n "$repo_line" ] || continue + repo_path=${repo_line%% *} + repo_url=${repo_line#"$repo_path"} + repo_url=${repo_url# } + case "$repo_url" in + "") + finding "$repo_path: origin could not be read — the remote was not checked" ;; + https://git.cjennings.net/*|https://cjennings.net/*) + finding "$repo_path: origin is the read-only endpoint ($repo_url) — git push returns 403; set the ssh form, or ARCHSETUP_REPO/DOTFILES_REPO before installing" ;; + esac +done < "$STAGE" +report "check 8/8: repo remotes" # --- summary -------------------------------------------------------------- @@ -491,5 +593,5 @@ if [ "$TOTAL_FINDINGS" -eq 0 ]; then echo "all checks clean" exit 0 fi -echo "$TOTAL_FINDINGS finding(s) across 6 checks" +echo "$TOTAL_FINDINGS finding(s) across 8 checks" exit 1 diff --git a/tests/post-rebuild-check/test_post_rebuild_check.py b/tests/post-rebuild-check/test_post_rebuild_check.py index bad337e..9902299 100644 --- a/tests/post-rebuild-check/test_post_rebuild_check.py +++ b/tests/post-rebuild-check/test_post_rebuild_check.py @@ -2,7 +2,7 @@ A rebuilt machine looks finished and isn't: on velox 2026-08-13 five gaps surfaced within two days, three of which LOOKED fine (a stowed unit file, an -enabled timer, a present git clone). The script runs the five checks from the +enabled timer, a present git clone). The script runs the checks from the post-rebuild task and turns each silent no-op into a visible line: 1. failed systemd units (user and system scope) @@ -10,6 +10,9 @@ post-rebuild task and turns each silent no-op into a visible line: 3. tracked *.example files whose real sibling is missing 4. gitignore-mode projects missing tooling paths their own .gitignore names 5. signal-cli holds no registered account + 6. every NTP source named by hostname (a wrong clock takes DNS with it) + 7. hypridle installed but not running (nothing triggers idle suspend) + 8. a working repo cloned read-only (push returns 403) Exit 0 with every check clean, 1 when any check found something. @@ -24,6 +27,10 @@ probe"): PRC_NTP_SOURCES newline list of configured NTP server addresses ("MISSING" = no NTP daemon active) the special value MISSING means the binary is absent + PRC_IDLE_DAEMON pgrep output for hypridle ("" = installed but not + running; "MISSING" = not installed on this machine) + PRC_REPO_REMOTES newline list of "pathorigin-url" for the + push-capability check ("" = no repos to check) Run from repo root: python3 -m unittest tests.post-rebuild-check.test_post_rebuild_check @@ -43,7 +50,8 @@ CHECK = os.path.join(REPO_ROOT, "scripts", "post-rebuild-check") def run_check(failed_units="", unit_states="", local_roots="", project_roots="", signal_accounts="+15045551234", - ntp_sources="162.159.200.1\npool.ntp.org"): + ntp_sources="162.159.200.1\npool.ntp.org", + idle_daemon="4242", repo_remotes=""): """Run the script with every probe stubbed; defaults are all-clean. Roots are newline-separated. Empty means "the seam is set and names no @@ -57,6 +65,8 @@ def run_check(failed_units="", unit_states="", local_roots="", env["PRC_PROJECT_ROOTS"] = project_roots env["PRC_SIGNAL_ACCOUNTS"] = signal_accounts env["PRC_NTP_SOURCES"] = ntp_sources + env["PRC_IDLE_DAEMON"] = idle_daemon + env["PRC_REPO_REMOTES"] = repo_remotes return subprocess.run( ["sh", CHECK], capture_output=True, text=True, timeout=30, env=env, ) @@ -76,7 +86,7 @@ class NtpBootstrap(unittest.TestCase): def test_an_ip_addressed_source_is_clean(self): r = run_check(ntp_sources="162.159.200.1\npool.ntp.org") - self.assertIn("check 6/6: NTP bootstrap — ok", r.stdout) + self.assertIn("check 6/8: NTP bootstrap — ok", r.stdout) self.assertEqual(r.returncode, 0, r.stdout) def test_all_hostname_sources_is_a_finding(self): @@ -88,7 +98,7 @@ class NtpBootstrap(unittest.TestCase): def test_an_ipv6_addressed_source_counts(self): r = run_check(ntp_sources="2606:4700:f1::1") - self.assertIn("check 6/6: NTP bootstrap — ok", r.stdout) + self.assertIn("check 6/8: NTP bootstrap — ok", r.stdout) # --- Boundary cases ------------------------------------------------- @@ -96,11 +106,11 @@ class NtpBootstrap(unittest.TestCase): # Order must not matter; the property is "at least one", and the # drop-in that carries it is read after the main config. r = run_check(ntp_sources="a.pool.ntp.org\nb.pool.ntp.org\n162.159.200.1") - self.assertIn("check 6/6: NTP bootstrap — ok", r.stdout) + self.assertIn("check 6/8: NTP bootstrap — ok", r.stdout) def test_blank_lines_between_sources_are_ignored(self): r = run_check(ntp_sources="\n\n162.159.200.1\n\n") - self.assertIn("check 6/6: NTP bootstrap — ok", r.stdout) + self.assertIn("check 6/8: NTP bootstrap — ok", r.stdout) def test_a_hostname_containing_digits_and_dots_is_not_an_address(self): # The trap in any naive "looks like an IP" test: these resolve through @@ -153,6 +163,8 @@ class NtpBootstrap(unittest.TestCase): env.update({"PRC_FAILED_UNITS": "", "PRC_UNIT_STATES": "", "PRC_LOCAL_SCAN_ROOTS": "", "PRC_PROJECT_ROOTS": "", "PRC_SIGNAL_ACCOUNTS": "+15045551234", + "PRC_IDLE_DAEMON": "4242", + "PRC_REPO_REMOTES": "", "PRC_CHRONY_CONF": chrony_conf}) env.pop("PRC_NTP_SOURCES", None) return subprocess.run(["sh", CHECK], capture_output=True, text=True, @@ -177,7 +189,7 @@ class NtpBootstrap(unittest.TestCase): r = self._run_real_probe(conf) if "no NTP implementation is active" in r.stdout: self.skipTest("no chronyd on this host — the reader branch can't run") - self.assertIn("check 6/6: NTP bootstrap — ok", r.stdout) + self.assertIn("check 6/8: NTP bootstrap — ok", r.stdout) def test_confdir_naming_an_empty_directory_is_not_a_pass(self): # confdir present, nothing behind it: the sources are the hostname-only @@ -199,7 +211,161 @@ class NtpBootstrap(unittest.TestCase): env.pop("PRC_NTP_SOURCES", None) r = subprocess.run(["sh", CHECK], capture_output=True, text=True, timeout=30, env=env) - self.assertIn("check 6/6: NTP bootstrap", r.stdout) + self.assertIn("check 6/8: NTP bootstrap", r.stdout) + + +class IdleDaemon(unittest.TestCase): + """Check 7 — whether anything still triggers idle lock and suspend. + + A laptop that never sleeps has no symptom until the battery is gone, which + is why this needs a check rather than trusting the desktop to look right. + On velox 2026-08-19 hypridle started cleanly at 15:29:48 and `settings + restore` killed it six seconds later, replaying a caffeine stored in an + earlier boot. The machine then ran 11h40m fully awake on battery, died when + it flattened, and reset its RTC — which took DNS down with it, the very + deadlock check 6 exists for. Nothing looked wrong at any point. + + The check is behavioural: it asks whether the daemon is alive, not why it + might not be, so a stale caffeine, a crash and a bad config all surface the + same way. + """ + + # --- Normal cases --------------------------------------------------- + + def test_running_daemon_is_clean(self): + r = run_check(idle_daemon="4242") + self.assertEqual(r.returncode, 0, r.stdout + r.stderr) + self.assertNotIn("DEVIATION", r.stdout) + + def test_installed_but_not_running_flags(self): + r = run_check(idle_daemon="") + self.assertEqual(r.returncode, 1) + self.assertIn("hypridle", r.stdout) + self.assertIn("DEVIATION", r.stdout) + + def test_the_finding_names_the_consequence_not_just_the_process(self): + # "hypridle is not running" reads as a detail. The reason it matters is + # that the machine stays awake until the battery is gone, and that is + # what has to be in the line someone skims at 1am. + r = run_check(idle_daemon="") + self.assertIn("awake", r.stdout.lower()) + + def test_the_finding_names_the_known_cause(self): + # Behavioural checks are cheap to write and expensive to act on. Naming + # the one cause already seen saves the reader the investigation this + # session had to do from scratch. + r = run_check(idle_daemon="") + self.assertIn("caffeine", r.stdout.lower()) + + # --- Boundary cases ------------------------------------------------- + + def test_not_installed_is_not_a_finding(self): + # A headless or dwm machine never installs hypridle — archsetup pulls + # it in only for Hyprland. Flagging its absence there would be noise on + # every run, and noise is how a real finding gets skimmed past. + r = run_check(idle_daemon="MISSING") + self.assertEqual(r.returncode, 0, r.stdout + r.stderr) + self.assertNotIn("DEVIATION", r.stdout) + + def test_several_pids_still_read_as_running(self): + # pgrep prints one pid per line. More than one is its own problem (five + # concurrent daemons wedged a velox session on 2026-07-22) but it is + # not *this* check's, and it must not read as "not running". + r = run_check(idle_daemon="4242\n4243") + self.assertEqual(r.returncode, 0, r.stdout + r.stderr) + + def test_the_check_always_prints_its_line(self): + for pids in ("4242", "", "MISSING"): + with self.subTest(pids=pids): + self.assertIn("idle daemon", + run_check(idle_daemon=pids).stdout.lower()) + + +class RepoPushCapability(unittest.TestCase): + """Check 8 — a working repo cloned from the read-only endpoint. + + archsetup clones the user's own archsetup and dotfiles from + https://git.cjennings.net/..., the anonymous read-only endpoint. That is + the right default for a stranger installing archsetup, who has no key on + the server, and the wrong one for this machine, which has to push. The + override exists (ARCHSETUP_REPO / DOTFILES_REPO) but only applies where it + is configured — a curl|bash install, or a rebuild from a stock ISO, picks + the default straight back up. + + Nothing about the tree shows it. The clone is complete and ordinary, and + the machine finds out at the first push, with a 403. That is how velox's + dotfiles remote was found on 2026-08-17, four days after its rebuild. + + Only the read-only endpoint is flagged. An https remote to some other host + may well be pushable with a credential helper, and guessing about hosts + this machine does not own would put standing noise in front of the real + findings. + """ + + RO = "https://git.cjennings.net/dotfiles.git" + RW = "git@cjennings.net:dotfiles.git" + + # --- Normal cases --------------------------------------------------- + + def test_an_ssh_remote_is_clean(self): + r = run_check(repo_remotes=f"/home/x/.dotfiles {self.RW}") + self.assertEqual(r.returncode, 0, r.stdout) + self.assertNotIn("DEVIATION", r.stdout) + + def test_the_read_only_endpoint_flags(self): + r = run_check(repo_remotes=f"/home/x/.dotfiles {self.RO}") + self.assertEqual(r.returncode, 1) + self.assertIn("/home/x/.dotfiles", r.stdout) + + def test_the_finding_names_the_consequence(self): + # "the remote is https" is a detail. That pushing fails is the point. + r = run_check(repo_remotes=f"/home/x/.dotfiles {self.RO}") + self.assertIn("push", r.stdout.lower()) + + def test_every_offending_repo_is_named(self): + r = run_check(repo_remotes=(f"/home/x/.dotfiles {self.RO}\n" + f"/home/x/code/archsetup {self.RO}")) + self.assertIn("/home/x/.dotfiles", r.stdout) + self.assertIn("/home/x/code/archsetup", r.stdout) + + # --- Boundary cases ------------------------------------------------- + + def test_a_mixed_set_flags_only_the_read_only_one(self): + r = run_check(repo_remotes=(f"/home/x/.dotfiles {self.RW}\n" + f"/home/x/code/archsetup {self.RO}")) + self.assertEqual(r.returncode, 1) + self.assertIn("/home/x/code/archsetup", r.stdout) + self.assertNotIn("/home/x/.dotfiles", r.stdout) + + def test_an_https_remote_to_another_host_is_not_flagged(self): + # GitHub over https is pushable with a credential helper. Flagging it + # would be a guess about a host this machine does not own. + r = run_check(repo_remotes="/home/x/code/thing https://github.com/a/b.git") + self.assertEqual(r.returncode, 0, r.stdout) + + def test_no_repos_is_not_a_finding(self): + r = run_check(repo_remotes="") + self.assertEqual(r.returncode, 0, r.stdout) + + def test_blank_lines_are_ignored(self): + r = run_check(repo_remotes=f"\n\n/home/x/.dotfiles {self.RW}\n\n") + self.assertEqual(r.returncode, 0, r.stdout) + + def test_the_check_always_prints_its_line(self): + for remotes in ("", f"/home/x/.dotfiles {self.RW}", + f"/home/x/.dotfiles {self.RO}"): + with self.subTest(remotes=remotes): + self.assertIn("repo remotes", + run_check(repo_remotes=remotes).stdout.lower()) + + # --- Error cases ---------------------------------------------------- + + def test_a_repo_with_no_origin_is_a_finding(self): + # Fails closed. A repo whose origin could not be read was not checked, + # and reporting it clean is the false pass this script exists to avoid. + r = run_check(repo_remotes="/home/x/.dotfiles") + self.assertEqual(r.returncode, 1) + self.assertIn("/home/x/.dotfiles", r.stdout) class AllClean(unittest.TestCase): @@ -629,11 +795,30 @@ class SignalAccount(unittest.TestCase): "PRC_LOCAL_SCAN_ROOTS": "", "PRC_PROJECT_ROOTS": "", "PRC_NTP_SOURCES": "162.159.200.1", "PRC_SIGNAL_ACCOUNTS": "+15045551234", + "PRC_IDLE_DAEMON": "4242", + "PRC_REPO_REMOTES": "", "signal_missing": "1"}) r = subprocess.run(["sh", CHECK], capture_output=True, text=True, timeout=30, env=env) self.assertEqual(r.returncode, 0, r.stdout) + def test_a_stray_idle_absent_in_the_environment_is_ignored(self): + # Same class as the flag above, and the dangerous direction: an + # inherited idle_absent=1 would make check 7 skip silently and report + # ok on a machine that cannot sleep, which is the exact false pass the + # check exists to prevent. + env = dict(os.environ) + env.update({"PRC_FAILED_UNITS": "", "PRC_UNIT_STATES": "", + "PRC_LOCAL_SCAN_ROOTS": "", "PRC_PROJECT_ROOTS": "", + "PRC_NTP_SOURCES": "162.159.200.1", + "PRC_SIGNAL_ACCOUNTS": "+15045551234", + "PRC_IDLE_DAEMON": "", + "idle_absent": "1"}) + r = subprocess.run(["sh", CHECK], capture_output=True, text=True, + timeout=30, env=env) + self.assertEqual(r.returncode, 1, r.stdout) + self.assertIn("hypridle is installed but not running", r.stdout) + class ProbeFailure(unittest.TestCase): """A probe that could not run must never read as a clean check. @@ -680,6 +865,8 @@ class ProbeFailure(unittest.TestCase): "PRC_UNIT_STATES": "", "PRC_LOCAL_SCAN_ROOTS": "", "PRC_PROJECT_ROOTS": "", "PRC_SIGNAL_ACCOUNTS": "+15045551234", + "PRC_IDLE_DAEMON": "4242", + "PRC_REPO_REMOTES": "", "TMPDIR": "/nonexistent-tmp-dir"}) r = subprocess.run(["sh", CHECK], capture_output=True, text=True, timeout=30, env=env) @@ -701,6 +888,8 @@ class RealUnitDirEnumeration(unittest.TestCase): env.update({"PRC_FAILED_UNITS": "", "PRC_LOCAL_SCAN_ROOTS": "", "PRC_PROJECT_ROOTS": "", "PRC_SIGNAL_ACCOUNTS": "+15045551234", + "PRC_IDLE_DAEMON": "4242", + "PRC_REPO_REMOTES": "", "XDG_CONFIG_HOME": config_home}) env.pop("PRC_UNIT_STATES", None) return subprocess.run(["sh", CHECK], capture_output=True, text=True, @@ -754,6 +943,8 @@ class WedgedSystemctl(unittest.TestCase): env = dict(os.environ) env.update({"PRC_LOCAL_SCAN_ROOTS": "", "PRC_PROJECT_ROOTS": "", "PRC_SIGNAL_ACCOUNTS": "+15045551234", + "PRC_IDLE_DAEMON": "4242", + "PRC_REPO_REMOTES": "", "PRC_SYSTEMCTL": fake, "PRC_SYSTEMCTL_TIMEOUT": timeout_s, "XDG_CONFIG_HOME": d}) @@ -771,7 +962,7 @@ class WedgedSystemctl(unittest.TestCase): self.assertEqual(r.returncode, 1) self.assertIn("could not query user units", r.stdout) # The run must reach the end rather than stopping at the first call. - self.assertIn("check 6/6", r.stdout) + self.assertIn("check 8/8", r.stdout) def test_a_hanging_systemctl_does_not_stall_the_whole_run(self): # The fake sleeps 8s against a 1s bound, so a bounded run lands near @@ -795,7 +986,7 @@ class Reporting(unittest.TestCase): unit_states="c.timer disabled") self.assertEqual(r.returncode, 1) summary = r.stdout.strip().splitlines()[-1] - self.assertEqual(summary, "3 finding(s) across 6 checks") + self.assertEqual(summary, "3 finding(s) across 8 checks") def test_the_summary_count_tracks_every_check(self): # One finding from each of the five, so a counter that drops or @@ -811,7 +1002,7 @@ class Reporting(unittest.TestCase): local_roots=scan, project_roots=proj, signal_accounts="") summary = r.stdout.strip().splitlines()[-1] - self.assertEqual(summary, "5 finding(s) across 6 checks") + self.assertEqual(summary, "5 finding(s) across 8 checks") def test_help_exits_zero(self): r = subprocess.run(["sh", CHECK, "--help"], -- cgit v1.2.3