| ... | ... | @@ -0,0 +1,138 @@ |
| 1 | import { ClaudeSettings } from "@t3tools/contracts"; |
| 2 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 3 | import { assert, it } from "@effect/vitest"; |
| 4 | import * as Effect from "effect/Effect"; |
| 5 | import * as FileSystem from "effect/FileSystem"; |
| 6 | import * as Path from "effect/Path"; |
| 7 | import * as Schema from "effect/Schema"; |
| 8 | |
| 9 | import { |
| 10 | buildClaudeCapabilitiesProbeQueryOptions, |
| 11 | CLAUDE_CAPABILITIES_PROBE_SETTING_SOURCES, |
| 12 | probeClaudeCapabilities, |
| 13 | } from "./ClaudeProvider.ts"; |
| 14 | |
| 15 | const decodeClaudeSettings = Schema.decodeSync(ClaudeSettings); |
| 16 | |
| 17 | it("isolates Claude capability probes without dropping workspace setting sources", () => { |
| 18 | const abortController = new AbortController(); |
| 19 | const options = buildClaudeCapabilitiesProbeQueryOptions({ |
| 20 | executablePath: "/usr/bin/claude", |
| 21 | abortController, |
| 22 | environment: { |
| 23 | HOME: "/home/user", |
| 24 | ENABLE_CLAUDEAI_MCP_SERVERS: "true", |
| 25 | }, |
| 26 | cwd: "/workspace/project", |
| 27 | }); |
| 28 | |
| 29 | assert.deepEqual(options.mcpServers, {}); |
| 30 | assert.equal(options.strictMcpConfig, true); |
| 31 | assert.equal(options.cwd, "/workspace/project"); |
| 32 | assert.deepEqual(options.settingSources, [...CLAUDE_CAPABILITIES_PROBE_SETTING_SOURCES]); |
| 33 | assert.deepEqual(options.allowedTools, []); |
| 34 | assert.equal(options.persistSession, false); |
| 35 | assert.equal(options.pathToClaudeCodeExecutable, "/usr/bin/claude"); |
| 36 | assert.equal(options.abortController, abortController); |
| 37 | assert.equal(options.env?.HOME, "/home/user"); |
| 38 | assert.equal(options.env?.ENABLE_CLAUDEAI_MCP_SERVERS, "false"); |
| 39 | }); |
| 40 | |
| 41 | it.layer(NodeServices.layer)("Claude capability probe SDK boundary", (it) => { |
| 42 | it.effect("serializes strict no-MCP options and still resolves account capabilities", () => |
| 43 | Effect.gen(function* () { |
| 44 | const fs = yield* FileSystem.FileSystem; |
| 45 | const path = yield* Path.Path; |
| 46 | const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-claude-probe-sdk-" }); |
| 47 | const executablePath = path.join(tempDir, "fake-claude.mjs"); |
| 48 | const invocationPath = path.join(tempDir, "invocation.json"); |
| 49 | const workspaceCwd = path.join(tempDir, "workspace"); |
| 50 | yield* fs.makeDirectory(workspaceCwd, { recursive: true }); |
| 51 | |
| 52 | yield* fs.writeFileString( |
| 53 | executablePath, |
| 54 | [ |
| 55 | "#!/usr/bin/env node", |
| 56 | 'import { existsSync, readFileSync, writeFileSync } from "node:fs";', |
| 57 | 'import { createInterface } from "node:readline";', |
| 58 | "const args = process.argv.slice(2);", |
| 59 | 'const mcpConfigIndex = args.indexOf("--mcp-config");', |
| 60 | "const rawMcpConfig = mcpConfigIndex >= 0 ? args[mcpConfigIndex + 1] : undefined;", |
| 61 | "let mcpConfig;", |
| 62 | "if (rawMcpConfig) {", |
| 63 | ' const contents = existsSync(rawMcpConfig) ? readFileSync(rawMcpConfig, "utf8") : rawMcpConfig;', |
| 64 | " try { mcpConfig = JSON.parse(contents); } catch { mcpConfig = contents; }", |
| 65 | "}", |
| 66 | "writeFileSync(process.env.T3_PROBE_INVOCATION_PATH, JSON.stringify({", |
| 67 | " args,", |
| 68 | " cwd: process.cwd(),", |
| 69 | " connectorEnv: process.env.ENABLE_CLAUDEAI_MCP_SERVERS,", |
| 70 | " mcpConfig,", |
| 71 | "}));", |
| 72 | "const lines = createInterface({ input: process.stdin });", |
| 73 | 'lines.on("line", (line) => {', |
| 74 | " const message = JSON.parse(line);", |
| 75 | ' if (message.type !== "control_request" || message.request?.subtype !== "initialize") return;', |
| 76 | " process.stdout.write(JSON.stringify({", |
| 77 | ' type: "control_response",', |
| 78 | " response: {", |
| 79 | ' subtype: "success",', |
| 80 | " request_id: message.request_id,", |
| 81 | " response: {", |
| 82 | ' commands: [{ name: "review", description: "Review changes", argumentHint: "[path]" }],', |
| 83 | " agents: [],", |
| 84 | ' output_style: "default",', |
| 85 | ' available_output_styles: ["default"],', |
| 86 | " models: [],", |
| 87 | ' account: { email: "dev@example.com", subscriptionType: "pro", tokenSource: "oauth" },', |
| 88 | " },", |
| 89 | " },", |
| 90 | ' }) + "\\n");', |
| 91 | "});", |
| 92 | "setInterval(() => {}, 1_000);", |
| 93 | "", |
| 94 | ].join("\n"), |
| 95 | ); |
| 96 | yield* fs.chmod(executablePath, 0o755); |
| 97 | |
| 98 | const capabilities = yield* probeClaudeCapabilities( |
| 99 | decodeClaudeSettings({ binaryPath: executablePath }), |
| 100 | { |
| 101 | ...process.env, |
| 102 | T3_PROBE_INVOCATION_PATH: invocationPath, |
| 103 | ENABLE_CLAUDEAI_MCP_SERVERS: "true", |
| 104 | }, |
| 105 | workspaceCwd, |
| 106 | ); |
| 107 | |
| 108 | assert.deepEqual(capabilities, { |
| 109 | email: "dev@example.com", |
| 110 | subscriptionType: "pro", |
| 111 | tokenSource: "oauth", |
| 112 | apiProvider: undefined, |
| 113 | slashCommands: [ |
| 114 | { |
| 115 | name: "review", |
| 116 | description: "Review changes", |
| 117 | input: { hint: "[path]" }, |
| 118 | }, |
| 119 | ], |
| 120 | }); |
| 121 | |
| 122 | // @effect-diagnostics-next-line preferSchemaOverJson:off |
| 123 | const invocation = JSON.parse(yield* fs.readFileString(invocationPath)) as { |
| 124 | readonly args: ReadonlyArray<string>; |
| 125 | readonly cwd: string; |
| 126 | readonly connectorEnv: string; |
| 127 | readonly mcpConfig: unknown; |
| 128 | }; |
| 129 | assert.equal(invocation.cwd, yield* fs.realPath(workspaceCwd)); |
| 130 | assert.equal(invocation.connectorEnv, "false"); |
| 131 | assert.equal(invocation.args.includes("--strict-mcp-config"), true); |
| 132 | assert.equal(invocation.args.includes("--mcp-config"), false); |
| 133 | assert.equal(invocation.mcpConfig, undefined); |
| 134 | |
| 135 | assert.equal(invocation.args.includes("--setting-sources=user,project,local"), true); |
| 136 | }).pipe(Effect.scoped), |
| 137 | ); |
| 138 | }); |