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";
22import * as Effect from "effect/Effect";
33import * as FileSystem from "effect/FileSystem";
44import * 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";
59import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";
610import { HostProcessHostname, HostProcessPlatform } from "@t3tools/shared/hostProcess";
711import { vi } from "vite-plus/test";
812
913import * 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
1228const runMock = vi.fn<ProcessRunner.ProcessRunner["Service"]["run"]>();
1329
......@@ -47,7 +63,7 @@ afterEach(() => {
4763describe("resolveServerEnvironmentLabel", () => {
4864 it.effect("uses hostname fallback regardless of launch mode", () =>
4965 Effect.gen(function* () {
50 const result = yield* resolveServerEnvironmentLabel({
66 const result = yield* ServerEnvironmentLabel.resolveServerEnvironmentLabel({
5167 cwdBaseName: "t3code",
5268 }).pipe(Effect.provide(withHostPlatform(TestLayer, "win32", "macbook-pro")));
5369
......@@ -68,7 +84,7 @@ describe("resolveServerEnvironmentLabel", () => {
6884 }),
6985 );
7086
71 const result = yield* resolveServerEnvironmentLabel({
87 const result = yield* ServerEnvironmentLabel.resolveServerEnvironmentLabel({
7288 cwdBaseName: "t3code",
7389 }).pipe(Effect.provide(withHostPlatform(TestLayer, "darwin", "macbook-pro")));
7490
......@@ -85,7 +101,7 @@ describe("resolveServerEnvironmentLabel", () => {
85101
86102 it.effect("prefers Linux PRETTY_HOSTNAME from machine-info", () =>
87103 Effect.gen(function* () {
88 const result = yield* resolveServerEnvironmentLabel({
104 const result = yield* ServerEnvironmentLabel.resolveServerEnvironmentLabel({
89105 cwdBaseName: "t3code",
90106 }).pipe(Effect.provide(withHostPlatform(LinuxMachineInfoLayer, "linux", "buildbox")));
91107
......@@ -107,7 +123,7 @@ describe("resolveServerEnvironmentLabel", () => {
107123 }),
108124 );
109125
110 const result = yield* resolveServerEnvironmentLabel({
126 const result = yield* ServerEnvironmentLabel.resolveServerEnvironmentLabel({
111127 cwdBaseName: "t3code",
112128 }).pipe(Effect.provide(withHostPlatform(TestLayer, "linux", "runner-01")));
113129
......@@ -124,7 +140,7 @@ describe("resolveServerEnvironmentLabel", () => {
124140
125141 it.effect("falls back to the hostname when friendly labels are unavailable", () =>
126142 Effect.gen(function* () {
127 const result = yield* resolveServerEnvironmentLabel({
143 const result = yield* ServerEnvironmentLabel.resolveServerEnvironmentLabel({
128144 cwdBaseName: "t3code",
129145 }).pipe(Effect.provide(withHostPlatform(TestLayer, "win32", "JULIUS-LAPTOP")));
130146
......@@ -132,25 +148,111 @@ describe("resolveServerEnvironmentLabel", () => {
132148 }),
133149 );
134150
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));
146166
147 const result = yield* resolveServerEnvironmentLabel({
167 return Effect.gen(function* () {
168 const result = yield* ServerEnvironmentLabel.resolveServerEnvironmentLabel({
148169 cwdBaseName: "t3code",
149 }).pipe(Effect.provide(withHostPlatform(TestLayer, "darwin", "macbook-pro")));
170 });
150171
151172 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 });
154256
155257 it.effect("falls back to the cwd basename when the hostname is blank", () =>
156258 Effect.gen(function* () {
......@@ -165,7 +267,7 @@ describe("resolveServerEnvironmentLabel", () => {
165267 }),
166268 );
167269
168 const result = yield* resolveServerEnvironmentLabel({
270 const result = yield* ServerEnvironmentLabel.resolveServerEnvironmentLabel({
169271 cwdBaseName: "t3code",
170272 }).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
22import * as Effect from "effect/Effect";
33import * as FileSystem from "effect/FileSystem";
44import * as Option from "effect/Option";
5import * as Schema from "effect/Schema";
56
67import * as ProcessRunner from "../processRunner.ts";
78
......@@ -9,6 +10,39 @@ interface ResolveServerEnvironmentLabelInput {
910 readonly cwdBaseName: string;
1011}
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
1246function normalizeLabel(value: string | null | undefined): string | null {
1347 const trimmed = value?.trim();
1448 return trimmed && trimmed.length > 0 ? trimmed : null;
......@@ -34,30 +68,80 @@ function parseMachineInfoValue(raw: string, key: string): string | null {
3468
3569const readLinuxMachineInfo = Effect.fn("readLinuxMachineInfo")(function* () {
3670 const fileSystem = yield* FileSystem.FileSystem;
37 const exists = yield* fileSystem
38 .exists("/etc/machine-info")
39 .pipe(Effect.orElseSucceed(() => false));
40 if (!exists) {
41 return null;
42 }
43
44 return yield* fileSystem
45 .readFileString("/etc/machine-info")
46 .pipe(Effect.orElseSucceed(() => null));
71 const machineInfoPath = "/etc/machine-info";
72 return yield* fileSystem.exists(machineInfoPath).pipe(
73 Effect.mapError(
74 (cause) =>
75 new ServerEnvironmentLabelFileError({
76 operation: "inspect",
77 path: machineInfoPath,
78 cause,
79 }),
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 );
47107});
48108
49const runFriendlyLabelCommand = Effect.fn("runFriendlyLabelCommand")(function* (
50 command: string,
51 args: readonly string[],
52) {
109const runFriendlyLabelCommand = Effect.fn("runFriendlyLabelCommand")(function* (input: {
110 readonly probe: ServerEnvironmentLabelCommandProbe;
111 readonly command: string;
112 readonly args: readonly string[];
113}) {
53114 const processRunner = yield* ProcessRunner.ProcessRunner;
54115 const result = yield* processRunner
55116 .run({
56 command,
57 args,
117 command: input.command,
118 args: input.args,
58119 timeoutBehavior: "timedOutResult",
59120 })
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
62146 if (Option.isNone(result) || result.value.code !== 0) {
63147 return null;
......@@ -69,7 +153,11 @@ const runFriendlyLabelCommand = Effect.fn("runFriendlyLabelCommand")(function* (
69153const resolveFriendlyHostLabel = Effect.fn("resolveFriendlyHostLabel")(function* () {
70154 const platform = yield* HostProcessPlatform;
71155 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 });
73161 }
74162
75163 if (platform === "linux") {
......@@ -81,7 +169,11 @@ const resolveFriendlyHostLabel = Effect.fn("resolveFriendlyHostLabel")(function*
81169 }
82170 }
83171
84 return yield* runFriendlyLabelCommand("hostnamectl", ["--pretty"]);
172 return yield* runFriendlyLabelCommand({
173 probe: "linux-pretty-hostname",
174 command: "hostnamectl",
175 args: ["--pretty"],
176 });
85177 }
86178
87179 return null;