// SPDX-License-Identifier: GPL-3.0-or-later import { test } from "node:test"; import assert from "node:assert/strict"; import { mkdtempSync, rmSync, readdirSync, readFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; import { checks } from "../src/checks/index.js"; import { memory } from "../src/checks/memory.js"; import { zram } from "../src/checks/zram.js"; import { fstrim } from "../src/checks/fstrim.js"; import { load } from "../src/checks/load.js"; import { disk } from "../src/checks/disk.js"; import { inodes } from "../src/checks/inodes.js"; import { services } from "../src/checks/services.js"; import { timers } from "../src/checks/timers.js"; import { ntp } from "../src/checks/ntp.js"; import { journal } from "../src/checks/journal.js"; import { gpu } from "../src/checks/gpu.js"; import { updates } from "../src/checks/updates.js"; import { security } from "../src/checks/security.js"; import { secureboot } from "../src/checks/secureboot.js"; import { firmware } from "../src/checks/firmware.js"; import { flatpak } from "../src/checks/flatpak.js"; import { boot } from "../src/checks/boot.js"; import { thermal } from "../src/checks/thermal.js"; import { processes } from "../src/checks/processes.js"; import { certs } from "../src/checks/certs.js"; import { ports } from "../src/checks/ports.js"; import { suspend } from "../src/checks/suspend.js"; import { battery } from "../src/checks/battery.js"; import { bluetooth } from "../src/checks/bluetooth.js"; import { bringup } from "../src/checks/bringup.js"; import { wayland } from "../src/checks/wayland.js"; import { backup } from "../src/checks/backup.js"; import { hardware } from "../src/checks/hardware.js"; import { packages } from "../src/checks/packages.js"; import { smart } from "../src/checks/smart.js"; import { luks } from "../src/checks/luks.js"; import { audio } from "../src/checks/audio.js"; import { containers } from "../src/checks/containers.js"; import { containerdisk } from "../src/checks/containerdisk.js"; import { crash } from "../src/checks/crash.js"; import { network } from "../src/checks/network.js"; import { reboot, versionGt } from "../src/checks/reboot.js"; import { journald } from "../src/checks/journald.js"; import { parseSize } from "../src/utils.js"; import { countBySeverity } from "../src/report.js"; import { detectDistro } from "../src/distro.js"; import { loadThresholds } from "../src/thresholds.js"; // The updates check caches its result; the existing tests exercise the // command parsing, so disable the cache for this file's default env. The // cache behavior itself is tested below with a dedicated temp directory. const prevUpdatesTtl = process.env.LINUX_DOCTOR_UPDATES_TTL_MS; process.env.LINUX_DOCTOR_UPDATES_TTL_MS = "0"; test.after(() => { if (prevUpdatesTtl === undefined) delete process.env.LINUX_DOCTOR_UPDATES_TTL_MS; else process.env.LINUX_DOCTOR_UPDATES_TTL_MS = prevUpdatesTtl; }); /** * Build a stub ctx.run from a map of command → stdout string. The distro * profile comes from the given os-release (default: Bazzite, image-based) * and thresholds default to the shipped values. */ function stubCtx(map, osRelease = { id: "bazzite", id_like: "fedora" }) { return { osRelease, dist: detectDistro(osRelease), thresholds: loadThresholds({}), run: async (cmd) => { // Fixture keys are written unquoted; the checks quote interpolated // values (shq), so strip quotes before lookup. const entry = map[cmd] ?? map[cmd.replaceAll("'", "")]; if (entry === undefined) return { ok: false, code: 1, stdout: "", stderr: "" }; return { ok: true, code: 0, stdout: entry, stderr: "" }; }, }; } test("memory: flags low available memory as high severity", async () => { const ctx = stubCtx({ "free -b": ` total used free shared buff/cache available\nMem: 16106127360 12884901888 500000000 500000000 4718592000 1500000000\nSwap: 8267812045 2362232012 5905580033`, "swapon --show --bytes": `NAME TYPE SIZE USED PRIO\n/dev/zram0 partition 8267812045 2362232012 100`, }); const findings = await memory.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "high"); assert.match(findings[0].title, /low on usable memory/i); assert.ok(findings[0].fix, "high-severity findings should include a fix"); }); test("memory: healthy system produces no finding", async () => { const ctx = stubCtx({ "free -b": ` total used free shared buff/cache available\nMem: 16106127360 4000000000 1000000000 300000000 7000000000 11000000000\nSwap: 8267812045 0 8267812045`, "swapon --show --bytes": "", }); const findings = await memory.run(ctx); assert.equal(findings.length, 0); }); test("memory: configurable thresholds are honored", async () => { const ctx = stubCtx({ "free -b": ` total used free shared buff/cache available\nMem: 16106127360 4000000000 1000000000 300000000 7000000000 11000000000\nSwap: 8267812045 0 8267812045`, "swapon --show --bytes": "", }); // 11/16 GB available ≈ 0.68 — normally fine; a raised warn ratio flags it. ctx.thresholds = { ...ctx.thresholds, memWarnRatio: 0.7 }; const findings = await memory.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "medium"); }); test("memory: zram swap is described as compressed RAM, not as disk swap", async () => { const ctx = stubCtx({ "free -b": ` total used free shared buff/cache available\nMem: 16106127360 12884901888 500000000 500000000 4718592000 1500000000\nSwap: 8267812045 2362232012 5905580033`, "swapon --show --bytes": `NAME TYPE SIZE USED PRIO\n/dev/zram0 partition 8267812045 2362232012 100`, }); const f = (await memory.run(ctx)).find((x) => x.code === "memory/low"); assert.ok(f, "expected the memory/low finding"); assert.match(f.detail, /compressed zram swap/); assert.doesNotMatch(f.detail, /pushed to swap/, "zram is not disk I/O"); }); test("memory: swap on a real block device is still described as swapping", async () => { const ctx = stubCtx({ "free -b": ` total used free shared buff/cache available\nMem: 16106127360 12884901888 500000000 500000000 4718592000 1500000000\nSwap: 8267812045 2362232012 5905580033`, "swapon --show --bytes": `NAME TYPE SIZE USED PRIO\n/dev/sda2 partition 8267812045 2362232012 100`, }); const f = (await memory.run(ctx)).find((x) => x.code === "memory/low"); assert.match(f.detail, /pushed to swap/); }); test("load: overloaded CPU is flagged", async () => { const ctx = stubCtx({ "cat /proc/loadavg": "8.50 6.00 4.00 2/300 1234", nproc: "4", }); const findings = await load.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "high"); assert.match(findings[0].title, /overloaded/i); }); test("disk: composefs (immutable) root is NOT reported as full", async () => { const ctx = stubCtx({ "df -P -B1 --exclude-type=tmpfs --exclude-type=devtmpfs --exclude-type=squashfs --exclude-type=overlay --exclude-type=proc --exclude-type=sysfs --exclude-type=cgroup2": `Filesystem 1024-blocks Used Available Capacity Mounted on\ncomposefs 60000000 60000000 0 100% /\n/dev/sda3 957000000000 350000000000 600000000000 37% /var/home`, }); const findings = await disk.run(ctx); assert.equal(findings.length, 0, "a full composefs root must be ignored"); }); test("disk: AppImage runtime mounts (always 100% by design) are NOT reported as full", async () => { const ctx = stubCtx({ "df -P -B1 --exclude-type=tmpfs --exclude-type=devtmpfs --exclude-type=squashfs --exclude-type=overlay --exclude-type=proc --exclude-type=sysfs --exclude-type=cgroup2": `Filesystem 1024-blocks Used Available Capacity Mounted on\nLinux.Doctor_0.3.5_amd64.AppImage 79822848 79822848 0 100% /tmp/.mount_Linux.icbcpE\n/dev/sda3 957000000000 350000000000 600000000000 37% /var/home`, }); const findings = await disk.run(ctx); assert.equal(findings.length, 0, "a 100% AppImage FUSE mount is a fixed-size image, not a filling disk"); }); test("inodes: AppImage runtime mounts are NOT reported as inode-exhausted", async () => { const ctx = stubCtx({ "df -iP --exclude-type=tmpfs --exclude-type=devtmpfs --exclude-type=squashfs --exclude-type=overlay --exclude-type=proc --exclude-type=sysfs --exclude-type=cgroup2": `Filesystem Inodes IUsed IFree IUse% Mounted on\nLinux.Doctor_0.3.5_amd64.AppImage 100 100 0 100% /tmp/.mount_Linux.icbcpE\n/dev/sda3 1000000 100000 900000 10% /var/home`, }); const findings = await inodes.run(ctx); assert.equal(findings.length, 0, "same AppImage exemption as the disk check"); }); test("disk: configurable thresholds are honored", async () => { const ctx = stubCtx({ "df -P -B1 --exclude-type=tmpfs --exclude-type=devtmpfs --exclude-type=squashfs --exclude-type=overlay --exclude-type=proc --exclude-type=sysfs --exclude-type=cgroup2": `Filesystem 1024-blocks Used Available Capacity Mounted on\n/dev/sda1 100000000000 85000000000 15000000000 85% /data\n`, }); ctx.thresholds = { ...ctx.thresholds, diskFullPct: 80 }; const findings = await disk.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "high"); assert.match(findings[0].title, /nearly full/); }); test("disk: a real partition at 95% is reported as high", async () => { const ctx = stubCtx({ "df -P -B1 --exclude-type=tmpfs --exclude-type=devtmpfs --exclude-type=squashfs --exclude-type=overlay --exclude-type=proc --exclude-type=sysfs --exclude-type=cgroup2": `Filesystem 1024-blocks Used Available Capacity Mounted on\n/dev/sda1 100000000000 95000000000 5000000000 95% /data`, }); const findings = await disk.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "high"); assert.match(findings[0].title, /nearly full/i); }); test("disk: two mounts on the same device (btrfs subvolumes) share one dedupeKey", async () => { const ctx = stubCtx({ "df -P -B1 --exclude-type=tmpfs --exclude-type=devtmpfs --exclude-type=squashfs --exclude-type=overlay --exclude-type=proc --exclude-type=sysfs --exclude-type=cgroup2": `Filesystem 1024-blocks Used Available Capacity Mounted on\n/dev/sda2 100000000000 95000000000 5000000000 95% /\n/dev/sda2 100000000000 95000000000 5000000000 95% /home`, }); const findings = await disk.run(ctx); assert.equal(findings.length, 2, "both mounts are reported..."); assert.ok(findings.every((f) => f.dedupeKey === "disk:/dev/sda2"), "same device → same dedupeKey, so dedupe collapses them into one"); }); test("services: system-scope failures are high severity", async () => { const ctx = stubCtx({ "systemctl --failed --no-legend --plain": `NetworkManager.service loaded failed failed`, "systemctl --user --failed --no-legend --plain": "", }); const findings = await services.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "high"); assert.equal(findings[0].code, "services/failed"); assert.match(findings[0].detail, /NetworkManager/i); assert.match(findings[0].fix, /systemctl status NetworkManager/i); }); test("services: only user-scope failures are medium, not high", async () => { const ctx = stubCtx({ "systemctl --failed --no-legend --plain": "", "systemctl --user --failed --no-legend --plain": `bitwarden.service loaded failed failed\npicom.service loaded failed failed\nclamav.service loaded failed failed\ncustom-thing.service loaded failed failed\napp.service loaded failed failed`, }); const findings = await services.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "medium"); assert.match(findings[0].title, /5 services failed to start/); }); test("services: a failed system unit is not answered by --user", async () => { // The old text offered `systemctl --user status` in parentheses for every // systemd system, including when the failed unit was a system one — a command // that cannot show that unit at all. The log is also the step that actually // explains the failure, and it was missing. const ctx = stubCtx({ "systemctl --failed --no-legend --plain": `fwupd-refresh.service loaded failed failed`, "systemctl --user --failed --no-legend --plain": "", }); const findings = await services.run(ctx); assert.equal(findings.length, 1); assert.match(findings[0].fix, /journalctl -u fwupd-refresh\.service -b/, "the log is the first step"); assert.match(findings[0].fix, /systemctl status fwupd-refresh\.service/); assert.doesNotMatch(findings[0].fix, /--user/, "a system unit has no --user answer"); assert.match(findings[0].fix, /reset-failed/, "clearing the mark is offered once the cause is known"); }); test("services: a failed user unit gets the --user commands", async () => { const ctx = stubCtx({ "systemctl --failed --no-legend --plain": "", "systemctl --user --failed --no-legend --plain": `bitwarden.service loaded failed failed`, }); const findings = await services.run(ctx); assert.equal(findings.length, 1); assert.match(findings[0].fix, /systemctl --user status bitwarden\.service/); assert.match(findings[0].fix, /journalctl --user -u bitwarden\.service -b/); }); test("services: the detail does not guess a cause from the state alone", async () => { // "usually means a broken package, a bad configuration, or a hook that // errored" was wrong for the common case — a timer that failed once on a // network error — and it read as a verdict. The state is a fact; the log is // where the cause is. const ctx = stubCtx({ "systemctl --failed --no-legend --plain": `fwupd-refresh.service loaded failed failed`, "systemctl --user --failed --no-legend --plain": "", }); const findings = await services.run(ctx); assert.doesNotMatch(findings[0].detail, /broken package|bad configuration/i); assert.match(findings[0].detail, /fact, not a diagnosis/i); }); test("services: one failed unit is described in the singular", async () => { // "These 1 service are in a failed state" was the first sentence a user read. const ctx = stubCtx({ "systemctl --failed --no-legend --plain": `fwupd-refresh.service loaded failed failed`, "systemctl --user --failed --no-legend --plain": "", }); const findings = await services.run(ctx); assert.match(findings[0].detail, /^This service is in a failed state: fwupd-refresh\.service \(system\)\./); assert.doesNotMatch(findings[0].detail, /These 1 service/); }); test("services: clearing a user unit's mark does not ask for sudo", async () => { // Same trap as the status command: the user manager owns user units, and // `sudo systemctl reset-failed` would not reach one. const ctx = stubCtx({ "systemctl --failed --no-legend --plain": "", "systemctl --user --failed --no-legend --plain": `bitwarden.service loaded failed failed`, }); const findings = await services.run(ctx); assert.match(findings[0].fix, /`systemctl --user reset-failed bitwarden\.service`/); assert.doesNotMatch(findings[0].fix, /sudo systemctl reset-failed/); }); test("timers: a timer whose start condition is unmet is not a broken schedule", async () => { // Bazzite (immutable): dnf-makecache.timer is enabled, has never run and is // not scheduled, because its start condition is unmet by design on an atomic // system (systemctl status says "Condition: start condition unmet"). Calling // that a broken schedule blames the user for a timer that cannot run at all. const cols = (vals) => vals.map((v) => String(v).padEnd(20)).join(""); const ctx = stubCtx({ "systemctl list-timers --all --no-pager --plain 2>/dev/null": [ cols(["NEXT", "LEFT", "LAST", "PASSED", "UNIT", "ACTIVATES"]), cols(["-", "-", "-", "-", "dnf-makecache.timer", "dnf-makecache.service"]), "", ].join("\n"), "systemctl is-enabled dnf-makecache.timer 2>/dev/null": "enabled\n", "systemctl show dnf-makecache.timer -p ConditionResult --value 2>/dev/null": "no\n", }); const findings = await timers.run(ctx); assert.ok( !findings.some((f) => f.code === "timers/broken"), `a timer that cannot run is not broken: ${JSON.stringify(findings.map((f) => f.code))}` ); }); test("timers: an enabled timer with no condition problem is still broken", async () => { const cols = (vals) => vals.map((v) => String(v).padEnd(20)).join(""); const ctx = stubCtx({ "systemctl list-timers --all --no-pager --plain 2>/dev/null": [ cols(["NEXT", "LEFT", "LAST", "PASSED", "UNIT", "ACTIVATES"]), cols(["-", "-", "-", "-", "backup.timer", "backup.service"]), "", ].join("\n"), "systemctl is-enabled backup.timer 2>/dev/null": "enabled\n", "systemctl show backup.timer -p ConditionResult --value 2>/dev/null": "yes\n", }); const findings = await timers.run(ctx); assert.ok(findings.some((f) => f.code === "timers/broken"), "keep the real signal"); }); test("network: no iproute2 is a skip, not 'no default route'", async () => { // Debian, Fedora and Ubuntu minimal images do not ship `ip`. run() only sets // `missing` when the spawn itself fails, and these commands go through a // shell, so a missing `ip` looked like an empty route table: the check // reported a medium "No default network route" on a machine with a perfectly // good route. const ctx = stubCtx({}); // nothing stubbed: `command -v ip` finds nothing const findings = await network.run(ctx); assert.equal(findings.length, 1, `expected one skip finding: ${JSON.stringify(findings.map((f) => f.code))}`); assert.equal(findings[0].code, "network/skipped"); assert.ok(!findings.some((f) => f.code === "network/no-route"), "a missing tool is not a missing route"); }); test("load: inside a container the check is skipped, not called overloaded", async () => { // /proc/loadavg is not namespaced: in a container it is the HOST's load // average while nproc reports the container's CPUs, so the ratio invents an // overloaded system out of an idle container (all five test images did it). const ctx = stubCtx({ "test -f /.dockerenv -o -f /run/.containerenv && echo container 2>/dev/null": "container\n", "cat /proc/loadavg": "6.00 5.50 5.00 1/200 4242\n", "nproc": "2\n", }); const findings = await load.run(ctx); assert.ok(findings.some((f) => f.code === "load/skipped"), `expected a skip: ${JSON.stringify(findings.map((f) => f.code))}`); assert.ok(!findings.some((f) => f.code === "load/overloaded"), "the host's load is not this container's"); }); test("load: a real host with a high load is still reported", async () => { const ctx = stubCtx({ "cat /proc/loadavg": "6.00 5.50 5.00 1/200 4242\n", "nproc": "2\n", }); const findings = await load.run(ctx); assert.ok(findings.some((f) => f.code === "load/overloaded"), "keep the real signal"); }); test("processes: a missing ps is an explicit skip, not 'no consumers'", async () => { // `ps ... | head` exits 0 through head even when ps is absent, so the empty // result looked like a healthy machine. procps is not installed on minimal // Debian and Fedora images. const ctx = { osRelease: { id: "debian", id_like: "" }, dist: detectDistro({ id: "debian", id_like: "" }), thresholds: {}, run: async (cmd) => (cmd.startsWith("ps ") ? { ok: false, code: 127, stdout: "", stderr: "", missing: true } : { ok: false, code: 1, stdout: "", stderr: "" }), }; const findings = await processes.run(ctx); assert.equal(findings.length, 1, `expected one skip: ${JSON.stringify(findings.map((f) => f.code))}`); assert.equal(findings[0].code, "processes/skipped"); assert.match(findings[0].fix, /procps/); }); test("certs: a missing openssl is an explicit skip", async () => { const ctx = stubCtx({}); const findings = await certs.run(ctx); assert.equal(findings.length, 1, `expected one skip: ${JSON.stringify(findings.map((f) => f.code))}`); assert.equal(findings[0].code, "certs/skipped"); assert.match(findings[0].fix, /openssl/); }); test("ports: a missing ss is an explicit skip", async () => { // ss is iproute2, which minimal Debian and Fedora images do not ship. The // check used to return silently, so a machine exposing a database on 0.0.0.0 // got no signal that the check had not run. const ctx = { osRelease: { id: "debian", id_like: "" }, dist: detectDistro({ id: "debian", id_like: "" }), thresholds: {}, run: async (cmd) => (cmd.startsWith("ss ") ? { ok: false, code: 127, stdout: "", stderr: "", missing: true } : { ok: false, code: 1, stdout: "", stderr: "" }), }; const findings = await ports.run(ctx); assert.equal(findings.length, 1, `expected one skip: ${JSON.stringify(findings.map((f) => f.code))}`); assert.equal(findings[0].code, "ports/skipped"); assert.match(findings[0].fix, /iproute2/); }); test("memory: a missing free is an explicit skip (the .missing flag now fires)", async () => { const ctx = { osRelease: { id: "debian", id_like: "" }, dist: detectDistro({ id: "debian", id_like: "" }), thresholds: {}, run: async (cmd) => (cmd.startsWith("free ") ? { ok: false, code: 127, stdout: "", stderr: "", missing: true } : { ok: false, code: 1, stdout: "", stderr: "" }), }; const findings = await memory.run(ctx); assert.equal(findings.length, 1, `expected one skip: ${JSON.stringify(findings.map((f) => f.code))}`); assert.equal(findings[0].code, "memory/skipped"); }); test("memory: a container is an explicit skip, not the host's RAM", async () => { // /proc/meminfo is not namespaced: a 256MB-limited container's `free -b` // reports the host's 15GB. That is the same lie `load` used to tell. const ctx = stubCtx({ "test -f /.dockerenv -o -f /run/.containerenv && echo container 2>/dev/null": "container\n", "free -b": "Mem: 16106127360 1000 2000 0 3000 14000000000\n", }); const findings = await memory.run(ctx); assert.equal(findings[0].code, "memory/skipped", `expected a container skip: ${JSON.stringify(findings.map((f) => f.code))}`); assert.match(findings[0].title, /container/i); }); test("zram: a container is an explicit skip, not the host's swap", async () => { const ctx = stubCtx({ "test -f /.dockerenv -o -f /run/.containerenv && echo container 2>/dev/null": "container\n" }); const findings = await zram.run(ctx); assert.equal(findings[0].code, "zram/skipped", `expected a container skip: ${JSON.stringify(findings.map((f) => f.code))}`); }); test("fstrim: a container is an explicit skip, not the host's disks", async () => { const ctx = stubCtx({ "test -f /.dockerenv -o -f /run/.containerenv && echo container 2>/dev/null": "container\n" }); const findings = await fstrim.run(ctx); assert.equal(findings[0].code, "fstrim/skipped", `expected a container skip: ${JSON.stringify(findings.map((f) => f.code))}`); }); test("reboot: a container is an explicit skip, not 'no reboot needed'", async () => { // A container has no kernels of its own; the old check read an empty /boot // list and concluded "No reboot needed". const ctx = stubCtx({ "test -f /.dockerenv -o -f /run/.containerenv && echo container 2>/dev/null": "container\n", "uname -r 2>/dev/null": "6.12.0\n", }, { id: "alpine", id_like: "" }); const findings = await reboot.run(ctx); assert.equal(findings[0].code, "reboot/skipped", `expected a container skip: ${JSON.stringify(findings.map((f) => f.code))}`); }); test("reboot: no readable kernel list is silence, not 'no reboot needed'", async () => { const ctx = stubCtx({ "uname -r 2>/dev/null": "6.12.0\n" }, { id: "debian", id_like: "" }); assert.deepEqual(await reboot.run(ctx), [], "cannot compare kernels → say nothing"); }); test("journal: known noise is filtered into an informational finding", async () => { const ctx = stubCtx({ "journalctl -p err --since \"-24 hours\" --no-pager -o short 2>/dev/null": `Aug 15 14:32:47 bazzite systemd-udevd[465]: /usr/lib/udev/rules.d/50-udev-default.rules:105 Failed to resolve group 'disk', ignoring: Unknown group\nAug 15 14:33:01 bazzite setroubleshoot[1807]: SELinux is preventing bootupctl from read access on the directory /proc.\nAug 15 14:36:58 bazzite cupsd[1512]: Returning IPP client-error-bad-request for Create-Printer-Subscriptions (ipp://localhost/) from localhost.`, }); const findings = await journal.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "info"); assert.match(findings[0].title, /routine noise/i); }); test("journal: the KDE screen locker's retry message is noise, not a fault", async () => { // kscreenlocker_greet logs "[PAM worker kde] Authentication attempt too soon" // when you retype a wrong password quickly, and repeats it a few times. Our // /Authentication attempt too soon/ pattern read that as a recognized error, // so a normal desktop got a medium "6 recognized errors" worth 8 points on // the health score. Seen on the maintainer's own Bazzite box. const ctx = stubCtx({ "journalctl -p err --since \"-24 hours\" --no-pager -o short 2>/dev/null": [ "Sep 22 16:41:02 bazzite kscreenlocker_greet[12697]: [PAM worker kde] Authentication attempt too soon.", "Sep 22 16:41:05 bazzite kscreenlocker_greet[12697]: [PAM worker kde] Authentication attempt too soon.", "Sep 22 16:41:09 bazzite kscreenlocker_greet[12697]: [PAM worker kde] Authentication attempt too soon.", ].join("\n") + "\n", }); const findings = await journal.run(ctx); assert.ok( !findings.some((f) => f.code === "journal/errors"), `a screen-locker retry is not a system error: ${JSON.stringify(findings.map((f) => f.code))}` ); }); test("journal: 'Authentication attempt too soon' outside the screen locker is still an error", async () => { // The signal the pattern exists for: something hammering authentication. const ctx = stubCtx({ "journalctl -p err --since \"-24 hours\" --no-pager -o short 2>/dev/null": "Sep 22 16:41:02 bazzite sshd[9001]: pam_unix(sshd:auth): Authentication attempt too soon.\n", }); const findings = await journal.run(ctx); assert.ok(findings.some((f) => f.code === "journal/errors"), "keep the brute-force signal"); }); test("journal: pipewire losing an ALSA device is noise, not an error", async () => { // Seen on a Bazzite desktop: the monitor sleeps, the HDMI/S-PDIF device goes // away, and PipeWire logs err-level lines for a few seconds. Audio works. const ctx = stubCtx({ "journalctl -p err --since \"-24 hours\" --no-pager -o short 2>/dev/null": [ "Oct 31 15:09:01 bazzite pipewire[2625]: spa.alsa: iec958:0p: snd_pcm_drop: No such device", "Oct 31 15:09:01 bazzite pipewire[2625]: spa.alsa: iec958:0p: snd_pcm_status error: No such device", "Oct 31 15:09:01 bazzite pipewire[2625]: spa.alsa: sw_params: No such device", ].join("\n") + "\n", }); const findings = await journal.run(ctx); assert.ok(!findings.some((f) => f.code === "journal/errors"), "a sleeping monitor is not a fault"); }); test("journal: the NM dispatcher activation failure at boot is noise", async () => { const ctx = stubCtx({ "journalctl -p err --since \"-24 hours\" --no-pager -o short 2>/dev/null": [ "Oct 31 13:35:40 bazzite dbus-broker-launch[1177]: Activation request for 'org.freedesktop.nm_dispatcher' failed.", "Oct 31 13:35:41 bazzite dbus-broker-launch[1177]: Activation request for 'org.freedesktop.nm_dispatcher' failed.", ].join("\n") + "\n", }); const findings = await journal.run(ctx); assert.ok(!findings.some((f) => f.code === "journal/errors"), "it retries and succeeds"); }); test("journal: system-sleep failures are deferred to the suspend check", async () => { const ctx = stubCtx({ "journalctl -p err --since \"-24 hours\" --no-pager -o short 2>/dev/null": `Aug 15 15:49:09 bazzite (system-sleep)[11514]: /usr/lib/systemd/system-sleep/fw-fanctrl-suspend failed with exit status 1.`, }); const findings = await journal.run(ctx); assert.equal(findings.length, 0, "the suspend check owns system-sleep failures"); }); test("journal: MCE lines are deferred to the hardware check", async () => { const ctx = stubCtx({ "journalctl -p err --since \"-24 hours\" --no-pager -o short 2>/dev/null": `Aug 13 03:11:22 bazzite kernel: mce: [Hardware Error]: Machine check events logged`, }); const findings = await journal.run(ctx); assert.equal(findings.length, 0, "the hardware check owns MCE lines"); }); test("journal: unit failures are deferred to the services check", async () => { const ctx = stubCtx({ "journalctl -p err --since \"-24 hours\" --no-pager -o short 2>/dev/null": `Aug 15 16:02:11 bazzite systemd[1]: homebrew.clamav.service: Failed with result 'exit-code'.\nAug 15 16:02:11 bazzite systemd[1]: Failed to start Homebrew ClamAV.`, }); const findings = await journal.run(ctx); assert.equal(findings.length, 0, "the services check owns unit failures — one problem, one finding"); }); test("journal: missing journalctl (non-systemd) is explained, not silent", async () => { const ctx = stubCtx({}); ctx.run = async () => ({ ok: false, code: -1, stdout: "", stderr: "", missing: true }); const findings = await journal.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "info"); assert.match(findings[0].title, /log check skipped/i); }); test("gpu: NVIDIA present but no driver loaded → medium", async () => { const ctx = stubCtx({ "lspci -nn 2>/dev/null | grep -iE 'vga|3d|display'": "01:00.0 VGA compatible controller [0300]: NVIDIA Corporation GA106 [GeForce RTX 3060] [10de:2503]", "lsmod 2>/dev/null | awk '{print $1}' | grep -iE '^(nvidia|nouveau|amdgpu|i915|xe)$'": "", "ls /dev/dri/ 2>/dev/null": "card0\nrenderD128", "glxinfo -B 2>/dev/null | grep -i 'renderer string'": "", }); const findings = await gpu.run(ctx); const med = findings.find((f) => f.severity === "medium"); assert.ok(med, "expected a medium-severity finding"); assert.match(med.title, /no driver is loaded/i); }); test("gpu: NVIDIA with proprietary driver → info", async () => { const ctx = stubCtx({ "lspci -nn 2>/dev/null | grep -iE 'vga|3d|display'": "01:00.0 VGA compatible controller [0300]: NVIDIA Corporation GA106 [10de:2503]", "lsmod 2>/dev/null | awk '{print $1}' | grep -iE '^(nvidia|nouveau|amdgpu|i915|xe)$'": "nvidia", "ls /dev/dri/ 2>/dev/null": "card0\nrenderD128", "glxinfo -B 2>/dev/null | grep -i 'renderer string'": "", "cat /proc/driver/nvidia/version 2>/dev/null": "NVRM version: NVIDIA UNIX x86_64 Kernel Module 570.124", }); const findings = await gpu.run(ctx); const info = findings.find((f) => f.severity === "info"); assert.ok(info); assert.match(info.title, /proprietary driver is loaded/i); assert.match(info.evidence, /570\.124/); }); test("gpu: NVIDIA with nouveau → medium", async () => { const ctx = stubCtx({ "lspci -nn 2>/dev/null | grep -iE 'vga|3d|display'": "01:00.0 VGA compatible controller [0300]: NVIDIA Corporation GA106 [10de:2503]", "lsmod 2>/dev/null | awk '{print $1}' | grep -iE '^(nvidia|nouveau|amdgpu|i915|xe)$'": "nouveau", "ls /dev/dri/ 2>/dev/null": "card0\nrenderD128", "glxinfo -B 2>/dev/null | grep -i 'renderer string'": "", }); const findings = await gpu.run(ctx); const med = findings.find((f) => f.severity === "medium"); assert.ok(med); assert.match(med.title, /nouveau/i); }); test("gpu: AMD with amdgpu driver → info", async () => { const ctx = stubCtx({ "lspci -nn 2>/dev/null | grep -iE 'vga|3d|display'": "06:00.0 VGA compatible controller [0300]: Advanced Micro Devices, Inc. [AMD/ATI] Navi 22 [Radeon RX 6700 XT]", "lsmod 2>/dev/null | awk '{print $1}' | grep -iE '^(nvidia|nouveau|amdgpu|i915|xe)$'": "amdgpu", "ls /dev/dri/ 2>/dev/null": "card0\nrenderD128", "glxinfo -B 2>/dev/null | grep -i 'renderer string'": "", }); const findings = await gpu.run(ctx); const info = findings.find((f) => f.severity === "info"); assert.ok(info); assert.match(info.title, /driver is working/i); }); test("gpu: AMD with built-in amdgpu (not in lsmod) is NOT a missing driver", async () => { // amdgpu is often compiled INTO the kernel, so lsmod shows nothing even // though the driver is active. A working display means it is fine. const ctx = stubCtx({ "lspci -nn 2>/dev/null | grep -iE 'vga|3d|display'": "06:00.0 VGA compatible controller [0300]: Advanced Micro Devices, Inc. [AMD/ATI] Navi 22 [Radeon RX 6700 XT]", "lsmod 2>/dev/null | awk '{print $1}' | grep -iE '^(nvidia|nouveau|amdgpu|i915|xe)$'": "", "ls /dev/dri/ 2>/dev/null": "card0\nrenderD128", "glxinfo -B 2>/dev/null | grep -i 'renderer string'": "renderer string: AMD Radeon RX 6700 XT (RADV NAVI22)", }); const findings = await gpu.run(ctx); assert.ok(!findings.some((f) => f.severity === "medium"), "built-in amdgpu must not be reported as 'driver not loaded'"); const info = findings.find((f) => f.severity === "info"); assert.ok(info, "expected an informational driver finding"); }); test("gpu: AMD GPU with no working driver is medium", async () => { // No DRI and no usable renderer → the driver really is not driving the card. const ctx = stubCtx({ "lspci -nn 2>/dev/null | grep -iE 'vga|3d|display'": "06:00.0 VGA compatible controller [0300]: Advanced Micro Devices, Inc. [AMD/ATI] Navi 22 [Radeon RX 6700 XT]", "lsmod 2>/dev/null | awk '{print $1}' | grep -iE '^(nvidia|nouveau|amdgpu|i915|xe)$'": "", "ls /dev/dri/ 2>/dev/null": "", "glxinfo -B 2>/dev/null | grep -i 'renderer string'": "renderer string: llvmpipe (LLVM 19.1.4, 256 bits)", }); const findings = await gpu.run(ctx); const med = findings.find((f) => f.severity === "medium"); assert.ok(med, "expected a medium finding when the driver genuinely is not working"); assert.match(med.title, /amdgpu driver is not loaded/i); }); test("gpu: software rendering (llvmpipe) → medium", async () => { const ctx = stubCtx({ "lspci -nn 2>/dev/null | grep -iE 'vga|3d|display'": "00:02.0 VGA compatible controller [0300]: Intel Corporation UHD Graphics", "lsmod 2>/dev/null | awk '{print $1}' | grep -iE '^(nvidia|nouveau|amdgpu|i915|xe)$'": "i915", "ls /dev/dri/ 2>/dev/null": "card0\nrenderD128", "glxinfo -B 2>/dev/null | grep -i 'renderer string'": "renderer string: llvmpipe (LLVM 19.1.4, 256 bits)", }); const findings = await gpu.run(ctx); const med = findings.find((f) => f.severity === "medium"); assert.ok(med, "software rendering must be flagged"); assert.match(med.title, /software rendering/i); }); test("countBySeverity buckets findings", () => { const counts = countBySeverity([ { severity: "high" }, { severity: "high" }, { severity: "medium" }, { severity: "info" }, ]); assert.deepEqual(counts, [ { severity: "high", count: 2 }, { severity: "medium", count: 1 }, { severity: "info", count: 1 }, ]); }); test("updates: apt counts pending updates correctly", async () => { const ctx = stubCtx({ "apt-get -s upgrade 2>&1": "Inst libssl3 [3.0.13-1] (3.0.14-1 Debian:stable-updates [amd64])\nInst firefox [130.0] (131.0 Debian:stable [amd64])\nInst vim [9.0] (9.1 Debian:stable [amd64])\n", }); ctx.osRelease = { id: "debian", id_like: "" }; ctx.dist = detectDistro(ctx.osRelease); const findings = await updates.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "info"); assert.match(findings[0].title, /3 update/i); assert.match(findings[0].fix, /apt upgrade/); }); test("updates: apt lock/error is silent, never 'up to date'", async () => { const ctx = stubCtx({ "apt-get -s upgrade 2>&1": "E: Could not get lock /var/lib/dpkg/lock-frontend. It is held by process 123 (unattended-upgr)\n", }); ctx.osRelease = { id: "debian", id_like: "" }; ctx.dist = detectDistro(ctx.osRelease); const findings = await updates.run(ctx); assert.equal(findings.length, 0, "a locked apt must not be reported as 'up to date'"); }); test("updates: dnf exit code 100 (updates available) is counted", async () => { const ctx = { osRelease: { id: "fedora", id_like: "fedora" }, dist: detectDistro({ id: "fedora", id_like: "fedora" }), run: async (cmd) => { if (cmd.startsWith("dnf check-update")) { return { ok: false, code: 100, stdout: "kernel.x86_64 6.9.0-1 fedora\nfirefox.x86_64 130.0-1 fedora\n", stderr: "" }; } return { ok: false, code: 1, stdout: "", stderr: "" }; }, }; const findings = await updates.run(ctx); assert.equal(findings.length, 1); assert.match(findings[0].title, /2 update/i); }); // Regression: `dnf check-update` prints a "Upgrades" header, an indented // continuation line under "Obsoleting packages", and one entry per enabled // repo — with updates + updates-archive that is the same package twice. The old // fallback counted all of it, so 2 real updates read as 6. test("updates: dnf counts unique packages, not headers/continuations/repo duplicates", async () => { const dir = mkdtempSync(join(tmpdir(), "ld-updates-dnf-")); const prevCache = process.env.LINUX_DOCTOR_CACHE; const prevTtl = process.env.LINUX_DOCTOR_UPDATES_TTL_MS; process.env.LINUX_DOCTOR_CACHE = dir; process.env.LINUX_DOCTOR_UPDATES_TTL_MS = "3600000"; try { const stdout = [ "Upgrades", "firefox.x86_64 130.0-1.fc44 updates", "firefox.x86_64 130.0-1.fc44 updates-archive", "kernel.x86_64 6.9.0-1.fc44 updates", "kernel.x86_64 6.9.0-1.fc44 updates-archive", "Obsoleting packages", " oldpkg.x86_64 1.0-1.fc44 updates", ].join("\n") + "\n"; const ctx = { osRelease: { id: "fedora", id_like: "fedora" }, dist: detectDistro({ id: "fedora", id_like: "fedora" }), run: async (cmd) => { if (cmd.startsWith("dnf check-update")) return { ok: false, code: 100, stdout, stderr: "" }; return { ok: false, code: 1, stdout: "", stderr: "" }; }, }; const findings = await updates.run(ctx); assert.equal(findings.length, 1); assert.match(findings[0].title, /2 update/i); } finally { if (prevCache === undefined) delete process.env.LINUX_DOCTOR_CACHE; else process.env.LINUX_DOCTOR_CACHE = prevCache; if (prevTtl === undefined) delete process.env.LINUX_DOCTOR_UPDATES_TTL_MS; else process.env.LINUX_DOCTOR_UPDATES_TTL_MS = prevTtl; rmSync(dir, { recursive: true, force: true }); } }); test("updates: a failed check stays silent instead of claiming up to date", async () => { const ctx = { osRelease: { id: "debian", id_like: "" }, dist: detectDistro({ id: "debian", id_like: "" }), run: async () => ({ ok: false, code: 100, stdout: "", stderr: "" }), }; const findings = await updates.run(ctx); assert.equal(findings.length, 0); }); test("updates: unknown distro family is skipped with info", async () => { const ctx = stubCtx({}); ctx.osRelease = { id: "plan9", id_like: "" }; ctx.dist = detectDistro(ctx.osRelease); const findings = await updates.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "info"); assert.match(findings[0].title, /skipped/i); }); test("updates: rpm-ostree (Bazzite) reports an available image update", async () => { const ctx = { osRelease: { id: "bazzite", id_like: "fedora" }, dist: detectDistro({ id: "bazzite", id_like: "fedora" }), run: async (cmd) => { if (cmd.startsWith("rpm-ostree upgrade --check")) { return { ok: true, code: 0, stdout: "Note: --check and --preview may be unreliable.\nAvailable update: 42.20260815.0 (checksum 1a2b3c)\n", stderr: "" }; } return { ok: false, code: 1, stdout: "", stderr: "" }; }, }; const findings = await updates.run(ctx); assert.equal(findings.length, 1); assert.match(findings[0].title, /1 update/i); assert.match(findings[0].fix, /rpm-ostree upgrade/); assert.match(findings[0].fix, /Reboot/); }); test("updates: rpm-ostree with no update available is up to date", async () => { const ctx = { osRelease: { id: "bazzite", id_like: "fedora" }, dist: detectDistro({ id: "bazzite", id_like: "fedora" }), run: async (cmd) => ({ ok: true, code: 0, stdout: "No updates available.\n", stderr: "" }), }; const findings = await updates.run(ctx); assert.equal(findings.length, 1); assert.match(findings[0].title, /up to date/i); }); test("updates: openSUSE (zypper) counts pending updates", async () => { const ctx = { osRelease: { id: "opensuse-tumbleweed", id_like: "suse" }, dist: detectDistro({ id: "opensuse-tumbleweed", id_like: "suse" }), run: async (cmd) => { if (cmd.startsWith("zypper -q lu")) { return { ok: true, code: 0, stdout: "S | Repository | Name | Current Version | Available Version | Arch\n" + "---+----------------------------+----------+-----------------+-------------------+-------\n" + "v | openSUSE-Tumbleweed-Oss | aaa_base | 1.0-1 | 1.1-1 | x86_64\n" + "v | openSUSE-Tumbleweed-Oss | bbb | 1.0-1 | 1.1-1 | x86_64\n" + "v | openSUSE-Tumbleweed-Oss | ccc | 1.0-1 | 1.1-1 | x86_64\n" + "v | openSUSE-Tumbleweed-Oss | ddd | 1.0-1 | 1.1-1 | x86_64\n", stderr: "" }; } return { ok: false, code: 1, stdout: "", stderr: "" }; }, }; const findings = await updates.run(ctx); assert.equal(findings.length, 1); assert.match(findings[0].title, /4 update/i); // Tumbleweed is rolling: `zypper up` is the wrong verb there, `dup` is the // supported full upgrade. assert.match(findings[0].fix, /zypper dup/); }); test("updates: openSUSE Leap keeps 'zypper update' (only Tumbleweed needs dup)", async () => { const ctx = { osRelease: { id: "opensuse-leap", id_like: "suse" }, dist: detectDistro({ id: "opensuse-leap", id_like: "suse" }), run: async (cmd) => { if (cmd.startsWith("zypper -q lu")) { return { ok: true, code: 0, stdout: "S | Repository | Name | Current Version | Available Version | Arch\n" + "---+----------------------------+----------+-----------------+-------------------+-------\n" + "v | openSUSE-Leap-Oss | aaa_base | 1.0-1 | 1.1-1 | x86_64\n", stderr: "" }; } return { ok: false, code: 1, stdout: "", stderr: "" }; }, }; const findings = await updates.run(ctx); assert.match(findings[0].fix, /zypper update/); assert.doesNotMatch(findings[0].fix, /zypper dup/); }); test("updates: the apk header is not counted as a package", async () => { // `apk version -l '<'` always prints "Installed: ... Available:" first. Counting // every non-empty line would make an up-to-date Alpine report "1 update". const ctx = { osRelease: { id: "alpine", id_like: "" }, dist: detectDistro({ id: "alpine", id_like: "" }), run: async (cmd) => { if (cmd.startsWith("apk version -l")) return { ok: true, code: 0, stdout: "Installed: Available:\n", stderr: "" }; return { ok: false, code: 1, stdout: "", stderr: "" }; }, }; const findings = await updates.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].code, "updates/none", `header must not become a finding: ${JSON.stringify(findings)}`); }); test("updates: Void (xbps) counts pending updates", async () => { // Void had no update branch at all, so a machine with 54 pending updates was // silently skipped and scored as if it were current. const ctx = { osRelease: { id: "void", id_like: "" }, dist: detectDistro({ id: "void", id_like: "" }), run: async (cmd) => { if (cmd.startsWith("xbps-install -un")) { return { ok: true, code: 0, stdout: "acl-2.4.0_1 update x86_64 https://repo-default.voidlinux.org/current 43080 20653\n" + "attr-2.6.0_1 update x86_64 https://repo-default.voidlinux.org/current 27348 9495\n" + "bzip2-1.0.8_2 update x86_64 https://repo-default.voidlinux.org/current 150551 61517\n", stderr: "" }; } return { ok: false, code: 1, stdout: "", stderr: "" }; }, }; const findings = await updates.run(ctx); assert.equal(findings.length, 1); assert.match(findings[0].title, /3 update/i); assert.match(findings[0].fix, /xbps-install -Su/); }); test("updates: apt with an empty index says 'could not check', not 'up to date'", async () => { // A fresh minimal image (and a machine that never ran `apt update`) answers // "0 upgraded" with a zero exit status. That is not being up to date, and // reporting it as such is the dangerous direction of being wrong. const ctx = stubCtx({ "apt-get -s upgrade 2>&1": "Reading state information...\nCalculating upgrade...\n0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.\n", }); ctx.osRelease = { id: "debian", id_like: "" }; ctx.dist = detectDistro(ctx.osRelease); const findings = await updates.run(ctx); assert.equal(findings[0].code, "updates/stale", `an empty index is not 'up to date': ${JSON.stringify(findings)}`); assert.match(findings[0].fix, /apt update/); }); test("updates: results are cached within the TTL (second run does not re-exec)", async () => { const dir = mkdtempSync(join(tmpdir(), "ld-updates-cache-")); const prevCache = process.env.LINUX_DOCTOR_CACHE; const prevTtl = process.env.LINUX_DOCTOR_UPDATES_TTL_MS; process.env.LINUX_DOCTOR_CACHE = dir; process.env.LINUX_DOCTOR_UPDATES_TTL_MS = "3600000"; try { let calls = 0; const ctx = { osRelease: { id: "fedora", id_like: "fedora" }, dist: detectDistro({ id: "fedora", id_like: "fedora" }), run: async (cmd) => { calls += 1; if (cmd.startsWith("dnf check-update")) { return { ok: false, code: 100, stdout: "firefox.x86_64 130.0-1 fedora\n", stderr: "" }; } return { ok: false, code: 1, stdout: "", stderr: "" }; }, }; const first = await updates.run(ctx); assert.match(first[0].title, /1 update/); assert.equal(calls, 1); const second = await updates.run(ctx); assert.match(second[0].title, /1 update/); assert.equal(calls, 1, "second run must come from the cache, not re-exec"); } finally { if (prevCache === undefined) delete process.env.LINUX_DOCTOR_CACHE; else process.env.LINUX_DOCTOR_CACHE = prevCache; if (prevTtl === undefined) delete process.env.LINUX_DOCTOR_UPDATES_TTL_MS; else process.env.LINUX_DOCTOR_UPDATES_TTL_MS = prevTtl; rmSync(dir, { recursive: true, force: true }); } }); test("updates: a failed check is not cached (next run retries)", async () => { const dir = mkdtempSync(join(tmpdir(), "ld-updates-cache-")); const prevCache = process.env.LINUX_DOCTOR_CACHE; const prevTtl = process.env.LINUX_DOCTOR_UPDATES_TTL_MS; process.env.LINUX_DOCTOR_CACHE = dir; process.env.LINUX_DOCTOR_UPDATES_TTL_MS = "3600000"; try { let calls = 0; const ctx = { osRelease: { id: "fedora", id_like: "fedora" }, dist: detectDistro({ id: "fedora", id_like: "fedora" }), run: async () => { calls += 1; return { ok: false, code: 1, stdout: "", stderr: "" }; }, }; const first = await updates.run(ctx); assert.equal(first.length, 0); const second = await updates.run(ctx); assert.equal(second.length, 0); // A failed determination is not cached, so the second run re-executes // instead of serving a stale "up to date" from the cache. assert.equal(calls, 2); } finally { if (prevCache === undefined) delete process.env.LINUX_DOCTOR_CACHE; else process.env.LINUX_DOCTOR_CACHE = prevCache; if (prevTtl === undefined) delete process.env.LINUX_DOCTOR_UPDATES_TTL_MS; else process.env.LINUX_DOCTOR_UPDATES_TTL_MS = prevTtl; rmSync(dir, { recursive: true, force: true }); } }); test("updates: Alpine (apk) counts upgradable packages", async () => { const ctx = { osRelease: { id: "alpine", id_like: "" }, dist: detectDistro({ id: "alpine", id_like: "" }), run: async (cmd) => { if (cmd.startsWith("apk version -l")) { return { ok: true, code: 0, stdout: "Installed: Available:\n" + "musl-1.2.4-r2 < 1.2.5-r0\n" + "openssl-3.0.1-r0 < 3.0.2-r0\n" + "busybox-1.36.0-r0 < 1.36.1-r0\n", stderr: "" }; } return { ok: false, code: 1, stdout: "", stderr: "" }; }, }; const findings = await updates.run(ctx); assert.equal(findings.length, 1); assert.match(findings[0].title, /3 update/i); assert.match(findings[0].fix, /apk upgrade/); }); test("security: active firewall and SELinux enforcing are reported", async () => { const ctx = stubCtx({ "systemctl is-active firewalld 2>/dev/null": "active\n", "systemctl is-active ufw 2>/dev/null": "inactive\n", "nft list ruleset 2>/dev/null | head -5": "", "getenforce 2>/dev/null": "Enforcing\n", "systemctl is-active packagekit 2>/dev/null || systemctl is-active dnf-makecache 2>/dev/null": "inactive\n", }); const findings = await security.run(ctx); const titles = findings.map((f) => f.title).join(" "); assert.match(titles, /Firewall is active/); assert.match(titles, /SELinux is enforcing/); assert.ok(!findings.some((f) => f.severity === "medium"), "no medium finding when the firewall is up"); }); test("security: no firewall detected → informational finding", async () => { const ctx = stubCtx({ "systemctl is-active firewalld 2>/dev/null": "inactive\n", "systemctl is-active ufw 2>/dev/null": "inactive\n", "nft list ruleset 2>/dev/null | head -5": "", "getenforce 2>/dev/null": "", "cat /sys/kernel/security/apparmor/profiles 2>/dev/null | head -3": "", "systemctl is-active packagekit 2>/dev/null || systemctl is-active dnf-makecache 2>/dev/null": "inactive\n", }); const findings = await security.run(ctx); const info = findings.find((f) => f.title.includes("No active firewall")); assert.ok(info, "expected a firewall finding"); assert.equal(info.severity, "info", "absence of optional hardening is informational, not a fault"); }); test("security: AppArmor is reported on Debian-family systems", async () => { const ctx = stubCtx({ "systemctl is-active firewalld 2>/dev/null": "active\n", "systemctl is-active ufw 2>/dev/null": "inactive\n", "nft list ruleset 2>/dev/null | head -5": "", "getenforce 2>/dev/null": "", "cat /sys/kernel/security/apparmor/profiles 2>/dev/null | head -3": "docker-default (enforce)\nsnap.discord.discord (enforce)\n", "systemctl is-active packagekit 2>/dev/null || systemctl is-active dnf-makecache 2>/dev/null": "inactive\n", }); const findings = await security.run(ctx); const aa = findings.find((f) => f.title.includes("AppArmor")); assert.ok(aa, "expected an AppArmor finding"); assert.equal(aa.severity, "info"); assert.match(aa.evidence, /docker-default/); }); test("security: unreadable nftables without root is 'unknown', not 'no firewall'", async () => { const ctx = stubCtx({ "systemctl is-active firewalld 2>/dev/null": "inactive\n", "systemctl is-active ufw 2>/dev/null": "inactive\n", "systemctl is-active nftables 2>/dev/null": "inactive\n", // The binary is there; reading the ruleset needs CAP_NET_ADMIN, so the // nft command itself fails. (The probe used to end in `| head -5`, whose // exit code is 0 — so this case used to read as "readable and empty", // i.e. "no firewall".) "command -v nft 2>/dev/null": "/usr/sbin/nft\n", // `nft list ruleset 2>/dev/null` intentionally unstubbed: it fails without root. "getenforce 2>/dev/null": "", "cat /sys/kernel/security/apparmor/profiles 2>/dev/null | head -3": "", "systemctl is-active packagekit 2>/dev/null || systemctl is-active dnf-makecache 2>/dev/null": "inactive\n", }); const findings = await security.run(ctx); const unknown = findings.find((f) => f.code === "security/firewall-unknown"); assert.ok(unknown, "expected a firewall-unknown finding"); assert.equal(unknown.severity, "info"); assert.equal(unknown.fix, null, "unknown must never carry a fix that could enable a firewall"); assert.ok(!findings.some((f) => f.code === "security/no-firewall"), "must not claim there is no firewall"); }); test("security: nftables not installed is a determinate 'no firewall', not 'unknown'", async () => { const ctx = stubCtx({ "systemctl is-active firewalld 2>/dev/null": "inactive\n", "systemctl is-active ufw 2>/dev/null": "inactive\n", "systemctl is-active nftables 2>/dev/null": "inactive\n", "command -v nft 2>/dev/null": "", "getenforce 2>/dev/null": "", "cat /sys/kernel/security/apparmor/profiles 2>/dev/null | head -3": "", "systemctl is-active packagekit 2>/dev/null || systemctl is-active dnf-makecache 2>/dev/null": "inactive\n", }); const findings = await security.run(ctx); assert.ok(findings.some((f) => f.code === "security/no-firewall"), "no nftables and no service is a determinate answer"); assert.ok(!findings.some((f) => f.code === "security/firewall-unknown"), "a missing tool is not an unreadable ruleset"); }); test("security: a readable, non-empty ruleset counts as an active firewall", async () => { const ctx = stubCtx({ "systemctl is-active firewalld 2>/dev/null": "inactive\n", "systemctl is-active ufw 2>/dev/null": "inactive\n", "systemctl is-active nftables 2>/dev/null": "inactive\n", "command -v nft 2>/dev/null": "/usr/sbin/nft\n", "nft list ruleset 2>/dev/null": "table inet filter {\n\tchain input {\n\t\ttype filter hook input priority 0; policy drop;\n\t}\n}\n", "getenforce 2>/dev/null": "", "cat /sys/kernel/security/apparmor/profiles 2>/dev/null | head -3": "", "systemctl is-active packagekit 2>/dev/null || systemctl is-active dnf-makecache 2>/dev/null": "inactive\n", }); const findings = await security.run(ctx); const fw = findings.find((f) => f.code === "security/firewall"); assert.ok(fw, "rules present means the firewall is active"); assert.match(fw.evidence, /nftables rules present/); }); test("processes: a single app over 20% of RAM is flagged medium", async () => { const ctx = stubCtx({ // ps -o rss reports KiB: 4000000 KiB ≈ 3.8 GB of 15 GB (~25%). "command -v ps 2>/dev/null": "/usr/bin/ps\n", "ps -eo pid=,rss=,args= 2>/dev/null": `1 4000000 brave\n2 800000 firefox\n3 500000 plasma\n`, "free -b": ` total used free shared buff/cache available\nMem: 16106127360 12000000000 500000000 500000000 4718592000 1500000000\nSwap: 8267812045 0 8267812045`, }); const findings = await processes.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "medium"); assert.match(findings[0].title, /using a lot of memory/); }); test("processes: a single app over 40% of RAM is flagged medium", async () => { const ctx = stubCtx({ // 8000000 KiB ≈ 7.6 GB of 15 GB (~50%). "command -v ps 2>/dev/null": "/usr/bin/ps\n", "ps -eo pid=,rss=,args= 2>/dev/null": `1 8000000 brave\n2 800000 firefox\n3 500000 plasma\n`, "free -b": ` total used free shared buff/cache available\nMem: 16106127360 14000000000 500000000 500000000 4718592000 1500000000\nSwap: 8267812045 0 8267812045`, }); const findings = await processes.run(ctx); assert.equal(findings[0].severity, "medium"); assert.match(findings[0].title, /huge amount of memory/); }); test("processes: RSS is summed per app, so a multi-process app is not split", async () => { // Brave spreads its memory across processes that share one binary. The check // listed "brave" twice and reported the largest process as if it were the // app. Summed: 3M + 1.5M KiB = 4.5M KiB ≈ 28.6% of 16GB. One process alone // (19.1%) stayed under the warn threshold, so the split hid the footprint. const ctx = stubCtx({ "command -v ps 2>/dev/null": "/usr/bin/ps\n", "ps -eo pid=,rss=,args= 2>/dev/null": `1 3000000 brave --type=renderer\n2 1500000 brave\n3 800000 firefox\n`, "free -b": ` total used free shared buff/cache available\nMem: 16106127360 12000000000 500000000 500000000 4718592000 1500000000\nSwap: 8267812045 0 8267812045`, }); const findings = await processes.run(ctx); assert.equal(findings[0].code, "processes/warn", `expected the summed app: ${JSON.stringify(findings.map((f) => f.code))}`); const braveRows = findings[0].evidence.split("\n").filter((l) => /^brave\t/.test(l)); assert.equal(braveRows.length, 1, `brave must appear once: ${findings[0].evidence}`); }); test("processes: shared pages count once via PSS, not once per process", async () => { // The false positive that started this: 64 Brave processes summed to 8.19 GB // of RSS while their unique footprint (PSS) was 4.16 GB — 53% against the 40% // threshold became 27%, below it. With PSS the check must not claim the app is // "using 8 GB of 15.4 GB". const ctx = stubCtx({ "command -v ps 2>/dev/null": "/usr/bin/ps\n", "ps -eo pid=,rss=,args= 2>/dev/null": `1 8000000 brave --type=renderer\n2 4000000 brave --type=gpu\n3 1000000 firefox\n`, "grep -m1 -H '^Pss:' /proc/[0-9]*/smaps_rollup 2>/dev/null": "/proc/1/smaps_rollup:Pss:\t1800000 kB\n/proc/2/smaps_rollup:Pss:\t900000 kB\n/proc/3/smaps_rollup:Pss:\t400000 kB\n", "free -b": ` total used free shared buff/cache available\nMem: 16106127360 14000000000 500000000 500000000 4718592000 1500000000\nSwap: 8267812045 0 8267812045`, }); const findings = await processes.run(ctx); // brave: 2.7M KiB PSS ≈ 2.6 GB ≈ 17% of RAM, under the 20% warn threshold, // though its summed RSS (12M KiB ≈ 11.4 GB ≈ 71%) would have been "huge". assert.equal(findings[0].code, "processes/ok", `PSS must be used when present: ${JSON.stringify(findings.map((f) => f.code))}`); const braveRows = findings[0].evidence.split("\n").filter((l) => /^brave\t/.test(l)); assert.equal(braveRows.length, 1); }); test("processes: unreadable smaps_rollup falls back to RSS", async () => { const ctx = stubCtx({ "command -v ps 2>/dev/null": "/usr/bin/ps\n", "ps -eo pid=,rss=,args= 2>/dev/null": `1 8000000 brave\n2 800000 firefox\n`, // grep prints nothing: another user's processes, or an old kernel "free -b": ` total used free shared buff/cache available\nMem: 16106127360 14000000000 500000000 500000000 4718592000 1500000000\nSwap: 8267812045 0 8267812045`, }); const findings = await processes.run(ctx); assert.equal(findings[0].code, "processes/high", "without PSS the RSS path must still work"); }); test("processes: a large app on a box with free RAM is informational, not a warning", async () => { // The false alarm a 15GB box got: Brave at ~19% while 9.4GB was free. Here // the top app is 5M KiB ≈ 32% of RAM, over the 20% warn threshold, but 58% // of RAM is available — so it is the top consumer, not a problem. const ctx = stubCtx({ "command -v ps 2>/dev/null": "/usr/bin/ps\n", "ps -eo pid=,rss=,args= 2>/dev/null": `1 5000000 brave\n2 800000 firefox\n`, "free -b": ` total used free shared buff/cache available\nMem: 16106127360 6000000000 5000000000 147000000 4900000000 9400000000\nSwap: 8267812045 0 8267812045`, }); const findings = await processes.run(ctx); assert.equal(findings[0].code, "processes/ok", `a big app with free RAM is not a warning: ${JSON.stringify(findings.map((f) => f.code))}`); assert.equal(findings[0].severity, "info"); }); test("processes: healthy memory usage produces an info finding", async () => { const ctx = stubCtx({ "command -v ps 2>/dev/null": "/usr/bin/ps\n", "ps -eo pid=,rss=,args= 2>/dev/null": `1 500000 plasma\n2 400000 firefox\n3 300000 brave\n`, "free -b": ` total used free shared buff/cache available\nMem: 16106127360 5000000000 1000000000 300000000 7000000000 11000000000\nSwap: 8267812045 0 8267812045`, }); const findings = await processes.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "info"); assert.match(findings[0].title, /Top memory consumers/); }); test("suspend: failing system-sleep hooks are surfaced as medium", async () => { const ctx = stubCtx({ "journalctl -g \"system-sleep.*failed\" --since \"-7 days\" --no-pager -o short 2>/dev/null": `Aug 10 22:11:03 bazzite (system-sleep)[11514]: /usr/lib/systemd/system-sleep/fw-fanctrl-suspend failed with exit status 1.\nAug 11 07:30:12 bazzite (system-sleep)[11514]: /usr/lib/systemd/system-sleep/fw-fanctrl-suspend failed with exit status 1.`, }); const findings = await suspend.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "medium"); assert.match(findings[0].evidence, /fw-fanctrl-suspend/); }); test("suspend: clean log produces no finding", async () => { const ctx = stubCtx({ "journalctl -g \"system-sleep.*failed\" --since \"-7 days\" --no-pager -o short 2>/dev/null": "", }); const findings = await suspend.run(ctx); assert.equal(findings.length, 0); }); test("suspend: boot separators alone are NOT a failing hook", async () => { // journalctl -g prints "-- Boot ... --" separators for every boot in the // window even when nothing matched — this used to produce a false positive. const ctx = stubCtx({ "journalctl -g \"system-sleep.*failed\" --since \"-7 days\" --no-pager -o short 2>/dev/null": "", }); const findings = await suspend.run(ctx); assert.equal(findings.length, 0, "separator-only output must not be a finding"); }); test("journal: benign kernel noise (i2c address) is not an error finding", async () => { const ctx = stubCtx({ "journalctl -p err --since \"-24 hours\" --no-pager -o short 2>/dev/null": `Aug 15 14:32:47 bazzite kernel: i2c i2c-1: Invalid 7-bit I2C address 0xffff`, }); const findings = await journal.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "info"); assert.match(findings[0].title, /routine noise/i); assert.ok(!findings.some((f) => /recognized error|unrecognized log entry/i.test(f.title)), "known noise must not be reported as an error"); }); test("journal: unrecognized entries stay informational, not medium", async () => { const ctx = stubCtx({ "journalctl -p err --since \"-24 hours\" --no-pager -o short 2>/dev/null": `Aug 15 14:32:47 bazzite app[123]: Something unknown and weird happened\nAug 15 14:33:01 bazzite app[456]: Another unknown message\n`, }); const findings = await journal.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "info", "unrecognized entries must not escalate to medium"); assert.match(findings[0].title, /unrecognized log entr/i); }); // Regression: `-o short` prints a crash and its stack trace as one timestamped // head line followed by indented continuation lines. Counting every // continuation as its own entry turned a handful of crashes into a // "3062 unrecognized log entries" finding. test("journal: a multi-line entry counts once, not once per stack line", async () => { const ctx = stubCtx({ "journalctl -p err --since \"-24 hours\" --no-pager -o short 2>/dev/null": "Aug 15 14:32:47 bazzite plasmashell[123]: Something unknown happened\n" + " Stack trace of thread 123:\n" + " #0 0x7f0 in __syscall_cancel_arch (libc.so.6)\n" + " #1 0x7f0 in start_thread (libc.so.6)\n", }); const findings = await journal.run(ctx); assert.equal(findings.length, 1); assert.match(findings[0].title, /^1 unrecognized log entry/); }); test("battery: low battery is flagged medium", async () => { const ctx = stubCtx({ "ls /sys/class/power_supply/ 2>/dev/null": "AC\nBAT0\n", "cat /sys/class/power_supply/BAT0/type 2>/dev/null": "Battery\n", "cat /sys/class/power_supply/BAT0/capacity 2>/dev/null": "12\n", "cat /sys/class/power_supply/BAT0/status 2>/dev/null": "Discharging\n", }); const findings = await battery.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "medium"); assert.match(findings[0].title, /very low/); }); test("battery: healthy battery is informational", async () => { const ctx = stubCtx({ "ls /sys/class/power_supply/ 2>/dev/null": "AC\nBAT0\n", "cat /sys/class/power_supply/BAT0/type 2>/dev/null": "Battery\n", "cat /sys/class/power_supply/BAT0/capacity 2>/dev/null": "87\n", "cat /sys/class/power_supply/BAT0/status 2>/dev/null": "Charging\n", }); const findings = await battery.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "info"); assert.match(findings[0].detail, /87%/); }); test("battery: desktop with no battery is skipped with info", async () => { const ctx = stubCtx({ "ls /sys/class/power_supply/ 2>/dev/null": "AC\n", }); const findings = await battery.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "info"); assert.match(findings[0].title, /No battery detected/); }); test("services: non-systemd system explains why the check is skipped", async () => { const ctx = { osRelease: { id: "alpine", id_like: "" }, run: async (cmd) => { if (cmd.startsWith("systemctl")) return { ok: false, code: -1, stdout: "", stderr: "", missing: true }; if (cmd.startsWith("ps -p 1")) return { ok: true, code: 0, stdout: "openrc\n", stderr: "" }; return { ok: false, code: 1, stdout: "", stderr: "" }; }, }; const findings = await services.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "info"); assert.match(findings[0].title, /Non-systemd/); assert.match(findings[0].detail, /openrc/); }); test("firmware: pending fwupd updates are reported as medium", async () => { const ctx = stubCtx({ "command -v fwupdmgr 2>/dev/null": "/usr/bin/fwupdmgr\n", "systemctl is-active fwupd 2>/dev/null": "active\n", "fwupdmgr get-updates 2>/dev/null": "• System Firmware has updates: 1.2.3 → 1.2.4\n", }); const findings = await firmware.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "medium"); assert.match(findings[0].title, /1 firmware update/); assert.match(findings[0].fix, /fwupdmgr update/); }); test("firmware: no pending updates is informational", async () => { const ctx = stubCtx({ "command -v fwupdmgr 2>/dev/null": "/usr/bin/fwupdmgr\n", "systemctl is-active fwupd 2>/dev/null": "active\n", "fwupdmgr get-updates 2>/dev/null": "No updates available\n", }); const findings = await firmware.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "info"); assert.match(findings[0].title, /up to date/i); }); test("firmware: inactive daemon skips fwupdmgr and explains", async () => { const ran = []; const ctx = { osRelease: { id: "bazzite", id_like: "fedora" }, run: async (cmd) => { ran.push(cmd); if (cmd === "command -v fwupdmgr 2>/dev/null") return { ok: true, code: 0, stdout: "/usr/bin/fwupdmgr\n", stderr: "" }; if (cmd === "systemctl is-active fwupd 2>/dev/null") return { ok: true, code: 0, stdout: "inactive\n", stderr: "" }; return { ok: false, code: 1, stdout: "", stderr: "" }; }, }; const findings = await firmware.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "info"); assert.match(findings[0].title, /not checked/); assert.match(findings[0].fix, /enable --now fwupd/); assert.ok(!ran.some((c) => c.startsWith("fwupdmgr get-updates")), "fwupdmgr must not run when the daemon is inactive"); }); test("firmware: fwupd not installed stays silent", async () => { const ctx = { osRelease: { id: "bazzite", id_like: "fedora" }, run: async (cmd) => cmd.startsWith("fwupdmgr") ? { ok: false, code: -1, stdout: "", stderr: "", missing: true } : { ok: false, code: 1, stdout: "", stderr: "" }, }; const findings = await firmware.run(ctx); assert.equal(findings.length, 0); }); // Regression: `df -P /boot` prints the ROOT filesystem row when /boot is just a // directory on /, with the same columns as a real boot partition. A 95%-full // root therefore became a high "Boot partition (/boot) is nearly full". The row // is only trusted when its mount point is the one we asked about. test("boot: a plain /boot directory on the root filesystem is not a boot partition", async () => { const ctx = stubCtx({ "df -P /boot 2>/dev/null": "Filesystem Size Used Avail Use% Mounted on\n/dev/nvme0n1p2 100G 90G 5G 95% /\n", "df -P /boot/efi 2>/dev/null": "Filesystem Size Used Avail Use% Mounted on\n/dev/nvme0n1p1 511M 10M 501M 2% /boot/efi\n", "ls /boot/grub/grub.cfg /boot/grub2/grub.cfg /boot/loader/entries/*.conf 2>/dev/null | head -1": "/boot/grub2/grub.cfg\n", "ls /boot/efi/EFI/*/grub*.cfg /boot/efi/EFI/*/grubx64.efi 2>/dev/null | head -1": "/boot/efi/EFI/fedora/grubx64.efi\n", "test -d /boot 2>/dev/null && echo yes": "yes\n", "ls /boot/vmlinuz-* 2>/dev/null | head -1": "/boot/vmlinuz-6.9\n", }, { id: "debian", id_like: "debian" }); const findings = await boot.run(ctx); assert.ok(!findings.some((f) => f.code === "boot/full"), "the root filesystem must not be reported as /boot"); }); test("boot: a full separate /boot is high", async () => { const ctx = stubCtx({ "df -P /boot 2>/dev/null": "Filesystem Size Used Avail Use% Mounted on\n/dev/sda2 500M 480M 20M 96% /boot\n", "df -P /boot/efi 2>/dev/null | tail -1": "", "ls /boot/grub/grub.cfg /boot/grub2/grub.cfg /boot/loader/entries/*.conf 2>/dev/null | head -1": "/boot/grub/grub.cfg\n", "test -d /boot 2>/dev/null && echo yes": "yes\n", }, { id: "debian", id_like: "debian" }); const findings = await boot.run(ctx); assert.equal(findings[0].code, "boot/full"); assert.equal(findings[0].severity, "high"); assert.match(findings[0].evidence, /\/boot$/); }); test("secureboot: UEFI with Secure Boot enabled and TPM is informational", async () => { const ctx = stubCtx({ "ls /sys/firmware/efi 2>/dev/null | head -1": "efi\n", "od -An -tu1 /sys/firmware/efi/efivars/SecureBoot-8be4df61-93ca-11d2-aa0d-00e098032b8c 2>/dev/null": "8 0 0 0 1\n", "mokutil --sb-state 2>/dev/null": "SecureBoot enabled\n", "ls /sys/class/tpm/tpm0 2>/dev/null": "tpm0\n", }); const findings = await secureboot.run(ctx); const titles = findings.map((f) => f.title).join(" "); assert.match(titles, /Secure Boot is enabled/); assert.match(titles, /TPM is present/); assert.ok(!findings.some((f) => f.severity === "medium")); }); test("secureboot: disabled Secure Boot and missing TPM are informational", async () => { const ctx = stubCtx({ "ls /sys/firmware/efi 2>/dev/null | head -1": "efi\n", "mokutil --sb-state 2>/dev/null": "SecureBoot disabled\n", }); const findings = await secureboot.run(ctx); const infos = findings.filter((f) => f.severity === "info"); assert.equal(infos.length, 2, "absence of optional hardening is informational, not a fault"); assert.match(infos.map((f) => f.title).join(" "), /Secure Boot is disabled/); assert.match(infos.map((f) => f.title).join(" "), /No TPM detected/); }); test("secureboot: legacy BIOS boot is informational", async () => { const ctx = stubCtx({}); const findings = await secureboot.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "info"); assert.match(findings[0].title, /Legacy BIOS/); }); test("thermal: 95°C CPU is flagged high", async () => { const ctx = stubCtx({ 'for z in /sys/class/thermal/thermal_zone*; do [ -f "$z/type" ] && [ -f "$z/temp" ] && echo "$(cat "$z/type"):$(cat "$z/temp")"; done 2>/dev/null': "x86_pkg_temp:95000\nacpitz:51000\n", }); const findings = await thermal.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "high"); assert.match(findings[0].title, /very hot/); assert.match(findings[0].evidence, /95°C/); }); test("thermal: normal temperature is informational", async () => { const ctx = stubCtx({ 'for z in /sys/class/thermal/thermal_zone*; do [ -f "$z/type" ] && [ -f "$z/temp" ] && echo "$(cat "$z/type"):$(cat "$z/temp")"; done 2>/dev/null': "x86_pkg_temp:58000\n", }); const findings = await thermal.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "info"); assert.match(findings[0].title, /look fine/); }); test("thermal: throttling events in the journal are medium", async () => { const ctx = stubCtx({ 'for z in /sys/class/thermal/thermal_zone*; do [ -f "$z/type" ] && [ -f "$z/temp" ] && echo "$(cat "$z/type"):$(cat "$z/temp")"; done 2>/dev/null': "x86_pkg_temp:45000\n", 'journalctl -g "clock throttl" --since "-24 hours" --no-pager -o short 2>/dev/null': "Aug 15 10:00:00 bazzite kernel: CPU3: Core temperature above threshold, cpu clock throttled\n", }); const findings = await thermal.run(ctx); const throttle = findings.find((f) => /throttling/i.test(f.title)); assert.ok(throttle, "expected a throttling finding"); assert.equal(throttle.severity, "medium"); }); test("thermal: app-level messages containing 'throttle' are NOT CPU throttling", async () => { // The old pattern `-g "throttl"` also matched app messages like // "setStartupThrottle" (Roblox, Chromium) — a false positive. const ctx = stubCtx({ 'for z in /sys/class/thermal/thermal_zone*; do [ -f "$z/type" ] && [ -f "$z/temp" ] && echo "$(cat "$z/type"):$(cat "$z/temp")"; done 2>/dev/null': "x86_pkg_temp:45000\n", 'journalctl -g "clock throttl" --since "-24 hours" --no-pager -o short 2>/dev/null': "Aug 18 13:39:06 bazzite flatpak[143639]: info: Roblox: ... setStartupThrottle: false\n", }); const findings = await thermal.run(ctx); assert.ok(!findings.some((f) => f.title.includes("throttling")), "app-level throttle messages must not be reported as CPU throttling"); }); test("thermal: journalctl boot separators alone are NOT throttling", async () => { const ctx = stubCtx({ 'journalctl -g "clock throttl" --since "-24 hours" --no-pager -o short 2>/dev/null': "", }); const findings = await thermal.run(ctx); assert.ok(!findings.some((f) => f.title.includes("throttling")), "separator-only output must not be a throttling finding"); }); test("flatpak: pending updates are counted", async () => { const ctx = stubCtx({ "flatpak remote-ls --updates --columns=application,version 2>/dev/null": "org.mozilla.firefox\t115.0\norg.gnome.Calculator\t46.0\norg.videolan.VLC\t3.0.20\n", }); const findings = await flatpak.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "info"); assert.match(findings[0].title, /3 Flatpak update/); assert.match(findings[0].fix, /flatpak update/); }); test("flatpak: up to date is informational", async () => { const ctx = stubCtx({ "flatpak remote-ls --updates --columns=application,version 2>/dev/null": "", }); const findings = await flatpak.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "info"); assert.match(findings[0].title, /up to date/); }); test("flatpak: not installed stays silent", async () => { const ctx = { osRelease: { id: "bazzite", id_like: "fedora" }, run: async () => ({ ok: false, code: -1, stdout: "", stderr: "", missing: true }), }; const findings = await flatpak.run(ctx); assert.equal(findings.length, 0); }); // Regression: `flatpak remote-ls --updates` prints a column table whose fields // contain no "/", so counting lines with "/" read zero and reported "Flatpak // apps are up to date" on a machine with pending updates. `--columns` is used // now; this keeps the fallback honest on flatpak versions without it. test("flatpak: a flatpak without --columns still counts the table rows", async () => { const ctx = stubCtx({ // The --columns probe is unknown to this stub, so it fails, which is exactly // how an older flatpak behaves. "flatpak remote-ls --updates 2>/dev/null": "Minecraft Bedrock Launcher\tio.mrarm.mcpelauncher\tv1.8.3\tstable\tx86_64\n" + "Obsidian\tmd.obsidian.Obsidian\t1.13.7\tstable\tx86_64\n", }); const findings = await flatpak.run(ctx); assert.equal(findings.length, 1); assert.match(findings[0].title, /2 Flatpak update/); }); test("bluetooth: no controller is informational and skips the rest", async () => { const ctx = stubCtx({ "ls /sys/class/bluetooth 2>/dev/null": "", }); const findings = await bluetooth.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "info"); assert.match(findings[0].title, /No Bluetooth hardware/); }); test("bluetooth: controller + running daemon is healthy", async () => { const ctx = stubCtx({ "ls /sys/class/bluetooth 2>/dev/null": "hci0\n", "systemctl is-failed bluetooth 2>/dev/null": "inactive\n", "pgrep -x bluetoothd 2>/dev/null": "1234\n", }); const findings = await bluetooth.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "info"); assert.match(findings[0].title, /Bluetooth is working/); assert.match(findings[0].evidence, /hci0/); }); test("bluetooth: failed service is medium", async () => { const ctx = stubCtx({ "ls /sys/class/bluetooth 2>/dev/null": "hci0\n", "systemctl is-failed bluetooth 2>/dev/null": "failed\n", }); const findings = await bluetooth.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "medium"); assert.match(findings[0].title, /failed state/); assert.ok(findings[0].fix, "failed services should include a fix"); }); test("bluetooth: controller present but daemon down is medium", async () => { const ctx = stubCtx({ "ls /sys/class/bluetooth 2>/dev/null": "hci0\n", "systemctl is-failed bluetooth 2>/dev/null": "inactive\n", "pgrep -x bluetoothd 2>/dev/null": "", }); const findings = await bluetooth.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "medium"); assert.match(findings[0].title, /not running/); assert.match(findings[0].fix, /systemctl enable --now bluetooth/); }); test("wayland: no graphical session is informational", async () => { const ctx = stubCtx({ 'loginctl list-sessions --no-legend 2>/dev/null | grep -m1 -E "(^|[[:space:]])seat[0-9]+([[:space:]]|$)" | cut -d" " -f1': "", }); const findings = await wayland.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "info"); assert.match(findings[0].title, /No graphical session/); }); test("wayland: X11 session is informational", async () => { const ctx = stubCtx({ 'loginctl list-sessions --no-legend 2>/dev/null | grep -m1 -E "(^|[[:space:]])seat[0-9]+([[:space:]]|$)" | cut -d" " -f1': "2\n", "loginctl show-session 2 -p Type -p Desktop 2>/dev/null": "Type=x11\nDesktop=KDE\n", }); const findings = await wayland.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "info"); assert.match(findings[0].title, /X11 session/); }); test("wayland: healthy Wayland session with running compositor is informational", async () => { const ctx = stubCtx({ 'loginctl list-sessions --no-legend 2>/dev/null | grep -m1 -E "(^|[[:space:]])seat[0-9]+([[:space:]]|$)" | cut -d" " -f1': "3\n", "loginctl show-session 3 -p Type -p Desktop 2>/dev/null": "Type=wayland\nDesktop=gnome\n", "pgrep -a -f 'kwin_[w]ayland|gnome-[s]hell|[s]way|[h]yprland|[r]iver|[w]ayfire|[l]abwc|[n]iri|cosmic-[c]omp|[m]utter|[w]eston|[c]age' 2>/dev/null | head -1": "1234 gnome-shell\n", }); const findings = await wayland.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "info"); assert.match(findings[0].title, /Wayland session looks healthy/); assert.match(findings[0].evidence, /gnome-shell/); }); test("wayland: Wayland session without a compositor is medium", async () => { const ctx = stubCtx({ 'loginctl list-sessions --no-legend 2>/dev/null | grep -m1 -E "(^|[[:space:]])seat[0-9]+([[:space:]]|$)" | cut -d" " -f1': "3\n", "loginctl show-session 3 -p Type -p Desktop 2>/dev/null": "Type=wayland\nDesktop=gnome\n", "pgrep -a -f 'kwin_[w]ayland|gnome-[s]hell|[s]way|[h]yprland|[r]iver|[w]ayfire|[l]abwc|[n]iri|cosmic-[c]omp|[m]utter|[w]eston|[c]age' 2>/dev/null | head -1": "", }); const findings = await wayland.run(ctx); const mediums = findings.filter((f) => f.severity === "medium"); assert.equal(mediums.length, 1); assert.match(mediums[0].title, /no compositor process/); }); test("wayland: software rendering in a Wayland session is medium", async () => { const ctx = stubCtx({ 'loginctl list-sessions --no-legend 2>/dev/null | grep -m1 -E "(^|[[:space:]])seat[0-9]+([[:space:]]|$)" | cut -d" " -f1': "3\n", "loginctl show-session 3 -p Type -p Desktop 2>/dev/null": "Type=wayland\nDesktop=hyprland\n", "pgrep -a -f 'kwin_[w]ayland|gnome-[s]hell|[s]way|[h]yprland|[r]iver|[w]ayfire|[l]abwc|[n]iri|cosmic-[c]omp|[m]utter|[w]eston|[c]age' 2>/dev/null | head -1": "42 Hyprland\n", "glxinfo -B 2>/dev/null | grep -i 'renderer string'": "OpenGL renderer string: llvmpipe (LLVM 17.0.6, 256 bits)\n", }); const findings = await wayland.run(ctx); const mediums = findings.filter((f) => f.severity === "medium"); assert.equal(mediums.length, 1); assert.match(mediums[0].title, /software rendering/); assert.match(mediums[0].evidence, /llvmpipe/); }); test("backup: nothing detected is informational", async () => { const ctx = stubCtx({ 'for t in borg restic rclone duplicity timeshift pika-backup backintime deja-dup; do command -v "$t" 2>/dev/null; done': "", "ls /etc/snapper/configs 2>/dev/null": "", "[ -f /etc/timeshift/timeshift.json ] && echo configured": "", "systemctl list-timers --all --no-pager 2>/dev/null | grep -iE 'backup|borg|restic|timeshift|snapper|pika|deja' | grep -oE '[A-Za-z0-9_.@-]+\\.timer' | sort -u": "", }); const findings = await backup.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "info", "no backup tool is a setup choice, not a fault"); assert.match(findings[0].title, /No backup or snapshot tool/); assert.ok(findings[0].fix, "missing backups should include a fix"); }); test("backup: tools installed but nothing scheduled is informational", async () => { const ctx = stubCtx({ 'for t in borg restic rclone duplicity timeshift pika-backup backintime deja-dup; do command -v "$t" 2>/dev/null; done': "/usr/bin/borg\n/usr/bin/restic\n", "systemctl list-timers --all --no-pager 2>/dev/null | grep -iE 'backup|borg|restic|timeshift|snapper|pika|deja' | grep -oE '[A-Za-z0-9_.@-]+\\.timer' | sort -u": "", "command -v systemctl 2>/dev/null": "/usr/bin/systemctl\n", }); const findings = await backup.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "info", "unscheduled backups are informational, not a detected fault"); assert.match(findings[0].title, /nothing is scheduled/); assert.match(findings[0].evidence, /borg/); }); test("backup: tools plus a scheduled timer is informational", async () => { const ctx = stubCtx({ 'for t in borg restic rclone duplicity timeshift pika-backup backintime deja-dup; do command -v "$t" 2>/dev/null; done': "/usr/bin/borg\n", "systemctl list-timers --all --no-pager 2>/dev/null | grep -iE 'backup|borg|restic|timeshift|snapper|pika|deja' | grep -oE '[A-Za-z0-9_.@-]+\\.timer' | sort -u": "borg-backup.timer\n", }); const findings = await backup.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "info"); assert.match(findings[0].title, /Backups are set up/); assert.match(findings[0].evidence, /borg-backup.timer/); }); test("backup: snapper configs count as a snapshot system", async () => { const ctx = stubCtx({ "ls /etc/snapper/configs 2>/dev/null": "home\nroot\n", "systemctl list-timers --all --no-pager 2>/dev/null | grep -iE 'backup|borg|restic|timeshift|snapper|pika|deja' | grep -oE '[A-Za-z0-9_.@-]+\\.timer' | sort -u": "snapper-timeline.timer\n", }); const findings = await backup.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "info"); assert.match(findings[0].title, /Backups are set up/); assert.match(findings[0].evidence, /snapper/); }); test("hardware: machine check exceptions are high", async () => { const ctx = stubCtx({ "command -v journalctl 2>/dev/null": "/usr/bin/journalctl\n", 'journalctl -k --since "-7 days" --no-pager -o short 2>/dev/null | grep -iE "mce|machine check|hardware error|edac|corrected error|ecc error"': "Aug 13 03:11:22 bazzite kernel: mce: [Hardware Error]: Machine check events logged\n", }); const findings = await hardware.run(ctx); const high = findings.filter((f) => f.severity === "high"); assert.equal(high.length, 1); assert.match(high[0].title, /Machine check exceptions/); assert.ok(high[0].fix, "MCE findings should include a fix"); }); // EDAC's own vocabulary: a UE is *uncorrected*. The check used to report every // EDAC line as "Corrected hardware errors ... medium", i.e. it filed a lost bit // as a routine correction. Here the same line must now be high. test("hardware: an uncorrected (UE) memory error is high, not medium", async () => { const ctx = stubCtx({ "command -v journalctl 2>/dev/null": "/usr/bin/journalctl\n", 'journalctl -k --since "-7 days" --no-pager -o short 2>/dev/null | grep -iE "mce|machine check|hardware error|edac|corrected error|ecc error"': "Aug 14 09:41:05 bazzite kernel: EDAC mc0: UE row 2, channel-a 0\n", }); const findings = await hardware.run(ctx); const high = findings.filter((f) => f.severity === "high"); assert.equal(high.length, 1); assert.equal(high[0].code, "hardware/ecc"); assert.match(high[0].title, /Uncorrected memory errors/); assert.match(high[0].evidence, /UE/); }); test("hardware: clean kernel log is informational", async () => { const ctx = stubCtx({ "command -v journalctl 2>/dev/null": "/usr/bin/journalctl\n", "command -v journalctl 2>/dev/null": "/usr/bin/journalctl\n", 'journalctl -k --since "-7 days" --no-pager -o short 2>/dev/null | grep -iE "mce|machine check|hardware error|edac|corrected error|ecc error"': "", }); const findings = await hardware.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "info"); assert.match(findings[0].title, /No hardware errors/); }); test("hardware: a readable log with no matches is 'no errors', not silence", async () => { // `journalctl -k ... | grep ...` exits 1 when nothing matches, so `.ok` meant // "grep found nothing", not "I could not read the log". On a healthy machine // with no MCE/EDAC line at all, the check said nothing instead of reporting // "No hardware errors logged". (A benign EDAC banner made grep exit 0 on the // maintainer's box, which hid this.) const ctx = stubCtx({ "command -v journalctl 2>/dev/null": "/usr/bin/journalctl\n", // the grep itself is left unstubbed: it fails with no output, exactly like // the real command when nothing matches }); const findings = await hardware.run(ctx); assert.equal(findings.length, 1, `a readable log must produce an answer: ${JSON.stringify(findings.map((f) => f.code))}`); assert.equal(findings[0].code, "hardware/ok"); }); test("hardware: boot separators alone are NOT hardware errors", async () => { // journalctl -k -g prints "-- Boot ... --" separators even with no matches. const ctx = stubCtx({ "command -v journalctl 2>/dev/null": "/usr/bin/journalctl\n", 'journalctl -k --since "-7 days" --no-pager -o short 2>/dev/null | grep -iE "mce|machine check|hardware error|edac|corrected error|ecc error"': "", "command -v journalctl 2>/dev/null": "/usr/bin/journalctl\n", 'journalctl -k --since "-7 days" --no-pager -o short 2>/dev/null | grep -iE "mce|machine check|hardware error|edac|corrected error|ecc error"': "", }); const findings = await hardware.run(ctx); assert.ok(!findings.some((f) => f.severity === "high" || f.severity === "medium"), "separator-only output must not be an error finding"); }); // Regression: "mce: CPU supports N MCE banks" is printed once per CPU at boot // on every Intel machine. A bare /mce/ match read it as a machine check // exception, i.e. a permanent high finding on healthy hardware. test("hardware: the MCE banks boot line is not a machine check exception", async () => { const ctx = stubCtx({ "command -v journalctl 2>/dev/null": "/usr/bin/journalctl\n", "command -v journalctl 2>/dev/null": "/usr/bin/journalctl\n", 'journalctl -k --since "-7 days" --no-pager -o short 2>/dev/null | grep -iE "mce|machine check|hardware error|edac|corrected error|ecc error"': [ "Aug 13 03:11:22 bazzite kernel: mce: CPU supports 32 MCE banks", "Aug 13 03:11:22 bazzite kernel: mce: CPU supports 32 MCE banks", "Aug 13 03:11:22 bazzite kernel: EDAC MC: Ver: 3.0.0", "Aug 13 03:11:22 bazzite kernel: EDAC ie31200: No ECC support", ].join("\n") + "\n", }); const findings = await hardware.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].code, "hardware/ok", "routine MCE/EDAC init lines must not be a hardware fault"); assert.equal(findings[0].severity, "info"); }); test("hardware: a single corrected (CE) memory error is routine, not a penalty", async () => { // One corrected bit flip over seven days is ECC doing its job. Reporting it // at medium made the score drop for normal operation, which is how a report // teaches people to stop reading it. It is still shown, as a note to watch. const ctx = stubCtx({ "command -v journalctl 2>/dev/null": "/usr/bin/journalctl\n", 'journalctl -k --since "-7 days" --no-pager -o short 2>/dev/null | grep -iE "mce|machine check|hardware error|edac|corrected error|ecc error"': "Aug 14 09:41:05 bazzite kernel: EDAC MC0: 1 CE memory read error on CPU_SrcID#0_MC#0_Chan#0_DIMM#0\n", }); const findings = await hardware.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].code, "hardware/ecc"); assert.equal(findings[0].severity, "info"); assert.match(findings[0].title, /corrected/i); assert.equal(findings[0].fix, null, "a routine correction is not an action"); }); test("hardware: repeated corrected (CE) memory errors are a medium finding", async () => { // "Occasional ones are normal, but frequent ones suggest a DIMM is starting // to fail" — the check's own words. Two in a week is no longer occasional. const ctx = stubCtx({ "command -v journalctl 2>/dev/null": "/usr/bin/journalctl\n", 'journalctl -k --since "-7 days" --no-pager -o short 2>/dev/null | grep -iE "mce|machine check|hardware error|edac|corrected error|ecc error"': [ "Aug 14 09:41:05 bazzite kernel: EDAC MC0: 1 CE memory read error on CPU_SrcID#0_MC#0_Chan#0_DIMM#0", "Aug 15 11:02:44 bazzite kernel: EDAC MC0: 1 CE memory read error on CPU_SrcID#0_MC#0_Chan#0_DIMM#0", ].join("\n") + "\n", }); const findings = await hardware.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].code, "hardware/ecc"); assert.equal(findings[0].severity, "medium"); assert.match(findings[0].fix, /Memtest/i); }); test("luks: encrypted system is informational", async () => { const ctx = stubCtx({ "lsblk -o NAME,FSTYPE,TYPE -n 2>/dev/null": "sda2 crypto_LUKS part\nluks-fedora crypto_LUKS crypt\n", }); const findings = await luks.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "info"); assert.match(findings[0].title, /encryption is active/); }); test("luks: unencrypted system is informational", async () => { const ctx = stubCtx({ "lsblk -o NAME,FSTYPE,TYPE -n 2>/dev/null": "sda1 vfat part\nsda2 ext4 part\n", }); const findings = await luks.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "info", "unencrypted is a setup choice, not a detected fault"); assert.match(findings[0].title, /No full-disk encryption/); assert.ok(findings[0].fix, "missing encryption should include a fix"); }); test("luks: no lsblk stays silent", async () => { const ctx = { osRelease: { id: "bazzite", id_like: "fedora" }, dist: detectDistro({ id: "bazzite", id_like: "fedora" }), run: async () => ({ ok: false, code: -1, stdout: "", stderr: "", missing: true }), }; const findings = await luks.run(ctx); assert.equal(findings.length, 0); }); test("network: no default route is medium", async () => { const ctx = stubCtx({ "command -v ip 2>/dev/null": "/usr/sbin/ip\n", "ip -brief addr show 2>/dev/null": "lo UNKNOWN 127.0.0.1/8 ::1/128\nwlan0 UP 192.168.1.5/24\n", "ip route show default 2>/dev/null": "", }); const findings = await network.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "medium"); assert.match(findings[0].title, /No default network route/); assert.match(findings[0].detail, /wlan0/); assert.ok(findings[0].fix); }); test("network: DNS failure is medium when a route exists", async () => { const ctx = stubCtx({ "command -v ip 2>/dev/null": "/usr/sbin/ip\n", "ip -brief addr show 2>/dev/null": "wlan0 UP 192.168.1.5/24\n", "ip route show default 2>/dev/null": "default via 192.168.1.1 dev wlan0 proto dhcp metric 600\n", "getent ahostsv4 kernel.org 2>&1 | head -1": "", }); const findings = await network.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "medium"); assert.match(findings[0].title, /DNS resolution is failing/); assert.ok(findings[0].fix); }); test("network: route + working DNS is informational", async () => { const ctx = stubCtx({ "command -v ip 2>/dev/null": "/usr/sbin/ip\n", "ip -brief addr show 2>/dev/null": "wlan0 UP 192.168.1.5/24\n", "ip route show default 2>/dev/null": "default via 192.168.1.1 dev wlan0 proto dhcp metric 600\n", "getent ahostsv4 kernel.org 2>&1 | head -1": "151.101.1.69 STREAM kernel.org\n", }); const findings = await network.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "info"); assert.match(findings[0].title, /Network and DNS look healthy/); }); test("network: missing iproute2 is an explicit skip, not silence", async () => { const ctx = { osRelease: { id: "bazzite", id_like: "fedora" }, dist: detectDistro({ id: "bazzite", id_like: "fedora" }), run: async () => ({ ok: false, code: -1, stdout: "", stderr: "", missing: true }), }; const findings = await network.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "info"); assert.match(findings[0].title, /network check skipped/i); }); test("reboot: newer installed kernel than booted is medium", async () => { const ctx = stubCtx({ "uname -r 2>/dev/null": "6.8.9-300.fc40.x86_64\n", "for f in /boot/vmlinuz-*; do readlink -f \"$f\"; done 2>/dev/null | sort -u | sed 's|.*/vmlinuz-||'": "6.8.9-300.fc40.x86_64\n6.9.12-200.fc40.x86_64\n", "[ -f /var/run/reboot-required ] && cat /var/run/reboot-required 2>/dev/null": "", }, { id: "fedora", id_like: "fedora" }); const findings = await reboot.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "medium"); assert.match(findings[0].title, /reboot is required/i); assert.match(findings[0].evidence, /6\.9\.12/); assert.ok(findings[0].fix); }); test("reboot: /var/run/reboot-required alone is medium", async () => { const ctx = stubCtx({ "uname -r 2>/dev/null": "6.8.9-300.fc40.x86_64\n", "for f in /boot/vmlinuz-*; do readlink -f \"$f\"; done 2>/dev/null | sort -u | sed 's|.*/vmlinuz-||'": "6.8.9-300.fc40.x86_64\n", "[ -f /var/run/reboot-required ] && cat /var/run/reboot-required 2>/dev/null": "*** System restart required ***\n", }, { id: "debian", id_like: "" }); const findings = await reboot.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "medium"); assert.match(findings[0].detail, /reboot-required/); }); test("reboot: newest kernel already booted is informational", async () => { const ctx = stubCtx({ "uname -r 2>/dev/null": "6.9.12-200.fc40.x86_64\n", "for f in /boot/vmlinuz-*; do readlink -f \"$f\"; done 2>/dev/null | sort -u | sed 's|.*/vmlinuz-||'": "6.9.12-200.fc40.x86_64\n", "[ -f /var/run/reboot-required ] && cat /var/run/reboot-required 2>/dev/null": "", }, { id: "fedora", id_like: "fedora" }); const findings = await reboot.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "info"); assert.match(findings[0].title, /No reboot needed/); }); test("reboot: image-based (ostree) distros stay silent", async () => { const ctx = stubCtx({ "uname -r 2>/dev/null": "6.8.9-300.fc40.x86_64\n", }); const findings = await reboot.run(ctx); assert.equal(findings.length, 0); }); test("reboot: versionGt compares kernel versions numerically", () => { assert.equal(versionGt("6.9.12-200.fc40.x86_64", "6.8.9-300.fc40.x86_64"), true); assert.equal(versionGt("6.8.9-300.fc40.x86_64", "6.9.12-200.fc40.x86_64"), false); assert.equal(versionGt("6.8.9", "6.8.9"), false); assert.equal(versionGt("6.1.0-28-amd64", "6.1.0-26-amd64"), true); }); test("journald: large journal is medium", async () => { const ctx = stubCtx({ "journalctl --disk-usage 2>/dev/null": "Archived and active journals take up 3.2G in the file system.\n", }); const findings = await journald.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "medium"); assert.match(findings[0].title, /journal is large/i); assert.match(findings[0].fix, /vacuum/); }); test("journald: small journal is informational", async () => { const ctx = stubCtx({ "journalctl --disk-usage 2>/dev/null": "Archived and active journals take up 18.2M in the file system.\n", }); const findings = await journald.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "info"); assert.match(findings[0].title, /Journal size is fine/); }); test("journald: no systemd journal is an explicit skip, not silence", async () => { const ctx = { osRelease: { id: "bazzite", id_like: "fedora" }, dist: detectDistro({ id: "bazzite", id_like: "fedora" }), run: async () => ({ ok: false, code: -1, stdout: "", stderr: "", missing: true }), }; const findings = await journald.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "info"); assert.match(findings[0].title, /journal size check skipped/i); }); test("journald: parseSize handles units", () => { assert.equal(parseSize("512.0M"), 512 * 1024 ** 2); assert.equal(parseSize("3.2G"), Math.round(3.2 * 1024 ** 3)); assert.equal(parseSize("1024B"), 1024); assert.equal(parseSize("garbage"), 0); }); test("smart: failing drive is high", async () => { const ctx = stubCtx({ "smartctl --scan 2>/dev/null": "/dev/sda -d scsi # /dev/sda, SCSI device\n", "smartctl -H -c /dev/sda 2>/dev/null": "SMART overall-health self-assessment test result: FAILED!\n", }); const findings = await smart.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "high"); assert.match(findings[0].title, /failing SMART health/); assert.ok(findings[0].fix); }); test("smart: healthy drives are informational", async () => { const ctx = stubCtx({ "smartctl --scan 2>/dev/null": "/dev/sda -d scsi # /dev/sda, SCSI device\n/dev/nvme0 -d nvme # /dev/nvme0, NVMe device\n", "smartctl -H -c /dev/sda 2>/dev/null": "SMART overall-health self-assessment test result: PASSED\n", "smartctl -H -c /dev/nvme0 2>/dev/null": "SMART overall-health self-assessment test result: PASSED\n", }); const findings = await smart.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "info"); assert.match(findings[0].title, /Disk health is good/); assert.match(findings[0].detail, /2 disk/); }); test("smart: smartctl not installed is an explicit skip, not silence", async () => { const ctx = { osRelease: { id: "bazzite", id_like: "fedora" }, dist: detectDistro({ id: "bazzite", id_like: "fedora" }), run: async () => ({ ok: false, code: -1, stdout: "", stderr: "", missing: true }), }; const findings = await smart.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "info"); assert.match(findings[0].title, /smartmontools missing/i); }); // systemd pads header and rows to the same fixed columns, so the parser // slices by the header's column offsets. Rows are built with matching widths // (short values glue to the next column with a single space, like real output). const timerRow = (next, left, last, passed, unit, activates) => [next.padEnd(34), left.padEnd(11), last.padEnd(34), passed.padEnd(12), unit.padEnd(28), activates].join(""); const TIMER_HEADER = timerRow("NEXT", "LEFT", "LAST", "PASSED", "UNIT", "ACTIVATES"); test("timers: an enabled timer that never fires is medium", async () => { const ctx = stubCtx({ "systemctl list-timers --all --no-pager --plain 2>/dev/null": TIMER_HEADER + "\n" + timerRow("Mon 2026-08-18 19:00:00 EEST", "1h 30min left", "Mon 2026-08-18 08:00:00 EEST", "9h ago", "dnf-makecache.timer", "dnf-makecache.service") + "\n" + timerRow("-", "n/a", "-", "n/a", "borg-backup.timer", "borg-backup.service") + "\n\n2 timers listed.", "systemctl is-enabled borg-backup.timer 2>/dev/null": "enabled\n", }); const findings = await timers.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "medium"); assert.match(findings[0].title, /enabled but never running/); assert.match(findings[0].evidence, /borg-backup\.timer/); assert.ok(findings[0].fix); }); test("timers: disabled timers showing '-' are NOT broken", async () => { const ctx = stubCtx({ "systemctl list-timers --all --no-pager --plain 2>/dev/null": TIMER_HEADER + "\n" + timerRow("-", "n/a", "-", "n/a", "old-thing.timer", "old-thing.service") + "\n", "systemctl is-enabled old-thing.timer 2>/dev/null": "disabled\n", }); const findings = await timers.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "info"); assert.match(findings[0].title, /healthy/); }); test("timers: healthy timers are informational", async () => { const ctx = stubCtx({ "systemctl list-timers --all --no-pager --plain 2>/dev/null": TIMER_HEADER + "\n" + timerRow("Mon 2026-08-18 19:00:00 EEST", "1h 30min left", "Mon 2026-08-18 08:00:00 EEST", "9h ago", "dnf-makecache.timer", "dnf-makecache.service") + "\n\n1 timers listed.", }); const findings = await timers.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "info"); assert.match(findings[0].title, /healthy/); }); test("timers: non-systemd stays silent", async () => { const ctx = { osRelease: { id: "alpine", id_like: "" }, dist: detectDistro({ id: "alpine", id_like: "" }), thresholds: loadThresholds({}), run: async () => ({ ok: false, code: -1, stdout: "", stderr: "", missing: true }), }; const findings = await timers.run(ctx); assert.equal(findings.length, 1, `a non-systemd system is an explicit skip: ${JSON.stringify(findings.map((f) => f.code))}`); assert.equal(findings[0].code, "timers/skipped"); }); test("ntp: synchronized clock is informational", async () => { const ctx = stubCtx({ "timedatectl show -p NTPSynchronized 2>/dev/null": "NTPSynchronized=yes\n", }); const findings = await ntp.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "info"); assert.match(findings[0].title, /synchronized/); }); test("ntp: no NTP daemon is medium", async () => { const ctx = stubCtx({ "timedatectl show -p NTPSynchronized 2>/dev/null": "NTPSynchronized=no\n", "systemctl is-active systemd-timesyncd chronyd ntpd 2>/dev/null || true": "inactive\ninactive\ninactive\n", }); const findings = await ntp.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "medium"); assert.match(findings[0].title, /not kept in sync/); assert.ok(findings[0].fix); }); test("ntp: daemon active but unsynchronized is medium", async () => { const ctx = stubCtx({ "timedatectl show -p NTPSynchronized 2>/dev/null": "NTPSynchronized=no\n", "systemctl is-active systemd-timesyncd chronyd ntpd 2>/dev/null || true": "active\n", }); const findings = await ntp.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "medium"); assert.match(findings[0].title, /not synchronized yet/); }); test("ntp: no timedatectl (non-systemd) is an explicit skip, not silence", async () => { const ctx = { osRelease: { id: "alpine", id_like: "" }, dist: detectDistro({ id: "alpine", id_like: "" }), thresholds: loadThresholds({}), run: async () => ({ ok: false, code: -1, stdout: "", stderr: "", missing: true }), }; const findings = await ntp.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "info"); assert.match(findings[0].title, /time sync check skipped/i); }); test("battery: heavy wear is medium", async () => { const ctx = stubCtx({ "ls /sys/class/power_supply/ 2>/dev/null": "AC\nBAT0\n", "cat /sys/class/power_supply/BAT0/type 2>/dev/null": "Battery\n", "cat /sys/class/power_supply/BAT0/capacity 2>/dev/null": "60\n", "cat /sys/class/power_supply/BAT0/status 2>/dev/null": "Discharging\n", "cat /sys/class/power_supply/BAT0/charge_full 2>/dev/null": "3000000\n", "cat /sys/class/power_supply/BAT0/charge_full_design 2>/dev/null": "5000000\n", }); const findings = await battery.run(ctx); const wear = findings.find((f) => /lost a lot of capacity/.test(f.title)); assert.ok(wear, "expected a wear finding"); assert.equal(wear.severity, "medium"); assert.match(wear.evidence, /40% wear/); }); test("battery: moderate wear is informational", async () => { const ctx = stubCtx({ "ls /sys/class/power_supply/ 2>/dev/null": "AC\nBAT0\n", "cat /sys/class/power_supply/BAT0/type 2>/dev/null": "Battery\n", "cat /sys/class/power_supply/BAT0/capacity 2>/dev/null": "87\n", "cat /sys/class/power_supply/BAT0/status 2>/dev/null": "Charging\n", "cat /sys/class/power_supply/BAT0/charge_full 2>/dev/null": "4000000\n", "cat /sys/class/power_supply/BAT0/charge_full_design 2>/dev/null": "5000000\n", }); const findings = await battery.run(ctx); const wear = findings.find((f) => /showing wear/.test(f.title)); assert.ok(wear, "expected a wear info finding"); assert.equal(wear.severity, "info"); }); test("battery: no wear data produces no wear finding", async () => { const ctx = stubCtx({ "ls /sys/class/power_supply/ 2>/dev/null": "AC\nBAT0\n", "cat /sys/class/power_supply/BAT0/type 2>/dev/null": "Battery\n", "cat /sys/class/power_supply/BAT0/capacity 2>/dev/null": "87\n", "cat /sys/class/power_supply/BAT0/status 2>/dev/null": "Charging\n", }); const findings = await battery.run(ctx); assert.equal(findings.length, 1, "only the status finding, no wear"); assert.equal(findings[0].severity, "info"); }); test("smart: unreadable devices without root are explained, not silent", async () => { const ctx = { osRelease: { id: "bazzite", id_like: "fedora" }, dist: detectDistro({ id: "bazzite", id_like: "fedora" }), run: async (cmd) => { if (cmd === "smartctl --scan 2>/dev/null") return { ok: true, code: 0, stdout: "/dev/sda -d scsi # /dev/sda, SCSI device\n", stderr: "" }; if (cmd.startsWith("smartctl -H -c")) return { ok: false, code: 4, stdout: "", stderr: "smartctl: Permission denied\n" }; return { ok: false, code: 1, stdout: "", stderr: "" }; }, }; const findings = await smart.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "info"); assert.match(findings[0].title, /needs root/); }); test("memory: missing free is an explicit skip, not a silent all-clear", async () => { const ctx = stubCtx({}); ctx.run = async () => ({ ok: false, code: -1, stdout: "", stderr: "", missing: true }); const findings = await memory.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "info"); assert.match(findings[0].title, /memory check skipped/i); }); test("ntp: missing timedatectl (non-systemd) is an explicit skip", async () => { const ctx = stubCtx({}); ctx.run = async () => ({ ok: false, code: -1, stdout: "", stderr: "", missing: true }); const findings = await ntp.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "info"); assert.match(findings[0].title, /time sync check skipped/i); }); test("updates: package manager missing is an explicit skip, not silence", async () => { const ctx = stubCtx({}); ctx.osRelease = { id: "debian", id_like: "" }; ctx.dist = detectDistro(ctx.osRelease); ctx.run = async () => ({ ok: false, code: -1, stdout: "", stderr: "", missing: true }); const findings = await updates.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "info"); assert.match(findings[0].title, /update check skipped/i); }); test("smart: missing smartctl is an explicit skip with an install hint", async () => { const ctx = stubCtx({}); ctx.run = async () => ({ ok: false, code: -1, stdout: "", stderr: "", missing: true }); const findings = await smart.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "info"); assert.match(findings[0].title, /smartmontools missing/i); assert.match(findings[0].fix, /smartmontools/); }); test("thermal: no thermal zones is an explicit skip", async () => { const ctx = stubCtx({}); const findings = await thermal.run(ctx); assert.ok(findings.some((f) => /temperature check skipped/i.test(f.title)), "explains why temperatures are unknown"); }); test("audio: no sound server running is a medium finding with a distro-aware fix", async () => { const ctx = { osRelease: { id: "fedora", id_like: "fedora" }, dist: detectDistro({ id: "fedora", id_like: "fedora" }), thresholds: {}, run: async () => ({ ok: true, code: 0, stdout: "down\n", stderr: "" }), }; const findings = await audio.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "medium"); assert.match(findings[0].title, /No sound server is running/); assert.match(findings[0].fix, /dnf install pipewire/); }); test("audio: server up with a real sink is healthy; dummy sink is not", async () => { const makeCtx = (sinksOut) => ({ osRelease: { id: "ubuntu", id_like: "debian" }, dist: detectDistro({ id: "ubuntu", id_like: "debian" }), thresholds: {}, run: async (cmd) => { if (cmd.includes("pgrep -x pipewire")) return { ok: true, code: 0, stdout: "up\n", stderr: "" }; if (cmd.includes("command -v pactl")) return { ok: true, code: 0, stdout: "yes\n", stderr: "" }; if (cmd.includes("pactl list sinks short")) return { ok: true, code: 0, stdout: sinksOut, stderr: "" }; return { ok: true, code: 0, stdout: "down\n", stderr: "" }; }, }); const healthy = await audio.run(makeCtx("0\talsa_output.pci-0000_00_1f.3.analog-stereo\tRUNNING\n")); assert.equal(healthy.length, 1); assert.match(healthy[0].title, /Audio is working/); assert.equal(healthy[0].severity, "info"); const dummy = await audio.run(makeCtx("0\tauto_null\tRUNNING\n")); assert.equal(dummy.length, 1); assert.equal(dummy[0].severity, "medium"); assert.match(dummy[0].title, /No audio output device detected/); }); test("containers: no runtime installed is informational with an install hint", async () => { const ctx = { osRelease: { id: "debian", id_like: "" }, dist: detectDistro({ id: "debian", id_like: "" }), thresholds: {}, run: async () => ({ ok: true, code: 0, stdout: "", stderr: "" }), }; const findings = await containers.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "info"); assert.match(findings[0].title, /No container runtime installed/); assert.match(findings[0].fix, /apt install podman/); }); test("containers: docker installed but daemon stopped is medium", async () => { const ctx = { osRelease: { id: "fedora", id_like: "fedora" }, dist: detectDistro({ id: "fedora", id_like: "fedora" }), thresholds: {}, run: async (cmd) => { if (cmd === "command -v podman 2>/dev/null") return { ok: false, code: 1, stdout: "", stderr: "" }; if (cmd === "command -v docker 2>/dev/null") return { ok: true, code: 0, stdout: "/usr/bin/docker\n", stderr: "" }; if (cmd.startsWith("systemctl is-active docker")) return { ok: true, code: 0, stdout: "inactive\n", stderr: "" }; return { ok: false, code: 1, stdout: "", stderr: "" }; }, }; const findings = await containers.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "medium"); assert.match(findings[0].title, /Docker daemon is not running/); assert.match(findings[0].fix, /systemctl enable --now docker/); }); test("containers: podman usable is reported healthy", async () => { const ctx = { osRelease: { id: "bazzite", id_like: "fedora" }, dist: detectDistro({ id: "bazzite", id_like: "fedora" }), thresholds: {}, run: async (cmd) => { if (cmd === "command -v podman 2>/dev/null") return { ok: true, code: 0, stdout: "/usr/bin/podman\n", stderr: "" }; if (cmd === "command -v docker 2>/dev/null") return { ok: false, code: 1, stdout: "", stderr: "" }; if (cmd.startsWith("podman info")) return { ok: true, code: 0, stdout: "ok\n", stderr: "" }; return { ok: false, code: 1, stdout: "", stderr: "" }; }, }; const findings = await containers.run(ctx); assert.equal(findings.length, 1); assert.match(findings[0].title, /Container runtimes are ready/); }); test("containerdisk: large podman image storage is flagged medium", async () => { const ctx = stubCtx({}); ctx.thresholds = { containerWarnGB: 20, containerHighGB: 50 }; ctx.run = async (cmd) => { if (cmd === "podman system df 2>/dev/null") return { ok: true, code: 0, stdout: "TYPE TOTAL ACTIVE SIZE RECLAIMABLE\nImages 25 3 25.0GB 22.0GB (88%)\nContainers 4 1 150MB 100MB (66%)\nLocal Volumes 3 1 200MB 50MB (25%)\n", stderr: "" }; return { ok: false, code: 1, stdout: "", stderr: "" }; }; const findings = await containerdisk.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "medium"); assert.match(findings[0].title, /25\.0 GB/); }); test("containerdisk: no runtime installed is an info skip", async () => { const ctx = stubCtx({}); ctx.thresholds = { containerWarnGB: 20, containerHighGB: 50 }; const findings = await containerdisk.run(ctx); assert.equal(findings.length, 1); assert.equal(findings[0].severity, "info"); assert.match(findings[0].title, /container storage check skipped/i); }); test("crash: many reboots in a week is flagged", async () => { const ctx = stubCtx({}); ctx.run = async (cmd) => { if (cmd.includes("--list-boots")) { const lines = JSON.stringify(Array.from({ length: 25 }, (_, i) => ({ index: -i, boot_id: "b" + i, first_entry: (Date.now() - i * 60000) * 1000, last_entry: (Date.now() - i * 60000) * 1000 }))); return { ok: true, code: 0, stdout: lines + "\n", stderr: "" }; } if (cmd.includes("coredumpctl")) return { ok: false, code: 1, stdout: "", stderr: "" }; return { ok: false, code: 1, stdout: "", stderr: "" }; }; const findings = await crash.run(ctx); assert.ok(findings.length > 0, "expected at least one finding"); assert.match(findings[0].title, /reboot/i); assert.equal(findings[0].severity, "high"); }); // Regression: "Intel machine check reporting enabled on CPU#N" is a per-CPU boot // banner on Intel machines and it matches the crash grep's "machine check" // alternative. A reboot-heavy Intel box therefore reported crash/panic (high) // with a boot banner as its evidence. The hardware check already rejects that // line; the crash check now defers to the same classifier. test("crash: the Intel machine-check boot banner is not a kernel panic", async () => { const ctx = stubCtx({}); ctx.run = async (cmd) => { if (cmd.includes("--list-boots")) { const boots = JSON.stringify( Array.from({ length: 25 }, (_, i) => ({ index: -i, boot_id: "b" + i, first_entry: (Date.now() - i * 60000) * 1000, last_entry: (Date.now() - i * 60000) * 1000 })), ); return { ok: true, code: 0, stdout: boots + "\n", stderr: "" }; } if (cmd.includes("coredumpctl")) return { ok: false, code: 1, stdout: "", stderr: "" }; if (cmd.includes("journalctl -k")) { return { ok: true, code: 0, stdout: "Aug 13 03:11:22 host kernel: mce: Machine check reporting enabled on CPU#0\n", stderr: "" }; } return { ok: false, code: 1, stdout: "", stderr: "" }; }; const findings = await crash.run(ctx); assert.ok( !findings.some((f) => f.code === "crash/panic"), "a boot banner must not be reported as a kernel panic", ); }); test("crash: many reboots explained by an auto-update mechanism are not alarming", async () => { const ctx = stubCtx({}); ctx.run = async (cmd) => { if (cmd.includes("--list-boots")) { const l = JSON.stringify(Array.from({ length: 25 }, (_, i) => ({ index: -i, boot_id: "b" + i, first_entry: (Date.now() - i * 60000) * 1000, last_entry: (Date.now() - i * 60000) * 1000 }))); return { ok: true, code: 0, stdout: l + "\n", stderr: "" }; } if (cmd.includes("journalctl -k")) return { ok: true, code: 0, stdout: "", stderr: "" }; if (cmd.includes("systemctl list-timers")) return { ok: true, code: 0, stdout: " uupd.timer ...\n", stderr: "" }; if (cmd.includes("command -v uupd")) return { ok: true, code: 0, stdout: "/usr/bin/uupd\n/usr/bin/rpm-ostree\n", stderr: "" }; if (cmd.includes("coredumpctl")) return { ok: false, code: 1, stdout: "", stderr: "" }; return { ok: false, code: 1, stdout: "", stderr: "" }; }; const findings = await crash.run(ctx); assert.equal(findings.length, 1); assert.notEqual(findings[0].severity, "high"); assert.match(findings[0].title, /reboot/i); assert.match(findings[0].detail, /automatic updates/); }); test("crash: a real kernel panic is high even when an auto-update mechanism exists", async () => { const ctx = stubCtx({}); ctx.run = async (cmd) => { if (cmd.includes("--list-boots")) { const l = JSON.stringify(Array.from({ length: 25 }, (_, i) => ({ index: -i, boot_id: "b" + i, first_entry: (Date.now() - i * 60000) * 1000, last_entry: (Date.now() - i * 60000) * 1000 }))); return { ok: true, code: 0, stdout: l + "\n", stderr: "" }; } if (cmd.includes("journalctl -k")) return { ok: true, code: 0, stdout: "Aug 19 10:00:00 host kernel: Kernel panic - not syncing: Fatal exception\n", stderr: "" }; if (cmd.includes("systemctl list-timers")) return { ok: true, code: 0, stdout: " uupd.timer ...\n", stderr: "" }; if (cmd.includes("coredumpctl")) return { ok: false, code: 1, stdout: "", stderr: "" }; return { ok: false, code: 1, stdout: "", stderr: "" }; }; const findings = await crash.run(ctx); const panic = findings.find((f) => f.code === "crash/panic"); assert.ok(panic, "expected a panic finding"); assert.equal(panic.severity, "high"); assert.match(panic.evidence, /Kernel panic/); }); test("crash: coredumps are reported", async () => { const ctx = stubCtx({}); ctx.run = async (cmd) => { if (cmd.includes("--list-boots")) return { ok: true, code: 0, stdout: " 0 ...\n", stderr: "" }; if (cmd.includes("coredumpctl")) return { ok: true, code: 0, stdout: "TIME PID UID GID SIG EXE\nMon 2026-08-18 10:00:00 1234 1000 1000 SIGSEGV /usr/bin/foo\nMon 2026-08-18 11:00:00 5678 1000 1000 SIGABRT /usr/bin/bar\n", stderr: "" }; return { ok: false, code: 1, stdout: "", stderr: "" }; }; const findings = await crash.run(ctx); assert.ok(findings.some((f) => /crash/i.test(f.title)), "expected a crash finding"); }); // Trust budget: SIGTRAP cores come from debuggers/ptrace (gdb, emulators) — // an intentional signal, not a crash. Pinned here so a future refactor cannot // silently turn a gdb session into a "your system is crashing" finding. test("crash: SIGTRAP coredumps (debuggers/emulators) are NOT crashes", async () => { const ctx = stubCtx({}); ctx.run = async (cmd) => { if (cmd.includes("--list-boots")) return { ok: true, code: 0, stdout: " 0 ...\n", stderr: "" }; if (cmd.includes("coredumpctl")) return { ok: true, code: 0, stdout: "TIME PID UID GID SIG EXE\nMon 2026-08-18 10:00:00 1234 1000 1000 SIGTRAP /usr/bin/gdb\nMon 2026-08-18 10:05:00 1235 1000 1000 SIGTRAP /usr/bin/qemu-system-x86\n", stderr: "" }; return { ok: false, code: 1, stdout: "", stderr: "" }; }; const findings = await crash.run(ctx); assert.ok(!findings.some((f) => /crash/i.test(f.title)), "debugger SIGTRAP cores must not be reported as crashes"); }); test("crash: real crash signals (SIGSEGV/SIGABRT/SIGBUS) are still crashes", async () => { const ctx = stubCtx({}); ctx.run = async (cmd) => { if (cmd.includes("--list-boots")) return { ok: true, code: 0, stdout: " 0 ...\n", stderr: "" }; if (cmd.includes("coredumpctl")) return { ok: true, code: 0, stdout: "TIME PID UID GID SIG EXE\nMon 2026-08-18 10:00:00 1234 1000 1000 SIGSEGV /usr/bin/foo\nMon 2026-08-18 10:02:00 1235 1000 1000 SIGBUS /usr/bin/bar\nMon 2026-08-18 10:04:00 1236 1000 1000 SIGABRT /usr/bin/baz\n", stderr: "" }; return { ok: false, code: 1, stdout: "", stderr: "" }; }; const findings = await crash.run(ctx); const finding = findings.find((f) => /crash/i.test(f.title)); assert.ok(finding, "expected a crash finding for real signals"); assert.doesNotMatch(finding.evidence, /SIGTRAP/, "SIGTRAP cores must not appear in crash evidence"); assert.match(finding.evidence, /SIGBUS|SIGSEGV/, "real crash signals must remain visible"); }); test("stable codes: every built-in check emits findings with an explicit code", async () => { for (const check of checks) { const findings = await check.run(stubCtx({})); for (const f of findings) { assert.ok( typeof f.code === "string" && f.code.length > 0, `${check.id} finding "${f.title}" must carry a stable code` ); } } }); test("stable codes: every built-in findings.push block declares a code", () => { const dir = join(dirname(fileURLToPath(import.meta.url)), "..", "src", "checks"); for (const file of readdirSync(dir)) { if (!file.endsWith(".js")) continue; const src = readFileSync(join(dir, file), "utf8"); const re = /findings\.push\(\{([\s\S]*?)\}\);/g; let m; while ((m = re.exec(src))) { if (/severity:/.test(m[1]) && !/code:/.test(m[1])) { assert.fail(`${file}: findings.push block without a code`); } } } }); // Real smartctl exits non-zero when the drive is FAILING (status bit 3), so // the health string must be read before the !ok branch — otherwise a dying // disk is silently skipped. test("smart: FAILED health with a non-zero smartctl exit is still reported", async () => { const ctx = stubCtx({}); ctx.run = async (cmd) => { if (cmd === "smartctl --scan 2>/dev/null") return { ok: true, code: 0, stdout: "/dev/sda -d scsi # /dev/sda\n", stderr: "" }; if (cmd.includes("smartctl -H -c")) return { ok: false, code: 8, stdout: "SMART overall-health self-assessment test result: FAILED!\n", stderr: "" }; return { ok: false, code: 1, stdout: "", stderr: "" }; }; const findings = await smart.run(ctx); const failing = findings.find((f) => f.code === "smart/failing"); assert.ok(failing, "a failing disk must be reported even when smartctl exits non-zero"); assert.equal(failing.severity, "high"); }); test("crash: many boots outside the window are not flagged", async () => { const ctx = stubCtx({}); ctx.run = async (cmd) => { if (cmd.includes("--list-boots")) { const old = Array.from({ length: 25 }, (_, i) => ({ index: -i, boot_id: "b" + i, first_entry: (Date.now() - (30 + i) * 86400000) * 1000, last_entry: (Date.now() - (30 + i) * 86400000) * 1000, })); return { ok: true, code: 0, stdout: JSON.stringify(old), stderr: "" }; } return { ok: false, code: 1, stdout: "", stderr: "" }; }; const findings = await crash.run(ctx); assert.ok(!findings.some((f) => /reboot/i.test(f.title)), "lifetime boot count must not be reported as recent reboots"); }); test("crash: crashes concentrated in one program do not blame the hardware", async () => { // The real case: 26 of 33 cores from WebKitWebProcess after a bad AppImage // build. "Usually points at a broken library or bad RAM" sent that user // after the wrong thing entirely. const ctx = stubCtx({}); ctx.run = async (cmd) => { if (cmd.includes("--list-boots")) return { ok: true, code: 0, stdout: " 0 ...\n", stderr: "" }; if (cmd.includes("--json=short")) { const rows = Array.from({ length: 26 }, () => ({ sig: 6, exe: "/tmp/.mount_x/usr/lib/webkit2gtk-4.1/WebKitWebProcess" })); rows.push({ sig: 11, exe: "/usr/bin/xdg-desktop-portal-kde" }); rows.push({ sig: 6, exe: "/usr/bin/ksecretd" }); return { ok: true, code: 0, stdout: JSON.stringify(rows), stderr: "" }; } return { ok: false, code: 1, stdout: "", stderr: "" }; }; const findings = await crash.run(ctx); const f = findings.find((x) => x.code === "crash/coredumps"); assert.ok(f, "expected a coredump finding"); assert.match(f.detail, /26 of them from `WebKitWebProcess`/); assert.doesNotMatch(f.detail, /bad RAM/); assert.doesNotMatch(f.fix, /memtest/); assert.match(f.fix, /different build/, "points at the app, not at the system"); }); test("crash: crashes across unrelated programs do point at the hardware", async () => { const ctx = stubCtx({}); ctx.run = async (cmd) => { if (cmd.includes("--list-boots")) return { ok: true, code: 0, stdout: " 0 ...\n", stderr: "" }; if (cmd.includes("--json=short")) { const rows = ["alpha", "bravo", "charlie", "delta", "echo", "foxtrot"].map((n) => ({ sig: 11, exe: `/usr/bin/${n}` })); return { ok: true, code: 0, stdout: JSON.stringify(rows), stderr: "" }; } return { ok: false, code: 1, stdout: "", stderr: "" }; }; const findings = await crash.run(ctx); const f = findings.find((x) => x.code === "crash/coredumps"); assert.match(f.detail, /bad RAM/); assert.match(f.fix, /memtest/); }); test("crash: SIGTRAP cores are filtered in the JSON path too", async () => { const ctx = stubCtx({}); ctx.run = async (cmd) => { if (cmd.includes("--list-boots")) return { ok: true, code: 0, stdout: " 0 ...\n", stderr: "" }; if (cmd.includes("--json=short")) { return { ok: true, code: 0, stdout: JSON.stringify([{ sig: 5, exe: "/usr/bin/gdb" }, { sig: 5, exe: "/usr/bin/qemu-system-x86" }]), stderr: "" }; } return { ok: false, code: 1, stdout: "", stderr: "" }; }; const findings = await crash.run(ctx); assert.ok(!findings.some((f) => /crash/i.test(f.title))); }); test("crash: the table fallback still groups by executable", async () => { // systemd older than 252 has no --json; the table path must reach the same // verdict, so a machine without it does not get the scarier message. const ctx = stubCtx({}); ctx.run = async (cmd) => { if (cmd.includes("--list-boots")) return { ok: true, code: 0, stdout: " 0 ...\n", stderr: "" }; if (cmd.includes("--json=short")) return { ok: false, code: 1, stdout: "", stderr: "" }; if (cmd.includes("coredumpctl")) { const rows = ["TIME PID UID GID SIG COREFILE EXE SIZE"]; for (let i = 0; i < 6; i++) rows.push(`Mon 2026-10-26 10:0${i}:00 1234 1000 1000 SIGABRT present /usr/lib/webkit2gtk-4.1/WebKitWebProcess 2M`); return { ok: true, code: 0, stdout: rows.join("\n") + "\n", stderr: "" }; } return { ok: false, code: 1, stdout: "", stderr: "" }; }; const findings = await crash.run(ctx); const f = findings.find((x) => x.code === "crash/coredumps"); assert.ok(f, "the fallback must still produce a finding"); assert.match(f.detail, /WebKitWebProcess/); assert.doesNotMatch(f.detail, /bad RAM/); }); test("network: missing getent is a skip, not a false DNS failure", async () => { const ctx = stubCtx({ "ip route show default 2>/dev/null": "default via 192.168.1.1 dev wlan0\n", "getent ahostsv4 kernel.org 2>&1 | head -1": "/bin/sh: 1: getent: not found\n", }); const findings = await network.run(ctx); const skip = findings.find((f) => f.code === "network/skipped"); assert.ok(skip, "expected a network/skipped finding"); assert.equal(skip.severity, "info"); assert.ok(!findings.some((f) => f.code === "network/dns"), "must not claim DNS is failing when getent is absent"); }); test("disk: an unusable df is an explicit skip, not silence", async () => { const findings = await disk.run(stubCtx({})); assert.equal(findings.length, 1); assert.equal(findings[0].code, "disk/skipped"); assert.equal(findings[0].severity, "info"); }); test("inodes: an unusable df -i is an explicit skip, not silence", async () => { const findings = await inodes.run(stubCtx({})); assert.equal(findings.length, 1); assert.equal(findings[0].code, "inodes/skipped"); assert.equal(findings[0].severity, "info"); });