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 for minimal service environments from the user account", () => {
16 const env: NodeJS.ProcessEnv = {};
17
18 hydratePosixHome(env);
19
20 assert.equal(env.HOME, NodeOS.userInfo().homedir);
21});
22
23it("hydrates HOME independently of a blank process HOME", () => {
24 const originalHome = process.env.HOME;
25 const env: NodeJS.ProcessEnv = { HOME: " " };
26
27 try {
28 process.env.HOME = " ";
29 hydratePosixHome(env);
30 } finally {
31 if (originalHome === undefined) {
32 delete process.env.HOME;
33 } else {
34 process.env.HOME = originalHome;
35 }
36 }
37
38 assert.equal(env.HOME, NodeOS.userInfo().homedir);
39});
40
41it("preserves an explicitly configured HOME", () => {
42 const env: NodeJS.ProcessEnv = { HOME: "/custom/home" };
43
44 hydratePosixHome(env, () => {
45 throw new Error("HOME lookup should not run");
46 });
47
48 assert.equal(env.HOME, "/custom/home");
49});
50
51effectIt.effect.each(["darwin", "linux"] as const)(
52 "keeps the prepared PATH and still hydrates HOME on %s",
53 (platform) =>
54 Effect.acquireUseRelease(
55 Effect.sync(() => vi.spyOn(NodeChildProcess, "execFileSync")),
56 (probe) =>
57 Effect.gen(function* () {
58 probe.mockImplementation(() => {
59 throw new Error("The prepared environment must not launch another shell");
60 });
61 const env: NodeJS.ProcessEnv = { PATH: "/prepared/bin:/usr/bin" };
62
63 yield* fixPath({ shellEnvironmentPrepared: true }).pipe(
64 Effect.provideService(HostProcess.Platform, platform),
65 Effect.provideService(HostProcess.Environment, env),
66 Effect.provide(NodeServices.layer),
67 );
68
69 assert.equal(probe.mock.calls.length, 0);
70 assert.equal(env.PATH, "/prepared/bin:/usr/bin");
71 assert.equal(env.HOME, NodeOS.userInfo().homedir);
72 }),
73 (probe) => Effect.sync(() => probe.mockRestore()),
74 ),
75);
76
77effectIt.effect("hydrates PATH when the desktop handoff is absent", () =>
78 Effect.acquireUseRelease(
79 Effect.sync(() =>
80 vi
81 .spyOn(NodeChildProcess, "execFileSync")
82 .mockReturnValue("__T3CODE_ENV_PATH_START__\n/opt/tools/bin\n__T3CODE_ENV_PATH_END__\n"),
83 ),
84 (probe) =>
85 Effect.gen(function* () {
86 const env: NodeJS.ProcessEnv = { SHELL: "/bin/bash", PATH: "/usr/bin" };
87
88 yield* fixPath().pipe(
89 Effect.provideService(HostProcess.Platform, "linux"),
90 Effect.provideService(HostProcess.Environment, env),
91 Effect.provide(NodeServices.layer),
92 );
93
94 assert.equal(probe.mock.calls.length, 1);
95 assert.equal(env.PATH, "/opt/tools/bin:/usr/bin");
96 }),
97 (probe) => Effect.sync(() => probe.mockRestore()),
98 ),
99);
100
101effectIt.effect("keeps mise's PATH record with the login shell PATH", () =>
102 Effect.acquireUseRelease(
103 Effect.sync(() =>
104 vi
105 .spyOn(NodeChildProcess, "execFileSync")
106 .mockReturnValue(
107 [
108 "__T3CODE_ENV_PATH_START__",
109 "/home/me/.local/bin:/mise/installs/claude/latest:/mise/shims",
110 "__T3CODE_ENV_PATH_END__",
111 "__T3CODE_ENV___MISE_DIFF_START__",
112 "mise-diff",
113 "__T3CODE_ENV___MISE_DIFF_END__",
114 ].join("\n"),
115 ),
116 ),
117 () =>
118 Effect.gen(function* () {
119 const env: NodeJS.ProcessEnv = { SHELL: "/bin/bash", PATH: "/usr/bin" };
120
121 yield* fixPath().pipe(
122 Effect.provideService(HostProcess.Platform, "linux"),
123 Effect.provideService(HostProcess.Environment, env),
124 Effect.provide(NodeServices.layer),
125 );
126
127 assert.equal(env.__MISE_DIFF, "mise-diff");
128 }),
129 (probe) => Effect.sync(() => probe.mockRestore()),
130 ),
131);
132
133effectIt.effect("repairs Windows PATH even when the desktop marks it prepared", () =>
134 Effect.gen(function* () {
135 const env: NodeJS.ProcessEnv = { PATH: "C:\\Windows" };
136 const commands: string[] = [];
137
138 yield* fixPath({ shellEnvironmentPrepared: true }).pipe(
139 Effect.provideService(HostProcess.Platform, "win32"),
140 Effect.provideService(HostProcess.Environment, env),
141 Effect.provideService(WindowsShellEnvironment, () => ({ PATH: "C:\\Tools" })),
142 Effect.provideService(CommandAvailability, (command) =>
143 Effect.sync(() => {
144 commands.push(command);
145 return true;
146 }),
147 ),
148 Effect.provide(NodeServices.layer),
149 );
150
151 assert.deepEqual(commands, ["node"]);
152 assert.equal(env.PATH, "C:\\Tools;C:\\Windows");
153 }),
154);