| ... | @@ -2,12 +2,28 @@ import { afterEach, describe, expect, it } from "@effect/vitest"; | ... | @@ -2,12 +2,28 @@ import { afterEach, describe, expect, it } from "@effect/vitest"; |
| 2 | import * as Effect from "effect/Effect"; | 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as FileSystem from "effect/FileSystem"; | 3 | import * as FileSystem from "effect/FileSystem"; |
| 4 | import * as Layer from "effect/Layer"; | 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 | import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner"; | 9 | import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner"; |
| 6 | import { HostProcessHostname, HostProcessPlatform } from "@t3tools/shared/hostProcess"; | 10 | import { HostProcessHostname, HostProcessPlatform } from "@t3tools/shared/hostProcess"; |
| 7 | import { vi } from "vite-plus/test"; | 11 | import { vi } from "vite-plus/test"; |
| 8 | | 12 | |
| 9 | import * as ProcessRunner from "../processRunner.ts"; | 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 | const runMock = vi.fn<ProcessRunner.ProcessRunner["Service"]["run"]>(); | 28 | const runMock = vi.fn<ProcessRunner.ProcessRunner["Service"]["run"]>(); |
| 13 | | 29 | |
| ... | @@ -47,7 +63,7 @@ afterEach(() => { | ... | @@ -47,7 +63,7 @@ afterEach(() => { |
| 47 | describe("resolveServerEnvironmentLabel", () => { | 63 | describe("resolveServerEnvironmentLabel", () => { |
| 48 | it.effect("uses hostname fallback regardless of launch mode", () => | 64 | it.effect("uses hostname fallback regardless of launch mode", () => |
| 49 | Effect.gen(function* () { | 65 | Effect.gen(function* () { |
| 50 | const result = yield* resolveServerEnvironmentLabel({ | 66 | const result = yield* ServerEnvironmentLabel.resolveServerEnvironmentLabel({ |
| 51 | cwdBaseName: "t3code", | 67 | cwdBaseName: "t3code", |
| 52 | }).pipe(Effect.provide(withHostPlatform(TestLayer, "win32", "macbook-pro"))); | 68 | }).pipe(Effect.provide(withHostPlatform(TestLayer, "win32", "macbook-pro"))); |
| 53 | | 69 | |
| ... | @@ -68,7 +84,7 @@ describe("resolveServerEnvironmentLabel", () => { | ... | @@ -68,7 +84,7 @@ describe("resolveServerEnvironmentLabel", () => { |
| 68 | }), | 84 | }), |
| 69 | ); | 85 | ); |
| 70 | | 86 | |
| 71 | const result = yield* resolveServerEnvironmentLabel({ | 87 | const result = yield* ServerEnvironmentLabel.resolveServerEnvironmentLabel({ |
| 72 | cwdBaseName: "t3code", | 88 | cwdBaseName: "t3code", |
| 73 | }).pipe(Effect.provide(withHostPlatform(TestLayer, "darwin", "macbook-pro"))); | 89 | }).pipe(Effect.provide(withHostPlatform(TestLayer, "darwin", "macbook-pro"))); |
| 74 | | 90 | |
| ... | @@ -85,7 +101,7 @@ describe("resolveServerEnvironmentLabel", () => { | ... | @@ -85,7 +101,7 @@ describe("resolveServerEnvironmentLabel", () => { |
| 85 | | 101 | |
| 86 | it.effect("prefers Linux PRETTY_HOSTNAME from machine-info", () => | 102 | it.effect("prefers Linux PRETTY_HOSTNAME from machine-info", () => |
| 87 | Effect.gen(function* () { | 103 | Effect.gen(function* () { |
| 88 | const result = yield* resolveServerEnvironmentLabel({ | 104 | const result = yield* ServerEnvironmentLabel.resolveServerEnvironmentLabel({ |
| 89 | cwdBaseName: "t3code", | 105 | cwdBaseName: "t3code", |
| 90 | }).pipe(Effect.provide(withHostPlatform(LinuxMachineInfoLayer, "linux", "buildbox"))); | 106 | }).pipe(Effect.provide(withHostPlatform(LinuxMachineInfoLayer, "linux", "buildbox"))); |
| 91 | | 107 | |
| ... | @@ -107,7 +123,7 @@ describe("resolveServerEnvironmentLabel", () => { | ... | @@ -107,7 +123,7 @@ describe("resolveServerEnvironmentLabel", () => { |
| 107 | }), | 123 | }), |
| 108 | ); | 124 | ); |
| 109 | | 125 | |
| 110 | const result = yield* resolveServerEnvironmentLabel({ | 126 | const result = yield* ServerEnvironmentLabel.resolveServerEnvironmentLabel({ |
| 111 | cwdBaseName: "t3code", | 127 | cwdBaseName: "t3code", |
| 112 | }).pipe(Effect.provide(withHostPlatform(TestLayer, "linux", "runner-01"))); | 128 | }).pipe(Effect.provide(withHostPlatform(TestLayer, "linux", "runner-01"))); |
| 113 | | 129 | |
| ... | @@ -124,7 +140,7 @@ describe("resolveServerEnvironmentLabel", () => { | ... | @@ -124,7 +140,7 @@ describe("resolveServerEnvironmentLabel", () => { |
| 124 | | 140 | |
| 125 | it.effect("falls back to the hostname when friendly labels are unavailable", () => | 141 | it.effect("falls back to the hostname when friendly labels are unavailable", () => |
| 126 | Effect.gen(function* () { | 142 | Effect.gen(function* () { |
| 127 | const result = yield* resolveServerEnvironmentLabel({ | 143 | const result = yield* ServerEnvironmentLabel.resolveServerEnvironmentLabel({ |
| 128 | cwdBaseName: "t3code", | 144 | cwdBaseName: "t3code", |
| 129 | }).pipe(Effect.provide(withHostPlatform(TestLayer, "win32", "JULIUS-LAPTOP"))); | 145 | }).pipe(Effect.provide(withHostPlatform(TestLayer, "win32", "JULIUS-LAPTOP"))); |
| 130 | | 146 | |
| ... | @@ -132,25 +148,111 @@ describe("resolveServerEnvironmentLabel", () => { | ... | @@ -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", () => | 151 | it.effect("falls back to the hostname when the friendly-label command is missing", () => { |
| 136 | Effect.gen(function* () { | 152 | const logs: CapturedLog[] = []; |
| 137 | runMock.mockReturnValueOnce( | 153 | const logger = Logger.make(({ fiber, message }) => { |
| 138 | Effect.fail( | 154 | logs.push({ |
| 139 | new ProcessRunner.ProcessSpawnError({ | 155 | message, |
| 140 | command: "scutil", | 156 | annotations: fiber.getRef(References.CurrentLogAnnotations), |
| 141 | argumentCount: 2, | 157 | }); |
| 142 | cause: new Error("spawn scutil ENOENT"), | 158 | }); |
| 143 | }), | 159 | const spawnCause = new Error("spawn scutil ENOENT"); |
| 144 | ), | 160 | const processError = new ProcessRunner.ProcessSpawnError({ |
| 145 | ); | 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 | cwdBaseName: "t3code", | 169 | cwdBaseName: "t3code", |
| 149 | }).pipe(Effect.provide(withHostPlatform(TestLayer, "darwin", "macbook-pro"))); | 170 | }); |
| 150 | | 171 | |
| 151 | expect(result).toBe("macbook-pro"); | 172 | expect(result).toBe("macbook-pro"); |
| 152 | }), | 173 | expect(logs[0]?.message).toEqual([ |
| 153 | ); | 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 | it.effect("falls back to the cwd basename when the hostname is blank", () => | 257 | it.effect("falls back to the cwd basename when the hostname is blank", () => |
| 156 | Effect.gen(function* () { | 258 | Effect.gen(function* () { |
| ... | @@ -165,7 +267,7 @@ describe("resolveServerEnvironmentLabel", () => { | ... | @@ -165,7 +267,7 @@ describe("resolveServerEnvironmentLabel", () => { |
| 165 | }), | 267 | }), |
| 166 | ); | 268 | ); |
| 167 | | 269 | |
| 168 | const result = yield* resolveServerEnvironmentLabel({ | 270 | const result = yield* ServerEnvironmentLabel.resolveServerEnvironmentLabel({ |
| 169 | cwdBaseName: "t3code", | 271 | cwdBaseName: "t3code", |
| 170 | }).pipe(Effect.provide(withHostPlatform(TestLayer, "linux", " "))); | 272 | }).pipe(Effect.provide(withHostPlatform(TestLayer, "linux", " "))); |
| 171 | | 273 | |