1import { afterEach, describe, expect, it } from "@effect/vitest";
2import * as Effect from "effect/Effect";
3import * as FileSystem from "effect/FileSystem";
4import * as Layer from "effect/Layer";
5import * as PlatformError from "effect/PlatformError";
6import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
7import * as HostProcess from "@t3tools/shared/HostProcess";
8import { vi } from "vite-plus/test";
9
10import * as ProcessRunner from "../processRunner.ts";
11import {
12 detectServerEnvironmentMachineKind,
13 machineKindFromAppleProductName,
14 machineKindFromDmi,
15} from "./ServerEnvironmentMachine.ts";
16
17const runMock = vi.fn<ProcessRunner.ProcessRunner["Service"]["run"]>();
18
19const layerProcessRunnerTest = Layer.succeed(
20 ProcessRunner.ProcessRunner,
21 ProcessRunner.ProcessRunner.of({ run: (input) => runMock(input) }),
22);
23
24const processOutput = (stdout: string, code = 0) =>
25 Effect.succeed({
26 stdout,
27 stderr: "",
28 code: ChildProcessSpawner.ExitCode(code),
29 timedOut: false,
30 stdoutTruncated: false,
31 stderrTruncated: false,
32 stdoutInvalidUtf8: false,
33 stderrInvalidUtf8: false,
34 });
35
36const layerDmiFileSystem = (files: Readonly<Record<string, string>>) =>
37 FileSystem.layerNoop({
38 readFileString: (path) => {
39 const name = path.slice(path.lastIndexOf("/") + 1);
40 return name in files
41 ? Effect.succeed(files[name]!)
42 : Effect.fail(
43 PlatformError.systemError({
44 _tag: "NotFound",
45 module: "FileSystem",
46 method: "readFileString",
47 pathOrDescriptor: path,
48 cause: new Error("ENOENT"),
49 }),
50 );
51 },
52 });
53
54const withPlatform = (platform: NodeJS.Platform, fileSystem = FileSystem.layerNoop({})) =>
55 Layer.mergeAll(layerProcessRunnerTest, fileSystem, Layer.succeed(HostProcess.Platform, platform));
56
57afterEach(() => {
58 runMock.mockReset();
59});
60
61describe("machineKindFromAppleProductName", () => {
62 it("maps marketing names and model identifiers", () => {
63 expect(machineKindFromAppleProductName("Mac mini (2024)")).toBe("mac-mini");
64 expect(machineKindFromAppleProductName("Macmini8,1")).toBe("mac-mini");
65 expect(machineKindFromAppleProductName("Mac Studio (2023)")).toBe("mac-studio");
66 expect(machineKindFromAppleProductName("MacBook Pro (14-inch, 2024)")).toBe("laptop");
67 expect(machineKindFromAppleProductName("MacBookAir10,1")).toBe("laptop");
68 expect(machineKindFromAppleProductName("iMac (24-inch, 2024)")).toBe("desktop");
69 expect(machineKindFromAppleProductName("Mac Pro (2023)")).toBe("desktop");
70 });
71
72 it("returns null for Apple silicon model identifiers, which carry no product family", () => {
73 expect(machineKindFromAppleProductName("Mac16,10")).toBeNull();
74 });
75});
76
77describe("machineKindFromDmi", () => {
78 it("prefers virtualization markers over chassis type", () => {
79 expect(
80 machineKindFromDmi({ chassisType: "1", sysVendor: "QEMU", productName: "Standard PC" }),
81 ).toBe("cloud");
82 expect(
83 machineKindFromDmi({
84 chassisType: "3",
85 sysVendor: "Microsoft Corporation",
86 productName: "Virtual Machine",
87 }),
88 ).toBe("cloud");
89 expect(
90 machineKindFromDmi({ chassisType: "1", sysVendor: "Amazon EC2", productName: "t3.large" }),
91 ).toBe("cloud");
92 });
93
94 it("does not treat Microsoft hardware as a VM", () => {
95 expect(
96 machineKindFromDmi({
97 chassisType: "9",
98 sysVendor: "Microsoft Corporation",
99 productName: "Surface Laptop 5",
100 }),
101 ).toBe("laptop");
102 });
103
104 it("maps SMBIOS chassis codes", () => {
105 expect(
106 machineKindFromDmi({ chassisType: "3", sysVendor: "GMKtec", productName: "NucBox K8 Plus" }),
107 ).toBe("desktop");
108 expect(
109 machineKindFromDmi({ chassisType: "10", sysVendor: "LENOVO", productName: "ThinkPad X1" }),
110 ).toBe("laptop");
111 expect(
112 machineKindFromDmi({ chassisType: "23", sysVendor: "Supermicro", productName: "X11" }),
113 ).toBe("server");
114 expect(machineKindFromDmi({ chassisType: "1", sysVendor: null, productName: null })).toBeNull();
115 expect(
116 machineKindFromDmi({ chassisType: null, sysVendor: null, productName: null }),
117 ).toBeNull();
118 });
119
120 it("recognizes Apple hardware running Linux", () => {
121 expect(
122 machineKindFromDmi({ chassisType: "3", sysVendor: "Apple", productName: "Mac Studio" }),
123 ).toBe("mac-studio");
124 });
125});
126
127describe("detectServerEnvironmentMachineKind", () => {
128 it.effect("reads the IOKit product name on macOS", () =>
129 Effect.gen(function* () {
130 runMock.mockReturnValueOnce(
131 processOutput(
132 '+-o product <class IOPlatformDevice>\n {\n "product-name" = <"Mac mini (2024)">\n }\n',
133 ),
134 );
135
136 const result = yield* detectServerEnvironmentMachineKind().pipe(
137 Effect.provide(withPlatform("darwin")),
138 );
139
140 expect(result).toBe("mac-mini");
141 expect(runMock).toHaveBeenCalledTimes(1);
142 expect(runMock).toHaveBeenCalledWith(
143 expect.objectContaining({ command: "ioreg", args: ["-rd1", "-n", "product"] }),
144 );
145 }),
146 );
147
148 it.effect("falls back to hw.model when IOKit has no product node", () =>
149 Effect.gen(function* () {
150 runMock.mockReturnValueOnce(processOutput("", 1));
151 runMock.mockReturnValueOnce(processOutput("MacBookPro16,1\n"));
152
153 const result = yield* detectServerEnvironmentMachineKind().pipe(
154 Effect.provide(withPlatform("darwin")),
155 );
156
157 expect(result).toBe("laptop");
158 expect(runMock).toHaveBeenLastCalledWith(
159 expect.objectContaining({ command: "sysctl", args: ["-n", "hw.model"] }),
160 );
161 }),
162 );
163
164 it.effect("returns null when both macOS probes fail", () =>
165 Effect.gen(function* () {
166 runMock.mockImplementation((input) =>
167 Effect.fail(
168 new ProcessRunner.ProcessSpawnError({
169 command: input.command,
170 argumentCount: input.args.length,
171 cause: new Error("ENOENT"),
172 }),
173 ),
174 );
175
176 const result = yield* detectServerEnvironmentMachineKind().pipe(
177 Effect.provide(withPlatform("darwin")),
178 );
179
180 expect(result).toBeNull();
181 expect(runMock).toHaveBeenCalledTimes(2);
182 }),
183 );
184
185 it.effect("reads DMI on Linux", () =>
186 Effect.gen(function* () {
187 const result = yield* detectServerEnvironmentMachineKind().pipe(
188 Effect.provide(
189 withPlatform(
190 "linux",
191 layerDmiFileSystem({
192 chassis_type: "3\n",
193 sys_vendor: "GMKtec\n",
194 product_name: "NucBox K8 Plus\n",
195 }),
196 ),
197 ),
198 );
199
200 expect(result).toBe("desktop");
201 expect(runMock).not.toHaveBeenCalled();
202 }),
203 );
204
205 it.effect("recognizes WSL before its Hyper-V DMI identity", () =>
206 Effect.gen(function* () {
207 const result = yield* detectServerEnvironmentMachineKind().pipe(
208 Effect.provide(
209 withPlatform(
210 "linux",
211 layerDmiFileSystem({
212 osrelease: "5.15.153.1-microsoft-standard-WSL2\n",
213 chassis_type: "3\n",
214 sys_vendor: "Microsoft Corporation\n",
215 product_name: "Virtual Machine\n",
216 }),
217 ),
218 ),
219 );
220
221 expect(result).toBe("linux");
222 }),
223 );
224
225 it.effect("returns null on Linux without DMI (containers, ARM boards)", () =>
226 Effect.gen(function* () {
227 const result = yield* detectServerEnvironmentMachineKind().pipe(
228 Effect.provide(withPlatform("linux", layerDmiFileSystem({}))),
229 );
230
231 expect(result).toBeNull();
232 }),
233 );
234
235 it.effect("skips detection on other platforms", () =>
236 Effect.gen(function* () {
237 const result = yield* detectServerEnvironmentMachineKind().pipe(
238 Effect.provide(withPlatform("win32")),
239 );
240
241 expect(result).toBeNull();
242 expect(runMock).not.toHaveBeenCalled();
243 }),
244 );
245});