| ... | @@ -165,3 +165,143 @@ if (args.includes("--package")) { | ... | @@ -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 | ); |