import * as NodeServices from "@effect/platform-node/NodeServices"; import { describe, expect, it } from "@effect/vitest"; import type { ModelCapabilities } from "@t3tools/contracts"; import * as HostProcess from "@t3tools/shared/HostProcess"; import { createModelCapabilities } from "@t3tools/shared/model"; import * as Effect from "effect/Effect"; import * as Option from "effect/Option"; import * as Sink from "effect/Sink"; import * as Stream from "effect/Stream"; import * as ChildProcess from "effect/process/ChildProcess"; import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; import { isCommandMissingCause, parseGenericCliVersion, providerModelsFromSettings, spawnAndCollect, } from "./snapshotProbe.ts"; const OPENCODE_CUSTOM_MODEL_CAPABILITIES: ModelCapabilities = createModelCapabilities({ optionDescriptors: [ { id: "variant", label: "Reasoning", type: "select", options: [{ id: "medium", label: "Medium", isDefault: true }], currentValue: "medium", }, { id: "agent", label: "Agent", type: "select", options: [{ id: "build", label: "Build", isDefault: true }], currentValue: "build", }, ], }); describe("providerModelsFromSettings", () => { it("keeps an entry's own name and capabilities over the driver default", () => { const capabilities = createModelCapabilities({ optionDescriptors: [{ id: "fastMode", label: "Fast Mode", type: "boolean" }], }); const models = providerModelsFromSettings( [], ["bare", { slug: "named", name: "Named", capabilities }], OPENCODE_CUSTOM_MODEL_CAPABILITIES, ); expect(models).toEqual([ { slug: "bare", name: "bare", isCustom: true, capabilities: OPENCODE_CUSTOM_MODEL_CAPABILITIES, }, { slug: "named", name: "Named", isCustom: true, capabilities }, ]); }); it("renames a built-in model when a named entry uses its slug", () => { const capabilities = createModelCapabilities({ optionDescriptors: [] }); const models = providerModelsFromSettings( [ { slug: "gpt-5.5", name: "GPT-5.5", shortName: "5.5", isCustom: false, capabilities, }, ], [{ slug: "gpt-5.5", name: "Team GPT" }], capabilities, ); expect(models).toEqual([{ slug: "gpt-5.5", name: "Team GPT", isCustom: false, capabilities }]); }); it("preserves a custom slug that collides with a provider alias", () => { const capabilities = createModelCapabilities({ optionDescriptors: [] }); const models = providerModelsFromSettings( [ { slug: "claude-opus-4-8", name: "Claude Opus 4.8", isCustom: false, capabilities, }, ], [" opus "], capabilities, ); expect(models.map((model) => model.slug)).toEqual(["claude-opus-4-8", "opus"]); expect(models[1]?.isCustom).toBe(true); }); }); describe("parseGenericCliVersion", () => { it("parses a v-prefixed version", () => { expect(parseGenericCliVersion("opencode v2.0.3")).toBe("2.0.3"); expect(parseGenericCliVersion("v22.19.0")).toBe("22.19.0"); }); it("parses a version embedded in other output", () => { expect(parseGenericCliVersion("codex-cli 0.53.0 (build abc)")).toBe("0.53.0"); }); it("ignores versions glued to other word characters", () => { expect(parseGenericCliVersion("build2.0.3artifact")).toBeNull(); }); }); describe("ProviderCommandNotFoundError", () => { 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, }), ), ); return Effect.gen(function* () { const error = yield* spawnAndCollect( "C:\\tools\\codex.cmd", ChildProcess.make("codex", ["--version"]), ).pipe( Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), Effect.provideService(HostProcess.Platform, "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"); }); }); }); describe("spawnAndCollect", () => { // regression: #35262 - without forceKillAfter a timed-out probe whose CLI ignores // SIGTERM waits for it to exit, so the caller's timeout never returns. it.live.skipIf(HostProcess.Platform.defaultValue() === "win32")( "lets a timed-out probe return when the CLI ignores SIGTERM", () => Effect.gen(function* () { const result = yield* spawnAndCollect( "sh", ChildProcess.make("sh", ["-c", 'trap "" TERM; sleep 30']), ).pipe(Effect.timeoutOption("200 millis")); expect(Option.isNone(result)).toBe(true); }).pipe(Effect.provide(NodeServices.layer)), ); });