import { describe, expect, it } from "bun:test"; import { type OpenCodeClient as OpencodeClient } from "@opencode/client"; import { createServer } from "node:net"; import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { config } from "../../src/config.js"; import { OpencodeRuntimeAdapter, resolveEmbeddedOpencodePort, } from "../../src/runtime/opencode.js"; const createRuntimeAdapter = ( messages: unknown[], calls: { reverted: string[]; removed: string[]; } = { reverted: [], removed: [] }, ) => Object.assign(Object.create(OpencodeRuntimeAdapter.prototype), { messages: async () => messages, revertMessage: async (_sessionId: string, messageId: string) => { calls.reverted.push(messageId); }, removeMessage: async (_sessionId: string, messageId: string) => { calls.removed.push(messageId); }, }) as OpencodeRuntimeAdapter; describe("OpencodeRuntimeAdapter.revertToUserMessage", () => { it("skips reverting the first user message when the runtime session is empty", async () => { const calls = { reverted: [] as string[], removed: [] as string[] }; const runtime = createRuntimeAdapter([], calls); await runtime.revertToUserMessage("session-1", { userOrdinal: 1 }); expect(calls).toEqual({ reverted: [], removed: [] }); }); it("keeps ordinal mismatches visible when runtime messages exist", async () => { const runtime = createRuntimeAdapter([ { info: { id: "user-1", role: "user" } }, { info: { id: "assistant-1", role: "assistant" } }, ]); await expect( runtime.revertToUserMessage("session-1", { userOrdinal: 2 }), ).rejects.toThrow("target user message not found to revert"); }); it("reverts and removes messages from the target user message onward", async () => { const calls = { reverted: [] as string[], removed: [] as string[] }; const runtime = createRuntimeAdapter( [ { info: { id: "user-1", role: "user" } }, { info: { id: "assistant-1", role: "assistant" } }, { info: { id: "user-2", role: "user" } }, { info: { id: "assistant-2", role: "assistant" } }, ], calls, ); await runtime.revertToUserMessage("session-1", { userOrdinal: 2 }); expect(calls).toEqual({ reverted: ["user-2"], removed: ["user-2"], }); }); }); describe("OpencodeRuntimeAdapter.ensureClient", () => { it("retries bootstrap after a failed startup attempt", async () => { let attempts = 0; const client = { global: { health: async () => ({ data: { healthy: true } }) }, } as unknown as OpencodeClient; const runtime = Object.assign(Object.create(OpencodeRuntimeAdapter.prototype), { clientPromise: null, closeServer: null, bootstrapClient: async () => { attempts += 1; if (attempts === 1) { throw new Error("startup failed"); } return client; }, }) as OpencodeRuntimeAdapter; await expect(runtime.ensureClient()).rejects.toThrow("startup failed"); await expect(runtime.ensureClient()).resolves.toBe(client); expect(attempts).toBe(2); }); }); describe("OpencodeRuntimeAdapter.resolveAvailableModel", () => { it("selects the first preferred model actually exposed by OpenCode", async () => { const client = { rpc: { call: async () => ({ output: [] }) }, model: { list: async () => ({ data: [{ providerID: "deepseek", id: "deepseek-v4-pro", enabled: true, capabilities: { tools: true } }] }), }, } as unknown as OpencodeClient; const runtime = Object.assign(Object.create(OpencodeRuntimeAdapter.prototype), { ensureClient: async () => client, }) as OpencodeRuntimeAdapter; await expect(runtime.resolveAvailableModel([ "deepseek/deepseek-flash", "deepseek/deepseek-v4-pro", ])).resolves.toBe("deepseek/deepseek-v4-pro"); }); }); describe("resolveEmbeddedOpencodePort", () => { it("falls back to a dynamic port when the configured port is occupied", async () => { const server = createServer(); await new Promise((resolve, reject) => { server.once("error", reject); server.listen(0, "127.0.0.1", resolve); }); const address = server.address(); if (!address || typeof address === "string") { server.close(); throw new Error("expected a TCP server address"); } try { await expect( resolveEmbeddedOpencodePort("127.0.0.1", address.port), ).resolves.toBe(0); } finally { await new Promise((resolve, reject) => { server.close((error) => (error ? reject(error) : resolve())); }); } }); }); describe("OpencodeRuntimeAdapter.warmup", () => { const toolIds = ["shell", "web_search", "geocode", "query_project_data", "final_answer", "locate_features", "zoom_to_map", "show_chart", "view_history", "session_search", "memory_manager", "skill_manager", "render_scada_analysis", "clear_scada_analysis", "get_operational_task_context", "query_operational_task_data", "publish_execution_plan", "publish_task_report", "propose_task_action", "add_task_human_evidence", "request_task_data_access", "todoread", "todowrite"]; async function createWarmupRuntime(missing: string[] = []) { const root = await mkdtemp(join(tmpdir(), "warmup-workspace-test-")); const previousRoot = config.CONVERSATION_WORKSPACE_ROOT; config.CONVERSATION_WORKSPACE_ROOT = root; const calls: string[] = []; const [providerID, id] = config.OPENCODE_MODEL.split("/"); const client = { server: { info: async () => { calls.push("health"); return { version: "2.0.24" }; } }, session: { create: async (input: { location: { directory: string } }) => { calls.push("session.create"); return { id: "warmup-session", location: input.location }; }, remove: async () => { calls.push("session.remove"); }, }, model: { list: async () => { calls.push("model.list"); return { data: [{ providerID, id, enabled: true, capabilities: { tools: true } }] }; } }, rpc: { call: async (input: { method: string }) => { if (input.method === "mcp") { calls.push("mcp.list"); return { output: { servers: [], configuration_errors: [] } }; } calls.push("tool.list"); return { output: toolIds.filter((id) => !missing.includes(id)).map((id) => ({ id, direct: true, sandboxed: id === "shell", workspaceRoot: root })) }; } }, } as unknown as OpencodeClient; const runtime = Object.assign(Object.create(OpencodeRuntimeAdapter.prototype), { ensureClient: async () => client }) as OpencodeRuntimeAdapter; return { runtime, calls, cleanup: async () => { config.CONVERSATION_WORKSPACE_ROOT = previousRoot; await rm(root, { recursive: true, force: true }); } }; } it("verifies business tools and model before reporting ready and removes the warmup session", async () => { const { runtime, calls, cleanup } = await createWarmupRuntime(); try { await runtime.warmup(); expect(calls).toEqual(["health", "session.create", "tool.list", "mcp.list", "model.list", "session.remove"]); } finally { await cleanup(); } }); it("fails readiness if a business tool was lost and still cleans up", async () => { const { runtime, calls, cleanup } = await createWarmupRuntime(["render_scada_analysis"]); try { await expect(runtime.warmup()).rejects.toThrow("business tools missing: render_scada_analysis"); expect(calls.at(-1)).toBe("session.remove"); } finally { await cleanup(); } }); }); describe("conversation workspace binding", () => { it("reads MCP status without migrating a legacy session or creating a workspace", async () => { let location: unknown; const runtime = Object.assign(Object.create(OpencodeRuntimeAdapter.prototype), { ensureClient: async () => ({ session: { get: async () => ({ location: { directory: process.cwd() } }), move: async () => { throw new Error("GET must not move sessions"); } }, rpc: { call: async (input: { location: unknown }) => { location = input.location; return { output: { servers: [], configuration_errors: [] } }; } }, }), ensureSessionWorkspace: async () => { throw new Error("GET must not create workspaces"); }, }) as OpencodeRuntimeAdapter; expect(await runtime.mcpStatus("legacy-session")).toEqual({ servers: [], configuration_errors: [] }); expect(location).toEqual({ directory: process.cwd() }); }); it("coalesces old-session migration and restores its persisted directory", async () => { const previousRoot = config.CONVERSATION_WORKSPACE_ROOT; const root = await mkdtemp(join(tmpdir(), "runtime-workspace-test-")); config.CONVERSATION_WORKSPACE_ROOT = root; let directory = process.cwd(); let moves = 0; const client = { session: { get: async () => ({ id: "old-session", location: { directory } }), move: async (input: { directory: string }) => { moves++; directory = input.directory; }, }, } as unknown as OpencodeClient; const runtime = Object.assign(Object.create(OpencodeRuntimeAdapter.prototype), { ensureClient: async () => client }) as OpencodeRuntimeAdapter; try { const [first, second] = await Promise.all([runtime.ensureSessionWorkspace("old-session"), runtime.ensureSessionWorkspace("old-session")]); expect(first).toBe(second); expect(first).not.toBe(process.cwd()); expect(moves).toBe(1); const restored = Object.assign(Object.create(OpencodeRuntimeAdapter.prototype), { ensureClient: async () => client }) as OpencodeRuntimeAdapter; expect(await restored.ensureSessionWorkspace("old-session")).toBe(first); expect(moves).toBe(1); directory = "/tmp"; await expect(restored.ensureSessionWorkspace("foreign-session")).rejects.toThrow("inside"); } finally { config.CONVERSATION_WORKSPACE_ROOT = previousRoot; await rm(root, { recursive: true }); } }); }); describe("explicit native Shell approval", () => { it("ties the pending decision to the full command and originating native call", async () => { const events = [ { type: "connected" }, { type: "permission.replied", data: { sessionID: "other", requestID: "perm-test", reply: "once" } }, { type: "permission.replied", data: { sessionID: "session-test", requestID: "perm-test", reply: "once" } }, ]; let created: unknown; const client = { event: { subscribe: () => ({ async *[Symbol.asyncIterator]() { for (const event of events) yield event; } }) }, permission: { create: async (input: unknown) => { created = input; return { id: "perm-test", effect: "ask" }; } }, } as unknown as OpencodeClient; const runtime = Object.assign(Object.create(OpencodeRuntimeAdapter.prototype), { ensureClient: async () => client }) as OpencodeRuntimeAdapter; await runtime.approveSandboxShell({ sessionId: "session-test", command: "printf hello", callId: "call-test", messageId: "message-test" }, new AbortController().signal); expect(created).toMatchObject({ action: "shell", resources: ["printf hello"], save: [], source: { type: "tool", id: "call-test", messageID: "message-test" } }); events[2] = { type: "permission.replied", data: { sessionID: "session-test", requestID: "perm-test", reply: "reject" } }; await expect(runtime.approveSandboxShell({ sessionId: "session-test", command: "printf hello", callId: "call-test", messageId: "message-test" }, new AbortController().signal)).rejects.toThrow("rejected"); }); });