authorgravatar for 153842990+yashranaway@users.noreply.github.comAditya Garud <153842990+yashranaway@users.noreply.github.com> 2026-09-02 13:12:12+05:30
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-02 00:42:12-07:00
log4116db9807cb37b49d3db35c7a45301f5e851b01
tree1159ba891539e43a604ce3927f388696b56940b7
parentd2042d288eaf7aaf7feda85eb7d1a6f546302c12
signature Signed by PGP key B5690EEEBB952194

fix(server): bound OpenCode version probes (#8750)


3 files changed, 56 insertions(+), 10 deletions(-)

apps/server/src/provider/Layers/OpenCodeProvider.test.ts+33-9
......@@ -3,8 +3,10 @@ import * as NodeAssert from "node:assert/strict";
33import * as NodeServices from "@effect/platform-node/NodeServices";
44import { it } from "@effect/vitest";
55import * as Effect from "effect/Effect";
6import * as Fiber from "effect/Fiber";
67import * as Layer from "effect/Layer";
78import * as Schema from "effect/Schema";
9import * as TestClock from "effect/testing/TestClock";
810import { beforeEach } from "vite-plus/test";
911
1012import { OpenCodeSettings } from "@t3tools/contracts";
......@@ -34,6 +36,7 @@ const DEFAULT_VERSION_STDOUT = "opencode 1.14.19\n";
3436const runtimeMock = {
3537 state: {
3638 runVersionError: null as Error | null,
39 runVersionPending: false,
3740 versionStdout: DEFAULT_VERSION_STDOUT,
3841 inventoryError: null as Error | null,
3942 connectionError: null as Error | null,
......@@ -52,6 +55,7 @@ const runtimeMock = {
5255 },
5356 reset() {
5457 this.state.runVersionError = null;
58 this.state.runVersionPending = false;
5559 this.state.versionStdout = DEFAULT_VERSION_STDOUT;
5660 this.state.inventoryError = null;
5761 this.state.connectionError = null;
......@@ -114,15 +118,17 @@ const OpenCodeRuntimeTestDouble: OpenCodeRuntimeShape = {
114118 };
115119 }),
116120 runOpenCodeCommand: () =>
117 runtimeMock.state.runVersionError
118 ? Effect.fail(
119 new OpenCodeRuntimeError({
120 operation: "runOpenCodeCommand",
121 detail: runtimeMock.state.runVersionError.message,
122 cause: runtimeMock.state.runVersionError,
123 }),
124 )
125 : Effect.succeed({ stdout: runtimeMock.state.versionStdout, stderr: "", code: 0 }),
121 runtimeMock.state.runVersionPending
122 ? Effect.never
123 : runtimeMock.state.runVersionError
124 ? Effect.fail(
125 new OpenCodeRuntimeError({
126 operation: "runOpenCodeCommand",
127 detail: runtimeMock.state.runVersionError.message,
128 cause: runtimeMock.state.runVersionError,
129 }),
130 )
131 : Effect.succeed({ stdout: runtimeMock.state.versionStdout, stderr: "", code: 0 }),
126132 createOpenCodeSdkClient: (input) => {
127133 runtimeMock.state.sdkClientInputs.push(input);
128134 return {} as unknown as ReturnType<OpenCodeRuntimeShape["createOpenCodeSdkClient"]>;
......@@ -218,6 +224,24 @@ it.layer(testLayer)("checkOpenCodeProviderStatus", (it) => {
218224 }),
219225 );
220226
227 it.effect("times out a hanging local CLI version probe", () =>
228 Effect.gen(function* () {
229 runtimeMock.state.runVersionPending = true;
230 const probeFiber = yield* checkProvider(makeOpenCodeSettings()).pipe(Effect.forkChild);
231
232 yield* Effect.yieldNow;
233 yield* TestClock.adjust("4 seconds");
234 const snapshot = yield* Fiber.join(probeFiber);
235
236 NodeAssert.equal(snapshot.status, "error");
237 NodeAssert.equal(snapshot.installed, true);
238 NodeAssert.equal(
239 snapshot.message,
240 "Failed to execute OpenCode CLI health check: OpenCode CLI version probe timed out after 4 seconds.",
241 );
242 }).pipe(Effect.provide(TestClock.layer())),
243 );
244
221245 it.effect("emits OpenCode variant defaults so trait picker can resolve a visible selection", () =>
222246 Effect.gen(function* () {
223247 runtimeMock.state.inventory = {
apps/server/src/provider/Layers/OpenCodeProvider.ts+11-1
......@@ -31,9 +31,10 @@ const OPENCODE_PRESENTATION = {
3131 displayName: "OpenCode",
3232 showInteractionModeToggle: false,
3333} as const;
34const OPENCODE_VERSION_PROBE_TIMEOUT = "4 seconds";
3435
3536class OpenCodeProbeError extends Data.TaggedError("OpenCodeProbeError")<{
36 readonly cause: unknown;
37 readonly cause?: unknown;
3738 readonly detail: string;
3839}> {}
3940
......@@ -401,6 +402,15 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu
401402 Effect.mapError(
402403 (cause) => new OpenCodeProbeError({ cause, detail: openCodeRuntimeErrorDetail(cause) }),
403404 ),
405 Effect.timeoutOrElse({
406 duration: OPENCODE_VERSION_PROBE_TIMEOUT,
407 orElse: () =>
408 Effect.fail(
409 new OpenCodeProbeError({
410 detail: `OpenCode CLI version probe timed out after ${OPENCODE_VERSION_PROBE_TIMEOUT}.`,
411 }),
412 ),
413 }),
404414 ),
405415 );
406416 if (versionExit._tag === "Failure") {
apps/server/src/provider/opencodeRuntime.ts+12
......@@ -563,11 +563,23 @@ const makeOpenCodeRuntime = Effect.gen(function* () {
563563 const spawnCommand = yield* resolveCommand(input.binaryPath, input.args, input.environment);
564564 const child = yield* spawner.spawn(
565565 ChildProcess.make(spawnCommand.command, spawnCommand.args, {
566 detached: hostPlatform !== "win32",
566567 shell: spawnCommand.shell,
567568 ...(input.cwd ? { cwd: input.cwd } : {}),
568569 ...(input.environment ? { env: input.environment } : { extendEnv: true }),
569570 }),
570571 );
572 const terminateCommandGroup =
573 hostPlatform === "win32"
574 ? child.kill({ killSignal: "SIGKILL" }).pipe(Effect.asVoid)
575 : Effect.sync(() => {
576 try {
577 process.kill(-Number(child.pid), "SIGKILL");
578 } catch {
579 // The command and its process group may already have exited.
580 }
581 });
582 yield* Effect.addFinalizer(() => terminateCommandGroup.pipe(Effect.ignore));
571583 const collectOptions =
572584 input.maxOutputBytes === undefined ? undefined : { maxBytes: input.maxOutputBytes };
573585 const [stdout, stderr, code] = yield* Effect.all(