import { describe, expect, it } from "bun:test" import type { AgentSideConnection, LoadSessionResponse, NewSessionResponse } from "@agentclientprotocol/sdk" import type { OpencodeClient } from "@opencode-ai/sdk/v2" import { Effect } from "effect" import * as ACPNextService from "@/acp-next/service" import * as ACPNextError from "@/acp-next/error" import { ModelID, ProviderID } from "@/provider/schema" import type { Provider } from "@/provider/provider" const providerID = ProviderID.make("test") const modelID = ModelID.make("test-model") const configuredModelID = ModelID.make("configured-model") const provider: Provider.Info = { id: providerID, name: "Test", source: "config", env: [], options: {}, models: { [modelID]: { id: modelID, providerID, api: { id: modelID, url: "https://example.com", npm: "@ai-sdk/openai-compatible", }, name: "Test Model", family: "test", capabilities: { temperature: true, reasoning: true, attachment: false, toolcall: true, input: { text: true, audio: false, image: false, video: false, pdf: false }, output: { text: true, audio: false, image: false, video: false, pdf: false }, interleaved: false, }, cost: { input: 0, output: 0, cache: { read: 0, write: 0 }, }, limit: { context: 128000, output: 4096, }, status: "active", options: {}, headers: {}, release_date: "2026-01-01", variants: { default: {}, high: { reasoningEffort: "high" }, }, }, [configuredModelID]: { id: configuredModelID, providerID, api: { id: configuredModelID, url: "https://example.com", npm: "@ai-sdk/openai-compatible", }, name: "Configured Model", family: "test", capabilities: { temperature: true, reasoning: false, attachment: false, toolcall: true, input: { text: true, audio: false, image: false, video: false, pdf: false }, output: { text: true, audio: false, image: false, video: false, pdf: false }, interleaved: false, }, cost: { input: 0, output: 0, cache: { read: 0, write: 0 }, }, limit: { context: 128000, output: 4096, }, status: "active", options: {}, headers: {}, release_date: "2026-01-01", }, }, } describe("ACP next service sessions", () => { const makeService = (messages: readonly { info: unknown; parts: readonly unknown[] }[] = []) => { const updates: unknown[] = [] const mcpAdds: string[] = [] const sdk = { config: { providers: () => Promise.resolve({ data: { providers: [provider], default: { test: modelID } } }), get: () => Promise.resolve({ data: {} }), }, app: { agents: () => Promise.resolve({ data: [ { name: "build", mode: "primary", permission: [], options: {} }, { name: "plan", mode: "primary", description: "Plan first", permission: [], options: {} }, { name: "hidden", mode: "primary", hidden: true, permission: [], options: {} }, ], }), skills: () => Promise.resolve({ data: [{ name: "review-skill", description: "Review", location: "/skills/review", content: "review" }], }), }, command: { list: () => Promise.resolve({ data: [{ name: "init", description: "Initialize", source: "command", template: "init", hints: [] }], }), }, session: { create: () => Promise.resolve({ data: { id: "ses_new" } }), get: () => Promise.resolve({ data: { id: "ses_loaded" } }), list: () => Promise.resolve({ data: [] }), messages: () => Promise.resolve({ data: messages }), }, mcp: { add: (input: { name?: string }) => { if (input.name) mcpAdds.push(input.name) return Promise.resolve({ data: {} }) }, }, } as unknown as OpencodeClient const connection = { sessionUpdate: (update: unknown) => { updates.push(update) return Promise.resolve() }, } as Pick return { service: ACPNextService.make({ sdk, connection }), updates, mcpAdds } } it("creates a backed session with config options and command update", async () => { const { service, updates, mcpAdds } = makeService() const result = await Effect.runPromise( service.newSession({ cwd: "/workspace", mcpServers: [ { name: "tools", command: "node", args: ["server.js"], env: [] }, { name: "tools", command: "node", args: ["server.js"], env: [] }, ], }), ) await new Promise((resolve) => setTimeout(resolve, 5)) expect(result.sessionId).toBe("ses_new") expect(categories(result)).toContain("model") expect(categories(result)).toContain("thought_level") expect(categories(result)).toContain("mode") expect(updates).toHaveLength(1) expect(JSON.stringify(updates[0])).toContain("available_commands_update") expect(JSON.stringify(updates[0])).toContain("review-skill") expect(mcpAdds).toEqual(["tools"]) }) it("loads a session and restores model variant and mode from messages", async () => { const { service } = makeService([ { info: { role: "assistant", providerID: "test", modelID: "test-model", variant: "high", mode: "plan", }, parts: [], }, ]) const result = await Effect.runPromise( service.loadSession({ cwd: "/workspace", sessionId: "ses_loaded", mcpServers: [] }), ) expect(result.configOptions?.find((option) => option.id === "effort")?.currentValue).toBe("high") expect(result.configOptions?.find((option) => option.id === "mode")?.currentValue).toBe("plan") }) it("restores model variant and mode from the latest user message", async () => { const { service } = makeService([ { info: { role: "user", model: { providerID: "test", modelID: "test-model", variant: "default" }, agent: "build", }, parts: [], }, { info: { role: "user", model: { providerID: "test", modelID: "test-model", variant: "high" }, agent: "plan", }, parts: [], }, ]) const result = await Effect.runPromise( service.loadSession({ cwd: "/workspace", sessionId: "ses_loaded", mcpServers: [] }), ) expect(result.configOptions?.find((option) => option.id === "effort")?.currentValue).toBe("high") expect(result.configOptions?.find((option) => option.id === "mode")?.currentValue).toBe("plan") }) it("maps provider auth failures to auth-required request errors", async () => { const service = ACPNextService.make({ sdk: { config: { providers: () => Promise.reject({ name: "ProviderAuthError", data: { providerID: "test" } }), get: () => Promise.resolve({ data: {} }), }, app: { agents: () => Promise.resolve({ data: [] }), skills: () => Promise.resolve({ data: [] }), }, command: { list: () => Promise.resolve({ data: [] }), }, } as unknown as OpencodeClient, }) const error = await Effect.runPromise( service .newSession({ cwd: "/workspace", mcpServers: [] }) .pipe(Effect.mapError(ACPNextError.toRequestError), Effect.flip), ) expect(error.code).toBe(-32000) }) it("does not cache failed directory snapshots", async () => { let providersCalls = 0 const sdk = { config: { providers: () => { providersCalls++ if (providersCalls === 1) { return Promise.reject({ name: "ProviderAuthError", data: { providerID: "test" } }) } return Promise.resolve({ data: { providers: [provider], default: { test: modelID } } }) }, get: () => Promise.resolve({ data: {} }), }, app: { agents: () => Promise.resolve({ data: [{ name: "build", mode: "primary", permission: [], options: {} }] }), skills: () => Promise.resolve({ data: [] }), }, command: { list: () => Promise.resolve({ data: [] }), }, session: { create: () => Promise.resolve({ data: { id: "ses_retry" } }), list: () => Promise.resolve({ data: [] }), }, mcp: { add: () => Promise.resolve({ data: {} }), }, } as unknown as OpencodeClient const service = ACPNextService.make({ sdk }) const first = await Effect.runPromise( service .newSession({ cwd: "/workspace", mcpServers: [] }) .pipe(Effect.mapError(ACPNextError.toRequestError), Effect.flip), ) const second = await Effect.runPromise(service.newSession({ cwd: "/workspace", mcpServers: [] })) expect(first.code).toBe(-32000) expect(second.sessionId).toBe("ses_retry") expect(providersCalls).toBe(2) }) it("uses the configured model as the new session default", async () => { const sdk = { config: { providers: () => Promise.resolve({ data: { providers: [provider], default: { test: modelID } } }), get: () => Promise.resolve({ data: { model: "test/configured-model" } }), }, app: { agents: () => Promise.resolve({ data: [{ name: "build", mode: "primary", permission: [], options: {} }] }), skills: () => Promise.resolve({ data: [] }), }, command: { list: () => Promise.resolve({ data: [] }), }, session: { create: (input: { model?: { id?: string } }) => Promise.resolve({ data: { id: input.model?.id } }), list: () => Promise.resolve({ data: [] }), }, mcp: { add: () => Promise.resolve({ data: {} }), }, } as unknown as OpencodeClient const service = ACPNextService.make({ sdk }) const result = await Effect.runPromise(service.newSession({ cwd: "/workspace", mcpServers: [] })) expect(result.sessionId).toBe("configured-model") expect(result.configOptions?.find((option) => option.id === "model")?.currentValue).toBe("test/configured-model") }) }) function categories(result: NewSessionResponse | LoadSessionResponse) { return result.configOptions?.map((option) => option.category) ?? [] }