| ... | ... | @@ -45,6 +45,8 @@ import * as Scope from "effect/Scope"; |
| 45 | 45 | import * as Stream from "effect/Stream"; |
| 46 | 46 | import { Tool } from "effect/unstable/ai"; |
| 47 | 47 | import { formatClaudeResumeCompactionQuestion } from "@t3tools/shared/claudeCompaction"; |
| 48 | import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; |
| 49 | import { SpawnExecutableResolution } from "@t3tools/shared/shell"; |
| 48 | 50 | |
| 49 | 51 | import { attachmentRelativePath } from "../../attachmentStore.ts"; |
| 50 | 52 | import * as ServerConfig from "../../config.ts"; |
| ... | ... | @@ -55,6 +57,7 @@ import { ProjectToolkit } from "../../mcp/toolkits/project/tools.ts"; |
| 55 | 57 | import { WorktreeToolkit } from "../../mcp/toolkits/worktree/tools.ts"; |
| 56 | 58 | import { ThreadToolkit } from "../../mcp/toolkits/thread/tools.ts"; |
| 57 | 59 | import { OrchestratorToolkit } from "../../mcp/toolkits/orchestrator/tools.ts"; |
| 60 | import { ClaudeExecutableFileCheck } from "../../provider/Drivers/ClaudeExecutable.ts"; |
| 58 | 61 | import type { EventNdjsonLogger } from "../../provider/Layers/EventNdjsonLogger.ts"; |
| 59 | 62 | import { |
| 60 | 63 | ProviderAdapterV2RuntimePolicy, |
| ... | ... | @@ -1064,72 +1067,97 @@ describe("ClaudeAdapterV2 approval cancellation", () => { |
| 1064 | 1067 | ); |
| 1065 | 1068 | }); |
| 1066 | 1069 | |
| 1070 | // Opens a session with the given configured binary path, runs one turn, and |
| 1071 | // returns the executable paths the SDK was asked to spawn. |
| 1072 | const captureSdkExecutablePaths = Effect.fn("captureSdkExecutablePaths")(function* ( |
| 1073 | binaryPath: string, |
| 1074 | ) { |
| 1075 | const executablePaths: Array<string | undefined> = []; |
| 1076 | const adapter = yield* ClaudeAdapterV2.createClaudeAdapterV2( |
| 1077 | { |
| 1078 | instanceId: ClaudeAdapterV2.CLAUDE_DEFAULT_INSTANCE_ID, |
| 1079 | displayName: undefined, |
| 1080 | environment: [], |
| 1081 | enabled: true, |
| 1082 | config: { ...DEFAULT_CLAUDE_SETTINGS, binaryPath }, |
| 1083 | }, |
| 1084 | {}, |
| 1085 | ).pipe( |
| 1086 | Effect.provide( |
| 1087 | ServerConfig.layerTest(process.cwd(), { |
| 1088 | prefix: "t3-claude-binary-path-", |
| 1089 | }), |
| 1090 | ), |
| 1091 | Effect.provideService(ClaudeAdapterV2.ClaudeAgentSdkQueryRunner, { |
| 1092 | allocateSessionId: Effect.succeed("native-thread-claude-binary-path"), |
| 1093 | open: (input) => |
| 1094 | Effect.sync(() => { |
| 1095 | executablePaths.push(input.options.pathToClaudeCodeExecutable); |
| 1096 | return { |
| 1097 | messages: Stream.never, |
| 1098 | offer: () => Effect.void, |
| 1099 | setModel: () => Effect.void, |
| 1100 | interrupt: Effect.void, |
| 1101 | close: Effect.void, |
| 1102 | }; |
| 1103 | }), |
| 1104 | forkSession: () => Effect.die("unused"), |
| 1105 | subagentLaunchToolUseId: () => Effect.succeed(null), |
| 1106 | assertComplete: Effect.void, |
| 1107 | }), |
| 1108 | ); |
| 1109 | const threadId = ThreadId.make("thread-claude-binary-path"); |
| 1110 | const runtime = yield* adapter.openSession({ |
| 1111 | threadId, |
| 1112 | providerSessionId: ProviderSessionId.make("provider-session-claude-binary-path"), |
| 1113 | modelSelection: CLAUDE_TEST_MODEL_SELECTION, |
| 1114 | runtimePolicy: CLAUDE_TEST_RUNTIME_POLICY, |
| 1115 | }); |
| 1116 | const providerThread = yield* runtime.ensureThread({ |
| 1117 | threadId, |
| 1118 | modelSelection: CLAUDE_TEST_MODEL_SELECTION, |
| 1119 | runtimePolicy: CLAUDE_TEST_RUNTIME_POLICY, |
| 1120 | }); |
| 1121 | yield* runtime.startTurn( |
| 1122 | makeClaudeTestTurnInput({ |
| 1123 | threadId, |
| 1124 | providerThread, |
| 1125 | now: yield* DateTime.now, |
| 1126 | attemptId: RunAttemptId.make("attempt-claude-binary-path"), |
| 1127 | text: "hello", |
| 1128 | attachments: [], |
| 1129 | }), |
| 1130 | ); |
| 1131 | return executablePaths; |
| 1132 | }); |
| 1133 | |
| 1067 | 1134 | describe("ClaudeAdapterV2 executable path", () => { |
| 1068 | 1135 | it.effect("expands ~ in the configured binary path for the SDK", () => |
| 1069 | 1136 | Effect.scoped( |
| 1070 | 1137 | Effect.gen(function* () { |
| 1071 | 1138 | const path = yield* Path.Path; |
| 1072 | | const executablePaths: Array<string | undefined> = []; |
| 1073 | | const adapter = yield* ClaudeAdapterV2.createClaudeAdapterV2( |
| 1074 | | { |
| 1075 | | instanceId: ClaudeAdapterV2.CLAUDE_DEFAULT_INSTANCE_ID, |
| 1076 | | displayName: undefined, |
| 1077 | | environment: [], |
| 1078 | | enabled: true, |
| 1079 | | config: { ...DEFAULT_CLAUDE_SETTINGS, binaryPath: "~/bin/claude" }, |
| 1080 | | }, |
| 1081 | | {}, |
| 1082 | | ).pipe( |
| 1083 | | Effect.provide( |
| 1084 | | ServerConfig.layerTest(process.cwd(), { |
| 1085 | | prefix: "t3-claude-binary-home-", |
| 1086 | | }), |
| 1087 | | ), |
| 1088 | | Effect.provideService(ClaudeAdapterV2.ClaudeAgentSdkQueryRunner, { |
| 1089 | | allocateSessionId: Effect.succeed("native-thread-claude-binary-home"), |
| 1090 | | open: (input) => |
| 1091 | | Effect.sync(() => { |
| 1092 | | executablePaths.push(input.options.pathToClaudeCodeExecutable); |
| 1093 | | return { |
| 1094 | | messages: Stream.never, |
| 1095 | | offer: () => Effect.void, |
| 1096 | | setModel: () => Effect.void, |
| 1097 | | interrupt: Effect.void, |
| 1098 | | close: Effect.void, |
| 1099 | | }; |
| 1100 | | }), |
| 1101 | | forkSession: () => Effect.die("unused"), |
| 1102 | | subagentLaunchToolUseId: () => Effect.succeed(null), |
| 1103 | | assertComplete: Effect.void, |
| 1104 | | }), |
| 1105 | | ); |
| 1106 | | const threadId = ThreadId.make("thread-claude-binary-home"); |
| 1107 | | const runtime = yield* adapter.openSession({ |
| 1108 | | threadId, |
| 1109 | | providerSessionId: ProviderSessionId.make("provider-session-claude-binary-home"), |
| 1110 | | modelSelection: CLAUDE_TEST_MODEL_SELECTION, |
| 1111 | | runtimePolicy: CLAUDE_TEST_RUNTIME_POLICY, |
| 1112 | | }); |
| 1113 | | const providerThread = yield* runtime.ensureThread({ |
| 1114 | | threadId, |
| 1115 | | modelSelection: CLAUDE_TEST_MODEL_SELECTION, |
| 1116 | | runtimePolicy: CLAUDE_TEST_RUNTIME_POLICY, |
| 1117 | | }); |
| 1118 | | yield* runtime.startTurn( |
| 1119 | | makeClaudeTestTurnInput({ |
| 1120 | | threadId, |
| 1121 | | providerThread, |
| 1122 | | now: yield* DateTime.now, |
| 1123 | | attemptId: RunAttemptId.make("attempt-claude-binary-home"), |
| 1124 | | text: "hello", |
| 1125 | | attachments: [], |
| 1126 | | }), |
| 1127 | | ); |
| 1139 | const executablePaths = yield* captureSdkExecutablePaths("~/bin/claude"); |
| 1128 | 1140 | |
| 1129 | 1141 | assert.deepEqual(executablePaths, [path.join(NodeOS.homedir(), "bin", "claude")]); |
| 1130 | 1142 | }), |
| 1131 | 1143 | ).pipe(Effect.provide(Layer.merge(IdAllocator.layer, NodeServices.layer))), |
| 1132 | 1144 | ); |
| 1145 | |
| 1146 | it.effect("follows a bare claude on Windows to the npm package executable", () => |
| 1147 | Effect.scoped( |
| 1148 | Effect.gen(function* () { |
| 1149 | const npmDir = "C:\\Users\\dev\\AppData\\Roaming\\npm"; |
| 1150 | const packageExe = `${npmDir}\\node_modules\\@anthropic-ai\\claude-code\\bin\\claude.exe`; |
| 1151 | const executablePaths = yield* captureSdkExecutablePaths("claude").pipe( |
| 1152 | Effect.provideService(HostProcessPlatform, "win32"), |
| 1153 | Effect.provideService(SpawnExecutableResolution, () => `${npmDir}\\claude.cmd`), |
| 1154 | Effect.provideService(ClaudeExecutableFileCheck, (filePath) => filePath === packageExe), |
| 1155 | ); |
| 1156 | |
| 1157 | assert.deepEqual(executablePaths, [packageExe]); |
| 1158 | }), |
| 1159 | ).pipe(Effect.provide(Layer.merge(IdAllocator.layer, NodeServices.layer))), |
| 1160 | ); |
| 1133 | 1161 | }); |
| 1134 | 1162 | |
| 1135 | 1163 | describe("ClaudeAdapterV2 resume compaction", () => { |