1import { describe, expect, it } from "@effect/vitest";
2import * as HostProcess from "@t3tools/shared/HostProcess";
3import { SpawnExecutableResolution } from "@t3tools/shared/shell";
4import * as Effect from "effect/Effect";
5
6import { ClaudeExecutableFileCheck, resolveClaudeSdkExecutablePath } from "./ClaudeExecutable.ts";
7
8const NPM_DIR = "C:\\Users\\dev\\AppData\\Roaming\\npm";
9const NPM_SHIM = `${NPM_DIR}\\claude.cmd`;
10const NPM_PACKAGE_EXE = `${NPM_DIR}\\node_modules\\@anthropic-ai\\claude-code\\bin\\claude.exe`;
11const NPM_PACKAGE_CLI = `${NPM_DIR}\\node_modules\\@anthropic-ai\\claude-code\\cli.js`;
12
13function withWindowsResolution(input: {
14 readonly resolvedCommand: string | undefined;
15 readonly existingFiles?: ReadonlyArray<string>;
16}) {
17 const existing = new Set(input.existingFiles ?? []);
18 return <A, E, R>(effect: Effect.Effect<A, E, R>) =>
19 effect.pipe(
20 Effect.provideService(HostProcess.Platform, "win32"),
21 Effect.provideService(SpawnExecutableResolution, () => input.resolvedCommand),
22 Effect.provideService(ClaudeExecutableFileCheck, (filePath) => existing.has(filePath)),
23 );
24}
25
26describe("resolveClaudeSdkExecutablePath", () => {
27 it.effect("returns the configured path unchanged on non-Windows platforms", () =>
28 Effect.gen(function* () {
29 expect(
30 yield* resolveClaudeSdkExecutablePath("claude", {}).pipe(
31 Effect.provideService(HostProcess.Platform, "darwin"),
32 Effect.provideService(SpawnExecutableResolution, () => {
33 throw new Error("must not resolve on non-Windows platforms");
34 }),
35 ),
36 ).toBe("claude");
37 }),
38 );
39
40 it.effect("returns the resolved absolute path for native Windows executables", () =>
41 Effect.gen(function* () {
42 const nativeBinary = "C:\\Users\\dev\\.local\\bin\\claude.exe";
43 expect(
44 yield* resolveClaudeSdkExecutablePath("claude", {}).pipe(
45 withWindowsResolution({ resolvedCommand: nativeBinary }),
46 ),
47 ).toBe(nativeBinary);
48 }),
49 );
50
51 it.effect("follows an npm launcher shim to the packaged native binary", () =>
52 Effect.gen(function* () {
53 expect(
54 yield* resolveClaudeSdkExecutablePath("claude", {}).pipe(
55 withWindowsResolution({
56 resolvedCommand: NPM_SHIM,
57 existingFiles: [NPM_PACKAGE_EXE, NPM_PACKAGE_CLI],
58 }),
59 ),
60 ).toBe(NPM_PACKAGE_EXE);
61 }),
62 );
63
64 it.effect("follows .bat and .ps1 launcher shims the same way", () =>
65 Effect.gen(function* () {
66 for (const shim of [`${NPM_DIR}\\claude.bat`, `${NPM_DIR}\\claude.ps1`]) {
67 expect(
68 yield* resolveClaudeSdkExecutablePath("claude", {}).pipe(
69 withWindowsResolution({
70 resolvedCommand: shim,
71 existingFiles: [NPM_PACKAGE_EXE],
72 }),
73 ),
74 ).toBe(NPM_PACKAGE_EXE);
75 }
76 }),
77 );
78
79 it.effect("normalizes mixed-case shim extensions before matching", () =>
80 Effect.gen(function* () {
81 expect(
82 yield* resolveClaudeSdkExecutablePath("claude", {}).pipe(
83 withWindowsResolution({
84 resolvedCommand: `${NPM_DIR}\\claude.CMD`,
85 existingFiles: [NPM_PACKAGE_EXE],
86 }),
87 ),
88 ).toBe(NPM_PACKAGE_EXE);
89 }),
90 );
91
92 it.effect("falls back to cli.js when the package ships no native binary", () =>
93 Effect.gen(function* () {
94 expect(
95 yield* resolveClaudeSdkExecutablePath("claude", {}).pipe(
96 withWindowsResolution({
97 resolvedCommand: NPM_SHIM,
98 existingFiles: [NPM_PACKAGE_CLI],
99 }),
100 ),
101 ).toBe(NPM_PACKAGE_CLI);
102 }),
103 );
104
105 it.effect("returns the configured path when a shim has no known package entry", () =>
106 Effect.gen(function* () {
107 expect(
108 yield* resolveClaudeSdkExecutablePath("claude", {}).pipe(
109 withWindowsResolution({ resolvedCommand: NPM_SHIM }),
110 ),
111 ).toBe("claude");
112 }),
113 );
114
115 it.effect("returns the configured path when command resolution finds nothing", () =>
116 Effect.gen(function* () {
117 expect(
118 yield* resolveClaudeSdkExecutablePath("claude", {}).pipe(
119 withWindowsResolution({ resolvedCommand: undefined }),
120 ),
121 ).toBe("claude");
122 }),
123 );
124});