Files
NetMesh/application/state/useOutputTriggers.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

971 lines
28 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import React from 'react';
import { act, create, type ReactTestRenderer } from 'react-test-renderer';
import {
createDeferredOutputTriggerEventProcessor,
createOutputTriggerScanBuffer,
droppedOutputOverflowMayAffectAlternateScreenState,
findMatchEndingAfter,
hasApplicableOutputTriggerSnippet,
inspectDroppedOutputOverflowAlternateScreenState,
useOutputTriggers,
} from './useOutputTriggers.ts';
import { createTerminalOutputTriggerFilter } from '@/domain/terminalOutputTriggerFilter.ts';
import type { Snippet } from '@/domain/models';
test('findMatchEndingAfter skips stale overlap matches and finds current output', () => {
const text = 'NETCATTY_TRIGGER_PROBE old\r\nprompt# NETCATTY_TRIGGER_PROBE: command not found\r\n';
const minEndOffset = 'NETCATTY_TRIGGER_PROBE old\r\n'.length;
const match = findMatchEndingAfter(text, 'NETCATTY_TRIGGER_PROBE', minEndOffset);
assert.deepEqual(match, {
value: 'NETCATTY_TRIGGER_PROBE',
endOffset: 'NETCATTY_TRIGGER_PROBE old\r\nprompt# NETCATTY_TRIGGER_PROBE'.length,
});
});
test('findMatchEndingAfter supports regex patterns', () => {
const text = 'old done\r\nservice ready: 200\r\n';
const minEndOffset = 'old done\r\n'.length;
const match = findMatchEndingAfter(text, 'ready:\\s+\\d+', minEndOffset);
assert.deepEqual(match, {
value: 'ready: 200',
endOffset: 'old done\r\nservice ready: 200'.length,
});
});
test('findMatchEndingAfter returns null when matches are only stale', () => {
const text = 'NETCATTY_TRIGGER_PROBE old\r\nnew output\r\n';
const minEndOffset = 'NETCATTY_TRIGGER_PROBE old\r\n'.length;
assert.equal(findMatchEndingAfter(text, 'NETCATTY_TRIGGER_PROBE', minEndOffset), null);
});
test('hasApplicableOutputTriggerSnippet ignores non-output-trigger snippets', () => {
const snippets: Snippet[] = [
{
id: 'plain',
label: 'Plain',
command: 'echo hello',
kind: 'snippet',
},
{
id: 'manual-script',
label: 'Manual',
command: '',
kind: 'script',
trigger: 'manual',
triggerPattern: 'READY',
},
];
assert.equal(hasApplicableOutputTriggerSnippet(snippets, 'host-a'), false);
});
test('hasApplicableOutputTriggerSnippet requires a runnable output trigger for this host', () => {
const snippets: Snippet[] = [
{
id: 'wrong-host',
label: 'Wrong host',
command: '',
kind: 'script',
trigger: 'onOutput',
triggerPattern: 'READY',
targets: ['host-b'],
},
{
id: 'missing-pattern',
label: 'Missing pattern',
command: '',
kind: 'script',
trigger: 'onOutput',
},
];
assert.equal(hasApplicableOutputTriggerSnippet(snippets, 'host-a'), false);
snippets.push({
id: 'right-host',
label: 'Right host',
command: '',
kind: 'script',
trigger: 'onOutput',
triggerPattern: 'READY',
targets: ['host-a'],
});
assert.equal(hasApplicableOutputTriggerSnippet(snippets, 'host-a'), true);
});
test('hasApplicableOutputTriggerSnippet follows the host current group', () => {
const snippets: Snippet[] = [{
id: 'group-trigger',
label: 'Group trigger',
command: '',
kind: 'script',
trigger: 'onOutput',
triggerPattern: 'READY',
targetGroups: ['Production'],
}];
assert.equal(
hasApplicableOutputTriggerSnippet(snippets, { id: 'host-a', group: 'Production/Web' }),
true,
);
assert.equal(
hasApplicableOutputTriggerSnippet(snippets, { id: 'host-a', group: 'Staging' }),
false,
);
});
test('equivalent host objects do not reset a pending output trigger match', async () => {
const actEnvironment = globalThis as typeof globalThis & {
IS_REACT_ACT_ENVIRONMENT?: boolean;
};
const previousActEnvironment = actEnvironment.IS_REACT_ACT_ENVIRONMENT;
actEnvironment.IS_REACT_ACT_ENVIRONMENT = true;
const snippets: Snippet[] = [{
id: 'stable-host-trigger',
label: 'Stable host trigger',
command: '',
kind: 'script',
trigger: 'onOutput',
triggerPattern: 'READY',
targetGroups: ['Production'],
}];
let appendOutput: ((chunk: string) => void) | undefined;
let runCount = 0;
let renderer: ReactTestRenderer | null = null;
const onRunScript = () => {
runCount += 1;
};
const Probe = ({ host }: { host: { id: string; group: string } }) => {
appendOutput = useOutputTriggers({
sessionId: 'session-1',
host,
snippets,
onRunScript,
}).appendOutput;
return null;
};
await act(async () => {
renderer = create(React.createElement(Probe, {
host: { id: 'host-a', group: 'Production/Web' },
}));
});
try {
await act(async () => {
appendOutput?.('REA');
renderer?.update(React.createElement(Probe, {
host: { id: 'host-a', group: 'Production/Web' },
}));
appendOutput?.('DY');
await new Promise((resolve) => setTimeout(resolve, 40));
});
} finally {
await act(async () => renderer?.unmount());
actEnvironment.IS_REACT_ACT_ENVIRONMENT = previousActEnvironment;
}
assert.equal(runCount, 1);
});
test('output trigger scan buffer consumes scanned content and only keeps overlap', () => {
const buffer = createOutputTriggerScanBuffer(4);
assert.deepEqual(buffer.append('abcdef'), {
text: 'abcdef',
minEndOffset: 0,
baseOffset: 0,
});
assert.deepEqual(buffer.append('gh'), {
text: 'cdefgh',
minEndOffset: 4,
baseOffset: 2,
});
});
test('output trigger scan buffer scans all new large output instead of only the tail', () => {
const buffer = createOutputTriggerScanBuffer(4);
assert.deepEqual(buffer.append('abc'), {
text: 'abc',
minEndOffset: 0,
baseOffset: 0,
});
assert.deepEqual(buffer.append('defghijkl'), {
text: 'abcdefghijkl',
minEndOffset: 3,
baseOffset: 0,
});
});
test('deferred output trigger processor schedules output instead of processing synchronously', () => {
const processed: string[] = [];
const scheduled: Array<() => void> = [];
const processor = createDeferredOutputTriggerEventProcessor({
processOutput: (chunk) => processed.push(chunk),
processInput: () => {},
schedule: (callback) => {
scheduled.push(callback);
return () => {};
},
});
processor.enqueueOutput('READY');
assert.deepEqual(processed, []);
assert.equal(scheduled.length, 1);
scheduled.shift()?.();
assert.deepEqual(processed, ['READY']);
});
test('deferred output trigger processor coalesces output while preserving input order', () => {
const events: string[] = [];
const scheduled: Array<() => void> = [];
const processor = createDeferredOutputTriggerEventProcessor({
processOutput: (chunk) => events.push(`output:${chunk}`),
processInput: (data) => events.push(`input:${data}`),
schedule: (callback) => {
scheduled.push(callback);
return () => {};
},
});
processor.enqueueOutput('he');
processor.enqueueOutput('llo');
processor.enqueueInput('cmd');
processor.enqueueOutput('world');
scheduled.shift()?.();
assert.deepEqual(events, ['output:hello', 'input:cmd', 'output:world']);
});
test('deferred output trigger processor splits large output and yields between flushes', () => {
const processed: string[] = [];
const scheduled: Array<() => void> = [];
const processor = createDeferredOutputTriggerEventProcessor({
maxOutputChunkBytes: 4,
maxOutputBytesPerFlush: 8,
processOutput: (chunk) => processed.push(chunk),
processInput: () => {},
schedule: (callback) => {
scheduled.push(callback);
return () => {};
},
});
processor.enqueueOutput('abcdefghijkl');
scheduled.shift()?.();
assert.deepEqual(processed, ['abcd', 'efgh']);
assert.equal(scheduled.length, 1);
scheduled.shift()?.();
assert.deepEqual(processed, ['abcd', 'efgh', 'ijkl']);
});
test('deferred output trigger processor splits large input and yields between flushes', () => {
const processed: string[] = [];
const scheduled: Array<() => void> = [];
const processor = createDeferredOutputTriggerEventProcessor({
maxOutputChunkBytes: 4,
maxOutputBytesPerFlush: 8,
processOutput: () => {},
processInput: (chunk) => processed.push(chunk),
schedule: (callback) => {
scheduled.push(callback);
return () => {};
},
});
processor.enqueueInput('abcdefghijkl');
scheduled.shift()?.();
assert.deepEqual(processed, ['abcd', 'efgh']);
assert.equal(scheduled.length, 1);
scheduled.shift()?.();
assert.deepEqual(processed, ['abcd', 'efgh', 'ijkl']);
});
test('deferred output trigger processor caps pending input and resets input state', () => {
const processed: string[] = [];
const scheduled: Array<() => void> = [];
let inputOverflows = 0;
let inputOverflowLines = 0;
const processor = createDeferredOutputTriggerEventProcessor({
maxOutputChunkBytes: 4,
maxOutputBytesPerFlush: 8,
maxPendingInputBytes: 8,
processOutput: () => {},
processInput: (chunk) => processed.push(chunk),
processInputOverflow: (lineCount) => {
inputOverflows += 1;
inputOverflowLines += lineCount;
},
schedule: (callback) => {
scheduled.push(callback);
return () => {};
},
});
processor.enqueueInput('abcdefghijkl');
while (scheduled.length > 0) {
scheduled.shift()?.();
}
assert.equal(inputOverflows, 1);
assert.equal(inputOverflowLines, 1);
assert.deepEqual(processed, ['efgh', 'ijkl']);
});
test('deferred output trigger processor reports dropped input echo line count', () => {
const scheduled: Array<() => void> = [];
const overflowLines: number[] = [];
const processor = createDeferredOutputTriggerEventProcessor({
maxOutputChunkBytes: 16,
maxOutputBytesPerFlush: 32,
maxPendingInputBytes: 5,
processOutput: () => {},
processInput: () => {},
processInputOverflow: (lineCount) => {
overflowLines.push(lineCount);
},
schedule: (callback) => {
scheduled.push(callback);
return () => {};
},
});
processor.enqueueInput('one\r\ntwo\nthree');
processor.flush();
assert.deepEqual(overflowLines, [3]);
});
test('deferred output trigger processor reports more than 1024 dropped input echo lines', () => {
const scheduled: Array<() => void> = [];
const overflowLines: number[] = [];
const processor = createDeferredOutputTriggerEventProcessor({
maxOutputChunkBytes: 16,
maxOutputBytesPerFlush: 32,
maxPendingInputBytes: 4,
processOutput: () => {},
processInput: () => {},
processInputOverflow: (lineCount) => {
overflowLines.push(lineCount);
},
schedule: (callback) => {
scheduled.push(callback);
return () => {};
},
});
processor.enqueueInput(`${'x\n'.repeat(1100)}tail`);
processor.flush();
assert.deepEqual(overflowLines, [1101]);
});
test('deferred output trigger processor includes retained input lines in overflow echo suppression', () => {
const scheduled: Array<() => void> = [];
const overflowLines: number[] = [];
const retainedInput = 'READY c\nREADY d\n';
const processor = createDeferredOutputTriggerEventProcessor({
maxOutputChunkBytes: 64,
maxOutputBytesPerFlush: 128,
maxPendingInputBytes: retainedInput.length,
processOutput: () => {},
processInput: () => {},
processInputOverflow: (lineCount) => {
overflowLines.push(lineCount);
},
schedule: (callback) => {
scheduled.push(callback);
return () => {};
},
});
processor.enqueueInput(`READY a\nREADY b\n${retainedInput}`);
processor.flush();
assert.deepEqual(overflowLines, [4]);
});
test('deferred output trigger processor accumulates repeated input overflows before flush', () => {
const overflowLines: number[] = [];
const processor = createDeferredOutputTriggerEventProcessor({
maxPendingInputBytes: 0,
processOutput: () => {},
processInput: () => {},
processInputOverflow: (lineCount) => {
overflowLines.push(lineCount);
},
schedule: () => () => {},
});
processor.enqueueInput('one\ntwo\n');
processor.enqueueInput('three\nfour\n');
processor.flush();
assert.deepEqual(overflowLines, [4]);
});
test('deferred output trigger processor counts input split across overflow boundary as one line', () => {
const overflowLines: number[] = [];
const processor = createDeferredOutputTriggerEventProcessor({
maxPendingInputBytes: 'def\n'.length,
processOutput: () => {},
processInput: () => {},
processInputOverflow: (lineCount) => {
overflowLines.push(lineCount);
},
schedule: () => () => {},
});
processor.enqueueInput('abcdef\n');
processor.flush();
assert.deepEqual(overflowLines, [1]);
});
test('deferred output trigger processor resets input overflow accounting after flush', () => {
const scheduled: Array<() => void> = [];
const overflowLines: number[] = [];
const processor = createDeferredOutputTriggerEventProcessor({
maxPendingInputBytes: 0,
processOutput: () => {},
processInput: () => {},
processInputOverflow: (lineCount) => {
overflowLines.push(lineCount);
},
schedule: (callback) => {
scheduled.push(callback);
return () => {};
},
});
processor.enqueueInput('one\ntwo\n');
scheduled.shift()?.();
processor.enqueueInput('three\n');
scheduled.shift()?.();
assert.deepEqual(overflowLines, [2, 1]);
});
test('deferred output trigger processor preserves output before later input overflow', () => {
const processed: string[] = [];
const processor = createDeferredOutputTriggerEventProcessor({
maxPendingInputBytes: 0,
processOutput: (chunk) => {
processed.push(`output:${chunk}`);
},
processInput: () => {},
processInputOverflow: (lineCount) => {
processed.push(`overflow:${lineCount}`);
},
schedule: () => () => {},
});
processor.enqueueOutput('READY\n');
processor.enqueueInput('abcdef\n');
processor.flush();
assert.deepEqual(processed, ['output:READY\n', 'overflow:1']);
});
test('deferred output trigger processor suppresses retained multi-line input echo after overflow', () => {
const scheduled: Array<() => void> = [];
const filter = createTerminalOutputTriggerFilter();
const retainedInput = 'READY c\nREADY d\n';
const fullInput = `READY a\nREADY b\n${retainedInput}`;
const processor = createDeferredOutputTriggerEventProcessor({
maxOutputChunkBytes: 64,
maxOutputBytesPerFlush: 128,
maxPendingInputBytes: retainedInput.length,
processOutput: () => {},
processInput: (chunk) => {
filter.noteUserInput(chunk);
},
processInputOverflow: (lineCount) => {
filter.markInputEchoUncertain(lineCount);
},
schedule: (callback) => {
scheduled.push(callback);
return () => {};
},
});
processor.enqueueInput(fullInput);
while (scheduled.length > 0) {
scheduled.shift()?.();
}
const result = filter.processServerChunk(`${fullInput}server READY\n`);
assert.equal(result.scannableText, 'server READY\n');
});
test('deferred output trigger processor keeps pending scan output bounded', () => {
const processed: string[] = [];
const scheduled: Array<() => void> = [];
const processor = createDeferredOutputTriggerEventProcessor({
maxOutputChunkBytes: 4,
maxOutputBytesPerFlush: 64,
maxPendingOutputBytes: 8,
pendingOutputOverlapChars: 4,
processOutput: (chunk) => processed.push(chunk),
processInput: () => {},
schedule: (callback) => {
scheduled.push(callback);
return () => {};
},
});
processor.enqueueOutput('x'.repeat(80));
processor.enqueueOutput('READY');
assert.equal(processor.getPendingOutputBytes(), 12);
scheduled.shift()?.();
assert.equal(processed.join('').length, 12);
assert.equal(processed.join('').endsWith('READY'), true);
});
test('deferred output trigger processor defers dropped output maintenance work', () => {
const dropped: string[] = [];
const processed: string[] = [];
const scheduled: Array<() => void> = [];
const processor = createDeferredOutputTriggerEventProcessor({
maxOutputChunkBytes: 4,
maxOutputBytesPerFlush: 8,
maxPendingOutputBytes: 8,
pendingOutputOverlapChars: 0,
processDroppedOutput: (chunk) => dropped.push(chunk),
processOutput: (chunk) => processed.push(chunk),
processInput: () => {},
schedule: (callback) => {
scheduled.push(callback);
return () => {};
},
});
processor.enqueueOutput('x'.repeat(40));
processor.enqueueOutput('READY');
assert.deepEqual(dropped, []);
assert.deepEqual(processed, []);
scheduled.shift()?.();
assert.equal(dropped.join('').length, 8);
assert.deepEqual(processed, []);
assert.equal(scheduled.length, 1);
});
test('deferred output trigger processor applies output metadata in queue order', () => {
const scanned: string[] = [];
const scheduled: Array<() => void> = [];
let suppressed = false;
const processor = createDeferredOutputTriggerEventProcessor({
maxOutputChunkBytes: 64,
maxOutputBytesPerFlush: 64,
processOutput: (chunk, meta) => {
if (meta?.droppedOutputMayAffectTerminalState) {
suppressed = true;
}
if (!suppressed) {
scanned.push(chunk);
}
},
processInput: () => {},
schedule: (callback) => {
scheduled.push(callback);
return () => {};
},
});
processor.enqueueOutput('READY');
processor.enqueueOutput('AFTER_DROP', { droppedOutputMayAffectTerminalState: true });
scheduled.shift()?.();
assert.deepEqual(scanned, ['READY']);
});
test('deferred output trigger processor preserves metadata when trimmed output is fully dropped', () => {
const scanned: string[] = [];
const scheduled: Array<() => void> = [];
let suppressed = false;
const processor = createDeferredOutputTriggerEventProcessor({
maxOutputChunkBytes: 64,
maxOutputBytesPerFlush: 64,
maxPendingOutputBytes: 5,
pendingOutputOverlapChars: 0,
processDroppedOutput: (_chunk, meta) => {
if (meta?.droppedOutputMayAffectTerminalState) {
suppressed = true;
}
},
processOutput: (chunk) => {
if (!suppressed) {
scanned.push(chunk);
}
},
processInput: () => {},
schedule: (callback) => {
scheduled.push(callback);
return () => {};
},
});
processor.enqueueOutput('DROP', { droppedOutputMayAffectTerminalState: true });
processor.enqueueOutput('READY');
while (scheduled.length > 0) {
scheduled.shift()?.();
}
assert.deepEqual(scanned, []);
});
test('deferred output trigger processor caps dropped maintenance before retained output', () => {
const dropped: string[] = [];
const processed: string[] = [];
const scheduled: Array<() => void> = [];
const processor = createDeferredOutputTriggerEventProcessor({
maxOutputChunkBytes: 4,
maxOutputBytesPerFlush: 8,
maxPendingOutputBytes: 8,
pendingOutputOverlapChars: 0,
processDroppedOutput: (chunk) => dropped.push(chunk),
processOutput: (chunk) => processed.push(chunk),
processInput: () => {},
schedule: (callback) => {
scheduled.push(callback);
return () => {};
},
});
processor.enqueueOutput('x'.repeat(200));
processor.enqueueOutput('READY');
scheduled.shift()?.();
assert.equal(dropped.join('').length, 8);
assert.deepEqual(processed, []);
scheduled.shift()?.();
assert.equal(processed.join('').endsWith('READY'), true);
});
test('deferred output trigger processor inspects dropped overflow with retained context', () => {
let mayAffectAlternateScreen = false;
const processor = createDeferredOutputTriggerEventProcessor({
maxOutputBytesPerFlush: 5,
maxPendingOutputBytes: 0,
pendingOutputOverlapChars: 0,
processDroppedOutput: () => {},
processDroppedOutputOverflow: (overflow) => {
mayAffectAlternateScreen ||= droppedOutputOverflowMayAffectAlternateScreenState(overflow);
},
processOutput: () => {},
processInput: () => {},
schedule: () => () => {},
});
processor.enqueueOutput('\x1b[?10');
processor.enqueueOutput('49hREADY');
assert.equal(mayAffectAlternateScreen, true);
});
test('deferred output trigger processor does not carry dropped context across real output', () => {
const scheduled: Array<() => void> = [];
let mayAffectAlternateScreen = false;
const processor = createDeferredOutputTriggerEventProcessor({
maxOutputChunkBytes: 8,
maxOutputBytesPerFlush: 3,
maxPendingOutputBytes: 1,
pendingOutputOverlapChars: 0,
processDroppedOutput: () => {},
processDroppedOutputOverflow: (overflow) => {
mayAffectAlternateScreen ||= droppedOutputOverflowMayAffectAlternateScreenState(overflow);
},
processOutput: () => {},
processInput: () => {},
schedule: (callback) => {
scheduled.push(callback);
return () => {};
},
});
processor.enqueueOutput('\x1b[?Z');
while (scheduled.length > 0) {
scheduled.shift()?.();
}
processor.enqueueOutput('1049hREADY');
assert.equal(mayAffectAlternateScreen, false);
});
test('deferred output trigger processor suppresses retained scans after dropped maintenance overflow', () => {
const scannable: string[] = [];
const scheduled: Array<() => void> = [];
const filter = createTerminalOutputTriggerFilter();
let suppressScan = false;
const processor = createDeferredOutputTriggerEventProcessor({
maxOutputChunkBytes: 4,
maxOutputBytesPerFlush: 8,
maxPendingOutputBytes: 8,
pendingOutputOverlapChars: 0,
processDroppedOutput: (chunk) => {
filter.processServerChunk(chunk);
},
processDroppedOutputOverflow: (overflow) => {
if (droppedOutputOverflowMayAffectAlternateScreenState(overflow)) {
suppressScan = true;
}
},
processOutput: (chunk) => {
const result = filter.processServerChunk(chunk);
if (!suppressScan && result.scannableText && !result.alternateScreenActive) {
scannable.push(result.scannableText);
}
},
processInput: () => {},
schedule: (callback) => {
scheduled.push(callback);
return () => {};
},
});
processor.enqueueOutput(`${'x'.repeat(40)}\x1b[?1049h${'y'.repeat(40)}`);
processor.enqueueOutput('READY');
while (scheduled.length > 0) {
scheduled.shift()?.();
}
assert.equal(suppressScan, true);
assert.deepEqual(scannable, []);
});
test('deferred output trigger processor can recover from dropped split alternate-screen leave', () => {
const scannable: string[] = [];
const scheduled: Array<() => void> = [];
const filter = createTerminalOutputTriggerFilter();
const recoveryFilter = createTerminalOutputTriggerFilter();
let suppressScan = true;
const maybeRecover = (chunk: string) => {
const recovery = recoveryFilter.processServerChunk(chunk);
if (recovery.meta.alternateScreenAction === 'leave' && !recovery.alternateScreenActive) {
suppressScan = false;
recoveryFilter.reset();
return true;
}
return false;
};
const processor = createDeferredOutputTriggerEventProcessor({
maxOutputChunkBytes: 16,
maxOutputBytesPerFlush: 16,
maxPendingOutputBytes: 8,
pendingOutputOverlapChars: 0,
processDroppedOutput: (chunk) => {
filter.processServerChunk(chunk);
if (suppressScan) {
maybeRecover(chunk);
}
},
processOutput: (chunk) => {
const result = filter.processServerChunk(chunk);
if (suppressScan) {
maybeRecover(chunk);
return;
}
if (result.scannableText && !result.alternateScreenActive) {
scannable.push(result.scannableText);
}
},
processInput: () => {},
schedule: (callback) => {
scheduled.push(callback);
return () => {};
},
});
processor.enqueueOutput('\x1b[?10');
processor.enqueueOutput('49lREADY');
while (scheduled.length > 0) {
scheduled.shift()?.();
}
assert.equal(suppressScan, false);
assert.deepEqual(scannable, []);
processor.enqueueOutput('NEXT');
while (scheduled.length > 0) {
scheduled.shift()?.();
}
assert.deepEqual(scannable, ['NEXT']);
});
test('deferred output trigger processor keeps scanning after plain dropped maintenance overflow', () => {
const scannable: string[] = [];
const scheduled: Array<() => void> = [];
const filter = createTerminalOutputTriggerFilter();
let suppressScan = false;
const processor = createDeferredOutputTriggerEventProcessor({
maxOutputChunkBytes: 4,
maxOutputBytesPerFlush: 8,
maxPendingOutputBytes: 8,
pendingOutputOverlapChars: 0,
processDroppedOutput: (chunk) => {
filter.processServerChunk(chunk);
},
processDroppedOutputOverflow: (overflow) => {
if (droppedOutputOverflowMayAffectAlternateScreenState(overflow)) {
suppressScan = true;
}
},
processOutput: (chunk) => {
const result = filter.processServerChunk(chunk);
if (!suppressScan && result.scannableText && !result.alternateScreenActive) {
scannable.push(result.scannableText);
}
},
processInput: () => {},
schedule: (callback) => {
scheduled.push(callback);
return () => {};
},
});
processor.enqueueOutput('x'.repeat(200));
processor.enqueueOutput('READY');
while (scheduled.length > 0) {
scheduled.shift()?.();
}
assert.equal(suppressScan, false);
assert.equal(scannable.join('').endsWith('READY'), true);
});
test('dropped output overflow detection ignores color sequences but catches alternate screen candidates', () => {
assert.equal(droppedOutputOverflowMayAffectAlternateScreenState({
retainedPrefix: 'plain',
discardedSuffix: '\x1b[31mcolored text',
}), false);
assert.equal(droppedOutputOverflowMayAffectAlternateScreenState({
retainedPrefix: '\x1b[?10',
discardedSuffix: '49hmore output',
}), true);
assert.equal(droppedOutputOverflowMayAffectAlternateScreenState({
retainedPrefix: 'plain',
discardedSuffix: '\x1b[?1;1049l',
}), true);
assert.equal(droppedOutputOverflowMayAffectAlternateScreenState({
retainedPrefix: `before\x1b[?1;2;3;4;5;6;7;8;9;10`,
discardedSuffix: ';1049hafter',
}), true);
assert.deepEqual(inspectDroppedOutputOverflowAlternateScreenState({
retainedPrefix: '\x1b[?1049h',
discardedSuffix: `${'x'.repeat(5000)}\x1b[?1;1049l`,
}), {
mayAffectAlternateScreen: true,
mayAffectScanState: false,
finalAction: 'leave',
});
assert.deepEqual(inspectDroppedOutputOverflowAlternateScreenState({
retainedPrefix: 'plain',
discardedSuffix: `${'x'.repeat(3000)}\x1b[?1049h${'y'.repeat(3000)}`,
}), {
mayAffectAlternateScreen: true,
mayAffectScanState: false,
finalAction: undefined,
});
assert.deepEqual(inspectDroppedOutputOverflowAlternateScreenState({
retainedPrefix: 'plain',
discardedSuffix: `\x1b[?1049l${'x'.repeat(3000)}\x1b[?1049h${'y'.repeat(3000)}`,
}), {
mayAffectAlternateScreen: true,
mayAffectScanState: false,
finalAction: undefined,
});
assert.deepEqual(inspectDroppedOutputOverflowAlternateScreenState({
retainedPrefix: 'plain',
discardedSuffix: `${'x'.repeat(5000)}\x1b[?1049lplain\x1b[?10`,
}), {
mayAffectAlternateScreen: true,
mayAffectScanState: true,
finalAction: undefined,
});
assert.deepEqual(inspectDroppedOutputOverflowAlternateScreenState({
retainedPrefix: 'plain',
discardedSuffix: `\x1b[?1049l${'x'.repeat(2500)}\x1b[?10`,
}), {
mayAffectAlternateScreen: true,
mayAffectScanState: true,
finalAction: undefined,
});
assert.deepEqual(inspectDroppedOutputOverflowAlternateScreenState({
retainedPrefix: '\x1b[31',
discardedSuffix: 'mREADY',
}), {
mayAffectAlternateScreen: false,
mayAffectScanState: true,
finalAction: undefined,
});
});
test('deferred output trigger processor preserves filter state for trimmed output', () => {
const scannable: string[] = [];
const scheduled: Array<() => void> = [];
const filter = createTerminalOutputTriggerFilter();
const processor = createDeferredOutputTriggerEventProcessor({
maxOutputChunkBytes: 4,
maxOutputBytesPerFlush: 64,
maxPendingOutputBytes: 8,
pendingOutputOverlapChars: 0,
processDroppedOutput: (chunk) => {
filter.processServerChunk(chunk);
},
processOutput: (chunk) => {
const result = filter.processServerChunk(chunk);
if (result.scannableText && !result.alternateScreenActive) {
scannable.push(result.scannableText);
}
},
processInput: () => {},
schedule: (callback) => {
scheduled.push(callback);
return () => {};
},
});
processor.enqueueOutput(`\x1b[?1049h${'x'.repeat(80)}`);
processor.enqueueOutput('READY');
assert.equal(processor.getPendingOutputBytes(), 8);
while (scheduled.length > 0) {
scheduled.shift()?.();
}
assert.deepEqual(scannable, []);
});