| ... | ... | @@ -165,3 +165,143 @@ if (args.includes("--package")) { |
| 165 | 165 | ); |
| 166 | 166 | }, |
| 167 | 167 | ); |
| 168 | |
| 169 | describe.skipIf(HostProcessPlatform.defaultValue() === "win32")( |
| 170 | "remote runner install diagnostics", |
| 171 | () => { |
| 172 | const decodeArguments = Schema.decodeUnknownSync( |
| 173 | Schema.fromJsonString(Schema.Array(Schema.String)), |
| 174 | ); |
| 175 | const cases = (["npx", "npm"] as const).flatMap((packageManager) => |
| 176 | ( |
| 177 | [ |
| 178 | "etarget", |
| 179 | "network", |
| 180 | "empty-success", |
| 181 | "success", |
| 182 | "failed-with-path", |
| 183 | "existing-cli", |
| 184 | "node-override", |
| 185 | ] as const |
| 186 | ).map((mode) => ({ packageManager, mode })), |
| 187 | ); |
| 188 | |
| 189 | it.live.each(cases)("handles $packageManager/$mode", ({ packageManager, mode }) => |
| 190 | Effect.gen(function* () { |
| 191 | const fs = yield* FileSystem.FileSystem; |
| 192 | const path = yield* Path.Path; |
| 193 | const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; |
| 194 | const fixture = yield* fs.makeTempDirectoryScoped({ prefix: "t3-runner-install-" }); |
| 195 | const bin = path.join(fixture, "bin"); |
| 196 | const cliPath = path.join(fixture, "installed cli.mjs"); |
| 197 | const callsPath = path.join(fixture, "installer-calls.jsonl"); |
| 198 | const packageSpec = "t3@0.0.39-nightly.20260905.1286"; |
| 199 | const args = ["serve", "a path with spaces"]; |
| 200 | yield* fs.makeDirectory(bin); |
| 201 | yield* fs.symlink(process.execPath, path.join(bin, "node")); |
| 202 | yield* fs.writeFileString( |
| 203 | cliPath, |
| 204 | `#!/usr/bin/env node |
| 205 | process.stdout.write(JSON.stringify(process.argv.slice(2)) + "\\n"); |
| 206 | `, |
| 207 | ); |
| 208 | yield* fs.chmod(cliPath, 0o700); |
| 209 | yield* fs.writeFileString(callsPath, ""); |
| 210 | yield* fs.writeFileString( |
| 211 | path.join(bin, packageManager), |
| 212 | `#!/usr/bin/env node |
| 213 | const fs = require("node:fs"); |
| 214 | fs.appendFileSync(process.env.T3_TEST_CALLS, JSON.stringify(process.argv.slice(2)) + "\\n"); |
| 215 | const mode = process.env.T3_TEST_MODE; |
| 216 | if (mode === "success" || mode === "failed-with-path") { |
| 217 | process.stdout.write(process.env.T3_TEST_CLI + "\\n"); |
| 218 | } |
| 219 | if (mode === "etarget" || mode === "failed-with-path") { |
| 220 | process.stderr.write("npm error code ETARGET\\nnpm error notarget No matching version found.\\n"); |
| 221 | process.exitCode = 42; |
| 222 | } else if (mode === "network") { |
| 223 | process.stderr.write("npm error code ENETUNREACH\\n"); |
| 224 | process.exitCode = 43; |
| 225 | } |
| 226 | `, |
| 227 | ); |
| 228 | yield* fs.chmod(path.join(bin, packageManager), 0o700); |
| 229 | if (mode === "existing-cli") yield* fs.symlink(cliPath, path.join(bin, "t3")); |
| 230 | |
| 231 | const child = yield* spawner.spawn( |
| 232 | ChildProcess.make("/bin/sh", ["-s", "--", ...args], { |
| 233 | cwd: fixture, |
| 234 | extendEnv: false, |
| 235 | env: { |
| 236 | PATH: bin, |
| 237 | T3_TEST_MODE: mode, |
| 238 | T3_TEST_CLI: cliPath, |
| 239 | T3_TEST_CALLS: callsPath, |
| 240 | }, |
| 241 | stdin: Stream.make( |
| 242 | new TextEncoder().encode( |
| 243 | buildRemoteT3RunnerScript({ |
| 244 | packageSpec, |
| 245 | ...(mode === "node-override" ? { nodeScriptPath: cliPath } : {}), |
| 246 | }), |
| 247 | ), |
| 248 | ), |
| 249 | }), |
| 250 | ); |
| 251 | const { stdout, stderr, exitCode } = yield* Effect.all( |
| 252 | { |
| 253 | stdout: child.stdout.pipe(Stream.decodeText(), Stream.mkString), |
| 254 | stderr: child.stderr.pipe(Stream.decodeText(), Stream.mkString), |
| 255 | exitCode: child.exitCode, |
| 256 | }, |
| 257 | { concurrency: "unbounded" }, |
| 258 | ); |
| 259 | const installFailed = |
| 260 | mode === "etarget" || mode === "network" || mode === "failed-with-path"; |
| 261 | const missingExecutable = mode === "empty-success"; |
| 262 | assert.equal(exitCode, installFailed || missingExecutable ? 1 : 0); |
| 263 | if (installFailed || missingExecutable) { |
| 264 | assert.equal(stdout, ""); |
| 265 | } else { |
| 266 | assert.deepEqual(decodeArguments(stdout), args); |
| 267 | } |
| 268 | if (installFailed) { |
| 269 | const npmError = mode === "network" ? "ENETUNREACH" : "ETARGET"; |
| 270 | assert.include(stderr, `npm error code ${npmError}\n`); |
| 271 | assert.include(stderr, `Remote host could not install ${packageSpec}.`); |
| 272 | assert.notInclude(stderr, "Remote host installed"); |
| 273 | assert.notInclude(stderr, "Install a C toolchain"); |
| 274 | } else if (missingExecutable) { |
| 275 | assert.include(stderr, `Remote host installed ${packageSpec}`); |
| 276 | assert.include(stderr, "npm produced no t3 executable"); |
| 277 | assert.include(stderr, "Install a C toolchain"); |
| 278 | } else { |
| 279 | assert.equal(stderr, ""); |
| 280 | } |
| 281 | const expectedCall = [ |
| 282 | ...(packageManager === "npm" ? ["exec"] : []), |
| 283 | "--yes", |
| 284 | "--package", |
| 285 | packageSpec, |
| 286 | "--", |
| 287 | "sh", |
| 288 | "-c", |
| 289 | "command -v t3", |
| 290 | ]; |
| 291 | const usesInstaller = mode !== "existing-cli" && mode !== "node-override"; |
| 292 | const calls = yield* fs.readFileString(callsPath); |
| 293 | if (usesInstaller) { |
| 294 | assert.deepEqual( |
| 295 | calls |
| 296 | .trim() |
| 297 | .split("\n") |
| 298 | .map((line) => decodeArguments(line)), |
| 299 | [expectedCall], |
| 300 | ); |
| 301 | } else { |
| 302 | assert.equal(calls, ""); |
| 303 | } |
| 304 | }).pipe(Effect.provide(NodeServices.layer), Effect.scoped), |
| 305 | ); |
| 306 | }, |
| 307 | ); |