Skip to content
Source
Sign inSign up

nodeRuntime.test.ts

207 lines8.8 KB

1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { describe, expect, it } from "@effect/vitest";
3import * as Effect from "effect/Effect";
4import * as FileSystem from "effect/FileSystem";
5import * as Path from "effect/Path";
6import * as Result from "effect/Result";
7​
8import * as HostProcess from "./HostProcess.ts";
9import { resolveNodeExecutable, resolveSelfInvocation, selfInvocationArgs } from "./nodeRuntime.ts";
10import { symlinksSupported } from "./testing/symlinks.ts";
11​
12describe("Self invocation", () => {
13 it.effect("runs the entrypoint script with the current runtime", () =>
14 Effect.gen(function* () {
15 const path = yield* Path.Path;
16 const invocation = yield* resolveSelfInvocation().pipe(
17 Effect.provideService(HostProcess.ExecutablePath, "/runtime/node"),
18 Effect.provideService(HostProcess.IsExecutable, false),
19 Effect.provideService(HostProcess.Arguments, ["/runtime/node", "dist/bin.mjs", "serve"]),
20 );
21 expect(invocation.command).toBe("/runtime/node");
22 expect(invocation.entrypoint).toBe(path.resolve("dist/bin.mjs"));
23 expect(selfInvocationArgs(invocation, ["acp-mcp-bridge"])).toEqual([
24 path.resolve("dist/bin.mjs"),
25 "acp-mcp-bridge",
26 ]);
27 }).pipe(Effect.provide(NodeServices.layer)),
28 );
29​
30 it.effect("passes subcommands straight to the standalone executable", () =>
31 Effect.gen(function* () {
32 // Node repeats the binary at argv[1] for a single-executable; it is not a script.
33 const invocation = yield* resolveSelfInvocation().pipe(
34 Effect.provideService(HostProcess.ExecutablePath, "/packaged/t3"),
35 Effect.provideService(HostProcess.IsExecutable, true),
36 Effect.provideService(HostProcess.Arguments, ["/packaged/t3", "/packaged/t3", "serve"]),
37 );
38 expect(invocation).toEqual({ command: "/packaged/t3", entrypoint: undefined });
39 expect(selfInvocationArgs(invocation, ["acp-mcp-bridge"])).toEqual(["acp-mcp-bridge"]);
40 }).pipe(Effect.provide(NodeServices.layer)),
41 );
42});
43​
44describe("Node runtime selection", () => {
45 it.effect("keeps the current Node or Electron runtime without requiring Node on PATH", () =>
46 Effect.gen(function* () {
47 for (const executable of ["/runtime/node", "/Applications/T3 Code.app/Electron"]) {
48 expect(
49 yield* resolveNodeExecutable("Local device support", { PATH: "" }).pipe(
50 Effect.provideService(HostProcess.ExecutablePath, executable),
51 Effect.provideService(HostProcess.IsExecutable, false),
52 ),
53 ).toBe(executable);
54 }
55 }).pipe(Effect.provide(NodeServices.layer)),
56 );
57​
58 it.effect("uses installed Node instead of the standalone T3 executable", () =>
59 Effect.gen(function* () {
60 const path = yield* Path.Path;
61 expect(
62 yield* resolveNodeExecutable("Local device support", {
63 PATH: path.dirname(process.execPath),
64 }),
65 ).toBe(process.execPath);
66 }).pipe(
67 Effect.provideService(HostProcess.ExecutablePath, "/packaged/t3"),
68 Effect.provideService(HostProcess.IsExecutable, true),
69 Effect.provide(NodeServices.layer),
70 ),
71 );
72​
73 it.effect("finds a newly installed runtime immediately after a failed lookup", () =>
74 Effect.gen(function* () {
75 const fs = yield* FileSystem.FileSystem;
76 const path = yield* Path.Path;
77 const directory = yield* fs.makeTempDirectoryScoped();
78 const platform = yield* HostProcess.Platform;
79 const node = path.join(directory, platform === "win32" ? "node.exe" : "node");
80 const env = { PATH: directory };
81 expect(
82 Result.isFailure(
83 yield* resolveNodeExecutable("Local device support", env).pipe(Effect.result),
84 ),
85 ).toBe(true);
86 yield* fs.copyFile(process.execPath, node);
87 yield* fs.chmod(node, 0o755);
88 expect(yield* resolveNodeExecutable("Local device support", env)).toBe(node);
89 }).pipe(
90 Effect.scoped,
91 Effect.provideService(HostProcess.ExecutablePath, "/packaged/t3"),
92 Effect.provideService(HostProcess.IsExecutable, true),
93 Effect.provide(NodeServices.layer),
94 ),
95 );
96​
97 it.effect("uses node.exe even when Windows batch wrappers appear first on PATH", () =>
98 Effect.gen(function* () {
99 const fs = yield* FileSystem.FileSystem;
100 const path = yield* Path.Path;
101 const directory = yield* fs.makeTempDirectoryScoped();
102 const wrappers = path.join(directory, "wrappers");
103 const runtime = path.join(directory, "runtime");
104 yield* fs.makeDirectory(wrappers);
105 yield* fs.makeDirectory(runtime);
106 yield* fs.writeFileString(path.join(wrappers, "node.cmd"), "@echo off");
107 yield* fs.writeFileString(path.join(wrappers, "node.bat"), "@echo off");
108 const node = path.join(runtime, "node.exe");
109 yield* fs.copyFile(process.execPath, node);
110 expect(
111 yield* resolveNodeExecutable("Local device support", {
112 PATH: `${wrappers};${runtime}`,
113 PATHEXT: ".CMD;.BAT",
114 }),
115 ).toBe(node);
116 }).pipe(
117 Effect.scoped,
118 Effect.provideService(HostProcess.ExecutablePath, "/packaged/t3"),
119 Effect.provideService(HostProcess.IsExecutable, true),
120 Effect.provideService(HostProcess.Platform, "win32"),
121 Effect.provide(NodeServices.layer),
122 ),
123 );
124​
125 it.effect("reports install guidance when Windows only has batch runtime wrappers", () =>
126 Effect.gen(function* () {
127 const fs = yield* FileSystem.FileSystem;
128 const path = yield* Path.Path;
129 const directory = yield* fs.makeTempDirectoryScoped();
130 yield* fs.writeFileString(path.join(directory, "node.cmd"), "@echo off");
131 yield* fs.writeFileString(path.join(directory, "node.bat"), "@echo off");
132 const error = yield* resolveNodeExecutable("Local device support", {
133 PATH: directory,
134 PATHEXT: ".CMD;.BAT;.EXE",
135 }).pipe(Effect.flip);
136 expect(error._tag).toBe("NodeRuntimeUnavailableError");
137 expect(error.message).toContain("Install Node.js");
138 }).pipe(
139 Effect.scoped,
140 Effect.provideService(HostProcess.IsExecutable, true),
141 Effect.provideService(HostProcess.Platform, "win32"),
142 Effect.provide(NodeServices.layer),
143 ),
144 );
145​
146 it.effect("rejects a hard-linked node alias pointing back at the standalone app", () =>
147 Effect.gen(function* () {
148 const fs = yield* FileSystem.FileSystem;
149 const path = yield* Path.Path;
150 const directory = yield* fs.makeTempDirectoryScoped();
151 const platform = yield* HostProcess.Platform;
152 const executable = path.join(directory, platform === "win32" ? "t3.exe" : "t3");
153 const node = path.join(directory, platform === "win32" ? "node.exe" : "node");
154 yield* fs.writeFileString(executable, "standalone executable fixture");
155 yield* fs.chmod(executable, 0o755);
156 yield* fs.link(executable, node);
157 const error = yield* resolveNodeExecutable("Local device support", { PATH: directory }).pipe(
158 Effect.provideService(HostProcess.ExecutablePath, executable),
159 Effect.flip,
160 );
161 expect(error._tag).toBe("NodeRuntimeUnavailableError");
162 expect(error.message).toContain("Install Node.js");
163 }).pipe(
164 Effect.scoped,
165 Effect.provideService(HostProcess.IsExecutable, true),
166 Effect.provide(NodeServices.layer),
167 ),
168 );
169​
170 it.effect.skipIf(!symlinksSupported)("preserves the node alias used by runtime launchers", () =>
171 Effect.gen(function* () {
172 const fs = yield* FileSystem.FileSystem;
173 const path = yield* Path.Path;
174 const directory = yield* fs.makeTempDirectoryScoped();
175 const platform = yield* HostProcess.Platform;
176 const node = path.join(directory, platform === "win32" ? "node.exe" : "node");
177 yield* fs.symlink(process.execPath, node);
178 expect(yield* resolveNodeExecutable("Local device support", { PATH: directory })).toBe(node);
179 }).pipe(
180 Effect.scoped,
181 Effect.provideService(HostProcess.ExecutablePath, "/packaged/t3"),
182 Effect.provideService(HostProcess.IsExecutable, true),
183 Effect.provide(NodeServices.layer),
184 ),
185 );
186​
187 it.effect.skipIf(!symlinksSupported)(
188 "rejects a node alias pointing back at the standalone app",
189 () =>
190 Effect.gen(function* () {
191 const fs = yield* FileSystem.FileSystem;
192 const path = yield* Path.Path;
193 const directory = yield* fs.makeTempDirectoryScoped();
194 const platform = yield* HostProcess.Platform;
195 const node = path.join(directory, platform === "win32" ? "node.exe" : "node");
196 yield* fs.symlink(process.execPath, node);
197 const error = yield* resolveNodeExecutable("Local device support", {
198 PATH: directory,
199 }).pipe(Effect.flip);
200 expect(error.message).toContain("Install Node.js");
201 }).pipe(
202 Effect.scoped,
203 Effect.provideService(HostProcess.IsExecutable, true),
204 Effect.provide(NodeServices.layer),
205 ),
206 );
207});