Skip to content
Source
Sign inSign up
Open History to see the last commit

os-jank.test.ts

206 lines6.9 KB

1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { it as effectIt } from "@effect/vitest";
3import * as HostProcess from "@t3tools/shared/HostProcess";
4import { CommandAvailability, WindowsShellEnvironment } from "@t3tools/shared/shell";
5import * as Effect from "effect/Effect";
6// @effect-diagnostics-next-line nodeBuiltinImport:off -- Observe the existing synchronous shell probe without launching the user's login shell.
7import * as NodeChildProcess from "node:child_process";
8import * as NodeOS from "node:os";
9import { assert, it, vi } from "vite-plus/test";
10​
11import { fixPath, hydratePosixHome } from "./os-jank.ts";
12​
13vi.mock("node:child_process", { spy: true });
14​
15it("hydrates HOME independently of a blank process HOME", () => {
16 const originalHome = process.env.HOME;
17 const env: NodeJS.ProcessEnv = { HOME: " " };
18​
19 try {
20 process.env.HOME = " ";
21 hydratePosixHome(env);
22 } finally {
23 if (originalHome === undefined) {
24 delete process.env.HOME;
25 } else {
26 process.env.HOME = originalHome;
27 }
28 }
29​
30 assert.equal(env.HOME, NodeOS.userInfo().homedir);
31});
32​
33it("preserves an explicitly configured HOME", () => {
34 const env: NodeJS.ProcessEnv = { HOME: "/custom/home" };
35​
36 hydratePosixHome(env, () => {
37 throw new Error("HOME lookup should not run");
38 });
39​
40 assert.equal(env.HOME, "/custom/home");
41});
42​
43effectIt.effect("hydrates PATH when the desktop handoff is absent", () =>
44 Effect.acquireUseRelease(
45 Effect.sync(() =>
46 vi
47 .spyOn(NodeChildProcess, "execFileSync")
48 .mockReturnValue("__T3CODE_ENV_PATH_START__\n/opt/tools/bin\n__T3CODE_ENV_PATH_END__\n"),
49 ),
50 (probe) =>
51 Effect.gen(function* () {
52 const env: NodeJS.ProcessEnv = { SHELL: "/bin/bash", PATH: "/usr/bin" };
53​
54 yield* fixPath().pipe(
55 Effect.provideService(HostProcess.Platform, "linux"),
56 Effect.provideService(HostProcess.Environment, env),
57 Effect.provide(NodeServices.layer),
58 );
59​
60 assert.equal(probe.mock.calls.length, 1);
61 assert.equal(env.PATH, "/opt/tools/bin:/usr/bin");
62 }),
63 (probe) => Effect.sync(() => probe.mockRestore()),
64 ),
65);
66​
67const timedOut = (file: string) =>
68 Object.assign(new Error(`spawnSync ${file} ETIMEDOUT`), { code: "ETIMEDOUT" });
69​
70effectIt.effect("waits longer for a login shell that times out at startup", () =>
71 Effect.acquireUseRelease(
72 Effect.sync(() =>
73 vi.spyOn(NodeChildProcess, "execFileSync").mockImplementation((file, _args, options) => {
74 // This profile takes 8 seconds while the machine is under load.
75 if ((options?.timeout ?? 0) < 8_000) throw timedOut(String(file));
76 return "__T3CODE_ENV_PATH_START__\n/opt/tools/bin\n__T3CODE_ENV_PATH_END__\n";
77 }),
78 ),
79 () =>
80 Effect.gen(function* () {
81 const env: NodeJS.ProcessEnv = { SHELL: "/bin/zsh", PATH: "/usr/bin" };
82​
83 yield* fixPath().pipe(
84 Effect.provideService(HostProcess.Platform, "linux"),
85 Effect.provideService(HostProcess.Environment, env),
86 Effect.provide(NodeServices.layer),
87 );
88​
89 assert.equal(env.PATH, "/opt/tools/bin:/usr/bin");
90 }),
91 (probe) => Effect.sync(() => probe.mockRestore()),
92 ),
93);
94​
95effectIt.effect("waits longer for only one hung login shell", () =>
96 Effect.acquireUseRelease(
97 Effect.sync(() =>
98 vi.spyOn(NodeChildProcess, "execFileSync").mockImplementation((file) => {
99 throw timedOut(String(file));
100 }),
101 ),
102 (probe) =>
103 Effect.gen(function* () {
104 const env: NodeJS.ProcessEnv = { SHELL: "/bin/zsh", PATH: "/usr/bin" };
105​
106 yield* fixPath().pipe(
107 Effect.provideService(HostProcess.Platform, "linux"),
108 Effect.provideService(HostProcess.Environment, env),
109 Effect.provide(NodeServices.layer),
110 );
111​
112 const timeouts = probe.mock.calls.map(([, , options]) => options?.timeout);
113 assert.equal(timeouts.filter((timeout) => (timeout ?? 0) > 5_000).length, 1);
114 assert.equal(env.PATH, "/usr/bin");
115 }),
116 (probe) => Effect.sync(() => probe.mockRestore()),
117 ),
118);
119​
120effectIt.effect("keeps mise's PATH record with the login shell PATH", () =>
121 Effect.acquireUseRelease(
122 Effect.sync(() =>
123 vi
124 .spyOn(NodeChildProcess, "execFileSync")
125 .mockReturnValue(
126 [
127 "__T3CODE_ENV_PATH_START__",
128 "/home/me/.local/bin:/mise/installs/claude/latest:/mise/shims",
129 "__T3CODE_ENV_PATH_END__",
130 "__T3CODE_ENV___MISE_DIFF_START__",
131 "mise-diff",
132 "__T3CODE_ENV___MISE_DIFF_END__",
133 ].join("\n"),
134 ),
135 ),
136 () =>
137 Effect.gen(function* () {
138 const env: NodeJS.ProcessEnv = { SHELL: "/bin/bash", PATH: "/usr/bin" };
139​
140 yield* fixPath().pipe(
141 Effect.provideService(HostProcess.Platform, "linux"),
142 Effect.provideService(HostProcess.Environment, env),
143 Effect.provide(NodeServices.layer),
144 );
145​
146 assert.equal(env.__MISE_DIFF, "mise-diff");
147 }),
148 (probe) => Effect.sync(() => probe.mockRestore()),
149 ),
150);
151​
152effectIt.effect("hydrates a missing JAVA_HOME from the login shell and keeps an explicit one", () =>
153 Effect.acquireUseRelease(
154 Effect.sync(() =>
155 vi
156 .spyOn(NodeChildProcess, "execFileSync")
157 .mockReturnValue(
158 [
159 "__T3CODE_ENV_PATH_START__",
160 "/opt/homebrew/bin",
161 "__T3CODE_ENV_PATH_END__",
162 "__T3CODE_ENV_JAVA_HOME_START__",
163 "/opt/homebrew/opt/openjdk@17",
164 "__T3CODE_ENV_JAVA_HOME_END__",
165 ].join("\n"),
166 ),
167 ),
168 () =>
169 Effect.gen(function* () {
170 const missing: NodeJS.ProcessEnv = { SHELL: "/bin/zsh", PATH: "/usr/bin" };
171 const explicit: NodeJS.ProcessEnv = {
172 SHELL: "/bin/zsh",
173 PATH: "/usr/bin",
174 JAVA_HOME: "/custom/jdk",
175 };
176​
177 for (const env of [missing, explicit]) {
178 yield* fixPath().pipe(
179 Effect.provideService(HostProcess.Platform, "darwin"),
180 Effect.provideService(HostProcess.Environment, env),
181 Effect.provide(NodeServices.layer),
182 );
183 }
184​
185 assert.equal(missing.JAVA_HOME, "/opt/homebrew/opt/openjdk@17");
186 assert.equal(explicit.JAVA_HOME, "/custom/jdk");
187 }),
188 (probe) => Effect.sync(() => probe.mockRestore()),
189 ),
190);
191​
192effectIt.effect("replaces a Path-spelled Windows PATH instead of adding a second key", () =>
193 Effect.gen(function* () {
194 const env: NodeJS.ProcessEnv = { Path: "C:\\Windows" };
195​
196 yield* fixPath().pipe(
197 Effect.provideService(HostProcess.Platform, "win32"),
198 Effect.provideService(HostProcess.Environment, env),
199 Effect.provideService(WindowsShellEnvironment, () => Effect.succeed({ PATH: "C:\\Tools" })),
200 Effect.provideService(CommandAvailability, () => Effect.succeed(true)),
201 Effect.provide(NodeServices.layer),
202 );
203​
204 assert.deepEqual(env, { PATH: "C:\\Tools;C:\\Windows" });
205 }),
206);