Fix Claude binary resolution for replace-mode command override
isAvailable() honored runtimeSettings.command.mode === "replace" but resolveClaudeBinary() only looked for `claude` on PATH. Users with a custom claude wrapper and no `claude` itself on PATH passed the availability check and then failed at session creation with "Claude binary not found". resolveClaudeBinary now mirrors isAvailable: if the runtime command is in replace mode, look up the override binary first.
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
import { describe, expect, test, vi } from "vitest";
|
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||||
import type { SDKMessage } from "@anthropic-ai/claude-agent-sdk";
|
import type { SDKMessage } from "@anthropic-ai/claude-agent-sdk";
|
||||||
|
|
||||||
import { createTestLogger } from "../../../../test-utils/test-logger.js";
|
import { createTestLogger } from "../../../../test-utils/test-logger.js";
|
||||||
|
import * as executableUtils from "../../../../utils/executable.js";
|
||||||
import {
|
import {
|
||||||
ClaudeAgentClient,
|
ClaudeAgentClient,
|
||||||
convertClaudeHistoryEntry,
|
convertClaudeHistoryEntry,
|
||||||
@@ -14,6 +15,10 @@ interface TestClaudeSession {
|
|||||||
convertUsage(message: SDKMessage): AgentUsage | undefined;
|
convertUsage(message: SDKMessage): AgentUsage | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
describe("convertClaudeHistoryEntry", () => {
|
describe("convertClaudeHistoryEntry", () => {
|
||||||
test("maps user tool results to timeline items", () => {
|
test("maps user tool results to timeline items", () => {
|
||||||
const toolUseId = "toolu_test";
|
const toolUseId = "toolu_test";
|
||||||
@@ -369,6 +374,59 @@ describe("ClaudeAgentClient.listModels", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("ClaudeAgentClient binary resolution", () => {
|
||||||
|
const logger = createTestLogger();
|
||||||
|
|
||||||
|
test("uses the replace-command override binary when claude is not on PATH", async () => {
|
||||||
|
const customClaudePath = "/path/to/custom-claude";
|
||||||
|
vi.spyOn(executableUtils, "findExecutable").mockImplementation(async (name: string) => {
|
||||||
|
if (name === "claude") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (name === customClaudePath) {
|
||||||
|
return customClaudePath;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const queryReturn = vi.fn();
|
||||||
|
queryReturn.mockResolvedValue(undefined);
|
||||||
|
const queryFactory = vi.fn(() => ({
|
||||||
|
close: vi.fn(),
|
||||||
|
return: queryReturn,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const client = new ClaudeAgentClient({
|
||||||
|
logger,
|
||||||
|
queryFactory,
|
||||||
|
runtimeSettings: {
|
||||||
|
command: {
|
||||||
|
mode: "replace",
|
||||||
|
argv: [customClaudePath],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const session = await client.createSession({
|
||||||
|
provider: "claude",
|
||||||
|
cwd: process.cwd(),
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
(
|
||||||
|
session as unknown as {
|
||||||
|
ensureQuery(): Promise<unknown>;
|
||||||
|
}
|
||||||
|
).ensureQuery(),
|
||||||
|
).resolves.toBeDefined();
|
||||||
|
|
||||||
|
expect(queryFactory.mock.calls[0]?.[0].options.pathToClaudeCodeExecutable).toBe(
|
||||||
|
customClaudePath,
|
||||||
|
);
|
||||||
|
|
||||||
|
await session.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("normalizeClaudeAskUserQuestionUpdatedInput", () => {
|
describe("normalizeClaudeAskUserQuestionUpdatedInput", () => {
|
||||||
test("maps frontend header-keyed answers to Claude question text keys", () => {
|
test("maps frontend header-keyed answers to Claude question text keys", () => {
|
||||||
expect(
|
expect(
|
||||||
|
|||||||
@@ -1169,7 +1169,7 @@ export class ClaudeAgentClient implements AgentClient {
|
|||||||
this.logger = options.logger.child({ module: "agent", provider: "claude" });
|
this.logger = options.logger.child({ module: "agent", provider: "claude" });
|
||||||
this.runtimeSettings = options.runtimeSettings;
|
this.runtimeSettings = options.runtimeSettings;
|
||||||
this.queryFactory = options.queryFactory;
|
this.queryFactory = options.queryFactory;
|
||||||
this.resolveBinary = options.resolveBinary ?? resolveClaudeBinary;
|
this.resolveBinary = options.resolveBinary ?? (() => resolveClaudeBinary(this.runtimeSettings));
|
||||||
}
|
}
|
||||||
|
|
||||||
async createSession(
|
async createSession(
|
||||||
@@ -1296,7 +1296,15 @@ export class ClaudeAgentClient implements AgentClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolveClaudeBinary(): Promise<string> {
|
async function resolveClaudeBinary(runtimeSettings?: ProviderRuntimeSettings): Promise<string> {
|
||||||
|
const command = runtimeSettings?.command;
|
||||||
|
if (command?.mode === "replace") {
|
||||||
|
const foundOverride = await findExecutable(command.argv[0]);
|
||||||
|
if (foundOverride) {
|
||||||
|
return foundOverride;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const found = await findExecutable("claude");
|
const found = await findExecutable("claude");
|
||||||
if (found) {
|
if (found) {
|
||||||
return found;
|
return found;
|
||||||
|
|||||||
Reference in New Issue
Block a user