diff --git a/packages/server/src/server/agent/agent-metadata-generator.real.e2e.test.ts b/packages/server/src/server/agent/agent-metadata-generator.real.e2e.test.ts index 6b4524b8a..0dd23ad70 100644 --- a/packages/server/src/server/agent/agent-metadata-generator.real.e2e.test.ts +++ b/packages/server/src/server/agent/agent-metadata-generator.real.e2e.test.ts @@ -7,13 +7,19 @@ import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"; import { AgentManager } from "./agent-manager.js"; import { AgentStorage } from "./agent-storage.js"; -import { createAllClients, shutdownProviders } from "./provider-registry.js"; +import { shutdownProviders } from "./provider-registry.js"; import { generateAndApplyAgentMetadata } from "./agent-metadata-generator.js"; -import { isProviderAvailable } from "../daemon-e2e/agent-configs.js"; import { OpenCodeServerManager } from "./providers/opencode/server-manager.js"; +import { + canRunRealProvider, + createRealProviderClients, + getRealProviderConfig, +} from "../daemon-e2e/real-provider-test-config.js"; -const CODEX_TEST_MODEL = "gpt-5.4-mini"; -const CODEX_TEST_THINKING_OPTION_ID = "low"; +const CODEX_TEST_MODEL = getRealProviderConfig("codex").model; +const CODEX_TEST_THINKING_OPTION_ID = getRealProviderConfig("codex").thinkingOptionId; +const CLAUDE_TEST_MODEL = getRealProviderConfig("claude").model; +const OPENCODE_TEST_MODEL = getRealProviderConfig("opencode").model; function collectFilesRecursively(root: string, filter: (name: string) => boolean): Set { const results = new Set(); @@ -75,9 +81,9 @@ describe("agent metadata generation (real agents)", () => { beforeAll(async () => { [codexAvailable, claudeAvailable, opencodeAvailable] = await Promise.all([ - isProviderAvailable("codex"), - isProviderAvailable("claude"), - isProviderAvailable("opencode"), + canRunRealProvider("codex"), + canRunRealProvider("claude"), + canRunRealProvider("opencode"), ]); }); @@ -86,7 +92,7 @@ describe("agent metadata generation (real agents)", () => { paseoHome = tmpCwd("metadata-paseo-home-"); storage = new AgentStorage(path.join(paseoHome, "agents"), logger); manager = new AgentManager({ - clients: createAllClients(logger), + clients: createRealProviderClients(["codex", "claude", "opencode"], logger), registry: storage, logger, }); @@ -106,7 +112,9 @@ describe("agent metadata generation (real agents)", () => { { provider: "codex", model: CODEX_TEST_MODEL, - thinkingOptionId: CODEX_TEST_THINKING_OPTION_ID, + ...(CODEX_TEST_THINKING_OPTION_ID + ? { thinkingOptionId: CODEX_TEST_THINKING_OPTION_ID } + : {}), modeId: "auto", cwd: cwd, title: "Main Agent", @@ -144,7 +152,7 @@ describe("agent metadata generation (real agents)", () => { const agent = await manager.createAgent( { provider: "claude", - model: "haiku", + model: CLAUDE_TEST_MODEL, thinkingOptionId: "on", cwd: cwd, title: "Main Claude Agent", @@ -182,7 +190,7 @@ describe("agent metadata generation (real agents)", () => { const agent = await manager.createAgent( { provider: "opencode", - model: "opencode/gpt-5-nano", + model: OPENCODE_TEST_MODEL, modeId: "build", cwd: cwd, title: "Main OpenCode Agent", diff --git a/packages/server/src/server/agent/agent-response-loop.e2e.test.ts b/packages/server/src/server/agent/agent-response-loop.real.e2e.test.ts similarity index 90% rename from packages/server/src/server/agent/agent-response-loop.e2e.test.ts rename to packages/server/src/server/agent/agent-response-loop.real.e2e.test.ts index aacbf046e..ec9dbfdb1 100644 --- a/packages/server/src/server/agent/agent-response-loop.e2e.test.ts +++ b/packages/server/src/server/agent/agent-response-loop.real.e2e.test.ts @@ -12,14 +12,17 @@ import { generateStructuredAgentResponse } from "./agent-response-loop.js"; import { AgentManager } from "./agent-manager.js"; import { AgentStorage } from "./agent-storage.js"; import { createAgentMcpServer } from "./mcp-server.js"; -import { createAllClients, shutdownProviders } from "./provider-registry.js"; -import { isProviderAvailable } from "../daemon-e2e/agent-configs.js"; +import { shutdownProviders } from "./provider-registry.js"; +import { + canRunRealProvider, + createRealProviderClients, + getRealProviderConfig, +} from "../daemon-e2e/real-provider-test-config.js"; import pino from "pino"; -const CODEX_TEST_MODEL = "gpt-5.4-mini"; -const CODEX_TEST_THINKING_OPTION_ID = "low"; - -const hasOpenAICredentials = !!process.env.OPENAI_API_KEY; +const CODEX_TEST_MODEL = getRealProviderConfig("codex").model; +const CODEX_TEST_THINKING_OPTION_ID = getRealProviderConfig("codex").thinkingOptionId; +const CLAUDE_TEST_MODEL = getRealProviderConfig("claude").model; interface AgentMcpServerHandle { url: string; @@ -154,8 +157,8 @@ describe("getStructuredAgentResponse (e2e)", () => { const logger = pino({ level: "silent" }); beforeAll(async () => { - canRunCodex = !process.env.CI && hasOpenAICredentials; - canRunClaude = await isProviderAvailable("claude"); + canRunCodex = await canRunRealProvider("codex"); + canRunClaude = await canRunRealProvider("claude"); if (!canRunCodex && !canRunClaude) { return; } @@ -169,7 +172,7 @@ describe("getStructuredAgentResponse (e2e)", () => { beforeEach(async () => { cwd = mkdtempSync(path.join(tmpdir(), "agent-response-loop-")); manager = new AgentManager({ - clients: createAllClients(logger), + clients: createRealProviderClients(["codex", "claude"], logger), logger, }); }); @@ -193,7 +196,9 @@ describe("getStructuredAgentResponse (e2e)", () => { agentConfig: { provider: "codex", model: CODEX_TEST_MODEL, - thinkingOptionId: CODEX_TEST_THINKING_OPTION_ID, + ...(CODEX_TEST_THINKING_OPTION_ID + ? { thinkingOptionId: CODEX_TEST_THINKING_OPTION_ID } + : {}), cwd, title: "Structured Response Test", }, @@ -222,7 +227,7 @@ describe("getStructuredAgentResponse (e2e)", () => { manager, agentConfig: { provider: "claude", - model: "haiku", + model: CLAUDE_TEST_MODEL, thinkingOptionId: "on", cwd, title: "Claude Haiku Structured Test", diff --git a/packages/server/src/server/agent/providers/claude/agent-commands.real.e2e.test.ts b/packages/server/src/server/agent/providers/claude/agent-commands.real.e2e.test.ts index f9823eca3..e11f0dd7a 100644 --- a/packages/server/src/server/agent/providers/claude/agent-commands.real.e2e.test.ts +++ b/packages/server/src/server/agent/providers/claude/agent-commands.real.e2e.test.ts @@ -1,15 +1,17 @@ import { beforeAll, beforeEach, describe, expect, test } from "vitest"; import pino from "pino"; -import { isProviderAvailable } from "../../../daemon-e2e/agent-configs.js"; -import { ClaudeAgentClient } from "./agent.js"; +import { + canRunRealProvider, + createRealProviderClient, +} from "../../../daemon-e2e/real-provider-test-config.js"; // Real-Claude contract coverage: validates slash command shape from a live Claude CLI session. describe("claude agent commands contract (real)", () => { let canRun = false; beforeAll(async () => { - canRun = await isProviderAvailable("claude"); + canRun = await canRunRealProvider("claude"); }); beforeEach((context) => { @@ -19,9 +21,7 @@ describe("claude agent commands contract (real)", () => { }); test("lists slash commands with the expected contract", async () => { - const client = new ClaudeAgentClient({ - logger: pino({ level: "silent" }), - }); + const client = createRealProviderClient("claude", pino({ level: "silent" })); const session = await client.createSession({ provider: "claude", cwd: process.cwd(), diff --git a/packages/server/src/server/agent/providers/claude/agent.event-stream.real.e2e.test.ts b/packages/server/src/server/agent/providers/claude/agent.event-stream.real.e2e.test.ts index e03dc2757..2113758f5 100644 --- a/packages/server/src/server/agent/providers/claude/agent.event-stream.real.e2e.test.ts +++ b/packages/server/src/server/agent/providers/claude/agent.event-stream.real.e2e.test.ts @@ -19,15 +19,18 @@ import { type AgentSession, type AgentStreamEvent, } from "../../agent-sdk-types.js"; -import { isProviderAvailable } from "../../../daemon-e2e/agent-configs.js"; -import { ClaudeAgentClient } from "./agent.js"; +import { + canRunRealProvider, + createRealProviderClient, + getRealProviderConfig, +} from "../../../daemon-e2e/real-provider-test-config.js"; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- const logger = pino({ level: "silent" }); -const client = new ClaudeAgentClient({ logger }); +const client = createRealProviderClient("claude", logger); function tmpCwd(prefix: string): string { return mkdtempSync(path.join(tmpdir(), prefix)); @@ -62,11 +65,10 @@ async function createSession(params?: { }): Promise<{ cwd: string; session: AgentSession }> { const cwd = tmpCwd(params?.cwdPrefix ?? "event-stream-integration-"); const session = await client.createSession({ - provider: "claude", + ...getRealProviderConfig("claude"), cwd, title: "event-stream integration", modeId: "acceptEdits", - model: "haiku", }); return { cwd, session }; } @@ -202,7 +204,7 @@ function assertInvariants(events: AgentStreamEvent[], foregroundTurnIds: string[ let canRun = false; beforeAll(async () => { - canRun = await isProviderAvailable("claude"); + canRun = await canRunRealProvider("claude"); }); beforeEach((context) => { diff --git a/packages/server/src/server/agent/providers/claude/agent.max-effort.real.e2e.test.ts b/packages/server/src/server/agent/providers/claude/agent.max-effort.real.e2e.test.ts index aac27bfca..a99b72096 100644 --- a/packages/server/src/server/agent/providers/claude/agent.max-effort.real.e2e.test.ts +++ b/packages/server/src/server/agent/providers/claude/agent.max-effort.real.e2e.test.ts @@ -2,8 +2,10 @@ import { beforeAll, beforeEach, describe, expect, test } from "vitest"; import pino from "pino"; import type { AgentStreamEvent, AgentSession } from "../../agent-sdk-types.js"; -import { isProviderAvailable } from "../../../daemon-e2e/agent-configs.js"; -import { ClaudeAgentClient } from "./agent.js"; +import { + canRunRealProvider, + createRealProviderClient, +} from "../../../daemon-e2e/real-provider-test-config.js"; import { streamSession } from "../test-utils/session-stream-adapter.js"; function isTerminalEvent(event: AgentStreamEvent): boolean { @@ -29,7 +31,7 @@ describe("Claude max effort availability (real)", () => { let canRun = false; beforeAll(async () => { - canRun = await isProviderAvailable("claude"); + canRun = await canRunRealProvider("claude"); }); beforeEach((context) => { @@ -39,9 +41,7 @@ describe("Claude max effort availability (real)", () => { }); test("surfaces the Claude stderr diagnostic when bypassPermissions + max effort is unavailable", async () => { - const client = new ClaudeAgentClient({ - logger: pino({ level: "silent" }), - }); + const client = createRealProviderClient("claude", pino({ level: "silent" })); const session = await client.createSession({ provider: "claude", cwd: process.cwd(), diff --git a/packages/server/src/server/agent/providers/claude/agent.real.e2e.test.ts b/packages/server/src/server/agent/providers/claude/agent.real.e2e.test.ts index 9e9bc0c8f..b0178f83c 100644 --- a/packages/server/src/server/agent/providers/claude/agent.real.e2e.test.ts +++ b/packages/server/src/server/agent/providers/claude/agent.real.e2e.test.ts @@ -10,15 +10,19 @@ import type { AgentStreamEvent, ToolCallTimelineItem, } from "../../agent-sdk-types.js"; -import { isProviderAvailable } from "../../../daemon-e2e/agent-configs.js"; +import { + canRunRealProvider, + createRealProviderClient, + getRealProviderConfig, + getRealProviderRuntimeSettings, +} from "../../../daemon-e2e/real-provider-test-config.js"; import { findExecutable } from "../../../../utils/executable.js"; import { withTimeout } from "../../../../utils/promise-timeout.js"; -import { ClaudeAgentClient } from "./agent.js"; import { claudeQuery } from "./query.js"; import { streamSession } from "../test-utils/session-stream-adapter.js"; const logger = pino({ level: "silent" }); -const client = new ClaudeAgentClient({ logger }); +const client = createRealProviderClient("claude", logger); function tmpCwd(prefix: string): string { return mkdtempSync(path.join(tmpdir(), prefix)); @@ -150,11 +154,10 @@ async function createSession(params?: { }): Promise<{ cwd: string; session: AgentSession }> { const cwd = tmpCwd(params?.cwdPrefix ?? "claude-agent-integration-"); const session = await client.createSession({ - provider: "claude", + ...getRealProviderConfig("claude"), cwd, title: params?.title ?? "ClaudeAgentSession integration", modeId: params?.modeId ?? "acceptEdits", - model: "haiku", }); return { cwd, session }; } @@ -175,7 +178,7 @@ describe("ClaudeAgentSession integration", () => { let canRun = false; beforeAll(async () => { - canRun = await isProviderAvailable("claude"); + canRun = await canRunRealProvider("claude"); }); beforeEach((context) => { @@ -233,16 +236,19 @@ describe("ClaudeAgentSession integration", () => { test("supportedModels returns the current abstract Claude SDK model shape", async () => { const claudeBinary = await findExecutable("claude"); if (!claudeBinary) throw new Error("claude binary required for this integration test"); - const query = claudeQuery({ - prompt: createEmptyPrompt(), - options: { - cwd: process.cwd(), - permissionMode: "plan", - includePartialMessages: false, - settingSources: ["user", "project"], - pathToClaudeCodeExecutable: claudeBinary, + const query = claudeQuery( + { + prompt: createEmptyPrompt(), + options: { + cwd: process.cwd(), + permissionMode: "plan", + includePartialMessages: false, + settingSources: ["user", "project"], + pathToClaudeCodeExecutable: claudeBinary, + }, }, - }); + { runtimeSettings: getRealProviderRuntimeSettings("claude") }, + ); try { const models = await query.supportedModels(); diff --git a/packages/server/src/server/agent/providers/claude/sdk-behavior.test.ts b/packages/server/src/server/agent/providers/claude/sdk-behavior.real.e2e.test.ts similarity index 86% rename from packages/server/src/server/agent/providers/claude/sdk-behavior.test.ts rename to packages/server/src/server/agent/providers/claude/sdk-behavior.real.e2e.test.ts index 77f521d0b..a38bbe1e6 100644 --- a/packages/server/src/server/agent/providers/claude/sdk-behavior.test.ts +++ b/packages/server/src/server/agent/providers/claude/sdk-behavior.real.e2e.test.ts @@ -6,7 +6,10 @@ import { tmpdir } from "node:os"; import path from "node:path"; import { beforeAll, beforeEach, describe, expect, test } from "vitest"; import type { SDKMessage, SDKUserMessage } from "@anthropic-ai/claude-agent-sdk"; -import { isProviderAvailable } from "../../../daemon-e2e/agent-configs.js"; +import { + canRunRealProvider, + getRealProviderRuntimeSettings, +} from "../../../daemon-e2e/real-provider-test-config.js"; import { findExecutable } from "../../../../utils/executable.js"; import { claudeQuery } from "./query.js"; @@ -89,7 +92,7 @@ describe("Claude SDK direct behavior", () => { let canRun = false; beforeAll(async () => { - canRun = await isProviderAvailable("claude"); + canRun = await canRunRealProvider("claude"); }); beforeEach((context) => { @@ -104,20 +107,23 @@ describe("Claude SDK direct behavior", () => { const claudeBinary = await findExecutable("claude"); // Use same options as the Claude provider - const q = claudeQuery({ - prompt: input, - options: { - cwd, - includePartialMessages: true, - permissionMode: "bypassPermissions", - ...(claudeBinary ? { pathToClaudeCodeExecutable: claudeBinary } : {}), - systemPrompt: { - type: "preset", - preset: "claude_code", + const q = claudeQuery( + { + prompt: input, + options: { + cwd, + includePartialMessages: true, + permissionMode: "bypassPermissions", + ...(claudeBinary ? { pathToClaudeCodeExecutable: claudeBinary } : {}), + systemPrompt: { + type: "preset", + preset: "claude_code", + }, + settingSources: ["user", "project"], }, - settingSources: ["user", "project"], }, - }); + { runtimeSettings: getRealProviderRuntimeSettings("claude") }, + ); try { // Send first message diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.e2e.test.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.local.e2e.test.ts similarity index 90% rename from packages/server/src/server/agent/providers/codex-app-server-agent.e2e.test.ts rename to packages/server/src/server/agent/providers/codex-app-server-agent.local.e2e.test.ts index d0f1303ac..28d0252cf 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.e2e.test.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.local.e2e.test.ts @@ -8,12 +8,8 @@ import { once } from "node:events"; import { CodexAppServerAgentClient } from "./codex-app-server-agent.js"; import { createTestLogger } from "../../../test-utils/test-logger.js"; -import { agentConfigs } from "../../daemon-e2e/agent-configs.js"; import type { AgentStreamEvent } from "../agent-sdk-types.js"; -const CODEX_TEST_MODEL = agentConfigs.codex.model; -const CODEX_TEST_THINKING_OPTION_ID = agentConfigs.codex.thinkingOptionId; - function isCodexInstalled(): boolean { try { const out = execFileSync("which", ["codex"], { encoding: "utf8" }).trim(); @@ -217,35 +213,7 @@ function waitForEvent(params: { }); } -describe("Codex app-server provider (e2e)", () => { - test.runIf(isCodexInstalled())( - "lists models and runs a simple prompt", - async () => { - const client = new CodexAppServerAgentClient(createTestLogger()); - const cwd = mkdtempSync(path.join(os.tmpdir(), "codex-app-server-e2e-")); - const models = await client.listModels({ cwd, force: false }); - expect(models.some((m) => m.id.includes("gpt-5.1-codex"))).toBe(true); - - const session = await client.createSession({ - provider: "codex", - cwd, - modeId: "auto", - model: CODEX_TEST_MODEL, - thinkingOptionId: CODEX_TEST_THINKING_OPTION_ID, - }); - try { - expect(session.features?.some((feature) => feature.id === "plan_mode")).toBe(true); - - const result = await session.run("Say hello in one sentence."); - expect(result.finalText.length).toBeGreaterThan(0); - } finally { - await session.close(); - rmSync(cwd, { recursive: true, force: true }); - } - }, - 30000, - ); - +describe("Codex app-server provider (local e2e)", () => { test.runIf(isCodexInstalled())( "surfaces request_user_input from the app-server as question permissions and timeline tool calls", async () => { diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.real.e2e.test.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.real.e2e.test.ts new file mode 100644 index 000000000..861467e01 --- /dev/null +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.real.e2e.test.ts @@ -0,0 +1,47 @@ +import { mkdtempSync, rmSync } from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { beforeAll, beforeEach, describe, expect, test } from "vitest"; + +import { createTestLogger } from "../../../test-utils/test-logger.js"; +import { + canRunRealProvider, + createRealProviderClient, + getRealProviderConfig, +} from "../../daemon-e2e/real-provider-test-config.js"; + +describe("Codex app-server provider (real)", () => { + let canRun = false; + + beforeAll(async () => { + canRun = await canRunRealProvider("codex"); + }); + + beforeEach((context) => { + if (!canRun) { + context.skip(); + } + }); + + test("lists models and runs a simple prompt", async () => { + const client = createRealProviderClient("codex", createTestLogger()); + const cwd = mkdtempSync(path.join(os.tmpdir(), "codex-app-server-e2e-")); + const models = await client.listModels({ cwd, force: false }); + expect(models.length).toBeGreaterThan(0); + + const session = await client.createSession({ + ...getRealProviderConfig("codex"), + cwd, + modeId: "auto", + }); + try { + expect(session.features?.some((feature) => feature.id === "plan_mode")).toBe(true); + + const result = await session.run("Say hello in one sentence."); + expect(result.finalText.length).toBeGreaterThan(0); + } finally { + await session.close(); + rmSync(cwd, { recursive: true, force: true }); + } + }, 30_000); +}); diff --git a/packages/server/src/server/agent/providers/codex-plan-mode.real.e2e.test.ts b/packages/server/src/server/agent/providers/codex-plan-mode.real.e2e.test.ts index dd87d7137..7c55a5f0f 100644 --- a/packages/server/src/server/agent/providers/codex-plan-mode.real.e2e.test.ts +++ b/packages/server/src/server/agent/providers/codex-plan-mode.real.e2e.test.ts @@ -3,9 +3,12 @@ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; -import { CodexAppServerAgentClient } from "./codex-app-server-agent.js"; import { createTestLogger } from "../../../test-utils/test-logger.js"; -import { isProviderAvailable } from "../../daemon-e2e/agent-configs.js"; +import { + canRunRealProvider, + createRealProviderClient, + getRealProviderConfig, +} from "../../daemon-e2e/real-provider-test-config.js"; function tmpCwd(): string { return mkdtempSync(path.join(tmpdir(), "codex-plan-mode-real-")); @@ -15,7 +18,7 @@ describe("Codex app-server provider (real) plan mode", () => { let canRun = false; beforeAll(async () => { - canRun = await isProviderAvailable("codex"); + canRun = await canRunRealProvider("codex"); }); beforeEach((context) => { @@ -26,14 +29,13 @@ describe("Codex app-server provider (real) plan mode", () => { test("maps gpt-5.4 markdown plans to a plan tool call instead of todo items", async () => { const cwd = tmpCwd(); - const client = new CodexAppServerAgentClient(createTestLogger()); + const client = createRealProviderClient("codex", createTestLogger()); try { const session = await client.createSession({ - provider: "codex", + ...getRealProviderConfig("codex"), cwd, modeId: "auto", - model: "gpt-5.4", thinkingOptionId: "medium", }); diff --git a/packages/server/src/server/agent/providers/opencode-agent-commands.real.e2e.test.ts b/packages/server/src/server/agent/providers/opencode-agent-commands.real.e2e.test.ts index bffaeeac1..b1b87aa26 100644 --- a/packages/server/src/server/agent/providers/opencode-agent-commands.real.e2e.test.ts +++ b/packages/server/src/server/agent/providers/opencode-agent-commands.real.e2e.test.ts @@ -1,18 +1,21 @@ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest"; import pino from "pino"; -import { isCommandAvailable } from "../../../utils/executable.js"; -import { OpenCodeAgentClient } from "./opencode-agent.js"; import { OpenCodeServerManager } from "./opencode/server-manager.js"; +import { + canRunRealProvider, + createRealProviderClient, + getRealProviderConfig, +} from "../../daemon-e2e/real-provider-test-config.js"; -const BIG_PICKLE_MODEL = "opencode/big-pickle"; +const OPENCODE_REAL_TEST_MODEL = getRealProviderConfig("opencode").model; const logger = pino({ level: "silent" }); describe("opencode agent commands contract (real)", () => { let canRun = false; beforeAll(async () => { - canRun = await isCommandAvailable("opencode"); + canRun = await canRunRealProvider("opencode"); }); beforeEach((context) => { @@ -26,13 +29,11 @@ describe("opencode agent commands contract (real)", () => { }); test("lists slash commands with the expected contract", async () => { - expect(await isCommandAvailable("opencode")).toBe(true); - - const client = new OpenCodeAgentClient(logger); + const client = createRealProviderClient("opencode", logger); const session = await client.createSession({ provider: "opencode", cwd: process.cwd(), - model: BIG_PICKLE_MODEL, + model: OPENCODE_REAL_TEST_MODEL, modeId: "plan", }); @@ -57,13 +58,11 @@ describe("opencode agent commands contract (real)", () => { }, 60_000); test("executes a slash command without arguments", async () => { - expect(await isCommandAvailable("opencode")).toBe(true); - - const client = new OpenCodeAgentClient(logger); + const client = createRealProviderClient("opencode", logger); const session = await client.createSession({ provider: "opencode", cwd: process.cwd(), - model: BIG_PICKLE_MODEL, + model: OPENCODE_REAL_TEST_MODEL, modeId: "plan", }); diff --git a/packages/server/src/server/agent/providers/opencode-agent-custom-command.real.e2e.test.ts b/packages/server/src/server/agent/providers/opencode-agent-custom-command.real.e2e.test.ts index a18bc212f..1d1874370 100644 --- a/packages/server/src/server/agent/providers/opencode-agent-custom-command.real.e2e.test.ts +++ b/packages/server/src/server/agent/providers/opencode-agent-custom-command.real.e2e.test.ts @@ -6,21 +6,24 @@ import path from "node:path"; import { beforeAll, beforeEach, describe, expect, test } from "vitest"; import pino from "pino"; -import { isCommandAvailable } from "../../../utils/executable.js"; import { createDaemonTestContext, type DaemonTestContext } from "../../test-utils/index.js"; -import { OpenCodeAgentClient } from "./opencode-agent.js"; import { OpenCodeServerManager } from "./opencode/server-manager.js"; +import { + canRunRealProvider, + createRealProviderClients, + getRealProviderConfig, +} from "../../daemon-e2e/real-provider-test-config.js"; const COMMAND_NAME = "paseo-issue-903-big-pickle"; const COMMAND_FILE_NAME = `${COMMAND_NAME}.md`; -const BIG_PICKLE_MODEL = "opencode/big-pickle"; +const OPENCODE_REAL_TEST_MODEL = getRealProviderConfig("opencode").model; const EXPECTED_RESPONSE = "PASEO_ISSUE_903_BIG_PICKLE_OK"; describe("opencode custom command Big Pickle E2E (real)", () => { let canRun = false; beforeAll(async () => { - canRun = await isCommandAvailable("opencode"); + canRun = await canRunRealProvider("opencode"); }); beforeEach((context) => { @@ -59,21 +62,19 @@ describe("opencode custom command Big Pickle E2E (real)", () => { ctx = await createDaemonTestContext({ logger, - agentClients: { - opencode: new OpenCodeAgentClient(logger), - }, + agentClients: createRealProviderClients(["opencode"], logger), }); const agent = await ctx.client.createAgent({ provider: "opencode", cwd: projectDir, - model: BIG_PICKLE_MODEL, + model: OPENCODE_REAL_TEST_MODEL, modeId: "plan", title: "OpenCode issue 903 Big Pickle custom command", }); expect(agent.provider).toBe("opencode"); - expect(agent.model).toBe(BIG_PICKLE_MODEL); + expect(agent.model).toBe(OPENCODE_REAL_TEST_MODEL); expect(agent.status).toBe("idle"); const commands = await ctx.client.listCommands(agent.id); diff --git a/packages/server/src/server/agent/providers/opencode-agent.error-handling.real.e2e.test.ts b/packages/server/src/server/agent/providers/opencode-agent.error-handling.real.e2e.test.ts index bf852d30c..68ef21811 100644 --- a/packages/server/src/server/agent/providers/opencode-agent.error-handling.real.e2e.test.ts +++ b/packages/server/src/server/agent/providers/opencode-agent.error-handling.real.e2e.test.ts @@ -2,8 +2,10 @@ import { beforeAll, beforeEach, describe, expect, test } from "vitest"; import pino from "pino"; import type { AgentStreamEvent } from "../agent-sdk-types.js"; -import { isCommandAvailable } from "../../../utils/executable.js"; -import { OpenCodeAgentClient } from "./opencode-agent.js"; +import { + canRunRealProvider, + createRealProviderClient, +} from "../../daemon-e2e/real-provider-test-config.js"; import { streamSession } from "./test-utils/session-stream-adapter.js"; function isTerminalEvent(event: AgentStreamEvent): boolean { @@ -24,7 +26,7 @@ describe("opencode agent error handling (real)", () => { let canRun = false; beforeAll(async () => { - canRun = await isCommandAvailable("opencode"); + canRun = await canRunRealProvider("opencode"); }); beforeEach((context) => { @@ -34,7 +36,7 @@ describe("opencode agent error handling (real)", () => { }); test("surfaces error for the exact opencode/ path used by paseo run", async () => { - const client = new OpenCodeAgentClient(pino({ level: "silent" })); + const client = createRealProviderClient("opencode", pino({ level: "silent" })); const session = await client.createSession({ provider: "opencode", cwd: process.cwd(), @@ -59,7 +61,7 @@ describe("opencode agent error handling (real)", () => { }, 45_000); test("surfaces error for unknown provider model (fast path)", async () => { - const client = new OpenCodeAgentClient(pino({ level: "silent" })); + const client = createRealProviderClient("opencode", pino({ level: "silent" })); const session = await client.createSession({ provider: "opencode", cwd: process.cwd(), @@ -84,7 +86,7 @@ describe("opencode agent error handling (real)", () => { }, 30_000); test("sequential sessions: second session works after first errors", async () => { - const client = new OpenCodeAgentClient(pino({ level: "silent" })); + const client = createRealProviderClient("opencode", pino({ level: "silent" })); // Session 1: bogus model, will error quickly const s1 = await client.createSession({ @@ -121,7 +123,7 @@ describe("opencode agent error handling (real)", () => { test("surfaces error for known provider with nonexistent model (retry path)", async () => { // When the provider is recognized (anthropic) but the model doesn't exist, // OpenCode retries before surfacing the error. This must not hang. - const client = new OpenCodeAgentClient(pino({ level: "silent" })); + const client = createRealProviderClient("opencode", pino({ level: "silent" })); const session = await client.createSession({ provider: "opencode", cwd: process.cwd(), diff --git a/packages/server/src/server/agent/providers/opencode-assistant-message.real.e2e.test.ts b/packages/server/src/server/agent/providers/opencode-assistant-message.real.e2e.test.ts index bf6853d07..7a79913e9 100644 --- a/packages/server/src/server/agent/providers/opencode-assistant-message.real.e2e.test.ts +++ b/packages/server/src/server/agent/providers/opencode-assistant-message.real.e2e.test.ts @@ -4,19 +4,22 @@ import { tmpdir } from "node:os"; import path from "node:path"; import pino from "pino"; -import { OpenCodeAgentClient } from "./opencode-agent.js"; import { OpenCodeServerManager } from "./opencode/server-manager.js"; -import { isProviderAvailable } from "../../daemon-e2e/agent-configs.js"; import type { AgentStreamEvent } from "../agent-sdk-types.js"; +import { + canRunRealProvider, + createRealProviderClient, + getRealProviderConfig, +} from "../../daemon-e2e/real-provider-test-config.js"; -const BIG_PICKLE_MODEL = "opencode/big-pickle"; +const OPENCODE_REAL_TEST_MODEL = getRealProviderConfig("opencode").model; describe("OpenCode assistant message", () => { let canRun = false; const logger = pino({ level: "silent" }); beforeAll(async () => { - canRun = await isProviderAvailable("opencode"); + canRun = await canRunRealProvider("opencode"); }); beforeEach((context) => { @@ -29,15 +32,15 @@ describe("OpenCode assistant message", () => { await OpenCodeServerManager.getInstance(logger).shutdown(); }); - test("assistant_message appears in live stream with opencode/big-pickle", async () => { + test("assistant_message appears in live stream", async () => { const cwd = mkdtempSync(path.join(tmpdir(), "opencode-msg-")); - const client = new OpenCodeAgentClient(logger); + const client = createRealProviderClient("opencode", logger); try { const session = await client.createSession({ provider: "opencode", cwd, - model: BIG_PICKLE_MODEL, + model: OPENCODE_REAL_TEST_MODEL, modeId: "build", }); @@ -53,13 +56,13 @@ describe("OpenCode assistant message", () => { test("streamHistory returns assistant_message after a completed turn", async () => { const cwd = mkdtempSync(path.join(tmpdir(), "opencode-history-")); - const client = new OpenCodeAgentClient(logger); + const client = createRealProviderClient("opencode", logger); try { const session = await client.createSession({ provider: "opencode", cwd, - model: BIG_PICKLE_MODEL, + model: OPENCODE_REAL_TEST_MODEL, modeId: "build", }); diff --git a/packages/server/src/server/agent/providers/opencode-reasoning-dedup.real.e2e.test.ts b/packages/server/src/server/agent/providers/opencode-reasoning-dedup.real.e2e.test.ts index 95a426031..ebfea9b52 100644 --- a/packages/server/src/server/agent/providers/opencode-reasoning-dedup.real.e2e.test.ts +++ b/packages/server/src/server/agent/providers/opencode-reasoning-dedup.real.e2e.test.ts @@ -4,19 +4,22 @@ import { tmpdir } from "node:os"; import path from "node:path"; import pino from "pino"; -import { OpenCodeAgentClient } from "./opencode-agent.js"; import { OpenCodeServerManager } from "./opencode/server-manager.js"; -import { isProviderAvailable } from "../../daemon-e2e/agent-configs.js"; import type { AgentStreamEvent } from "../agent-sdk-types.js"; +import { + canRunRealProvider, + createRealProviderClient, + getRealProviderConfig, +} from "../../daemon-e2e/real-provider-test-config.js"; -const BIG_PICKLE_MODEL = "opencode/big-pickle"; +const OPENCODE_REAL_TEST_MODEL = getRealProviderConfig("opencode").model; describe("OpenCode reasoning dedup", () => { let canRun = false; const logger = pino({ level: "silent" }); beforeAll(async () => { - canRun = await isProviderAvailable("opencode"); + canRun = await canRunRealProvider("opencode"); }); beforeEach((context) => { @@ -31,13 +34,13 @@ describe("OpenCode reasoning dedup", () => { test("reasoning content is not duplicated as assistant_message", async () => { const cwd = mkdtempSync(path.join(tmpdir(), "opencode-reasoning-dedup-")); - const client = new OpenCodeAgentClient(logger); + const client = createRealProviderClient("opencode", logger); try { const session = await client.createSession({ provider: "opencode", cwd, - model: BIG_PICKLE_MODEL, + model: OPENCODE_REAL_TEST_MODEL, modeId: "build", }); diff --git a/packages/server/src/server/agent/providers/opencode-retry-timeout.real.e2e.test.ts b/packages/server/src/server/agent/providers/opencode-retry-timeout.local.e2e.test.ts similarity index 100% rename from packages/server/src/server/agent/providers/opencode-retry-timeout.real.e2e.test.ts rename to packages/server/src/server/agent/providers/opencode-retry-timeout.local.e2e.test.ts diff --git a/packages/server/src/server/daemon-e2e/agent-configs.ts b/packages/server/src/server/daemon-e2e/agent-configs.ts index 762c1d09f..76932d84f 100644 --- a/packages/server/src/server/daemon-e2e/agent-configs.ts +++ b/packages/server/src/server/daemon-e2e/agent-configs.ts @@ -2,12 +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, dirname, resolve } from "node:path"; +import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -import { homedir } from "node:os"; import dotenv from "dotenv"; -import { isCommandAvailable } from "../../utils/executable.js"; const serverRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../../.."); dotenv.config({ path: resolve(serverRoot, ".env.test"), override: true }); @@ -63,7 +60,6 @@ export const agentConfigs = { } as const satisfies Record; export type AgentProvider = keyof typeof agentConfigs; -const providerAvailabilityCache = new Map>(); /** * Get test config for creating an agent with full permissions (no prompts). @@ -93,56 +89,7 @@ export function getAskModeConfig(provider: AgentProvider) { }; } -/** - * 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 isProviderAvailable(provider: AgentProvider): Promise { - const cached = providerAvailabilityCache.get(provider); - if (cached) { - return cached; - } - - const availability = (async (): Promise => { - switch (provider) { - case "claude": - const hasClaudeEnvCredentials = - Boolean(process.env.CLAUDE_CODE_OAUTH_TOKEN) || Boolean(process.env.ANTHROPIC_API_KEY); - return (await isCommandAvailable("claude")) && (!process.env.CI || hasClaudeEnvCredentials); - case "codex": - return ( - (await isCommandAvailable("codex")) && - (existsSync(join(homedir(), ".codex", "auth.json")) || - Boolean(process.env.OPENAI_API_KEY)) - ); - case "copilot": - return await isCommandAvailable("copilot"); - case "opencode": - return await isCommandAvailable("opencode"); - case "pi": - return ( - (await isCommandAvailable( - process.env.PI_COMMAND ?? process.env.PI_ACP_PI_COMMAND ?? "pi", - )) && - (Boolean(process.env.OPENAI_API_KEY) || - Boolean(process.env.ANTHROPIC_API_KEY) || - Boolean(process.env.OPENROUTER_API_KEY) || - existsSync(join(homedir(), ".pi", "agent", "auth.json"))) - ); - default: - return false; - } - })(); - - providerAvailabilityCache.set(provider, availability); - return availability; -} - /** * Helper to run a test for each provider. */ -export const allProviders: AgentProvider[] = ["claude", "codex", "copilot", "opencode", "pi"]; +export const allProviders: AgentProvider[] = ["claude", "codex", "opencode", "pi"]; 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 bd9e7a971..77bd16473 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 @@ -4,10 +4,13 @@ import { tmpdir } from "node:os"; import path from "node:path"; import pino from "pino"; -import { ClaudeAgentClient } from "../agent/providers/claude/agent.js"; import { DaemonClient } from "../test-utils/daemon-client.js"; import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js"; -import { getFullAccessConfig, isProviderAvailable } from "./agent-configs.js"; +import { + canRunRealProvider, + createRealProviderClients, + getRealProviderConfig, +} from "./real-provider-test-config.js"; function tmpCwd(): string { return mkdtempSync(path.join(tmpdir(), "daemon-real-claude-autonomous-simple-")); @@ -21,7 +24,7 @@ describe("daemon E2E (real claude) - autonomous wake simple", () => { let canRun = false; beforeAll(async () => { - canRun = await isProviderAvailable("claude"); + canRun = await canRunRealProvider("claude"); }); beforeEach((context) => { @@ -34,7 +37,7 @@ describe("daemon E2E (real claude) - autonomous wake simple", () => { const logger = pino({ level: "silent" }); const cwd = tmpCwd(); const daemon = await createTestPaseoDaemon({ - agentClients: { claude: new ClaudeAgentClient({ logger }) }, + agentClients: createRealProviderClients(["claude"], logger), logger, }); const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` }); @@ -48,7 +51,7 @@ describe("daemon E2E (real claude) - autonomous wake simple", () => { const agent = await client.createAgent({ cwd, title: "claude-autonomous-simple-real", - ...getFullAccessConfig("claude"), + ...getRealProviderConfig("claude"), }); const autonomousWakeToken = `AUTONOMOUS_SIMPLE_${Date.now().toString(36)}`; 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 d1de53e77..83cbc0521 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 @@ -4,10 +4,13 @@ import path from "node:path"; import pino from "pino"; import { beforeAll, beforeEach, describe, expect, test } from "vitest"; import { WebSocket } from "ws"; -import { ClaudeAgentClient } from "../agent/providers/claude/agent.js"; import { DaemonClient } from "../test-utils/daemon-client.js"; import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js"; -import { getFullAccessConfig, isProviderAvailable } from "./agent-configs.js"; +import { + canRunRealProvider, + createRealProviderClients, + getRealProviderConfig, +} from "./real-provider-test-config.js"; function tmpCwd(): string { return mkdtempSync(path.join(tmpdir(), "daemon-real-claude-autonomous-wake-")); @@ -427,7 +430,7 @@ describe("daemon E2E (real claude) - autonomous wake from background task", () = let canRun = false; beforeAll(async () => { - canRun = await isProviderAvailable("claude"); + canRun = await canRunRealProvider("claude"); }); beforeEach((context) => { @@ -440,7 +443,7 @@ describe("daemon E2E (real claude) - autonomous wake from background task", () = const logger = pino({ level: "silent" }); const cwd = tmpCwd(); const daemon = await createTestPaseoDaemon({ - agentClients: { claude: new ClaudeAgentClient({ logger }) }, + agentClients: createRealProviderClients(["claude"], logger), logger, }); const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` }); @@ -454,7 +457,7 @@ describe("daemon E2E (real claude) - autonomous wake from background task", () = const agent = await client.createAgent({ cwd, title: "claude-autonomous-abc-a", - ...getFullAccessConfig("claude"), + ...getRealProviderConfig("claude"), }); const autonomousWakeToken = `AUTONOMOUS_WAKE_${Date.now().toString(36)}`; @@ -513,7 +516,7 @@ describe("daemon E2E (real claude) - autonomous wake from background task", () = const logger = pino({ level: "silent" }); const cwd = tmpCwd(); const daemon = await createTestPaseoDaemon({ - agentClients: { claude: new ClaudeAgentClient({ logger }) }, + agentClients: createRealProviderClients(["claude"], logger), logger, }); const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` }); @@ -527,7 +530,7 @@ describe("daemon E2E (real claude) - autonomous wake from background task", () = const agent = await client.createAgent({ cwd, title: "claude-autonomous-abc-b", - ...getFullAccessConfig("claude"), + ...getRealProviderConfig("claude"), }); await client.sendMessage(agent.id, BACKGROUND_TASK_SLEEP_5_PROMPT); @@ -586,7 +589,7 @@ describe("daemon E2E (real claude) - autonomous wake from background task", () = const logger = pino({ level: "silent" }); const cwd = tmpCwd(); const daemon = await createTestPaseoDaemon({ - agentClients: { claude: new ClaudeAgentClient({ logger }) }, + agentClients: createRealProviderClients(["claude"], logger), logger, }); const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` }); @@ -600,7 +603,7 @@ describe("daemon E2E (real claude) - autonomous wake from background task", () = const agent = await client.createAgent({ cwd, title: "claude-autonomous-abc-c", - ...getFullAccessConfig("claude"), + ...getRealProviderConfig("claude"), }); await client.sendMessage(agent.id, BACKGROUND_TASK_SLEEP_5_PROMPT); @@ -652,7 +655,7 @@ describe("daemon E2E (real claude) - autonomous wake from background task", () = const logger = pino({ level: "silent" }); const cwd = tmpCwd(); const daemon = await createTestPaseoDaemon({ - agentClients: { claude: new ClaudeAgentClient({ logger }) }, + agentClients: createRealProviderClients(["claude"], logger), logger, }); const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` }); @@ -666,7 +669,7 @@ describe("daemon E2E (real claude) - autonomous wake from background task", () = const agent = await client.createAgent({ cwd, title: "claude-autonomous-wake-real", - ...getFullAccessConfig("claude"), + ...getRealProviderConfig("claude"), }); await client.sendMessage( @@ -710,7 +713,7 @@ describe("daemon E2E (real claude) - autonomous wake from background task", () = const logger = pino({ level: "silent" }); const cwd = tmpCwd(); const daemon = await createTestPaseoDaemon({ - agentClients: { claude: new ClaudeAgentClient({ logger }) }, + agentClients: createRealProviderClients(["claude"], logger), logger, }); const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` }); @@ -724,7 +727,7 @@ describe("daemon E2E (real claude) - autonomous wake from background task", () = const agent = await client.createAgent({ cwd, title: "claude-autonomous-followup-real", - ...getFullAccessConfig("claude"), + ...getRealProviderConfig("claude"), }); await client.sendMessage( @@ -755,7 +758,7 @@ describe("daemon E2E (real claude) - autonomous wake from background task", () = const logger = pino({ level: "silent" }); const cwd = tmpCwd(); const daemon = await createTestPaseoDaemon({ - agentClients: { claude: new ClaudeAgentClient({ logger }) }, + agentClients: createRealProviderClients(["claude"], logger), logger, }); const wsUrl = `ws://127.0.0.1:${daemon.port}/ws`; @@ -770,7 +773,7 @@ describe("daemon E2E (real claude) - autonomous wake from background task", () = const agent = await client.createAgent({ cwd, title: "claude-hang-repro-real", - ...getFullAccessConfig("claude"), + ...getRealProviderConfig("claude"), }); for (let cycle = 0; cycle < 20; cycle += 1) { @@ -858,7 +861,7 @@ describe("daemon E2E (real claude) - autonomous wake from background task", () = const logger = pino({ level: "silent" }); const cwd = tmpCwd(); const daemon = await createTestPaseoDaemon({ - agentClients: { claude: new ClaudeAgentClient({ logger }) }, + agentClients: createRealProviderClients(["claude"], logger), logger, }); const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` }); @@ -872,7 +875,7 @@ describe("daemon E2E (real claude) - autonomous wake from background task", () = const agent = await client.createAgent({ cwd, title: "claude-background-repro-real", - ...getFullAccessConfig("claude"), + ...getRealProviderConfig("claude"), }); await client.sendMessage(agent.id, BACKGROUND_TASK_SLEEP_5_PROMPT); @@ -930,7 +933,7 @@ describe("daemon E2E (real claude) - autonomous wake from background task", () = const logger = pino({ level: "silent" }); const cwd = tmpCwd(); const daemon = await createTestPaseoDaemon({ - agentClients: { claude: new ClaudeAgentClient({ logger }) }, + agentClients: createRealProviderClients(["claude"], logger), logger, }); const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` }); @@ -944,7 +947,7 @@ describe("daemon E2E (real claude) - autonomous wake from background task", () = const agent = await client.createAgent({ cwd, title: "claude-autonomous-race-stress-real", - ...getFullAccessConfig("claude"), + ...getRealProviderConfig("claude"), }); for (let cycle = 0; cycle < 20; cycle += 1) { @@ -1016,7 +1019,7 @@ describe("daemon E2E (real claude) - autonomous wake from background task", () = const logger = pino({ level: "silent" }); const cwd = tmpCwd(); const daemon = await createTestPaseoDaemon({ - agentClients: { claude: new ClaudeAgentClient({ logger }) }, + agentClients: createRealProviderClients(["claude"], logger), logger, }); const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` }); @@ -1030,7 +1033,7 @@ describe("daemon E2E (real claude) - autonomous wake from background task", () = const agent = await client.createAgent({ cwd, title: "claude-transcript-parity-race", - ...getFullAccessConfig("claude"), + ...getRealProviderConfig("claude"), }); await client.sendMessage(agent.id, BACKGROUND_TASK_SLEEP_5_PROMPT); diff --git a/packages/server/src/server/daemon-e2e/claude-rewind.real.e2e.test.ts b/packages/server/src/server/daemon-e2e/claude-rewind.real.e2e.test.ts index 8f43cad58..553591f24 100644 --- a/packages/server/src/server/daemon-e2e/claude-rewind.real.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/claude-rewind.real.e2e.test.ts @@ -1,13 +1,16 @@ import { readdir, writeFile } from "node:fs/promises"; import path from "node:path"; import pino from "pino"; -import { afterAll, beforeAll, describe, expect, test } from "vitest"; +import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest"; -import { ClaudeAgentClient } from "../agent/providers/claude/agent.js"; import type { AgentTimelineItem } from "../agent/agent-sdk-types.js"; import { DaemonClient } from "../test-utils/daemon-client.js"; import { createTestPaseoDaemon, type TestPaseoDaemon } from "../test-utils/paseo-daemon.js"; -import { getFullAccessConfig } from "./agent-configs.js"; +import { + canRunRealProvider, + createRealProviderClients, + getRealProviderConfig, +} from "./real-provider-test-config.js"; import { closeRewindSession, fetchTimelineItems, @@ -114,7 +117,7 @@ async function launchClaudeRewindSession( const agent = await harness.client.createAgent({ cwd, title, - ...getFullAccessConfig("claude"), + ...getRealProviderConfig("claude"), }); return { agentId: agent.id, cwd }; @@ -212,12 +215,17 @@ function assertTimeline(items: AgentTimelineItem[], expectedKept: ClaudeTurnSpec } describe("daemon E2E (real claude) - rewind", () => { + let canRun = false; let harness: ClaudeRewindHarness; beforeAll(async () => { + canRun = await canRunRealProvider("claude"); + if (!canRun) { + return; + } const logger = pino({ level: "silent" }); const daemon = await createTestPaseoDaemon({ - agentClients: { claude: new ClaudeAgentClient({ logger }) }, + agentClients: createRealProviderClients(["claude"], logger), logger, }); const client = new DaemonClient({ @@ -235,9 +243,16 @@ describe("daemon E2E (real claude) - rewind", () => { await harness?.daemon.close().catch(() => undefined); }); + beforeEach((context) => { + if (!canRun) { + context.skip(); + } + }); + test.each(MATRIX)( "$name", async (scenario) => { + if (!harness) throw new Error("Claude rewind harness was not initialized"); const session = await launchClaudeRewindSession( harness, `claude-rewind-${scenario.mode}-${scenario.turnCount}-${scenario.rewindTurn}`, diff --git a/packages/server/src/server/daemon-e2e/claude-thinking-memory.real.e2e.test.ts b/packages/server/src/server/daemon-e2e/claude-thinking-memory.real.e2e.test.ts index 5a2437f47..937f3d0c6 100644 --- a/packages/server/src/server/daemon-e2e/claude-thinking-memory.real.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/claude-thinking-memory.real.e2e.test.ts @@ -5,10 +5,13 @@ import path from "node:path"; import pino from "pino"; import type { AgentTimelineItem } from "../agent/agent-sdk-types.js"; -import { ClaudeAgentClient } from "../agent/providers/claude/agent.js"; import { DaemonClient } from "../test-utils/daemon-client.js"; import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js"; -import { getFullAccessConfig, isProviderAvailable } from "./agent-configs.js"; +import { + canRunRealProvider, + createRealProviderClients, + getRealProviderConfig, +} from "./real-provider-test-config.js"; function tmpCwd(): string { return mkdtempSync(path.join(tmpdir(), "daemon-real-claude-thinking-memory-")); @@ -48,7 +51,7 @@ describe("daemon E2E (real claude) - thinking effort memory", () => { let canRun = false; beforeAll(async () => { - canRun = await isProviderAvailable("claude"); + canRun = await canRunRealProvider("claude"); }); beforeEach((context) => { @@ -61,7 +64,7 @@ describe("daemon E2E (real claude) - thinking effort memory", () => { const logger = pino({ level: "silent" }); const cwd = tmpCwd(); const daemon = await createTestPaseoDaemon({ - agentClients: { claude: new ClaudeAgentClient({ logger }) }, + agentClients: createRealProviderClients(["claude"], logger), logger, }); const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` }); @@ -81,7 +84,7 @@ describe("daemon E2E (real claude) - thinking effort memory", () => { const agent = await client.createAgent({ cwd, title: "claude-thinking-memory-real", - ...getFullAccessConfig("claude"), + ...getRealProviderConfig("claude"), model: model.id, }); diff --git a/packages/server/src/server/daemon-e2e/codex-auto-review.real.e2e.test.ts b/packages/server/src/server/daemon-e2e/codex-auto-review.local.e2e.test.ts similarity index 100% rename from packages/server/src/server/daemon-e2e/codex-auto-review.real.e2e.test.ts rename to packages/server/src/server/daemon-e2e/codex-auto-review.local.e2e.test.ts diff --git a/packages/server/src/server/daemon-e2e/codex-goal-mid-turn.real.e2e.test.ts b/packages/server/src/server/daemon-e2e/codex-goal-mid-turn.real.e2e.test.ts index cfbdf9555..9e4722ea2 100644 --- a/packages/server/src/server/daemon-e2e/codex-goal-mid-turn.real.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/codex-goal-mid-turn.real.e2e.test.ts @@ -4,11 +4,14 @@ import { tmpdir } from "node:os"; import path from "node:path"; import pino from "pino"; -import { CodexAppServerAgentClient } from "../agent/providers/codex-app-server-agent.js"; import { resolveBinaryVersion } from "../agent/providers/diagnostic-utils.js"; import { DaemonClient } from "../test-utils/daemon-client.js"; import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js"; -import { getFullAccessConfig, isProviderAvailable } from "./agent-configs.js"; +import { + canRunRealProvider, + createRealProviderClients, + getRealProviderConfig, +} from "./real-provider-test-config.js"; function tmpCwd(): string { return mkdtempSync(path.join(tmpdir(), "daemon-codex-goal-")); @@ -89,7 +92,7 @@ describe("daemon E2E (real codex) - /goal command", () => { let canRun = false; beforeAll(async () => { - if (!(await isProviderAvailable("codex"))) { + if (!(await canRunRealProvider("codex"))) { return; } const versionOutput = await resolveBinaryVersion("codex"); @@ -106,7 +109,7 @@ describe("daemon E2E (real codex) - /goal command", () => { const logger = pino({ level: "silent" }); const cwd = tmpCwd(); const daemon = await createTestPaseoDaemon({ - agentClients: { codex: new CodexAppServerAgentClient(logger) }, + agentClients: createRealProviderClients(["codex"], logger), logger, }); const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` }); @@ -118,7 +121,7 @@ describe("daemon E2E (real codex) - /goal command", () => { const agent = await client.createAgent({ cwd, title: "codex-goal-set", - ...getFullAccessConfig("codex"), + ...getRealProviderConfig("codex"), }); const sentinel = "TX9Q4Z"; @@ -160,7 +163,7 @@ describe("daemon E2E (real codex) - /goal command", () => { const logger = pino({ level: "silent" }); const cwd = tmpCwd(); const daemon = await createTestPaseoDaemon({ - agentClients: { codex: new CodexAppServerAgentClient(logger) }, + agentClients: createRealProviderClients(["codex"], logger), logger, }); const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` }); @@ -172,7 +175,7 @@ describe("daemon E2E (real codex) - /goal command", () => { const agent = await client.createAgent({ cwd, title: "codex-goal-mid-turn", - ...getFullAccessConfig("codex"), + ...getRealProviderConfig("codex"), }); // Set an initial goal so pause has something to act on. @@ -237,7 +240,7 @@ describe("daemon E2E (real codex) - /goal command", () => { const logger = pino({ level: "silent" }); const cwd = tmpCwd(); const daemon = await createTestPaseoDaemon({ - agentClients: { codex: new CodexAppServerAgentClient(logger) }, + agentClients: createRealProviderClients(["codex"], logger), logger, }); const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` }); @@ -249,7 +252,7 @@ describe("daemon E2E (real codex) - /goal command", () => { const agent = await client.createAgent({ cwd, title: "codex-goal-clear", - ...getFullAccessConfig("codex"), + ...getRealProviderConfig("codex"), }); await client.sendMessage(agent.id, "/goal something to be cleared"); diff --git a/packages/server/src/server/daemon-e2e/codex-rewind.real.e2e.test.ts b/packages/server/src/server/daemon-e2e/codex-rewind.real.e2e.test.ts index 493e33012..a3c403105 100644 --- a/packages/server/src/server/daemon-e2e/codex-rewind.real.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/codex-rewind.real.e2e.test.ts @@ -1,13 +1,16 @@ import { readdir, writeFile } from "node:fs/promises"; import path from "node:path"; import pino from "pino"; -import { afterAll, beforeAll, describe, expect, test } from "vitest"; +import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest"; -import { CodexAppServerAgentClient } from "../agent/providers/codex-app-server-agent.js"; import type { AgentTimelineItem } from "../agent/agent-sdk-types.js"; import { DaemonClient } from "../test-utils/daemon-client.js"; import { createTestPaseoDaemon, type TestPaseoDaemon } from "../test-utils/paseo-daemon.js"; -import { getFullAccessConfig } from "./agent-configs.js"; +import { + canRunRealProvider, + createRealProviderClients, + getRealProviderConfig, +} from "./real-provider-test-config.js"; import { closeRewindSession, fetchTimelineItems, @@ -84,7 +87,7 @@ async function launchCodexRewindSession( const agent = await harness.client.createAgent({ cwd, title, - ...getFullAccessConfig("codex"), + ...getRealProviderConfig("codex"), }); return { agentId: agent.id, cwd, scratchPath }; @@ -142,12 +145,17 @@ async function askCodexToEditFile( } describe("daemon E2E (real codex) - rewind", () => { + let canRun = false; let harness: CodexRewindHarness; beforeAll(async () => { + canRun = await canRunRealProvider("codex"); + if (!canRun) { + return; + } const logger = pino({ level: "silent" }); const daemon = await createTestPaseoDaemon({ - agentClients: { codex: new CodexAppServerAgentClient(logger) }, + agentClients: createRealProviderClients(["codex"], logger), logger, }); const client = new DaemonClient({ @@ -165,7 +173,14 @@ describe("daemon E2E (real codex) - rewind", () => { await harness?.daemon.close().catch(() => undefined); }); + beforeEach((context) => { + if (!canRun) { + context.skip(); + } + }); + test("rewinds a real Codex conversation after a single edit turn while leaving the file edit on disk", async () => { + if (!harness) throw new Error("Codex rewind harness was not initialized"); const session = await launchCodexRewindSession(harness, "codex-rewind-single-edit-real"); try { 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 5938cc707..71e9f9ed7 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 @@ -6,8 +6,11 @@ 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 { isProviderAvailable } from "./agent-configs.js"; +import { + canRunRealProvider, + createRealProviderClients, + getRealProviderConfig, +} from "./real-provider-test-config.js"; function tmpCwd(): string { return mkdtempSync(path.join(tmpdir(), "daemon-claude-model-init-")); @@ -17,7 +20,7 @@ describe("daemon E2E (real claude) - model resolution on init", () => { let canRun = false; beforeAll(async () => { - canRun = await isProviderAvailable("claude"); + canRun = await canRunRealProvider("claude"); }); beforeEach((context) => { @@ -30,7 +33,7 @@ describe("daemon E2E (real claude) - model resolution on init", () => { const logger = pino({ level: "silent" }); const cwd = tmpCwd(); const daemon = await createTestPaseoDaemon({ - agentClients: { claude: new ClaudeAgentClient({ logger }) }, + agentClients: createRealProviderClients(["claude"], logger), logger, }); const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` }); @@ -45,9 +48,8 @@ describe("daemon E2E (real claude) - model resolution on init", () => { const catalogModelIds = new Set(modelsResult.models.map((m) => m.id)); const agent = await client.createAgent({ - provider: "claude", + ...getRealProviderConfig("claude"), cwd, - model: "haiku", title: "model-init-test", }); 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 761fe6a40..cecbeebe9 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 @@ -6,8 +6,7 @@ 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 { isProviderAvailable } from "./agent-configs.js"; +import { canRunRealProvider, createRealProviderClients } from "./real-provider-test-config.js"; function tmpCwd(): string { return mkdtempSync(path.join(tmpdir(), "daemon-claude-runtime-model-reconcile-")); @@ -17,7 +16,7 @@ describe("daemon E2E (real claude) - runtime model reconciliation", () => { let canRun = false; beforeAll(async () => { - canRun = await isProviderAvailable("claude"); + canRun = await canRunRealProvider("claude"); }); beforeEach((context) => { @@ -30,7 +29,7 @@ describe("daemon E2E (real claude) - runtime model reconciliation", () => { const logger = pino({ level: "silent" }); const cwd = tmpCwd(); const daemon = await createTestPaseoDaemon({ - agentClients: { claude: new ClaudeAgentClient({ logger }) }, + agentClients: createRealProviderClients(["claude"], logger), logger, }); const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` }); diff --git a/packages/server/src/server/daemon-e2e/opencode-custom-agents.real.e2e.test.ts b/packages/server/src/server/daemon-e2e/opencode-custom-agents.real.e2e.test.ts index b0d569647..048a8dd0c 100644 --- a/packages/server/src/server/daemon-e2e/opencode-custom-agents.real.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/opencode-custom-agents.real.e2e.test.ts @@ -4,10 +4,9 @@ import { tmpdir } from "node:os"; import path from "node:path"; import pino from "pino"; -import { OpenCodeAgentClient } from "../agent/providers/opencode-agent.js"; import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js"; import { DaemonClient } from "../test-utils/daemon-client.js"; -import { isProviderAvailable } from "./agent-configs.js"; +import { canRunRealProvider, createRealProviderClients } from "./real-provider-test-config.js"; function tmpCwd(): string { return mkdtempSync(path.join(tmpdir(), "daemon-real-opencode-custom-")); @@ -19,7 +18,7 @@ async function createHarness(): Promise<{ }> { const logger = pino({ level: "silent" }); const daemon = await createTestPaseoDaemon({ - agentClients: { opencode: new OpenCodeAgentClient(logger) }, + agentClients: createRealProviderClients(["opencode"], logger), logger, }); const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` }); @@ -32,7 +31,7 @@ describe("daemon E2E (real opencode) - custom agent discovery", () => { let canRun = false; beforeAll(async () => { - canRun = await isProviderAvailable("opencode"); + canRun = await canRunRealProvider("opencode"); }); beforeEach((context) => { diff --git a/packages/server/src/server/daemon-e2e/opencode-draft-features.real.e2e.test.ts b/packages/server/src/server/daemon-e2e/opencode-draft-features.real.e2e.test.ts index fcdb015cf..d4a196a97 100644 --- a/packages/server/src/server/daemon-e2e/opencode-draft-features.real.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/opencode-draft-features.real.e2e.test.ts @@ -5,12 +5,11 @@ import path from "node:path"; import pino from "pino"; import { beforeAll, beforeEach, describe, expect, test } from "vitest"; -import type { PersistedAgentDescriptor } from "../agent/agent-sdk-types.js"; -import { OpenCodeAgentClient } from "../agent/providers/opencode-agent.js"; +import type { AgentClient, PersistedAgentDescriptor } from "../agent/agent-sdk-types.js"; import { OpenCodeServerManager } from "../agent/providers/opencode/server-manager.js"; import { DaemonClient } from "../test-utils/daemon-client.js"; import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js"; -import { isProviderAvailable } from "./agent-configs.js"; +import { canRunRealProvider, createRealProviderClient } from "./real-provider-test-config.js"; function tmpCwd(): string { const dir = mkdtempSync(path.join(tmpdir(), "daemon-real-opencode-draft-features-")); @@ -18,7 +17,7 @@ function tmpCwd(): string { } async function withConnectedOpenCodeDaemon( - provider: OpenCodeAgentClient, + provider: AgentClient, run: (context: { client: DaemonClient }) => Promise, ): Promise { const logger = pino({ level: "silent" }); @@ -41,7 +40,7 @@ async function withConnectedOpenCodeDaemon( } async function deletePersistedSessions( - provider: OpenCodeAgentClient, + provider: AgentClient, sessions: ReadonlyArray, ): Promise { await Promise.all( @@ -56,7 +55,7 @@ describe("daemon E2E (real opencode) - draft feature discovery", () => { let canRun = false; beforeAll(async () => { - canRun = await isProviderAvailable("opencode"); + canRun = await canRunRealProvider("opencode"); }); beforeEach((context) => { @@ -67,12 +66,12 @@ describe("daemon E2E (real opencode) - draft feature discovery", () => { test("listing draft features does not leave an OpenCode provider session behind", async () => { const logger = pino({ level: "silent" }); - const provider = new OpenCodeAgentClient(logger); + const provider = createRealProviderClient("opencode", logger); const cwd = tmpCwd(); let after: PersistedAgentDescriptor[] = []; try { - expect(await provider.listPersistedAgents({ cwd })).toEqual([]); + expect(await provider.listPersistedAgents?.({ cwd })).toEqual([]); await withConnectedOpenCodeDaemon(provider, async ({ client }) => { const response = await client.listProviderFeatures({ @@ -85,7 +84,7 @@ describe("daemon E2E (real opencode) - draft feature discovery", () => { expect(response.features ?? []).toEqual([]); }); - after = await provider.listPersistedAgents({ cwd }); + after = (await provider.listPersistedAgents?.({ cwd })) ?? []; } finally { await deletePersistedSessions(provider, after); rmSync(cwd, { recursive: true, force: true }); @@ -97,12 +96,12 @@ describe("daemon E2E (real opencode) - draft feature discovery", () => { test("listing draft commands does not leave an OpenCode provider session behind", async () => { const logger = pino({ level: "silent" }); - const provider = new OpenCodeAgentClient(logger); + const provider = createRealProviderClient("opencode", logger); const cwd = tmpCwd(); let after: PersistedAgentDescriptor[] = []; try { - expect(await provider.listPersistedAgents({ cwd })).toEqual([]); + expect(await provider.listPersistedAgents?.({ cwd })).toEqual([]); await withConnectedOpenCodeDaemon(provider, async ({ client }) => { const response = await client.listCommands("draft-opencode-agent", { @@ -116,7 +115,7 @@ describe("daemon E2E (real opencode) - draft feature discovery", () => { expect(response.commands.length).toBeGreaterThan(0); }); - after = await provider.listPersistedAgents({ cwd }); + after = (await provider.listPersistedAgents?.({ cwd })) ?? []; } finally { await deletePersistedSessions(provider, after); rmSync(cwd, { recursive: true, force: true }); diff --git a/packages/server/src/server/daemon-e2e/opencode-import-persistence.real.e2e.test.ts b/packages/server/src/server/daemon-e2e/opencode-import-persistence.real.e2e.test.ts index 4500cb459..3d1aec0a9 100644 --- a/packages/server/src/server/daemon-e2e/opencode-import-persistence.real.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/opencode-import-persistence.real.e2e.test.ts @@ -6,13 +6,16 @@ import path from "node:path"; import pino from "pino"; import type { AgentPersistenceHandle, AgentTimelineItem } from "../agent/agent-sdk-types.js"; -import { OpenCodeAgentClient } from "../agent/providers/opencode-agent.js"; import { DaemonClient } from "../test-utils/daemon-client.js"; import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js"; -import { isProviderAvailable } from "./agent-configs.js"; +import { + canRunRealProvider, + createRealProviderClients, + getRealProviderConfig, +} from "./real-provider-test-config.js"; import type { FetchRecentProviderSessionEntry } from "../../client/daemon-client.js"; -const OPENCODE_REAL_TEST_MODEL = "opencode/big-pickle"; +const OPENCODE_REAL_TEST_MODEL = getRealProviderConfig("opencode").model; const OPENCODE_REAL_TEST_TIMEOUT_MS = 180_000; function tmpCwd(): string { @@ -25,7 +28,7 @@ async function withConnectedOpenCodeDaemon( ): Promise { const logger = pino({ level: "silent" }); const daemon = await createTestPaseoDaemon({ - agentClients: { opencode: new OpenCodeAgentClient(logger) }, + agentClients: createRealProviderClients(["opencode"], logger), logger, }); const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` }); @@ -112,7 +115,7 @@ describe("daemon E2E (real opencode) - persisted import resume", () => { let canRun = false; beforeAll(async () => { - canRun = await isProviderAvailable("opencode"); + canRun = await canRunRealProvider("opencode"); }); beforeEach((context) => { diff --git a/packages/server/src/server/daemon-e2e/opencode-initial-prompt-wait.real.e2e.test.ts b/packages/server/src/server/daemon-e2e/opencode-initial-prompt-wait.real.e2e.test.ts index b71a7f03d..585194628 100644 --- a/packages/server/src/server/daemon-e2e/opencode-initial-prompt-wait.real.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/opencode-initial-prompt-wait.real.e2e.test.ts @@ -4,12 +4,15 @@ import { tmpdir } from "node:os"; import path from "node:path"; import pino from "pino"; -import { OpenCodeAgentClient } from "../agent/providers/opencode-agent.js"; import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js"; import { DaemonClient } from "../test-utils/daemon-client.js"; -import { isProviderAvailable } from "./agent-configs.js"; +import { + canRunRealProvider, + createRealProviderClients, + getRealProviderConfig, +} from "./real-provider-test-config.js"; -const OPENCODE_REAL_TEST_MODEL = "opencode/big-pickle"; +const OPENCODE_REAL_TEST_MODEL = getRealProviderConfig("opencode").model; function tmpCwd(): string { return mkdtempSync(path.join(tmpdir(), "daemon-real-opencode-init-prompt-")); @@ -21,7 +24,7 @@ async function createHarness(): Promise<{ }> { const logger = pino({ level: "silent" }); const daemon = await createTestPaseoDaemon({ - agentClients: { opencode: new OpenCodeAgentClient(logger) }, + agentClients: createRealProviderClients(["opencode"], logger), logger, }); const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` }); @@ -34,7 +37,7 @@ describe("daemon E2E (real opencode) - initial prompt wait", () => { let canRun = false; beforeAll(async () => { - canRun = await isProviderAvailable("opencode"); + canRun = await canRunRealProvider("opencode"); }); beforeEach((context) => { diff --git a/packages/server/src/server/daemon-e2e/opencode-invalid-mode.real.e2e.test.ts b/packages/server/src/server/daemon-e2e/opencode-invalid-mode.real.e2e.test.ts index 6c431d1e1..aa67c0a32 100644 --- a/packages/server/src/server/daemon-e2e/opencode-invalid-mode.real.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/opencode-invalid-mode.real.e2e.test.ts @@ -4,10 +4,9 @@ import { tmpdir } from "node:os"; import path from "node:path"; import pino from "pino"; -import { OpenCodeAgentClient } from "../agent/providers/opencode-agent.js"; import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js"; import { DaemonClient } from "../test-utils/daemon-client.js"; -import { isProviderAvailable } from "./agent-configs.js"; +import { canRunRealProvider, createRealProviderClients } from "./real-provider-test-config.js"; function tmpCwd(): string { return mkdtempSync(path.join(tmpdir(), "daemon-real-opencode-invalid-mode-")); @@ -19,7 +18,7 @@ async function createHarness(): Promise<{ }> { const logger = pino({ level: "silent" }); const daemon = await createTestPaseoDaemon({ - agentClients: { opencode: new OpenCodeAgentClient(logger) }, + agentClients: createRealProviderClients(["opencode"], logger), logger, }); const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` }); @@ -32,7 +31,7 @@ describe("daemon E2E (real opencode) - invalid mode handling", () => { let canRun = false; beforeAll(async () => { - canRun = await isProviderAvailable("opencode"); + canRun = await canRunRealProvider("opencode"); }); beforeEach((context) => { diff --git a/packages/server/src/server/daemon-e2e/opencode-invalid-model.real.e2e.test.ts b/packages/server/src/server/daemon-e2e/opencode-invalid-model.real.e2e.test.ts index 6d0af170c..4796f0423 100644 --- a/packages/server/src/server/daemon-e2e/opencode-invalid-model.real.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/opencode-invalid-model.real.e2e.test.ts @@ -4,10 +4,9 @@ import { tmpdir } from "node:os"; import path from "node:path"; import pino from "pino"; -import { OpenCodeAgentClient } from "../agent/providers/opencode-agent.js"; import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js"; import { DaemonClient } from "../test-utils/daemon-client.js"; -import { isProviderAvailable } from "./agent-configs.js"; +import { canRunRealProvider, createRealProviderClients } from "./real-provider-test-config.js"; function tmpCwd(): string { return mkdtempSync(path.join(tmpdir(), "daemon-real-opencode-invalid-model-")); @@ -19,7 +18,7 @@ async function createHarness(): Promise<{ }> { const logger = pino({ level: "silent" }); const daemon = await createTestPaseoDaemon({ - agentClients: { opencode: new OpenCodeAgentClient(logger) }, + agentClients: createRealProviderClients(["opencode"], logger), logger, }); const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` }); @@ -32,7 +31,7 @@ describe("daemon E2E (real opencode) - invalid model handling", () => { let canRun = false; beforeAll(async () => { - canRun = await isProviderAvailable("opencode"); + canRun = await canRunRealProvider("opencode"); }); beforeEach((context) => { diff --git a/packages/server/src/server/daemon-e2e/opencode-plan-and-questions.real.e2e.test.ts b/packages/server/src/server/daemon-e2e/opencode-plan-and-questions.real.e2e.test.ts index 07c8f3d0d..a75d5207b 100644 --- a/packages/server/src/server/daemon-e2e/opencode-plan-and-questions.real.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/opencode-plan-and-questions.real.e2e.test.ts @@ -4,10 +4,9 @@ import { tmpdir } from "node:os"; import path from "node:path"; import pino from "pino"; -import { OpenCodeAgentClient } from "../agent/providers/opencode-agent.js"; import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js"; import { DaemonClient } from "../test-utils/daemon-client.js"; -import { isProviderAvailable } from "./agent-configs.js"; +import { canRunRealProvider, createRealProviderClients } from "./real-provider-test-config.js"; function tmpCwd(): string { return mkdtempSync(path.join(tmpdir(), "daemon-real-opencode-")); @@ -19,7 +18,7 @@ async function createHarness(): Promise<{ }> { const logger = pino({ level: "silent" }); const daemon = await createTestPaseoDaemon({ - agentClients: { opencode: new OpenCodeAgentClient(logger) }, + agentClients: createRealProviderClients(["opencode"], logger), logger, }); const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` }); @@ -32,7 +31,7 @@ describe("daemon E2E (real opencode) - plan mode and clarifying questions", () = let canRun = false; beforeAll(async () => { - canRun = await isProviderAvailable("opencode"); + canRun = await canRunRealProvider("opencode"); }); beforeEach((context) => { diff --git a/packages/server/src/server/daemon-e2e/opencode-rewind.real.e2e.test.ts b/packages/server/src/server/daemon-e2e/opencode-rewind.real.e2e.test.ts index 2a203969c..995da59ef 100644 --- a/packages/server/src/server/daemon-e2e/opencode-rewind.real.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/opencode-rewind.real.e2e.test.ts @@ -2,14 +2,17 @@ import { execFileSync } from "node:child_process"; import { readdir, writeFile } from "node:fs/promises"; import path from "node:path"; import pino from "pino"; -import { afterAll, beforeAll, describe, expect, test } from "vitest"; +import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest"; -import { OpenCodeAgentClient } from "../agent/providers/opencode-agent.js"; import type { AgentTimelineItem } from "../agent/agent-sdk-types.js"; import type { AgentLifecycleStatus } from "@getpaseo/protocol/agent-lifecycle"; import { DaemonClient } from "../test-utils/daemon-client.js"; import { createTestPaseoDaemon, type TestPaseoDaemon } from "../test-utils/paseo-daemon.js"; -import { getFullAccessConfig } from "./agent-configs.js"; +import { + canRunRealProvider, + createRealProviderClients, + getRealProviderConfig, +} from "./real-provider-test-config.js"; import { closeRewindSession, fetchTimelineItems, @@ -56,7 +59,7 @@ async function launchOpenCodeRewindSession( const agent = await harness.client.createAgent({ cwd, title, - ...getFullAccessConfig("opencode"), + ...getRealProviderConfig("opencode"), }); return { agentId: agent.id, cwd, scratchPath }; @@ -212,12 +215,17 @@ async function rewindOpenCode( } describe("daemon E2E (real opencode) - rewind", () => { + let canRun = false; let harness: OpenCodeRewindHarness; beforeAll(async () => { + canRun = await canRunRealProvider("opencode"); + if (!canRun) { + return; + } const logger = pino({ level: "silent" }); const daemon = await createTestPaseoDaemon({ - agentClients: { opencode: new OpenCodeAgentClient(logger) }, + agentClients: createRealProviderClients(["opencode"], logger), logger, }); const client = new DaemonClient({ @@ -235,7 +243,14 @@ describe("daemon E2E (real opencode) - rewind", () => { await harness?.daemon.close().catch(() => undefined); }); + beforeEach((context) => { + if (!canRun) { + context.skip(); + } + }); + test("rewinds conversation and files after a single real OpenCode edit turn", async () => { + if (!harness) throw new Error("OpenCode rewind harness was not initialized"); const session = await launchOpenCodeRewindSession(harness, "opencode-rewind-single-edit-real"); try { diff --git a/packages/server/src/server/daemon-e2e/opencode-send-interrupt.real.e2e.test.ts b/packages/server/src/server/daemon-e2e/opencode-send-interrupt.real.e2e.test.ts index 15f264592..64e0ecdd8 100644 --- a/packages/server/src/server/daemon-e2e/opencode-send-interrupt.real.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/opencode-send-interrupt.real.e2e.test.ts @@ -4,11 +4,10 @@ import { tmpdir } from "node:os"; import path from "node:path"; import pino from "pino"; -import { OpenCodeAgentClient } from "../agent/providers/opencode-agent.js"; import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js"; import { DaemonClient, type WaitForFinishResult } from "../test-utils/daemon-client.js"; import { createMessageCollector } from "../test-utils/message-collector.js"; -import { isProviderAvailable } from "./agent-configs.js"; +import { canRunRealProvider, createRealProviderClients } from "./real-provider-test-config.js"; import type { AgentPermissionRequest } from "../agent/agent-sdk-types.js"; import type { SessionOutboundMessage } from "../messages.js"; @@ -252,7 +251,7 @@ async function createHarness(): Promise<{ }> { const logger = pino({ level: "silent" }); const daemon = await createTestPaseoDaemon({ - agentClients: { opencode: new OpenCodeAgentClient(logger) }, + agentClients: createRealProviderClients(["opencode"], logger), logger, }); const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` }); @@ -265,7 +264,7 @@ describe("daemon E2E (real opencode) - send while working and interrupt", () => let canRun = false; beforeAll(async () => { - canRun = await isProviderAvailable("opencode"); + canRun = await canRunRealProvider("opencode"); }); beforeEach((context) => { diff --git a/packages/server/src/server/daemon-e2e/pi-rewind.real.e2e.test.ts b/packages/server/src/server/daemon-e2e/pi-rewind.real.e2e.test.ts index 939e17f47..848e0fec8 100644 --- a/packages/server/src/server/daemon-e2e/pi-rewind.real.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/pi-rewind.real.e2e.test.ts @@ -1,11 +1,11 @@ import { readdir } from "node:fs/promises"; import pino from "pino"; -import { afterAll, beforeAll, describe, expect, test } from "vitest"; +import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest"; -import { PiRpcAgentClient } from "../agent/providers/pi/agent.js"; import type { AgentTimelineItem } from "../agent/agent-sdk-types.js"; import { DaemonClient } from "../test-utils/daemon-client.js"; import { createTestPaseoDaemon, type TestPaseoDaemon } from "../test-utils/paseo-daemon.js"; +import { canRunRealProvider, createRealProviderClients } from "./real-provider-test-config.js"; import { closeRewindSession, fetchTimelineItems, @@ -103,12 +103,17 @@ async function askPi( } describe("daemon E2E (real pi) - rewind", () => { + let canRun = false; let harness: PiRewindHarness; beforeAll(async () => { + canRun = await canRunRealProvider("pi"); + if (!canRun) { + return; + } const logger = pino({ level: "silent" }); const daemon = await createTestPaseoDaemon({ - agentClients: { pi: new PiRpcAgentClient({ logger }) }, + agentClients: createRealProviderClients(["pi"], logger), logger, }); const client = new DaemonClient({ @@ -126,7 +131,14 @@ describe("daemon E2E (real pi) - rewind", () => { await harness?.daemon.close().catch(() => undefined); }); + beforeEach((context) => { + if (!canRun) { + context.skip(); + } + }); + test("rewinds a real Pi conversation after a single turn and keeps the session usable", async () => { + if (!harness) throw new Error("Pi rewind harness was not initialized"); const session = await launchPiRewindSession(harness, "pi-rewind-single-conversation-real"); try { diff --git a/packages/server/src/server/daemon-e2e/pi.real.e2e.test.ts b/packages/server/src/server/daemon-e2e/pi.real.e2e.test.ts index e2fa0a02d..8b2a9cd40 100644 --- a/packages/server/src/server/daemon-e2e/pi.real.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/pi.real.e2e.test.ts @@ -6,19 +6,24 @@ import { beforeAll, beforeEach, expect, test } from "vitest"; import pino from "pino"; import type { + AgentClient, AgentPersistenceHandle, AgentStreamEvent, AgentTimelineItem, } from "../agent/agent-sdk-types.js"; -import { PiRpcAgentClient } from "../agent/providers/pi/agent.js"; import { DaemonClient } from "../test-utils/daemon-client.js"; import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js"; -import { isProviderAvailable } from "./agent-configs.js"; +import { + canRunRealProvider, + createRealProviderClient, + createRealProviderClients, + getRealProviderConfig, +} from "./real-provider-test-config.js"; process.env.PASEO_SUPERVISED = "0"; const PI_TEST_TIMEOUT_MS = 240_000; -const PI_REAL_TEST_MODEL = "openrouter/google/gemini-2.5-flash-lite"; +const PI_REAL_TEST_MODEL = getRealProviderConfig("pi").model; type ToolCallItem = Extract; @@ -26,14 +31,14 @@ function tmpCwd(prefix = "daemon-real-pi-"): string { return mkdtempSync(path.join(tmpdir(), prefix)); } -function createPiClient(): PiRpcAgentClient { - return new PiRpcAgentClient({ logger: pino({ level: "silent" }) }); +function createPiClient(): AgentClient { + return createRealProviderClient("pi", pino({ level: "silent" })); } function createPiToolDaemon() { const logger = pino({ level: "silent" }); return createTestPaseoDaemon({ - agentClients: { pi: new PiRpcAgentClient({ logger }) }, + agentClients: createRealProviderClients(["pi"], logger), logger, }); } @@ -87,7 +92,7 @@ async function withConnectedPiDaemon( let canRun = false; beforeAll(async () => { - canRun = await isProviderAvailable("pi"); + canRun = await canRunRealProvider("pi"); }); beforeEach((context) => { diff --git a/packages/server/src/server/daemon-e2e/real-provider-test-config.ts b/packages/server/src/server/daemon-e2e/real-provider-test-config.ts new file mode 100644 index 000000000..b7edca291 --- /dev/null +++ b/packages/server/src/server/daemon-e2e/real-provider-test-config.ts @@ -0,0 +1,170 @@ +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; + +import type { Logger } from "pino"; + +import type { AgentClient, AgentProvider, AgentSessionConfig } from "../agent/agent-sdk-types.js"; +import type { ProviderRuntimeSettings } from "../agent/provider-launch-config.js"; +import { ClaudeAgentClient } from "../agent/providers/claude/agent.js"; +import { CodexAppServerAgentClient } from "../agent/providers/codex-app-server-agent.js"; +import { OpenCodeAgentClient } from "../agent/providers/opencode-agent.js"; +import { PiRpcAgentClient } from "../agent/providers/pi/agent.js"; +import { isCommandAvailable } from "../../utils/executable.js"; + +export const realProviders = ["claude", "codex", "opencode", "pi"] as const; +export type RealProvider = (typeof realProviders)[number]; +export type RealProviderConfig = Pick< + AgentSessionConfig, + "provider" | "model" | "modeId" | "thinkingOptionId" +>; + +const OPENROUTER_BASE_URL = "https://openrouter.ai/api"; +const OPENROUTER_OPENAI_BASE_URL = "https://openrouter.ai/api"; +const CLAUDE_REAL_TEST_MODEL = "haiku"; +const CODEX_REAL_TEST_MODEL = "~openai/gpt-latest"; +const OPENCODE_REAL_TEST_MODEL = "openrouter/google/gemini-2.5-flash-lite"; +const PI_REAL_TEST_MODEL = "openrouter/google/gemini-2.5-flash-lite"; + +const availabilityCache = new Map>(); + +export function getRealProviderConfig(provider: RealProvider): RealProviderConfig { + switch (provider) { + case "claude": + return { + provider, + model: CLAUDE_REAL_TEST_MODEL, + modeId: "bypassPermissions", + }; + case "codex": + return { + provider, + model: CODEX_REAL_TEST_MODEL, + thinkingOptionId: "low", + modeId: "full-access", + }; + case "opencode": + return { + provider, + model: OPENCODE_REAL_TEST_MODEL, + modeId: "build", + }; + case "pi": + return { + provider, + model: PI_REAL_TEST_MODEL, + thinkingOptionId: "medium", + }; + } +} + +export function getRealProviderRuntimeSettings(provider: RealProvider): ProviderRuntimeSettings { + const apiKey = getOpenRouterApiKey(); + switch (provider) { + case "claude": + return { + env: { + OPENROUTER_API_KEY: apiKey, + ANTHROPIC_BASE_URL: OPENROUTER_BASE_URL, + ANTHROPIC_AUTH_TOKEN: apiKey, + ANTHROPIC_API_KEY: "", + ANTHROPIC_DEFAULT_HAIKU_MODEL: "~anthropic/claude-haiku-latest", + ANTHROPIC_DEFAULT_SONNET_MODEL: "~anthropic/claude-sonnet-latest", + ANTHROPIC_DEFAULT_OPUS_MODEL: "~anthropic/claude-opus-latest", + CLAUDE_CODE_SUBAGENT_MODEL: "~anthropic/claude-haiku-latest", + }, + }; + case "codex": + return { + env: { + OPENROUTER_API_KEY: apiKey, + OPENAI_API_KEY: apiKey, + OPENAI_BASE_URL: OPENROUTER_OPENAI_BASE_URL, + }, + }; + case "opencode": { + const root = mkdtempSync(path.join(tmpdir(), "paseo-real-opencode-")); + return { + env: { + OPENROUTER_API_KEY: apiKey, + OPENCODE_DISABLE_AUTO_UPDATE: "1", + XDG_CONFIG_HOME: path.join(root, "config"), + XDG_DATA_HOME: path.join(root, "data"), + XDG_CACHE_HOME: path.join(root, "cache"), + }, + }; + } + case "pi": + return { + env: { + OPENROUTER_API_KEY: apiKey, + }, + }; + } +} + +export function createRealProviderClient(provider: RealProvider, logger: Logger): AgentClient { + const runtimeSettings = getRealProviderRuntimeSettings(provider); + switch (provider) { + case "claude": + return new ClaudeAgentClient({ logger, runtimeSettings }); + case "codex": + return new CodexAppServerAgentClient(logger, runtimeSettings, { + customProvider: { + id: "codex-openrouter", + label: "Codex OpenRouter", + extends: "codex", + }, + }); + case "opencode": + return new OpenCodeAgentClient(logger, runtimeSettings); + case "pi": + return new PiRpcAgentClient({ logger, runtimeSettings }); + } +} + +export function createRealProviderClients( + providers: readonly RealProvider[], + logger: Logger, +): Partial> { + return Object.fromEntries( + providers.map((provider) => [provider, createRealProviderClient(provider, logger)]), + ); +} + +export function canRunRealProvider(provider: RealProvider): Promise { + const cached = availabilityCache.get(provider); + if (cached) { + return cached; + } + + const availability = (async () => { + if (!getOpenRouterApiKeyOrNull()) { + return false; + } + return await isCommandAvailable(getProviderBinary(provider)); + })(); + + availabilityCache.set(provider, availability); + return availability; +} + +function getOpenRouterApiKey(): string { + const apiKey = getOpenRouterApiKeyOrNull(); + if (!apiKey) { + throw new Error("OPENROUTER_API_KEY is required for real provider tests"); + } + return apiKey; +} + +function getOpenRouterApiKeyOrNull(): string | null { + const value = process.env.OPENROUTER_API_KEY?.trim(); + return value && value.length > 0 ? value : null; +} + +function getProviderBinary(provider: RealProvider): string { + if (provider === "pi") { + return process.env.PI_COMMAND ?? process.env.PI_ACP_PI_COMMAND ?? "pi"; + } + return provider; +} 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 2dc87b02c..aaea430b7 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 @@ -6,8 +6,11 @@ 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 { + canRunRealProvider, + createRealProviderClients, + getRealProviderConfig, +} from "./real-provider-test-config.js"; function tmpCwd(): string { return mkdtempSync(path.join(tmpdir(), "daemon-rewind-dedupe-real-claude-")); @@ -17,7 +20,7 @@ describe("daemon E2E (real claude) - rewind user message dedupe", () => { let canRun = false; beforeAll(async () => { - canRun = await isProviderAvailable("claude"); + canRun = await canRunRealProvider("claude"); }); beforeEach((context) => { @@ -30,7 +33,7 @@ describe("daemon E2E (real claude) - rewind user message dedupe", () => { const logger = pino({ level: "silent" }); const cwd = tmpCwd(); const daemon = await createTestPaseoDaemon({ - agentClients: { claude: new ClaudeAgentClient({ logger }) }, + agentClients: createRealProviderClients(["claude"], logger), logger, }); const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` }); @@ -44,7 +47,7 @@ describe("daemon E2E (real claude) - rewind user message dedupe", () => { const agent = await client.createAgent({ cwd, title: "rewind-user-message-dedupe-real-claude", - ...getFullAccessConfig("claude"), + ...getRealProviderConfig("claude"), }); await client.sendMessage(agent.id, "Reply with exactly: READY"); 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 index 7fca214b1..86e725add 100644 --- 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 @@ -6,8 +6,11 @@ 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 { + canRunRealProvider, + createRealProviderClients, + getRealProviderConfig, +} from "./real-provider-test-config.js"; import { createMessageCollector } from "../test-utils/message-collector.js"; import type { AgentTimelineItem } from "../agent/agent-sdk-types.js"; import type { SessionOutboundMessage } from "../messages.js"; @@ -171,7 +174,7 @@ describe("daemon E2E (real claude) - send message during tool call", () => { let canRun = false; beforeAll(async () => { - canRun = await isProviderAvailable("claude"); + canRun = await canRunRealProvider("claude"); }); beforeEach((context) => { @@ -184,7 +187,7 @@ describe("daemon E2E (real claude) - send message during tool call", () => { const logger = pino({ level: "silent" }); const cwd = tmpCwd(); const daemon = await createTestPaseoDaemon({ - agentClients: { claude: new ClaudeAgentClient({ logger }) }, + agentClients: createRealProviderClients(["claude"], logger), logger, }); @@ -197,7 +200,7 @@ describe("daemon E2E (real claude) - send message during tool call", () => { const agent = await client.createAgent({ cwd, title: "tool-interrupt-repro", - ...getFullAccessConfig("claude"), + ...getRealProviderConfig("claude"), }); const collector = createMessageCollector(client); diff --git a/packages/server/src/server/daemon-e2e/send-during-tool-call-codex.real.e2e.test.ts b/packages/server/src/server/daemon-e2e/send-during-tool-call-codex.real.e2e.test.ts index 9d3cdc747..5fd5d99ba 100644 --- a/packages/server/src/server/daemon-e2e/send-during-tool-call-codex.real.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/send-during-tool-call-codex.real.e2e.test.ts @@ -4,13 +4,16 @@ import { tmpdir } from "node:os"; import path from "node:path"; import pino from "pino"; -import { CodexAppServerAgentClient } from "../agent/providers/codex-app-server-agent.js"; import type { AgentTimelineItem } from "../agent/agent-sdk-types.js"; import { DaemonClient } from "../test-utils/daemon-client.js"; import { createMessageCollector } from "../test-utils/message-collector.js"; import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js"; import type { SessionOutboundMessage } from "../messages.js"; -import { getFullAccessConfig, isProviderAvailable } from "./agent-configs.js"; +import { + canRunRealProvider, + createRealProviderClients, + getRealProviderConfig, +} from "./real-provider-test-config.js"; function tmpCwd(): string { return mkdtempSync(path.join(tmpdir(), "daemon-real-codex-tool-interrupt-")); @@ -143,7 +146,7 @@ describe("daemon E2E (real codex) - send message during tool call", () => { let canRun = false; beforeAll(async () => { - canRun = await isProviderAvailable("codex"); + canRun = await canRunRealProvider("codex"); }); beforeEach((context) => { @@ -156,7 +159,7 @@ describe("daemon E2E (real codex) - send message during tool call", () => { const logger = pino({ level: "silent" }); const cwd = tmpCwd(); const daemon = await createTestPaseoDaemon({ - agentClients: { codex: new CodexAppServerAgentClient(logger) }, + agentClients: createRealProviderClients(["codex"], logger), logger, }); const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` }); @@ -169,7 +172,7 @@ describe("daemon E2E (real codex) - send message during tool call", () => { const agent = await client.createAgent({ cwd, title: "codex-tool-interrupt-repro", - ...getFullAccessConfig("codex"), + ...getRealProviderConfig("codex"), }); collector = createMessageCollector(client); @@ -232,7 +235,7 @@ describe("daemon E2E (real codex) - send message during tool call", () => { const logger = pino({ level: "silent" }); const cwd = tmpCwd(); const daemon = await createTestPaseoDaemon({ - agentClients: { codex: new CodexAppServerAgentClient(logger) }, + agentClients: createRealProviderClients(["codex"], logger), logger, }); const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` }); @@ -245,7 +248,7 @@ describe("daemon E2E (real codex) - send message during tool call", () => { const agent = await client.createAgent({ cwd, title: "codex-quick-follow-up-repro", - ...getFullAccessConfig("codex"), + ...getRealProviderConfig("codex"), }); collector = createMessageCollector(client); 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 2dfdab257..1d14fc737 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 @@ -6,8 +6,11 @@ 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 { + canRunRealProvider, + createRealProviderClients, + getRealProviderConfig, +} from "./real-provider-test-config.js"; import { applyAgentInputProcessingTransition } from "./send-while-running-stuck-test-utils.js"; function tmpCwd(): string { @@ -18,7 +21,7 @@ describe("daemon E2E (real claude) - send while running recovery", () => { let canRun = false; beforeAll(async () => { - canRun = await isProviderAvailable("claude"); + canRun = await canRunRealProvider("claude"); }); beforeEach((context) => { @@ -31,7 +34,7 @@ describe("daemon E2E (real claude) - send while running recovery", () => { const logger = pino({ level: "silent" }); const cwd = tmpCwd(); const daemon = await createTestPaseoDaemon({ - agentClients: { claude: new ClaudeAgentClient({ logger }) }, + agentClients: createRealProviderClients(["claude"], logger), logger, }); @@ -47,7 +50,7 @@ describe("daemon E2E (real claude) - send while running recovery", () => { const agent = await primary.createAgent({ cwd, title: "stuck-repro-real-claude", - ...getFullAccessConfig("claude"), + ...getRealProviderConfig("claude"), }); await primary.sendMessage( 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 bc40fab46..1ed3f52f0 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 @@ -6,8 +6,11 @@ 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, isProviderAvailable } from "./agent-configs.js"; +import { + canRunRealProvider, + createRealProviderClients, + getRealProviderConfig, +} from "./real-provider-test-config.js"; import { applyAgentInputProcessingTransition } from "./send-while-running-stuck-test-utils.js"; function tmpCwd(): string { @@ -18,7 +21,7 @@ describe("daemon E2E (real codex) - send while running recovery", () => { let canRun = false; beforeAll(async () => { - canRun = await isProviderAvailable("codex"); + canRun = await canRunRealProvider("codex"); }); beforeEach((context) => { @@ -31,7 +34,7 @@ describe("daemon E2E (real codex) - send while running recovery", () => { const logger = pino({ level: "silent" }); const cwd = tmpCwd(); const daemon = await createTestPaseoDaemon({ - agentClients: { codex: new CodexAppServerAgentClient(logger) }, + agentClients: createRealProviderClients(["codex"], logger), logger, }); @@ -47,7 +50,7 @@ describe("daemon E2E (real codex) - send while running recovery", () => { const agent = await primary.createAgent({ cwd, title: "stuck-repro-real-codex", - ...getFullAccessConfig("codex"), + ...getRealProviderConfig("codex"), }); await primary.sendMessage(agent.id, "Run: sleep 5"); 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 1e360c005..925415a96 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 @@ -4,18 +4,15 @@ import { tmpdir } from "node:os"; import path from "node:path"; import pino from "pino"; -import type { AgentClient } from "../agent/agent-sdk-types.js"; -import { ClaudeAgentClient } from "../agent/providers/claude/agent.js"; -import { CodexAppServerAgentClient } from "../agent/providers/codex-app-server-agent.js"; -import { OpenCodeAgentClient } from "../agent/providers/opencode-agent.js"; import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js"; import { DaemonClient } from "../test-utils/daemon-client.js"; import { - allProviders, - getFullAccessConfig, - isProviderAvailable, - type AgentProvider, -} from "./agent-configs.js"; + canRunRealProvider, + createRealProviderClients, + getRealProviderConfig, + realProviders, + type RealProvider, +} from "./real-provider-test-config.js"; type UiAction = | { @@ -109,7 +106,7 @@ function longRunningPrompt(sleepSeconds: number, token: string): string { ].join(" "); } -function buildNormalUsageScenario(provider: AgentProvider, seed: number): UiScenario { +function buildNormalUsageScenario(provider: RealProvider, seed: number): UiScenario { const token1 = `UI_${provider}_${seed}_A`; const token2 = `UI_${provider}_${seed}_B`; return { @@ -146,7 +143,7 @@ function buildNormalUsageScenario(provider: AgentProvider, seed: number): UiScen } function buildOverlapScenario( - provider: AgentProvider, + provider: RealProvider, seed: number, firstPick: "first" | "last", ): UiScenario { @@ -444,21 +441,11 @@ async function runUiScenario(params: { } } -function createRealAgentClient(provider: AgentProvider, logger: pino.Logger): AgentClient { - if (provider === "claude") { - return new ClaudeAgentClient({ logger }); - } - if (provider === "codex") { - return new CodexAppServerAgentClient(logger); - } - return new OpenCodeAgentClient(logger); -} - -describe.each(allProviders)("daemon E2E (real %s) - UI action stress", (provider) => { +describe.each(realProviders)("daemon E2E (real %s) - UI action stress", (provider) => { let shouldRun = false; beforeAll(async () => { - shouldRun = await isProviderAvailable(provider); + shouldRun = await canRunRealProvider(provider); }); beforeEach((context) => { @@ -471,9 +458,7 @@ describe.each(allProviders)("daemon E2E (real %s) - UI action stress", (provider const logger = pino({ level: "silent" }); const cwd = tmpCwd(); const daemon = await createTestPaseoDaemon({ - agentClients: { - [provider]: createRealAgentClient(provider, logger), - } as Partial>, + agentClients: createRealProviderClients([provider], logger), logger, }); const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` }); @@ -486,7 +471,7 @@ describe.each(allProviders)("daemon E2E (real %s) - UI action stress", (provider const agent = await client.createAgent({ cwd, title: `uist-n-${provider}`, - ...getFullAccessConfig(provider), + ...getRealProviderConfig(provider), }); await runUiScenario({ @@ -507,9 +492,7 @@ describe.each(allProviders)("daemon E2E (real %s) - UI action stress", (provider const logger = pino({ level: "silent" }); const cwd = tmpCwd(); const daemon = await createTestPaseoDaemon({ - agentClients: { - [provider]: createRealAgentClient(provider, logger), - } as Partial>, + agentClients: createRealProviderClients([provider], logger), logger, }); const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` }); @@ -528,7 +511,7 @@ describe.each(allProviders)("daemon E2E (real %s) - UI action stress", (provider const agent = await client.createAgent({ cwd, title: `uist-o-${provider}`, - ...getFullAccessConfig(provider), + ...getRealProviderConfig(provider), }); await runUiScenario({ diff --git a/packages/server/src/server/daemon-e2e/user-message-contract.real.e2e.test.ts b/packages/server/src/server/daemon-e2e/user-message-contract.real.e2e.test.ts index 7cbefbeb5..b22327c6b 100644 --- a/packages/server/src/server/daemon-e2e/user-message-contract.real.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/user-message-contract.real.e2e.test.ts @@ -4,65 +4,54 @@ import path from "node:path"; import pino from "pino"; import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest"; -import { ClaudeAgentClient } from "../agent/providers/claude/agent.js"; -import { CodexAppServerAgentClient } from "../agent/providers/codex-app-server-agent.js"; -import { OpenCodeAgentClient } from "../agent/providers/opencode-agent.js"; -import { PiRpcAgentClient } from "../agent/providers/pi/agent.js"; import type { - AgentClient, AgentProvider, AgentSessionConfig, AgentStreamEvent, } from "../agent/agent-sdk-types.js"; import { DaemonClient } from "../test-utils/daemon-client.js"; import { createTestPaseoDaemon, type TestPaseoDaemon } from "../test-utils/paseo-daemon.js"; -import { getFullAccessConfig, isProviderAvailable } from "./agent-configs.js"; +import { + canRunRealProvider, + createRealProviderClient, + getRealProviderConfig, + type RealProvider, +} from "./real-provider-test-config.js"; import { fetchTimelineItems } from "./test-utils/rewind-helpers.js"; -type ContractProvider = Extract; +type ContractProvider = Extract; interface ProviderContractCase { provider: ContractProvider; title: string; timeoutMs: number; - createClient: (logger: pino.Logger) => AgentClient; createConfig: () => AgentSessionConfig; } -const PI_REAL_TEST_MODEL = "openrouter/google/gemini-2.5-flash-lite"; - const CONTRACT_CASES: ProviderContractCase[] = [ { provider: "claude", title: "Claude", timeoutMs: 180_000, - createClient: (logger) => new ClaudeAgentClient({ logger }), - createConfig: () => getFullAccessConfig("claude"), + createConfig: () => getRealProviderConfig("claude"), }, { provider: "codex", title: "Codex", timeoutMs: 180_000, - createClient: (logger) => new CodexAppServerAgentClient(logger), - createConfig: () => getFullAccessConfig("codex"), + createConfig: () => getRealProviderConfig("codex"), }, { provider: "opencode", title: "OpenCode", timeoutMs: 180_000, - createClient: (logger) => new OpenCodeAgentClient(logger), - createConfig: () => getFullAccessConfig("opencode"), + createConfig: () => getRealProviderConfig("opencode"), }, { provider: "pi", title: "Pi", timeoutMs: 240_000, - createClient: (logger) => new PiRpcAgentClient({ logger }), - createConfig: () => ({ - provider: "pi", - model: PI_REAL_TEST_MODEL, - thinkingOptionId: "medium", - }), + createConfig: () => getRealProviderConfig("pi"), }, ]; @@ -94,7 +83,7 @@ describe.each(CONTRACT_CASES)("daemon E2E (real $provider) - user_message contra let cwd: string | null = null; beforeAll(async () => { - canRun = await isProviderAvailable(entry.provider); + canRun = await canRunRealProvider(entry.provider); }); beforeEach(async (context) => { @@ -106,7 +95,7 @@ describe.each(CONTRACT_CASES)("daemon E2E (real $provider) - user_message contra cwd = tmpCwd(entry.provider); const logger = pino({ level: "silent" }); daemon = await createTestPaseoDaemon({ - agentClients: { [entry.provider]: entry.createClient(logger) }, + agentClients: { [entry.provider]: createRealProviderClient(entry.provider, logger) }, logger, }); client = new DaemonClient({