From 0b2b3d813fd85fb69dd228db71e920803bc6a8d0 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 20 Jun 2026 07:14:37 -0700 Subject: [PATCH 1/3] Structure missing provider command failures Co-authored-by: codex --- .../src/provider/providerSnapshot.test.ts | 25 +++++++++++++++- apps/server/src/provider/providerSnapshot.ts | 30 ++++++++++++++----- 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/apps/server/src/provider/providerSnapshot.test.ts b/apps/server/src/provider/providerSnapshot.test.ts index fdc8b4c4a71..83802f23036 100644 --- a/apps/server/src/provider/providerSnapshot.test.ts +++ b/apps/server/src/provider/providerSnapshot.test.ts @@ -2,7 +2,11 @@ import { describe, expect, it } from "vite-plus/test"; import { ProviderDriverKind, type ModelCapabilities } from "@t3tools/contracts"; import { createModelCapabilities } from "@t3tools/shared/model"; -import { providerModelsFromSettings } from "./providerSnapshot.ts"; +import { + isCommandMissingCause, + ProviderCommandNotFoundError, + providerModelsFromSettings, +} from "./providerSnapshot.ts"; const OPENCODE_CUSTOM_MODEL_CAPABILITIES: ModelCapabilities = createModelCapabilities({ optionDescriptors: [ @@ -42,3 +46,22 @@ describe("providerModelsFromSettings", () => { ]); }); }); + +describe("ProviderCommandNotFoundError", () => { + it("retains the failed command result as structured diagnostics", () => { + const error = new ProviderCommandNotFoundError({ + binaryPath: "C:\\tools\\codex.cmd", + exitCode: 9009, + stdout: "", + stderr: "'codex' is not recognized as an internal or external command", + }); + + expect(error.binaryPath).toBe("C:\\tools\\codex.cmd"); + expect(error.exitCode).toBe(9009); + expect(error.stderr).toContain("not recognized"); + expect(error.message).toBe( + "Provider command C:\\tools\\codex.cmd was not found (exit code 9009).", + ); + expect(isCommandMissingCause(error)).toBe(true); + }); +}); diff --git a/apps/server/src/provider/providerSnapshot.ts b/apps/server/src/provider/providerSnapshot.ts index 2ecb3220773..2d560ffb4ff 100644 --- a/apps/server/src/provider/providerSnapshot.ts +++ b/apps/server/src/provider/providerSnapshot.ts @@ -9,7 +9,7 @@ import type { ServerProviderState, } from "@t3tools/contracts"; import * as Effect from "effect/Effect"; -import * as Data from "effect/Data"; +import * as Schema from "effect/Schema"; import * as Stream from "effect/Stream"; import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; import { normalizeModelSlug } from "@t3tools/shared/model"; @@ -27,11 +27,21 @@ export interface CommandResult { readonly code: number; } -export class ProviderCommandExecutionError extends Data.TaggedError( - "ProviderCommandExecutionError", -)<{ - readonly message: string; -}> {} +export class ProviderCommandNotFoundError extends Schema.TaggedErrorClass()( + "ProviderCommandNotFoundError", + { + binaryPath: Schema.String, + exitCode: Schema.Number, + stdout: Schema.String, + stderr: Schema.String, + }, +) { + override get message(): string { + return `Provider command ${this.binaryPath} was not found (exit code ${this.exitCode}).`; + } +} + +const isProviderCommandNotFoundError = Schema.is(ProviderCommandNotFoundError); export interface ProviderProbeResult { readonly installed: boolean; @@ -57,6 +67,7 @@ export function nonEmptyTrimmed(value: string | undefined): string | undefined { } export function isCommandMissingCause(error: { readonly message: string }): boolean { + if (isProviderCommandNotFoundError(error)) return true; const lower = error.message.toLowerCase(); return lower.includes("enoent") || lower.includes("notfound"); } @@ -76,7 +87,12 @@ export const spawnAndCollect = (binaryPath: string, command: ChildProcess.Comman const result: CommandResult = { stdout, stderr, code: exitCode }; if (yield* isWindowsCommandNotFound(exitCode, stderr)) { - return yield* new ProviderCommandExecutionError({ message: `spawn ${binaryPath} ENOENT` }); + return yield* new ProviderCommandNotFoundError({ + binaryPath, + exitCode, + stdout, + stderr, + }); } return result; }).pipe(Effect.scoped); From 74c760bf753309afc0a8f9f13a0c96e94e22bbd9 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 20 Jun 2026 09:24:11 -0700 Subject: [PATCH 2/3] fix(provider): redact missing command output Co-authored-by: codex --- .../src/provider/providerSnapshot.test.ts | 68 ++++++++++++++----- apps/server/src/provider/providerSnapshot.ts | 8 +-- 2 files changed, 56 insertions(+), 20 deletions(-) diff --git a/apps/server/src/provider/providerSnapshot.test.ts b/apps/server/src/provider/providerSnapshot.test.ts index 83802f23036..a5f9713ea17 100644 --- a/apps/server/src/provider/providerSnapshot.test.ts +++ b/apps/server/src/provider/providerSnapshot.test.ts @@ -1,11 +1,17 @@ -import { describe, expect, it } from "vite-plus/test"; +import { describe, expect, it } from "@effect/vitest"; import { ProviderDriverKind, type ModelCapabilities } from "@t3tools/contracts"; +import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; import { createModelCapabilities } from "@t3tools/shared/model"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Sink from "effect/Sink"; +import * as Stream from "effect/Stream"; +import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; import { isCommandMissingCause, - ProviderCommandNotFoundError, providerModelsFromSettings, + spawnAndCollect, } from "./providerSnapshot.ts"; const OPENCODE_CUSTOM_MODEL_CAPABILITIES: ModelCapabilities = createModelCapabilities({ @@ -48,20 +54,50 @@ describe("providerModelsFromSettings", () => { }); describe("ProviderCommandNotFoundError", () => { - it("retains the failed command result as structured diagnostics", () => { - const error = new ProviderCommandNotFoundError({ - binaryPath: "C:\\tools\\codex.cmd", - exitCode: 9009, - stdout: "", - stderr: "'codex' is not recognized as an internal or external command", - }); - - expect(error.binaryPath).toBe("C:\\tools\\codex.cmd"); - expect(error.exitCode).toBe(9009); - expect(error.stderr).toContain("not recognized"); - expect(error.message).toBe( - "Provider command C:\\tools\\codex.cmd was not found (exit code 9009).", + it.effect("retains safe failed-command diagnostics without process output", () => { + const stderr = "'codex' is not recognized: secret-token-value"; + const spawner = ChildProcessSpawner.make(() => + Effect.succeed( + ChildProcessSpawner.makeHandle({ + pid: ChildProcessSpawner.ProcessId(1), + exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(9009)), + isRunning: Effect.succeed(false), + kill: () => Effect.void, + unref: Effect.succeed(Effect.void), + stdin: Sink.drain, + stdout: Stream.empty, + stderr: Stream.encodeText(Stream.make(stderr)), + all: Stream.empty, + getInputFd: () => Sink.drain, + getOutputFd: () => Stream.empty, + }), + ), ); - expect(isCommandMissingCause(error)).toBe(true); + return Effect.gen(function* () { + const error = yield* spawnAndCollect( + "C:\\tools\\codex.cmd", + ChildProcess.make("codex", ["--version"]), + ).pipe( + Effect.provide(Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner)), + Effect.provideService(HostProcessPlatform, "win32"), + Effect.flip, + ); + + if (error._tag !== "ProviderCommandNotFoundError") { + throw new Error(`Unexpected error: ${error._tag}`); + } + + expect(error.binaryPath).toBe("C:\\tools\\codex.cmd"); + expect(error.exitCode).toBe(9009); + expect(error.stdoutLength).toBe(0); + expect(error.stderrLength).toBe(stderr.length); + expect(error.message).toBe( + "Provider command C:\\tools\\codex.cmd was not found (exit code 9009).", + ); + expect(isCommandMissingCause(error)).toBe(true); + expect(error).not.toHaveProperty("stdout"); + expect(error).not.toHaveProperty("stderr"); + expect(error.message).not.toContain("secret-token-value"); + }); }); }); diff --git a/apps/server/src/provider/providerSnapshot.ts b/apps/server/src/provider/providerSnapshot.ts index 2d560ffb4ff..eb60f7a30b2 100644 --- a/apps/server/src/provider/providerSnapshot.ts +++ b/apps/server/src/provider/providerSnapshot.ts @@ -32,8 +32,8 @@ export class ProviderCommandNotFoundError extends Schema.TaggedErrorClass Date: Sat, 20 Jun 2026 10:35:47 -0700 Subject: [PATCH 3/3] classify missing provider commands structurally Co-authored-by: codex --- apps/server/src/provider/providerSnapshot.test.ts | 15 +++++++++++++++ apps/server/src/provider/providerSnapshot.ts | 6 +++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/apps/server/src/provider/providerSnapshot.test.ts b/apps/server/src/provider/providerSnapshot.test.ts index a5f9713ea17..abe138fdfb9 100644 --- a/apps/server/src/provider/providerSnapshot.test.ts +++ b/apps/server/src/provider/providerSnapshot.test.ts @@ -4,6 +4,7 @@ import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; import { createModelCapabilities } from "@t3tools/shared/model"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; +import * as PlatformError from "effect/PlatformError"; import * as Sink from "effect/Sink"; import * as Stream from "effect/Stream"; import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; @@ -54,6 +55,20 @@ describe("providerModelsFromSettings", () => { }); describe("ProviderCommandNotFoundError", () => { + it("classifies normalized platform failures without parsing messages", () => { + expect( + isCommandMissingCause( + PlatformError.systemError({ + _tag: "NotFound", + module: "ChildProcess", + method: "spawn", + description: "arbitrary host detail", + }), + ), + ).toBe(true); + expect(isCommandMissingCause(new Error("spawn provider ENOENT"))).toBe(false); + }); + it.effect("retains safe failed-command diagnostics without process output", () => { const stderr = "'codex' is not recognized: secret-token-value"; const spawner = ChildProcessSpawner.make(() => diff --git a/apps/server/src/provider/providerSnapshot.ts b/apps/server/src/provider/providerSnapshot.ts index eb60f7a30b2..dfe31ffdc44 100644 --- a/apps/server/src/provider/providerSnapshot.ts +++ b/apps/server/src/provider/providerSnapshot.ts @@ -9,6 +9,7 @@ import type { ServerProviderState, } from "@t3tools/contracts"; import * as Effect from "effect/Effect"; +import * as PlatformError from "effect/PlatformError"; import * as Schema from "effect/Schema"; import * as Stream from "effect/Stream"; import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; @@ -66,10 +67,9 @@ export function nonEmptyTrimmed(value: string | undefined): string | undefined { return trimmed.length > 0 ? trimmed : undefined; } -export function isCommandMissingCause(error: { readonly message: string }): boolean { +export function isCommandMissingCause(error: unknown): boolean { if (isProviderCommandNotFoundError(error)) return true; - const lower = error.message.toLowerCase(); - return lower.includes("enoent") || lower.includes("notfound"); + return error instanceof PlatformError.PlatformError && error.reason._tag === "NotFound"; } export const spawnAndCollect = (binaryPath: string, command: ChildProcess.Command) =>