| ... | @@ -45,6 +45,8 @@ import * as Scope from "effect/Scope"; | ... | @@ -45,6 +45,8 @@ import * as Scope from "effect/Scope"; |
| 45 | import * as Stream from "effect/Stream"; | 45 | import * as Stream from "effect/Stream"; |
| 46 | import { Tool } from "effect/unstable/ai"; | 46 | import { Tool } from "effect/unstable/ai"; |
| 47 | import { formatClaudeResumeCompactionQuestion } from "@t3tools/shared/claudeCompaction"; | 47 | import { formatClaudeResumeCompactionQuestion } from "@t3tools/shared/claudeCompaction"; |
| | 48 | import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; |
| | 49 | import { SpawnExecutableResolution } from "@t3tools/shared/shell"; |
| 48 | | 50 | |
| 49 | import { attachmentRelativePath } from "../../attachmentStore.ts"; | 51 | import { attachmentRelativePath } from "../../attachmentStore.ts"; |
| 50 | import * as ServerConfig from "../../config.ts"; | 52 | import * as ServerConfig from "../../config.ts"; |
| ... | @@ -55,6 +57,7 @@ import { ProjectToolkit } from "../../mcp/toolkits/project/tools.ts"; | ... | @@ -55,6 +57,7 @@ import { ProjectToolkit } from "../../mcp/toolkits/project/tools.ts"; |
| 55 | import { WorktreeToolkit } from "../../mcp/toolkits/worktree/tools.ts"; | 57 | import { WorktreeToolkit } from "../../mcp/toolkits/worktree/tools.ts"; |
| 56 | import { ThreadToolkit } from "../../mcp/toolkits/thread/tools.ts"; | 58 | import { ThreadToolkit } from "../../mcp/toolkits/thread/tools.ts"; |
| 57 | import { OrchestratorToolkit } from "../../mcp/toolkits/orchestrator/tools.ts"; | 59 | import { OrchestratorToolkit } from "../../mcp/toolkits/orchestrator/tools.ts"; |
| | 60 | import { ClaudeExecutableFileCheck } from "../../provider/Drivers/ClaudeExecutable.ts"; |
| 58 | import type { EventNdjsonLogger } from "../../provider/Layers/EventNdjsonLogger.ts"; | 61 | import type { EventNdjsonLogger } from "../../provider/Layers/EventNdjsonLogger.ts"; |
| 59 | import { | 62 | import { |
| 60 | ProviderAdapterV2RuntimePolicy, | 63 | ProviderAdapterV2RuntimePolicy, |
| ... | @@ -1064,72 +1067,97 @@ describe("ClaudeAdapterV2 approval cancellation", () => { | ... | @@ -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 | describe("ClaudeAdapterV2 executable path", () => { | 1134 | describe("ClaudeAdapterV2 executable path", () => { |
| 1068 | it.effect("expands ~ in the configured binary path for the SDK", () => | 1135 | it.effect("expands ~ in the configured binary path for the SDK", () => |
| 1069 | Effect.scoped( | 1136 | Effect.scoped( |
| 1070 | Effect.gen(function* () { | 1137 | Effect.gen(function* () { |
| 1071 | const path = yield* Path.Path; | 1138 | const path = yield* Path.Path; |
| 1072 | const executablePaths: Array<string | undefined> = []; | 1139 | const executablePaths = yield* captureSdkExecutablePaths("~/bin/claude"); |
| 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 | ); | | |
| 1128 | | 1140 | |
| 1129 | assert.deepEqual(executablePaths, [path.join(NodeOS.homedir(), "bin", "claude")]); | 1141 | assert.deepEqual(executablePaths, [path.join(NodeOS.homedir(), "bin", "claude")]); |
| 1130 | }), | 1142 | }), |
| 1131 | ).pipe(Effect.provide(Layer.merge(IdAllocator.layer, NodeServices.layer))), | 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 | describe("ClaudeAdapterV2 resume compaction", () => { | 1163 | describe("ClaudeAdapterV2 resume compaction", () => { |