test: move send-keys validation to helper

This commit is contained in:
Peter Steinberger
2026-04-11 12:43:16 +01:00
parent f770206311
commit 5f162973cf
4 changed files with 133 additions and 95 deletions

View File

@@ -0,0 +1,49 @@
import { expect, test } from "vitest";
import { createProcessSessionFixture } from "./bash-process-registry.test-helpers.js";
import { handleProcessSendKeys, type WritableStdin } from "./bash-tools.process-send-keys.js";
function createWritableStdinStub(): WritableStdin {
return {
write(_data: string, cb?: (err?: Error | null) => void) {
cb?.();
},
end() {},
destroyed: false,
};
}
test("process send-keys fails loud for unknown cursor mode when arrows depend on it", async () => {
const result = await handleProcessSendKeys({
sessionId: "sess-unknown-mode",
session: createProcessSessionFixture({
id: "sess-unknown-mode",
command: "vim",
backgrounded: true,
cursorKeyMode: "unknown",
}),
stdin: createWritableStdinStub(),
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 result = await handleProcessSendKeys({
sessionId: "sess-unknown-enter",
session: createProcessSessionFixture({
id: "sess-unknown-enter",
command: "vim",
backgrounded: true,
cursorKeyMode: "unknown",
}),
stdin: createWritableStdinStub(),
keys: ["Enter"],
});
expect(result.details).toMatchObject({ status: "running" });
});

View File

@@ -0,0 +1,76 @@
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import type { ProcessSession } from "./bash-process-registry.js";
import { deriveSessionName } from "./bash-tools.shared.js";
import { encodeKeySequence, hasCursorModeSensitiveKeys } from "./pty-keys.js";
export type WritableStdin = {
write: (data: string, cb?: (err?: Error | null) => void) => void;
end: () => void;
destroyed?: boolean;
};
function failText(text: string): AgentToolResult<unknown> {
return {
content: [
{
type: "text",
text,
},
],
details: { status: "failed" },
};
}
async function writeToStdin(stdin: WritableStdin, data: string) {
await new Promise<void>((resolve, reject) => {
stdin.write(data, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
export async function handleProcessSendKeys(params: {
sessionId: string;
session: ProcessSession;
stdin: WritableStdin;
keys?: string[];
hex?: string[];
literal?: string;
}): Promise<AgentToolResult<unknown>> {
const request = {
keys: params.keys,
hex: params.hex,
literal: params.literal,
};
if (params.session.cursorKeyMode === "unknown" && hasCursorModeSensitiveKeys(request)) {
return failText(
`Session ${params.sessionId} cursor key mode is not known yet. Poll or log until startup output appears, then retry send-keys.`,
);
}
const cursorKeyMode =
params.session.cursorKeyMode === "unknown" ? undefined : params.session.cursorKeyMode;
const { data, warnings } = encodeKeySequence(request, cursorKeyMode);
if (!data) {
return failText("No key data provided.");
}
await writeToStdin(params.stdin, data);
return {
content: [
{
type: "text",
text:
`Sent ${data.length} bytes to session ${params.sessionId}.` +
(warnings.length ? `\nWarnings:\n- ${warnings.join("\n- ")}` : ""),
},
],
details: {
status: "running",
sessionId: params.sessionId,
name: deriveSessionName(params.session.command),
},
};
}

View File

@@ -1,23 +1,8 @@
import { afterEach, expect, test } from "vitest";
import {
addSession,
markBackgrounded,
resetProcessRegistryForTests,
} from "./bash-process-registry.js";
import { createProcessSessionFixture } from "./bash-process-registry.test-helpers.js";
import { markBackgrounded, resetProcessRegistryForTests } from "./bash-process-registry.js";
import { runExecProcess } from "./bash-tools.exec-runtime.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();
});
@@ -103,47 +88,3 @@ test("process submit sends Enter for pty sessions", async () => {
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" });
});

View File

@@ -16,9 +16,10 @@ import {
setJobTtlMs,
} from "./bash-process-registry.js";
import { describeProcessTool } from "./bash-tools.descriptions.js";
import { handleProcessSendKeys, type WritableStdin } from "./bash-tools.process-send-keys.js";
import { deriveSessionName, pad, sliceLogLines, truncateMiddle } from "./bash-tools.shared.js";
import { recordCommandPoll, resetCommandPollCount } from "./command-poll-backoff.js";
import { encodeKeySequence, encodePaste, hasCursorModeSensitiveKeys } from "./pty-keys.js";
import { encodePaste } from "./pty-keys.js";
import { PROCESS_TOOL_DISPLAY_SUMMARY } from "./tool-description-presets.js";
import type { AgentToolWithMeta } from "./tools/common.js";
@@ -28,11 +29,6 @@ export type ProcessToolDefaults = {
scopeKey?: string;
};
type WritableStdin = {
write: (data: string, cb?: (err?: Error | null) => void) => void;
end: () => void;
destroyed?: boolean;
};
const DEFAULT_LOG_TAIL_LINES = 200;
function resolveLogSliceWindow(offset?: number, limit?: number) {
@@ -480,38 +476,14 @@ export function createProcessTool(
if (!resolved.ok) {
return resolved.result;
}
const request = {
return await handleProcessSendKeys({
sessionId: params.sessionId,
session: resolved.session,
stdin: resolved.stdin,
keys: params.keys,
hex: params.hex,
literal: params.literal,
};
if (resolved.session.cursorKeyMode === "unknown" && hasCursorModeSensitiveKeys(request)) {
return failText(
`Session ${params.sessionId} cursor key mode is not known yet. Poll or log until startup output appears, then retry send-keys.`,
);
}
const cursorKeyMode =
resolved.session.cursorKeyMode === "unknown"
? undefined
: resolved.session.cursorKeyMode;
const { data, warnings } = encodeKeySequence(request, cursorKeyMode);
if (!data) {
return {
content: [
{
type: "text",
text: "No key data provided.",
},
],
details: { status: "failed" },
};
}
await writeToStdin(resolved.stdin, data);
return runningSessionResult(
resolved.session,
`Sent ${data.length} bytes to session ${params.sessionId}.` +
(warnings.length ? `\nWarnings:\n- ${warnings.join("\n- ")}` : ""),
);
});
}
case "submit": {