import { describe, expect, it } from "bun:test"; import { type OpencodeClient } from "@opencode-ai/sdk/v2"; import { createServer } from "node:net"; 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: ["assistant-2", "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("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", () => { it("initializes the project session and model tools before reporting ready", async () => { const calls: string[] = []; const client = { global: { health: async () => { calls.push("health"); return { data: { healthy: true, version: "test" } }; }, }, session: { create: async () => { calls.push("session.create"); return { data: { id: "warmup-session" } }; }, delete: async ({ sessionID }: { sessionID: string }) => { calls.push(`session.delete:${sessionID}`); return { data: true }; }, }, tool: { list: async (model: { provider: string; model: string }) => { calls.push(`tool.list:${model.provider}/${model.model}`); return { data: [] }; }, }, } as unknown as OpencodeClient; const runtime = Object.assign(Object.create(OpencodeRuntimeAdapter.prototype), { clientPromise: null, closeServer: null, ensureClient: async () => client, }) as OpencodeRuntimeAdapter; await runtime.warmup(); expect(calls).toEqual([ "health", "session.create", `tool.list:${config.OPENCODE_MODEL}`, "session.delete:warmup-session", ]); }); });