Files
NetMesh/components/terminal/runtime/promptLineBreak.test.ts
zhaolei 3c72efcb7f
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

1235 lines
37 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import test from "node:test";
import assert from "node:assert/strict";
import {
consumeOsc133CommandCompletion,
createPromptLineBreakState,
detectTerminalCommandCompletions,
findTerminalPromptSourceChunkVisibleStarts,
insertPromptLineBreakBeforePrompt,
markPromptLineBreakCommandPending,
markTerminalCommandCompletionPending,
prepareTerminalDataForPromptLineBreak,
syncPromptLineBreakState,
} from "./promptLineBreak";
test("finds prompt chunks across trailing and leading ANSI sequences", () => {
const trailingAnsiData = "file tail\x1b[0m$ ";
assert.deepEqual(
findTerminalPromptSourceChunkVisibleStarts(
trailingAnsiData,
"$ ",
["file tail\x1b[0m".length],
),
["file tail".length],
);
const leadingAnsiData = "file tail\x1b[32m$ ";
assert.deepEqual(
findTerminalPromptSourceChunkVisibleStarts(leadingAnsiData, "$ ", ["file tail".length]),
["file tail".length],
);
assert.deepEqual(findTerminalPromptSourceChunkVisibleStarts("file tail$ ", "$ ", []), []);
const clearData = "file tail\x1b[2J$ ";
const promptVisibleStarts = findTerminalPromptSourceChunkVisibleStarts(
clearData,
"$ ",
["file tail\x1b[2J".length],
);
const state = createPromptLineBreakState();
state.lastPromptText = "$ ";
state.pendingCommand = true;
assert.equal(
prepareTerminalDataForPromptLineBreak(
createFakeTerm("", 0) as never,
"file tail\x1b[2J\x1b[3J$ ",
state,
true,
promptVisibleStarts,
),
"file tail\r\n\x1b[2J\x1b[3J$ ",
);
});
test("keeps cursor movement before a prompt-side line break", () => {
const state = createPromptLineBreakState();
state.lastPromptText = "$ ";
state.pendingCommand = true;
const output = "foo\x1b[E";
const data = `${output}$ `;
const promptStarts = findTerminalPromptSourceChunkVisibleStarts(
data,
state.lastPromptText,
[output.length],
);
assert.equal(
prepareTerminalDataForPromptLineBreak(
createFakeTerm("", 0) as never,
data,
state,
true,
promptStarts,
),
data,
);
});
test("measures cursor-only prompt prefixes before deciding on a line break", () => {
const state = createPromptLineBreakState();
state.lastPromptText = "$ ";
state.pendingCommand = true;
const cursorForward = "\x1b[10C";
const data = `${cursorForward}$ `;
assert.equal(
prepareTerminalDataForPromptLineBreak(
createFakeTerm("", 0) as never,
data,
state,
true,
findTerminalPromptSourceChunkVisibleStarts(data, "$ ", [cursorForward.length]),
),
`${cursorForward}\r\n$ `,
);
});
test("does not move a clear-screen prompt away from cursor home", () => {
const state = createPromptLineBreakState();
state.lastPromptText = "$ ";
state.pendingCommand = true;
const data = "\x1b[H\x1b[2J$ ";
assert.equal(
prepareTerminalDataForPromptLineBreak(
createFakeTerm("", 5) as never,
data,
state,
true,
findTerminalPromptSourceChunkVisibleStarts(data, "$ "),
),
data,
);
});
test("does not add a second break after carriage return", () => {
const state = createPromptLineBreakState();
state.lastPromptText = "$ ";
state.pendingCommand = true;
const output = "foo\r";
const data = `${output}$ `;
assert.equal(
prepareTerminalDataForPromptLineBreak(
createFakeTerm("", 5) as never,
data,
state,
true,
findTerminalPromptSourceChunkVisibleStarts(data, "$ ", [output.length]),
),
data,
);
});
test("does not move prompts after row-positioning controls", () => {
for (const control of ["\x1b[r", "\x1b[A", "\x1b[B", "\x1b[d", "\x1b[e"]) {
const state = createPromptLineBreakState();
state.lastPromptText = "$ ";
state.pendingCommand = true;
const data = `${control}$ `;
assert.equal(
prepareTerminalDataForPromptLineBreak(
createFakeTerm("", 0) as never,
data,
state,
true,
findTerminalPromptSourceChunkVisibleStarts(data, "$ ", [control.length]),
),
data,
JSON.stringify(control),
);
}
});
test("keeps non-moving mode toggles on the prompt side of an inserted break", () => {
for (const control of ["\x1b[?2004h", "\x1b[?25h", "\x1b[?25l"]) {
const stateAtLineStart = createPromptLineBreakState();
stateAtLineStart.lastPromptText = "$ ";
stateAtLineStart.pendingCommand = true;
const data = `${control}$ `;
const promptStarts = findTerminalPromptSourceChunkVisibleStarts(
data,
stateAtLineStart.lastPromptText,
[control.length],
);
assert.equal(
prepareTerminalDataForPromptLineBreak(
createFakeTerm("", 0) as never,
data,
stateAtLineStart,
true,
promptStarts,
),
data,
JSON.stringify(control),
);
const stateMidLine = createPromptLineBreakState();
stateMidLine.lastPromptText = "$ ";
stateMidLine.pendingCommand = true;
assert.equal(
prepareTerminalDataForPromptLineBreak(
createFakeTerm("output", 6) as never,
data,
stateMidLine,
true,
promptStarts,
),
`\r\n${data}`,
JSON.stringify(control),
);
}
});
test("inserts a prompt line break after leaving the alternate screen", () => {
const state = createPromptLineBreakState();
state.lastPromptText = "$ ";
state.pendingCommand = true;
const leaveAlternateScreen = "\x1b[?1049l";
const data = `${leaveAlternateScreen}$ `;
const promptStarts = findTerminalPromptSourceChunkVisibleStarts(
data,
state.lastPromptText,
[leaveAlternateScreen.length],
);
assert.equal(
prepareTerminalDataForPromptLineBreak(
createFakeTerm("", 5) as never,
data,
state,
true,
promptStarts,
),
`${leaveAlternateScreen}\r\n$ `,
);
});
function createFakeTerm(lineText = "", cursorX = lineText.length) {
return {
buffer: {
active: {
cursorX,
cursorY: 0,
baseY: 0,
getLine(line: number) {
if (line !== 0) return undefined;
return {
isWrapped: false,
translateToString() {
return lineText;
},
};
},
},
},
};
}
test("command completion tracking prefers OSC 133 and consumes one submitted command", () => {
const state = createPromptLineBreakState();
const stateRef = { current: state };
markTerminalCommandCompletionPending(stateRef);
markTerminalCommandCompletionPending(stateRef);
assert.equal(consumeOsc133CommandCompletion("C", state), false);
assert.equal(consumeOsc133CommandCompletion("D;0", state), true);
assert.equal(state.pendingCommandCompletions, 1);
assert.equal(consumeOsc133CommandCompletion("D;1", state), true);
assert.equal(consumeOsc133CommandCompletion("D;0", state), false);
});
test("command completion prompt fallback drains bounded submitted commands only at an empty prompt", () => {
const state = createPromptLineBreakState();
const stateRef = { current: state };
markTerminalCommandCompletionPending(stateRef);
markTerminalCommandCompletionPending(stateRef);
assert.equal(
detectTerminalCommandCompletions(createFakeTerm("$ echo pending") as never, state),
0,
);
assert.equal(state.pendingCommandCompletions, 2);
assert.equal(
detectTerminalCommandCompletions(createFakeTerm("$ ") as never, state),
2,
);
assert.equal(state.pendingCommandCompletions, 0);
assert.equal(
detectTerminalCommandCompletions(createFakeTerm("$ ") as never, state),
0,
);
});
function createWrappedFakeTerm(rows: string[], cursorY: number, cursorX: number, cols: number) {
return {
cols,
buffer: {
active: {
cursorX,
cursorY,
baseY: 0,
getLine(line: number) {
const lineText = rows[line];
if (lineText === undefined) return undefined;
return {
isWrapped: line > 0,
translateToString() {
return lineText;
},
};
},
},
},
};
}
test("does not insert before prompt-like suffixes in a larger output chunk", () => {
assert.equal(
insertPromptLineBreakBeforePrompt("hello$ ", "$ ", 0),
"hello$ ",
);
});
test("inserts at the start of a prompt chunk when previous output left the cursor mid-line", () => {
assert.equal(
insertPromptLineBreakBeforePrompt("$ ", "$ ", 5),
"\r\n$ ",
);
});
test("does not insert when the output already ends with a line break", () => {
assert.equal(
insertPromptLineBreakBeforePrompt("hello\r\n$ ", "$ ", 0),
"hello\r\n$ ",
);
});
test("keeps prompt ANSI styling on the prompt side of the inserted line break", () => {
assert.equal(
insertPromptLineBreakBeforePrompt("\x1b[32m$ \x1b[0m", "$ ", 5),
"\r\n\x1b[32m$ \x1b[0m",
);
});
test("does not insert for non-prompt output", () => {
assert.equal(
insertPromptLineBreakBeforePrompt("hello> ", "$ ", 0),
"hello> ",
);
});
test("does not insert for output chunks that only end with the cached prompt text", () => {
assert.equal(
insertPromptLineBreakBeforePrompt("total $ ", "$ ", 0),
"total $ ",
);
});
test("does not insert before an ambiguous prompt suffix inside output", () => {
assert.equal(
insertPromptLineBreakBeforePrompt("world$ ", "$ ", 5),
"world$ ",
);
});
test("does not insert before prompt-like output after a line break", () => {
assert.equal(
insertPromptLineBreakBeforePrompt("\r\nhello$ ", "$ ", 0),
"\r\nhello$ ",
);
});
test("inserts before a distinct root prompt in the same output chunk", () => {
const prompt = "[root@iZwz9ftrhzy4b3hduolf6yZ ~]# ";
assert.equal(
insertPromptLineBreakBeforePrompt(`file tail${prompt}`, prompt, 0),
`file tail\r\n${prompt}`,
);
});
test("inserts before a distinct conda prompt in the same output chunk", () => {
const prompt = "(base) rynn@aiserver:~$ ";
assert.equal(
insertPromptLineBreakBeforePrompt(`file tail${prompt}`, prompt, 0),
`file tail\r\n${prompt}`,
);
});
test("inserts before a distinct no-space root prompt in the same output chunk", () => {
const prompt = " root@stwo:~#";
assert.equal(
insertPromptLineBreakBeforePrompt(`file tail${prompt}`, prompt, 0),
`file tail\r\n${prompt}`,
);
});
test("does not insert before an already separated distinct prompt", () => {
const prompt = "(base) rynn@aiserver:~$ ";
assert.equal(
insertPromptLineBreakBeforePrompt(`file tail\r\n${prompt}`, prompt, 0),
`file tail\r\n${prompt}`,
);
});
test("does not refresh cached prompt from output that only ends with the prompt text", () => {
const state = createPromptLineBreakState();
state.lastPromptText = "$ ";
state.pendingCommand = true;
assert.equal(
prepareTerminalDataForPromptLineBreak(
createFakeTerm("", 0) as never,
"total $ ",
state,
true,
),
"total $ ",
);
assert.equal(state.suppressNextPromptCache, true);
syncPromptLineBreakState(createFakeTerm("total $ ") as never, state);
assert.equal(state.lastPromptText, "$ ");
assert.equal(state.pendingCommand, true);
assert.equal(state.suppressNextPromptCache, false);
});
test("uses a preserved PTY chunk boundary to separate a short prompt", () => {
const state = createPromptLineBreakState();
state.lastPromptText = "$ ";
state.pendingCommand = true;
assert.equal(
prepareTerminalDataForPromptLineBreak(
createFakeTerm("", 0) as never,
"file tail$ ",
state,
true,
["file tail".length],
),
"file tail\r\n$ ",
);
assert.equal(state.suppressNextPromptCache, false);
});
test("keeps waiting for the real prompt after an output suffix matches the prompt text", () => {
const state = createPromptLineBreakState();
state.lastPromptText = "$ ";
state.pendingCommand = true;
assert.equal(
prepareTerminalDataForPromptLineBreak(
createFakeTerm("", 0) as never,
"total $ ",
state,
true,
),
"total $ ",
);
syncPromptLineBreakState(createFakeTerm("total $ ") as never, state);
assert.equal(
prepareTerminalDataForPromptLineBreak(
createFakeTerm("total $ ", 8) as never,
"$ ",
state,
true,
),
"\r\n$ ",
);
});
test("keeps waiting after prompt-like output on a fresh line", () => {
const state = createPromptLineBreakState();
state.lastPromptText = "$ ";
state.pendingCommand = true;
assert.equal(
prepareTerminalDataForPromptLineBreak(
createFakeTerm("", 0) as never,
"\r\nhello$ ",
state,
true,
),
"\r\nhello$ ",
);
syncPromptLineBreakState(createFakeTerm("hello$ ") as never, state);
assert.equal(state.lastPromptText, "$ ");
assert.equal(state.pendingCommand, true);
assert.equal(
prepareTerminalDataForPromptLineBreak(
createFakeTerm("hello$ ", 7) as never,
"$ ",
state,
true,
),
"\r\n$ ",
);
});
test("prepares a same-chunk cat output break for a distinct prompt", () => {
const state = createPromptLineBreakState();
state.lastPromptText = "(base) rynn@aiserver:~$ ";
state.pendingCommand = true;
assert.equal(
prepareTerminalDataForPromptLineBreak(
createFakeTerm("", 0) as never,
"without trailing newline(base) rynn@aiserver:~$ ",
state,
true,
),
"without trailing newline\r\n(base) rynn@aiserver:~$ ",
);
assert.equal(state.suppressNextPromptCache, false);
});
test("caches a no-space root prompt from typed command alignment", () => {
const prompt = " root@stwo:~#";
const command = "printf ok";
const state = createPromptLineBreakState();
markPromptLineBreakCommandPending(
{ current: state },
createFakeTerm(`${prompt}${command}`) as never,
command,
);
assert.equal(state.lastPromptText, prompt);
assert.equal(state.pendingCommand, true);
});
test("caches a no-space root prompt when command echo lags", () => {
const prompt = " root@stwo:~#";
const command = "printf ok";
const state = createPromptLineBreakState();
markPromptLineBreakCommandPending(
{ current: state },
createFakeTerm(`${prompt}${command.slice(0, -1)}`) as never,
command,
);
assert.equal(state.lastPromptText, prompt);
assert.equal(state.pendingCommand, true);
});
test("caches a no-space root prompt when command echo lags by a word", () => {
const prompt = " root@stwo:~#";
const command = "printf ok";
const state = createPromptLineBreakState();
markPromptLineBreakCommandPending(
{ current: state },
createFakeTerm(`${prompt}printf `) as never,
command,
);
assert.equal(state.lastPromptText, prompt);
assert.equal(state.pendingCommand, true);
});
test("caches a no-space root prompt when a longer command echo lags by a word", () => {
const prompt = "root@host:~#";
const command = "git status";
const state = createPromptLineBreakState();
markPromptLineBreakCommandPending(
{ current: state },
createFakeTerm(`${prompt}git `) as never,
command,
);
assert.equal(state.lastPromptText, prompt);
assert.equal(state.pendingCommand, true);
});
test("caches a no-space root prompt when command echo lags mid-word", () => {
const prompt = "root@host:~#";
const command = "git status";
const state = createPromptLineBreakState();
markPromptLineBreakCommandPending(
{ current: state },
createFakeTerm(`${prompt}git st`) as never,
command,
);
assert.equal(state.lastPromptText, prompt);
assert.equal(state.pendingCommand, true);
});
test("caches a standard prompt when command echo lags near completion", () => {
const state = createPromptLineBreakState();
markPromptLineBreakCommandPending(
{ current: state },
createFakeTerm("$ git statu") as never,
"git status",
);
assert.equal(state.lastPromptText, "$ ");
assert.equal(state.pendingCommand, true);
});
test("caches a standard prompt when command echo lags after a word boundary", () => {
const cases = ["$ git ", "$ git st"];
for (const lineText of cases) {
const state = createPromptLineBreakState();
markPromptLineBreakCommandPending(
{ current: state },
createFakeTerm(lineText) as never,
"git status",
);
assert.equal(state.lastPromptText, "$ ", lineText);
assert.equal(state.pendingCommand, true, lineText);
}
});
test("caches a standard prompt when short command echo lags by one character", () => {
const cases = [
{ lineText: "$ l", command: "ls" },
{ lineText: "$ c", command: "cd" },
{ lineText: "prod-web> l", command: "ls", promptText: "prod-web> " },
{ lineText: "prod> l", command: "ls", promptText: "prod> " },
{ lineText: "prod.web> l", command: "ls", promptText: "prod.web> " },
{ lineText: "user@host:~$ l", command: "ls", promptText: "user@host:~$ " },
{ lineText: "[user@host ~]$ l", command: "ls", promptText: "[user@host ~]$ " },
{ lineText: "➜ netcatty $ l", command: "ls", promptText: "➜ netcatty $ " },
{ lineText: "➜ git l", command: "ls", promptText: "➜ git " },
{ lineText: "➜ git np", command: "npm", promptText: "➜ git " },
];
for (const { lineText, command, promptText = "$ " } of cases) {
const state = createPromptLineBreakState();
markPromptLineBreakCommandPending(
{ current: state },
createFakeTerm(lineText) as never,
command,
);
assert.equal(state.lastPromptText, promptText, lineText);
assert.equal(state.pendingCommand, true, lineText);
}
});
test("caches a no-space root prompt when a short command echo lags by a word", () => {
const prompt = "root@host:~#";
const cases = [
{ echoedInput: "ls ", command: "ls -la" },
{ echoedInput: "cd ", command: "cd /tmp" },
];
for (const { echoedInput, command } of cases) {
const state = createPromptLineBreakState();
markPromptLineBreakCommandPending(
{ current: state },
createFakeTerm(`${prompt}${echoedInput}`) as never,
command,
);
assert.equal(state.lastPromptText, prompt, command);
assert.equal(state.pendingCommand, true, command);
}
});
test("caches a no-space root prompt when a short command echo lags by one character", () => {
const prompt = " root@stwo:~#";
const cases = [
{ echoedInput: "l", command: "ls" },
{ echoedInput: "c", command: "cd" },
];
for (const { echoedInput, command } of cases) {
const state = createPromptLineBreakState();
markPromptLineBreakCommandPending(
{ current: state },
createFakeTerm(`${prompt}${echoedInput}`) as never,
command,
);
assert.equal(state.lastPromptText, prompt, command);
assert.equal(state.pendingCommand, true, command);
}
});
test("does not cache a stale command as prompt text", () => {
const state = createPromptLineBreakState();
markPromptLineBreakCommandPending(
{ current: state },
createFakeTerm("$ ls") as never,
"sudo",
);
assert.equal(state.lastPromptText, "");
assert.equal(state.pendingCommand, true);
});
test("does not cache common interactive program prompts", () => {
const cases = [
{ lineText: "sftp> get file", command: "get file" },
{ lineText: "ftp> ls", command: "ls" },
{ lineText: "ghci> :t map", command: ":t map" },
{ lineText: "node> .help", command: ".help" },
{ lineText: "mongo> db.stats()", command: "db.stats()" },
{ lineText: "rs0:PRIMARY> db.stats()", command: "db.stats()" },
{ lineText: "test> const x = 1", command: "const x = 1" },
{ lineText: "test> await db.users.findOne()", command: "await db.users.findOne()" },
{ lineText: "rs0 [direct: primary] test> db.stats()", command: "db.stats()" },
{ lineText: "rs0 [direct: primary] reporting> db.stats()", command: "db.stats()" },
{ lineText: "rs0 primary reporting> exit", command: "exit" },
{ lineText: "irb(main):001> puts 1", command: "puts 1" },
{ lineText: "pry(main)> whereami", command: "whereami" },
{ lineText: "[1] pry(main)> whereami", command: "whereami" },
{ lineText: "SQL> select 1", command: "select 1" },
{ lineText: "cqlsh> select * from users", command: "select * from users" },
{ lineText: "hive> select 1", command: "select 1" },
{ lineText: "spark-sql> select 1", command: "select 1" },
{ lineText: "jshell> /help", command: "/help" },
{ lineText: " ...> System.out.println(1)", command: "System.out.println(1)" },
{ lineText: "ksql> select 1", command: "select 1" },
{ lineText: "trino> select 1", command: "select 1" },
{ lineText: "trino:tpch> select 1", command: "select 1" },
{ lineText: "presto> show catalogs", command: "show catalogs" },
{ lineText: "presto:default> show tables", command: "show tables" },
{ lineText: "duckdb> select 1", command: "select 1" },
{ lineText: "lftp user@example.com:~> ls", command: "ls" },
{ lineText: "cqlsh:cycling> select * from cyclist", command: "select * from cyclist" },
{ lineText: "hive (default)> select 1", command: "select 1" },
{ lineText: "0: jdbc:hive2://localhost:10000/default> select 1", command: "select 1" },
{ lineText: "spark-sql (default)> select 1", command: "select 1" },
{ lineText: "test> db.stats()", command: "db.stats()" },
{ lineText: "test> db", command: "db" },
{ lineText: "test> const x = 1", command: "const x = 1" },
{ lineText: "test> await db.users.findOne()", command: "await db.users.findOne()" },
{ lineText: "rs0 [direct: primary] reporting> const x = 1", command: "const x = 1" },
{ lineText: "rs0 [direct: primary] reporting> await db.users.findOne()", command: "await db.users.findOne()" },
{ lineText: "Atlas a [primary] reporting> db.stats()", command: "db.stats()" },
{ lineText: "Atlas a [primary] reporting> await db.users.findOne()", command: "await db.users.findOne()" },
{ lineText: "rs0 primary test> db.stats()", command: "db.stats()" },
{ lineText: "test> rs.status()", command: "rs.status()" },
{ lineText: "test> print(1)", command: "print(1)" },
{ lineText: "test> 1 + 1", command: "1 + 1" },
{ lineText: "admin@localhost:27017> db.stats()", command: "db.stats()" },
];
for (const { lineText, command } of cases) {
const state = createPromptLineBreakState();
markPromptLineBreakCommandPending(
{ current: state },
createFakeTerm(lineText) as never,
command,
);
assert.equal(state.lastPromptText, "", lineText);
assert.equal(state.pendingCommand, true, lineText);
}
});
test("does not cache wrapped common interactive program prompts", () => {
const cases = [
{ rows: ["sftp> get very-long-", "remote-file"], command: "get very-long-remote-file" },
{ rows: ["node> console.", "log('ok')"], command: "console.log('ok')" },
{ rows: ["mongo> db.", "stats()"], command: "db.stats()" },
{ rows: ["cqlsh> select *", " from users"], command: "select * from users" },
{ rows: ["jshell> System.out.", "println(1)"], command: "System.out.println(1)" },
{ rows: [" ...> System.out.", "println(1)"], command: "System.out.println(1)" },
{ rows: ["trino> select", " 1"], command: "select 1" },
{ rows: ["trino:tpch> select", " 1"], command: "select 1" },
{ rows: ["duckdb> select", " 1"], command: "select 1" },
{ rows: ["cqlsh:cycling> select *", " from cyclist"], command: "select * from cyclist" },
{ rows: ["hive (default)> select", " 1"], command: "select 1" },
{ rows: ["0: jdbc:hive2://localhost:10000/default> select", " 1"], command: "select 1" },
{ rows: ["test> db.", "stats()"], command: "db.stats()" },
{ rows: ["test> d", "b"], command: "db" },
{ rows: ["rs0:PRIMARY> db.", "stats()"], command: "db.stats()" },
{ rows: ["rs0 [direct: primary] test> db.", "stats()"], command: "db.stats()" },
{ rows: ["rs0 [direct: primary]", " test> db.stats()"], command: "db.stats()" },
{ rows: ["rs0 [direct: primary]", " reporting> db.stats()"], command: "db.stats()" },
{ rows: ["rs0 [direct: primary]", " reporting> const x = 1"], command: "const x = 1" },
{ rows: ["Atlas a [primary]", " reporting> db.stats()"], command: "db.stats()" },
{ rows: ["rs0 primary test> db.", "stats()"], command: "db.stats()" },
{ rows: ["test> print", "(1)"], command: "print(1)" },
{ rows: ["test> 1 ", "+ 1"], command: "1 + 1" },
{ rows: ["admin@localhost:27017> db.", "stats()"], command: "db.stats()" },
];
for (const { rows, command } of cases) {
const state = createPromptLineBreakState();
markPromptLineBreakCommandPending(
{ current: state },
createWrappedFakeTerm(rows, 1, rows[1].length, 20) as never,
command,
);
assert.equal(state.lastPromptText, "", rows[0]);
assert.equal(state.pendingCommand, true, rows[0]);
}
});
test("caches wrapped non-Mongo-looking default-name greater-than prompts", () => {
const cases = [
{ rows: ["test> hel", "p"], command: "help", promptText: "test> " },
{ rows: ["test> show ", "dbs"], command: "show dbs", promptText: "test> " },
{ rows: ["admin> ex", "it"], command: "exit", promptText: "admin> " },
{ rows: ["local> dep", "loy"], command: "deploy", promptText: "local> " },
];
for (const { rows, command, promptText } of cases) {
const state = createPromptLineBreakState();
markPromptLineBreakCommandPending(
{ current: state },
createWrappedFakeTerm(rows, 1, rows[1].length, 20) as never,
command,
);
assert.equal(state.lastPromptText, promptText, rows[0]);
assert.equal(state.pendingCommand, true, rows[0]);
}
});
test("does not cache a live command suffix as prompt text", () => {
const state = createPromptLineBreakState();
markPromptLineBreakCommandPending(
{ current: state },
createFakeTerm("$ echo sudo") as never,
"sudo",
);
assert.equal(state.lastPromptText, "");
assert.equal(state.pendingCommand, true);
});
test("does not cache host prompt command symbols as prompt text", () => {
const prompt = "user@host:~$ ";
const cases = [
`${prompt}echo # sudo`,
`${prompt}printf % sudo`,
`${prompt}echo $ sudo`,
];
for (const lineText of cases) {
const state = createPromptLineBreakState();
markPromptLineBreakCommandPending(
{ current: state },
createFakeTerm(lineText) as never,
"sudo",
);
assert.equal(state.lastPromptText, "", lineText);
assert.equal(state.pendingCommand, true, lineText);
}
});
test("does not cache a themed prompt live command suffix as prompt text", () => {
for (const lineText of [
"➜ ~ echo sudo",
"➜ echo sudo",
"➜ make sudo",
"➜ docker sudo",
"➜ ./script sudo",
"➜ ./script sudo",
"➜ ~ echo # sudo",
]) {
const state = createPromptLineBreakState();
markPromptLineBreakCommandPending(
{ current: state },
createFakeTerm(lineText) as never,
"sudo",
);
assert.equal(state.lastPromptText, "", lineText);
assert.equal(state.pendingCommand, true, lineText);
}
});
test("caches themed prompt decorations from typed command alignment", () => {
const cases = [
{ lineText: "➜ ~/repo do", command: "do", promptText: "➜ ~/repo " },
{
lineText: "➜ netcatty git:(main) ✗ ls",
command: "ls",
promptText: "➜ netcatty git:(main) ✗ ",
},
{
lineText: "➜ netcatty git:(main) ✗ + ls",
command: "ls",
promptText: "➜ netcatty git:(main) ✗ + ",
},
{ lineText: "➜ netcatty ✗ $ ls", command: "ls", promptText: "➜ netcatty ✗ $ " },
{ lineText: "➜ netcatty $ ls", command: "ls", promptText: "➜ netcatty $ " },
];
for (const { lineText, command, promptText } of cases) {
const state = createPromptLineBreakState();
markPromptLineBreakCommandPending(
{ current: state },
createFakeTerm(lineText) as never,
command,
);
assert.equal(state.lastPromptText, promptText, lineText);
assert.equal(state.pendingCommand, true, lineText);
}
});
test("caches themed prompt decorations when command echo lags", () => {
const cases = [
{ lineText: "➜ ~ git ", command: "git status", promptText: "➜ ~ " },
{ lineText: "➜ ~ git st", command: "git status", promptText: "➜ ~ " },
{
lineText: "➜ netcatty git:(main) ✗ git ",
command: "git status",
promptText: "➜ netcatty git:(main) ✗ ",
},
{
lineText: "➜ netcatty git:(main) ✗ git st",
command: "git status",
promptText: "➜ netcatty git:(main) ✗ ",
},
];
for (const { lineText, command, promptText } of cases) {
const state = createPromptLineBreakState();
markPromptLineBreakCommandPending(
{ current: state },
createFakeTerm(lineText) as never,
command,
);
assert.equal(state.lastPromptText, promptText, lineText);
assert.equal(state.pendingCommand, true, lineText);
}
});
test("caches themed bare directory prompts for direct sends before command echo", () => {
const cases = [
{ lineText: "➜ netcatty ", command: "ls", promptText: "➜ netcatty " },
{ lineText: "➜ git ", command: "npm", promptText: "➜ git " },
{ lineText: "➜ git ", command: "git status", promptText: "➜ git " },
{ lineText: "➜ make ", command: "sudo", promptText: "➜ make " },
{ lineText: "➜ make ", command: "make build", promptText: "➜ make " },
{ lineText: "➜ node ", command: "yarn", promptText: "➜ node " },
];
for (const { lineText, command, promptText } of cases) {
const state = createPromptLineBreakState();
markPromptLineBreakCommandPending(
{ current: state },
createFakeTerm(lineText) as never,
command,
);
assert.equal(state.lastPromptText, promptText, lineText);
assert.equal(state.pendingCommand, true, lineText);
}
});
test("does not cache interactive prompts for direct sends before command echo", () => {
const cases = [
{ lineText: "test> ", command: "const x = 1" },
{ lineText: "test> ", command: "await db.users.findOne()" },
{ lineText: "test> ", command: "db" },
{ lineText: "rs0 [direct: primary] reporting> ", command: "const x = 1" },
{ lineText: "rs0 [direct: primary] reporting> ", command: "await db.users.findOne()" },
{ lineText: "rs0 [direct: primary] reporting> ", command: "db.stats()" },
{ lineText: "Atlas a [primary] reporting> ", command: "db.stats()" },
];
for (const { lineText, command } of cases) {
const state = createPromptLineBreakState();
markPromptLineBreakCommandPending(
{ current: state },
createFakeTerm(lineText) as never,
command,
);
assert.equal(state.lastPromptText, "", lineText);
assert.equal(state.pendingCommand, true, lineText);
}
});
test("clears an old cached prompt when a direct send is interactive", () => {
const state = createPromptLineBreakState();
state.lastPromptText = "rs0 [direct: primary] reporting> ";
markPromptLineBreakCommandPending(
{ current: state },
createFakeTerm("rs0 [direct: primary] reporting> ") as never,
"db.stats()",
);
assert.equal(state.lastPromptText, "");
assert.equal(state.pendingCommand, true);
});
test("caches host-style greater-than prompts for direct sends before command echo", () => {
const cases = [
{ lineText: "server> ", command: "exit" },
{ lineText: "staging> ", command: "show dbs" },
{ lineText: "server> ", command: "db.stats()" },
{ lineText: "webdb> ", command: "deploy" },
{ lineText: "prod.db> ", command: "deploy" },
{ lineText: "test> ", command: "deploy" },
{ lineText: "test> ", command: "exit" },
{ lineText: "test> ", command: "help" },
{ lineText: "test> ", command: "show dbs" },
{ lineText: "admin> ", command: "deploy" },
];
for (const { lineText, command } of cases) {
const state = createPromptLineBreakState();
markPromptLineBreakCommandPending(
{ current: state },
createFakeTerm(lineText) as never,
command,
);
assert.equal(state.lastPromptText, lineText, lineText);
assert.equal(state.pendingCommand, true, lineText);
}
});
test("does not cache a live path suffix as prompt text", () => {
for (const lineText of ["$ cd ~/sudo", "$ cat > sudo", "$ echo path#sudo"]) {
const state = createPromptLineBreakState();
markPromptLineBreakCommandPending(
{ current: state },
createFakeTerm(lineText) as never,
"sudo",
);
assert.equal(state.lastPromptText, "", lineText);
assert.equal(state.pendingCommand, true, lineText);
}
});
test("does not cache a stale command from a standard prompt echo prefix", () => {
for (const lineText of ["$ s", "$ su", "$ sud"]) {
const state = createPromptLineBreakState();
markPromptLineBreakCommandPending(
{ current: state },
createFakeTerm(lineText) as never,
"sudo",
);
assert.equal(state.lastPromptText, "", lineText);
assert.equal(state.pendingCommand, true, lineText);
}
});
test("does not cache partial stale commands after a no-space prompt", () => {
const prompt = " root@stwo:~#";
for (const lineText of [`${prompt}s`, `${prompt}sud`]) {
const state = createPromptLineBreakState();
markPromptLineBreakCommandPending(
{ current: state },
createFakeTerm(lineText) as never,
"sudo",
);
assert.equal(state.lastPromptText, "", lineText);
assert.equal(state.pendingCommand, true, lineText);
}
});
test("does not cache stale command suffixes after a no-space prompt", () => {
const prompt = " root@stwo:~#";
const cases = [
`${prompt}cat > sudo`,
`${prompt}echo # sudo`,
`${prompt}echo $ sudo`,
`${prompt}printf % sudo`,
`${prompt}echo path#sudo`,
`${prompt}> sudo`,
`${prompt}# sudo`,
`${prompt}% sudo`,
`${prompt}$ sudo`,
];
cases.push("root#echo $ sudo", "root@host:~#make $ sudo");
for (const lineText of cases) {
const state = createPromptLineBreakState();
markPromptLineBreakCommandPending(
{ current: state },
createFakeTerm(lineText) as never,
"sudo",
);
assert.equal(state.lastPromptText, "", lineText);
assert.equal(state.pendingCommand, true, lineText);
}
});
test("syncs prompts that contain prompt-like symbols", () => {
const prompts = [
"user@host ~/foo# bar $ ",
"user@host ~/foo# git $ ",
"user@host ~/foo#git $ ",
"root@host ~/foo# bar # ",
"root@host ~/foo#bar # ",
"fish@host ~/foo# bar % ",
"fish@host ~/foo%bar % ",
"user@host:~/foo# bar $ ",
"user@host ~/repo # $ ",
"➜ ~ $ ",
"user@host ~/foo% bar $ ",
"user@host ~/foo> bar $ ",
"user@host ~/foo# bar> ",
"user@host ~/foo# bar ",
"user@host ~/foo#bar> ",
];
for (const prompt of prompts) {
const state = createPromptLineBreakState();
syncPromptLineBreakState(createFakeTerm(prompt) as never, state);
assert.equal(state.lastPromptText, prompt, prompt);
assert.equal(state.pendingCommand, false, prompt);
}
});
test("syncs a no-space root prompt without xterm row padding", () => {
const prompt = " root@stwo:~#";
const state = createPromptLineBreakState();
syncPromptLineBreakState(createFakeTerm(`${prompt} `, prompt.length) as never, state);
assert.equal(state.lastPromptText, prompt);
assert.equal(state.pendingCommand, false);
});
test("refreshes cached prompt when a changed prompt arrives after a line break in the same chunk", () => {
const state = createPromptLineBreakState();
state.lastPromptText = "old$ ";
state.pendingCommand = true;
const termBeforeWrite = createFakeTerm("old$ cd /tmp", 12);
assert.equal(
prepareTerminalDataForPromptLineBreak(
termBeforeWrite as never,
"\r\nnew$ ",
state,
true,
),
"\r\nnew$ ",
);
assert.equal(state.suppressNextPromptCache, false);
syncPromptLineBreakState(createFakeTerm("new$ ") as never, state);
assert.equal(state.lastPromptText, "new$ ");
assert.equal(state.pendingCommand, false);
});
test("caches the first valid prompt even when a command is already pending", () => {
const state = createPromptLineBreakState();
state.pendingCommand = true;
syncPromptLineBreakState(createFakeTerm("$ ") as never, state);
assert.equal(state.lastPromptText, "$ ");
assert.equal(state.pendingCommand, false);
assert.equal(state.suppressNextPromptCache, false);
});
test("does not refresh cached prompt from an unchanged mid-line write without a line reset", () => {
const state = createPromptLineBreakState();
state.lastPromptText = "old$ ";
state.pendingCommand = true;
const termBeforeWrite = createFakeTerm("old$ run", 8);
assert.equal(
prepareTerminalDataForPromptLineBreak(
termBeforeWrite as never,
"outputnew$ ",
state,
true,
),
"outputnew$ ",
);
assert.equal(state.suppressNextPromptCache, true);
syncPromptLineBreakState(createFakeTerm("outputnew$ ") as never, state);
assert.equal(state.lastPromptText, "old$ ");
assert.equal(state.pendingCommand, true);
assert.equal(state.suppressNextPromptCache, false);
});
test("does not insert a blank line after a full-width prefix that already wrapped", () => {
const state = createPromptLineBreakState();
state.lastPromptText = "$ ";
state.pendingCommand = true;
const cols = 10;
const fullWidth = "x".repeat(cols);
const term = createWrappedFakeTerm([fullWidth], 0, 0, cols);
assert.equal(
prepareTerminalDataForPromptLineBreak(
term as never,
`${fullWidth}$ `,
state,
true,
[fullWidth.length],
),
`${fullWidth}$ `,
);
});
test("finds prompt starts on the display string after identity transforms", () => {
const prompt = "$ ";
const data = `file tail${prompt}`;
const starts = findTerminalPromptSourceChunkVisibleStarts(
data,
prompt,
["file tail".length],
);
assert.deepEqual(starts, ["file tail".length]);
});