feat(simulation): share control protocol schemas (#35230)

This commit is contained in:
James Long 2026-07-03 15:46:04 -04:00 committed by GitHub
commit 37b26e495b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 170 additions and 148 deletions

View file

@ -1,36 +1,11 @@
import type { CliRenderer, Renderable } from "@opentui/core"
import { createMockKeys, createMockMouse, type MockInput, type MockMouse } from "@opentui/core/testing"
import type { SimulationProtocol } from "../protocol"
import { SimulationRenderer } from "./renderer"
import { SimulationTrace } from "./trace"
export interface KeyModifiers {
readonly ctrl?: boolean
readonly shift?: boolean
readonly meta?: boolean
readonly super?: boolean
readonly hyper?: boolean
}
export type Action =
| { readonly type: "typeText"; readonly text: string }
| { readonly type: "pressKey"; readonly key: string; readonly modifiers?: KeyModifiers }
| { readonly type: "pressEnter" }
| { readonly type: "pressArrow"; readonly direction: "up" | "down" | "left" | "right" }
| { readonly type: "focus"; readonly target: number }
| { readonly type: "click"; readonly target: number; readonly x: number; readonly y: number }
export interface Element {
readonly id: string
readonly num: number
readonly x: number
readonly y: number
readonly width: number
readonly height: number
readonly focusable: boolean
readonly focused: boolean
readonly clickable: boolean
readonly editor: boolean
}
export type Action = SimulationProtocol.Frontend.Action
export type Element = SimulationProtocol.Frontend.Element
export interface Harness {
readonly renderer: CliRenderer

View file

@ -1,27 +1,10 @@
import { SimulationActions, type Action, type Harness } from "./actions"
import { SimulationProtocol } from "../protocol"
import { SimulationActions, type Harness } from "./actions"
import { SimulationTrace } from "./trace"
const DefaultPort = 40900
const MaxPortAttempts = 100
type JsonRpcRequest = {
readonly jsonrpc: "2.0"
readonly id?: string | number | null
readonly method: string
readonly params?: unknown
}
type JsonRpcResponse = {
readonly jsonrpc: "2.0"
readonly id: string | number | null
readonly result?: unknown
readonly error?: {
readonly code: number
readonly message: string
readonly data?: unknown
}
}
export interface Server {
readonly url: string
readonly stop: () => void
@ -36,63 +19,15 @@ function isPortUnavailable(error: unknown) {
return message.includes("eaddrinuse") || message.includes("address already in use") || message.includes(" in use")
}
function parseRequest(input: string | Buffer): JsonRpcRequest {
const value = JSON.parse(typeof input === "string" ? input : input.toString()) as unknown
if (typeof value !== "object" || value === null) throw new Error("Invalid JSON-RPC request")
if (!("jsonrpc" in value) || value.jsonrpc !== "2.0") throw new Error("Invalid JSON-RPC version")
if (!("method" in value) || typeof value.method !== "string") throw new Error("Invalid JSON-RPC method")
return value as JsonRpcRequest
}
function isAction(input: unknown): input is Action {
if (typeof input !== "object" || input === null || !("type" in input)) return false
switch (input.type) {
case "typeText":
return "text" in input && typeof input.text === "string"
case "pressKey":
return "key" in input && typeof input.key === "string"
case "pressEnter":
return true
case "pressArrow":
return "direction" in input && ["up", "down", "left", "right"].includes(String(input.direction))
case "focus":
return "target" in input && typeof input.target === "number"
case "click":
return (
"target" in input &&
typeof input.target === "number" &&
"x" in input &&
typeof input.x === "number" &&
"y" in input &&
typeof input.y === "number"
)
}
return false
}
function actionParam(params: unknown) {
if (typeof params !== "object" || params === null || !("action" in params)) throw new Error("Missing action")
if (!isAction(params.action)) throw new Error("Invalid action")
return params.action
return SimulationProtocol.Frontend.decodeActionParams(params).action
}
function response(id: JsonRpcRequest["id"], result: unknown): JsonRpcResponse | undefined {
if (id === undefined) return undefined
return { jsonrpc: "2.0", id, result }
function parseRequest(input: string | Buffer) {
return SimulationProtocol.JsonRpc.decodeRequest(JSON.parse(typeof input === "string" ? input : input.toString()))
}
function errorResponse(id: JsonRpcRequest["id"], error: unknown): JsonRpcResponse {
return {
jsonrpc: "2.0",
id: id ?? null,
error: {
code: -32000,
message: error instanceof Error ? error.message : String(error),
},
}
}
async function handle(harness: Harness, request: JsonRpcRequest) {
async function handle(harness: Harness, request: SimulationProtocol.JsonRpc.Request) {
switch (request.method) {
case "ui.state": {
const result = SimulationActions.state(harness)
@ -139,14 +74,14 @@ function serve(
SimulationTrace.add("control.disconnect")
},
async message(socket, message) {
let request: JsonRpcRequest | undefined
let request: SimulationProtocol.JsonRpc.Request | undefined
try {
request = parseRequest(message)
const result = await handle(harness, request)
const next = response(request.id, result)
const next = SimulationProtocol.JsonRpc.success(request.id, result)
if (next) socket.send(JSON.stringify(next))
} catch (error) {
socket.send(JSON.stringify(errorResponse(request?.id, error)))
socket.send(JSON.stringify(SimulationProtocol.JsonRpc.failure(request?.id, error)))
}
},
},