test(tui): reduce keyboard negotiation coverage
This commit is contained in:
@@ -1,11 +1,7 @@
|
||||
import assert from "node:assert";
|
||||
import { describe, it, mock } from "node:test";
|
||||
import { setKittyProtocolActive } from "../src/keys.ts";
|
||||
import {
|
||||
normalizeAppleTerminalInput,
|
||||
ProcessTerminal,
|
||||
parseKeyboardProtocolNegotiationSequence,
|
||||
} from "../src/terminal.ts";
|
||||
import { normalizeAppleTerminalInput, ProcessTerminal } from "../src/terminal.ts";
|
||||
|
||||
describe("normalizeAppleTerminalInput", () => {
|
||||
it("rewrites Apple Terminal Return to CSI-u Shift+Enter when Shift is pressed", () => {
|
||||
@@ -26,24 +22,6 @@ describe("normalizeAppleTerminalInput", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseKeyboardProtocolNegotiationSequence", () => {
|
||||
it("parses Kitty keyboard protocol flag responses", () => {
|
||||
assert.deepEqual(parseKeyboardProtocolNegotiationSequence("\x1b[?7u"), { type: "kitty-flags", flags: 7 });
|
||||
assert.deepEqual(parseKeyboardProtocolNegotiationSequence("\x1b[?1u"), { type: "kitty-flags", flags: 1 });
|
||||
assert.deepEqual(parseKeyboardProtocolNegotiationSequence("\x1b[?0u"), { type: "kitty-flags", flags: 0 });
|
||||
});
|
||||
|
||||
it("parses DA responses as the negotiation sentinel", () => {
|
||||
assert.deepEqual(parseKeyboardProtocolNegotiationSequence("\x1b[?62;4;52c"), { type: "device-attributes" });
|
||||
assert.deepEqual(parseKeyboardProtocolNegotiationSequence("\x1b[?1;2c"), { type: "device-attributes" });
|
||||
});
|
||||
|
||||
it("ignores unrelated input", () => {
|
||||
assert.equal(parseKeyboardProtocolNegotiationSequence("\x1b[13;2u"), undefined);
|
||||
assert.equal(parseKeyboardProtocolNegotiationSequence("a"), undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ProcessTerminal Kitty keyboard protocol negotiation", () => {
|
||||
type NegotiationHarness = {
|
||||
terminal: ProcessTerminal;
|
||||
@@ -104,295 +82,54 @@ describe("ProcessTerminal Kitty keyboard protocol negotiation", () => {
|
||||
};
|
||||
}
|
||||
|
||||
function runNegotiation(response: string): { kittyProtocolActive: boolean; writes: string[]; input?: string } {
|
||||
const harness = setupNegotiation();
|
||||
try {
|
||||
harness.send(response);
|
||||
return {
|
||||
kittyProtocolActive: harness.terminal.kittyProtocolActive,
|
||||
writes: [...harness.writes],
|
||||
input: harness.getInput(),
|
||||
};
|
||||
} finally {
|
||||
harness.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
it("requests flags before querying and uses DA as unsupported sentinel", () => {
|
||||
const { kittyProtocolActive, writes, input } = runNegotiation("\x1b[?0u\x1b[?62;4;52c");
|
||||
|
||||
assert.equal(writes[0], "\x1b[>7u\x1b[?u\x1b[c");
|
||||
assert.equal(writes.at(-1), "\x1b[>4;2m");
|
||||
assert.equal(kittyProtocolActive, false);
|
||||
assert.equal(input, undefined);
|
||||
});
|
||||
|
||||
it("tracks delayed Kitty flags after DA sentinel", () => {
|
||||
const harness = setupNegotiation();
|
||||
try {
|
||||
harness.send("\x1b[?62;4;52c");
|
||||
harness.send("\x1b[?7u");
|
||||
|
||||
assert.equal(harness.writes[0], "\x1b[>7u\x1b[?u\x1b[c");
|
||||
assert.equal(harness.writes.includes("\x1b[>4;2m"), true);
|
||||
assert.equal(harness.getInput(), undefined);
|
||||
assert.equal(harness.terminal.kittyProtocolActive, true);
|
||||
|
||||
harness.cleanup();
|
||||
assert.equal(harness.writes.filter((write) => write === "\x1b[<u").length, 1);
|
||||
assert.equal(harness.writes.filter((write) => write === "\x1b[>4;0m").length, 1);
|
||||
} finally {
|
||||
harness.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
it("activates Kitty mode for non-zero negotiated flags", () => {
|
||||
const { kittyProtocolActive, writes, input } = runNegotiation("\x1b[?1u\x1b[?62;4;52c");
|
||||
|
||||
assert.equal(writes[0], "\x1b[>7u\x1b[?u\x1b[c");
|
||||
assert.equal(writes.includes("\x1b[>4;2m"), false);
|
||||
assert.equal(kittyProtocolActive, true);
|
||||
assert.equal(input, undefined);
|
||||
});
|
||||
|
||||
it("does not fall back after Kitty mode activates", () => {
|
||||
mock.timers.enable({ apis: ["setTimeout"] });
|
||||
const harness = setupNegotiation();
|
||||
try {
|
||||
harness.send("\x1b[?1u");
|
||||
mock.timers.tick(150);
|
||||
|
||||
assert.equal(harness.writes.includes("\x1b[>4;2m"), false);
|
||||
assert.equal(harness.terminal.kittyProtocolActive, true);
|
||||
} finally {
|
||||
harness.cleanup();
|
||||
mock.timers.reset();
|
||||
}
|
||||
});
|
||||
|
||||
it("forwards Escape input after Kitty mode activates without DA sentinel", () => {
|
||||
mock.timers.enable({ apis: ["setTimeout"] });
|
||||
const harness = setupNegotiation();
|
||||
try {
|
||||
harness.send("\x1b[?1u");
|
||||
mock.timers.tick(150);
|
||||
harness.send("\x1b");
|
||||
mock.timers.tick(10);
|
||||
|
||||
assert.equal(harness.writes.includes("\x1b[>4;2m"), false);
|
||||
assert.equal(harness.getInput(), "\x1b");
|
||||
assert.equal(harness.terminal.kittyProtocolActive, true);
|
||||
} finally {
|
||||
harness.cleanup();
|
||||
mock.timers.reset();
|
||||
}
|
||||
});
|
||||
|
||||
it("swallows delayed DA sentinel after Kitty mode activates", () => {
|
||||
mock.timers.enable({ apis: ["setTimeout"] });
|
||||
const harness = setupNegotiation();
|
||||
try {
|
||||
harness.send("\x1b[?1u");
|
||||
mock.timers.tick(150);
|
||||
harness.send("\x1b[?62;4;52c");
|
||||
|
||||
assert.equal(harness.writes.includes("\x1b[>4;2m"), false);
|
||||
assert.equal(harness.getInput(), undefined);
|
||||
assert.equal(harness.terminal.kittyProtocolActive, true);
|
||||
} finally {
|
||||
harness.cleanup();
|
||||
mock.timers.reset();
|
||||
}
|
||||
});
|
||||
|
||||
it("pops optimistic Kitty mode on stop while negotiation is pending", () => {
|
||||
const harness = setupNegotiation();
|
||||
harness.cleanup();
|
||||
|
||||
assert.equal(harness.writes.filter((write) => write === "\x1b[<u").length, 1);
|
||||
});
|
||||
|
||||
it("pops optimistic Kitty mode from drainInput while negotiation is pending", async () => {
|
||||
const harness = setupNegotiation();
|
||||
try {
|
||||
await harness.terminal.drainInput(0);
|
||||
harness.cleanup();
|
||||
|
||||
assert.equal(harness.writes.filter((write) => write === "\x1b[<u").length, 1);
|
||||
} finally {
|
||||
harness.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
it("falls back to modifyOtherKeys when no negotiation response arrives", () => {
|
||||
mock.timers.enable({ apis: ["setTimeout"] });
|
||||
const harness = setupNegotiation();
|
||||
try {
|
||||
mock.timers.tick(150);
|
||||
|
||||
assert.equal(harness.writes[0], "\x1b[>7u\x1b[?u\x1b[c");
|
||||
assert.equal(harness.writes.includes("\x1b[>4;2m"), true);
|
||||
assert.equal(harness.terminal.kittyProtocolActive, false);
|
||||
} finally {
|
||||
harness.cleanup();
|
||||
mock.timers.reset();
|
||||
}
|
||||
});
|
||||
|
||||
it("replays Escape input buffered during pending negotiation", () => {
|
||||
mock.timers.enable({ apis: ["setTimeout"] });
|
||||
const harness = setupNegotiation();
|
||||
try {
|
||||
harness.send("\x1b");
|
||||
mock.timers.tick(10);
|
||||
|
||||
assert.equal(harness.writes.includes("\x1b[>4;2m"), false);
|
||||
assert.equal(harness.getInput(), undefined);
|
||||
|
||||
mock.timers.tick(140);
|
||||
|
||||
assert.equal(harness.writes.includes("\x1b[>4;2m"), true);
|
||||
assert.equal(harness.getInput(), "\x1b");
|
||||
} finally {
|
||||
harness.cleanup();
|
||||
mock.timers.reset();
|
||||
}
|
||||
});
|
||||
|
||||
it("replays CSI prefix input buffered during pending negotiation", () => {
|
||||
mock.timers.enable({ apis: ["setTimeout"] });
|
||||
const harness = setupNegotiation();
|
||||
try {
|
||||
harness.send("\x1b[");
|
||||
mock.timers.tick(10);
|
||||
|
||||
assert.equal(harness.getInput(), undefined);
|
||||
|
||||
mock.timers.tick(140);
|
||||
|
||||
assert.equal(harness.writes.includes("\x1b[>4;2m"), true);
|
||||
assert.equal(harness.getInput(), undefined);
|
||||
|
||||
mock.timers.tick(150);
|
||||
|
||||
assert.equal(harness.getInput(), "\x1b[");
|
||||
} finally {
|
||||
harness.cleanup();
|
||||
mock.timers.reset();
|
||||
}
|
||||
});
|
||||
|
||||
it("forwards Escape input after no-response fallback", () => {
|
||||
mock.timers.enable({ apis: ["setTimeout"] });
|
||||
const harness = setupNegotiation();
|
||||
try {
|
||||
mock.timers.tick(150);
|
||||
harness.send("\x1b");
|
||||
mock.timers.tick(10);
|
||||
|
||||
assert.equal(harness.getInput(), "\x1b");
|
||||
} finally {
|
||||
harness.cleanup();
|
||||
mock.timers.reset();
|
||||
}
|
||||
});
|
||||
|
||||
it("replays CSI prefix input after no-response fallback", () => {
|
||||
mock.timers.enable({ apis: ["setTimeout"] });
|
||||
const harness = setupNegotiation();
|
||||
try {
|
||||
mock.timers.tick(150);
|
||||
harness.send("\x1b[");
|
||||
mock.timers.tick(10);
|
||||
|
||||
assert.equal(harness.getInput(), undefined);
|
||||
|
||||
mock.timers.tick(150);
|
||||
|
||||
assert.equal(harness.getInput(), "\x1b[");
|
||||
} finally {
|
||||
harness.cleanup();
|
||||
mock.timers.reset();
|
||||
}
|
||||
});
|
||||
|
||||
it("tracks late Kitty confirmation after modifyOtherKeys fallback", () => {
|
||||
mock.timers.enable({ apis: ["setTimeout"] });
|
||||
const harness = setupNegotiation();
|
||||
try {
|
||||
mock.timers.tick(150);
|
||||
harness.send("\x1b[?7u");
|
||||
|
||||
assert.equal(harness.writes.includes("\x1b[>4;2m"), true);
|
||||
assert.equal(harness.terminal.kittyProtocolActive, true);
|
||||
|
||||
harness.cleanup();
|
||||
assert.equal(harness.writes.filter((write) => write === "\x1b[<u").length, 1);
|
||||
assert.equal(harness.writes.filter((write) => write === "\x1b[>4;0m").length, 1);
|
||||
} finally {
|
||||
harness.cleanup();
|
||||
mock.timers.reset();
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps fallback active for delayed DA responses", () => {
|
||||
it("falls back to modifyOtherKeys for unsupported or silent terminals", () => {
|
||||
const unsupported = setupNegotiation();
|
||||
try {
|
||||
unsupported.send("\x1b[?62;4;52c");
|
||||
|
||||
assert.equal(unsupported.writes[0], "\x1b[>7u\x1b[?u\x1b[c");
|
||||
assert.equal(unsupported.writes.includes("\x1b[>4;2m"), true);
|
||||
assert.equal(unsupported.getInput(), undefined);
|
||||
assert.equal(unsupported.terminal.kittyProtocolActive, false);
|
||||
} finally {
|
||||
unsupported.cleanup();
|
||||
}
|
||||
|
||||
mock.timers.enable({ apis: ["setTimeout"] });
|
||||
const harness = setupNegotiation();
|
||||
const silent = setupNegotiation();
|
||||
try {
|
||||
mock.timers.tick(150);
|
||||
harness.send("\x1b[?62;4;52c");
|
||||
|
||||
assert.equal(harness.writes.filter((write) => write === "\x1b[>4;2m").length, 1);
|
||||
assert.equal(harness.getInput(), undefined);
|
||||
assert.equal(harness.terminal.kittyProtocolActive, false);
|
||||
assert.equal(silent.writes[0], "\x1b[>7u\x1b[?u\x1b[c");
|
||||
assert.equal(silent.writes.includes("\x1b[>4;2m"), true);
|
||||
assert.equal(silent.terminal.kittyProtocolActive, false);
|
||||
} finally {
|
||||
harness.cleanup();
|
||||
silent.cleanup();
|
||||
mock.timers.reset();
|
||||
}
|
||||
});
|
||||
|
||||
it("swallows and reassembles split DA responses flushed incomplete", () => {
|
||||
mock.timers.enable({ apis: ["setTimeout"] });
|
||||
const harness = setupNegotiation();
|
||||
try {
|
||||
harness.send("\x1b[?62;");
|
||||
mock.timers.tick(10);
|
||||
|
||||
assert.equal(harness.getInput(), undefined);
|
||||
|
||||
harness.send("4;52c");
|
||||
mock.timers.tick(140);
|
||||
|
||||
assert.equal(harness.writes.includes("\x1b[>4;2m"), true);
|
||||
assert.equal(harness.getInput(), undefined);
|
||||
assert.equal(harness.terminal.kittyProtocolActive, false);
|
||||
} finally {
|
||||
harness.cleanup();
|
||||
mock.timers.reset();
|
||||
}
|
||||
});
|
||||
|
||||
it("swallows and reassembles split late DA responses after fallback", () => {
|
||||
mock.timers.enable({ apis: ["setTimeout"] });
|
||||
const harness = setupNegotiation();
|
||||
try {
|
||||
mock.timers.tick(150);
|
||||
harness.send("\x1b[?62;");
|
||||
mock.timers.tick(10);
|
||||
|
||||
assert.equal(harness.getInput(), undefined);
|
||||
|
||||
harness.send("4;52c");
|
||||
|
||||
assert.equal(harness.writes.filter((write) => write === "\x1b[>4;2m").length, 1);
|
||||
assert.equal(harness.getInput(), undefined);
|
||||
assert.equal(harness.terminal.kittyProtocolActive, false);
|
||||
} finally {
|
||||
harness.cleanup();
|
||||
mock.timers.reset();
|
||||
}
|
||||
});
|
||||
|
||||
it("swallows and reassembles split late Kitty responses after fallback", () => {
|
||||
it("tracks late split Kitty confirmation after fallback", () => {
|
||||
mock.timers.enable({ apis: ["setTimeout"] });
|
||||
const harness = setupNegotiation();
|
||||
try {
|
||||
@@ -405,8 +142,30 @@ describe("ProcessTerminal Kitty keyboard protocol negotiation", () => {
|
||||
harness.send("u");
|
||||
|
||||
assert.equal(harness.writes.includes("\x1b[>4;2m"), true);
|
||||
assert.equal(harness.getInput(), undefined);
|
||||
assert.equal(harness.terminal.kittyProtocolActive, true);
|
||||
|
||||
harness.cleanup();
|
||||
assert.equal(harness.writes.filter((write) => write === "\x1b[<u").length, 1);
|
||||
assert.equal(harness.writes.filter((write) => write === "\x1b[>4;0m").length, 1);
|
||||
} finally {
|
||||
harness.cleanup();
|
||||
mock.timers.reset();
|
||||
}
|
||||
});
|
||||
|
||||
it("replays buffered CSI-prefix input after fallback", () => {
|
||||
mock.timers.enable({ apis: ["setTimeout"] });
|
||||
const harness = setupNegotiation();
|
||||
try {
|
||||
harness.send("\x1b[");
|
||||
mock.timers.tick(150);
|
||||
|
||||
assert.equal(harness.writes.includes("\x1b[>4;2m"), true);
|
||||
assert.equal(harness.getInput(), undefined);
|
||||
|
||||
mock.timers.tick(150);
|
||||
|
||||
assert.equal(harness.getInput(), "\x1b[");
|
||||
} finally {
|
||||
harness.cleanup();
|
||||
mock.timers.reset();
|
||||
|
||||
Reference in New Issue
Block a user