import { describe, expect, it } from "bun:test"; import { type OpenCodeClient } from "@opencode-ai/client"; import { getEmbeddedServicePaths, OpencodeRuntimeAdapter, } from "../../src/runtime/opencode.js"; describe("getEmbeddedServicePaths", () => { it("isolates service registrations by workspace and Agent port", () => { const internal = getEmbeddedServicePaths("/srv/tjwater-agent", 8787); const customer = getEmbeddedServicePaths("/srv/tjwater-agent-customer", 8787); const secondPort = getEmbeddedServicePaths("/srv/tjwater-agent", 8788); expect(internal.registrationFile).not.toBe(customer.registrationFile); expect(internal.registrationFile).not.toBe(secondPort.registrationFile); expect(internal.registrationFile).toEndWith( "/data/opencode-service/8787/opencode/service.json", ); }); }); const createRuntimeAdapter = ( messages: unknown[], calls: { staged: string[]; committed: string[] } = { staged: [], committed: [], }, ) => Object.assign(Object.create(OpencodeRuntimeAdapter.prototype), { messages: async () => messages, ensureClient: async () => ({ session: { revert: { stage: async ({ messageID }: { messageID: string }) => { calls.staged.push(messageID); }, commit: async ({ sessionID }: { sessionID: string }) => { calls.committed.push(sessionID); }, }, }, }) as unknown as OpenCodeClient, }) as OpencodeRuntimeAdapter; describe("OpencodeRuntimeAdapter.revertToUserMessage", () => { it("skips reverting the first user message when the runtime session is empty", async () => { const calls = { staged: [] as string[], committed: [] as string[] }; const runtime = createRuntimeAdapter([], calls); await runtime.revertToUserMessage("session-1", { userOrdinal: 1 }); expect(calls).toEqual({ staged: [], committed: [] }); }); 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("stages and commits the V2 revert at the target user message", async () => { const calls = { staged: [] as string[], committed: [] 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({ staged: ["user-2"], committed: ["session-1"], }); }); }); describe("OpencodeRuntimeAdapter.ensureClient", () => { it("retries bootstrap after a failed startup attempt", async () => { let attempts = 0; const client = { health: { get: async () => ({ healthy: true }) }, } as unknown as OpenCodeClient; const runtime = Object.assign(Object.create(OpencodeRuntimeAdapter.prototype), { clientPromise: 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.warmup", () => { it("rejects a service that is not the pinned V2 release", async () => { const client = { health: { get: async () => ({ healthy: true, version: "1.18.12" }), }, } as unknown as OpenCodeClient; const runtime = Object.assign(Object.create(OpencodeRuntimeAdapter.prototype), { clientPromise: null, ensureClient: async () => client, }) as OpencodeRuntimeAdapter; await expect(runtime.warmup()).rejects.toThrow( "incompatible OpenCode service version", ); }); it("checks V2 health, catalogs and plugins, then removes the probe session", async () => { const calls: string[] = []; const client = { health: { get: async () => { calls.push("health.get"); return { healthy: true, version: "0.0.0-next-16741" }; }, }, session: { create: async () => { calls.push("session.create"); return { id: "warmup-session" }; }, remove: async ({ sessionID }: { sessionID: string }) => { calls.push(`session.remove:${sessionID}`); }, }, model: { list: async () => { calls.push("model.list"); return { data: [] }; }, }, plugin: { list: async () => { calls.push("plugin.list"); return []; }, }, } as unknown as OpenCodeClient; const runtime = Object.assign(Object.create(OpencodeRuntimeAdapter.prototype), { clientPromise: null, ensureClient: async () => client, }) as OpencodeRuntimeAdapter; await runtime.warmup(); expect(calls).toEqual([ "health.get", "session.create", "model.list", "plugin.list", "session.remove:warmup-session", ]); }); });