Something went wrong. Try again.
opencode v2 mirror
Something went wrong. Try again.
10 kB · 347 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348import { describe, expect, test } from "bun:test"import { Effect, Schema } from "effect"import { Backend, Frontend, Handshake, JsonRpc } from "../src/protocol"
const successResponse: Schema.Schema.Type<typeof JsonRpc.Response> = { jsonrpc: "2.0", id: 1, result: null }// @ts-expect-error responses require one outcomeconst missingResponse: Schema.Schema.Type<typeof JsonRpc.Response> = { jsonrpc: "2.0", id: 1 }// @ts-expect-error responses cannot contain both outcomesconst invalidResponse: Schema.Schema.Type<typeof JsonRpc.Response> = { jsonrpc: "2.0", id: 1, result: null, error: { code: -32600, message: "Invalid request" },}void [successResponse, missingResponse, invalidResponse]
test("normalizes an omitted finish reason", () => { expect(Backend.decodeRequest({ jsonrpc: "2.0", id: 1, method: "llm.finish", params: { id: "inv_1" } })).toMatchObject( { params: { id: "inv_1", reason: "stop" } }, )})
test("decodes typed backend notifications", () => { expect( Backend.decodeNotification({ jsonrpc: "2.0", method: "tool.cancel", params: { id: "tool_1", reason: "interrupted" }, }), ).toEqual({ jsonrpc: "2.0", method: "tool.cancel", params: { id: "tool_1", reason: "interrupted" }, }) expect(() => Backend.decodeNotification({ jsonrpc: "2.0", method: "tool.cancel", params: { id: "tool_1", reason: "unknown" }, }), ).toThrow()})
test("requires exactly one JSON-RPC response outcome", () => { const decode = Schema.decodeUnknownSync(JsonRpc.Response) expect(decode({ jsonrpc: "2.0", id: 1, result: null })).toEqual({ jsonrpc: "2.0", id: 1, result: null }) expect(decode({ jsonrpc: "2.0", id: 1, error: { code: -32600, message: "Invalid request" } })).toEqual({ jsonrpc: "2.0", id: 1, error: { code: -32600, message: "Invalid request" }, }) expect(() => decode({ jsonrpc: "2.0", id: 1 })).toThrow() expect(() => decode({ jsonrpc: "2.0", id: 1, result: null, error: { code: -32600, message: "Invalid request" }, }), ).toThrow()})
test("decodes ui.matches text params", () => { expect( Frontend.decodeRequest({ jsonrpc: "2.0", id: 1, method: "ui.matches", params: { text: "OpenCode [ready].*" }, }), ).toMatchObject({ method: "ui.matches", params: { text: "OpenCode [ready].*" } }) expect(() => Frontend.decodeRequest({ jsonrpc: "2.0", id: 1, method: "ui.matches", params: { pattern: "OpenCode.*" }, }), ).toThrow()})
test("decodes semantic click identity", () => { expect( Frontend.decodeRequest({ jsonrpc: "2.0", id: 1, method: "ui.click", params: { target: 12, x: 4, y: 0, semantic: { id: "session.permission.action.once", instance: "permission-1", element: 12, }, }, }), ).toMatchObject({ method: "ui.click", params: { semantic: { id: "session.permission.action.once", instance: "permission-1", element: 12, }, }, })})
test("decodes semantic UI snapshots", () => { expect( Frontend.decodeRequest({ jsonrpc: "2.0", id: 1, method: "ui.snapshot", }), ).toMatchObject({ method: "ui.snapshot" }) const decode = Schema.decodeUnknownSync(Frontend.SemanticSnapshot) expect( decode({ format: "opencode-ui-snapshot-v1", nodes: [ { id: "session.permission", role: "dialog", label: "Permission required", element: 1, expanded: false, }, ], }), ).toMatchObject({ nodes: [{ role: "dialog", expanded: false }] }) expect(() => decode({ format: "opencode-ui-snapshot-v1", nodes: [{ id: "", role: "dialog", element: 0 }], }), ).toThrow() for (const nodes of [ [ { id: "duplicate", role: "dialog", element: 1 }, { id: "duplicate", role: "option", element: 2 }, ], [ { id: "first", role: "dialog", element: 1 }, { id: "second", role: "option", element: 1 }, ], [{ id: "orphan", parent: "missing", role: "option", element: 1 }], [ { id: "first", parent: "second", role: "dialog", element: 1 }, { id: "second", parent: "first", role: "option", element: 2 }, ], ]) expect(() => decode({ format: "opencode-ui-snapshot-v1", nodes })).toThrow()})
test("decodes the simulated tool lifecycle", () => { const registration = { name: "lookup", description: "Look up a value", inputSchema: { type: "object", properties: { query: { type: "string" } }, required: ["query"], additionalProperties: false, }, outputSchema: { type: "object" }, permission: "lookup", options: { codemode: false }, } expect( Backend.decodeRequest({ jsonrpc: "2.0", id: 1, method: "tool.attach", params: { tools: [registration] }, }), ).toMatchObject({ method: "tool.attach", params: { tools: [registration] } }) expect( Backend.decodeRequest({ jsonrpc: "2.0", id: 2, method: "tool.update", params: { id: "tool_1", sequence: 0, update: { phase: "searching" }, }, }), ).toMatchObject({ method: "tool.update" }) expect( Backend.decodeRequest({ jsonrpc: "2.0", id: 3, method: "tool.finish", params: { id: "tool_1", output: { structured: { answer: 42 }, content: [{ type: "text", text: "42" }] }, }, }), ).toMatchObject({ method: "tool.finish" }) expect( Backend.decodeRequest({ jsonrpc: "2.0", id: 4, method: "tool.fail", params: { id: "tool_2", message: "lookup failed" }, }), ).toMatchObject({ method: "tool.fail" }) expect(() => Backend.decodeRequest({ jsonrpc: "2.0", id: 5, method: "tool.attach", params: { tools: [registration, registration] }, }), ).toThrow() for (const invalid of [ { ...registration, name: "1lookup" }, { ...registration, options: { namespace: "bad group", codemode: false } }, { ...registration, name: "execute", options: { codemode: false } }, { ...registration, options: { namespace: "a".repeat(64), codemode: false } }, ]) expect(() => Backend.decodeRequest({ jsonrpc: "2.0", id: 6, method: "tool.attach", params: { tools: [invalid] }, }), ).toThrow() expect(() => Backend.decodeRequest({ jsonrpc: "2.0", id: 7, method: "tool.attach", params: { tools: [ { ...registration, name: "b_c", options: { namespace: "a", codemode: false } }, { ...registration, name: "c", options: { namespace: "a.b", codemode: false } }, ], }, }), ).toThrow()})
const params: Handshake.Params = { client: { name: "opencode-drive", version: "test" }, expectedRole: "ui", offeredVersions: [1], requiredCapabilities: ["ui.state"], optionalCapabilities: ["ui.capture", "future.capability"],}
const ui: Handshake.DispatchAction = { role: "ui", server: { name: "opencode", version: "test" }, capabilities: Frontend.Capabilities,}
describe("simulation.handshake", () => { test("decodes through both endpoint request protocols", () => { const request = { jsonrpc: "2.0" as const, id: 1, method: "simulation.handshake" as const, params, } expect(Frontend.decodeRequest(request)).toEqual(request) expect(Backend.decodeRequest({ ...request, params: { ...params, expectedRole: "backend" } })).toMatchObject({ method: "simulation.handshake", params: { expectedRole: "backend" }, }) })
test("rejects invalid version and capability declarations", () => { const request = { jsonrpc: "2.0" as const, id: 1, method: "simulation.handshake" as const, params, } expect(() => Frontend.decodeRequest({ ...request, params: { ...params, offeredVersions: [] }, }), ).toThrow() expect(() => Frontend.decodeRequest({ ...request, params: { ...params, requiredCapabilities: ["ui.state", "ui.state"] }, }), ).toThrow() expect(() => Frontend.decodeRequest({ ...request, params: { ...params, optionalCapabilities: [""] }, }), ).toThrow() })
test("selects the protocol and advertises only installed capabilities", async () => { await expect(Effect.runPromise(Handshake.dispatch(ui, params))).resolves.toEqual({ protocolVersion: 1, role: "ui", server: { name: "opencode", version: "test" }, capabilities: [...Frontend.Capabilities], }) })
test("rejects a role mismatch", async () => { await expect( Effect.runPromise(Handshake.dispatch(ui, { ...params, expectedRole: "backend" })), ).rejects.toMatchObject({ _tag: "SimulationHandshake.RoleMismatchError", expected: "backend", actual: "ui" }) })
test("rejects unsupported protocol versions", async () => { await expect(Effect.runPromise(Handshake.dispatch(ui, { ...params, offeredVersions: [2] }))).rejects.toMatchObject({ _tag: "SimulationHandshake.UnsupportedProtocolError", offered: [2], supported: [1], }) })
test("rejects a missing required capability but ignores missing optional capabilities", async () => { await expect( Effect.runPromise( Handshake.dispatch(ui, { ...params, requiredCapabilities: ["ui.state", "ui.future"], }), ), ).rejects.toMatchObject({ _tag: "SimulationHandshake.MissingCapabilityError", missing: ["ui.future"] })
await expect( Effect.runPromise( Handshake.dispatch(ui, { ...params, requiredCapabilities: [], optionalCapabilities: ["ui.future"], }), ), ).resolves.toMatchObject({ capabilities: Frontend.Capabilities }) })})