const assert = require('assert'); const S = require('./sysinfo.js'); // ============================================================================ // Fixtures captured from the host (2012 ThinkPad X230) via /proc, df, systemctl, ps, hwmon. // ============================================================================ const STAT0 = 'cpu 5430899 3815162 1035595 41251079 269664 239859 55955 0 0 0\ncpu0 1 2 3 4'; // second snapshot: +100 system, +100 idle => totalDelta 200, idleDelta 100 => usage 0.5 const STAT1 = 'cpu 5430899 3815162 1035695 41251179 269664 239859 55955 0 0 0\ncpu0 9 9 9 9'; const MEMINFO = [ 'MemTotal: 16176232 kB', 'MemFree: 9155632 kB', 'MemAvailable: 12945364 kB', 'Buffers: 680 kB', 'SwapTotal: 33554428 kB', 'SwapFree: 26795148 kB' ].join('\n'); const NETDEV = [ 'Inter-| Receive | Transmit', ' face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed', ' lo: 54663953 73730 0 0 0 0 0 0 54663953 73730 0 0 0 0 0 0', 'enp0s25: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0', ' wlan0: 2445277803 4752440 0 129580 0 0 0 0 5890720566 5360204 0 82 0 0 0 0' ].join('\n'); const DISKSTATS = [ ' 8 0 sda 9645 2012 90857 763 42944 11803 281531 93778 0 74294 161809 0 0 0 0 39089 67268', ' 8 1 sda1 234 2012 8145 99 12 1 11 3 0 56 102 0 0 0 0 0 0', ' 7 0 loop0 10 0 80 1 0 0 0 0 0 0 0', ' 259 0 nvme0n1 287018 21005 15602232 437312 519616 172971 20543952 3947826 0 1207756 4536137 13779 0 34278912 73655 19543 77343' ].join('\n'); const LOADAVG = '0.76 0.64 0.56 1/714 710938'; const DF = [ 'Filesystem 1-blocks Used Available Capacity Mounted on', '/dev/sda3 590718976000 31650119680 556774883328 6% /', 'tmpfs 8282230784 5876457472 2405773312 71% /tmp', '/dev/sda1 1071624192 193921024 877703168 19% /boot', '/dev/sda3 590718976000 31650119680 556774883328 6% /home' ].join('\n'); const FAILED_USER = 'app-Handy@autostart.service not-found failed failed app-Handy@autostart.service'; const FAILED_NONE = ''; const PS = [ ' PID COMMAND %CPU %MEM', ' 710984 rtk 77.7 0.0', ' 493586 claude 2.7 2.8', ' 619330 firefox 1.7 2.9' ].join('\n'); // ============================================================================ // cpuDelta — first call (no prev), steady-state delta, garbage guard // ============================================================================ let cd0 = S.cpuDelta(null, STAT0); assert.strictEqual(cd0.usage, 0, 'first call usage 0'); assert.strictEqual(cd0.next.idle, 41251079 + 269664, 'idle = idle + iowait'); assert.strictEqual(cd0.next.total, 5430899 + 3815162 + 1035595 + 41251079 + 269664 + 239859 + 55955, 'total = sum'); let cd1 = S.cpuDelta(cd0.next, STAT1); assert.strictEqual(cd1.usage, 0.5, 'delta 200 total / 100 non-idle => 0.5'); let cdg = S.cpuDelta(cd0.next, 'garbage no numbers here'); assert.strictEqual(cdg.usage, 0, 'garbage -> usage 0'); assert.strictEqual(cdg.next, cd0.next, 'garbage keeps prev snapshot'); // ============================================================================ // parseMeminfo — bytes (kB*1024) + usage ratios // ============================================================================ let mem = S.parseMeminfo(MEMINFO); assert.strictEqual(mem.memTotal, 16176232 * 1024, 'memTotal bytes'); assert.strictEqual(mem.memAvailable, 12945364 * 1024, 'memAvailable bytes (prefers MemAvailable)'); assert.strictEqual(mem.swapTotal, 33554428 * 1024, 'swapTotal bytes'); assert.strictEqual(mem.swapFree, 26795148 * 1024, 'swapFree bytes'); assert.ok(Math.abs(mem.memoryUsage - (16176232 - 12945364) / 16176232) < 1e-9, 'memoryUsage ratio'); assert.ok(Math.abs(mem.swapUsage - (33554428 - 26795148) / 33554428) < 1e-9, 'swapUsage ratio'); // no-swap machine -> swapUsage 0, no divide-by-zero assert.strictEqual(S.parseMeminfo('MemTotal: 100 kB\nMemAvailable: 50 kB').swapUsage, 0, 'no swap -> 0'); // ============================================================================ // parseNetDev — sum non-lo // ============================================================================ let net = S.parseNetDev(NETDEV); assert.strictEqual(net.rx, 0 + 2445277803, 'rx sums enp0s25 + wlan0, excludes lo'); assert.strictEqual(net.tx, 0 + 5890720566, 'tx sums enp0s25 + wlan0, excludes lo'); // ============================================================================ // parseDiskstats — sectors*512, skip loop/ram AND partitions (avoid double-count) // ============================================================================ let disk = S.parseDiskstats(DISKSTATS); // whole-disk only: sda=90857, nvme0n1=15602232 ; sda1 partition + loop0 skipped assert.strictEqual(disk.readBytes, (90857 + 15602232) * 512, 'readBytes whole-disk sum *512'); assert.strictEqual(disk.writeBytes, (281531 + 20543952) * 512, 'writeBytes whole-disk sum *512'); // ============================================================================ // rate — per-second, guards reset + bad interval // ============================================================================ assert.strictEqual(S.rate(1000, 3000, 2), 1000, 'rate (3000-1000)/2'); assert.strictEqual(S.rate(5000, 1000, 2), 0, 'counter reset -> 0'); assert.strictEqual(S.rate(0, 100, 0), 0, 'zero interval -> 0'); // ============================================================================ // parseLoadavg // ============================================================================ let la = S.parseLoadavg(LOADAVG); assert.strictEqual(la.one, 0.76); assert.strictEqual(la.five, 0.64); assert.strictEqual(la.fifteen, 0.56); // ============================================================================ // parseDf — skip header, parse percent, capture source + target // ============================================================================ let df = S.parseDf(DF); assert.strictEqual(df.length, 4, 'four real rows, header skipped'); assert.deepStrictEqual(df[0], { source: '/dev/sda3', target: '/', usedPercent: 6 }); assert.deepStrictEqual(df[1], { source: 'tmpfs', target: '/tmp', usedPercent: 71 }); assert.strictEqual(df[3].target, '/home'); // ============================================================================ // parseFailedUnits // ============================================================================ assert.deepStrictEqual(S.parseFailedUnits(FAILED_USER), ['app-Handy@autostart.service']); assert.deepStrictEqual(S.parseFailedUnits(FAILED_NONE), [], 'empty -> no units'); // ============================================================================ // parsePsTop — skip header (non-numeric pid) // ============================================================================ let ps = S.parsePsTop(PS); assert.strictEqual(ps.length, 3, 'header dropped'); assert.deepStrictEqual(ps[0], { pid: 710984, name: 'rtk', cpu: 77.7, mem: 0.0 }); assert.strictEqual(ps[1].name, 'claude'); // ============================================================================ // parseTemp / parseFan // ============================================================================ assert.strictEqual(S.parseTemp('65000'), 65, 'millidegrees -> C'); assert.strictEqual(S.parseTemp(''), null, 'empty temp -> null'); assert.strictEqual(S.parseFan('3816'), 3816, 'fan rpm'); assert.strictEqual(S.parseFan(' '), null, 'empty fan -> null'); // ============================================================================ // formatBytes / formatRate — verbatim thresholds // ============================================================================ assert.strictEqual(S.formatBytes(512), '512 B'); assert.strictEqual(S.formatBytes(2048), '2.0 KiB'); assert.strictEqual(S.formatBytes(5 * 1048576), '5.0 MiB'); assert.strictEqual(S.formatBytes(3 * 1073741824), '3.0 GiB'); assert.strictEqual(S.formatBytes(-5), '0 B', 'negative clamps'); assert.strictEqual(S.formatRate(2048), '2.0 KiB/s'); // ============================================================================ // pushRing — append + cap, returns new array, original untouched // ============================================================================ let r = []; for (let i = 1; i <= 5; i++) r = S.pushRing(r, i, 3); assert.deepStrictEqual(r, [3, 4, 5], 'capped to last 3'); let orig = [1, 2]; let r2 = S.pushRing(orig, 3, 32); assert.deepStrictEqual(orig, [1, 2], 'input not mutated'); assert.deepStrictEqual(r2, [1, 2, 3], 'appended copy'); // ============================================================================ // detectIssues — empty all-clear, then each rule: absent / below / above // ============================================================================ const CORES = 4; function base() { // a fully-healthy state: every check present and below threshold return { failedUnits: [], cpuTemperatureC: 55, tempHighC: 87, degradationReason: null, diskUsage: [{ source: '/dev/sda3', target: '/', usedPercent: 6 }], memoryUsage: 0.5, swapGrowing: false, healthPercentage: 95, isLaptopBattery: true, loadOne: 1.0, cores: CORES, updateCount: 0 }; } function ids(state) { return S.detectIssues(state).map(function (i) { return i.id; }); } function find(state, id) { return S.detectIssues(state).filter(function (i) { return i.id === id; })[0]; } // all-clear -> [] (no synthetic ok row; that's a view concern) assert.deepStrictEqual(S.detectIssues(base()), [], 'healthy -> no issues'); assert.deepStrictEqual(S.detectIssues({}), [], 'empty state -> no issues (all absent)'); // --- Failed services (error) --- { let s = base(); s.failedUnits = ['app-Handy@autostart.service']; let f = find(s, 'failed-services'); assert.strictEqual(f.severity, 'error', 'failed -> error'); assert.deepStrictEqual(f.action, { kind: 'systemctl-status', units: ['app-Handy@autostart.service'] }); let absent = base(); delete absent.failedUnits; assert.strictEqual(ids(absent).indexOf('failed-services'), -1, 'no failedUnits -> skip'); } // --- Thermal: absent / below 87 / >=87 warning / degradation error --- { let absent = base(); absent.cpuTemperatureC = null; absent.degradationReason = null; assert.strictEqual(ids(absent).indexOf('thermal'), -1, 'no temp + no signal -> skip'); let below = base(); below.cpuTemperatureC = 86; assert.strictEqual(ids(below).indexOf('thermal'), -1, '86 < 87 -> none'); let at = base(); at.cpuTemperatureC = 87; assert.strictEqual(find(at, 'thermal').severity, 'warning', '87 >= 87 -> warning'); let deg = base(); deg.cpuTemperatureC = 60; deg.degradationReason = 'ThermalLimit'; assert.strictEqual(find(deg, 'thermal').severity, 'error', 'degradation thermal -> error even when cool'); let degOther = base(); degOther.degradationReason = 'PowerSupply'; assert.strictEqual(ids(degOther).indexOf('thermal'), -1, 'non-thermal degradation -> skip'); } // --- Low disk: absent / 89 none / 90 warn / 94 warn / 95 error ; per-source dedupe --- { let absent = base(); delete absent.diskUsage; assert.strictEqual(ids(absent).indexOf('low-disk:/dev/sda3'), -1, 'no diskUsage -> skip'); function diskAt(pct) { let s = base(); s.diskUsage = [{ source: '/dev/sda3', target: '/', usedPercent: pct }]; return s; } assert.strictEqual(S.detectIssues(diskAt(89)).length, 0, '89 -> none'); assert.strictEqual(find(diskAt(90), 'low-disk:/dev/sda3').severity, 'warning', '90 -> warning'); assert.strictEqual(find(diskAt(94), 'low-disk:/dev/sda3').severity, 'warning', '94 -> warning'); assert.strictEqual(find(diskAt(95), 'low-disk:/dev/sda3').severity, 'error', '95 -> error'); // same source mounted twice over threshold -> one issue (worst) let dup = base(); dup.diskUsage = [ { source: '/dev/sda3', target: '/', usedPercent: 91 }, { source: '/dev/sda3', target: '/home', usedPercent: 96 } ]; let dupIssues = S.detectIssues(dup).filter(function (i) { return i.id.indexOf('low-disk') === 0; }); assert.strictEqual(dupIssues.length, 1, 'same device -> one disk issue'); assert.strictEqual(dupIssues[0].severity, 'error', 'worst mount wins'); } // --- Memory pressure: needs >=0.9 AND swapGrowing --- { let high = base(); high.memoryUsage = 0.95; high.swapGrowing = true; assert.strictEqual(find(high, 'mem-pressure').severity, 'warning', '0.95 + growing -> warning'); let noGrow = base(); noGrow.memoryUsage = 0.95; noGrow.swapGrowing = false; assert.strictEqual(ids(noGrow).indexOf('mem-pressure'), -1, 'high mem but no swap growth -> none'); let low = base(); low.memoryUsage = 0.5; low.swapGrowing = true; assert.strictEqual(ids(low).indexOf('mem-pressure'), -1, 'low mem -> none'); let absent = base(); absent.memoryUsage = null; assert.strictEqual(ids(absent).indexOf('mem-pressure'), -1, 'no mem reading -> skip'); } // --- Battery health tiers: 80 none, 79 warn, 60 warn, 59 error, absent/no-battery skip --- { function bat(h) { let s = base(); s.healthPercentage = h; return s; } assert.strictEqual(S.detectIssues(bat(80)).filter(i => i.id === 'battery-health').length, 0, '80 -> none'); assert.strictEqual(find(bat(79), 'battery-health').severity, 'warning', '79 -> warning'); assert.strictEqual(find(bat(60), 'battery-health').severity, 'warning', '60 -> warning'); assert.strictEqual(find(bat(59), 'battery-health').severity, 'error', '59 -> error'); let absent = base(); absent.healthPercentage = null; assert.strictEqual(ids(absent).indexOf('battery-health'), -1, 'no health -> skip'); let desktop = bat(50); desktop.isLaptopBattery = false; assert.strictEqual(ids(desktop).indexOf('battery-health'), -1, 'no laptop battery -> skip'); } // --- High load: loadOne > cores (strict) --- { let eq = base(); eq.loadOne = 4; eq.cores = 4; assert.strictEqual(ids(eq).indexOf('high-load'), -1, 'load == cores -> none (strict >)'); let over = base(); over.loadOne = 4.1; over.cores = 4; assert.strictEqual(find(over, 'high-load').severity, 'warning', '4.1 > 4 -> warning'); let absent = base(); absent.loadOne = null; assert.strictEqual(ids(absent).indexOf('high-load'), -1, 'no load -> skip'); } // --- Updates: 0 none, 1 info, absent (n/a) skip --- { let zero = base(); zero.updateCount = 0; assert.strictEqual(ids(zero).indexOf('updates'), -1, '0 updates -> none'); let one = base(); one.updateCount = 1; assert.strictEqual(find(one, 'updates').severity, 'info', '1 update -> info'); let absent = base(); absent.updateCount = null; assert.strictEqual(ids(absent).indexOf('updates'), -1, 'updateCount n/a -> skip'); } // --- Sort order: error -> warning -> info --- { let s = base(); s.failedUnits = ['x.service']; // error s.cpuTemperatureC = 90; // warning (thermal) s.updateCount = 3; // info let order = S.detectIssues(s).map(i => i.severity); let ranks = order.map(sev => sev === 'error' ? 0 : sev === 'warning' ? 1 : 2); for (let i = 1; i < ranks.length; i++) assert.ok(ranks[i] >= ranks[i - 1], 'sorted error->warning->info'); assert.strictEqual(order[0], 'error'); assert.strictEqual(order[order.length - 1], 'info'); } console.log('sysinfo.test.js: ALL PASS');