Files
oh-my-pi/packages/coding-agent/test/tools/ssh-render.test.ts
T
roboomp 7a1280784e fix(tools/ssh): rendered multiline remote commands in a framed body block
The SSH tool renderer fed the raw command into renderStatusLine's
description, so any newline in the remote command expanded the
single-line tool header — the bordered output block then opened
mid-command and the rest of the SSH cell rendered against a broken
frame.

The renderer now keeps only [host] in the header and renders the
full command (with a dim $ prefix and tab sanitization) as its own
framed section above Output, matching the bash renderer's shape.

renderStatusLine also flattens CR/LF in title, description, meta,
and badge labels so no future caller can accidentally produce a
multiline tool header.

Covered by:
- test/tools/ssh-render.test.ts: multiline command stays out of
  the header and every command line is present in the body, for
  both renderCall and renderResult.
- test/tui/status-line-newline-guard.test.ts: embedded LF, CRLF,
  and lone CR in description/meta are flattened to spaces.

Fixes #1828
2026-06-04 07:44:52 +00:00

59 lines
2.3 KiB
TypeScript

import { beforeAll, describe, expect, it } from "bun:test";
import { getThemeByName, initTheme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
import { sshToolRenderer } from "@oh-my-pi/pi-coding-agent/tools/ssh";
import { sanitizeText } from "@oh-my-pi/pi-utils";
beforeAll(async () => {
await initTheme();
});
describe("sshToolRenderer", () => {
it("keeps the status header on one line when the command spans multiple lines", async () => {
const uiTheme = (await getThemeByName("dark"))!;
expect(uiTheme).toBeDefined();
const command = ["set -e", "cat > /etc/apt/sources.list <<'EOF'", "# mirrors", "EOF"].join("\n");
const component = sshToolRenderer.renderResult(
{ content: [{ type: "text", text: "" }] },
{ expanded: false, isPartial: false },
uiTheme,
{ host: "router", command },
);
const rendered = component.render(120);
const sanitized = rendered.map(line => sanitizeText(line));
// First visible row is the status header, and it MUST remain a single line.
// It carries the host but NOT the command — the command lives in the body.
const header = sanitized[0]!;
expect(header).toContain("SSH");
expect(header).toContain("[router]");
expect(header).not.toContain("set -e");
expect(header).not.toContain("EOF");
// Every command line still appears, inside the framed body.
const body = sanitized.slice(1).join("\n");
expect(body).toContain("$ set -e");
expect(body).toContain("cat > /etc/apt/sources.list <<'EOF'");
expect(body).toContain("# mirrors");
expect(body).toContain("EOF");
});
it("keeps the pending-call header on one line for multiline commands", async () => {
const uiTheme = (await getThemeByName("dark"))!;
expect(uiTheme).toBeDefined();
const command = "set -e\ndo-something";
const component = sshToolRenderer.renderCall(
{ host: "router", command },
{ expanded: false, isPartial: true },
uiTheme,
);
const rendered = component.render(120);
const sanitized = rendered.map(line => sanitizeText(line));
const header = sanitized[0]!;
expect(header).toContain("SSH");
expect(header).toContain("[router]");
expect(header).not.toContain("set -e");
expect(header).not.toContain("do-something");
const body = sanitized.slice(1).join("\n");
expect(body).toContain("$ set -e");
expect(body).toContain("do-something");
});
});