| ... | ... | @@ -2,12 +2,28 @@ import { afterEach, describe, expect, it } from "@effect/vitest"; |
| 2 | 2 | import * as Effect from "effect/Effect"; |
| 3 | 3 | import * as FileSystem from "effect/FileSystem"; |
| 4 | 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"; |
| 5 | 9 | import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner"; |
| 6 | 10 | import { HostProcessHostname, HostProcessPlatform } from "@t3tools/shared/hostProcess"; |
| 7 | 11 | import { vi } from "vite-plus/test"; |
| 8 | 12 | |
| 9 | 13 | import * as ProcessRunner from "../processRunner.ts"; |
| 10 | | import { resolveServerEnvironmentLabel } from "./ServerEnvironmentLabel.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 | } |
| 11 | 27 | |
| 12 | 28 | const runMock = vi.fn<ProcessRunner.ProcessRunner["Service"]["run"]>(); |
| 13 | 29 | |
| ... | ... | @@ -47,7 +63,7 @@ afterEach(() => { |
| 47 | 63 | describe("resolveServerEnvironmentLabel", () => { |
| 48 | 64 | it.effect("uses hostname fallback regardless of launch mode", () => |
| 49 | 65 | Effect.gen(function* () { |
| 50 | | const result = yield* resolveServerEnvironmentLabel({ |
| 66 | const result = yield* ServerEnvironmentLabel.resolveServerEnvironmentLabel({ |
| 51 | 67 | cwdBaseName: "t3code", |
| 52 | 68 | }).pipe(Effect.provide(withHostPlatform(TestLayer, "win32", "macbook-pro"))); |
| 53 | 69 | |
| ... | ... | @@ -68,7 +84,7 @@ describe("resolveServerEnvironmentLabel", () => { |
| 68 | 84 | }), |
| 69 | 85 | ); |
| 70 | 86 | |
| 71 | | const result = yield* resolveServerEnvironmentLabel({ |
| 87 | const result = yield* ServerEnvironmentLabel.resolveServerEnvironmentLabel({ |
| 72 | 88 | cwdBaseName: "t3code", |
| 73 | 89 | }).pipe(Effect.provide(withHostPlatform(TestLayer, "darwin", "macbook-pro"))); |
| 74 | 90 | |
| ... | ... | @@ -85,7 +101,7 @@ describe("resolveServerEnvironmentLabel", () => { |
| 85 | 101 | |
| 86 | 102 | it.effect("prefers Linux PRETTY_HOSTNAME from machine-info", () => |
| 87 | 103 | Effect.gen(function* () { |
| 88 | | const result = yield* resolveServerEnvironmentLabel({ |
| 104 | const result = yield* ServerEnvironmentLabel.resolveServerEnvironmentLabel({ |
| 89 | 105 | cwdBaseName: "t3code", |
| 90 | 106 | }).pipe(Effect.provide(withHostPlatform(LinuxMachineInfoLayer, "linux", "buildbox"))); |
| 91 | 107 | |
| ... | ... | @@ -107,7 +123,7 @@ describe("resolveServerEnvironmentLabel", () => { |
| 107 | 123 | }), |
| 108 | 124 | ); |
| 109 | 125 | |
| 110 | | const result = yield* resolveServerEnvironmentLabel({ |
| 126 | const result = yield* ServerEnvironmentLabel.resolveServerEnvironmentLabel({ |
| 111 | 127 | cwdBaseName: "t3code", |
| 112 | 128 | }).pipe(Effect.provide(withHostPlatform(TestLayer, "linux", "runner-01"))); |
| 113 | 129 | |
| ... | ... | @@ -124,7 +140,7 @@ describe("resolveServerEnvironmentLabel", () => { |
| 124 | 140 | |
| 125 | 141 | it.effect("falls back to the hostname when friendly labels are unavailable", () => |
| 126 | 142 | Effect.gen(function* () { |
| 127 | | const result = yield* resolveServerEnvironmentLabel({ |
| 143 | const result = yield* ServerEnvironmentLabel.resolveServerEnvironmentLabel({ |
| 128 | 144 | cwdBaseName: "t3code", |
| 129 | 145 | }).pipe(Effect.provide(withHostPlatform(TestLayer, "win32", "JULIUS-LAPTOP"))); |
| 130 | 146 | |
| ... | ... | @@ -132,25 +148,111 @@ describe("resolveServerEnvironmentLabel", () => { |
| 132 | 148 | }), |
| 133 | 149 | ); |
| 134 | 150 | |
| 135 | | it.effect("falls back to the hostname when the friendly-label command is missing", () => |
| 136 | | Effect.gen(function* () { |
| 137 | | runMock.mockReturnValueOnce( |
| 138 | | Effect.fail( |
| 139 | | new ProcessRunner.ProcessSpawnError({ |
| 140 | | command: "scutil", |
| 141 | | argumentCount: 2, |
| 142 | | cause: new Error("spawn scutil ENOENT"), |
| 143 | | }), |
| 144 | | ), |
| 145 | | ); |
| 151 | it.effect("falls back to the hostname when the friendly-label command is missing", () => { |
| 152 | const logs: CapturedLog[] = []; |
| 153 | const logger = Logger.make(({ fiber, message }) => { |
| 154 | logs.push({ |
| 155 | message, |
| 156 | annotations: fiber.getRef(References.CurrentLogAnnotations), |
| 157 | }); |
| 158 | }); |
| 159 | const spawnCause = new Error("spawn scutil ENOENT"); |
| 160 | const processError = new ProcessRunner.ProcessSpawnError({ |
| 161 | command: "scutil", |
| 162 | argumentCount: 2, |
| 163 | cause: spawnCause, |
| 164 | }); |
| 165 | runMock.mockReturnValueOnce(Effect.fail(processError)); |
| 146 | 166 | |
| 147 | | const result = yield* resolveServerEnvironmentLabel({ |
| 167 | return Effect.gen(function* () { |
| 168 | const result = yield* ServerEnvironmentLabel.resolveServerEnvironmentLabel({ |
| 148 | 169 | cwdBaseName: "t3code", |
| 149 | | }).pipe(Effect.provide(withHostPlatform(TestLayer, "darwin", "macbook-pro"))); |
| 170 | }); |
| 150 | 171 | |
| 151 | 172 | expect(result).toBe("macbook-pro"); |
| 152 | | }), |
| 153 | | ); |
| 173 | expect(logs[0]?.message).toEqual([ |
| 174 | "Failed to run environment-label probe 'macos-computer-name' with scutil.", |
| 175 | ]); |
| 176 | const error = logs[0]?.annotations.cause; |
| 177 | expect(isServerEnvironmentLabelCommandError(error)).toBe(true); |
| 178 | if (isServerEnvironmentLabelCommandError(error)) { |
| 179 | expect(error.probe).toBe("macos-computer-name"); |
| 180 | expect(error.executable).toBe("scutil"); |
| 181 | expect(error.argumentCount).toBe(2); |
| 182 | expect(error).not.toHaveProperty("args"); |
| 183 | expect(error.message).not.toContain("--get"); |
| 184 | expect(error.message).not.toContain("ComputerName"); |
| 185 | expect(error.cause).toBe(processError); |
| 186 | expect(processError.cause).toBe(spawnCause); |
| 187 | } |
| 188 | }).pipe( |
| 189 | Effect.provide( |
| 190 | Layer.mergeAll( |
| 191 | withHostPlatform(TestLayer, "darwin", "macbook-pro"), |
| 192 | Logger.layer([logger], { mergeWithExisting: false }), |
| 193 | Layer.succeed(References.MinimumLogLevel, "Debug"), |
| 194 | ), |
| 195 | ), |
| 196 | ); |
| 197 | }); |
| 198 | |
| 199 | it.effect("continues to hostnamectl after a machine-info inspect failure", () => { |
| 200 | const logs: CapturedLog[] = []; |
| 201 | const logger = Logger.make(({ fiber, message }) => { |
| 202 | logs.push({ |
| 203 | message, |
| 204 | annotations: fiber.getRef(References.CurrentLogAnnotations), |
| 205 | }); |
| 206 | }); |
| 207 | const fileCause = new Error("permission denied"); |
| 208 | const platformError = PlatformError.systemError({ |
| 209 | _tag: "PermissionDenied", |
| 210 | module: "FileSystem", |
| 211 | method: "exists", |
| 212 | pathOrDescriptor: "/etc/machine-info", |
| 213 | cause: fileCause, |
| 214 | }); |
| 215 | const fileSystemLayer = FileSystem.layerNoop({ |
| 216 | exists: () => Effect.fail(platformError), |
| 217 | }); |
| 218 | runMock.mockReturnValueOnce( |
| 219 | Effect.succeed({ |
| 220 | stdout: "CI Runner\n", |
| 221 | stderr: "", |
| 222 | code: ChildProcessSpawner.ExitCode(0), |
| 223 | timedOut: false, |
| 224 | stdoutTruncated: false, |
| 225 | stderrTruncated: false, |
| 226 | }), |
| 227 | ); |
| 228 | |
| 229 | return Effect.gen(function* () { |
| 230 | const result = yield* ServerEnvironmentLabel.resolveServerEnvironmentLabel({ |
| 231 | cwdBaseName: "t3code", |
| 232 | }); |
| 233 | |
| 234 | expect(result).toBe("CI Runner"); |
| 235 | expect(logs[0]?.message).toEqual([ |
| 236 | "Failed to inspect environment-label file at /etc/machine-info.", |
| 237 | ]); |
| 238 | const error = logs[0]?.annotations.cause; |
| 239 | expect(isServerEnvironmentLabelFileError(error)).toBe(true); |
| 240 | if (isServerEnvironmentLabelFileError(error)) { |
| 241 | expect(error.operation).toBe("inspect"); |
| 242 | expect(error.path).toBe("/etc/machine-info"); |
| 243 | expect(error.cause).toBe(platformError); |
| 244 | expect(platformError.cause).toBe(fileCause); |
| 245 | } |
| 246 | }).pipe( |
| 247 | Effect.provide( |
| 248 | Layer.mergeAll( |
| 249 | withHostPlatform(Layer.merge(ProcessRunnerTest, fileSystemLayer), "linux", "buildbox"), |
| 250 | Logger.layer([logger], { mergeWithExisting: false }), |
| 251 | Layer.succeed(References.MinimumLogLevel, "Debug"), |
| 252 | ), |
| 253 | ), |
| 254 | ); |
| 255 | }); |
| 154 | 256 | |
| 155 | 257 | it.effect("falls back to the cwd basename when the hostname is blank", () => |
| 156 | 258 | Effect.gen(function* () { |
| ... | ... | @@ -165,7 +267,7 @@ describe("resolveServerEnvironmentLabel", () => { |
| 165 | 267 | }), |
| 166 | 268 | ); |
| 167 | 269 | |
| 168 | | const result = yield* resolveServerEnvironmentLabel({ |
| 270 | const result = yield* ServerEnvironmentLabel.resolveServerEnvironmentLabel({ |
| 169 | 271 | cwdBaseName: "t3code", |
| 170 | 272 | }).pipe(Effect.provide(withHostPlatform(TestLayer, "linux", " "))); |
| 171 | 273 | |