diff --git a/apps/server/src/provider/providerMaintenanceRunner.test.ts b/apps/server/src/provider/providerMaintenanceRunner.test.ts index 5f5f975a4e3..632529975fb 100644 --- a/apps/server/src/provider/providerMaintenanceRunner.test.ts +++ b/apps/server/src/provider/providerMaintenanceRunner.test.ts @@ -16,7 +16,7 @@ import * as Schema from "effect/Schema"; import * as Sink from "effect/Sink"; import * as Stream from "effect/Stream"; import { HttpClient, HttpClientResponse } from "effect/unstable/http"; -import { ChildProcessSpawner } from "effect/unstable/process"; +import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; import { ProviderRegistry, type ProviderRegistryShape } from "./Services/ProviderRegistry.ts"; import * as ProviderMaintenanceRunner from "./providerMaintenanceRunner.ts"; @@ -125,6 +125,7 @@ function mockSpawnerLayer( handler: ( command: string, args: ReadonlyArray, + options: ChildProcess.CommandOptions, ) => { readonly stdout?: string; readonly stderr?: string; @@ -138,9 +139,32 @@ function mockSpawnerLayer( const childProcess = command as unknown as { readonly command: string; readonly args: ReadonlyArray; + readonly options: ChildProcess.CommandOptions; }; - return Effect.succeed(mockHandle(handler(childProcess.command, childProcess.args))); + return Effect.succeed( + mockHandle(handler(childProcess.command, childProcess.args, childProcess.options)), + ); + }), + ); +} + +function withProcessPlatform( + platform: NodeJS.Platform, + effect: Effect.Effect, +): Effect.Effect { + return Effect.acquireUseRelease( + Effect.sync(() => { + const descriptor = Object.getOwnPropertyDescriptor(process, "platform"); + Object.defineProperty(process, "platform", { value: platform }); + return descriptor; }), + () => effect, + (descriptor) => + Effect.sync(() => { + if (descriptor) { + Object.defineProperty(process, "platform", descriptor); + } + }), ); } @@ -319,6 +343,42 @@ describe("providerMaintenanceRunner", () => { }, ); + it.effect("runs provider update commands through a shell on Windows", () => { + const calls: Array<{ + command: string; + args: ReadonlyArray; + shell: ChildProcess.CommandOptions["shell"]; + }> = []; + return withProcessPlatform( + "win32", + Effect.gen(function* () { + const { registry } = yield* makeRegistry(baseProvider); + const runner = yield* makeTestRunner(registry); + + const result = yield* runner.updateProvider(CODEX_DRIVER); + + assert.deepStrictEqual(calls, [ + { + command: "npm", + args: ["install", "-g", "@openai/codex@latest"], + shell: true, + }, + ]); + assert.strictEqual(result.providers[0]?.updateState?.status, "succeeded"); + }), + ).pipe( + Effect.provide( + Layer.mergeAll( + latestVersionHttpClient("0.0.0"), + mockSpawnerLayer((command, args, options) => { + calls.push({ command, args, shell: options.shell }); + return { stdout: "updated" }; + }), + ), + ), + ); + }); + it.effect("updates a single provider instance without touching sibling instances", () => { const calls: Array<{ command: string; args: ReadonlyArray }> = []; return Effect.gen(function* () { diff --git a/apps/server/src/provider/providerMaintenanceRunner.ts b/apps/server/src/provider/providerMaintenanceRunner.ts index 5f76afb34d3..64e97c549e8 100644 --- a/apps/server/src/provider/providerMaintenanceRunner.ts +++ b/apps/server/src/provider/providerMaintenanceRunner.ts @@ -76,7 +76,13 @@ const runProviderMaintenanceCommandWithSpawner = Effect.fn("ProviderMaintenanceR const collectCommandResult = Effect.fn("ProviderMaintenanceRunner.collectCommandResult")( function* () { const child = yield* input.spawner - .spawn(ChildProcess.make(input.command, [...input.args])) + .spawn( + ChildProcess.make( + input.command, + [...input.args], + process.platform === "win32" ? { shell: true } : {}, + ), + ) .pipe( Effect.mapError( (cause) =>