feat(simulation): add endpoint handshake protocol (#37157)
This commit is contained in:
@@ -17,6 +17,7 @@ Implementation checklist:
|
|||||||
- [x] Add OpenTUI UI state extraction for screen, focus, elements, and generated actions.
|
- [x] Add OpenTUI UI state extraction for screen, focus, elements, and generated actions.
|
||||||
- [x] Add OpenTUI UI action execution for typing, keys, enter, arrows, focus, and click.
|
- [x] Add OpenTUI UI action execution for typing, keys, enter, arrows, focus, and click.
|
||||||
- [x] Add reusable JSON-RPC WebSocket server at the manifest's UI endpoint.
|
- [x] Add reusable JSON-RPC WebSocket server at the manifest's UI endpoint.
|
||||||
|
- [x] Add `simulation.handshake` protocol, role, identity, version, and capability negotiation to both control endpoints.
|
||||||
- [x] Expose `ui.state`, `ui.action`, `ui.render`.
|
- [x] Expose `ui.state`, `ui.action`, `ui.render`.
|
||||||
- [x] Expose `trace.list`, `trace.clear`, `trace.export`.
|
- [x] Expose `trace.list`, `trace.clear`, `trace.export`.
|
||||||
- [x] Wire visible V1/full-TUI renderer path through the same action protocol.
|
- [x] Wire visible V1/full-TUI renderer path through the same action protocol.
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ This is important because the frontend has direct access to the renderer, screen
|
|||||||
Protocol:
|
Protocol:
|
||||||
|
|
||||||
- JSON-RPC 2.0 over WebSocket.
|
- JSON-RPC 2.0 over WebSocket.
|
||||||
|
- Clients negotiate protocol version, endpoint role, and capabilities with `simulation.handshake` before using endpoint methods.
|
||||||
- Loopback only.
|
- Loopback only.
|
||||||
- `OPENCODE_DRIVE` names a manifest in the opencode-drive registry, or is `1` for the unnamed default endpoints.
|
- `OPENCODE_DRIVE` names a manifest in the opencode-drive registry, or is `1` for the unnamed default endpoints.
|
||||||
- The manifest supplies exact loopback `ui` and `backend` WebSocket endpoints.
|
- The manifest supplies exact loopback `ui` and `backend` WebSocket endpoints.
|
||||||
@@ -77,6 +78,42 @@ Protocol:
|
|||||||
|
|
||||||
The app should not send JSON-RPC requests back to the driver in the first milestone. The driver sends requests; the app responds and emits notifications/events as useful.
|
The app should not send JSON-RPC requests back to the driver in the first milestone. The driver sends requests; the app responds and emits notifications/events as useful.
|
||||||
|
|
||||||
|
The canonical handshake request is:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
jsonrpc: "2.0"
|
||||||
|
id: string | number | null
|
||||||
|
method: "simulation.handshake"
|
||||||
|
params: {
|
||||||
|
client: {
|
||||||
|
name: string
|
||||||
|
version: string
|
||||||
|
}
|
||||||
|
expectedRole: "ui" | "backend"
|
||||||
|
offeredVersions: Array<number>
|
||||||
|
requiredCapabilities: Array<string>
|
||||||
|
optionalCapabilities: Array<string>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The response result is:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
protocolVersion: 1
|
||||||
|
role: "ui" | "backend"
|
||||||
|
server: {
|
||||||
|
name: string
|
||||||
|
version: string
|
||||||
|
}
|
||||||
|
capabilities: Array<string>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Capabilities are open strings. Each endpoint advertises only methods and notifications it actually implements. A role mismatch, no supported offered protocol version, or a missing required capability fails the request; unsupported optional capabilities do not.
|
||||||
|
|
||||||
Initial method groups:
|
Initial method groups:
|
||||||
|
|
||||||
- `ui.state`: return screen, elements, focus, and generated possible actions.
|
- `ui.state`: return screen, elements, focus, and generated possible actions.
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||||
import { Cause, Context, Effect, Fiber, FiberSet, Layer, PubSub, Queue, Ref, Schema, Semaphore, Stream } from "effect"
|
import { Cause, Context, Effect, Fiber, FiberSet, Layer, PubSub, Queue, Ref, Schema, Semaphore, Stream } from "effect"
|
||||||
import { SimulationControlServer } from "../control-server"
|
import { SimulationControlServer } from "../control-server"
|
||||||
import { SimulationProtocol } from "../protocol"
|
import { SimulationProtocol } from "../protocol"
|
||||||
@@ -211,6 +212,15 @@ function handle(
|
|||||||
request: SimulationProtocol.Backend.Request,
|
request: SimulationProtocol.Backend.Request,
|
||||||
) {
|
) {
|
||||||
switch (request.method) {
|
switch (request.method) {
|
||||||
|
case "simulation.handshake":
|
||||||
|
return SimulationProtocol.Handshake.dispatch(
|
||||||
|
{
|
||||||
|
role: "backend",
|
||||||
|
server: { name: "opencode", version: InstallationVersion },
|
||||||
|
capabilities: SimulationProtocol.Backend.Capabilities,
|
||||||
|
},
|
||||||
|
request.params,
|
||||||
|
)
|
||||||
case "llm.attach":
|
case "llm.attach":
|
||||||
return controllerLock.withPermit(
|
return controllerLock.withPermit(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { SimulationControlServer } from "../control-server"
|
import { SimulationControlServer } from "../control-server"
|
||||||
import { SimulationProtocol } from "../protocol"
|
import { SimulationProtocol } from "../protocol"
|
||||||
@@ -6,6 +7,15 @@ import { SimulationRenderer } from "./renderer"
|
|||||||
|
|
||||||
function handle(harness: Harness, request: SimulationProtocol.Frontend.Request) {
|
function handle(harness: Harness, request: SimulationProtocol.Frontend.Request) {
|
||||||
switch (request.method) {
|
switch (request.method) {
|
||||||
|
case "simulation.handshake":
|
||||||
|
return SimulationProtocol.Handshake.dispatch(
|
||||||
|
{
|
||||||
|
role: "ui",
|
||||||
|
server: { name: "opencode", version: InstallationVersion },
|
||||||
|
capabilities: SimulationProtocol.Frontend.Capabilities,
|
||||||
|
},
|
||||||
|
request.params,
|
||||||
|
)
|
||||||
case "ui.capture":
|
case "ui.capture":
|
||||||
return SimulationActions.capture(harness)
|
return SimulationActions.capture(harness)
|
||||||
case "ui.screenshot":
|
case "ui.screenshot":
|
||||||
|
|||||||
@@ -48,7 +48,136 @@ export namespace JsonRpc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export namespace Handshake {
|
||||||
|
export const ProtocolVersion = Schema.Literal(1)
|
||||||
|
export type ProtocolVersion = Schema.Schema.Type<typeof ProtocolVersion>
|
||||||
|
|
||||||
|
export const Capability = Schema.NonEmptyString
|
||||||
|
export type Capability = Schema.Schema.Type<typeof Capability>
|
||||||
|
|
||||||
|
export const EndpointRole = Schema.Literals(["ui", "backend"])
|
||||||
|
export type EndpointRole = Schema.Schema.Type<typeof EndpointRole>
|
||||||
|
|
||||||
|
export const Identity = Schema.Struct({
|
||||||
|
name: Schema.NonEmptyString,
|
||||||
|
version: Schema.NonEmptyString,
|
||||||
|
})
|
||||||
|
export interface Identity extends Schema.Schema.Type<typeof Identity> {}
|
||||||
|
|
||||||
|
export const Params = Schema.Struct({
|
||||||
|
client: Identity,
|
||||||
|
expectedRole: EndpointRole,
|
||||||
|
offeredVersions: Schema.Array(
|
||||||
|
Schema.Int.check(Schema.isGreaterThan(0)),
|
||||||
|
).check(Schema.isMinLength(1), Schema.isUnique()),
|
||||||
|
requiredCapabilities: Schema.Array(Capability).check(Schema.isUnique()),
|
||||||
|
optionalCapabilities: Schema.Array(Capability).check(Schema.isUnique()),
|
||||||
|
})
|
||||||
|
export interface Params extends Schema.Schema.Type<typeof Params> {}
|
||||||
|
|
||||||
|
export const Response = Schema.Struct({
|
||||||
|
protocolVersion: ProtocolVersion,
|
||||||
|
role: EndpointRole,
|
||||||
|
server: Identity,
|
||||||
|
capabilities: Schema.Array(Capability),
|
||||||
|
})
|
||||||
|
export interface Response extends Schema.Schema.Type<typeof Response> {}
|
||||||
|
|
||||||
|
export const Request = Schema.Struct({
|
||||||
|
...JsonRpc.RequestFields,
|
||||||
|
method: Schema.Literal("simulation.handshake"),
|
||||||
|
params: Params,
|
||||||
|
})
|
||||||
|
export interface Request extends Schema.Schema.Type<typeof Request> {}
|
||||||
|
|
||||||
|
export interface DispatchAction {
|
||||||
|
readonly role: EndpointRole
|
||||||
|
readonly server: Identity
|
||||||
|
readonly capabilities: ReadonlyArray<Capability>
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RoleMismatchError extends Schema.TaggedErrorClass<RoleMismatchError>()(
|
||||||
|
"SimulationHandshake.RoleMismatchError",
|
||||||
|
{
|
||||||
|
expected: EndpointRole,
|
||||||
|
actual: EndpointRole,
|
||||||
|
message: Schema.String,
|
||||||
|
},
|
||||||
|
) {}
|
||||||
|
|
||||||
|
export class UnsupportedProtocolError extends Schema.TaggedErrorClass<UnsupportedProtocolError>()(
|
||||||
|
"SimulationHandshake.UnsupportedProtocolError",
|
||||||
|
{
|
||||||
|
offered: Schema.Array(Schema.Number),
|
||||||
|
supported: Schema.Array(ProtocolVersion),
|
||||||
|
message: Schema.String,
|
||||||
|
},
|
||||||
|
) {}
|
||||||
|
|
||||||
|
export class MissingCapabilityError extends Schema.TaggedErrorClass<MissingCapabilityError>()(
|
||||||
|
"SimulationHandshake.MissingCapabilityError",
|
||||||
|
{
|
||||||
|
missing: Schema.Array(Capability),
|
||||||
|
message: Schema.String,
|
||||||
|
},
|
||||||
|
) {}
|
||||||
|
|
||||||
|
export function dispatch(action: DispatchAction, params: Params) {
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
if (params.expectedRole !== action.role) {
|
||||||
|
return yield* Effect.fail(
|
||||||
|
new RoleMismatchError({
|
||||||
|
expected: params.expectedRole,
|
||||||
|
actual: action.role,
|
||||||
|
message: `Expected simulation endpoint role ${params.expectedRole}, received ${action.role}`,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (!params.offeredVersions.includes(1)) {
|
||||||
|
return yield* Effect.fail(
|
||||||
|
new UnsupportedProtocolError({
|
||||||
|
offered: params.offeredVersions,
|
||||||
|
supported: [1],
|
||||||
|
message: "No mutually supported simulation protocol version",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const installed = new Set(action.capabilities)
|
||||||
|
const missing = params.requiredCapabilities.filter((capability) => !installed.has(capability))
|
||||||
|
if (missing.length > 0) {
|
||||||
|
return yield* Effect.fail(
|
||||||
|
new MissingCapabilityError({
|
||||||
|
missing,
|
||||||
|
message: `Simulation endpoint is missing required capabilities: ${missing.join(", ")}`,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
protocolVersion: 1,
|
||||||
|
role: action.role,
|
||||||
|
server: action.server,
|
||||||
|
capabilities: Array.from(installed),
|
||||||
|
} satisfies Response
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export namespace Frontend {
|
export namespace Frontend {
|
||||||
|
export const Capabilities = [
|
||||||
|
"ui.type",
|
||||||
|
"ui.press",
|
||||||
|
"ui.enter",
|
||||||
|
"ui.arrow",
|
||||||
|
"ui.focus",
|
||||||
|
"ui.click",
|
||||||
|
"ui.resize",
|
||||||
|
"ui.matches",
|
||||||
|
"ui.screenshot",
|
||||||
|
"ui.state",
|
||||||
|
"ui.capture",
|
||||||
|
"ui.recording.finish",
|
||||||
|
] as const satisfies ReadonlyArray<Handshake.Capability>
|
||||||
|
|
||||||
export const KeyModifiers = Schema.Struct({
|
export const KeyModifiers = Schema.Struct({
|
||||||
ctrl: Schema.optional(Schema.Boolean),
|
ctrl: Schema.optional(Schema.Boolean),
|
||||||
shift: Schema.optional(Schema.Boolean),
|
shift: Schema.optional(Schema.Boolean),
|
||||||
@@ -149,6 +278,7 @@ export namespace Frontend {
|
|||||||
export interface ResizeParams extends Schema.Schema.Type<typeof ResizeParams> {}
|
export interface ResizeParams extends Schema.Schema.Type<typeof ResizeParams> {}
|
||||||
|
|
||||||
export const Request = Schema.Union([
|
export const Request = Schema.Union([
|
||||||
|
Handshake.Request,
|
||||||
Schema.Struct({ ...JsonRpc.RequestFields, method: Schema.Literal("ui.type"), params: TypeParams }),
|
Schema.Struct({ ...JsonRpc.RequestFields, method: Schema.Literal("ui.type"), params: TypeParams }),
|
||||||
Schema.Struct({ ...JsonRpc.RequestFields, method: Schema.Literal("ui.press"), params: PressParams }),
|
Schema.Struct({ ...JsonRpc.RequestFields, method: Schema.Literal("ui.press"), params: PressParams }),
|
||||||
Schema.Struct({ ...JsonRpc.RequestFields, method: Schema.Literal("ui.arrow"), params: ArrowParams }),
|
Schema.Struct({ ...JsonRpc.RequestFields, method: Schema.Literal("ui.arrow"), params: ArrowParams }),
|
||||||
@@ -173,6 +303,15 @@ export namespace Frontend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export namespace Backend {
|
export namespace Backend {
|
||||||
|
export const Capabilities = [
|
||||||
|
"llm.attach",
|
||||||
|
"llm.chunk",
|
||||||
|
"llm.finish",
|
||||||
|
"llm.disconnect",
|
||||||
|
"llm.pending",
|
||||||
|
"llm.request",
|
||||||
|
] as const satisfies ReadonlyArray<Handshake.Capability>
|
||||||
|
|
||||||
export const Item = Schema.Union([
|
export const Item = Schema.Union([
|
||||||
Schema.Struct({ type: Schema.Literal("textDelta"), text: Schema.String }),
|
Schema.Struct({ type: Schema.Literal("textDelta"), text: Schema.String }),
|
||||||
Schema.Struct({ type: Schema.Literal("reasoningDelta"), text: Schema.String }),
|
Schema.Struct({ type: Schema.Literal("reasoningDelta"), text: Schema.String }),
|
||||||
@@ -203,6 +342,7 @@ export namespace Backend {
|
|||||||
export interface DisconnectParams extends Schema.Schema.Type<typeof DisconnectParams> {}
|
export interface DisconnectParams extends Schema.Schema.Type<typeof DisconnectParams> {}
|
||||||
|
|
||||||
export const Request = Schema.Union([
|
export const Request = Schema.Union([
|
||||||
|
Handshake.Request,
|
||||||
Schema.Struct({ ...JsonRpc.RequestFields, method: Schema.Literal("llm.chunk"), params: ChunkParams }),
|
Schema.Struct({ ...JsonRpc.RequestFields, method: Schema.Literal("llm.chunk"), params: ChunkParams }),
|
||||||
Schema.Struct({ ...JsonRpc.RequestFields, method: Schema.Literal("llm.finish"), params: FinishParams }),
|
Schema.Struct({ ...JsonRpc.RequestFields, method: Schema.Literal("llm.finish"), params: FinishParams }),
|
||||||
Schema.Struct({ ...JsonRpc.RequestFields, method: Schema.Literal("llm.disconnect"), params: DisconnectParams }),
|
Schema.Struct({ ...JsonRpc.RequestFields, method: Schema.Literal("llm.disconnect"), params: DisconnectParams }),
|
||||||
|
|||||||
@@ -19,6 +19,30 @@ test("scopes the frontend control server and reports malformed JSON", async () =
|
|||||||
Queue.offerUnsafe(messages, JSON.parse(String(event.data)))
|
Queue.offerUnsafe(messages, JSON.parse(String(event.data)))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
socket.send(
|
||||||
|
JSON.stringify({
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
id: 0,
|
||||||
|
method: "simulation.handshake",
|
||||||
|
params: {
|
||||||
|
client: { name: "test", version: "test" },
|
||||||
|
expectedRole: "ui",
|
||||||
|
offeredVersions: [1],
|
||||||
|
requiredCapabilities: ["ui.state"],
|
||||||
|
optionalCapabilities: [],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
expect(yield* Queue.take(messages)).toMatchObject({
|
||||||
|
id: 0,
|
||||||
|
result: {
|
||||||
|
protocolVersion: 1,
|
||||||
|
role: "ui",
|
||||||
|
server: { name: "opencode", version: expect.any(String) },
|
||||||
|
capabilities: expect.arrayContaining(["ui.state", "ui.capture"]),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
socket.send(JSON.stringify({ jsonrpc: "2.0", id: 1, method: "ui.state" }))
|
socket.send(JSON.stringify({ jsonrpc: "2.0", id: 1, method: "ui.state" }))
|
||||||
expect(yield* Queue.take(messages)).toMatchObject({
|
expect(yield* Queue.take(messages)).toMatchObject({
|
||||||
id: 1,
|
id: 1,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import { Frontend } from "../src/protocol"
|
import { Effect } from "effect"
|
||||||
|
import { Backend, Frontend, Handshake } from "../src/protocol"
|
||||||
|
|
||||||
test("decodes ui.matches text params", () => {
|
test("decodes ui.matches text params", () => {
|
||||||
expect(
|
expect(
|
||||||
@@ -19,3 +20,104 @@ test("decodes ui.matches text params", () => {
|
|||||||
}),
|
}),
|
||||||
).toThrow()
|
).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 })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -7,6 +7,30 @@ import { availableEndpoint, connect } from "./fixture/websocket"
|
|||||||
test("streams a Drive-controlled provider response and removes the finished invocation", async () => {
|
test("streams a Drive-controlled provider response and removes the finished invocation", async () => {
|
||||||
await runProvider((provider, socket, messages) =>
|
await runProvider((provider, socket, messages) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
socket.send(
|
||||||
|
JSON.stringify({
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
id: 0,
|
||||||
|
method: "simulation.handshake",
|
||||||
|
params: {
|
||||||
|
client: { name: "test", version: "test" },
|
||||||
|
expectedRole: "backend",
|
||||||
|
offeredVersions: [1],
|
||||||
|
requiredCapabilities: ["llm.attach", "llm.request"],
|
||||||
|
optionalCapabilities: [],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
expect(yield* Queue.take(messages)).toMatchObject({
|
||||||
|
id: 0,
|
||||||
|
result: {
|
||||||
|
protocolVersion: 1,
|
||||||
|
role: "backend",
|
||||||
|
server: { name: "opencode", version: expect.any(String) },
|
||||||
|
capabilities: expect.arrayContaining(["llm.attach", "llm.request"]),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
socket.send("{")
|
socket.send("{")
|
||||||
expect(yield* Queue.take(messages)).toMatchObject({ id: null, error: { code: -32000 } })
|
expect(yield* Queue.take(messages)).toMatchObject({ id: null, error: { code: -32000 } })
|
||||||
yield* attach(socket, messages)
|
yield* attach(socket, messages)
|
||||||
|
|||||||
Reference in New Issue
Block a user