|
|
@@ -1,293 +0,0 @@
|
|
|
-import { describe, expect } from "bun:test"
|
|
|
-import type {
|
|
|
- AuthenticateResponse,
|
|
|
- CloseSessionResponse,
|
|
|
- InitializeResponse,
|
|
|
- LoadSessionResponse,
|
|
|
- NewSessionResponse,
|
|
|
- PromptResponse,
|
|
|
- ResumeSessionResponse,
|
|
|
- SessionNotification,
|
|
|
- SetSessionConfigOptionResponse,
|
|
|
-} from "@agentclientprotocol/sdk"
|
|
|
-import { Effect } from "effect"
|
|
|
-import { cliIt } from "../../lib/cli-process"
|
|
|
-import { testProviderConfig } from "../../lib/test-provider"
|
|
|
-import { createAcpClient, expectOk, firstAlternateValue, selectConfigOption } from "../acp/acp-test-client"
|
|
|
-
|
|
|
-describe("opencode acp-next (subprocess)", () => {
|
|
|
- cliIt.live(
|
|
|
- "responds to initialize behind OPENCODE_ACP_NEXT",
|
|
|
- ({ opencode }) =>
|
|
|
- Effect.gen(function* () {
|
|
|
- const acp = createAcpClient(yield* opencode.acp({ env: { OPENCODE_ACP_NEXT: "1" } }))
|
|
|
- const initialized = expectOk(
|
|
|
- yield* acp.request<InitializeResponse>("initialize", {
|
|
|
- protocolVersion: 1,
|
|
|
- clientCapabilities: { _meta: { "terminal-auth": true } },
|
|
|
- }),
|
|
|
- )
|
|
|
-
|
|
|
- expect(initialized.protocolVersion).toBe(1)
|
|
|
- expect(initialized.agentCapabilities?.promptCapabilities?.embeddedContext).toBe(true)
|
|
|
- expect(initialized.agentCapabilities?.promptCapabilities?.image).toBe(true)
|
|
|
- expect(initialized.agentCapabilities?.mcpCapabilities?.http).toBe(true)
|
|
|
- expect(initialized.agentCapabilities?.mcpCapabilities?.sse).toBe(true)
|
|
|
- expect(initialized.agentCapabilities?.loadSession).toBe(true)
|
|
|
- expect(initialized.agentCapabilities?.sessionCapabilities?.close).toEqual({})
|
|
|
- expect(initialized.agentCapabilities?.sessionCapabilities?.fork).toEqual({})
|
|
|
- expect(initialized.agentCapabilities?.sessionCapabilities?.list).toEqual({})
|
|
|
- expect(initialized.agentCapabilities?.sessionCapabilities?.resume).toEqual({})
|
|
|
- expect(initialized.agentInfo?.name).toBe("OpenCode")
|
|
|
- expect(initialized.authMethods?.[0]?.id).toBe("opencode-login")
|
|
|
- expect(initialized.authMethods?.[0]?._meta?.["terminal-auth"]).toBeDefined()
|
|
|
- }),
|
|
|
- 60_000,
|
|
|
- )
|
|
|
-
|
|
|
- cliIt.live(
|
|
|
- "authenticate succeeds for the advertised auth method and rejects unknown methods safely",
|
|
|
- ({ opencode }) =>
|
|
|
- Effect.gen(function* () {
|
|
|
- const acp = createAcpClient(yield* opencode.acp({ env: { OPENCODE_ACP_NEXT: "1" } }))
|
|
|
- const initialized = expectOk(yield* acp.request<InitializeResponse>("initialize", { protocolVersion: 1 }))
|
|
|
- const methodId = initialized.authMethods?.[0]?.id
|
|
|
- expect(methodId).toBe("opencode-login")
|
|
|
-
|
|
|
- expectOk(yield* acp.request<AuthenticateResponse>("authenticate", { methodId }))
|
|
|
-
|
|
|
- const rejected = yield* acp.request<AuthenticateResponse>("authenticate", { methodId: "missing-auth-method" })
|
|
|
- expect(errorCode(rejected.error)).toBe(-32602)
|
|
|
- }),
|
|
|
- 60_000,
|
|
|
- )
|
|
|
-
|
|
|
- cliIt.live(
|
|
|
- "creates and loads sessions behind OPENCODE_ACP_NEXT",
|
|
|
- ({ home, llm, opencode }) =>
|
|
|
- Effect.gen(function* () {
|
|
|
- const acp = createAcpClient(
|
|
|
- yield* opencode.acp({
|
|
|
- env: {
|
|
|
- OPENCODE_ACP_NEXT: "1",
|
|
|
- OPENCODE_CONFIG_CONTENT: JSON.stringify(testProviderConfig(llm.url)),
|
|
|
- },
|
|
|
- }),
|
|
|
- )
|
|
|
- yield* acp.request<InitializeResponse>("initialize", { protocolVersion: 1 })
|
|
|
-
|
|
|
- const session = expectOk(yield* acp.request<NewSessionResponse>("session/new", { cwd: home, mcpServers: [] }))
|
|
|
- expect(typeof session.sessionId).toBe("string")
|
|
|
- expect(selectConfigOption(session.configOptions, "model")?.category).toBe("model")
|
|
|
-
|
|
|
- const update = yield* acp.waitForNotification<SessionNotification>(
|
|
|
- "session/update",
|
|
|
- (params) =>
|
|
|
- params.sessionId === session.sessionId && params.update.sessionUpdate === "available_commands_update",
|
|
|
- )
|
|
|
- expect(update.params?.sessionId).toBe(session.sessionId)
|
|
|
-
|
|
|
- const loaded = expectOk(
|
|
|
- yield* acp.request<LoadSessionResponse>("session/load", {
|
|
|
- cwd: home,
|
|
|
- sessionId: session.sessionId,
|
|
|
- mcpServers: [],
|
|
|
- }),
|
|
|
- )
|
|
|
- expect(selectConfigOption(loaded.configOptions, "model")?.category).toBe("model")
|
|
|
-
|
|
|
- yield* llm.text("hello from acp-next", { usage: { input: 11, output: 7 } })
|
|
|
- const prompted = expectOk(
|
|
|
- yield* acp.request<PromptResponse>("session/prompt", {
|
|
|
- sessionId: session.sessionId,
|
|
|
- prompt: [{ type: "text", text: "hello" }],
|
|
|
- }),
|
|
|
- )
|
|
|
- expect(prompted.stopReason).toBe("end_turn")
|
|
|
- expect(prompted.usage?.totalTokens).toBeGreaterThan(0)
|
|
|
-
|
|
|
- const missing = yield* acp.request("session/prompt", {
|
|
|
- sessionId: "ses_missing",
|
|
|
- prompt: [{ type: "text", text: "hello" }],
|
|
|
- })
|
|
|
- expect(errorCode(missing.error)).toBe(-32602)
|
|
|
- }),
|
|
|
- 60_000,
|
|
|
- )
|
|
|
-
|
|
|
- cliIt.live(
|
|
|
- "switches model through config options behind OPENCODE_ACP_NEXT",
|
|
|
- ({ home, llm, opencode }) =>
|
|
|
- Effect.gen(function* () {
|
|
|
- const acp = createAcpClient(
|
|
|
- yield* opencode.acp({
|
|
|
- env: {
|
|
|
- OPENCODE_ACP_NEXT: "1",
|
|
|
- OPENCODE_CONFIG_CONTENT: JSON.stringify(verifierConfig(llm.url)),
|
|
|
- },
|
|
|
- }),
|
|
|
- )
|
|
|
- yield* acp.request<InitializeResponse>("initialize", { protocolVersion: 1 })
|
|
|
- const session = expectOk(yield* acp.request<NewSessionResponse>("session/new", { cwd: home, mcpServers: [] }))
|
|
|
-
|
|
|
- const updated = expectOk(
|
|
|
- yield* acp.request<SetSessionConfigOptionResponse>("session/set_config_option", {
|
|
|
- sessionId: session.sessionId,
|
|
|
- configId: "model",
|
|
|
- value: "test/second-model",
|
|
|
- }),
|
|
|
- )
|
|
|
-
|
|
|
- expect(selectConfigOption(updated.configOptions, "model")?.currentValue).toBe("test/second-model")
|
|
|
- }),
|
|
|
- 60_000,
|
|
|
- )
|
|
|
-
|
|
|
- cliIt.live(
|
|
|
- "switches effort through config options behind OPENCODE_ACP_NEXT",
|
|
|
- ({ home, llm, opencode }) =>
|
|
|
- Effect.gen(function* () {
|
|
|
- const acp = createAcpClient(
|
|
|
- yield* opencode.acp({
|
|
|
- env: {
|
|
|
- OPENCODE_ACP_NEXT: "1",
|
|
|
- OPENCODE_CONFIG_CONTENT: JSON.stringify(verifierConfig(llm.url)),
|
|
|
- },
|
|
|
- }),
|
|
|
- )
|
|
|
- yield* acp.request<InitializeResponse>("initialize", { protocolVersion: 1 })
|
|
|
- const session = expectOk(yield* acp.request<NewSessionResponse>("session/new", { cwd: home, mcpServers: [] }))
|
|
|
- const effort = selectConfigOption(session.configOptions, "effort")
|
|
|
- expect(effort?.category).toBe("thought_level")
|
|
|
- const nextEffort = effort ? firstAlternateValue(effort) : undefined
|
|
|
- expect(nextEffort).toBe("high")
|
|
|
-
|
|
|
- const updated = expectOk(
|
|
|
- yield* acp.request<SetSessionConfigOptionResponse>("session/set_config_option", {
|
|
|
- sessionId: session.sessionId,
|
|
|
- configId: "effort",
|
|
|
- value: nextEffort,
|
|
|
- }),
|
|
|
- )
|
|
|
-
|
|
|
- expect(selectConfigOption(updated.configOptions, "effort")?.currentValue).toBe(nextEffort)
|
|
|
- }),
|
|
|
- 60_000,
|
|
|
- )
|
|
|
-
|
|
|
- cliIt.live(
|
|
|
- "advertises and supports close behind OPENCODE_ACP_NEXT",
|
|
|
- ({ home, llm, opencode }) =>
|
|
|
- Effect.gen(function* () {
|
|
|
- const acp = createAcpClient(
|
|
|
- yield* opencode.acp({
|
|
|
- env: {
|
|
|
- OPENCODE_ACP_NEXT: "1",
|
|
|
- OPENCODE_CONFIG_CONTENT: JSON.stringify(verifierConfig(llm.url)),
|
|
|
- },
|
|
|
- }),
|
|
|
- )
|
|
|
- const initialized = expectOk(yield* acp.request<InitializeResponse>("initialize", { protocolVersion: 1 }))
|
|
|
- expect(initialized.agentCapabilities?.sessionCapabilities?.close).toEqual({})
|
|
|
- const session = expectOk(yield* acp.request<NewSessionResponse>("session/new", { cwd: home, mcpServers: [] }))
|
|
|
-
|
|
|
- expectOk(yield* acp.request<CloseSessionResponse>("session/close", { sessionId: session.sessionId }))
|
|
|
- }),
|
|
|
- 60_000,
|
|
|
- )
|
|
|
-
|
|
|
- cliIt.live(
|
|
|
- "advertises and supports resume behind OPENCODE_ACP_NEXT",
|
|
|
- ({ home, llm, opencode }) =>
|
|
|
- Effect.gen(function* () {
|
|
|
- const acp = createAcpClient(
|
|
|
- yield* opencode.acp({
|
|
|
- env: {
|
|
|
- OPENCODE_ACP_NEXT: "1",
|
|
|
- OPENCODE_CONFIG_CONTENT: JSON.stringify(verifierConfig(llm.url)),
|
|
|
- },
|
|
|
- }),
|
|
|
- )
|
|
|
- const initialized = expectOk(yield* acp.request<InitializeResponse>("initialize", { protocolVersion: 1 }))
|
|
|
- expect(initialized.agentCapabilities?.sessionCapabilities?.resume).toEqual({})
|
|
|
- const session = expectOk(yield* acp.request<NewSessionResponse>("session/new", { cwd: home, mcpServers: [] }))
|
|
|
- const resumed = expectOk(
|
|
|
- yield* acp.request<ResumeSessionResponse>("session/resume", {
|
|
|
- cwd: home,
|
|
|
- sessionId: session.sessionId,
|
|
|
- mcpServers: [],
|
|
|
- }),
|
|
|
- )
|
|
|
-
|
|
|
- expect(selectConfigOption(resumed.configOptions, "model")?.category).toBe("model")
|
|
|
- }),
|
|
|
- 60_000,
|
|
|
- )
|
|
|
-
|
|
|
- cliIt.live(
|
|
|
- "exits cleanly when flagged stdin is closed",
|
|
|
- ({ opencode }) =>
|
|
|
- Effect.gen(function* () {
|
|
|
- const exitedPromise = yield* Effect.scoped(
|
|
|
- Effect.gen(function* () {
|
|
|
- const acp = yield* opencode.acp({ env: { OPENCODE_ACP_NEXT: "1" } })
|
|
|
- return acp.exited
|
|
|
- }),
|
|
|
- )
|
|
|
-
|
|
|
- const code = yield* Effect.promise(() => exitedPromise)
|
|
|
- expect(typeof code === "number" || code === null).toBe(true)
|
|
|
- }),
|
|
|
- 60_000,
|
|
|
- )
|
|
|
-
|
|
|
- cliIt.live(
|
|
|
- "default unflagged path still uses production ACP",
|
|
|
- ({ opencode }) =>
|
|
|
- Effect.gen(function* () {
|
|
|
- const acp = createAcpClient(yield* opencode.acp())
|
|
|
- const initialized = expectOk(yield* acp.request<InitializeResponse>("initialize", { protocolVersion: 1 }))
|
|
|
-
|
|
|
- expect(initialized.agentCapabilities?.sessionCapabilities?.close).toEqual({})
|
|
|
- expect(initialized.agentCapabilities?.sessionCapabilities?.resume).toEqual({})
|
|
|
- }),
|
|
|
- 60_000,
|
|
|
- )
|
|
|
-})
|
|
|
-
|
|
|
-function errorCode(error: unknown) {
|
|
|
- if (!error || typeof error !== "object") return undefined
|
|
|
- if (!("code" in error)) return undefined
|
|
|
- return typeof error.code === "number" ? error.code : undefined
|
|
|
-}
|
|
|
-
|
|
|
-function verifierConfig(llmUrl: string) {
|
|
|
- const config = testProviderConfig(llmUrl)
|
|
|
- return {
|
|
|
- ...config,
|
|
|
- model: "test/test-model",
|
|
|
- provider: {
|
|
|
- test: {
|
|
|
- ...config.provider.test,
|
|
|
- models: {
|
|
|
- "test-model": {
|
|
|
- ...config.provider.test.models["test-model"],
|
|
|
- variants: {
|
|
|
- low: {},
|
|
|
- high: {},
|
|
|
- },
|
|
|
- },
|
|
|
- "second-model": {
|
|
|
- ...config.provider.test.models["test-model"],
|
|
|
- id: "second-model",
|
|
|
- name: "Second Test Model",
|
|
|
- variants: {
|
|
|
- medium: {},
|
|
|
- max: {},
|
|
|
- },
|
|
|
- },
|
|
|
- },
|
|
|
- },
|
|
|
- },
|
|
|
- }
|
|
|
-}
|