fix(core): compare default shell output by logical wraps

This commit is contained in:
秦奇 2026-04-24 16:30:05 +08:00
parent b30bfa2020
commit 9bdeb71a19
2 changed files with 95 additions and 20 deletions

View file

@ -167,6 +167,21 @@ const expectNormalizedWindowsPathEnv = (env: NodeJS.ProcessEnv) => {
expect(env['Path']).toBeUndefined(); expect(env['Path']).toBeUndefined();
}; };
const waitForDataEventCount = async (
onOutputEventMock: Mock<(event: ShellOutputEvent) => void>,
expectedCount: number,
) => {
for (let attempt = 0; attempt < 20; attempt++) {
const dataEvents = onOutputEventMock.mock.calls.filter(
([event]) => event.type === 'data',
);
if (dataEvents.length >= expectedCount) {
return;
}
await new Promise((resolve) => setTimeout(resolve, 0));
}
};
describe('ShellExecutionService', () => { describe('ShellExecutionService', () => {
let mockPtyProcess: EventEmitter & { let mockPtyProcess: EventEmitter & {
pid: number; pid: number;
@ -835,6 +850,47 @@ describe('ShellExecutionService', () => {
); );
}); });
it('does not re-emit default plain live output when only soft-wrap segmentation changes', async () => {
const abortController = new AbortController();
const handle = await ShellExecutionService.execute(
'narrow-output',
'/test/dir',
onOutputEventMock,
abortController.signal,
true,
{
...shellExecutionConfig,
terminalWidth: 4,
terminalHeight: 4,
showColor: false,
disableDynamicLineTrimming: false,
},
);
await new Promise((resolve) => process.nextTick(resolve));
mockPtyProcess.onData.mock.calls[0][0]('abcdefgh');
await waitForDataEventCount(onOutputEventMock, 1);
ShellExecutionService.resizePty(handle.pid!, 2, 4);
mockPtyProcess.onData.mock.calls[0][0]('\r');
mockPtyProcess.onExit.mock.calls[0][0]({ exitCode: 0, signal: null });
await handle.result;
const dataEvents = onOutputEventMock.mock.calls.filter(
([event]) => event.type === 'data',
);
expect(dataEvents).toHaveLength(1);
const firstDataEvent = dataEvents[0][0];
if (firstDataEvent.type !== 'data') {
throw new Error('Expected a shell data event.');
}
const chunk = firstDataEvent.chunk as AnsiOutput;
expect(chunk.map((line) => line[0]?.text).filter(Boolean)).toEqual([
'abcd',
'efgh',
]);
});
it('should handle multi-line output correctly when showColor is false', async () => { it('should handle multi-line output correctly when showColor is false', async () => {
await simulateExecution( await simulateExecution(
'ls --color=auto', 'ls --color=auto',

View file

@ -205,6 +205,40 @@ const areAnsiOutputsEqual = (
}); });
}; };
const createPlainAnsiLine = (text: string) => [
{
text,
bold: false,
italic: false,
underline: false,
dim: false,
inverse: false,
fg: '',
bg: '',
},
];
const serializePlainViewportToAnsiOutput = (
terminal: pkg.Terminal,
unwrapWrappedLines = false,
): AnsiOutput => {
const buffer = terminal.buffer.active;
const lines: AnsiOutput = [];
for (let y = 0; y < terminal.rows; y++) {
const line = buffer.getLine(buffer.viewportY + y);
const lineContent = line ? line.translateToString(true) : '';
if (unwrapWrappedLines && line?.isWrapped && lines.length > 0) {
lines[lines.length - 1][0].text += lineContent;
} else {
lines.push(createPlainAnsiLine(lineContent));
}
}
return lines;
};
interface ProcessCleanupStrategy { interface ProcessCleanupStrategy {
killPty(pid: number, pty: ActivePty): void; killPty(pid: number, pty: ActivePty): void;
killChildProcesses(pids: Set<number>): void; killChildProcesses(pids: Set<number>): void;
@ -615,26 +649,11 @@ export class ShellExecutionService {
{ unwrapWrappedLines: true }, { unwrapWrappedLines: true },
); );
} else { } else {
const buffer = headlessTerminal.buffer.active; newOutput = serializePlainViewportToAnsiOutput(headlessTerminal);
const lines: AnsiOutput = []; newOutputComparison = serializePlainViewportToAnsiOutput(
for (let y = 0; y < headlessTerminal.rows; y++) { headlessTerminal,
const line = buffer.getLine(buffer.viewportY + y); true,
const lineContent = line ? line.translateToString(true) : ''; );
lines.push([
{
text: lineContent,
bold: false,
italic: false,
underline: false,
dim: false,
inverse: false,
fg: '',
bg: '',
},
]);
}
newOutput = lines;
newOutputComparison = lines;
} }
const trimmedOutput = trimTrailingEmptyAnsiLines(newOutput); const trimmedOutput = trimTrailingEmptyAnsiLines(newOutput);