authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 15:13:53-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 15:13:53-07:00
loga51691a9b0a98e65f0e9b2b775b22a2382595e87
tree7bdcdc3783ab25a1a60c20203b4b9161401a9519
parent97de7d7e1535ddf485ea8806c639f27e99cfbcb2
signature Signed by PGP key B5690EEEBB952194

[codex] Structure server environment-label probe failures (#3321)

Co-authored-by: codex <codex@users.noreply.github.com>

2 files changed, 235 insertions(+), 41 deletions(-)

apps/server/src/environment/ServerEnvironmentLabel.test.ts+124-22
...@@ -2,12 +2,28 @@ import { afterEach, describe, expect, it } from "@effect/vitest";...@@ -2,12 +2,28 @@ import { afterEach, describe, expect, it } from "@effect/vitest";
2import * as Effect from "effect/Effect";2import * as Effect from "effect/Effect";
3import * as FileSystem from "effect/FileSystem";3import * as FileSystem from "effect/FileSystem";
4import * as Layer from "effect/Layer";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";
5import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";9import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";
6import { HostProcessHostname, HostProcessPlatform } from "@t3tools/shared/hostProcess";10import { HostProcessHostname, HostProcessPlatform } from "@t3tools/shared/hostProcess";
7import { vi } from "vite-plus/test";11import { vi } from "vite-plus/test";
812
9import * as ProcessRunner from "../processRunner.ts";13import * as ProcessRunner from "../processRunner.ts";
10import { resolveServerEnvironmentLabel } from "./ServerEnvironmentLabel.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}
1127
12const runMock = vi.fn<ProcessRunner.ProcessRunner["Service"]["run"]>();28const runMock = vi.fn<ProcessRunner.ProcessRunner["Service"]["run"]>();
1329
...@@ -47,7 +63,7 @@ afterEach(() => {...@@ -47,7 +63,7 @@ afterEach(() => {
47describe("resolveServerEnvironmentLabel", () => {63describe("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")));
5369
...@@ -68,7 +84,7 @@ describe("resolveServerEnvironmentLabel", () => {...@@ -68,7 +84,7 @@ describe("resolveServerEnvironmentLabel", () => {
68 }),84 }),
69 );85 );
7086
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")));
7490
...@@ -85,7 +101,7 @@ describe("resolveServerEnvironmentLabel", () => {...@@ -85,7 +101,7 @@ describe("resolveServerEnvironmentLabel", () => {
85101
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")));
91107
...@@ -107,7 +123,7 @@ describe("resolveServerEnvironmentLabel", () => {...@@ -107,7 +123,7 @@ describe("resolveServerEnvironmentLabel", () => {
107 }),123 }),
108 );124 );
109125
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")));
113129
...@@ -124,7 +140,7 @@ describe("resolveServerEnvironmentLabel", () => {...@@ -124,7 +140,7 @@ describe("resolveServerEnvironmentLabel", () => {
124140
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")));
130146
...@@ -132,25 +148,111 @@ describe("resolveServerEnvironmentLabel", () => {...@@ -132,25 +148,111 @@ describe("resolveServerEnvironmentLabel", () => {
132 }),148 }),
133 );149 );
134150
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));
146166
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 });
150171
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 });
154256
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 );
167269
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", " ")));
171273
apps/server/src/environment/ServerEnvironmentLabel.ts+111-19
...@@ -2,6 +2,7 @@ import { HostProcessHostname, HostProcessPlatform } from "@t3tools/shared/hostPr...@@ -2,6 +2,7 @@ import { HostProcessHostname, HostProcessPlatform } from "@t3tools/shared/hostPr
2import * as Effect from "effect/Effect";2import * as Effect from "effect/Effect";
3import * as FileSystem from "effect/FileSystem";3import * as FileSystem from "effect/FileSystem";
4import * as Option from "effect/Option";4import * as Option from "effect/Option";
5import * as Schema from "effect/Schema";
56
6import * as ProcessRunner from "../processRunner.ts";7import * as ProcessRunner from "../processRunner.ts";
78
...@@ -9,6 +10,39 @@ interface ResolveServerEnvironmentLabelInput {...@@ -9,6 +10,39 @@ interface ResolveServerEnvironmentLabelInput {
9 readonly cwdBaseName: string;10 readonly cwdBaseName: string;
10}11}
1112
13const ServerEnvironmentLabelCommandProbe = Schema.Literals([
14 "macos-computer-name",
15 "linux-pretty-hostname",
16]);
17type ServerEnvironmentLabelCommandProbe = typeof ServerEnvironmentLabelCommandProbe.Type;
18
19export class ServerEnvironmentLabelFileError extends Schema.TaggedErrorClass<ServerEnvironmentLabelFileError>()(
20 "ServerEnvironmentLabelFileError",
21 {
22 operation: Schema.Literals(["inspect", "read"]),
23 path: Schema.String,
24 cause: Schema.Defect(),
25 },
26) {
27 override get message(): string {
28 return `Failed to ${this.operation} environment-label file at ${this.path}.`;
29 }
30}
31
32export class ServerEnvironmentLabelCommandError extends Schema.TaggedErrorClass<ServerEnvironmentLabelCommandError>()(
33 "ServerEnvironmentLabelCommandError",
34 {
35 probe: ServerEnvironmentLabelCommandProbe,
36 executable: Schema.String,
37 argumentCount: Schema.Number,
38 cause: Schema.Defect(),
39 },
40) {
41 override get message(): string {
42 return `Failed to run environment-label probe '${this.probe}' with ${this.executable}.`;
43 }
44}
45
12function normalizeLabel(value: string | null | undefined): string | null {46function normalizeLabel(value: string | null | undefined): string | null {
13 const trimmed = value?.trim();47 const trimmed = value?.trim();
14 return trimmed && trimmed.length > 0 ? trimmed : null;48 return trimmed && trimmed.length > 0 ? trimmed : null;
...@@ -34,30 +68,80 @@ function parseMachineInfoValue(raw: string, key: string): string | null {...@@ -34,30 +68,80 @@ function parseMachineInfoValue(raw: string, key: string): string | null {
3468
35const readLinuxMachineInfo = Effect.fn("readLinuxMachineInfo")(function* () {69const readLinuxMachineInfo = Effect.fn("readLinuxMachineInfo")(function* () {
36 const fileSystem = yield* FileSystem.FileSystem;70 const fileSystem = yield* FileSystem.FileSystem;
37 const exists = yield* fileSystem71 const machineInfoPath = "/etc/machine-info";
38 .exists("/etc/machine-info")72 return yield* fileSystem.exists(machineInfoPath).pipe(
39 .pipe(Effect.orElseSucceed(() => false));73 Effect.mapError(
40 if (!exists) {74 (cause) =>
41 return null;75 new ServerEnvironmentLabelFileError({
42 }76 operation: "inspect",
4377 path: machineInfoPath,
44 return yield* fileSystem78 cause,
45 .readFileString("/etc/machine-info")79 }),
46 .pipe(Effect.orElseSucceed(() => null));80 ),
81 Effect.flatMap((exists) =>
82 exists
83 ? fileSystem.readFileString(machineInfoPath).pipe(
84 Effect.mapError(
85 (cause) =>
86 new ServerEnvironmentLabelFileError({
87 operation: "read",
88 path: machineInfoPath,
89 cause,
90 }),
91 ),
92 )
93 : Effect.succeed(null),
94 ),
95 Effect.catchTags({
96 ServerEnvironmentLabelFileError: (error) =>
97 Effect.logDebug(error.message).pipe(
98 Effect.annotateLogs({
99 operation: error.operation,
100 path: error.path,
101 cause: error,
102 }),
103 Effect.as(null),
104 ),
105 }),
106 );
47});107});
48108
49const runFriendlyLabelCommand = Effect.fn("runFriendlyLabelCommand")(function* (109const runFriendlyLabelCommand = Effect.fn("runFriendlyLabelCommand")(function* (input: {
50 command: string,110 readonly probe: ServerEnvironmentLabelCommandProbe;
51 args: readonly string[],111 readonly command: string;
52) {112 readonly args: readonly string[];
113}) {
53 const processRunner = yield* ProcessRunner.ProcessRunner;114 const processRunner = yield* ProcessRunner.ProcessRunner;
54 const result = yield* processRunner115 const result = yield* processRunner
55 .run({116 .run({
56 command,117 command: input.command,
57 args,118 args: input.args,
58 timeoutBehavior: "timedOutResult",119 timeoutBehavior: "timedOutResult",
59 })120 })
60 .pipe(Effect.option);121 .pipe(
122 Effect.mapError(
123 (cause) =>
124 new ServerEnvironmentLabelCommandError({
125 probe: input.probe,
126 executable: input.command,
127 argumentCount: input.args.length,
128 cause,
129 }),
130 ),
131 Effect.map(Option.some),
132 Effect.catchTags({
133 ServerEnvironmentLabelCommandError: (error) =>
134 Effect.logDebug(error.message).pipe(
135 Effect.annotateLogs({
136 probe: error.probe,
137 executable: error.executable,
138 argumentCount: error.argumentCount,
139 cause: error,
140 }),
141 Effect.as(Option.none()),
142 ),
143 }),
144 );
61145
62 if (Option.isNone(result) || result.value.code !== 0) {146 if (Option.isNone(result) || result.value.code !== 0) {
63 return null;147 return null;
...@@ -69,7 +153,11 @@ const runFriendlyLabelCommand = Effect.fn("runFriendlyLabelCommand")(function* (...@@ -69,7 +153,11 @@ const runFriendlyLabelCommand = Effect.fn("runFriendlyLabelCommand")(function* (
69const resolveFriendlyHostLabel = Effect.fn("resolveFriendlyHostLabel")(function* () {153const resolveFriendlyHostLabel = Effect.fn("resolveFriendlyHostLabel")(function* () {
70 const platform = yield* HostProcessPlatform;154 const platform = yield* HostProcessPlatform;
71 if (platform === "darwin") {155 if (platform === "darwin") {
72 return yield* runFriendlyLabelCommand("scutil", ["--get", "ComputerName"]);156 return yield* runFriendlyLabelCommand({
157 probe: "macos-computer-name",
158 command: "scutil",
159 args: ["--get", "ComputerName"],
160 });
73 }161 }
74162
75 if (platform === "linux") {163 if (platform === "linux") {
...@@ -81,7 +169,11 @@ const resolveFriendlyHostLabel = Effect.fn("resolveFriendlyHostLabel")(function*...@@ -81,7 +169,11 @@ const resolveFriendlyHostLabel = Effect.fn("resolveFriendlyHostLabel")(function*
81 }169 }
82 }170 }
83171
84 return yield* runFriendlyLabelCommand("hostnamectl", ["--pretty"]);172 return yield* runFriendlyLabelCommand({
173 probe: "linux-pretty-hostname",
174 command: "hostnamectl",
175 args: ["--pretty"],
176 });
85 }177 }
86178
87 return null;179 return null;