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 Logger from "effect/Logger";
6import * as PlatformError from "effect/PlatformError";
7import * as References from "effect/References";
8import * as Schema from "effect/Schema";
9import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
10import * as HostProcess from "@t3tools/shared/HostProcess";
11import { vi } from "vite-plus/test";
12
13import * as ProcessRunner from "../processRunner.ts";
14import * as ServerEnvironmentLabel from "./ServerEnvironmentLabel.ts";
15
16const isServerEnvironmentLabelFileError = Schema.is(
17 ServerEnvironmentLabel.ServerEnvironmentLabelFileError,
18);
19const isServerEnvironmentLabelCommandError = Schema.is(
20 ServerEnvironmentLabel.ServerEnvironmentLabelCommandError,
21);
22
23interface CapturedLog {
24 readonly message: unknown;
25 readonly annotations: Readonly<Record<string, unknown>>;
26}
27
28const runMock = vi.fn<ProcessRunner.ProcessRunner["Service"]["run"]>();
29
30const layerProcessRunnerTest = Layer.succeed(
31 ProcessRunner.ProcessRunner,
32 ProcessRunner.ProcessRunner.of({
33 run: (input) => runMock(input),
34 }),
35);
36const layerNoopFileSystem = FileSystem.layerNoop({});
37const layerTest = Layer.merge(layerNoopFileSystem, layerProcessRunnerTest);
38const layerLinuxMachineInfo = Layer.merge(
39 layerProcessRunnerTest,
40 FileSystem.layerNoop({
41 exists: (path) => Effect.succeed(path === "/etc/machine-info"),
42 readFileString: (path) =>
43 path === "/etc/machine-info"
44 ? Effect.succeed('PRETTY_HOSTNAME="Build Agent 01"\nICON_NAME="computer-vm"\n')
45 : Effect.succeed(""),
46 }),
47);
48const withHostPlatform = <ROut, E, RIn>(
49 layer: Layer.Layer<ROut, E, RIn>,
50 platform: NodeJS.Platform,
51 hostname: string,
52) =>
53 Layer.mergeAll(
54 layer,
55 Layer.succeed(HostProcess.Platform, platform),
56 Layer.succeed(HostProcess.Hostname, hostname),
57 );
58
59afterEach(() => {
60 runMock.mockReset();
61});
62
63describe("resolveServerEnvironmentLabel", () => {
64 it.effect("uses hostname fallback regardless of launch mode", () =>
65 Effect.gen(function* () {
66 const result = yield* ServerEnvironmentLabel.resolveServerEnvironmentLabel({
67 cwdBaseName: "t3code",
68 }).pipe(Effect.provide(withHostPlatform(layerTest, "win32", "macbook-pro")));
69
70 expect(result).toBe("macbook-pro");
71 }),
72 );
73
74 it.effect("prefers the macOS ComputerName", () =>
75 Effect.gen(function* () {
76 runMock.mockReturnValueOnce(
77 Effect.succeed({
78 stdout: " Julius's MacBook Pro \n",
79 stderr: "",
80 code: ChildProcessSpawner.ExitCode(0),
81 timedOut: false,
82 stdoutTruncated: false,
83 stderrTruncated: false,
84 stdoutInvalidUtf8: false,
85 stderrInvalidUtf8: false,
86 }),
87 );
88
89 const result = yield* ServerEnvironmentLabel.resolveServerEnvironmentLabel({
90 cwdBaseName: "t3code",
91 }).pipe(Effect.provide(withHostPlatform(layerTest, "darwin", "macbook-pro")));
92
93 expect(result).toBe("Julius's MacBook Pro");
94 expect(runMock).toHaveBeenCalledWith(
95 expect.objectContaining({
96 command: "scutil",
97 args: ["--get", "ComputerName"],
98 timeoutBehavior: "timedOutResult",
99 }),
100 );
101 }),
102 );
103
104 it.effect("prefers Linux PRETTY_HOSTNAME from machine-info", () =>
105 Effect.gen(function* () {
106 const result = yield* ServerEnvironmentLabel.resolveServerEnvironmentLabel({
107 cwdBaseName: "t3code",
108 }).pipe(Effect.provide(withHostPlatform(layerLinuxMachineInfo, "linux", "buildbox")));
109
110 expect(result).toBe("Build Agent 01");
111 expect(runMock).not.toHaveBeenCalled();
112 }),
113 );
114
115 it.effect("falls back to hostnamectl pretty hostname on Linux", () =>
116 Effect.gen(function* () {
117 runMock.mockReturnValueOnce(
118 Effect.succeed({
119 stdout: "CI Runner\n",
120 stderr: "",
121 code: ChildProcessSpawner.ExitCode(0),
122 timedOut: false,
123 stdoutTruncated: false,
124 stderrTruncated: false,
125 stdoutInvalidUtf8: false,
126 stderrInvalidUtf8: false,
127 }),
128 );
129
130 const result = yield* ServerEnvironmentLabel.resolveServerEnvironmentLabel({
131 cwdBaseName: "t3code",
132 }).pipe(Effect.provide(withHostPlatform(layerTest, "linux", "runner-01")));
133
134 expect(result).toBe("CI Runner");
135 expect(runMock).toHaveBeenCalledWith(
136 expect.objectContaining({
137 command: "hostnamectl",
138 args: ["--pretty"],
139 timeoutBehavior: "timedOutResult",
140 }),
141 );
142 }),
143 );
144
145 it.effect("falls back to the hostname when friendly labels are unavailable", () =>
146 Effect.gen(function* () {
147 const result = yield* ServerEnvironmentLabel.resolveServerEnvironmentLabel({
148 cwdBaseName: "t3code",
149 }).pipe(Effect.provide(withHostPlatform(layerTest, "win32", "JULIUS-LAPTOP")));
150
151 expect(result).toBe("JULIUS-LAPTOP");
152 }),
153 );
154
155 it.effect("falls back to the hostname when the friendly-label command is missing", () => {
156 const logs: CapturedLog[] = [];
157 const logger = Logger.make(({ fiber, message }) => {
158 logs.push({
159 message,
160 annotations: fiber.getRef(References.CurrentLogAnnotations),
161 });
162 });
163 const spawnCause = new Error("spawn scutil ENOENT");
164 const processError = new ProcessRunner.ProcessSpawnError({
165 command: "scutil",
166 argumentCount: 2,
167 cause: spawnCause,
168 });
169 runMock.mockReturnValueOnce(Effect.fail(processError));
170
171 return Effect.gen(function* () {
172 const result = yield* ServerEnvironmentLabel.resolveServerEnvironmentLabel({
173 cwdBaseName: "t3code",
174 });
175
176 expect(result).toBe("macbook-pro");
177 expect(logs[0]?.message).toEqual([
178 "Failed to run environment-label probe 'macos-computer-name' with scutil.",
179 ]);
180 const error = logs[0]?.annotations.cause;
181 expect(isServerEnvironmentLabelCommandError(error)).toBe(true);
182 if (isServerEnvironmentLabelCommandError(error)) {
183 expect(error.probe).toBe("macos-computer-name");
184 expect(error.executable).toBe("scutil");
185 expect(error.argumentCount).toBe(2);
186 expect(error).not.toHaveProperty("args");
187 expect(error.message).not.toContain("--get");
188 expect(error.message).not.toContain("ComputerName");
189 expect(error.cause).toBe(processError);
190 expect(processError.cause).toBe(spawnCause);
191 }
192 }).pipe(
193 Effect.provide(
194 Layer.mergeAll(
195 withHostPlatform(layerTest, "darwin", "macbook-pro"),
196 Logger.layer([logger], { mergeWithExisting: false }),
197 Layer.succeed(References.MinimumLogLevel, "Debug"),
198 ),
199 ),
200 );
201 });
202
203 it.effect("continues to hostnamectl after a machine-info inspect failure", () => {
204 const logs: CapturedLog[] = [];
205 const logger = Logger.make(({ fiber, message }) => {
206 logs.push({
207 message,
208 annotations: fiber.getRef(References.CurrentLogAnnotations),
209 });
210 });
211 const fileCause = new Error("permission denied");
212 const platformError = PlatformError.systemError({
213 _tag: "PermissionDenied",
214 module: "FileSystem",
215 method: "exists",
216 pathOrDescriptor: "/etc/machine-info",
217 cause: fileCause,
218 });
219 const layerFileSystem = FileSystem.layerNoop({
220 exists: () => Effect.fail(platformError),
221 });
222 runMock.mockReturnValueOnce(
223 Effect.succeed({
224 stdout: "CI Runner\n",
225 stderr: "",
226 code: ChildProcessSpawner.ExitCode(0),
227 timedOut: false,
228 stdoutTruncated: false,
229 stderrTruncated: false,
230 stdoutInvalidUtf8: false,
231 stderrInvalidUtf8: false,
232 }),
233 );
234
235 return Effect.gen(function* () {
236 const result = yield* ServerEnvironmentLabel.resolveServerEnvironmentLabel({
237 cwdBaseName: "t3code",
238 });
239
240 expect(result).toBe("CI Runner");
241 expect(logs[0]?.message).toEqual([
242 "Failed to inspect environment-label file at /etc/machine-info.",
243 ]);
244 const error = logs[0]?.annotations.cause;
245 expect(isServerEnvironmentLabelFileError(error)).toBe(true);
246 if (isServerEnvironmentLabelFileError(error)) {
247 expect(error.operation).toBe("inspect");
248 expect(error.path).toBe("/etc/machine-info");
249 expect(error.cause).toBe(platformError);
250 expect(platformError.cause).toBe(fileCause);
251 }
252 }).pipe(
253 Effect.provide(
254 Layer.mergeAll(
255 withHostPlatform(
256 Layer.merge(layerProcessRunnerTest, layerFileSystem),
257 "linux",
258 "buildbox",
259 ),
260 Logger.layer([logger], { mergeWithExisting: false }),
261 Layer.succeed(References.MinimumLogLevel, "Debug"),
262 ),
263 ),
264 );
265 });
266
267 it.effect("falls back to the cwd basename when the hostname is blank", () =>
268 Effect.gen(function* () {
269 runMock.mockReturnValueOnce(
270 Effect.succeed({
271 stdout: " ",
272 stderr: "",
273 code: ChildProcessSpawner.ExitCode(0),
274 timedOut: false,
275 stdoutTruncated: false,
276 stderrTruncated: false,
277 stdoutInvalidUtf8: false,
278 stderrInvalidUtf8: false,
279 }),
280 );
281
282 const result = yield* ServerEnvironmentLabel.resolveServerEnvironmentLabel({
283 cwdBaseName: "t3code",
284 }).pipe(Effect.provide(withHostPlatform(layerTest, "linux", " ")));
285
286 expect(result).toBe("t3code");
287 }),
288 );
289});