diff --git a/packages/server/src/server/agent/agent-manager.test.ts b/packages/server/src/server/agent/agent-manager.test.ts index 149a14526..e22cabb44 100644 --- a/packages/server/src/server/agent/agent-manager.test.ts +++ b/packages/server/src/server/agent/agent-manager.test.ts @@ -3294,4 +3294,90 @@ describe("AgentManager", () => { "continuation prompt", ); }); + + test("replaceAgentRun succeeds when foreground turn terminal event is never delivered", async () => { + const workdir = mkdtempSync(join(tmpdir(), "agent-manager-stale-fg-")); + const storagePath = join(workdir, "agents"); + const storage = new AgentStorage(storagePath, logger); + const allowSecondRunToEnd = deferred(); + + // Session where the first foreground turn never emits a terminal event + // (simulates the claude-agent pendingInterruptAbort suppression bug), + // and interrupt() does not produce events either. + class StaleForegroundSession extends TestAgentSession { + override async startTurn(): Promise<{ turnId: string }> { + this.interrupted = false; + const turnId = `turn-${++this.turnIdCounter}`; + const turnNum = this.turnIdCounter; + + setTimeout(async () => { + this.pushEvent({ type: "turn_started", provider: this.provider, turnId }); + if (turnNum === 1) { + // First turn: emit turn_started but NEVER emit a terminal event. + // This simulates the provider suppressing the result. + } else { + // Subsequent turns: complete normally + await allowSecondRunToEnd.promise; + this.pushEvent({ type: "turn_completed", provider: this.provider, turnId }); + } + }, 0); + return { turnId }; + } + + override async interrupt(): Promise { + this.interrupted = true; + // No events produced — the terminal event was suppressed + } + } + + class StaleForegroundClient extends TestAgentClient { + override async createSession(config: AgentSessionConfig): Promise { + return new StaleForegroundSession(config); + } + } + + const manager = new AgentManager({ + clients: { codex: new StaleForegroundClient() }, + registry: storage, + logger, + idFactory: () => "00000000-0000-4000-8000-000000000500", + }); + + const snapshot = await manager.createAgent({ provider: "codex", cwd: workdir }); + + // Start first foreground run — it will hang (no terminal event) + const firstRun = manager.streamAgent(snapshot.id, "hanging prompt"); + const firstRunDrain = (async () => { + for await (const _event of firstRun) { + // Draining — will hang until force-cleaned + } + })(); + + await manager.waitForAgentRunStart(snapshot.id); + + const beforeReplace = manager.getAgent(snapshot.id); + expect(beforeReplace?.lifecycle).toBe("running"); + expect(beforeReplace?.activeForegroundTurnId).toBe("turn-1"); + + // Replace the hung run. cancelAgentRun will time out after 2s because + // no terminal event arrives. After the fix, it should force-clear the + // stale foreground state so streamAgent can proceed. + const secondRun = manager.replaceAgentRun(snapshot.id, "replacement prompt"); + const collectedEvents: AgentStreamEvent[] = []; + const secondRunDrain = (async () => { + for await (const event of secondRun) { + collectedEvents.push(event); + } + })(); + + await manager.waitForAgentRunStart(snapshot.id); + allowSecondRunToEnd.resolve(); + + await secondRunDrain; + await firstRunDrain; + + expect(collectedEvents.some((e) => e.type === "turn_completed")).toBe(true); + expect(manager.getAgent(snapshot.id)?.lifecycle).toBe("idle"); + expect(manager.getAgent(snapshot.id)?.activeForegroundTurnId).toBeNull(); + }, 10_000); }); diff --git a/packages/server/src/server/agent/agent-manager.ts b/packages/server/src/server/agent/agent-manager.ts index 4c72f5e75..5080cfb45 100644 --- a/packages/server/src/server/agent/agent-manager.ts +++ b/packages/server/src/server/agent/agent-manager.ts @@ -1388,6 +1388,28 @@ export class AgentManager { await Promise.race([pendingRun.settledPromise, timeout]); } + // If the foreground turn is still stuck after the timeout, force-dispatch a + // synthetic turn_canceled so the normal event pipeline cleans up + // activeForegroundTurnId, settles waiters, and unblocks the streamForwarder. + if (foregroundTurnId && agent.activeForegroundTurnId === foregroundTurnId) { + this.logger.warn( + { agentId, foregroundTurnId }, + "cancelAgentRun: foreground turn still active after timeout, force-canceling", + ); + this.dispatchSessionEvent(agent, { + type: "turn_canceled", + provider: agent.provider, + reason: "interrupted", + turnId: foregroundTurnId, + }); + // The synthetic event unblocks the streamForwarder generator, whose finally + // block settles the pending foreground run asynchronously. Wait for it. + const staleRun = this.getPendingForegroundRun(agentId); + if (staleRun && !staleRun.settled) { + await staleRun.settledPromise; + } + } + // Clear any pending permissions that weren't cleaned up by handleStreamEvent. if (agent.pendingPermissions.size > 0) { for (const [requestId] of agent.pendingPermissions) { diff --git a/packages/server/src/server/agent/provider-launch-config.test.ts b/packages/server/src/server/agent/provider-launch-config.test.ts index 505556ce7..0fc5715af 100644 --- a/packages/server/src/server/agent/provider-launch-config.test.ts +++ b/packages/server/src/server/agent/provider-launch-config.test.ts @@ -111,6 +111,26 @@ describe("applyProviderEnv", () => { expect(env.PATH).toBe("/custom/path"); }); + + test("strips parent Claude Code session env vars", () => { + const base = { + PATH: "/usr/bin", + CLAUDECODE: "1", + CLAUDE_CODE_ENTRYPOINT: "sdk-ts", + CLAUDE_CODE_SSE_PORT: "11803", + CLAUDE_AGENT_SDK_VERSION: "0.2.71", + CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING: "true", + }; + + const env = applyProviderEnv(base, undefined, {}); + + expect(env.PATH).toBe("/usr/bin"); + expect(env.CLAUDECODE).toBeUndefined(); + expect(env.CLAUDE_CODE_ENTRYPOINT).toBeUndefined(); + expect(env.CLAUDE_CODE_SSE_PORT).toBeUndefined(); + expect(env.CLAUDE_AGENT_SDK_VERSION).toBeUndefined(); + expect(env.CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING).toBeUndefined(); + }); }); describe("findExecutable", () => { diff --git a/packages/server/src/server/agent/provider-launch-config.ts b/packages/server/src/server/agent/provider-launch-config.ts index a66df62ed..5da27bf1d 100644 --- a/packages/server/src/server/agent/provider-launch-config.ts +++ b/packages/server/src/server/agent/provider-launch-config.ts @@ -125,16 +125,31 @@ export function resolveShellEnv(): Record { return cachedShellEnv; } +// Env vars that indicate a running Claude Code session. If the daemon itself is +// launched from inside Claude Code (e.g. by a Paseo agent), these leak into +// child processes and cause "cannot be launched inside another session" errors. +const PARENT_SESSION_ENV_VARS = [ + "CLAUDECODE", + "CLAUDE_CODE_ENTRYPOINT", + "CLAUDE_CODE_SSE_PORT", + "CLAUDE_AGENT_SDK_VERSION", + "CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING", +]; + export function applyProviderEnv( baseEnv: Record, runtimeSettings?: ProviderRuntimeSettings, shellEnv?: Record, ): Record { - return { + const merged: Record = { ...baseEnv, ...(shellEnv ?? resolveShellEnv()), ...(runtimeSettings?.env ?? {}), }; + for (const key of PARENT_SESSION_ENV_VARS) { + delete merged[key]; + } + return merged; } /** diff --git a/packages/server/src/server/daemon-e2e/agent-configs.ts b/packages/server/src/server/daemon-e2e/agent-configs.ts index c164e2a58..d95e81875 100644 --- a/packages/server/src/server/daemon-e2e/agent-configs.ts +++ b/packages/server/src/server/daemon-e2e/agent-configs.ts @@ -2,6 +2,9 @@ * Shared agent configurations for e2e tests. * Enables running the same tests against Claude, Codex, and OpenCode providers. */ +import { existsSync } from "node:fs"; +import { join } from "node:path"; +import { homedir } from "node:os"; import { isCommandAvailable } from "../agent/provider-launch-config.js"; export interface AgentTestConfig { @@ -44,9 +47,6 @@ export const agentConfigs = { export type AgentProvider = keyof typeof agentConfigs; -const hasClaudeCredentials = - !!process.env.CLAUDE_CODE_OAUTH_TOKEN || !!process.env.ANTHROPIC_API_KEY; - /** * Get test config for creating an agent with full permissions (no prompts). */ @@ -76,17 +76,28 @@ export function getAskModeConfig(provider: AgentProvider) { } /** - * Whether the real provider is executable in this environment. - * Claude additionally requires credentials. + * Whether a real provider can run in this environment. + * Checks binary availability AND credentials (env vars, OAuth tokens, auth files). + * + * Credentials are typically loaded from .env.test via the vitest setup file. + * This MUST be a function (not a const) so process.env is read at call time, + * after dotenv has injected the test credentials. */ -export function isRealProviderReady(provider: AgentProvider): boolean { - if (provider === "claude") { - return isCommandAvailable("claude") && hasClaudeCredentials; +export function isProviderAvailable(provider: AgentProvider): boolean { + switch (provider) { + case "claude": + return ( + isCommandAvailable("claude") && + (Boolean(process.env.CLAUDE_CODE_OAUTH_TOKEN) || Boolean(process.env.ANTHROPIC_API_KEY)) + ); + case "codex": + return ( + isCommandAvailable("codex") && + (existsSync(join(homedir(), ".codex", "auth.json")) || Boolean(process.env.OPENAI_API_KEY)) + ); + case "opencode": + return isCommandAvailable("opencode"); } - if (provider === "codex") { - return isCommandAvailable("codex"); - } - return isCommandAvailable("opencode"); } /** diff --git a/packages/server/src/server/daemon-e2e/claude-autonomous-wake-simple.real.e2e.test.ts b/packages/server/src/server/daemon-e2e/claude-autonomous-wake-simple.real.e2e.test.ts index b6f69a403..6dd2fe622 100644 --- a/packages/server/src/server/daemon-e2e/claude-autonomous-wake-simple.real.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/claude-autonomous-wake-simple.real.e2e.test.ts @@ -5,10 +5,9 @@ import path from "node:path"; import pino from "pino"; import { ClaudeAgentClient } from "../agent/providers/claude-agent.js"; -import { isCommandAvailable } from "../agent/provider-launch-config.js"; import { DaemonClient } from "../test-utils/daemon-client.js"; import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js"; -import { getFullAccessConfig } from "./agent-configs.js"; +import { getFullAccessConfig, isProviderAvailable } from "./agent-configs.js"; function tmpCwd(): string { return mkdtempSync(path.join(tmpdir(), "daemon-real-claude-autonomous-simple-")); @@ -19,7 +18,7 @@ function compactText(value: string): string { } describe("daemon E2E (real claude) - autonomous wake simple", () => { - test.runIf(isCommandAvailable("claude"))( + test.runIf(isProviderAvailable("claude"))( "hello + background sleep returns idle, then wakes once on completion", async () => { const logger = pino({ level: "silent" }); diff --git a/packages/server/src/server/daemon-e2e/claude-autonomous-wake.real.e2e.test.ts b/packages/server/src/server/daemon-e2e/claude-autonomous-wake.real.e2e.test.ts index 92dabe438..fccca9e86 100644 --- a/packages/server/src/server/daemon-e2e/claude-autonomous-wake.real.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/claude-autonomous-wake.real.e2e.test.ts @@ -8,8 +8,7 @@ import WebSocket from "ws"; import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js"; import { DaemonClient } from "../test-utils/daemon-client.js"; import { ClaudeAgentClient } from "../agent/providers/claude-agent.js"; -import { getFullAccessConfig } from "./agent-configs.js"; -import { isCommandAvailable } from "../agent/provider-launch-config.js"; +import { getFullAccessConfig, isProviderAvailable } from "./agent-configs.js"; function tmpCwd(): string { return mkdtempSync(path.join(tmpdir(), "daemon-real-claude-autonomous-wake-")); @@ -354,7 +353,7 @@ function summarizeTimelineEntry(entry: { } describe("daemon E2E (real claude) - autonomous wake from background task", () => { - test.runIf(isCommandAvailable("claude"))( + test.runIf(isProviderAvailable("claude"))( "A: background sleep returns idle, then wakes autonomously and appends timeline activity", async () => { const logger = pino({ level: "silent" }); @@ -432,7 +431,7 @@ describe("daemon E2E (real claude) - autonomous wake from background task", () = 420_000, ); - test.runIf(isCommandAvailable("claude"))( + test.runIf(isProviderAvailable("claude"))( "B: immediate HELLO before task notification returns promptly without deadlock", async () => { const logger = pino({ level: "silent" }); @@ -480,7 +479,7 @@ describe("daemon E2E (real claude) - autonomous wake from background task", () = 420_000, ); - test.runIf(isCommandAvailable("claude"))( + test.runIf(isProviderAvailable("claude"))( "C: interrupt during overlap returns quickly and does not leave agent stuck running", async () => { const logger = pino({ level: "silent" }); @@ -546,7 +545,7 @@ describe("daemon E2E (real claude) - autonomous wake from background task", () = 600_000, ); - test.runIf(isCommandAvailable("claude"))( + test.runIf(isProviderAvailable("claude"))( "returns to running after background sleep completes without a second prompt", async () => { const logger = pino({ level: "silent" }); @@ -608,7 +607,7 @@ describe("daemon E2E (real claude) - autonomous wake from background task", () = 420_000, ); - test.runIf(isCommandAvailable("claude"))( + test.runIf(isProviderAvailable("claude"))( "accepts a new prompt after background sleep finishes and replies HELLO", async () => { const logger = pino({ level: "silent" }); @@ -657,7 +656,7 @@ describe("daemon E2E (real claude) - autonomous wake from background task", () = 420_000, ); - test.runIf(isCommandAvailable("claude"))( + test.runIf(isProviderAvailable("claude"))( "repro: do-it-again + immediate hello can hang after autonomous wake under churn", async () => { const logger = pino({ level: "silent" }); @@ -764,7 +763,7 @@ describe("daemon E2E (real claude) - autonomous wake from background task", () = 900_000, ); - test.runIf(isCommandAvailable("claude"))( + test.runIf(isProviderAvailable("claude"))( "repro: second background sleep completion after HELLO should settle back to idle", async () => { const logger = pino({ level: "silent" }); @@ -840,7 +839,7 @@ describe("daemon E2E (real claude) - autonomous wake from background task", () = 600_000, ); - test.runIf(isCommandAvailable("claude"))( + test.runIf(isProviderAvailable("claude"))( "stress: immediate HELLO before task notification should not leave autonomous run stuck", async () => { const logger = pino({ level: "silent" }); @@ -930,7 +929,7 @@ describe("daemon E2E (real claude) - autonomous wake from background task", () = 900_000, ); - test.runIf(isCommandAvailable("claude"))( + test.runIf(isProviderAvailable("claude"))( "repro: transcript/timeline parity after do-it-again + hello race (hang + interrupt + drop)", async () => { const logger = pino({ level: "silent" }); diff --git a/packages/server/src/server/daemon-e2e/model-resolution-on-init.real.e2e.test.ts b/packages/server/src/server/daemon-e2e/model-resolution-on-init.real.e2e.test.ts index 375034632..5668a904f 100644 --- a/packages/server/src/server/daemon-e2e/model-resolution-on-init.real.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/model-resolution-on-init.real.e2e.test.ts @@ -7,14 +7,14 @@ import pino from "pino"; import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js"; import { DaemonClient } from "../test-utils/daemon-client.js"; import { ClaudeAgentClient } from "../agent/providers/claude-agent.js"; -import { isCommandAvailable } from "../agent/provider-launch-config.js"; +import { isProviderAvailable } from "./agent-configs.js"; function tmpCwd(): string { return mkdtempSync(path.join(tmpdir(), "daemon-claude-model-init-")); } describe("daemon E2E (real claude) - model resolution on init", () => { - test.runIf(isCommandAvailable("claude"))( + test.runIf(isProviderAvailable("claude"))( "runtimeInfo.model is set as soon as the agent starts running, not after turn completes", async () => { const logger = pino({ level: "silent" }); diff --git a/packages/server/src/server/daemon-e2e/model-runtime-reconcile-claude.real.e2e.test.ts b/packages/server/src/server/daemon-e2e/model-runtime-reconcile-claude.real.e2e.test.ts index 346c3de5f..42cbcdcde 100644 --- a/packages/server/src/server/daemon-e2e/model-runtime-reconcile-claude.real.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/model-runtime-reconcile-claude.real.e2e.test.ts @@ -7,14 +7,14 @@ import pino from "pino"; import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js"; import { DaemonClient } from "../test-utils/daemon-client.js"; import { ClaudeAgentClient } from "../agent/providers/claude-agent.js"; -import { isCommandAvailable } from "../agent/provider-launch-config.js"; +import { isProviderAvailable } from "./agent-configs.js"; function tmpCwd(): string { return mkdtempSync(path.join(tmpdir(), "daemon-claude-runtime-model-reconcile-")); } describe("daemon E2E (real claude) - runtime model reconciliation", () => { - test.runIf(isCommandAvailable("claude"))( + test.runIf(isProviderAvailable("claude"))( "normalizes runtime model to a model ID exposed by the provider catalog", async () => { const logger = pino({ level: "silent" }); diff --git a/packages/server/src/server/daemon-e2e/rewind-user-message-dedupe-claude.real.e2e.test.ts b/packages/server/src/server/daemon-e2e/rewind-user-message-dedupe-claude.real.e2e.test.ts index b85c32d30..7c5627db1 100644 --- a/packages/server/src/server/daemon-e2e/rewind-user-message-dedupe-claude.real.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/rewind-user-message-dedupe-claude.real.e2e.test.ts @@ -7,18 +7,14 @@ import pino from "pino"; import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js"; import { DaemonClient } from "../test-utils/daemon-client.js"; import { ClaudeAgentClient } from "../agent/providers/claude-agent.js"; -import { getFullAccessConfig } from "./agent-configs.js"; -import { isCommandAvailable } from "../agent/provider-launch-config.js"; +import { getFullAccessConfig, isProviderAvailable } from "./agent-configs.js"; function tmpCwd(): string { return mkdtempSync(path.join(tmpdir(), "daemon-rewind-dedupe-real-claude-")); } -const hasClaudeCredentials = - !!process.env.CLAUDE_CODE_OAUTH_TOKEN || !!process.env.ANTHROPIC_API_KEY; - describe("daemon E2E (real claude) - rewind user message dedupe", () => { - test.runIf(isCommandAvailable("claude") && hasClaudeCredentials)( + test.runIf(isProviderAvailable("claude"))( "emits /rewind user message once in persisted timeline", async () => { const logger = pino({ level: "silent" }); diff --git a/packages/server/src/server/daemon-e2e/send-during-tool-call-claude.real.e2e.test.ts b/packages/server/src/server/daemon-e2e/send-during-tool-call-claude.real.e2e.test.ts new file mode 100644 index 000000000..401761f28 --- /dev/null +++ b/packages/server/src/server/daemon-e2e/send-during-tool-call-claude.real.e2e.test.ts @@ -0,0 +1,130 @@ +import { describe, test, expect } from "vitest"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import pino from "pino"; + +import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js"; +import { DaemonClient } from "../test-utils/daemon-client.js"; +import { ClaudeAgentClient } from "../agent/providers/claude-agent.js"; +import { getFullAccessConfig, isProviderAvailable } from "./agent-configs.js"; +import { createMessageCollector } from "../test-utils/message-collector.js"; +import type { AgentTimelineItem } from "../agent/agent-sdk-types.js"; +import type { SessionOutboundMessage } from "../messages.js"; + +function tmpCwd(): string { + return mkdtempSync(path.join(tmpdir(), "daemon-real-tool-interrupt-")); +} + +function hasRunningToolCall(messages: SessionOutboundMessage[], agentId: string): boolean { + for (const m of messages) { + if ( + m.type === "agent_stream" && + m.payload.agentId === agentId && + m.payload.event.type === "timeline" && + m.payload.event.item.type === "tool_call" && + m.payload.event.item.status === "running" + ) { + return true; + } + } + return false; +} + +describe("daemon E2E (real claude) - send message during tool call", () => { + test.runIf(isProviderAvailable("claude"))( + "sending a message while a tool call is running starts a new turn", + async () => { + const logger = pino({ level: "silent" }); + const cwd = tmpCwd(); + const daemon = await createTestPaseoDaemon({ + agentClients: { claude: new ClaudeAgentClient({ logger }) }, + logger, + }); + + const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` }); + + try { + await client.connect(); + await client.fetchAgents({ subscribe: { subscriptionId: "primary" } }); + + const agent = await client.createAgent({ + cwd, + title: "tool-interrupt-repro", + ...getFullAccessConfig("claude"), + }); + + const collector = createMessageCollector(client); + + // Step 1: Ask Claude to run sleep 60 in the foreground + await client.sendMessage( + agent.id, + "Run the bash command `sleep 60` and wait for it to complete. Do not run it in the background.", + ); + + // Step 2: Wait for the agent to be running + await client.waitForAgentUpsert( + agent.id, + (snapshot) => snapshot.status === "running", + 60_000, + ); + + // Step 3: Wait for a tool call to appear as "running" in the stream + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error("Timed out waiting for running tool call")); + }, 90_000); + + if (hasRunningToolCall(collector.messages, agent.id)) { + clearTimeout(timeout); + resolve(); + return; + } + + const unsub = client.subscribeRawMessages((message) => { + if ( + message.type === "agent_stream" && + message.payload.agentId === agent.id && + message.payload.event.type === "timeline" && + message.payload.event.item.type === "tool_call" && + message.payload.event.item.status === "running" + ) { + clearTimeout(timeout); + unsub(); + resolve(); + } + }); + }); + + // Step 4: Send a second message while the tool call is still running + await client.sendMessage(agent.id, "Reply with exactly: INTERRUPT_RECEIVED"); + + // Step 5: Wait for the agent to finish — this is the critical assertion. + // If the bug is present, the agent will stop and never start a new turn. + const finish = await client.waitForFinish(agent.id, 120_000); + expect(finish.status).toBe("idle"); + + // Step 6: Verify the agent actually responded to our second message + const timeline = await client.fetchAgentTimeline(agent.id, { limit: 100 }); + const assistantTexts = timeline.entries + .filter((entry) => entry.item.type === "assistant_message") + .map((entry) => { + const item = entry.item as Extract; + return item.text; + }); + + const responded = assistantTexts.some((text) => + text.toUpperCase().includes("INTERRUPT_RECEIVED"), + ); + expect(responded).toBe(true); + + collector.unsubscribe(); + } finally { + await client.close(); + await daemon.close(); + rmSync(cwd, { recursive: true, force: true }); + } + }, + 300_000, + ); +}); diff --git a/packages/server/src/server/daemon-e2e/send-while-running-stuck-claude.real.e2e.test.ts b/packages/server/src/server/daemon-e2e/send-while-running-stuck-claude.real.e2e.test.ts index dbbf4e76c..2ed6ff1f1 100644 --- a/packages/server/src/server/daemon-e2e/send-while-running-stuck-claude.real.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/send-while-running-stuck-claude.real.e2e.test.ts @@ -7,16 +7,15 @@ import pino from "pino"; import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js"; import { DaemonClient } from "../test-utils/daemon-client.js"; import { ClaudeAgentClient } from "../agent/providers/claude-agent.js"; -import { getFullAccessConfig } from "./agent-configs.js"; +import { getFullAccessConfig, isProviderAvailable } from "./agent-configs.js"; import { applyAgentInputProcessingTransition } from "./send-while-running-stuck-test-utils.js"; -import { isCommandAvailable } from "../agent/provider-launch-config.js"; function tmpCwd(): string { return mkdtempSync(path.join(tmpdir(), "daemon-real-stuck-claude-")); } describe("daemon E2E (real claude) - send while running recovery", () => { - test.runIf(isCommandAvailable("claude"))( + test.runIf(isProviderAvailable("claude"))( "clears input processing when the interrupt transition is missed", async () => { const logger = pino({ level: "silent" }); diff --git a/packages/server/src/server/daemon-e2e/send-while-running-stuck.real.e2e.test.ts b/packages/server/src/server/daemon-e2e/send-while-running-stuck.real.e2e.test.ts index 41a0d36d0..fb5470b6a 100644 --- a/packages/server/src/server/daemon-e2e/send-while-running-stuck.real.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/send-while-running-stuck.real.e2e.test.ts @@ -7,16 +7,15 @@ import pino from "pino"; import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js"; import { DaemonClient } from "../test-utils/daemon-client.js"; import { CodexAppServerAgentClient } from "../agent/providers/codex-app-server-agent.js"; -import { getFullAccessConfig } from "./agent-configs.js"; +import { getFullAccessConfig, isProviderAvailable } from "./agent-configs.js"; import { applyAgentInputProcessingTransition } from "./send-while-running-stuck-test-utils.js"; -import { isCommandAvailable } from "../agent/provider-launch-config.js"; function tmpCwd(): string { return mkdtempSync(path.join(tmpdir(), "daemon-real-stuck-")); } describe("daemon E2E (real codex) - send while running recovery", () => { - test.runIf(isCommandAvailable("codex"))( + test.runIf(isProviderAvailable("codex"))( "clears input processing when the interrupt transition is missed", async () => { const logger = pino({ level: "silent" }); diff --git a/packages/server/src/server/daemon-e2e/ui-action-stress.real.e2e.test.ts b/packages/server/src/server/daemon-e2e/ui-action-stress.real.e2e.test.ts index 883b70c18..f07aa0900 100644 --- a/packages/server/src/server/daemon-e2e/ui-action-stress.real.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/ui-action-stress.real.e2e.test.ts @@ -13,7 +13,7 @@ import { DaemonClient } from "../test-utils/daemon-client.js"; import { allProviders, getFullAccessConfig, - isRealProviderReady, + isProviderAvailable, type AgentProvider, } from "./agent-configs.js"; @@ -456,7 +456,7 @@ function createRealAgentClient(provider: AgentProvider, logger: pino.Logger): Ag } describe.each(allProviders)("daemon E2E (real %s) - UI action stress", (provider) => { - const shouldRun = isRealProviderReady(provider); + const shouldRun = isProviderAvailable(provider); test.runIf(shouldRun)( "normal UI submit path (idle sends) stays correct",