import { describe, expect, it } from "@effect/vitest"; import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; import { SpawnExecutableResolution } from "@t3tools/shared/shell"; import * as Effect from "effect/Effect"; import { ClaudeExecutableFileCheck, resolveClaudeSdkExecutablePath } from "./ClaudeExecutable.ts"; const NPM_DIR = "C:\\Users\\dev\\AppData\\Roaming\\npm"; const NPM_SHIM = `${NPM_DIR}\\claude.cmd`; const NPM_PACKAGE_EXE = `${NPM_DIR}\\node_modules\\@anthropic-ai\\claude-code\\bin\\claude.exe`; const NPM_PACKAGE_CLI = `${NPM_DIR}\\node_modules\\@anthropic-ai\\claude-code\\cli.js`; function withWindowsResolution(input: { readonly resolvedCommand: string | undefined; readonly existingFiles?: ReadonlyArray; }) { const existing = new Set(input.existingFiles ?? []); return (effect: Effect.Effect) => effect.pipe( Effect.provideService(HostProcessPlatform, "win32"), Effect.provideService(SpawnExecutableResolution, () => input.resolvedCommand), Effect.provideService(ClaudeExecutableFileCheck, (filePath) => existing.has(filePath)), ); } describe("resolveClaudeSdkExecutablePath", () => { it.effect("returns the configured path unchanged on non-Windows platforms", () => Effect.gen(function* () { expect( yield* resolveClaudeSdkExecutablePath("claude", {}).pipe( Effect.provideService(HostProcessPlatform, "darwin"), Effect.provideService(SpawnExecutableResolution, () => { throw new Error("must not resolve on non-Windows platforms"); }), ), ).toBe("claude"); }), ); it.effect("returns the resolved absolute path for native Windows executables", () => Effect.gen(function* () { const nativeBinary = "C:\\Users\\dev\\.local\\bin\\claude.exe"; expect( yield* resolveClaudeSdkExecutablePath("claude", {}).pipe( withWindowsResolution({ resolvedCommand: nativeBinary }), ), ).toBe(nativeBinary); }), ); it.effect("follows an npm launcher shim to the packaged native binary", () => Effect.gen(function* () { expect( yield* resolveClaudeSdkExecutablePath("claude", {}).pipe( withWindowsResolution({ resolvedCommand: NPM_SHIM, existingFiles: [NPM_PACKAGE_EXE, NPM_PACKAGE_CLI], }), ), ).toBe(NPM_PACKAGE_EXE); }), ); it.effect("follows .bat and .ps1 launcher shims the same way", () => Effect.gen(function* () { for (const shim of [`${NPM_DIR}\\claude.bat`, `${NPM_DIR}\\claude.ps1`]) { expect( yield* resolveClaudeSdkExecutablePath("claude", {}).pipe( withWindowsResolution({ resolvedCommand: shim, existingFiles: [NPM_PACKAGE_EXE], }), ), ).toBe(NPM_PACKAGE_EXE); } }), ); it.effect("normalizes mixed-case shim extensions before matching", () => Effect.gen(function* () { expect( yield* resolveClaudeSdkExecutablePath("claude", {}).pipe( withWindowsResolution({ resolvedCommand: `${NPM_DIR}\\claude.CMD`, existingFiles: [NPM_PACKAGE_EXE], }), ), ).toBe(NPM_PACKAGE_EXE); }), ); it.effect("falls back to cli.js when the package ships no native binary", () => Effect.gen(function* () { expect( yield* resolveClaudeSdkExecutablePath("claude", {}).pipe( withWindowsResolution({ resolvedCommand: NPM_SHIM, existingFiles: [NPM_PACKAGE_CLI], }), ), ).toBe(NPM_PACKAGE_CLI); }), ); it.effect("returns the configured path when a shim has no known package entry", () => Effect.gen(function* () { expect( yield* resolveClaudeSdkExecutablePath("claude", {}).pipe( withWindowsResolution({ resolvedCommand: NPM_SHIM }), ), ).toBe("claude"); }), ); it.effect("returns the configured path when command resolution finds nothing", () => Effect.gen(function* () { expect( yield* resolveClaudeSdkExecutablePath("claude", {}).pipe( withWindowsResolution({ resolvedCommand: undefined }), ), ).toBe("claude"); }), ); });