mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-02 05:31:02 +00:00
fix(core): compare default shell output by logical wraps
This commit is contained in:
parent
b30bfa2020
commit
9bdeb71a19
2 changed files with 95 additions and 20 deletions
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue