Ctrl K
ComunidadGuías…

tests/checks.test.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// 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");
});