about-system 0.10.15 → 0.10.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,112 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const flags = vi.hoisted(() => ({ linux: true, mac: false, windows: false }));
4
+ vi.mock("../utils/platform", () => ({
5
+ get IS_LINUX() {
6
+ return flags.linux;
7
+ },
8
+ get IS_MAC() {
9
+ return flags.mac;
10
+ },
11
+ get IS_WINDOWS() {
12
+ return flags.windows;
13
+ },
14
+ }));
15
+
16
+ const execCommand = vi.hoisted(() => vi.fn());
17
+ vi.mock("../utils/command", () => ({ execCommand, commandExists: vi.fn() }));
18
+
19
+ const osMock = vi.hoisted(() => ({
20
+ uptime: vi.fn(() => 0),
21
+ tmpdir: vi.fn(() => "/tmp"),
22
+ platform: vi.fn(() => "linux"),
23
+ }));
24
+ vi.mock("os", () => ({ ...osMock, default: osMock }));
25
+
26
+ const p = await import("./process");
27
+ import type { InfoContext } from "../types/internal-types";
28
+
29
+ const ctx = (): InfoContext => ({ cache: {} });
30
+ const platform = (v: "linux" | "mac" | "windows") => {
31
+ flags.linux = v === "linux";
32
+ flags.mac = v === "mac";
33
+ flags.windows = v === "windows";
34
+ };
35
+
36
+ beforeEach(() => {
37
+ platform("linux");
38
+ execCommand.mockReset().mockReturnValue("");
39
+ });
40
+
41
+ describe("top_process", () => {
42
+ it("reports the busiest process on Linux with a single percent sign", () => {
43
+ execCommand.mockReturnValue(" 12.5 /usr/bin/node\n 3.0 bash");
44
+ expect(p.top_process(ctx())).toBe("12% node");
45
+ });
46
+
47
+ it("is empty when ps output cannot be parsed on Linux", () => {
48
+ execCommand.mockReturnValue("");
49
+ expect(p.top_process(ctx())).toBe("");
50
+ });
51
+
52
+ it("parses `ps -r` output on macOS and rounds the CPU value", () => {
53
+ platform("mac");
54
+ execCommand.mockReturnValue("%CPU COMM\n 37.6 Google Chrome\n 2.0 Finder");
55
+ expect(p.top_process(ctx())).toBe("38% Google Chrome");
56
+ });
57
+
58
+ it("is empty on macOS when there is no data row or it is malformed", () => {
59
+ platform("mac");
60
+ execCommand.mockReturnValue("%CPU COMM");
61
+ expect(p.top_process(ctx())).toBe("");
62
+ execCommand.mockReturnValue("%CPU COMM\nabc");
63
+ expect(p.top_process(ctx())).toBe("");
64
+ });
65
+
66
+ it("is empty on Windows", () => {
67
+ platform("windows");
68
+ expect(p.top_process(ctx())).toBe("");
69
+ });
70
+
71
+ it("serves from the cache", () => {
72
+ const c = ctx();
73
+ execCommand.mockReturnValue("5.1 node");
74
+ expect(p.top_process(c)).toBe("5% node");
75
+ execCommand.mockReturnValue("99.9 other");
76
+ expect(p.top_process(c)).toBe("5% node");
77
+ });
78
+ });
79
+
80
+ describe("uptime", () => {
81
+ it.each([
82
+ [0, "0d 0h 0m"],
83
+ [59, "0d 0h 0m"],
84
+ [3_660, "0d 1h 1m"],
85
+ [90_061, "1d 1h 1m"],
86
+ [86_400 * 10 + 7_200 + 1_800, "10d 2h 30m"],
87
+ ])("formats %i seconds", (secs, expected) => {
88
+ osMock.uptime.mockReturnValue(secs);
89
+ expect(p.uptime()).toBe(expected);
90
+ });
91
+ });
92
+
93
+ describe("users_logged_in", () => {
94
+ it("counts non-blank `who` lines", () => {
95
+ execCommand.mockReturnValue("a tty1\n\nb pts/0\n");
96
+ expect(p.users_logged_in()).toBe("2 users");
97
+ });
98
+
99
+ it("is empty with nobody logged in or on Windows", () => {
100
+ execCommand.mockReturnValue("");
101
+ expect(p.users_logged_in()).toBe("");
102
+ platform("windows");
103
+ expect(p.users_logged_in()).toBe("");
104
+ });
105
+
106
+ it("is empty when the command throws", () => {
107
+ execCommand.mockImplementation(() => {
108
+ throw new Error("x");
109
+ });
110
+ expect(p.users_logged_in()).toBe("");
111
+ });
112
+ });
@@ -26,7 +26,7 @@ export function top_process(context: InfoContext): string {
26
26
  const lines = ps.split("\n");
27
27
  if (lines.length > 0) {
28
28
  const topProcess = lines[0].trim().replace(/\s+/, " ").split(" ");
29
- const cpu = topProcess[0].replace(/\.\d+/, "%");
29
+ const cpu = topProcess[0].replace(/\.\d+/, "");
30
30
  const process = topProcess[1].split("/").pop();
31
31
  const result = `${cpu}% ${process}`;
32
32
  setCachedValue(context.cache, "top_process", result);
@@ -0,0 +1,105 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const fsMock = vi.hoisted(() => ({
4
+ existsSync: vi.fn(),
5
+ readFileSync: vi.fn(),
6
+ writeFileSync: vi.fn(),
7
+ mkdirSync: vi.fn(),
8
+ }));
9
+ vi.mock("fs", () => ({ ...fsMock, default: fsMock }));
10
+
11
+ const osMock = vi.hoisted(() => ({
12
+ homedir: vi.fn(() => "/home/ada"),
13
+ tmpdir: vi.fn(() => "/tmpdir"),
14
+ }));
15
+ vi.mock("os", () => ({ ...osMock, default: osMock }));
16
+
17
+ const s = await import("./settings");
18
+
19
+ beforeEach(() => {
20
+ for (const f of Object.values(fsMock)) f.mockReset();
21
+ });
22
+
23
+ describe("paths", () => {
24
+ it("places settings under ~/.config and the cache under the temp dir", () => {
25
+ expect(s.SETTINGS_FILE).toBe("/home/ada/.config/systeminfo-settings.json");
26
+ expect(s.CACHE_FILE).toBe("/tmpdir/systeminfo-cache.json");
27
+ });
28
+ });
29
+
30
+ describe("defaults", () => {
31
+ it("defines a color, emoji and label for every displayed item", () => {
32
+ const shown = s.DEFAULT_SETTINGS.display_order.flat();
33
+ for (const key of shown) {
34
+ if (key === "pacman" || key === "bench" || key === "gpu_bench") continue;
35
+ expect(s.DEFAULT_SETTINGS.labels).toHaveProperty(key);
36
+ }
37
+ for (const color of Object.values(s.DEFAULT_SETTINGS.colors)) {
38
+ if (color === "multicolor") continue;
39
+ expect(s.colors).toHaveProperty(color);
40
+ expect(s.backgrounds).toHaveProperty(color);
41
+ }
42
+ });
43
+
44
+ it("uses ANSI escapes for every palette entry", () => {
45
+ for (const v of [...Object.values(s.colors), ...Object.values(s.backgrounds)]) {
46
+ expect(v.startsWith("\x1b[")).toBe(true);
47
+ }
48
+ });
49
+
50
+ it("falls back to 100 columns when stdout has no width", () => {
51
+ expect(s.DEFAULT_SETTINGS.display.line_wrap_length).toBeGreaterThan(0);
52
+ });
53
+ });
54
+
55
+ describe("loadSettings", () => {
56
+ it("returns the defaults when no settings file exists", () => {
57
+ fsMock.existsSync.mockReturnValue(false);
58
+ expect(s.loadSettings()).toBe(s.DEFAULT_SETTINGS);
59
+ });
60
+
61
+ it("shallow-merges the user's file over the defaults", () => {
62
+ fsMock.existsSync.mockReturnValue(true);
63
+ fsMock.readFileSync.mockReturnValue(JSON.stringify({ labels: { user: "Me" } }));
64
+ const loaded = s.loadSettings();
65
+ expect(loaded.labels).toEqual({ user: "Me" });
66
+ expect(loaded.colors).toBe(s.DEFAULT_SETTINGS.colors);
67
+ expect(fsMock.readFileSync).toHaveBeenCalledWith(s.SETTINGS_FILE, "utf8");
68
+ });
69
+
70
+ it("falls back to the defaults on malformed JSON or a read error", () => {
71
+ fsMock.existsSync.mockReturnValue(true);
72
+ fsMock.readFileSync.mockReturnValue("{not json");
73
+ expect(s.loadSettings()).toBe(s.DEFAULT_SETTINGS);
74
+ fsMock.readFileSync.mockImplementation(() => {
75
+ throw new Error("EACCES");
76
+ });
77
+ expect(s.loadSettings()).toBe(s.DEFAULT_SETTINGS);
78
+ });
79
+ });
80
+
81
+ describe("saveSettings", () => {
82
+ it("creates the config dir when missing and writes pretty JSON", () => {
83
+ fsMock.existsSync.mockReturnValue(false);
84
+ expect(s.saveSettings(s.DEFAULT_SETTINGS)).toBe(true);
85
+ expect(fsMock.mkdirSync).toHaveBeenCalledWith("/home/ada/.config", { recursive: true });
86
+ const [file, body] = fsMock.writeFileSync.mock.calls[0];
87
+ expect(file).toBe(s.SETTINGS_FILE);
88
+ expect(JSON.parse(body)).toEqual(JSON.parse(JSON.stringify(s.DEFAULT_SETTINGS)));
89
+ expect(body).toContain('\n "display_order"');
90
+ });
91
+
92
+ it("skips mkdir when the dir exists", () => {
93
+ fsMock.existsSync.mockReturnValue(true);
94
+ expect(s.saveSettings(s.DEFAULT_SETTINGS)).toBe(true);
95
+ expect(fsMock.mkdirSync).not.toHaveBeenCalled();
96
+ });
97
+
98
+ it("reports failure instead of throwing", () => {
99
+ fsMock.existsSync.mockReturnValue(true);
100
+ fsMock.writeFileSync.mockImplementation(() => {
101
+ throw new Error("EROFS");
102
+ });
103
+ expect(s.saveSettings(s.DEFAULT_SETTINGS)).toBe(false);
104
+ });
105
+ });
@@ -0,0 +1,157 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const flags = vi.hoisted(() => ({ windows: false }));
4
+ vi.mock("../utils/platform", () => ({
5
+ get IS_WINDOWS() {
6
+ return flags.windows;
7
+ },
8
+ IS_LINUX: true,
9
+ IS_MAC: false,
10
+ }));
11
+
12
+ const execCommand = vi.hoisted(() => vi.fn());
13
+ const commandExists = vi.hoisted(() => vi.fn());
14
+ vi.mock("../utils/command", () => ({ execCommand, commandExists }));
15
+
16
+ const osMock = vi.hoisted(() => ({
17
+ userInfo: vi.fn(() => ({ shell: "/bin/zsh" })),
18
+ tmpdir: vi.fn(() => "/tmp"),
19
+ platform: vi.fn(() => "linux"),
20
+ }));
21
+ vi.mock("os", () => ({ ...osMock, default: osMock }));
22
+ vi.mock("process", () => ({ default: { ppid: 4242 } }));
23
+
24
+ const s = await import("./software");
25
+ import type { InfoContext } from "../types/internal-types";
26
+
27
+ const ctx = (): InfoContext => ({ cache: {} });
28
+
29
+ beforeEach(() => {
30
+ flags.windows = false;
31
+ execCommand.mockReset().mockReturnValue("");
32
+ commandExists.mockReset().mockReturnValue(false);
33
+ osMock.userInfo.mockReset().mockReturnValue({ shell: "/bin/zsh" });
34
+ });
35
+
36
+ describe("shell", () => {
37
+ it("returns the login shell's basename", () => {
38
+ expect(s.shell(ctx())).toBe("zsh");
39
+ });
40
+
41
+ it("falls back to the parent process name when there is no login shell", () => {
42
+ osMock.userInfo.mockReturnValue({ shell: "" });
43
+ execCommand.mockReturnValue("/usr/bin/fish");
44
+ expect(s.shell(ctx())).toBe("fish");
45
+ expect(execCommand).toHaveBeenCalledWith("ps -p 4242 -o comm=");
46
+ });
47
+
48
+ it("falls back to ps when userInfo throws", () => {
49
+ osMock.userInfo.mockImplementation(() => {
50
+ throw new Error("x");
51
+ });
52
+ execCommand.mockReturnValue("bash");
53
+ expect(s.shell(ctx())).toBe("bash");
54
+ });
55
+
56
+ it("is empty when nothing identifies the shell", () => {
57
+ osMock.userInfo.mockReturnValue({ shell: "" });
58
+ execCommand.mockReturnValue("");
59
+ expect(s.shell(ctx())).toBe("");
60
+ execCommand.mockImplementation(() => {
61
+ throw new Error("x");
62
+ });
63
+ expect(s.shell(ctx())).toBe("");
64
+ });
65
+
66
+ it("is empty on Windows and serves from cache", () => {
67
+ flags.windows = true;
68
+ const c = ctx();
69
+ expect(s.shell(c)).toBe("");
70
+ flags.windows = false;
71
+ expect(s.shell(c)).toBe("");
72
+ });
73
+ });
74
+
75
+ describe("packages", () => {
76
+ it("lists the package managers/tools that exist", () => {
77
+ commandExists.mockImplementation((c: string) => ["apt", "npm", "bun"].includes(c));
78
+ const c = ctx();
79
+ expect(s.packages(c)).toBe("apt npm bun");
80
+ commandExists.mockReturnValue(false);
81
+ expect(s.packages(c)).toBe("apt npm bun");
82
+ });
83
+
84
+ it("is empty when nothing is installed", () => {
85
+ expect(s.packages(ctx())).toBe("");
86
+ });
87
+ });
88
+
89
+ describe("common_versions", () => {
90
+ const outputs: Record<string, string> = {
91
+ "node --version": "v22.1.0",
92
+ "git --version": "git version 2.44.0",
93
+ "docker --version": "Docker version 26.1.0, build abc",
94
+ "python3 --version": "Python 3.12.3",
95
+ "go version": "go version go1.22.0 linux/amd64",
96
+ "rustc --version": "rustc 1.77.0 (abc 2024-03-17)",
97
+ };
98
+
99
+ it("cleans each tool's version output", () => {
100
+ commandExists.mockReturnValue(true);
101
+ execCommand.mockImplementation((c: string) => outputs[c]);
102
+ expect(s.common_versions(ctx())).toBe(
103
+ "node:22.1.0 git:2.44.0 docker:26.1.0 python3:3.12.3 go:1.22.0 rustc:1.77.0",
104
+ );
105
+ });
106
+
107
+ it("skips missing tools, empty versions and failing commands", () => {
108
+ commandExists.mockImplementation((c: string) => ["node", "git", "go"].includes(c));
109
+ execCommand.mockImplementation((c: string) => {
110
+ if (c.startsWith("git")) return "";
111
+ if (c.startsWith("go")) throw new Error("x");
112
+ return outputs[c];
113
+ });
114
+ const c = ctx();
115
+ expect(s.common_versions(c)).toBe("node:22.1.0");
116
+ execCommand.mockReturnValue("v1");
117
+ expect(s.common_versions(c)).toBe("node:22.1.0");
118
+ });
119
+ });
120
+
121
+ describe("containers", () => {
122
+ it("is empty without docker", () => {
123
+ expect(s.containers(ctx())).toBe("");
124
+ });
125
+
126
+ it("is empty when no containers are running", () => {
127
+ commandExists.mockReturnValue(true);
128
+ execCommand.mockReturnValue("");
129
+ expect(s.containers(ctx())).toBe("");
130
+ });
131
+
132
+ it("lists names with their de-duplicated published ports", () => {
133
+ commandExists.mockReturnValue(true);
134
+ execCommand.mockImplementation((c: string) =>
135
+ c === "docker ps -q"
136
+ ? "abc\ndef\nghi"
137
+ : [
138
+ "web\t0.0.0.0:8080->80/tcp, :::8080->80/tcp, 0.0.0.0:9000-9002->9000-9002/tcp",
139
+ "db\t5432/tcp",
140
+ "bare",
141
+ "\t0.0.0.0:1->1/tcp",
142
+ ].join("\n"),
143
+ );
144
+ const c = ctx();
145
+ expect(s.containers(c)).toBe("web 80 9000-9002 db bare");
146
+ execCommand.mockReturnValue("");
147
+ expect(s.containers(c)).toBe("web 80 9000-9002 db bare");
148
+ });
149
+
150
+ it("is empty when docker errors", () => {
151
+ commandExists.mockReturnValue(true);
152
+ execCommand.mockImplementation(() => {
153
+ throw new Error("x");
154
+ });
155
+ expect(s.containers(ctx())).toBe("");
156
+ });
157
+ });
@@ -185,7 +185,7 @@ export function containers(context: InfoContext): string {
185
185
  if (portMatches) {
186
186
  const uniquePorts = [
187
187
  ...new Set(
188
- portMatches.map((p) => p.replace(/->\d+(-\d+)?\//, ""))
188
+ portMatches.map((p) => p.replace(/^->(\d+(-\d+)?)\/$/, "$1"))
189
189
  ),
190
190
  ];
191
191
  containerInfo.push(...uniquePorts);
@@ -0,0 +1,207 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const flags = vi.hoisted(() => ({ linux: true, mac: false }));
4
+ vi.mock("../utils/platform", () => ({
5
+ get IS_LINUX() {
6
+ return flags.linux;
7
+ },
8
+ get IS_MAC() {
9
+ return flags.mac;
10
+ },
11
+ IS_WINDOWS: false,
12
+ }));
13
+
14
+ const execCommand = vi.hoisted(() => vi.fn());
15
+ const commandExists = vi.hoisted(() => vi.fn());
16
+ vi.mock("../utils/command", () => ({ execCommand, commandExists }));
17
+
18
+ const files = vi.hoisted(() => ({ map: {} as Record<string, string> }));
19
+ const fsMock = vi.hoisted(() => ({
20
+ existsSync: vi.fn((p: string) => p in files.map),
21
+ readFileSync: vi.fn((p: string) => {
22
+ if (!(p in files.map)) throw new Error("ENOENT");
23
+ return files.map[p];
24
+ }),
25
+ writeFileSync: vi.fn(),
26
+ }));
27
+ vi.mock("fs", () => ({ ...fsMock, default: fsMock }));
28
+
29
+ const osMock = vi.hoisted(() => ({
30
+ loadavg: vi.fn(() => [0.5, 1.25, 2]),
31
+ tmpdir: vi.fn(() => "/tmp"),
32
+ platform: vi.fn(() => "linux"),
33
+ }));
34
+ vi.mock("os", () => ({ ...osMock, default: osMock }));
35
+
36
+ const st = await import("./system-status");
37
+ import type { InfoContext } from "../types/internal-types";
38
+
39
+ const ctx = (): InfoContext => ({ cache: {} });
40
+ const platform = (v: "linux" | "mac" | "other") => {
41
+ flags.linux = v === "linux";
42
+ flags.mac = v === "mac";
43
+ };
44
+
45
+ beforeEach(() => {
46
+ platform("linux");
47
+ files.map = {};
48
+ execCommand.mockReset().mockReturnValue("");
49
+ commandExists.mockReset().mockReturnValue(false);
50
+ });
51
+
52
+ describe("load_average", () => {
53
+ it("reads /proc/loadavg on Linux", () => {
54
+ files.map["/proc/loadavg"] = "0.10 0.20 0.30 1/200 999";
55
+ expect(st.load_average(ctx())).toBe("0.10 0.20 0.30");
56
+ });
57
+
58
+ it("is empty when /proc/loadavg is unreadable", () => {
59
+ expect(st.load_average(ctx())).toBe("");
60
+ });
61
+
62
+ it("formats os.loadavg on macOS", () => {
63
+ platform("mac");
64
+ expect(st.load_average(ctx())).toBe("0.50 1.25 2.00");
65
+ });
66
+
67
+ it("is empty on other platforms", () => {
68
+ platform("other");
69
+ expect(st.load_average(ctx())).toBe("");
70
+ });
71
+ });
72
+
73
+ describe("battery", () => {
74
+ it("reads capacity/status from sysfs on Linux", () => {
75
+ files.map["/sys/class/power_supply/BAT0/capacity"] = "87\n";
76
+ files.map["/sys/class/power_supply/BAT0/status"] = "Charging\n";
77
+ expect(st.battery(ctx())).toBe("87%+");
78
+ });
79
+
80
+ it("omits the + when discharging, or when status is unavailable", () => {
81
+ files.map["/sys/class/power_supply/BAT0/capacity"] = "50";
82
+ files.map["/sys/class/power_supply/BAT0/status"] = "Discharging";
83
+ expect(st.battery(ctx())).toBe("50%");
84
+ delete files.map["/sys/class/power_supply/BAT0/status"];
85
+ expect(st.battery(ctx())).toBe("50%");
86
+ });
87
+
88
+ it("is empty with no battery", () => {
89
+ expect(st.battery(ctx())).toBe("");
90
+ });
91
+
92
+ it("parses pmset on macOS", () => {
93
+ platform("mac");
94
+ execCommand.mockReturnValue("Now drawing from 'AC Power'\n -InternalBattery-0 (id=1)\t64%; charging; 1:00 remaining");
95
+ expect(st.battery(ctx())).toBe("64%+");
96
+ execCommand.mockReturnValue("-InternalBattery-0\t64%; discharging; 3:00 remaining");
97
+ expect(st.battery(ctx())).toBe("64%");
98
+ execCommand.mockReturnValue("No batteries");
99
+ expect(st.battery(ctx())).toBe("");
100
+ execCommand.mockImplementation(() => {
101
+ throw new Error("x");
102
+ });
103
+ expect(st.battery(ctx())).toBe("");
104
+ });
105
+
106
+ it("is empty on other platforms, and cached", () => {
107
+ platform("other");
108
+ const c = ctx();
109
+ expect(st.battery(c)).toBe("");
110
+ platform("linux");
111
+ files.map["/sys/class/power_supply/BAT0/capacity"] = "10";
112
+ expect(st.battery(c)).toBe("");
113
+ });
114
+ });
115
+
116
+ describe("temperature", () => {
117
+ it("reads the first plausible sysfs thermal source", () => {
118
+ files.map["/sys/class/thermal/thermal_zone0/temp"] = "0";
119
+ files.map["/sys/class/hwmon/hwmon0/temp1_input"] = "54321";
120
+ expect(st.temperature(ctx())).toBe("54°C");
121
+ });
122
+
123
+ it("rejects implausible readings", () => {
124
+ files.map["/sys/class/thermal/thermal_zone0/temp"] = "200000";
125
+ expect(st.temperature(ctx())).toBe("");
126
+ });
127
+
128
+ it("is empty when no sensors exist", () => {
129
+ expect(st.temperature(ctx())).toBe("");
130
+ });
131
+
132
+ it("uses osx-cpu-temp on macOS when installed", () => {
133
+ platform("mac");
134
+ commandExists.mockReturnValue(true);
135
+ execCommand.mockReturnValue("61.7°C");
136
+ expect(st.temperature(ctx())).toBe("62°C");
137
+ execCommand.mockReturnValue("999.0°C");
138
+ expect(st.temperature(ctx())).toBe("");
139
+ execCommand.mockReturnValue("no temp");
140
+ expect(st.temperature(ctx())).toBe("");
141
+ execCommand.mockImplementation(() => {
142
+ throw new Error("x");
143
+ });
144
+ expect(st.temperature(ctx())).toBe("");
145
+ });
146
+
147
+ it("is empty on macOS without osx-cpu-temp and on other platforms", () => {
148
+ platform("mac");
149
+ expect(st.temperature(ctx())).toBe("");
150
+ platform("other");
151
+ expect(st.temperature(ctx())).toBe("");
152
+ });
153
+
154
+ it("serves from the cache", () => {
155
+ const c = ctx();
156
+ files.map["/sys/class/thermal/thermal_zone0/temp"] = "40000";
157
+ expect(st.temperature(c)).toBe("40°C");
158
+ files.map["/sys/class/thermal/thermal_zone0/temp"] = "90000";
159
+ expect(st.temperature(c)).toBe("40°C");
160
+ });
161
+ });
162
+
163
+ describe("services_running", () => {
164
+ it("counts running units via systemctl", () => {
165
+ commandExists.mockImplementation((c: string) => c === "systemctl");
166
+ execCommand.mockReturnValue("a.service loaded active running A\nb.service loaded active running B\nfooter");
167
+ expect(st.services_running(ctx())).toBe("2 services");
168
+ });
169
+
170
+ it("falls back to `service --status-all`", () => {
171
+ commandExists.mockImplementation((c: string) => c === "service");
172
+ execCommand.mockReturnValue(" [ + ] cron\n [ - ] nginx\n [ + ] ssh");
173
+ expect(st.services_running(ctx())).toBe("2 services");
174
+ });
175
+
176
+ it("is empty with no service manager, zero services, or on error", () => {
177
+ expect(st.services_running(ctx())).toBe("");
178
+ commandExists.mockReturnValue(true);
179
+ execCommand.mockReturnValue("");
180
+ expect(st.services_running(ctx())).toBe("");
181
+ execCommand.mockImplementation(() => {
182
+ throw new Error("x");
183
+ });
184
+ expect(st.services_running(ctx())).toBe("");
185
+ });
186
+
187
+ it("counts launchctl entries on macOS", () => {
188
+ platform("mac");
189
+ execCommand.mockReturnValue("PID Status Label\n1 0 com.a\n- 0 com.b\n\n");
190
+ expect(st.services_running(ctx())).toBe("2 services");
191
+ execCommand.mockReturnValue("PID Status Label");
192
+ expect(st.services_running(ctx())).toBe("");
193
+ execCommand.mockImplementation(() => {
194
+ throw new Error("x");
195
+ });
196
+ expect(st.services_running(ctx())).toBe("");
197
+ });
198
+
199
+ it("is empty on other platforms, and cached", () => {
200
+ platform("other");
201
+ const c = ctx();
202
+ expect(st.services_running(c)).toBe("");
203
+ platform("mac");
204
+ execCommand.mockReturnValue("PID\n1 0 a");
205
+ expect(st.services_running(c)).toBe("");
206
+ });
207
+ });