import assert from "node:assert/strict"; import test from "node:test"; import type { Terminal as XTerm } from "@xterm/xterm"; import { createOutputFlowController } from "./outputFlowController.ts"; import { armTerminalInterruptDisplayGate, filterTerminalInterruptDisplayOutput, prioritizeTerminalInput, releaseTerminalFlowOutputForTerm, teardownTerminalOutputPipeline, } from "./terminalOutputPipeline.ts"; import { FLOW_LOW_WATER_MARK } from "./terminalFlowConstants.ts"; import { enqueueCoalescedTerminalWrite, flushTerminalWriteCoalescer, getTerminalWriteCoalescerPendingBytes, resetTerminalWriteCoalescer, } from "./terminalWriteCoalescer.ts"; import { enqueueTerminalWrite, getTerminalWriteQueueDepth, } from "./terminalWriteQueue.ts"; import { accumulateDeferredTerminalWriteAck } from "./terminalWriteAckDeferral.ts"; import { clearTerminalSessionFlowAck } from "./terminalFlowAckBuffer.ts"; const createFakeTerm = () => ({}) as XTerm; test("teardownTerminalOutputPipeline resumes renderer pause and clears backlog", () => { const term = createFakeTerm(); const events: string[] = []; const flow = createOutputFlowController({ highWaterMark: 50, lowWaterMark: 10, onPause: () => events.push("pause"), onResume: () => events.push("resume"), }); const backend = { setSessionFlowPaused: (_sessionId: string, paused: boolean) => { events.push(paused ? "ipc-pause" : "ipc-resume"); }, ackSessionFlow: () => {}, }; flow.received(60); enqueueTerminalWrite(term, 20, (done) => done()); teardownTerminalOutputPipeline( { terminalBackend: backend, sessionRef: { current: "sess-1" } } as never, term, "sess-1", flow, ); assert.deepEqual(events, ["pause", "resume", "ipc-resume"]); }); test("releaseTerminalFlowOutputForTerm resumes renderer pause without a flow controller", () => { const term = createFakeTerm(); const events: string[] = []; const backend = { setSessionFlowPaused: (_sessionId: string, paused: boolean) => { events.push(paused ? "ipc-pause" : "ipc-resume"); }, ackSessionFlow: () => {}, }; releaseTerminalFlowOutputForTerm(term, backend, "sess-1", undefined); assert.deepEqual(events, ["ipc-resume"]); }); test("ordinary input priority preserves queued display output", () => { const term = createFakeTerm(); const acked: number[] = []; const deferred: Array<() => void> = []; let completed = 0; const flow = createOutputFlowController({ highWaterMark: 100, lowWaterMark: 20, onPause: () => {}, onResume: () => {}, }); const backend = { setSessionFlowPaused: () => {}, ackSessionFlow: (_sessionId: string, bytes: number) => { acked.push(bytes); }, }; flow.received(FLOW_LOW_WATER_MARK + 80); enqueueTerminalWrite(term, 50, () => {}); enqueueTerminalWrite(term, 30, (done) => { completed += 1; done(); }); const priority = prioritizeTerminalInput( term, "sess-1", flow, backend, (callback: () => void) => deferred.push(callback), ); assert.equal(priority.scheduledBackendResume, false); assert.equal(priority.ackAfterInputBytes, 0); assert.equal(getTerminalWriteQueueDepth(term), 1); assert.deepEqual(acked, []); assert.deepEqual(deferred, []); assert.equal(completed, 0); }); test("prioritizeTerminalInput flushes deferred xterm write ack bytes", () => { clearTerminalSessionFlowAck("sess-1"); const term = createFakeTerm(); const acked: number[] = []; const deferred: Array<() => void> = []; const flow = createOutputFlowController({ highWaterMark: 100, lowWaterMark: 20, onPause: () => {}, onResume: () => {}, }); const backend = { ackSessionFlow: (_sessionId: string, bytes: number) => { acked.push(bytes); }, setSessionFlowPaused: () => {}, }; accumulateDeferredTerminalWriteAck(term, 42); prioritizeTerminalInput( term, "sess-1", flow, backend, (callback: () => void) => deferred.push(callback), ); assert.deepEqual(acked, []); deferred[0]!(); assert.deepEqual(acked, [42]); assert.equal(flow.pendingBytes(), 0); clearTerminalSessionFlowAck("sess-1"); }); test("ordinary input priority leaves queued backlog intact", () => { const term = createFakeTerm(); const events: string[] = []; const deferred: Array<() => void> = []; const flow = createOutputFlowController({ highWaterMark: 100, lowWaterMark: 20, onPause: () => events.push("pause"), onResume: () => events.push("resume"), }); const backend = { setSessionFlowPaused: (_sessionId: string, paused: boolean) => { events.push(paused ? "ipc-pause" : "ipc-resume"); }, ackSessionFlow: () => {}, }; flow.received(FLOW_LOW_WATER_MARK + 1024); let release: (() => void) | null = null; enqueueTerminalWrite(term, 30, (done) => { release = done; }); const priority = prioritizeTerminalInput( term, "sess-1", flow, backend, (callback: () => void) => deferred.push(callback), ); assert.equal(priority.scheduledBackendResume, false); assert.equal(priority.writeQueueDepth, 0); assert.equal(getTerminalWriteQueueDepth(term), 0); assert.equal(events.includes("ipc-resume"), false); assert.deepEqual(deferred, []); events.push("input-forwarded"); release?.(); assert.deepEqual(events, ["pause", "input-forwarded"]); }); test("ordinary input priority does not drop queued bytes or resume paused output", () => { const term = createFakeTerm(); const events: string[] = []; const deferred: Array<() => void> = []; const flow = createOutputFlowController({ highWaterMark: 100, lowWaterMark: 20, onPause: () => events.push("pause"), onResume: () => events.push("resume"), }); const backend = { setSessionFlowPaused: (_sessionId: string, paused: boolean) => { events.push(paused ? "ipc-pause" : "ipc-resume"); }, ackSessionFlow: () => {}, }; flow.received(110); enqueueTerminalWrite(term, 10, () => {}); enqueueTerminalWrite(term, 100, () => {}); const priority = prioritizeTerminalInput( term, "sess-1", flow, backend, (callback: () => void) => deferred.push(callback), ); assert.equal(priority.scheduledBackendResume, false); assert.equal(priority.ackAfterInputBytes, 0); assert.deepEqual(events, ["pause"]); assert.equal(getTerminalWriteQueueDepth(term), 1); events.push("input-forwarded"); assert.deepEqual(deferred, []); assert.deepEqual(events, ["pause", "input-forwarded"]); }); test("ordinary input priority preserves a pending coalesced backlog above the pressure threshold", () => { clearTerminalSessionFlowAck("sess-1"); const scheduler = globalThis as typeof globalThis & { requestAnimationFrame?: (callback: FrameRequestCallback) => number; cancelAnimationFrame?: (handle: number) => void; }; const originalRequestAnimationFrame = scheduler.requestAnimationFrame; const originalCancelAnimationFrame = scheduler.cancelAnimationFrame; scheduler.requestAnimationFrame = () => 1; scheduler.cancelAnimationFrame = () => {}; const term = createFakeTerm(); const payload = "x".repeat(FLOW_LOW_WATER_MARK + 1024); const written: string[] = []; const events: string[] = []; const deferred: Array<() => void> = []; const flow = createOutputFlowController({ highWaterMark: 100, lowWaterMark: 20, onPause: () => events.push("pause"), onResume: () => events.push("resume"), }); const backend = { setSessionFlowPaused: (_sessionId: string, paused: boolean) => { events.push(paused ? "ipc-pause" : "ipc-resume"); }, ackSessionFlow: () => {}, }; try { flow.received(payload.length); enqueueCoalescedTerminalWrite( term, payload, (data, ingressBytes) => { written.push(data); flow.written(ingressBytes); }, payload.length, ); const priority = prioritizeTerminalInput( term, "sess-1", flow, backend, (callback: () => void) => deferred.push(callback), ); assert.equal(priority.scheduledBackendResume, false); assert.equal(flow.isPaused(), true); assert.deepEqual(events, ["pause"]); assert.deepEqual(deferred, []); assert.equal(getTerminalWriteCoalescerPendingBytes(term), payload.length); events.push("input-forwarded"); flushTerminalWriteCoalescer(term); assert.equal(written.join(""), payload); // Payload is under the Tabby-sized unbroken shard (~128KB), so flush is one write. assert.equal(written.length, 1); assert.equal(flow.pendingBytes(), 0); } finally { resetTerminalWriteCoalescer(term); scheduler.requestAnimationFrame = originalRequestAnimationFrame; scheduler.cancelAnimationFrame = originalCancelAnimationFrame; clearTerminalSessionFlowAck("sess-1"); } }); test("ordinary input priority preserves pending line-edit echo when flushing deferred acks", () => { clearTerminalSessionFlowAck("sess-edit"); const scheduler = globalThis as typeof globalThis & { requestAnimationFrame?: (callback: FrameRequestCallback) => number; cancelAnimationFrame?: (handle: number) => void; }; const originalRequestAnimationFrame = scheduler.requestAnimationFrame; const originalCancelAnimationFrame = scheduler.cancelAnimationFrame; scheduler.requestAnimationFrame = () => 1; scheduler.cancelAnimationFrame = () => {}; const term = createFakeTerm(); const echoedHistoryCommand = "systemctl dddd"; const written: string[] = []; const acked: number[] = []; const deferred: Array<() => void> = []; const flow = createOutputFlowController({ highWaterMark: 100, lowWaterMark: 20, onPause: () => {}, onResume: () => {}, }); const backend = { ackSessionFlow: (_sessionId: string, bytes: number) => { acked.push(bytes); }, setSessionFlowPaused: () => {}, }; try { accumulateDeferredTerminalWriteAck(term, 2); flow.received(echoedHistoryCommand.length); enqueueCoalescedTerminalWrite( term, echoedHistoryCommand, (data, ingressBytes) => { written.push(data); flow.written(ingressBytes); }, echoedHistoryCommand.length, ); assert.equal(getTerminalWriteCoalescerPendingBytes(term), echoedHistoryCommand.length); const priority = prioritizeTerminalInput( term, "sess-edit", flow, backend, (callback: () => void) => deferred.push(callback), { reason: "input" }, ); assert.equal(priority.ackAfterInputBytes, 2); assert.equal(flow.pendingBytes(), echoedHistoryCommand.length); assert.equal(getTerminalWriteCoalescerPendingBytes(term), echoedHistoryCommand.length); deferred[0]!(); assert.deepEqual(acked, [2]); flushTerminalWriteCoalescer(term); assert.deepEqual(written, [echoedHistoryCommand]); assert.equal(flow.pendingBytes(), 0); } finally { resetTerminalWriteCoalescer(term); scheduler.requestAnimationFrame = originalRequestAnimationFrame; scheduler.cancelAnimationFrame = originalCancelAnimationFrame; clearTerminalSessionFlowAck("sess-edit"); } }); test("ordinary input priority keeps queued visible output intact", () => { clearTerminalSessionFlowAck("sess-input"); const term = createFakeTerm(); const acked: number[] = []; const deferred: Array<() => void> = []; const order: string[] = []; const flow = createOutputFlowController({ highWaterMark: 100, lowWaterMark: 20, onPause: () => {}, onResume: () => {}, }); const backend = { ackSessionFlow: (_sessionId: string, bytes: number) => { acked.push(bytes); }, setSessionFlowPaused: () => {}, }; flow.received(FLOW_LOW_WATER_MARK + 200); let releaseFirst: (() => void) | null = null; enqueueTerminalWrite(term, 20, (done) => { order.push("first"); releaseFirst = done; }); enqueueTerminalWrite(term, 30, (done) => { order.push("second"); done(); }); accumulateDeferredTerminalWriteAck(term, 7); const priority = prioritizeTerminalInput( term, "sess-input", flow, backend, (callback: () => void) => deferred.push(callback), { reason: "input" }, ); assert.equal(priority.ackAfterInputBytes, 7); assert.equal(priority.writeQueueDepth, 1); assert.equal(flow.pendingBytes(), FLOW_LOW_WATER_MARK + 200); assert.deepEqual(order, ["first"]); deferred[0]!(); assert.deepEqual(acked, [7]); releaseFirst?.(); assert.deepEqual(order, ["first", "second"]); clearTerminalSessionFlowAck("sess-input"); }); test("interrupt priority drains queued display output for ssh-like interrupts", () => { const term = createFakeTerm(); const events: string[] = []; const backend = { ackSessionFlow: (_sessionId: string, bytes: number) => { events.push(`ack:${bytes}`); }, setSessionFlowPaused: (_sessionId: string, paused: boolean) => { events.push(paused ? "ipc-pause" : "ipc-resume"); }, }; const flow = createOutputFlowController({ highWaterMark: 100, lowWaterMark: 20, onPause: () => events.push("pause"), onResume: () => {}, }); flow.received(FLOW_LOW_WATER_MARK + 1024); enqueueTerminalWrite(term, 20, () => {}); enqueueTerminalWrite(term, 30, () => {}); const priority = prioritizeTerminalInput( term, "sess-1", flow, backend, (callback: () => void) => { events.push("input-forwarded"); callback(); }, { reason: "interrupt", drainStaleOutput: true, now: 4000, quietMs: 100, promptQuietMs: 80, maxDrainMs: 1000, }, ); assert.equal(priority.skippedReason, undefined); assert.equal(priority.scheduledBackendResume, true); assert.equal(priority.ackAfterInputBytes, 30); assert.equal(priority.writeQueueDepth, 1); assert.equal(flow.pendingBytes(), 0); assert.deepEqual(events, ["pause", "input-forwarded", "ack:30", "ipc-resume"]); assert.deepEqual( filterTerminalInterruptDisplayOutput(term, "old output", { now: 4001 }), { accepted: false, data: "", droppedBytes: 10, reason: "draining" }, ); assert.deepEqual( filterTerminalInterruptDisplayOutput(term, "more old^C\r\n$ ", { now: 4002 }), { accepted: true, data: "^C\r\n$ ", droppedBytes: 8, reason: "interrupt-echo" }, ); }); test("interrupt display drain preserves alternate-screen exit controls", () => { const term = createFakeTerm(); const backend = { ackSessionFlow: () => {}, setSessionFlowPaused: () => {}, }; const flow = createOutputFlowController({ highWaterMark: 100, lowWaterMark: 20, onPause: () => {}, onResume: () => {}, }); flow.received(FLOW_LOW_WATER_MARK + 1); prioritizeTerminalInput( term, "sess-1", flow, backend, (callback: () => void) => callback(), { reason: "interrupt", drainStaleOutput: true, now: 6100, quietMs: 500, promptQuietMs: 80, maxDrainMs: 1000, }, ); assert.deepEqual( filterTerminalInterruptDisplayOutput(term, "stale frame\x1b[?1049l", { now: 6101 }), { accepted: true, data: "\x1b[?1049l", droppedBytes: "stale frame".length, reason: "draining", }, ); assert.deepEqual( filterTerminalInterruptDisplayOutput(term, "stale frame\x1b[?1049l\r\n$ ", { now: 6200 }), { accepted: true, data: "\x1b[?1049l$ ", droppedBytes: "stale frame\r\n".length, reason: "prompt-gap", }, ); }); test("interrupt display drain excludes preserved restore from held password prefixes", () => { const term = createFakeTerm(); armTerminalInterruptDisplayGate(term, { now: 6250, quietMs: 500, promptQuietMs: 80, maxDrainMs: 2500, }); assert.deepEqual( filterTerminalInterruptDisplayOutput(term, "stale\n\x1b[?1049lPass", { now: 6251 }), { accepted: true, data: "\x1b[?1049l", droppedBytes: "stale\n".length, reason: "draining", }, ); assert.deepEqual( filterTerminalInterruptDisplayOutput(term, "word: ", { now: 6252 }), { accepted: true, data: "Password: ", droppedBytes: 0, reason: "prompt-gap", }, ); }); test("interrupt display drain excludes last of multiple restores from password prefixes", () => { const term = createFakeTerm(); armTerminalInterruptDisplayGate(term, { now: 6255, quietMs: 500, promptQuietMs: 80, maxDrainMs: 2500, }); assert.deepEqual( filterTerminalInterruptDisplayOutput(term, "\x1b[?25hstale\n\x1b[?1049lPass", { now: 6256 }), { accepted: true, data: "\x1b[?25h\x1b[?1049l", droppedBytes: "stale\n".length, reason: "draining", }, ); assert.deepEqual( filterTerminalInterruptDisplayOutput(term, "word: ", { now: 6257 }), { accepted: true, data: "Password: ", droppedBytes: 0, reason: "prompt-gap", }, ); }); test("interrupt display drain does not peel restore suffix from later password prefixes", () => { const term = createFakeTerm(); armTerminalInterruptDisplayGate(term, { now: 6260, quietMs: 500, promptQuietMs: 80, maxDrainMs: 2500, }); assert.deepEqual( filterTerminalInterruptDisplayOutput(term, "\x1b[?1049l\nlogin pass", { now: 6261 }), { accepted: true, data: "\x1b[?1049l", droppedBytes: 1, reason: "draining", }, ); assert.deepEqual( filterTerminalInterruptDisplayOutput(term, "word: ", { now: 6262 }), { accepted: true, data: "login password: ", droppedBytes: 0, reason: "prompt-gap", }, ); }); test("interrupt display drain does not leak incomplete SGR CSI into shell prompts", () => { const term = createFakeTerm(); armTerminalInterruptDisplayGate(term, { now: 6270, quietMs: 500, promptQuietMs: 80, maxDrainMs: 2500, }); assert.deepEqual( filterTerminalInterruptDisplayOutput(term, "old\x1b[31", { now: 6271 }), { accepted: false, data: "", droppedBytes: "old\x1b[31".length, reason: "draining", }, ); assert.deepEqual( filterTerminalInterruptDisplayOutput(term, "$ ", { now: 6400 }), { accepted: true, data: "$ ", droppedBytes: 0, reason: "prompt-gap", }, ); }); test("interrupt display drain preserves split alternate-screen exit controls", () => { clearTerminalSessionFlowAck("sess-1"); const term = createFakeTerm(); const backend = { ackSessionFlow: () => {}, setSessionFlowPaused: () => {}, }; const flow = createOutputFlowController({ highWaterMark: 100, lowWaterMark: 20, onPause: () => {}, onResume: () => {}, }); flow.received(FLOW_LOW_WATER_MARK + 1); prioritizeTerminalInput( term, "sess-1", flow, backend, (callback: () => void) => callback(), { reason: "interrupt", drainStaleOutput: true, now: 7100, quietMs: 500, promptQuietMs: 80, maxDrainMs: 1000, }, ); assert.deepEqual( filterTerminalInterruptDisplayOutput(term, "stale frame\x1b[?104", { now: 7101 }), { accepted: false, data: "", droppedBytes: "stale frame".length, reason: "draining", }, ); assert.deepEqual( filterTerminalInterruptDisplayOutput(term, "9l^C\r\n$ ", { now: 7102 }), { accepted: true, data: "\x1b[?1049l^C\r\n$ ", droppedBytes: 0, reason: "interrupt-echo", }, ); }); test("interrupt display drain preserves restore controls on split caret echo", () => { clearTerminalSessionFlowAck("sess-1"); const term = createFakeTerm(); const backend = { ackSessionFlow: () => {}, setSessionFlowPaused: () => {}, }; const flow = createOutputFlowController({ highWaterMark: 100, lowWaterMark: 20, onPause: () => {}, onResume: () => {}, }); flow.received(FLOW_LOW_WATER_MARK + 1); prioritizeTerminalInput( term, "sess-1", flow, backend, (callback: () => void) => callback(), { reason: "interrupt", drainStaleOutput: true, now: 7150, quietMs: 500, promptQuietMs: 80, maxDrainMs: 1000, }, ); assert.deepEqual( filterTerminalInterruptDisplayOutput(term, "stale frame\x1b[?104", { now: 7151 }), { accepted: false, data: "", droppedBytes: "stale frame".length, reason: "draining", }, ); assert.deepEqual( filterTerminalInterruptDisplayOutput(term, "9l^", { now: 7152 }), { accepted: true, data: "\x1b[?1049l", droppedBytes: 1, reason: "draining", }, ); assert.deepEqual( filterTerminalInterruptDisplayOutput(term, "C\r\n$ ", { now: 7153 }), { accepted: true, data: "^C\r\n$ ", droppedBytes: 0, acceptedBytes: 5, reason: "interrupt-echo", }, ); }); test("interrupt display drain does not preserve unsafe combined private modes", () => { clearTerminalSessionFlowAck("sess-1"); const term = createFakeTerm(); const backend = { ackSessionFlow: () => {}, setSessionFlowPaused: () => {}, }; const flow = createOutputFlowController({ highWaterMark: 100, lowWaterMark: 20, onPause: () => {}, onResume: () => {}, }); flow.received(FLOW_LOW_WATER_MARK + 1); prioritizeTerminalInput( term, "sess-1", flow, backend, (callback: () => void) => callback(), { reason: "interrupt", drainStaleOutput: true, now: 7200, quietMs: 500, promptQuietMs: 80, maxDrainMs: 1000, }, ); const unsafeSequence = "\x1b[?1049;25h"; assert.deepEqual( filterTerminalInterruptDisplayOutput(term, `stale frame${unsafeSequence}^C\r\n$ `, { now: 7201, }), { accepted: true, data: "^C\r\n$ ", droppedBytes: "stale frame".length + unsafeSequence.length, reason: "interrupt-echo", }, ); }); test("interrupt display drain accepts prompt candidates with OSC title and spaces", () => { clearTerminalSessionFlowAck("sess-1"); const term = createFakeTerm(); const backend = { ackSessionFlow: () => {}, setSessionFlowPaused: () => {}, }; const flow = createOutputFlowController({ highWaterMark: 100, lowWaterMark: 20, onPause: () => {}, onResume: () => {}, }); flow.received(FLOW_LOW_WATER_MARK + 1); prioritizeTerminalInput( term, "sess-1", flow, backend, (callback: () => void) => callback(), { reason: "interrupt", drainStaleOutput: true, now: 7300, quietMs: 500, promptQuietMs: 80, maxDrainMs: 1000, }, ); assert.equal( filterTerminalInterruptDisplayOutput(term, "old output", { now: 7301 }).accepted, false, ); const prompt = "\x1b]0;~/My Project\x07~/My Project$ "; assert.deepEqual( filterTerminalInterruptDisplayOutput(term, prompt, { now: 7400 }), { accepted: true, data: prompt, droppedBytes: 0, reason: "prompt-gap", }, ); }); test("interrupt display drain accepts split OSC prompt candidates", () => { clearTerminalSessionFlowAck("sess-1"); const term = createFakeTerm(); const backend = { ackSessionFlow: () => {}, setSessionFlowPaused: () => {}, }; const flow = createOutputFlowController({ highWaterMark: 100, lowWaterMark: 20, onPause: () => {}, onResume: () => {}, }); flow.received(FLOW_LOW_WATER_MARK + 1); prioritizeTerminalInput( term, "sess-1", flow, backend, (callback: () => void) => callback(), { reason: "interrupt", drainStaleOutput: true, now: 7500, quietMs: 500, promptQuietMs: 80, maxDrainMs: 1000, }, ); assert.deepEqual( filterTerminalInterruptDisplayOutput(term, "old output\x1b]0;~/My ", { now: 7501 }), { accepted: false, data: "", droppedBytes: "old output".length, reason: "draining", }, ); assert.deepEqual( filterTerminalInterruptDisplayOutput(term, "Project\x07~/My Project$ ", { now: 7600 }), { accepted: true, data: "\x1b]0;~/My Project\x07~/My Project$ ", droppedBytes: 0, reason: "prompt-gap", }, ); }); test("interrupt priority skips stale display drain but still flushes deferred acks", () => { clearTerminalSessionFlowAck("sess-deferred"); const term = createFakeTerm(); const acked: number[] = []; const deferred: Array<() => void> = []; const backend = { ackSessionFlow: (_sessionId: string, bytes: number) => { acked.push(bytes); }, setSessionFlowPaused: () => {}, }; accumulateDeferredTerminalWriteAck(term, 42); const priority = prioritizeTerminalInput( term, "sess-deferred", undefined, backend, (callback: () => void) => deferred.push(callback), { reason: "interrupt", drainStaleOutput: false, now: 5000, quietMs: 500, promptQuietMs: 80, maxDrainMs: 100, }, ); assert.equal(priority.skippedReason, undefined); assert.equal(priority.deferredAckBytes, 42); assert.equal(priority.scheduledBackendResume, true); assert.equal(priority.ackAfterInputBytes, 42); assert.equal(deferred.length, 1); deferred[0]!(); assert.deepEqual(acked, [42]); assert.deepEqual( filterTerminalInterruptDisplayOutput(term, "KeyboardInterrupt\r\n$ ", { now: 5001 }), { accepted: true, data: "KeyboardInterrupt\r\n$ ", droppedBytes: 0, reason: "inactive", }, ); clearTerminalSessionFlowAck("sess-deferred"); }); test("interrupt priority leaves display output alone below pressure threshold", () => { clearTerminalSessionFlowAck("sess-low-pressure"); const term = createFakeTerm(); const flow = createOutputFlowController({ highWaterMark: 100, lowWaterMark: 20, onPause: () => {}, onResume: () => {}, }); const backend = { ackSessionFlow: () => {}, setSessionFlowPaused: () => {}, }; flow.received(119); const priority = prioritizeTerminalInput( term, "sess-low-pressure", flow, backend, (callback: () => void) => callback(), { reason: "interrupt", drainStaleOutput: true, now: 7800, }, ); assert.equal(priority.skippedReason, "below-threshold"); assert.deepEqual( filterTerminalInterruptDisplayOutput( term, "Type :qa and press to exit Vim", { now: 7801 }, ), { accepted: true, data: "Type :qa and press to exit Vim", droppedBytes: 0, reason: "inactive", }, ); clearTerminalSessionFlowAck("sess-low-pressure"); }); test("interrupt display drain accepts a sudo password prompt (#2010)", () => { const term = createFakeTerm(); armTerminalInterruptDisplayGate(term, { now: 8000, quietMs: 500, promptQuietMs: 80, maxDrainMs: 2500, }); assert.equal( filterTerminalInterruptDisplayOutput(term, "Reading package lists...\n", { now: 8001 }).accepted, false, ); assert.deepEqual( filterTerminalInterruptDisplayOutput(term, "[sudo] password for alice: ", { now: 8010 }), { accepted: true, data: "[sudo] password for alice: ", droppedBytes: 0, reason: "password-prompt", }, ); }); test("interrupt display drain accepts localized password prompts (#2010)", () => { for (const prompt of ["Password: ", "[sudo] alice 的密码:", "输入密码", "用户 的密码"]) { const term = createFakeTerm(); armTerminalInterruptDisplayGate(term, { now: 8100, quietMs: 500, promptQuietMs: 80, maxDrainMs: 2500, }); assert.equal( filterTerminalInterruptDisplayOutput(term, "stale\n", { now: 8101 }).accepted, false, ); assert.deepEqual( filterTerminalInterruptDisplayOutput(term, prompt, { now: 8110 }), { accepted: true, data: prompt, droppedBytes: 0, reason: "password-prompt", }, `expected password prompt to resume drain: ${JSON.stringify(prompt)}`, ); } }); test("interrupt display drain holds a split Password: prompt across chunks (#2010)", () => { const term = createFakeTerm(); armTerminalInterruptDisplayGate(term, { now: 8300, quietMs: 500, promptQuietMs: 80, maxDrainMs: 2500, }); assert.equal( filterTerminalInterruptDisplayOutput(term, "stale\n", { now: 8301 }).accepted, false, ); assert.deepEqual( filterTerminalInterruptDisplayOutput(term, "Pass", { now: 8302 }), { accepted: false, data: "", droppedBytes: 0, reason: "draining" }, ); assert.deepEqual( filterTerminalInterruptDisplayOutput(term, "word: ", { now: 8303 }), { accepted: true, data: "Password: ", droppedBytes: 0, reason: "prompt-gap", }, ); }); test("interrupt display drain holds a split ANSI-colored Password: prompt", () => { const term = createFakeTerm(); const red = "\x1b[31m"; const reset = "\x1b[0m"; armTerminalInterruptDisplayGate(term, { now: 8350, quietMs: 500, promptQuietMs: 80, maxDrainMs: 2500, }); assert.equal( filterTerminalInterruptDisplayOutput(term, "stale\n", { now: 8351 }).accepted, false, ); assert.deepEqual( filterTerminalInterruptDisplayOutput(term, `${red}Pass`, { now: 8352 }), { accepted: false, data: "", droppedBytes: 0, reason: "draining" }, ); assert.deepEqual( filterTerminalInterruptDisplayOutput(term, `word:${reset} `, { now: 8353 }), { accepted: true, data: `${red}Password:${reset} `, droppedBytes: 0, reason: "prompt-gap", }, ); }); test("interrupt display drain holds a password prefix split before trailing ANSI", () => { const term = createFakeTerm(); const red = "\x1b[31m"; const reset = "\x1b[0m"; armTerminalInterruptDisplayGate(term, { now: 8370, quietMs: 500, promptQuietMs: 80, maxDrainMs: 2500, }); assert.equal( filterTerminalInterruptDisplayOutput(term, "stale\n", { now: 8371 }).accepted, false, ); assert.deepEqual( filterTerminalInterruptDisplayOutput(term, `${red}Pass\x1b[`, { now: 8372 }), { accepted: false, data: "", droppedBytes: 0, reason: "draining" }, ); assert.deepEqual( filterTerminalInterruptDisplayOutput(term, `0mword: `, { now: 8373 }), { accepted: true, data: `${red}Pass${reset}word: `, droppedBytes: 0, reason: "prompt-gap", }, ); }); test("interrupt display drain holds a password prefix split mid CSI parameter", () => { const term = createFakeTerm(); const red = "\x1b[31m"; const reset = "\x1b[0m"; armTerminalInterruptDisplayGate(term, { now: 8380, quietMs: 500, promptQuietMs: 80, maxDrainMs: 2500, }); assert.equal( filterTerminalInterruptDisplayOutput(term, "stale\n", { now: 8381 }).accepted, false, ); assert.deepEqual( filterTerminalInterruptDisplayOutput(term, `${red}Pass\x1b[0`, { now: 8382 }), { accepted: false, data: "", droppedBytes: 0, reason: "draining" }, ); assert.deepEqual( filterTerminalInterruptDisplayOutput(term, `mword: `, { now: 8383 }), { accepted: true, data: `${red}Pass${reset}word: `, droppedBytes: 0, reason: "prompt-gap", }, ); }); test("interrupt display drain discards a held password prefix that does not complete", () => { const term = createFakeTerm(); armTerminalInterruptDisplayGate(term, { now: 8400, quietMs: 500, promptQuietMs: 80, maxDrainMs: 2500, }); assert.equal( filterTerminalInterruptDisplayOutput(term, "stale\n", { now: 8401 }).accepted, false, ); assert.deepEqual( filterTerminalInterruptDisplayOutput(term, "Pass", { now: 8402 }), { accepted: false, data: "", droppedBytes: 0, reason: "draining" }, ); assert.deepEqual( filterTerminalInterruptDisplayOutput(term, "$ ", { now: 8600 }), { accepted: true, data: "$ ", droppedBytes: 4, reason: "prompt-gap", }, ); }); test("interrupt display drain discards CSI finals when mid-CSI password prefix fails", () => { const term = createFakeTerm(); armTerminalInterruptDisplayGate(term, { now: 8420, quietMs: 500, promptQuietMs: 80, maxDrainMs: 2500, }); assert.equal( filterTerminalInterruptDisplayOutput(term, "stale\n", { now: 8421 }).accepted, false, ); assert.deepEqual( filterTerminalInterruptDisplayOutput(term, "Pass\x1b[0", { now: 8422 }), { accepted: false, data: "", droppedBytes: 0, reason: "draining" }, ); assert.deepEqual( filterTerminalInterruptDisplayOutput(term, "m$ ", { now: 8600 }), { accepted: true, data: "$ ", droppedBytes: "Pass\x1b[0".length + 1, reason: "prompt-gap", }, ); }); test("interrupt display drain does not hold ordinary password-related output", () => { const term = createFakeTerm(); armTerminalInterruptDisplayGate(term, { now: 8700, quietMs: 500, promptQuietMs: 80, maxDrainMs: 2500, }); assert.equal( filterTerminalInterruptDisplayOutput(term, "stale\n", { now: 8701 }).accepted, false, ); assert.deepEqual( filterTerminalInterruptDisplayOutput(term, "Password authentication failed", { now: 8702 }), { accepted: false, data: "", droppedBytes: "Password authentication failed".length, reason: "draining", }, ); assert.deepEqual( filterTerminalInterruptDisplayOutput(term, "$ ", { now: 8900 }), { accepted: true, data: "$ ", droppedBytes: 0, reason: "prompt-gap", }, ); }); test("interrupt display drain holds split password prompts with leading text", () => { const term = createFakeTerm(); armTerminalInterruptDisplayGate(term, { now: 9000, quietMs: 500, promptQuietMs: 80, maxDrainMs: 2500, }); assert.equal( filterTerminalInterruptDisplayOutput(term, "stale\n", { now: 9001 }).accepted, false, ); assert.deepEqual( filterTerminalInterruptDisplayOutput(term, "alice@host's pass", { now: 9002 }), { accepted: false, data: "", droppedBytes: 0, reason: "draining" }, ); assert.deepEqual( filterTerminalInterruptDisplayOutput(term, "word: ", { now: 9003 }), { accepted: true, data: "alice@host's password: ", droppedBytes: 0, reason: "prompt-gap", }, ); }); test("interrupt display drain accepts a fresh one-chunk password prompt before the quiet gap", () => { const term = createFakeTerm(); armTerminalInterruptDisplayGate(term, { now: 9100, quietMs: 500, promptQuietMs: 80, maxDrainMs: 2500, }); assert.equal( filterTerminalInterruptDisplayOutput(term, "old flood\n", { now: 9101 }).accepted, false, ); assert.deepEqual( filterTerminalInterruptDisplayOutput(term, "Password: ", { now: 9110 }), { accepted: true, data: "Password: ", droppedBytes: 0, reason: "password-prompt", }, ); }); test("interrupt display drain accepts a last-line password prompt after a large stale prefix", () => { const term = createFakeTerm(); armTerminalInterruptDisplayGate(term, { now: 9120, quietMs: 500, promptQuietMs: 80, maxDrainMs: 2500, promptCandidateBytes: 512, }); assert.equal( filterTerminalInterruptDisplayOutput(term, "stale\n", { now: 9121 }).accepted, false, ); const largeChunk = `${"x".repeat(600)}\n[sudo] password for alice: `; assert.deepEqual( filterTerminalInterruptDisplayOutput(term, largeChunk, { now: 9130 }), { accepted: true, data: "[sudo] password for alice: ", droppedBytes: 601, reason: "password-prompt", }, ); }); test("interrupt display drain does not hold short ASCII trailing letters as password prefixes", () => { const term = createFakeTerm(); armTerminalInterruptDisplayGate(term, { now: 9300, quietMs: 500, promptQuietMs: 80, maxDrainMs: 2500, }); assert.equal( filterTerminalInterruptDisplayOutput(term, "stale\n", { now: 9301 }).accepted, false, ); // Full-width "password:" must not lower ASCII minLen to 1, or "copy p" is held // and a later "assword:" chunk can bypass quiet-gap. assert.deepEqual( filterTerminalInterruptDisplayOutput(term, "copy p", { now: 9302 }), { accepted: false, data: "", droppedBytes: "copy p".length, reason: "draining", }, ); assert.deepEqual( filterTerminalInterruptDisplayOutput(term, "assword: ", { now: 9310 }), { accepted: false, data: "", droppedBytes: "assword: ".length, reason: "draining", }, ); assert.deepEqual( filterTerminalInterruptDisplayOutput(term, "$ ", { now: 9500 }), { accepted: true, data: "$ ", droppedBytes: 0, reason: "prompt-gap", }, ); }); test("interrupt display drain rejects held password-prefix completion across a line break", () => { const term = createFakeTerm(); armTerminalInterruptDisplayGate(term, { now: 9200, quietMs: 500, promptQuietMs: 80, maxDrainMs: 2500, }); assert.equal( filterTerminalInterruptDisplayOutput(term, "stale\n", { now: 9201 }).accepted, false, ); assert.deepEqual( filterTerminalInterruptDisplayOutput(term, "Pass", { now: 9202 }), { accepted: false, data: "", droppedBytes: 0, reason: "draining" }, ); // A newline before Password: discards the held "Pass" prefix (not a same-line // completion), but the fresh complete password line is still preserved. assert.deepEqual( filterTerminalInterruptDisplayOutput(term, "\nPassword: ", { now: 9210 }), { accepted: true, data: "Password: ", droppedBytes: 4 + 1, reason: "password-prompt", }, ); });