diff options
| -rwxr-xr-x | scripts/post-rebuild-check | 124 | ||||
| -rw-r--r-- | tests/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 "path<space>origin-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"], |
