Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
179bb5dc4e | ||
|
|
3ca1e5dc75 | ||
|
|
7579528c44 | ||
|
|
e85c1fea30 |
No files matched your search
@@ -149,3 +149,48 @@ test("normalize keeps custom (non-built-in) rules verbatim", () => {
|
||||
assert.deepEqual(rule.patterns, customRule.patterns);
|
||||
assert.equal(rule.label, customRule.label);
|
||||
});
|
||||
|
||||
test("Unix path highlighting covers terminal paths without matching relative prose or URL slashes", () => {
|
||||
const rule = getRule(DEFAULT_KEYWORD_HIGHLIGHT_RULES, "unix-path");
|
||||
for (const path of ["/sbin/iptables", "/var/log/syslog", "/usr/local/bin/g++", "~/projects/netcatty"]) {
|
||||
const matches = [...`'${path}'`.matchAll(new RegExp(rule.patterns[0], "gi"))];
|
||||
assert.deepEqual(matches.map((match) => match[0]), [path]);
|
||||
}
|
||||
for (const text of ["RELATED,ESTABLISHED", "TCP/IP", "https://example.com/log", " / ", "./foo", "../foo", "../../foo"]) {
|
||||
assert.equal(matchesAny(rule.patterns, text), false, text);
|
||||
}
|
||||
});
|
||||
|
||||
test("Unix path rule upgrades old settings while preserving disabled highlights and edits", () => {
|
||||
const legacy = DEFAULT_KEYWORD_HIGHLIGHT_RULES.filter((rule) => rule.id !== "unix-path");
|
||||
const upgraded = normalizeTerminalSettings({ keywordHighlightEnabled: false, keywordHighlightRules: legacy });
|
||||
assert.equal(upgraded.keywordHighlightEnabled, false);
|
||||
assert.equal(getRule(upgraded.keywordHighlightRules, "unix-path").enabled, true);
|
||||
|
||||
const edited = { ...getRule(upgraded.keywordHighlightRules, "unix-path"), color: "#123456", enabled: false, customized: true, patterns: ["/chosen/path"] };
|
||||
const restored = normalizeTerminalSettings({ keywordHighlightRules: [...legacy, edited] });
|
||||
assert.deepEqual(getRule(restored.keywordHighlightRules, "unix-path"), edited);
|
||||
});
|
||||
|
||||
test("Unix path highlighting keeps Unicode paths and quoted spaces together", () => {
|
||||
const rule = getRule(DEFAULT_KEYWORD_HIGHLIGHT_RULES, "unix-path");
|
||||
for (const [text, path] of [
|
||||
["/home/用户/file", "/home/用户/file"],
|
||||
['cat "/Users/me/My Projects/app"', "/Users/me/My Projects/app"],
|
||||
["cat '/home/用户/My Projects/app'", "/home/用户/My Projects/app"],
|
||||
]) {
|
||||
const matches = [...text.matchAll(new RegExp(rule.patterns[0], "gi"))];
|
||||
assert.deepEqual(matches.map((match) => match[0]), [path]);
|
||||
}
|
||||
});
|
||||
|
||||
test("Unix path highlighting keeps punctuation up to the text boundary", () => {
|
||||
const rule = getRule(DEFAULT_KEYWORD_HIGHLIGHT_RULES, "unix-path");
|
||||
for (const path of [
|
||||
"/run/user/1000/gvfs/smb-share:server=nas,share=files/report.txt",
|
||||
"/tmp/build=release/output",
|
||||
]) {
|
||||
const matches = [...path.matchAll(new RegExp(rule.patterns[0], "gi"))];
|
||||
assert.deepEqual(matches.map((match) => match[0]), [path]);
|
||||
}
|
||||
});
|
||||
@@ -288,6 +288,7 @@ export const DEFAULT_KEYWORD_HIGHLIGHT_RULES: KeywordHighlightRule[] = [
|
||||
{ id: 'info', label: 'Info', patterns: ['\\[info\\]', '\\[notice\\]', '\\[note\\]', '\\bnotice\\b', '\\bnote\\b'], color: '#3B82F6', enabled: true },
|
||||
{ id: 'debug', label: 'Debug', patterns: ['\\[debug\\]', '\\[trace\\]', '\\[verbose\\]', '\\bdebug\\b', '\\btrace\\b', '\\bverbose\\b'], color: '#A78BFA', enabled: true },
|
||||
{ id: 'ip-mac', label: 'URL, IP & MAC', patterns: [URL_HIGHLIGHT_PATTERN, IPV4_HIGHLIGHT_PATTERN, IPV6_HIGHLIGHT_PATTERN, MAC_ADDRESS_HIGHLIGHT_PATTERN], color: '#EC4899', enabled: true },
|
||||
{ id: 'unix-path', label: 'Unix paths', patterns: ["(?<=\")(?:/|~/)[^\"\\r\\n]+(?=\")|(?<=')(?:/|~/)[^'\\r\\n]+(?=')|(?<![\\w.:/\"'\\u0080-\\uFFFF])(?:/|~/)[^\\s\"']+"], color: '#A78BFA', enabled: true },
|
||||
];
|
||||
|
||||
const cloneKeywordHighlightRule = (rule: KeywordHighlightRule): KeywordHighlightRule => ({
|
||||
|
||||
@@ -548,6 +548,37 @@ function startPtyJob(ptyStream, command, options) {
|
||||
if (!text) return;
|
||||
if (!pendingEnd && !deliveringInput) armOutputTimeout();
|
||||
|
||||
// Interactive shells can emit bypass escape-sequence output while typed
|
||||
// input is still being delivered: PS0 / PROMPT_COMMAND hooks such as
|
||||
// systemd ≥258's "Hierarchical Context Signalling" (Ubuntu 25.10/26.04
|
||||
// ships /etc/profile.d/80-systemd-osc-context.sh) print OSC 3008 between
|
||||
// command submission and execution (issue #3676). If the payload's own
|
||||
// trailing newline ends up desynced against that bypass output, the
|
||||
// command sits on the input line without ever being committed. Only
|
||||
// OSC 3008 gates this — common decorative sequences (e.g. OSC 0 terminal
|
||||
// titles echoed by Ubuntu's default PS1) have always coexisted with
|
||||
// typed delivery and must not trigger the extra submit Enter below.
|
||||
// PTY data callbacks may split the escape sequence at any byte, so search
|
||||
// the retained suffix of the previous chunk together with this chunk;
|
||||
// `osc3008Tail` is reset at the start of every input delivery.
|
||||
if (deliveringInput) {
|
||||
const osc3008Needle = "\x1b]3008;";
|
||||
const combined = osc3008Tail + text;
|
||||
if (!inputDeliverySawOsc3008 && combined.includes(osc3008Needle)) {
|
||||
inputDeliverySawOsc3008 = true;
|
||||
}
|
||||
// Retain up to needle.length - 1 chars from the *combined* text so a
|
||||
// sequence split across any number of callbacks (not just two) still
|
||||
// reassembles; building the suffix from `text` alone would drop bytes
|
||||
// retained from earlier callbacks.
|
||||
osc3008Tail = combined.slice(-(osc3008Needle.length - 1));
|
||||
// Any PTY output while a recovery Enter is pending means the shell is
|
||||
// still streaming data — see the recovery note in writeInput.
|
||||
if (osc3008RecoveryPending) {
|
||||
osc3008RecoveryOutputSeen = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (probingShell) {
|
||||
probeOutput = (probeOutput + text).slice(-16384);
|
||||
if (cancelRequested && hasExpectedPromptSuffix(probeOutput, expectedPrompt)) {
|
||||
@@ -753,6 +784,15 @@ function startPtyJob(ptyStream, command, options) {
|
||||
let inputWriteTimer = null;
|
||||
let inputDrainListener = null;
|
||||
let inputWriteGeneration = 0;
|
||||
// Whether OSC 3008 context reporting landed in the PTY while the current
|
||||
// input delivery was still being typed (see the PS0/OSC 3008 note in onData).
|
||||
let inputDeliverySawOsc3008 = false;
|
||||
// Retained suffix of the last decoded PTY chunk, so an escape sequence split
|
||||
// across any number of data callbacks is still detected during typed delivery.
|
||||
let osc3008Tail = "";
|
||||
// Recovery Enter state: see armOsc3008RecoveryEnter in writeInput.
|
||||
let osc3008RecoveryPending = false;
|
||||
let osc3008RecoveryOutputSeen = false;
|
||||
function stopInputWrite() {
|
||||
inputWriteGeneration += 1;
|
||||
clearTimeout(inputWriteTimer);
|
||||
@@ -767,11 +807,69 @@ function startPtyJob(ptyStream, command, options) {
|
||||
// so paced typing time never consumes the startup budget.
|
||||
if (!finished && !cancelRequested && generation === inputWriteGeneration) {
|
||||
deliveringInput = false;
|
||||
osc3008RecoveryPending = false;
|
||||
if (!pendingEnd) armOutputTimeout();
|
||||
if (!foundStart) armStartupTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
// Recovery window pacing. QUIET is one input pacing window: the Enter is
|
||||
// only sent after the PTY has stayed silent that long. MAX bounds the whole
|
||||
// recovery so a continuously chatty PTY (e.g. a tail -f tab) cannot postpone
|
||||
// the recovery forever — the payload must still be submitted.
|
||||
const OSC3008_RECOVERY_QUIET_MS = 30;
|
||||
const OSC3008_RECOVERY_MAX_MS = 1000;
|
||||
function armOsc3008RecoveryEnter(generation, isCurrent) {
|
||||
// Recovery must wait for positive evidence rather than a blind 30ms
|
||||
// timeout: on sessions whose output return latency exceeds the pacing
|
||||
// window, the payload may already be committed with its _S start marker
|
||||
// still in flight, and an extra Enter then would be consumed by the
|
||||
// foreground command's stdin. A committed submit always produces output —
|
||||
// the echoed newline / redraw, further bypass OSC chunks, or the wrapper's
|
||||
// own _S marker — so PTY output while it is in flight is evidence the
|
||||
// submission is resolving on its own, and onData records that activity via
|
||||
// osc3008RecoveryOutputSeen. The Enter is sent only once the PTY has been
|
||||
// quiet for a full pacing window (nothing left to resolve), or when the
|
||||
// hard cap expires on a chatty PTY that never goes quiet. foundStart
|
||||
// always cancels the recovery immediately: the wrapper's own final newline
|
||||
// already committed the payload.
|
||||
osc3008RecoveryPending = true;
|
||||
osc3008RecoveryOutputSeen = false;
|
||||
const maxQuietRounds = Math.max(1, Math.ceil(OSC3008_RECOVERY_MAX_MS / OSC3008_RECOVERY_QUIET_MS));
|
||||
const writeRecoveryNewline = () => {
|
||||
osc3008RecoveryPending = false;
|
||||
try {
|
||||
ptyStream.write("\n");
|
||||
} catch (error) {
|
||||
finish(preStartOutput, -1, `Terminal input failed: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
completeInputDelivery(generation);
|
||||
};
|
||||
const attempt = (roundsLeft) => {
|
||||
inputWriteTimer = setTimeout(() => {
|
||||
if (!isCurrent()) {
|
||||
osc3008RecoveryPending = false;
|
||||
return;
|
||||
}
|
||||
inputWriteTimer = null;
|
||||
if (foundStart) {
|
||||
osc3008RecoveryPending = false;
|
||||
completeInputDelivery(generation);
|
||||
return;
|
||||
}
|
||||
if (osc3008RecoveryOutputSeen && roundsLeft > 0) {
|
||||
// Commit evidence is still in flight; wait for the next quiet window.
|
||||
osc3008RecoveryOutputSeen = false;
|
||||
attempt(roundsLeft - 1);
|
||||
return;
|
||||
}
|
||||
writeRecoveryNewline();
|
||||
}, OSC3008_RECOVERY_QUIET_MS);
|
||||
};
|
||||
attempt(maxQuietRounds);
|
||||
}
|
||||
|
||||
function writeInput(text) {
|
||||
stopInputWrite();
|
||||
// Each delivery gets a fresh wait budget, including the transition from
|
||||
@@ -779,6 +877,10 @@ function startPtyJob(ptyStream, command, options) {
|
||||
clearStartupTimeout();
|
||||
clearTimeout(timeoutId);
|
||||
deliveringInput = true;
|
||||
inputDeliverySawOsc3008 = false;
|
||||
osc3008Tail = "";
|
||||
osc3008RecoveryPending = false;
|
||||
osc3008RecoveryOutputSeen = false;
|
||||
const generation = inputWriteGeneration;
|
||||
|
||||
// Keep each write to one Unicode code point for strict bastions, while
|
||||
@@ -816,6 +918,15 @@ function startPtyJob(ptyStream, command, options) {
|
||||
}
|
||||
}
|
||||
if (offset < text.length) scheduleNext();
|
||||
else if (inputDeliverySawOsc3008) {
|
||||
// Bypass OSC 3008 output (PS0 / systemd context reporting, issue
|
||||
// #3676) landed while this payload was being typed, so its own
|
||||
// trailing newline may have been desynced and left the shell holding
|
||||
// the line uncommitted. Send one standalone Enter — as its own
|
||||
// write, kept out of the paced payload batch — so the typed command
|
||||
// is always submitted.
|
||||
armOsc3008RecoveryEnter(generation, isCurrent);
|
||||
}
|
||||
else completeInputDelivery(generation);
|
||||
} catch (error) {
|
||||
finish(preStartOutput, -1, `Terminal input failed: ${error.message}`);
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { EventEmitter } = require('node:events');
|
||||
const { startPtyJob } = require('./ptyExec.cjs');
|
||||
const { buildWrappedCommand } = require('./ptyExecHelpers.cjs');
|
||||
const { buildLiveShellProbe, buildLiveShellProbeCleanup } = require('./liveShellProbe.cjs');
|
||||
|
||||
// systemd >=258 (Ubuntu 25.10/26.04) ships a PS0/PROMPT_COMMAND hook that emits
|
||||
// OSC 3008 context sequences around every submitted command (issue #3676).
|
||||
// During paced typed delivery that bypass output can desync the payload's own
|
||||
// final newline and leave the command sitting on the input line uncommitted.
|
||||
// A delivery that observed OSC 3008 must be followed by one standalone submit
|
||||
// Enter written as its own write; quiet deliveries must not change shape.
|
||||
|
||||
const COMMAND = "echo " + "x".repeat(1200);
|
||||
const OSC_3008 = "\x1b]3008;start=uuid;type=command\x1b\\";
|
||||
|
||||
function makePty() {
|
||||
const pty = new EventEmitter();
|
||||
const writes = [];
|
||||
pty.write = (data) => { writes.push(String(data)); return true; };
|
||||
return { pty, writes };
|
||||
}
|
||||
|
||||
function expectedDeliveryLength(marker) {
|
||||
const probe = 2 + buildLiveShellProbe(marker).length;
|
||||
const wrapper = 2 + buildLiveShellProbeCleanup(marker).length + buildWrappedCommand(COMMAND, "posix", marker, true).length;
|
||||
return probe + wrapper;
|
||||
}
|
||||
|
||||
async function driveDelivery(t, pty, writes, marker, { osc, finalTick = true }) {
|
||||
const totalLength = expectedDeliveryLength(marker);
|
||||
// Short probe is delivered as a single write; reply to finish probing.
|
||||
t.mock.timers.tick(30);
|
||||
assert.ok(writes.length >= 1, "probe was written");
|
||||
pty.emit("data", `${marker}_P:sh\n${marker}_Q`);
|
||||
let delivered = false;
|
||||
for (let i = 0; i < 4000; i += 1) {
|
||||
t.mock.timers.tick(30);
|
||||
if (osc && i % 4 === 0) pty.emit("data", osc);
|
||||
if (writes.join("").length >= totalLength) { delivered = true; break; }
|
||||
}
|
||||
assert.ok(delivered, "wrapper delivery completed");
|
||||
if (finalTick) t.mock.timers.tick(60);
|
||||
}
|
||||
|
||||
test("delivery that observes OSC 3008 sends a standalone submit Enter (issue #3676)", async (t) => {
|
||||
t.mock.timers.enable({ apis: ["setTimeout"] });
|
||||
const { pty, writes } = makePty();
|
||||
const job = startPtyJob(pty, COMMAND, {
|
||||
shellKind: "posix", probeLiveShell: true, timeoutMs: 5000,
|
||||
});
|
||||
await driveDelivery(t, pty, writes, job.marker, { osc: OSC_3008 });
|
||||
assert.equal(writes[writes.length - 1], "\n", "a standalone Enter is written after the noisy delivery");
|
||||
assert.ok(writes[writes.length - 2].endsWith("\n"), "payload batch still ends with its own newline");
|
||||
pty.emit("data", `${job.marker}_S\nout\n${job.marker}_E:0\n`);
|
||||
assert.equal((await job.resultPromise).exitCode, 0);
|
||||
pty.emit("close");
|
||||
t.mock.timers.reset();
|
||||
});
|
||||
|
||||
test("delivery without OSC 3008 noise does not gain an extra standalone Enter", async (t) => {
|
||||
t.mock.timers.enable({ apis: ["setTimeout"] });
|
||||
const { pty, writes } = makePty();
|
||||
const job = startPtyJob(pty, COMMAND, {
|
||||
shellKind: "posix", probeLiveShell: true, timeoutMs: 5000,
|
||||
});
|
||||
await driveDelivery(t, pty, writes, job.marker, { osc: null });
|
||||
const payload = writes[writes.length - 1];
|
||||
assert.notEqual(payload, "\n", "no standalone Enter is written on quiet delivery");
|
||||
assert.ok(payload.endsWith("\n"), "wrapper payload still ends with its own newline");
|
||||
pty.emit("data", `${job.marker}_S\nout\n${job.marker}_E:0\n`);
|
||||
assert.equal((await job.resultPromise).exitCode, 0);
|
||||
pty.emit("close");
|
||||
t.mock.timers.reset();
|
||||
});
|
||||
|
||||
test("decorative title OSC noise does not trigger the standalone Enter", async (t) => {
|
||||
t.mock.timers.enable({ apis: ["setTimeout"] });
|
||||
const { pty, writes } = makePty();
|
||||
const job = startPtyJob(pty, COMMAND, {
|
||||
shellKind: "posix", probeLiveShell: true, timeoutMs: 5000,
|
||||
});
|
||||
await driveDelivery(t, pty, writes, job.marker, { osc: "\x1b]0;user@host: /tmp\x07" });
|
||||
assert.notEqual(writes[writes.length - 1], "\n", "title-only noise must not add a standalone Enter");
|
||||
pty.emit("data", `${job.marker}_S\nout\n${job.marker}_E:0\n`);
|
||||
assert.equal((await job.resultPromise).exitCode, 0);
|
||||
pty.emit("close");
|
||||
t.mock.timers.reset();
|
||||
});
|
||||
|
||||
test("start marker arriving during the recovery delay suppresses the extra Enter", async (t) => {
|
||||
t.mock.timers.enable({ apis: ["setTimeout"] });
|
||||
const { pty, writes } = makePty();
|
||||
const job = startPtyJob(pty, COMMAND, {
|
||||
shellKind: "posix", probeLiveShell: true, timeoutMs: 5000,
|
||||
});
|
||||
// Skip the final tick so the recovery Enter is still pending when the marker
|
||||
// arrives; it must then be withheld instead of reaching the command's stdin.
|
||||
await driveDelivery(t, pty, writes, job.marker, { osc: OSC_3008, finalTick: false });
|
||||
pty.emit("data", `${job.marker}_S\nout\n${job.marker}_E:0\n`);
|
||||
assert.ok(writes[writes.length - 1].length > 1, "no standalone Enter was written after foundStart");
|
||||
assert.equal((await job.resultPromise).exitCode, 0);
|
||||
pty.emit("close");
|
||||
t.mock.timers.reset();
|
||||
});
|
||||
|
||||
test("OSC 3008 split across PTY chunks still triggers the standalone Enter", async (t) => {
|
||||
t.mock.timers.enable({ apis: ["setTimeout"] });
|
||||
const { pty, writes } = makePty();
|
||||
const job = startPtyJob(pty, COMMAND, {
|
||||
shellKind: "posix", probeLiveShell: true, timeoutMs: 5000,
|
||||
});
|
||||
const totalLength = expectedDeliveryLength(job.marker);
|
||||
t.mock.timers.tick(30);
|
||||
pty.emit("data", `${job.marker}_P:sh\n${job.marker}_Q`);
|
||||
let delivered = false;
|
||||
const half = Math.ceil(OSC_3008.length / 2);
|
||||
for (let i = 0; i < 4000; i += 1) {
|
||||
t.mock.timers.tick(30);
|
||||
if (i % 4 === 0) pty.emit("data", OSC_3008.slice(0, half));
|
||||
if (i % 4 === 1) pty.emit("data", OSC_3008.slice(half));
|
||||
if (writes.join("").length >= totalLength) { delivered = true; break; }
|
||||
}
|
||||
assert.ok(delivered, "wrapper delivery completed");
|
||||
t.mock.timers.tick(60);
|
||||
assert.equal(writes[writes.length - 1], "\n", "a split OSC 3008 sequence still latches the flag");
|
||||
pty.emit("data", `${job.marker}_S\nout\n${job.marker}_E:0\n`);
|
||||
assert.equal((await job.resultPromise).exitCode, 0);
|
||||
pty.emit("close");
|
||||
t.mock.timers.reset();
|
||||
});
|
||||
|
||||
test("OSC 3008 split across three PTY chunks still triggers the standalone Enter", async (t) => {
|
||||
t.mock.timers.enable({ apis: ["setTimeout"] });
|
||||
const { pty, writes } = makePty();
|
||||
const job = startPtyJob(pty, COMMAND, {
|
||||
shellKind: "posix", probeLiveShell: true, timeoutMs: 5000,
|
||||
});
|
||||
const totalLength = expectedDeliveryLength(job.marker);
|
||||
t.mock.timers.tick(30);
|
||||
pty.emit("data", `${job.marker}_P:sh\n${job.marker}_Q`);
|
||||
let delivered = false;
|
||||
// "\x1b]", "30" and "08;..." — the constant "\x1b]3008;" prefix itself
|
||||
// spans three data callbacks; the retained suffix must survive every hop.
|
||||
const three = [OSC_3008.slice(0, 2), OSC_3008.slice(2, 4), OSC_3008.slice(4)];
|
||||
for (let i = 0; i < 4000; i += 1) {
|
||||
t.mock.timers.tick(30);
|
||||
if (i % 6 === 0) pty.emit("data", three[0]);
|
||||
if (i % 6 === 1) pty.emit("data", three[1]);
|
||||
if (i % 6 === 2) pty.emit("data", three[2]);
|
||||
if (writes.join("").length >= totalLength) { delivered = true; break; }
|
||||
}
|
||||
assert.ok(delivered, "wrapper delivery completed");
|
||||
// Stop emitting: the first quiet recovery window (after the chunk that
|
||||
// landed alongside delivery completion is drained) must send the Enter.
|
||||
let enterSent = false;
|
||||
for (let i = 0; i < 10 && !enterSent; i += 1) {
|
||||
t.mock.timers.tick(30);
|
||||
enterSent = writes[writes.length - 1] === "\n";
|
||||
}
|
||||
assert.ok(enterSent, "a three-chunk split OSC 3008 sequence still latches the flag");
|
||||
pty.emit("data", `${job.marker}_S\nout\n${job.marker}_E:0\n`);
|
||||
assert.equal((await job.resultPromise).exitCode, 0);
|
||||
pty.emit("close");
|
||||
t.mock.timers.reset();
|
||||
});
|
||||
|
||||
test("PTY output during the recovery window postpones the standalone Enter", async (t) => {
|
||||
t.mock.timers.enable({ apis: ["setTimeout"] });
|
||||
const { pty, writes } = makePty();
|
||||
const job = startPtyJob(pty, COMMAND, {
|
||||
shellKind: "posix", probeLiveShell: true, timeoutMs: 5000,
|
||||
});
|
||||
await driveDelivery(t, pty, writes, job.marker, { osc: OSC_3008, finalTick: false });
|
||||
// Output keeps flowing after the payload finished (echo of the submit /
|
||||
// further OSC chunks): the recovery must not fire while it streams in.
|
||||
pty.emit("data", OSC_3008);
|
||||
t.mock.timers.tick(30);
|
||||
assert.notEqual(writes[writes.length - 1], "\n", "output in flight postpones the Enter");
|
||||
// One full quiet pacing window without any PTY output: the submission is
|
||||
// not resolving on its own, so the recovery Enter is justified.
|
||||
t.mock.timers.tick(30);
|
||||
assert.equal(writes[writes.length - 1], "\n", "quiet window sends the recovery Enter");
|
||||
pty.emit("data", `${job.marker}_S\nout\n${job.marker}_E:0\n`);
|
||||
assert.equal((await job.resultPromise).exitCode, 0);
|
||||
pty.emit("close");
|
||||
t.mock.timers.reset();
|
||||
});
|
||||
|
||||
test("a continuously chatty PTY cannot postpone the recovery Enter past the cap", async (t) => {
|
||||
t.mock.timers.enable({ apis: ["setTimeout"] });
|
||||
const { pty, writes } = makePty();
|
||||
const job = startPtyJob(pty, COMMAND, {
|
||||
shellKind: "posix", probeLiveShell: true, timeoutMs: 5000,
|
||||
});
|
||||
await driveDelivery(t, pty, writes, job.marker, { osc: OSC_3008, finalTick: false });
|
||||
// Output arriving during every recovery window: the cap (~1s) must still
|
||||
// submit the payload eventually.
|
||||
let enterSent = false;
|
||||
for (let i = 0; i < 60 && !enterSent; i += 1) {
|
||||
t.mock.timers.tick(15);
|
||||
pty.emit("data", OSC_3008);
|
||||
t.mock.timers.tick(15);
|
||||
enterSent = writes[writes.length - 1] === "\n";
|
||||
}
|
||||
assert.ok(enterSent, "the recovery Enter is sent despite ongoing PTY output");
|
||||
pty.emit("data", `${job.marker}_S\nout\n${job.marker}_E:0\n`);
|
||||
assert.equal((await job.resultPromise).exitCode, 0);
|
||||
pty.emit("close");
|
||||
t.mock.timers.reset();
|
||||
});
|
||||
Reference in new issue
Block a user