opencode/packages/client/test/solid-pty-protocol.test.ts

113 lines
4.6 KiB
TypeScript

import { describe, expect, test } from "bun:test"
import { Option } from "effect"
import { decodePersistentPtyMessage, persistentPtyFrame } from "../src/solid"
const info = {
id: "pty_1",
title: "zsh",
command: "/bin/zsh",
args: [],
cwd: "/repo",
status: "running",
pid: 42,
sessionID: "ses_1",
foregroundProcess: null,
size: { cols: 80, rows: 24 },
output: { head: 0, tail: 128 },
}
const decode = (value: object) => Option.getOrUndefined(decodePersistentPtyMessage(JSON.stringify(value)))
describe("decodePersistentPtyMessage", () => {
test("decodes every message the server sends, keeping only the fields clients read", () => {
const replay = { requestedOffset: 128, availableOffset: 0, endOffset: 256, truncated: false }
expect(
[
{ type: "attached", inputProtocol: 1, info, role: "controller", generation: 3, replay },
{ type: "replay_complete", endOffset: 256 },
{ type: "resized", cols: 100, rows: 30, generation: 4, checkpoint: "G2M=" },
{ type: "controller_changed", attachmentID: "attachment-1", generation: 5 },
{ type: "exited", exitCode: 0, finalOffset: 512 },
{ type: "title_changed", title: "vim" },
{ type: "foreground_process_changed", process: "vim" },
{ type: "foreground_process_changed", process: null },
].map(decode),
).toEqual([
{ type: "attached", inputProtocol: 1, info: { size: info.size }, role: "controller" },
{ type: "replay_complete" },
{ type: "resized", cols: 100, rows: 30, checkpoint: "G2M=" },
{ type: "controller_changed", attachmentID: "attachment-1" },
{ type: "exited", exitCode: 0 },
{ type: "title_changed", title: "vim" },
{ type: "foreground_process_changed", process: "vim" },
{ type: "foreground_process_changed", process: null },
])
})
test("reads only the size from attached terminal info, whatever else the server version sends", () => {
const attached = (terminal: object) => ({ type: "attached", inputProtocol: 1, info: terminal, role: "controller" })
expect(decode(attached(info))).toEqual(attached({ size: info.size }))
expect(decode(attached({ ...info, userTitle: "build", sessionID: "dummy" }))).toEqual(attached({ size: info.size }))
expect(decode(attached({ id: "pty_1" }))).toBeUndefined()
})
test("decodes minimal messages and the fields JSON.stringify drops", () => {
expect(decode({ type: "attached", info: { size: info.size } })).toEqual({
type: "attached",
info: { size: info.size },
})
expect(decode({ type: "replay_complete" })).toEqual({ type: "replay_complete" })
expect(decode({ type: "controller_changed", generation: 5 })).toEqual({ type: "controller_changed" })
expect(decode({ type: "controller_changed", attachmentID: null })).toEqual({
type: "controller_changed",
attachmentID: null,
})
expect(decode({ type: "exited", finalOffset: 512 })).toEqual({ type: "exited" })
expect(decode({ type: "exited", exitCode: null })).toEqual({ type: "exited", exitCode: null })
})
test("ignores fields it does not know", () => {
expect(decode({ type: "title_changed", title: "vim", extra: true })).toEqual({
type: "title_changed",
title: "vim",
})
})
test("returns None for unknown message types", () => {
expect(Option.isNone(decodePersistentPtyMessage(JSON.stringify({ type: "future_message", value: 1 })))).toBe(true)
})
test("returns None for malformed input", () => {
const inputs = [
"",
"not json",
"null",
"42",
JSON.stringify({ endOffset: 1 }),
JSON.stringify({ type: "resized", cols: "100", rows: 30, generation: 4, checkpoint: "" }),
JSON.stringify({ type: "resized", cols: 100, rows: 30, generation: 4 }),
JSON.stringify({ type: "attached", inputProtocol: 1, role: "controller", generation: 1 }),
JSON.stringify({ type: "foreground_process_changed" }),
]
expect(inputs.map((input) => Option.isNone(decodePersistentPtyMessage(input)))).toEqual(inputs.map(() => true))
})
})
describe("persistentPtyFrame", () => {
test("encodes a size-only interaction frame", () => {
expect(Array.from(persistentPtyFrame({ cols: 300, rows: 24 }))).toEqual([0, 1, 44, 0, 24])
})
test("encodes an input frame with its data", () => {
expect(Array.from(persistentPtyFrame({ cols: 80, rows: 513 }, new Uint8Array([104, 105])))).toEqual([
1, 0, 80, 2, 1, 104, 105,
])
})
test("encodes empty input as an input frame", () => {
expect(Array.from(persistentPtyFrame({ cols: 80, rows: 24 }, new Uint8Array()))).toEqual([1, 0, 80, 0, 24])
})
})