Files
openclaw/src/agents/bash-tools.process.send-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.1 KiB
TypeScript

import { afterEach, expect, test } from "vitest";
import { addSession, resetProcessRegistryForTests } from "./bash-process-registry.js";
import { createProcessSessionFixture } from "./bash-process-registry.test-helpers.js";
import { createExecTool } from "./bash-tools.exec.js";
import { createProcessTool } from "./bash-tools.process.js";
function createWritableStdinStub() {
return {
write(_data: string, cb?: (err?: Error | null) => void) {
cb?.();
},
end() {},
destroyed: false,
};
}
afterEach(() => {
resetProcessRegistryForTests();
});
async function startPtySession(command: string) {
const execTool = createExecTool({ security: "full", ask: "off" });
const processTool = createProcessTool();
const result = await execTool.execute("toolcall", {
command,
pty: true,
background: true,
});
expect(result.details.status).toBe("running");
const sessionId = (result.details as { sessionId: string }).sessionId;
expect(sessionId).toBeTruthy();
return { processTool, sessionId };
}
async function waitForSessionCompletion(params: {
processTool: ReturnType<typeof createProcessTool>;
sessionId: string;
expectedText: string;
}) {
await expect
.poll(
async () => {
const poll = await params.processTool.execute("toolcall", {
action: "poll",
sessionId: params.sessionId,
});
const details = poll.details as { status?: string; aggregated?: string };
if (details.status === "running") {
return false;
}
expect(details.status).toBe("completed");
expect(details.aggregated ?? "").toContain(params.expectedText);
return true;
},
{
timeout: process.platform === "win32" ? 12_000 : 8_000,
interval: 30,
},
)
.toBe(true);
}
test("process send-keys encodes Enter for pty sessions", async () => {
const { processTool, sessionId } = await startPtySession(
'node -e "const dataEvent=String.fromCharCode(100,97,116,97);process.stdin.on(dataEvent,d=>{process.stdout.write(d);if(d.includes(10)||d.includes(13))process.exit(0);});"',
);
await processTool.execute("toolcall", {
action: "send-keys",
sessionId,
keys: ["h", "i", "Enter"],
});
await waitForSessionCompletion({ processTool, sessionId, expectedText: "hi" });
});
test("process submit sends Enter for pty sessions", async () => {
const { processTool, sessionId } = await startPtySession(
'node -e "const dataEvent=String.fromCharCode(100,97,116,97);const submitted=String.fromCharCode(115,117,98,109,105,116,116,101,100);process.stdin.on(dataEvent,d=>{if(d.includes(10)||d.includes(13)){process.stdout.write(submitted);process.exit(0);}});"',
);
await processTool.execute("toolcall", {
action: "submit",
sessionId,
});
await waitForSessionCompletion({ processTool, sessionId, expectedText: "submitted" });
});
test("process send-keys fails loud for unknown cursor mode when arrows depend on it", async () => {
const session = createProcessSessionFixture({
id: "sess-unknown-mode",
command: "vim",
backgrounded: true,
cursorKeyMode: "unknown",
});
session.stdin = createWritableStdinStub();
addSession(session);
const processTool = createProcessTool();
const result = await processTool.execute("toolcall", {
action: "send-keys",
sessionId: "sess-unknown-mode",
keys: ["up"],
});
expect(result.details).toMatchObject({ status: "failed" });
expect(result.content[0]).toMatchObject({
type: "text",
text: expect.stringContaining("cursor key mode is not known yet"),
});
});
test("process send-keys still sends non-cursor keys while mode is unknown", async () => {
const session = createProcessSessionFixture({
id: "sess-unknown-enter",
command: "vim",
backgrounded: true,
cursorKeyMode: "unknown",
});
session.stdin = createWritableStdinStub();
addSession(session);
const processTool = createProcessTool();
const result = await processTool.execute("toolcall", {
action: "send-keys",
sessionId: "sess-unknown-enter",
keys: ["Enter"],
});
expect(result.details).toMatchObject({ status: "running" });
});