Files
openclaw/src/agents/pty-keys.test.ts
Liu Yuan 419824729a fix: fail loud when PTY cursor mode is unknown (#51490) (thanks @liuy)
* fix(process): auto-detect PTY cursor key mode for send-keys

When a PTY session sends smkx (\x1b[?1h) or rmkx (\x1b[?1l) to switch
cursor key mode, send-keys now detects this and encodes cursor keys
accordingly.

- smkx/rmkx detection in handleStdout before sanitizeBinaryOutput
- cursorKeyMode stored in ProcessSession
- encodeKeySequence accepts cursorKeyMode parameter
- DECCKM_SS3_KEYS for application mode (arrows + home/end)
- CSI sequences for normal mode
- Modified keys (including alt) always use xterm modifier scheme
- Extract detectCursorKeyMode for unit testing
- Use lastIndexOf to find last toggle in chunk (later one wins)

Fixes #51488

* fix: fail loud when PTY cursor mode is unknown (#51490) (thanks @liuy)

* style: format process send-keys guard (#51490) (thanks @liuy)

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-03-25 15:51:27 +05:30

134 lines
4.6 KiB
TypeScript

import { expect, test } from "vitest";
import { buildCursorPositionResponse, stripDsrRequests } from "./pty-dsr.js";
import {
BRACKETED_PASTE_END,
BRACKETED_PASTE_START,
encodeKeySequence,
encodePaste,
} from "./pty-keys.js";
const ESC = "\x1b";
test("encodeKeySequence maps common keys and modifiers", () => {
const enter = encodeKeySequence({ keys: ["Enter"] });
expect(enter.data).toBe("\r");
const ctrlC = encodeKeySequence({ keys: ["C-c"] });
expect(ctrlC.data).toBe("\x03");
const altX = encodeKeySequence({ keys: ["M-x"] });
expect(altX.data).toBe("\x1bx");
const shiftTab = encodeKeySequence({ keys: ["S-Tab"] });
expect(shiftTab.data).toBe("\x1b[Z");
const kpEnter = encodeKeySequence({ keys: ["KPEnter"] });
expect(kpEnter.data).toBe("\x1bOM");
});
test("encodeKeySequence uses CSI sequences in normal cursor key mode (default)", () => {
// Default mode (cursorKeyMode not specified) uses CSI sequences.
const up = encodeKeySequence({ keys: ["up"] });
expect(up.data).toBe(`${ESC}[A`);
const down = encodeKeySequence({ keys: ["down"] });
expect(down.data).toBe(`${ESC}[B`);
const right = encodeKeySequence({ keys: ["right"] });
expect(right.data).toBe(`${ESC}[C`);
const left = encodeKeySequence({ keys: ["left"] });
expect(left.data).toBe(`${ESC}[D`);
// Home/End use CSI sequences in normal mode.
const home = encodeKeySequence({ keys: ["home"] });
expect(home.data).toBe(`${ESC}[1~`);
const end = encodeKeySequence({ keys: ["end"] });
expect(end.data).toBe(`${ESC}[4~`);
});
test("encodeKeySequence uses CSI sequences in explicit normal cursor key mode", () => {
const up = encodeKeySequence({ keys: ["up"] }, "normal");
expect(up.data).toBe(`${ESC}[A`);
const down = encodeKeySequence({ keys: ["down"] }, "normal");
expect(down.data).toBe(`${ESC}[B`);
const right = encodeKeySequence({ keys: ["right"] }, "normal");
expect(right.data).toBe(`${ESC}[C`);
const left = encodeKeySequence({ keys: ["left"] }, "normal");
expect(left.data).toBe(`${ESC}[D`);
// Home/End use CSI sequences in explicit normal mode.
const home = encodeKeySequence({ keys: ["home"] }, "normal");
expect(home.data).toBe(`${ESC}[1~`);
const end = encodeKeySequence({ keys: ["end"] }, "normal");
expect(end.data).toBe(`${ESC}[4~`);
});
test("encodeKeySequence uses SS3 sequences in application cursor key mode", () => {
// Application mode (smkx) uses SS3 sequences.
const up = encodeKeySequence({ keys: ["up"] }, "application");
expect(up.data).toBe(`${ESC}OA`);
const down = encodeKeySequence({ keys: ["down"] }, "application");
expect(down.data).toBe(`${ESC}OB`);
const right = encodeKeySequence({ keys: ["right"] }, "application");
expect(right.data).toBe(`${ESC}OC`);
const left = encodeKeySequence({ keys: ["left"] }, "application");
expect(left.data).toBe(`${ESC}OD`);
// Home/End also use SS3 sequences in application mode.
const home = encodeKeySequence({ keys: ["home"] }, "application");
expect(home.data).toBe(`${ESC}OH`);
const end = encodeKeySequence({ keys: ["end"] }, "application");
expect(end.data).toBe(`${ESC}OF`);
});
test("encodeKeySequence applies xterm modifiers to arrows in application mode", () => {
// Modified arrow keys use xterm modifier scheme even in application mode.
// DECCKM only affects unmodified cursor keys.
const altUp = encodeKeySequence({ keys: ["M-up"] }, "application");
expect(altUp.data).toBe(`${ESC}[1;3A`);
const ctrlRight = encodeKeySequence({ keys: ["C-right"] }, "application");
expect(ctrlRight.data).toBe(`${ESC}[1;5C`);
const shiftDown = encodeKeySequence({ keys: ["S-down"] }, "application");
expect(shiftDown.data).toBe(`${ESC}[1;2B`);
});
test("encodeKeySequence supports hex + literal with warnings", () => {
const result = encodeKeySequence({
literal: "hi",
hex: ["0d", "0x0a", "zz"],
keys: ["Enter"],
});
expect(result.data).toBe("hi\r\n\r");
expect(result.warnings.length).toBe(1);
});
test("encodePaste wraps bracketed sequences by default", () => {
const payload = encodePaste("line1\nline2\n");
expect(payload.startsWith(BRACKETED_PASTE_START)).toBe(true);
expect(payload.endsWith(BRACKETED_PASTE_END)).toBe(true);
});
test("stripDsrRequests removes cursor queries and counts them", () => {
const input = "hi\x1b[6nthere\x1b[?6n";
const { cleaned, requests } = stripDsrRequests(input);
expect(cleaned).toBe("hithere");
expect(requests).toBe(2);
});
test("buildCursorPositionResponse returns CPR sequence", () => {
expect(buildCursorPositionResponse()).toBe("\x1b[1;1R");
expect(buildCursorPositionResponse(12, 34)).toBe("\x1b[12;34R");
});