refactor(server): inject opencode runtime (#868)
This commit is contained in:
@@ -13,7 +13,6 @@ const mockState = vi.hoisted(() => {
|
|||||||
claude: [] as ConstructorEntry[],
|
claude: [] as ConstructorEntry[],
|
||||||
codex: [] as ConstructorEntry[],
|
codex: [] as ConstructorEntry[],
|
||||||
copilot: [] as ConstructorEntry[],
|
copilot: [] as ConstructorEntry[],
|
||||||
opencode: [] as ConstructorEntry[],
|
|
||||||
pi: [] as ConstructorEntry[],
|
pi: [] as ConstructorEntry[],
|
||||||
genericAcp: [] as Array<{
|
genericAcp: [] as Array<{
|
||||||
command: string[];
|
command: string[];
|
||||||
@@ -26,7 +25,6 @@ const mockState = vi.hoisted(() => {
|
|||||||
this.constructorArgs.claude = [];
|
this.constructorArgs.claude = [];
|
||||||
this.constructorArgs.codex = [];
|
this.constructorArgs.codex = [];
|
||||||
this.constructorArgs.copilot = [];
|
this.constructorArgs.copilot = [];
|
||||||
this.constructorArgs.opencode = [];
|
|
||||||
this.constructorArgs.pi = [];
|
this.constructorArgs.pi = [];
|
||||||
this.constructorArgs.genericAcp = [];
|
this.constructorArgs.genericAcp = [];
|
||||||
this.isCommandAvailable.mockReset();
|
this.isCommandAvailable.mockReset();
|
||||||
@@ -188,54 +186,6 @@ vi.mock("./providers/copilot-acp-agent.js", () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("./providers/opencode-agent.js", () => ({
|
|
||||||
OpenCodeAgentClient: class OpenCodeAgentClient {
|
|
||||||
readonly capabilities = {
|
|
||||||
supportsStreaming: true,
|
|
||||||
supportsSessionPersistence: true,
|
|
||||||
supportsDynamicModes: true,
|
|
||||||
supportsMcpServers: true,
|
|
||||||
supportsReasoningStream: true,
|
|
||||||
supportsToolInvocations: true,
|
|
||||||
};
|
|
||||||
readonly provider = "opencode";
|
|
||||||
readonly runtimeSettings?: unknown;
|
|
||||||
|
|
||||||
constructor(_logger: unknown, runtimeSettings?: unknown) {
|
|
||||||
this.runtimeSettings = runtimeSettings;
|
|
||||||
mockState.constructorArgs.opencode.push({ runtimeSettings });
|
|
||||||
}
|
|
||||||
|
|
||||||
async createSession(): Promise<never> {
|
|
||||||
throw new Error("not implemented");
|
|
||||||
}
|
|
||||||
|
|
||||||
async resumeSession(): Promise<never> {
|
|
||||||
throw new Error("not implemented");
|
|
||||||
}
|
|
||||||
|
|
||||||
async listModels(): Promise<AgentModelDefinition[]> {
|
|
||||||
return mockState.runtimeModels.get(this.provider) ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
async listModes(): Promise<[]> {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
async isAvailable(): Promise<boolean> {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("./providers/opencode/server-manager.js", () => ({
|
|
||||||
OpenCodeServerManager: {
|
|
||||||
getInstance: vi.fn(() => ({
|
|
||||||
shutdown: vi.fn(),
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("./providers/pi-direct-agent.js", () => ({
|
vi.mock("./providers/pi-direct-agent.js", () => ({
|
||||||
PiDirectAgentClient: class PiDirectAgentClient {
|
PiDirectAgentClient: class PiDirectAgentClient {
|
||||||
readonly capabilities = {
|
readonly capabilities = {
|
||||||
|
|||||||
@@ -1,73 +1,28 @@
|
|||||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
import { describe, expect, test } from "vitest";
|
||||||
|
|
||||||
vi.mock("@opencode-ai/sdk/v2/client", () => ({
|
|
||||||
createOpencodeClient: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client";
|
|
||||||
|
|
||||||
import { createTestLogger } from "../../../test-utils/test-logger.js";
|
import { createTestLogger } from "../../../test-utils/test-logger.js";
|
||||||
import type { AgentStreamEvent } from "../agent-sdk-types.js";
|
import type { AgentStreamEvent } from "../agent-sdk-types.js";
|
||||||
import { OpenCodeAgentClient } from "./opencode-agent.js";
|
import { OpenCodeAgentClient } from "./opencode-agent.js";
|
||||||
import { createTestOpenCodeServerManager } from "./opencode/test-server-manager.js";
|
import {
|
||||||
|
createEventStream,
|
||||||
|
idleEvent,
|
||||||
|
TestOpenCodeClient,
|
||||||
|
TestOpenCodeRuntime,
|
||||||
|
} from "./opencode/test-utils/test-opencode-runtime.js";
|
||||||
|
|
||||||
interface MockOpenCodeClientOptions {
|
interface MockOpenCodeClientOptions {
|
||||||
agents?: unknown[];
|
agents?: unknown[];
|
||||||
events?: unknown[];
|
events?: unknown[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function createEventStream(events: unknown[]): AsyncGenerator<never> {
|
|
||||||
return (async function* () {
|
|
||||||
for (const event of events) {
|
|
||||||
yield event as never;
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
|
|
||||||
function mockOpenCodeClient(options: MockOpenCodeClientOptions = {}) {
|
function mockOpenCodeClient(options: MockOpenCodeClientOptions = {}) {
|
||||||
const promptAsync = vi.fn().mockResolvedValue({});
|
const runtime = new TestOpenCodeRuntime();
|
||||||
const permissionReply = vi.fn().mockResolvedValue({});
|
const openCodeClient = new TestOpenCodeClient();
|
||||||
const questionReply = vi.fn().mockResolvedValue({});
|
openCodeClient.appAgentsResponse = { data: options.agents ?? [] };
|
||||||
const questionReject = vi.fn().mockResolvedValue({});
|
openCodeClient.eventStream = createEventStream(options.events ?? [idleEvent()]);
|
||||||
const appAgents = vi.fn().mockResolvedValue({ data: options.agents ?? [] });
|
runtime.enqueueClient(openCodeClient);
|
||||||
const events = options.events ?? [idleEvent()];
|
|
||||||
|
|
||||||
vi.mocked(createOpencodeClient).mockReturnValue({
|
return { openCodeClient, runtime };
|
||||||
session: {
|
|
||||||
create: vi.fn().mockResolvedValue({ data: { id: "session-1" } }),
|
|
||||||
promptAsync,
|
|
||||||
abort: vi.fn().mockResolvedValue({}),
|
|
||||||
update: vi.fn().mockResolvedValue({}),
|
|
||||||
},
|
|
||||||
provider: {
|
|
||||||
list: vi.fn().mockResolvedValue({ data: { connected: [], all: [] } }),
|
|
||||||
},
|
|
||||||
event: {
|
|
||||||
subscribe: vi.fn().mockResolvedValue({ stream: createEventStream(events) }),
|
|
||||||
},
|
|
||||||
command: {
|
|
||||||
list: vi.fn().mockResolvedValue({ data: [] }),
|
|
||||||
},
|
|
||||||
app: {
|
|
||||||
agents: appAgents,
|
|
||||||
},
|
|
||||||
permission: {
|
|
||||||
reply: permissionReply,
|
|
||||||
},
|
|
||||||
question: {
|
|
||||||
reply: questionReply,
|
|
||||||
reject: questionReject,
|
|
||||||
},
|
|
||||||
} as never);
|
|
||||||
|
|
||||||
return { appAgents, permissionReply, promptAsync, questionReject, questionReply };
|
|
||||||
}
|
|
||||||
|
|
||||||
function idleEvent(): unknown {
|
|
||||||
return {
|
|
||||||
type: "session.idle",
|
|
||||||
properties: { sessionID: "session-1" },
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toolPermissionEvent(): unknown {
|
function toolPermissionEvent(): unknown {
|
||||||
@@ -108,22 +63,15 @@ function questionEvent(): unknown {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("OpenCode full-access mode", () => {
|
describe("OpenCode full-access mode", () => {
|
||||||
afterEach(() => {
|
|
||||||
vi.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("includes virtual full-access mode with dynamic OpenCode agents", async () => {
|
test("includes virtual full-access mode with dynamic OpenCode agents", async () => {
|
||||||
const serverManager = createTestOpenCodeServerManager();
|
const { runtime } = mockOpenCodeClient({
|
||||||
mockOpenCodeClient({
|
|
||||||
agents: [
|
agents: [
|
||||||
{ name: "build", mode: "primary", hidden: false, description: "Build agent" },
|
{ name: "build", mode: "primary", hidden: false, description: "Build agent" },
|
||||||
{ name: "paseo-custom", mode: "primary", hidden: false, description: "Custom agent" },
|
{ name: "paseo-custom", mode: "primary", hidden: false, description: "Custom agent" },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const client = new OpenCodeAgentClient(createTestLogger(), undefined, undefined, {
|
const client = new OpenCodeAgentClient(createTestLogger(), undefined, undefined, { runtime });
|
||||||
serverManager,
|
|
||||||
});
|
|
||||||
const modes = await client.listModes({ cwd: "/tmp/project", force: false });
|
const modes = await client.listModes({ cwd: "/tmp/project", force: false });
|
||||||
|
|
||||||
expect(modes.map((mode) => mode.id)).toEqual(["build", "plan", "full-access", "paseo-custom"]);
|
expect(modes.map((mode) => mode.id)).toEqual(["build", "plan", "full-access", "paseo-custom"]);
|
||||||
@@ -134,12 +82,9 @@ describe("OpenCode full-access mode", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("reports full-access but sends prompts through OpenCode build agent", async () => {
|
test("reports full-access but sends prompts through OpenCode build agent", async () => {
|
||||||
const serverManager = createTestOpenCodeServerManager();
|
const { openCodeClient, runtime } = mockOpenCodeClient();
|
||||||
const { promptAsync } = mockOpenCodeClient();
|
|
||||||
|
|
||||||
const client = new OpenCodeAgentClient(createTestLogger(), undefined, undefined, {
|
const client = new OpenCodeAgentClient(createTestLogger(), undefined, undefined, { runtime });
|
||||||
serverManager,
|
|
||||||
});
|
|
||||||
const session = await client.createSession({
|
const session = await client.createSession({
|
||||||
provider: "opencode",
|
provider: "opencode",
|
||||||
cwd: "/tmp/project",
|
cwd: "/tmp/project",
|
||||||
@@ -150,22 +95,21 @@ describe("OpenCode full-access mode", () => {
|
|||||||
|
|
||||||
await session.run("Implement the change");
|
await session.run("Implement the change");
|
||||||
|
|
||||||
expect(promptAsync).toHaveBeenCalledTimes(1);
|
expect(openCodeClient.calls.sessionPromptAsync).toHaveLength(1);
|
||||||
expect(promptAsync).toHaveBeenCalledWith(expect.objectContaining({ agent: "build" }));
|
expect(openCodeClient.calls.sessionPromptAsync[0]).toEqual(
|
||||||
|
expect.objectContaining({ agent: "build" }),
|
||||||
|
);
|
||||||
|
|
||||||
await session.close();
|
await session.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("auto-approves tool permissions in full-access without surfacing them", async () => {
|
test("auto-approves tool permissions in full-access without surfacing them", async () => {
|
||||||
const serverManager = createTestOpenCodeServerManager();
|
const { openCodeClient, runtime } = mockOpenCodeClient({
|
||||||
const { permissionReply } = mockOpenCodeClient({
|
|
||||||
events: [toolPermissionEvent(), idleEvent()],
|
events: [toolPermissionEvent(), idleEvent()],
|
||||||
});
|
});
|
||||||
const receivedEvents: AgentStreamEvent[] = [];
|
const receivedEvents: AgentStreamEvent[] = [];
|
||||||
|
|
||||||
const client = new OpenCodeAgentClient(createTestLogger(), undefined, undefined, {
|
const client = new OpenCodeAgentClient(createTestLogger(), undefined, undefined, { runtime });
|
||||||
serverManager,
|
|
||||||
});
|
|
||||||
const session = await client.createSession({
|
const session = await client.createSession({
|
||||||
provider: "opencode",
|
provider: "opencode",
|
||||||
cwd: "/tmp/project",
|
cwd: "/tmp/project",
|
||||||
@@ -175,8 +119,8 @@ describe("OpenCode full-access mode", () => {
|
|||||||
|
|
||||||
await session.run("Run verification");
|
await session.run("Run verification");
|
||||||
|
|
||||||
expect(permissionReply).toHaveBeenCalledTimes(1);
|
expect(openCodeClient.calls.permissionReply).toHaveLength(1);
|
||||||
expect(permissionReply).toHaveBeenCalledWith({
|
expect(openCodeClient.calls.permissionReply[0]).toEqual({
|
||||||
requestID: "permission-1",
|
requestID: "permission-1",
|
||||||
directory: "/tmp/project",
|
directory: "/tmp/project",
|
||||||
reply: "once",
|
reply: "once",
|
||||||
@@ -188,15 +132,12 @@ describe("OpenCode full-access mode", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("keeps questions separate from full-access tool auto-approval", async () => {
|
test("keeps questions separate from full-access tool auto-approval", async () => {
|
||||||
const serverManager = createTestOpenCodeServerManager();
|
const { openCodeClient, runtime } = mockOpenCodeClient({
|
||||||
const { permissionReply, questionReply } = mockOpenCodeClient({
|
|
||||||
events: [questionEvent(), idleEvent()],
|
events: [questionEvent(), idleEvent()],
|
||||||
});
|
});
|
||||||
const receivedEvents: AgentStreamEvent[] = [];
|
const receivedEvents: AgentStreamEvent[] = [];
|
||||||
|
|
||||||
const client = new OpenCodeAgentClient(createTestLogger(), undefined, undefined, {
|
const client = new OpenCodeAgentClient(createTestLogger(), undefined, undefined, { runtime });
|
||||||
serverManager,
|
|
||||||
});
|
|
||||||
const session = await client.createSession({
|
const session = await client.createSession({
|
||||||
provider: "opencode",
|
provider: "opencode",
|
||||||
cwd: "/tmp/project",
|
cwd: "/tmp/project",
|
||||||
@@ -221,13 +162,13 @@ describe("OpenCode full-access mode", () => {
|
|||||||
updatedInput: { answers: { Decision: "Proceed" } },
|
updatedInput: { answers: { Decision: "Proceed" } },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(questionReply).toHaveBeenCalledTimes(1);
|
expect(openCodeClient.calls.questionReply).toHaveLength(1);
|
||||||
expect(questionReply).toHaveBeenCalledWith({
|
expect(openCodeClient.calls.questionReply[0]).toEqual({
|
||||||
requestID: "question-1",
|
requestID: "question-1",
|
||||||
directory: "/tmp/project",
|
directory: "/tmp/project",
|
||||||
answers: [["Proceed"]],
|
answers: [["Proceed"]],
|
||||||
});
|
});
|
||||||
expect(permissionReply).not.toHaveBeenCalled();
|
expect(openCodeClient.calls.permissionReply).toEqual([]);
|
||||||
expect(session.getPendingPermissions()).toEqual([]);
|
expect(session.getPendingPermissions()).toEqual([]);
|
||||||
|
|
||||||
await session.close();
|
await session.close();
|
||||||
|
|||||||
+21
-35
@@ -1,25 +1,23 @@
|
|||||||
import { afterEach, expect, test, vi } from "vitest";
|
import { afterEach, expect, test, vi } from "vitest";
|
||||||
|
|
||||||
vi.mock("@opencode-ai/sdk/v2/client", () => ({
|
|
||||||
createOpencodeClient: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client";
|
|
||||||
|
|
||||||
import { createTestLogger } from "../../../test-utils/test-logger.js";
|
import { createTestLogger } from "../../../test-utils/test-logger.js";
|
||||||
import { OpenCodeAgentClient } from "./opencode-agent.js";
|
import { OpenCodeAgentClient } from "./opencode-agent.js";
|
||||||
import { createTestOpenCodeServerManager } from "./opencode/test-server-manager.js";
|
import {
|
||||||
|
TestOpenCodeClient,
|
||||||
|
TestOpenCodeRuntime,
|
||||||
|
} from "./opencode/test-utils/test-opencode-runtime.js";
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.useRealTimers();
|
vi.useRealTimers();
|
||||||
vi.restoreAllMocks();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("allows a slow provider.list call to succeed instead of failing after 10 seconds", async () => {
|
test("allows a slow provider.list call to succeed instead of failing after 10 seconds", async () => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
|
|
||||||
async function providerList(): Promise<unknown> {
|
const runtime = new TestOpenCodeRuntime();
|
||||||
return new Promise((resolve) => {
|
const openCodeClient = new TestOpenCodeClient();
|
||||||
|
openCodeClient.providerListImplementation = () =>
|
||||||
|
new Promise((resolve) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
resolve({
|
resolve({
|
||||||
data: {
|
data: {
|
||||||
@@ -40,18 +38,9 @@ test("allows a slow provider.list call to succeed instead of failing after 10 se
|
|||||||
});
|
});
|
||||||
}, 15_000);
|
}, 15_000);
|
||||||
});
|
});
|
||||||
}
|
runtime.enqueueClient(openCodeClient);
|
||||||
|
|
||||||
vi.mocked(createOpencodeClient).mockReturnValue({
|
const client = new OpenCodeAgentClient(createTestLogger(), undefined, undefined, { runtime });
|
||||||
provider: {
|
|
||||||
list: providerList,
|
|
||||||
},
|
|
||||||
} as never);
|
|
||||||
|
|
||||||
const serverManager = createTestOpenCodeServerManager();
|
|
||||||
const client = new OpenCodeAgentClient(createTestLogger(), undefined, undefined, {
|
|
||||||
serverManager,
|
|
||||||
});
|
|
||||||
const modelsPromise = client.listModels({ cwd: "/tmp/opencode-models", force: false });
|
const modelsPromise = client.listModels({ cwd: "/tmp/opencode-models", force: false });
|
||||||
|
|
||||||
await vi.advanceTimersByTimeAsync(15_000);
|
await vi.advanceTimersByTimeAsync(15_000);
|
||||||
@@ -63,26 +52,23 @@ test("allows a slow provider.list call to succeed instead of failing after 10 se
|
|||||||
label: "GLM 5.1",
|
label: "GLM 5.1",
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
expect(openCodeClient.calls.providerList).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("passes explicit refresh force through server acquisition", async () => {
|
test("passes explicit refresh force through server acquisition", async () => {
|
||||||
vi.mocked(createOpencodeClient).mockReturnValue({
|
const runtime = new TestOpenCodeRuntime();
|
||||||
provider: {
|
const openCodeClient = new TestOpenCodeClient();
|
||||||
list: async () => ({
|
openCodeClient.providerListResponse = {
|
||||||
data: {
|
data: {
|
||||||
connected: ["openai"],
|
connected: ["openai"],
|
||||||
all: [{ id: "openai", name: "OpenAI", models: {} }],
|
all: [{ id: "openai", name: "OpenAI", models: {} }],
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
} as never);
|
};
|
||||||
const serverManager = createTestOpenCodeServerManager();
|
runtime.enqueueClient(openCodeClient);
|
||||||
|
|
||||||
const client = new OpenCodeAgentClient(createTestLogger(), undefined, undefined, {
|
const client = new OpenCodeAgentClient(createTestLogger(), undefined, undefined, { runtime });
|
||||||
serverManager,
|
|
||||||
});
|
|
||||||
|
|
||||||
await client.listModels({ cwd: "/tmp/opencode-models", force: true });
|
await client.listModels({ cwd: "/tmp/opencode-models", force: true });
|
||||||
|
|
||||||
expect(serverManager.acquisitions).toEqual([{ force: true, released: true }]);
|
expect(runtime.acquisitions).toEqual([{ force: true, releaseCount: 1 }]);
|
||||||
});
|
});
|
||||||
|
|||||||
+43
-110
@@ -1,14 +1,11 @@
|
|||||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
import { describe, expect, test } from "vitest";
|
||||||
|
|
||||||
vi.mock("@opencode-ai/sdk/v2/client", () => ({
|
|
||||||
createOpencodeClient: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client";
|
|
||||||
|
|
||||||
import { createTestLogger } from "../../../test-utils/test-logger.js";
|
import { createTestLogger } from "../../../test-utils/test-logger.js";
|
||||||
import { OpenCodeAgentClient } from "./opencode-agent.js";
|
import { OpenCodeAgentClient } from "./opencode-agent.js";
|
||||||
import { createTestOpenCodeServerManager } from "./opencode/test-server-manager.js";
|
import {
|
||||||
|
TestOpenCodeClient,
|
||||||
|
TestOpenCodeRuntime,
|
||||||
|
} from "./opencode/test-utils/test-opencode-runtime.js";
|
||||||
|
|
||||||
function createDeferred<T>(): {
|
function createDeferred<T>(): {
|
||||||
promise: Promise<T>;
|
promise: Promise<T>;
|
||||||
@@ -25,34 +22,12 @@ function createDeferred<T>(): {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("OpenCodeAgentSession slash command timeout handling", () => {
|
describe("OpenCodeAgentSession slash command timeout handling", () => {
|
||||||
afterEach(() => {
|
|
||||||
vi.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("lists only OpenCode built-in slash commands Paseo can execute", async () => {
|
test("lists only OpenCode built-in slash commands Paseo can execute", async () => {
|
||||||
vi.mocked(createOpencodeClient).mockReturnValue({
|
const runtime = new TestOpenCodeRuntime();
|
||||||
session: {
|
const openCodeClient = createOpenCodeClientWithConnectedProvider();
|
||||||
create: vi.fn().mockResolvedValue({ data: { id: "session-1" } }),
|
runtime.enqueueClient(openCodeClient);
|
||||||
},
|
|
||||||
provider: {
|
|
||||||
list: vi.fn().mockResolvedValue({
|
|
||||||
data: {
|
|
||||||
connected: ["openai"],
|
|
||||||
all: [{ id: "openai", name: "OpenAI", models: {} }],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
command: {
|
|
||||||
list: vi.fn().mockResolvedValue({ data: [] }),
|
|
||||||
},
|
|
||||||
app: {
|
|
||||||
agents: vi.fn().mockResolvedValue({ data: [] }),
|
|
||||||
},
|
|
||||||
} as never);
|
|
||||||
|
|
||||||
const client = new OpenCodeAgentClient(createTestLogger(), undefined, undefined, {
|
const client = new OpenCodeAgentClient(createTestLogger(), undefined, undefined, { runtime });
|
||||||
serverManager: createTestOpenCodeServerManager(),
|
|
||||||
});
|
|
||||||
const session = await client.createSession({ provider: "opencode", cwd: "/tmp" });
|
const session = await client.createSession({ provider: "opencode", cwd: "/tmp" });
|
||||||
|
|
||||||
await expect(session.listCommands?.()).resolves.toEqual(
|
await expect(session.listCommands?.()).resolves.toEqual(
|
||||||
@@ -68,44 +43,11 @@ describe("OpenCodeAgentSession slash command timeout handling", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("executes compact through the OpenCode summarize endpoint", async () => {
|
test("executes compact through the OpenCode summarize endpoint", async () => {
|
||||||
const command = vi.fn();
|
const runtime = new TestOpenCodeRuntime();
|
||||||
const summarize = vi.fn().mockResolvedValue({ data: {} });
|
const openCodeClient = createOpenCodeClientWithConnectedProvider();
|
||||||
|
runtime.enqueueClient(openCodeClient);
|
||||||
|
|
||||||
vi.mocked(createOpencodeClient).mockReturnValue({
|
const client = new OpenCodeAgentClient(createTestLogger(), undefined, undefined, { runtime });
|
||||||
session: {
|
|
||||||
create: vi.fn().mockResolvedValue({ data: { id: "session-1" } }),
|
|
||||||
command,
|
|
||||||
summarize,
|
|
||||||
},
|
|
||||||
provider: {
|
|
||||||
list: vi.fn().mockResolvedValue({
|
|
||||||
data: {
|
|
||||||
connected: ["openai"],
|
|
||||||
all: [{ id: "openai", name: "OpenAI", models: {} }],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
event: {
|
|
||||||
subscribe: vi.fn().mockResolvedValue({
|
|
||||||
stream: (async function* () {
|
|
||||||
yield {
|
|
||||||
type: "session.idle",
|
|
||||||
properties: { sessionID: "session-1" },
|
|
||||||
};
|
|
||||||
})(),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
command: {
|
|
||||||
list: vi.fn().mockResolvedValue({ data: [] }),
|
|
||||||
},
|
|
||||||
app: {
|
|
||||||
agents: vi.fn().mockResolvedValue({ data: [] }),
|
|
||||||
},
|
|
||||||
} as never);
|
|
||||||
|
|
||||||
const client = new OpenCodeAgentClient(createTestLogger(), undefined, undefined, {
|
|
||||||
serverManager: createTestOpenCodeServerManager(),
|
|
||||||
});
|
|
||||||
const session = await client.createSession({ provider: "opencode", cwd: "/tmp" });
|
const session = await client.createSession({ provider: "opencode", cwd: "/tmp" });
|
||||||
|
|
||||||
await expect(session.run("/compact")).resolves.toMatchObject({
|
await expect(session.run("/compact")).resolves.toMatchObject({
|
||||||
@@ -114,50 +56,30 @@ describe("OpenCodeAgentSession slash command timeout handling", () => {
|
|||||||
timeline: [],
|
timeline: [],
|
||||||
usage: undefined,
|
usage: undefined,
|
||||||
});
|
});
|
||||||
expect(summarize).toHaveBeenCalledWith({ sessionID: "session-1", directory: "/tmp" });
|
expect(openCodeClient.calls.sessionSummarize).toEqual([
|
||||||
expect(command).not.toHaveBeenCalled();
|
{ sessionID: "session-1", directory: "/tmp" },
|
||||||
|
]);
|
||||||
|
expect(openCodeClient.calls.sessionCommand).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("waits for SSE completion when slash commands hit a header timeout", async () => {
|
test("waits for SSE completion when slash commands hit a header timeout", async () => {
|
||||||
const idleEventGate = createDeferred<void>();
|
const idleEventGate = createDeferred<void>();
|
||||||
|
const runtime = new TestOpenCodeRuntime();
|
||||||
|
const openCodeClient = createOpenCodeClientWithConnectedProvider();
|
||||||
|
openCodeClient.sessionCommandError = new Error("fetch failed: Headers Timeout Error");
|
||||||
|
openCodeClient.commandListResponse = {
|
||||||
|
data: [{ name: "help", description: "Show help", hints: [] }],
|
||||||
|
};
|
||||||
|
openCodeClient.eventStream = (async function* () {
|
||||||
|
await idleEventGate.promise;
|
||||||
|
yield {
|
||||||
|
type: "session.idle",
|
||||||
|
properties: { sessionID: "session-1" },
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
runtime.enqueueClient(openCodeClient);
|
||||||
|
|
||||||
vi.mocked(createOpencodeClient).mockReturnValue({
|
const client = new OpenCodeAgentClient(createTestLogger(), undefined, undefined, { runtime });
|
||||||
session: {
|
|
||||||
create: vi.fn().mockResolvedValue({ data: { id: "session-1" } }),
|
|
||||||
command: vi.fn().mockRejectedValue(new Error("fetch failed: Headers Timeout Error")),
|
|
||||||
},
|
|
||||||
provider: {
|
|
||||||
list: vi.fn().mockResolvedValue({
|
|
||||||
data: {
|
|
||||||
connected: ["openai"],
|
|
||||||
all: [{ id: "openai", name: "OpenAI", models: {} }],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
event: {
|
|
||||||
subscribe: vi.fn().mockResolvedValue({
|
|
||||||
stream: (async function* () {
|
|
||||||
await idleEventGate.promise;
|
|
||||||
yield {
|
|
||||||
type: "session.idle",
|
|
||||||
properties: { sessionID: "session-1" },
|
|
||||||
};
|
|
||||||
})(),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
command: {
|
|
||||||
list: vi.fn().mockResolvedValue({
|
|
||||||
data: [{ name: "help", description: "Show help", hints: [] }],
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
app: {
|
|
||||||
agents: vi.fn().mockResolvedValue({ data: [] }),
|
|
||||||
},
|
|
||||||
} as never);
|
|
||||||
|
|
||||||
const client = new OpenCodeAgentClient(createTestLogger(), undefined, undefined, {
|
|
||||||
serverManager: createTestOpenCodeServerManager(),
|
|
||||||
});
|
|
||||||
const session = await client.createSession({ provider: "opencode", cwd: "/tmp" });
|
const session = await client.createSession({ provider: "opencode", cwd: "/tmp" });
|
||||||
|
|
||||||
const runPromise = session.run("/help");
|
const runPromise = session.run("/help");
|
||||||
@@ -172,3 +94,14 @@ describe("OpenCodeAgentSession slash command timeout handling", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function createOpenCodeClientWithConnectedProvider(): TestOpenCodeClient {
|
||||||
|
const openCodeClient = new TestOpenCodeClient();
|
||||||
|
openCodeClient.providerListResponse = {
|
||||||
|
data: {
|
||||||
|
connected: ["openai"],
|
||||||
|
all: [{ id: "openai", name: "OpenAI", models: {} }],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return openCodeClient;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { readdir, readFile } from "node:fs/promises";
|
|||||||
import { homedir } from "node:os";
|
import { homedir } from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import {
|
import {
|
||||||
createOpencodeClient,
|
|
||||||
type AssistantMessage as OpenCodeAssistantMessage,
|
type AssistantMessage as OpenCodeAssistantMessage,
|
||||||
type Event as OpenCodeEvent,
|
type Event as OpenCodeEvent,
|
||||||
type FilePartInput as OpenCodeFilePartInput,
|
type FilePartInput as OpenCodeFilePartInput,
|
||||||
@@ -47,10 +46,7 @@ import { withTimeout } from "../../../utils/promise-timeout.js";
|
|||||||
import { execCommand } from "../../../utils/spawn.js";
|
import { execCommand } from "../../../utils/spawn.js";
|
||||||
import { buildToolCallDisplayModel } from "../../../shared/tool-call-display.js";
|
import { buildToolCallDisplayModel } from "../../../shared/tool-call-display.js";
|
||||||
import { mapOpencodeToolCall } from "./opencode/tool-call-mapper.js";
|
import { mapOpencodeToolCall } from "./opencode/tool-call-mapper.js";
|
||||||
import {
|
import { OpenCodeServerManager } from "./opencode/server-manager.js";
|
||||||
OpenCodeServerManager,
|
|
||||||
type OpenCodeServerManagerLike,
|
|
||||||
} from "./opencode/server-manager.js";
|
|
||||||
import {
|
import {
|
||||||
formatDiagnosticStatus,
|
formatDiagnosticStatus,
|
||||||
formatProviderDiagnostic,
|
formatProviderDiagnostic,
|
||||||
@@ -60,6 +56,11 @@ import {
|
|||||||
} from "./diagnostic-utils.js";
|
} from "./diagnostic-utils.js";
|
||||||
import { runProviderTurn } from "./provider-runner.js";
|
import { runProviderTurn } from "./provider-runner.js";
|
||||||
import { renderPromptAttachmentAsText } from "../prompt-attachments.js";
|
import { renderPromptAttachmentAsText } from "../prompt-attachments.js";
|
||||||
|
import {
|
||||||
|
createSdkOpenCodeClient,
|
||||||
|
type OpenCodeRuntime,
|
||||||
|
type OpenCodeServerAcquisition,
|
||||||
|
} from "./opencode/runtime.js";
|
||||||
|
|
||||||
const OPENCODE_CAPABILITIES: AgentCapabilityFlags = {
|
const OPENCODE_CAPABILITIES: AgentCapabilityFlags = {
|
||||||
supportsStreaming: true,
|
supportsStreaming: true,
|
||||||
@@ -898,14 +899,34 @@ export const __openCodeInternals = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface OpenCodeAgentClientDeps {
|
interface OpenCodeAgentClientDeps {
|
||||||
serverManager?: OpenCodeServerManagerLike;
|
runtime?: OpenCodeRuntime;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProductionOpenCodeRuntime implements OpenCodeRuntime {
|
||||||
|
constructor(private readonly serverManager: OpenCodeServerManager) {}
|
||||||
|
|
||||||
|
async acquireServer(options: { force: boolean }): Promise<OpenCodeServerAcquisition> {
|
||||||
|
return this.serverManager.acquire(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async ensureServerRunning(): Promise<{ port: number; url: string }> {
|
||||||
|
return this.serverManager.ensureRunning();
|
||||||
|
}
|
||||||
|
|
||||||
|
createClient(options: { baseUrl: string; directory: string }): OpencodeClient {
|
||||||
|
return createSdkOpenCodeClient(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async shutdown(): Promise<void> {
|
||||||
|
await this.serverManager.shutdown();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class OpenCodeAgentClient implements AgentClient {
|
export class OpenCodeAgentClient implements AgentClient {
|
||||||
readonly provider = "opencode" as const;
|
readonly provider = "opencode" as const;
|
||||||
readonly capabilities = OPENCODE_CAPABILITIES;
|
readonly capabilities = OPENCODE_CAPABILITIES;
|
||||||
|
|
||||||
private readonly serverManager: OpenCodeServerManagerLike;
|
private readonly runtime: OpenCodeRuntime;
|
||||||
private readonly logger: Logger;
|
private readonly logger: Logger;
|
||||||
private readonly runtimeSettings?: ProviderRuntimeSettings;
|
private readonly runtimeSettings?: ProviderRuntimeSettings;
|
||||||
private readonly modelContextWindows = new Map<string, number>();
|
private readonly modelContextWindows = new Map<string, number>();
|
||||||
@@ -920,8 +941,11 @@ export class OpenCodeAgentClient implements AgentClient {
|
|||||||
this.logger = logger.child({ module: "agent", provider: "opencode" });
|
this.logger = logger.child({ module: "agent", provider: "opencode" });
|
||||||
this.runtimeSettings = runtimeSettings;
|
this.runtimeSettings = runtimeSettings;
|
||||||
this.storageRoot = storageRoot ?? resolveOpenCodeStorageRoot();
|
this.storageRoot = storageRoot ?? resolveOpenCodeStorageRoot();
|
||||||
this.serverManager =
|
this.runtime =
|
||||||
deps.serverManager ?? OpenCodeServerManager.getInstance(this.logger, runtimeSettings);
|
deps.runtime ??
|
||||||
|
new ProductionOpenCodeRuntime(
|
||||||
|
OpenCodeServerManager.getInstance(this.logger, runtimeSettings),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createSession(
|
async createSession(
|
||||||
@@ -930,9 +954,9 @@ export class OpenCodeAgentClient implements AgentClient {
|
|||||||
options?: AgentCreateSessionOptions,
|
options?: AgentCreateSessionOptions,
|
||||||
): Promise<AgentSession> {
|
): Promise<AgentSession> {
|
||||||
const openCodeConfig = this.assertConfig(config);
|
const openCodeConfig = this.assertConfig(config);
|
||||||
const acquisition = await this.serverManager.acquire({ force: false });
|
const acquisition = await this.runtime.acquireServer({ force: false });
|
||||||
const { url } = acquisition.server;
|
const { url } = acquisition.server;
|
||||||
const client = createOpencodeClient({
|
const client = this.runtime.createClient({
|
||||||
baseUrl: url,
|
baseUrl: url,
|
||||||
directory: openCodeConfig.cwd,
|
directory: openCodeConfig.cwd,
|
||||||
});
|
});
|
||||||
@@ -986,9 +1010,9 @@ export class OpenCodeAgentClient implements AgentClient {
|
|||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
const openCodeConfig = this.assertConfig(config);
|
const openCodeConfig = this.assertConfig(config);
|
||||||
const acquisition = await this.serverManager.acquire({ force: false });
|
const acquisition = await this.runtime.acquireServer({ force: false });
|
||||||
const { url } = acquisition.server;
|
const { url } = acquisition.server;
|
||||||
const client = createOpencodeClient({
|
const client = this.runtime.createClient({
|
||||||
baseUrl: url,
|
baseUrl: url,
|
||||||
directory: openCodeConfig.cwd,
|
directory: openCodeConfig.cwd,
|
||||||
});
|
});
|
||||||
@@ -1011,9 +1035,9 @@ export class OpenCodeAgentClient implements AgentClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async listModels(options: ListModelsOptions): Promise<AgentModelDefinition[]> {
|
async listModels(options: ListModelsOptions): Promise<AgentModelDefinition[]> {
|
||||||
const acquisition = await this.serverManager.acquire({ force: options.force });
|
const acquisition = await this.runtime.acquireServer({ force: options.force });
|
||||||
const { url } = acquisition.server;
|
const { url } = acquisition.server;
|
||||||
const client = createOpencodeClient({
|
const client = this.runtime.createClient({
|
||||||
baseUrl: url,
|
baseUrl: url,
|
||||||
directory: options.cwd,
|
directory: options.cwd,
|
||||||
});
|
});
|
||||||
@@ -1074,10 +1098,10 @@ export class OpenCodeAgentClient implements AgentClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async listModes(options: ListModesOptions): Promise<AgentMode[]> {
|
async listModes(options: ListModesOptions): Promise<AgentMode[]> {
|
||||||
const acquisition = await this.serverManager.acquire({ force: options.force });
|
const acquisition = await this.runtime.acquireServer({ force: options.force });
|
||||||
const { url } = acquisition.server;
|
const { url } = acquisition.server;
|
||||||
const directory = options.cwd;
|
const directory = options.cwd;
|
||||||
const client = createOpencodeClient({ baseUrl: url, directory });
|
const client = this.runtime.createClient({ baseUrl: url, directory });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await withTimeout(
|
const response = await withTimeout(
|
||||||
@@ -1128,7 +1152,7 @@ export class OpenCodeAgentClient implements AgentClient {
|
|||||||
let status = formatDiagnosticStatus(available);
|
let status = formatDiagnosticStatus(available);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { url } = await this.serverManager.ensureRunning();
|
const { url } = await this.runtime.ensureServerRunning();
|
||||||
serverStatus = `Running (${url})`;
|
serverStatus = `Running (${url})`;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
serverStatus = `Unavailable (${toDiagnosticErrorMessage(error)})`;
|
serverStatus = `Unavailable (${toDiagnosticErrorMessage(error)})`;
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import {
|
||||||
|
createOpencodeClient,
|
||||||
|
type OpencodeClient,
|
||||||
|
type OpencodeClientConfig,
|
||||||
|
} from "@opencode-ai/sdk/v2/client";
|
||||||
|
|
||||||
|
export interface OpenCodeServerAcquisition {
|
||||||
|
server: { port: number; url: string };
|
||||||
|
release: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OpenCodeRuntime {
|
||||||
|
acquireServer(options: { force: boolean }): Promise<OpenCodeServerAcquisition>;
|
||||||
|
ensureServerRunning(): Promise<{ port: number; url: string }>;
|
||||||
|
createClient(options: { baseUrl: string; directory: string }): OpencodeClient;
|
||||||
|
shutdown(): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createSdkOpenCodeClient(options: {
|
||||||
|
baseUrl: string;
|
||||||
|
directory: string;
|
||||||
|
}): OpencodeClient {
|
||||||
|
return createOpencodeClient(options satisfies OpencodeClientConfig & { directory: string });
|
||||||
|
}
|
||||||
+184
@@ -0,0 +1,184 @@
|
|||||||
|
import type { OpencodeClient } from "@opencode-ai/sdk/v2/client";
|
||||||
|
|
||||||
|
import type { OpenCodeRuntime, OpenCodeServerAcquisition } from "../runtime.js";
|
||||||
|
|
||||||
|
interface OpenCodeResponse {
|
||||||
|
data?: unknown;
|
||||||
|
error?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TestOpenCodeRuntime implements OpenCodeRuntime {
|
||||||
|
readonly acquisitions: Array<{ force: boolean; releaseCount: number }> = [];
|
||||||
|
readonly clientCreations: Array<{ baseUrl: string; directory: string }> = [];
|
||||||
|
private readonly clients: TestOpenCodeClient[] = [];
|
||||||
|
|
||||||
|
server = { port: 1234, url: "http://127.0.0.1:1234" };
|
||||||
|
|
||||||
|
enqueueClient(client: TestOpenCodeClient): void {
|
||||||
|
this.clients.push(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
async acquireServer(options: { force: boolean }): Promise<OpenCodeServerAcquisition> {
|
||||||
|
const acquisition = { force: options.force, releaseCount: 0 };
|
||||||
|
this.acquisitions.push(acquisition);
|
||||||
|
return {
|
||||||
|
server: this.server,
|
||||||
|
release: () => {
|
||||||
|
acquisition.releaseCount += 1;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async ensureServerRunning(): Promise<{ port: number; url: string }> {
|
||||||
|
return this.server;
|
||||||
|
}
|
||||||
|
|
||||||
|
createClient(options: { baseUrl: string; directory: string }): OpencodeClient {
|
||||||
|
this.clientCreations.push(options);
|
||||||
|
const client = this.clients.shift() ?? new TestOpenCodeClient();
|
||||||
|
return client.asSdkClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
async shutdown(): Promise<void> {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TestOpenCodeClient {
|
||||||
|
readonly calls = {
|
||||||
|
appAgents: [] as unknown[],
|
||||||
|
commandList: [] as unknown[],
|
||||||
|
eventSubscribe: [] as unknown[],
|
||||||
|
permissionReply: [] as unknown[],
|
||||||
|
providerList: [] as unknown[],
|
||||||
|
questionReject: [] as unknown[],
|
||||||
|
questionReply: [] as unknown[],
|
||||||
|
sessionAbort: [] as unknown[],
|
||||||
|
sessionCommand: [] as unknown[],
|
||||||
|
sessionCreate: [] as unknown[],
|
||||||
|
sessionDelete: [] as unknown[],
|
||||||
|
sessionMessages: [] as unknown[],
|
||||||
|
sessionPromptAsync: [] as unknown[],
|
||||||
|
sessionSummarize: [] as unknown[],
|
||||||
|
sessionUpdate: [] as unknown[],
|
||||||
|
};
|
||||||
|
|
||||||
|
appAgentsResponse: OpenCodeResponse = { data: [] };
|
||||||
|
commandListResponse: OpenCodeResponse = { data: [] };
|
||||||
|
eventStream: AsyncIterable<unknown> = createEventStream([idleEvent()]);
|
||||||
|
permissionReplyResponse: OpenCodeResponse = {};
|
||||||
|
providerListResponse: OpenCodeResponse = { data: { connected: [], all: [] } };
|
||||||
|
providerListImplementation: (() => Promise<OpenCodeResponse>) | null = null;
|
||||||
|
questionRejectResponse: OpenCodeResponse = {};
|
||||||
|
questionReplyResponse: OpenCodeResponse = {};
|
||||||
|
sessionAbortResponse: OpenCodeResponse = {};
|
||||||
|
sessionCommandError: unknown = null;
|
||||||
|
sessionCommandResponse: OpenCodeResponse = {};
|
||||||
|
sessionCreateResponse: OpenCodeResponse = { data: { id: "session-1" } };
|
||||||
|
sessionDeleteResponse: OpenCodeResponse = {};
|
||||||
|
sessionMessagesResponse: OpenCodeResponse = { data: [] };
|
||||||
|
sessionPromptAsyncResponse: OpenCodeResponse = {};
|
||||||
|
sessionSummarizeResponse: OpenCodeResponse = { data: {} };
|
||||||
|
sessionUpdateResponse: OpenCodeResponse = {};
|
||||||
|
|
||||||
|
asSdkClient(): OpencodeClient {
|
||||||
|
return {
|
||||||
|
app: {
|
||||||
|
agents: async (parameters: unknown) => {
|
||||||
|
this.calls.appAgents.push(parameters);
|
||||||
|
return this.appAgentsResponse;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
command: {
|
||||||
|
list: async (parameters: unknown) => {
|
||||||
|
this.calls.commandList.push(parameters);
|
||||||
|
return this.commandListResponse;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
event: {
|
||||||
|
subscribe: async (parameters: unknown, options: unknown) => {
|
||||||
|
this.calls.eventSubscribe.push({ parameters, options });
|
||||||
|
return { stream: this.eventStream };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mcp: {
|
||||||
|
add: async () => ({}),
|
||||||
|
connect: async () => ({}),
|
||||||
|
},
|
||||||
|
permission: {
|
||||||
|
reply: async (parameters: unknown) => {
|
||||||
|
this.calls.permissionReply.push(parameters);
|
||||||
|
return this.permissionReplyResponse;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
provider: {
|
||||||
|
list: async (parameters: unknown) => {
|
||||||
|
this.calls.providerList.push(parameters);
|
||||||
|
return this.providerListImplementation
|
||||||
|
? await this.providerListImplementation()
|
||||||
|
: this.providerListResponse;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
question: {
|
||||||
|
reject: async (parameters: unknown) => {
|
||||||
|
this.calls.questionReject.push(parameters);
|
||||||
|
return this.questionRejectResponse;
|
||||||
|
},
|
||||||
|
reply: async (parameters: unknown) => {
|
||||||
|
this.calls.questionReply.push(parameters);
|
||||||
|
return this.questionReplyResponse;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
session: {
|
||||||
|
abort: async (parameters: unknown) => {
|
||||||
|
this.calls.sessionAbort.push(parameters);
|
||||||
|
return this.sessionAbortResponse;
|
||||||
|
},
|
||||||
|
command: async (parameters: unknown) => {
|
||||||
|
this.calls.sessionCommand.push(parameters);
|
||||||
|
if (this.sessionCommandError) {
|
||||||
|
throw this.sessionCommandError;
|
||||||
|
}
|
||||||
|
return this.sessionCommandResponse;
|
||||||
|
},
|
||||||
|
create: async (parameters: unknown) => {
|
||||||
|
this.calls.sessionCreate.push(parameters);
|
||||||
|
return this.sessionCreateResponse;
|
||||||
|
},
|
||||||
|
delete: async (parameters: unknown) => {
|
||||||
|
this.calls.sessionDelete.push(parameters);
|
||||||
|
return this.sessionDeleteResponse;
|
||||||
|
},
|
||||||
|
messages: async (parameters: unknown) => {
|
||||||
|
this.calls.sessionMessages.push(parameters);
|
||||||
|
return this.sessionMessagesResponse;
|
||||||
|
},
|
||||||
|
promptAsync: async (parameters: unknown) => {
|
||||||
|
this.calls.sessionPromptAsync.push(parameters);
|
||||||
|
return this.sessionPromptAsyncResponse;
|
||||||
|
},
|
||||||
|
summarize: async (parameters: unknown) => {
|
||||||
|
this.calls.sessionSummarize.push(parameters);
|
||||||
|
return this.sessionSummarizeResponse;
|
||||||
|
},
|
||||||
|
update: async (parameters: unknown) => {
|
||||||
|
this.calls.sessionUpdate.push(parameters);
|
||||||
|
return this.sessionUpdateResponse;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as unknown as OpencodeClient;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createEventStream(events: unknown[]): AsyncGenerator<unknown> {
|
||||||
|
return (async function* () {
|
||||||
|
for (const event of events) {
|
||||||
|
yield event;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function idleEvent(): unknown {
|
||||||
|
return {
|
||||||
|
type: "session.idle",
|
||||||
|
properties: { sessionID: "session-1" },
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user