diff --git a/packages/shared/src/relayClient.test.ts b/packages/shared/src/relayClient.test.ts index d743067b6209..55a6075c01c5 100644 --- a/packages/shared/src/relayClient.test.ts +++ b/packages/shared/src/relayClient.test.ts @@ -26,6 +26,7 @@ const layerHostRuntime = (env: Record = {}, platform: NodeJS.Pla Layer.mergeAll( Layer.succeed(HostProcess.Platform, platform), Layer.succeed(HostProcess.Architecture, "x64"), + Layer.succeed(HostProcess.Environment, env), ConfigProvider.layer(ConfigProvider.fromEnv({ env })), ); @@ -466,50 +467,77 @@ describe("RelayClient", () => { ); }); - it.effect.skipIf(windowsHost)( - "observes PATH changes after the manager has been constructed", - () => { - const env = { PATH: "" }; - return Effect.gen(function* () { - const fileSystem = yield* FileSystem.FileSystem; - const baseDir = yield* fileSystem.makeTempDirectoryScoped({ - prefix: "t3-cloudflared-test-", - }); - const binDir = `${baseDir}/bin`; - const executablePath = `${binDir}/cloudflared`; - const manager = yield* RelayClient.makeCloudflaredRelayClient({ - baseDir, - }); + it.effect("observes hydrated PATH changes after the manager has been constructed", () => { + const platform = windowsHost ? "win32" : "linux"; + const executableName = windowsHost ? "cloudflared.exe" : "cloudflared"; + const configEnv = { PATH: "" }; + const hostEnv = { PATH: "" }; + return Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const baseDir = yield* fileSystem.makeTempDirectoryScoped({ + prefix: "t3-cloudflared-test-", + }); + const binDir = `${baseDir}/bin`; + const executablePath = `${binDir}/${executableName}`; + const resolvedExecutablePath = windowsHost + ? executablePath.replaceAll("/", "\\") + : executablePath; + const manager = yield* RelayClient.makeCloudflaredRelayClient({ + baseDir, + }); - expect(yield* manager.resolve).toEqual({ - status: "missing", - version: RelayClient.CLOUDFLARED_VERSION, - }); + const resolveWithStaleConfig = manager.resolve.pipe( + Effect.provideService( + ConfigProvider.ConfigProvider, + ConfigProvider.fromEnv({ env: configEnv }), + ), + Effect.provideService(HostProcess.Environment, hostEnv), + ); - yield* fileSystem.makeDirectory(binDir); - yield* fileSystem.writeFileString(executablePath, "cloudflared"); - yield* fileSystem.chmod(executablePath, 0o755); - env.PATH = binDir; + expect(yield* resolveWithStaleConfig).toEqual({ + status: "missing", + version: RelayClient.CLOUDFLARED_VERSION, + }); - expect(yield* manager.resolve).toEqual({ - status: "available", - executablePath, - source: "path", - version: RelayClient.CLOUDFLARED_VERSION, - }); - }).pipe( - Effect.scoped, - Effect.provide( - Layer.mergeAll( - NodeServices.layer, - layerHttpClient(new Uint8Array()), - layerSpawner([]), - layerHostRuntime(env), - ), + yield* fileSystem.makeDirectory(binDir); + yield* fileSystem.writeFileString(executablePath, "cloudflared"); + if (!windowsHost) yield* fileSystem.chmod(executablePath, 0o755); + hostEnv.PATH = binDir; + + expect(yield* resolveWithStaleConfig).toEqual({ + status: "available", + executablePath: resolvedExecutablePath, + source: "path", + version: RelayClient.CLOUDFLARED_VERSION, + }); + + const resolveWithOverride = manager.resolve.pipe( + Effect.provideService( + ConfigProvider.ConfigProvider, + ConfigProvider.fromEnv({ + env: { PATH: "", T3CODE_CLOUDFLARED_PATH: executablePath }, + }), ), + Effect.provideService(HostProcess.Environment, { PATH: "" }), ); - }, - ); + expect(yield* resolveWithOverride).toEqual({ + status: "available", + executablePath, + source: "override", + version: RelayClient.CLOUDFLARED_VERSION, + }); + }).pipe( + Effect.scoped, + Effect.provide( + Layer.mergeAll( + NodeServices.layer, + layerHttpClient(new Uint8Array()), + layerSpawner([]), + layerHostRuntime(configEnv, platform), + ), + ), + ); + }); describe("version selection", () => { const run = ( @@ -553,6 +581,7 @@ describe("RelayClient", () => { ConfigProvider.ConfigProvider, ConfigProvider.fromEnv({ env }), ), + Effect.provideService(HostProcess.Environment, env), ), ).toEqual({ status: "available", @@ -618,6 +647,7 @@ describe("RelayClient", () => { ConfigProvider.ConfigProvider, ConfigProvider.fromEnv({ env: { PATH: `${baseDir}/bin` } }), ), + Effect.provideService(HostProcess.Environment, { PATH: `${baseDir}/bin` }), ); expect(yield* resolveWith).toMatchObject({ source: "managed", version: "2026.1.0" }); diff --git a/packages/shared/src/relayClient.ts b/packages/shared/src/relayClient.ts index 8dd0a6e3eaec..3160acd01866 100644 --- a/packages/shared/src/relayClient.ts +++ b/packages/shared/src/relayClient.ts @@ -138,7 +138,6 @@ const trimmedString = (name: string) => const CloudflaredConfig = Config.all({ executableOverride: trimmedString(CLOUDFLARED_PATH_ENV_NAME), - path: trimmedString("PATH"), }); export interface CloudflaredRelayClientOptions { @@ -347,8 +346,10 @@ export const makeCloudflaredRelayClient = Effect.fn("cloudflared.make")(function }); const resolvePathExecutable = Effect.gen(function* () { - const config = yield* loadCloudflaredConfig; - const pathValue = Option.getOrUndefined(config.path); + // The default ConfigProvider is cached the first time any Config is read. + // Server startup reads its config before fixPath hydrates the environment, + // so reading PATH through Config here can retain the service's stale PATH. + const pathValue = (yield* HostProcess.Environment).PATH?.trim(); if (!pathValue) return null; const delimiter = platform === "win32" ? ";" : ":"; for (const directory of pathValue.split(delimiter)) {