refactor(claude-agent): inject binary resolver via client constructor
The session previously called resolveClaudeBinary() directly in buildOptions(), with no seam for tests. CI ran without claude on PATH, so every test that exercised buildOptions hit the production throw. Apply the ports-and-adapters pattern at the client boundary (the same place tests already configure queryFactory): - ClaudeAgentClient takes optional resolveBinary, defaults to resolveClaudeBinary which throws "Claude binary not found..." when the executable is missing — preserving production fail-fast. - Client passes resolveBinary through to ClaudeAgentSession options. - buildOptions calls this.resolveBinary() instead of importing the free function directly. - Tests construct the public client with a stub resolver. No __claudeAgentInternals export, no reaching into the session. User-facing fail-fast still lives where it always did — AgentManager gates start/resume/draft flows on client.isAvailable(). The throw in resolveClaudeBinary is defense-in-depth that mirrors Codex.
This commit is contained in:
@@ -66,6 +66,7 @@ describe("Claude SDK env", () => {
|
||||
const client = new ClaudeAgentClient({
|
||||
logger: createTestLogger(),
|
||||
queryFactory,
|
||||
resolveBinary: async () => "/test/claude/bin",
|
||||
});
|
||||
const session = await client.createSession(
|
||||
{
|
||||
@@ -123,6 +124,7 @@ describe("Claude SDK env", () => {
|
||||
const client = new ClaudeAgentClient({
|
||||
logger: createTestLogger(),
|
||||
queryFactory,
|
||||
resolveBinary: async () => "/test/claude/bin",
|
||||
});
|
||||
const session = await client.resumeSession(
|
||||
{
|
||||
|
||||
+7
-7
@@ -261,7 +261,7 @@ test("interrupt only calls query.interrupt and leaves the query open", async ()
|
||||
return scriptedQuery;
|
||||
});
|
||||
|
||||
const client = new ClaudeAgentClient({ logger });
|
||||
const client = new ClaudeAgentClient({ logger, resolveBinary: async () => "/test/claude/bin" });
|
||||
const session = await client.createSession({
|
||||
provider: "claude",
|
||||
cwd: process.cwd(),
|
||||
@@ -311,7 +311,7 @@ test("reuses the existing query after interrupt before starting the next prompt"
|
||||
return scriptedQuery;
|
||||
});
|
||||
|
||||
const client = new ClaudeAgentClient({ logger });
|
||||
const client = new ClaudeAgentClient({ logger, resolveBinary: async () => "/test/claude/bin" });
|
||||
const session = await client.createSession({
|
||||
provider: "claude",
|
||||
cwd: process.cwd(),
|
||||
@@ -361,7 +361,7 @@ test("emits an assistant system notice when Claude changes session id mid-turn",
|
||||
return queryRef;
|
||||
});
|
||||
|
||||
const client = new ClaudeAgentClient({ logger });
|
||||
const client = new ClaudeAgentClient({ logger, resolveBinary: async () => "/test/claude/bin" });
|
||||
const session = await client.createSession({
|
||||
provider: "claude",
|
||||
cwd: process.cwd(),
|
||||
@@ -455,7 +455,7 @@ test("recovers when the query pump sees a single interrupt abort before the next
|
||||
return scriptedQuery;
|
||||
});
|
||||
|
||||
const client = new ClaudeAgentClient({ logger });
|
||||
const client = new ClaudeAgentClient({ logger, resolveBinary: async () => "/test/claude/bin" });
|
||||
const session = await client.createSession({
|
||||
provider: "claude",
|
||||
cwd: process.cwd(),
|
||||
@@ -488,7 +488,7 @@ test("stale abort result after replacement start does not poison the new foregro
|
||||
return queryRef;
|
||||
});
|
||||
|
||||
const client = new ClaudeAgentClient({ logger });
|
||||
const client = new ClaudeAgentClient({ logger, resolveBinary: async () => "/test/claude/bin" });
|
||||
const session = await client.createSession({
|
||||
provider: "claude",
|
||||
cwd: process.cwd(),
|
||||
@@ -564,7 +564,7 @@ test("creates an autonomous live turn when assistant output arrives without a fo
|
||||
return queryRef;
|
||||
});
|
||||
|
||||
const client = new ClaudeAgentClient({ logger });
|
||||
const client = new ClaudeAgentClient({ logger, resolveBinary: async () => "/test/claude/bin" });
|
||||
const session = await client.createSession({
|
||||
provider: "claude",
|
||||
cwd: process.cwd(),
|
||||
@@ -634,7 +634,7 @@ test("auto-completes an open autonomous turn when a foreground prompt starts", a
|
||||
return queryRef;
|
||||
});
|
||||
|
||||
const client = new ClaudeAgentClient({ logger });
|
||||
const client = new ClaudeAgentClient({ logger, resolveBinary: async () => "/test/claude/bin" });
|
||||
const session = await client.createSession({
|
||||
provider: "claude",
|
||||
cwd: process.cwd(),
|
||||
|
||||
@@ -66,6 +66,7 @@ async function createSession() {
|
||||
const client = new ClaudeAgentClient({
|
||||
logger: createTestLogger(),
|
||||
queryFactory: sdkQueryFactory,
|
||||
resolveBinary: async () => "/test/claude/bin",
|
||||
});
|
||||
return client.createSession({
|
||||
provider: "claude",
|
||||
@@ -77,6 +78,7 @@ function createSessionWithLogger(logger: Logger) {
|
||||
const client = new ClaudeAgentClient({
|
||||
logger,
|
||||
queryFactory: sdkQueryFactory,
|
||||
resolveBinary: async () => "/test/claude/bin",
|
||||
});
|
||||
return client.createSession({
|
||||
provider: "claude",
|
||||
@@ -223,6 +225,7 @@ test("logs redacted query summary and never leaks sentinel secrets", async () =>
|
||||
PASEO_RUNTIME_SENTINEL_SECRET: runtimeSecret,
|
||||
},
|
||||
},
|
||||
resolveBinary: async () => "/test/claude/bin",
|
||||
});
|
||||
const session = await client.createSession({
|
||||
provider: "claude",
|
||||
@@ -797,6 +800,7 @@ test("captures Claude stderr in the turn failure diagnostic when stderr arrives
|
||||
const client = new ClaudeAgentClient({
|
||||
logger: loggerSpy.logger,
|
||||
queryFactory: sdkQueryFactory,
|
||||
resolveBinary: async () => "/test/claude/bin",
|
||||
});
|
||||
const session = await client.createSession({
|
||||
provider: "claude",
|
||||
|
||||
@@ -77,6 +77,7 @@ describe("Claude spawn override", () => {
|
||||
const client = new ClaudeAgentClient({
|
||||
logger: createTestLogger(),
|
||||
queryFactory,
|
||||
resolveBinary: async () => "/test/claude/bin",
|
||||
});
|
||||
const session = await client.createSession({
|
||||
provider: "claude",
|
||||
|
||||
@@ -251,7 +251,10 @@ describe("ClaudeAgentSession sub-agent sidechain updates", () => {
|
||||
});
|
||||
|
||||
test("accumulates lightweight sub_agent detail and preserves callId lifecycle collapse", async () => {
|
||||
const session = await new ClaudeAgentClient({ logger }).createSession({
|
||||
const session = await new ClaudeAgentClient({
|
||||
logger,
|
||||
resolveBinary: async () => "/test/claude/bin",
|
||||
}).createSession({
|
||||
provider: "claude",
|
||||
cwd: process.cwd(),
|
||||
});
|
||||
@@ -300,7 +303,10 @@ describe("ClaudeAgentSession sub-agent sidechain updates", () => {
|
||||
test("tails sub-agent actions instead of dropping latest entries at cap", async () => {
|
||||
sdkMocks.query.mockImplementation(() => buildQueryMock(buildTailScenarioEvents(205)));
|
||||
|
||||
const session = await new ClaudeAgentClient({ logger }).createSession({
|
||||
const session = await new ClaudeAgentClient({
|
||||
logger,
|
||||
resolveBinary: async () => "/test/claude/bin",
|
||||
}).createSession({
|
||||
provider: "claude",
|
||||
cwd: process.cwd(),
|
||||
});
|
||||
|
||||
@@ -346,7 +346,7 @@ describe("ClaudeAgentClient.listModels", () => {
|
||||
const logger = createTestLogger();
|
||||
|
||||
test("returns hardcoded claude models", async () => {
|
||||
const client = new ClaudeAgentClient({ logger });
|
||||
const client = new ClaudeAgentClient({ logger, resolveBinary: async () => "/test/claude/bin" });
|
||||
const models = await client.listModels({ cwd: "/tmp/claude-models", force: false });
|
||||
|
||||
expect(models.map((m) => m.id)).toEqual([
|
||||
@@ -430,7 +430,10 @@ describe("normalizeClaudeAskUserQuestionUpdatedInput", () => {
|
||||
});
|
||||
|
||||
test("respondToPermission preserves full question input when UI returns answers-only payload", async () => {
|
||||
const client = new ClaudeAgentClient({ logger: createTestLogger() });
|
||||
const client = new ClaudeAgentClient({
|
||||
logger: createTestLogger(),
|
||||
resolveBinary: async () => "/test/claude/bin",
|
||||
});
|
||||
const session = await client.createSession({
|
||||
provider: "claude",
|
||||
cwd: process.cwd(),
|
||||
@@ -505,7 +508,7 @@ describe("ClaudeAgentSession context window usage", () => {
|
||||
const logger = createTestLogger();
|
||||
|
||||
async function createSessionForTest(): Promise<TestClaudeSession> {
|
||||
const client = new ClaudeAgentClient({ logger });
|
||||
const client = new ClaudeAgentClient({ logger, resolveBinary: async () => "/test/claude/bin" });
|
||||
const session = await client.createSession({
|
||||
provider: "claude",
|
||||
cwd: process.cwd(),
|
||||
@@ -605,6 +608,7 @@ describe("ClaudeAgentSession context window usage", () => {
|
||||
const nonPersistedClient = new ClaudeAgentClient({
|
||||
logger,
|
||||
queryFactory: nonPersistedQueryFactory,
|
||||
resolveBinary: async () => "/test/claude/bin",
|
||||
});
|
||||
const nonPersistedSession = await nonPersistedClient.createSession(
|
||||
{
|
||||
@@ -623,6 +627,7 @@ describe("ClaudeAgentSession context window usage", () => {
|
||||
const persistedClient = new ClaudeAgentClient({
|
||||
logger,
|
||||
queryFactory: persistedQueryFactory,
|
||||
resolveBinary: async () => "/test/claude/bin",
|
||||
});
|
||||
const persistedSession = await persistedClient.createSession(
|
||||
{
|
||||
@@ -980,7 +985,11 @@ describe("ClaudeAgentSession context window usage", () => {
|
||||
},
|
||||
],
|
||||
]);
|
||||
const client = new ClaudeAgentClient({ logger, queryFactory });
|
||||
const client = new ClaudeAgentClient({
|
||||
logger,
|
||||
queryFactory,
|
||||
resolveBinary: async () => "/test/claude/bin",
|
||||
});
|
||||
const session = await client.createSession({
|
||||
provider: "claude",
|
||||
cwd: process.cwd(),
|
||||
|
||||
@@ -254,6 +254,7 @@ interface ClaudeAgentClientOptions {
|
||||
logger: Logger;
|
||||
runtimeSettings?: ProviderRuntimeSettings;
|
||||
queryFactory?: typeof query;
|
||||
resolveBinary?: () => Promise<string>;
|
||||
}
|
||||
|
||||
interface ClaudeAgentSessionOptions {
|
||||
@@ -264,6 +265,7 @@ interface ClaudeAgentSessionOptions {
|
||||
persistSession?: boolean;
|
||||
logger: Logger;
|
||||
queryFactory?: typeof query;
|
||||
resolveBinary: () => Promise<string>;
|
||||
}
|
||||
|
||||
type ClaudeThinkingEffort = "low" | "medium" | "high" | "xhigh" | "max";
|
||||
@@ -1251,12 +1253,14 @@ export class ClaudeAgentClient implements AgentClient {
|
||||
private readonly logger: Logger;
|
||||
private readonly runtimeSettings?: ProviderRuntimeSettings;
|
||||
private readonly queryFactory: typeof query;
|
||||
private readonly resolveBinary: () => Promise<string>;
|
||||
|
||||
constructor(options: ClaudeAgentClientOptions) {
|
||||
this.defaults = options.defaults;
|
||||
this.logger = options.logger.child({ module: "agent", provider: "claude" });
|
||||
this.runtimeSettings = options.runtimeSettings;
|
||||
this.queryFactory = options.queryFactory ?? query;
|
||||
this.resolveBinary = options.resolveBinary ?? resolveClaudeBinary;
|
||||
}
|
||||
|
||||
async createSession(
|
||||
@@ -1272,6 +1276,7 @@ export class ClaudeAgentClient implements AgentClient {
|
||||
persistSession: options?.persistSession,
|
||||
logger: this.logger,
|
||||
queryFactory: this.queryFactory,
|
||||
resolveBinary: this.resolveBinary,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1298,6 +1303,7 @@ export class ClaudeAgentClient implements AgentClient {
|
||||
launchEnv: launchContext?.env,
|
||||
logger: this.logger,
|
||||
queryFactory: this.queryFactory,
|
||||
resolveBinary: this.resolveBinary,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1381,6 +1387,16 @@ export class ClaudeAgentClient implements AgentClient {
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveClaudeBinary(): Promise<string> {
|
||||
const found = await findExecutable("claude");
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
throw new Error(
|
||||
"Claude binary not found. Install Claude Code (https://github.com/anthropics/claude-code) and ensure it is available in your shell PATH.",
|
||||
);
|
||||
}
|
||||
|
||||
async function resolveClaudeVersion(
|
||||
runtimeSettings?: ProviderRuntimeSettings,
|
||||
): Promise<string | null> {
|
||||
@@ -1549,6 +1565,7 @@ class ClaudeAgentSession implements AgentSession {
|
||||
private readonly persistSession?: boolean;
|
||||
private readonly logger: Logger;
|
||||
private readonly queryFactory: typeof query;
|
||||
private readonly resolveBinary: () => Promise<string>;
|
||||
private query: Query | null = null;
|
||||
private input: AsyncMessageInput<SDKUserMessage> | null = null;
|
||||
private claudeSessionId: string | null;
|
||||
@@ -1597,6 +1614,7 @@ class ClaudeAgentSession implements AgentSession {
|
||||
this.persistSession = options.persistSession;
|
||||
this.logger = options.logger;
|
||||
this.queryFactory = options.queryFactory ?? query;
|
||||
this.resolveBinary = options.resolveBinary;
|
||||
const handle = options.handle;
|
||||
|
||||
if (handle) {
|
||||
@@ -2283,7 +2301,7 @@ class ClaudeAgentSession implements AgentSession {
|
||||
],
|
||||
});
|
||||
|
||||
const claudeBinary = await findExecutable("claude");
|
||||
const claudeBinary = await this.resolveBinary();
|
||||
this.logger.debug(
|
||||
{
|
||||
claudeBinary,
|
||||
@@ -2304,7 +2322,7 @@ class ClaudeAgentSession implements AgentSession {
|
||||
allowDangerouslySkipPermissions: true,
|
||||
agents: this.defaults?.agents,
|
||||
canUseTool: this.handlePermissionRequest,
|
||||
...(claudeBinary ? { pathToClaudeCodeExecutable: claudeBinary } : {}),
|
||||
pathToClaudeCodeExecutable: claudeBinary,
|
||||
// Use Claude Code preset system prompt and load CLAUDE.md files
|
||||
// Append provider-agnostic system prompt and orchestrator instructions for agents.
|
||||
systemPrompt: {
|
||||
|
||||
+4
-4
@@ -157,7 +157,7 @@ describe("ClaudeAgentSession history replay regression", () => {
|
||||
|
||||
test("does not replay persisted history during the first live stream turn", async () => {
|
||||
const logger = createTestLogger();
|
||||
const client = new ClaudeAgentClient({ logger });
|
||||
const client = new ClaudeAgentClient({ logger, resolveBinary: async () => "/test/claude/bin" });
|
||||
const handle: AgentPersistenceHandle = {
|
||||
provider: "claude",
|
||||
sessionId: "history-session",
|
||||
@@ -194,7 +194,7 @@ describe("ClaudeAgentSession history replay regression", () => {
|
||||
|
||||
test("still exposes persisted history through streamHistory", async () => {
|
||||
const logger = createTestLogger();
|
||||
const client = new ClaudeAgentClient({ logger });
|
||||
const client = new ClaudeAgentClient({ logger, resolveBinary: async () => "/test/claude/bin" });
|
||||
const handle: AgentPersistenceHandle = {
|
||||
provider: "claude",
|
||||
sessionId: "history-session",
|
||||
@@ -223,7 +223,7 @@ describe("ClaudeAgentSession history replay regression", () => {
|
||||
|
||||
test("listCommands includes rewind command", async () => {
|
||||
const logger = createTestLogger();
|
||||
const client = new ClaudeAgentClient({ logger });
|
||||
const client = new ClaudeAgentClient({ logger, resolveBinary: async () => "/test/claude/bin" });
|
||||
const handle: AgentPersistenceHandle = {
|
||||
provider: "claude",
|
||||
sessionId: "history-session",
|
||||
@@ -245,7 +245,7 @@ describe("ClaudeAgentSession history replay regression", () => {
|
||||
|
||||
test("slash /rewind uses latest user message id from persisted history", async () => {
|
||||
const logger = createTestLogger();
|
||||
const client = new ClaudeAgentClient({ logger });
|
||||
const client = new ClaudeAgentClient({ logger, resolveBinary: async () => "/test/claude/bin" });
|
||||
const handle: AgentPersistenceHandle = {
|
||||
provider: "claude",
|
||||
sessionId: "history-session",
|
||||
|
||||
Reference in New Issue
Block a user