Files
paseo/packages/protocol/src/binary-frames/terminal.test.ts
T
Mohamed Boudra 4cc72a84e8 Fix terminal snapshots reflowing after resize
Terminal snapshots now carry soft-wrap row metadata only to clients that advertise support, so restored output can resize like live output without breaking older clients.
2026-05-31 19:40:15 +07:00

117 lines
3.2 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
TerminalStreamOpcode,
decodeTerminalResizePayload,
decodeTerminalSnapshotPayload,
decodeTerminalStreamFrame,
encodeTerminalResizePayload,
encodeTerminalSnapshotPayload,
encodeTerminalStreamFrame,
} from "./index.js";
describe("terminal binary frames", () => {
it("encodes output frames as opcode plus slot plus payload", () => {
const payload = new TextEncoder().encode("hello");
const encoded = encodeTerminalStreamFrame({
opcode: TerminalStreamOpcode.Output,
slot: 7,
payload,
});
expect(encoded[0]).toBe(TerminalStreamOpcode.Output);
expect(encoded[1]).toBe(7);
expect(Array.from(encoded.subarray(2))).toEqual(Array.from(payload));
const decoded = decodeTerminalStreamFrame(encoded);
expect(decoded).toEqual({
opcode: TerminalStreamOpcode.Output,
slot: 7,
payload,
});
});
it("round-trips resize payloads", () => {
const payload = encodeTerminalResizePayload({
rows: 24,
cols: 80,
});
expect(decodeTerminalResizePayload(payload)).toEqual({
rows: 24,
cols: 80,
});
});
it("round-trips snapshot payloads", () => {
const state = {
rows: 1,
cols: 2,
grid: [[{ char: "A" }, { char: "B" }]],
scrollback: [],
cursor: { row: 0, col: 2 },
};
const payload = encodeTerminalSnapshotPayload(state);
expect(decodeTerminalSnapshotPayload(payload)).toEqual(state);
});
it("rejects unknown opcodes", () => {
expect(decodeTerminalStreamFrame(new Uint8Array([0xff, 0x01, 0x02]))).toBeNull();
});
it("rejects frames without a slot byte", () => {
expect(decodeTerminalStreamFrame(new Uint8Array([TerminalStreamOpcode.Output]))).toBeNull();
});
it("rejects malformed JSON payloads", () => {
const malformed = new TextEncoder().encode("{");
expect(decodeTerminalResizePayload(malformed)).toBeNull();
expect(decodeTerminalSnapshotPayload(malformed)).toBeNull();
});
it("rejects invalid resize and snapshot shapes", () => {
expect(
decodeTerminalResizePayload(
new TextEncoder().encode(JSON.stringify({ rows: "24", cols: 80 })),
),
).toBeNull();
expect(
decodeTerminalSnapshotPayload(
new TextEncoder().encode(
JSON.stringify({
rows: 1,
cols: 1,
grid: [[{ char: "A" }]],
scrollback: [],
}),
),
),
).toBeNull();
});
it("ignores unknown fields in resize and snapshot payloads", () => {
// Protocol schemas are non-strict: unknown keys are stripped, not rejected, so a
// new daemon can add fields without breaking an old client's parse.
expect(
decodeTerminalResizePayload(
new TextEncoder().encode(JSON.stringify({ rows: 24, cols: 80, extra: true })),
),
).toEqual({ rows: 24, cols: 80 });
const snapshot = decodeTerminalSnapshotPayload(
new TextEncoder().encode(
JSON.stringify({
rows: 1,
cols: 1,
grid: [[{ char: "A", extra: true }]],
scrollback: [],
cursor: { row: 0, col: 1 },
}),
),
);
expect(snapshot?.grid[0]?.[0]).toEqual({ char: "A" });
});
});