| 1 | import { afterEach, describe, expect, it } from "@effect/vitest"; |
| 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as FileSystem from "effect/FileSystem"; |
| 4 | import * as Layer from "effect/Layer"; |
| 5 | import * as Logger from "effect/Logger"; |
| 6 | import * as PlatformError from "effect/PlatformError"; |
| 7 | import * as References from "effect/References"; |
| 8 | import * as Schema from "effect/Schema"; |
| 9 | import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; |
| 10 | import * as HostProcess from "@t3tools/shared/HostProcess"; |
| 11 | import { vi } from "vite-plus/test"; |
| 12 | |
| 13 | import * as ProcessRunner from "../processRunner.ts"; |
| 14 | import * as ServerEnvironmentLabel from "./ServerEnvironmentLabel.ts"; |
| 15 | |
| 16 | const isServerEnvironmentLabelFileError = Schema.is( |
| 17 | ServerEnvironmentLabel.ServerEnvironmentLabelFileError, |
| 18 | ); |
| 19 | const isServerEnvironmentLabelCommandError = Schema.is( |
| 20 | ServerEnvironmentLabel.ServerEnvironmentLabelCommandError, |
| 21 | ); |
| 22 | |
| 23 | interface CapturedLog { |
| 24 | readonly message: unknown; |
| 25 | readonly annotations: Readonly<Record<string, unknown>>; |
| 26 | } |
| 27 | |
| 28 | const runMock = vi.fn<ProcessRunner.ProcessRunner["Service"]["run"]>(); |
| 29 | |
| 30 | const layerProcessRunnerTest = Layer.succeed( |
| 31 | ProcessRunner.ProcessRunner, |
| 32 | ProcessRunner.ProcessRunner.of({ |
| 33 | run: (input) => runMock(input), |
| 34 | }), |
| 35 | ); |
| 36 | const layerNoopFileSystem = FileSystem.layerNoop({}); |
| 37 | const layerTest = Layer.merge(layerNoopFileSystem, layerProcessRunnerTest); |
| 38 | const 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 | ); |
| 48 | const 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 | |
| 59 | afterEach(() => { |
| 60 | runMock.mockReset(); |
| 61 | }); |
| 62 | |
| 63 | describe("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 | }); |