fix(chat): normalize bash output by stripping ANSI sequences and applying terminal control codes
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
|
||||
import { renderTerminalOutput } from './toolOutput';
|
||||
import { readTaskTagSessionIdFromOutput } from './taskSessionIdParser';
|
||||
import { tryParseJsonOutput } from '../toolRenderers';
|
||||
import { getStreamingThrottleText } from '../../hooks/useStreamingTextThrottle';
|
||||
@@ -7,18 +8,73 @@ import { getStreamingOutputAppend, getToolOutput } from './toolOutput';
|
||||
import { getToolDescriptionFallback } from './toolRenderUtils';
|
||||
|
||||
describe('getToolOutput', () => {
|
||||
test('prefers authoritative state output', () => {
|
||||
expect(getToolOutput('bash', 'final output', 'streamed output')).toBe('final output');
|
||||
expect(getToolOutput('bash', '', 'streamed output')).toBe('');
|
||||
test('prefers state.output for completed tools', () => {
|
||||
expect(getToolOutput('bash', 'final output', 'partial output', 'completed')).toBe('final output');
|
||||
});
|
||||
|
||||
test('falls back to streamed metadata output for bash', () => {
|
||||
expect(getToolOutput('bash', undefined, 'streamed output')).toBe('streamed output');
|
||||
expect(getToolOutput('bash', undefined, '')).toBe(undefined);
|
||||
test('normalizes completed bash state output while preserving final-output precedence', () => {
|
||||
expect(getToolOutput('bash', '\u001B[32mFinal output\u001B[0m', 'partial output', 'completed')).toBe('Final output');
|
||||
});
|
||||
|
||||
test('does not expose metadata output for other tools', () => {
|
||||
expect(getToolOutput('read', undefined, 'metadata output')).toBe(undefined);
|
||||
test('falls back to metadata.output for bash tools without state output', () => {
|
||||
expect(getToolOutput('bash', undefined, 'partial output', 'completed')).toBe('partial output');
|
||||
});
|
||||
|
||||
test('normalizes bash metadata output for completed state', () => {
|
||||
expect(getToolOutput('bash', undefined, 'Progress 10%\r\u001B[2KProgress 90%', 'completed')).toBe('Progress 90%');
|
||||
});
|
||||
|
||||
test('does not normalize bash output while running', () => {
|
||||
expect(getToolOutput('bash', '\u001B[32mRunning\u001B[0m', undefined, 'running')).toBe('\u001B[32mRunning\u001B[0m');
|
||||
expect(getToolOutput('bash', undefined, 'Progress\r\u001B[2K', 'running')).toBe('Progress\r\u001B[2K');
|
||||
});
|
||||
|
||||
test('ignores metadata.output for non-bash tools', () => {
|
||||
expect(getToolOutput('read', undefined, 'partial output', 'completed')).toBe(undefined);
|
||||
expect(getToolOutput('read', 'final output', 'partial output', 'completed')).toBe('final output');
|
||||
});
|
||||
|
||||
test('returns undefined when bash has no output', () => {
|
||||
expect(getToolOutput('bash', undefined, undefined, 'completed')).toBe(undefined);
|
||||
});
|
||||
|
||||
test('ignores empty metadata.output for bash', () => {
|
||||
expect(getToolOutput('bash', undefined, '', 'completed')).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('renderTerminalOutput', () => {
|
||||
test('renders carriage-return progress updates as their latest value', () => {
|
||||
expect(renderTerminalOutput('Downloading 10%\r\u001B[2KDownloading 90%')).toBe('Downloading 90%');
|
||||
});
|
||||
|
||||
test('removes ANSI styles while preserving the output text', () => {
|
||||
expect(renderTerminalOutput('\u001B[32mComplete\u001B[0m\n')).toBe('Complete\n');
|
||||
});
|
||||
|
||||
test('applies cursor-up progress updates to the prior line', () => {
|
||||
expect(renderTerminalOutput('First\nWorking\u001B[1A\r\u001B[2KDone\n')).toBe('Done\nWorking');
|
||||
});
|
||||
|
||||
test('CSI K erases from cursor to end of line', () => {
|
||||
expect(renderTerminalOutput('Hello World\u001B[5G\u001B[K')).toBe('Hell');
|
||||
});
|
||||
|
||||
test('CSI 1 K erases from beginning of line through cursor, preserving suffix', () => {
|
||||
expect(renderTerminalOutput('Hello World\u001B[6G\u001B[1K')).toBe(' World');
|
||||
});
|
||||
|
||||
test('CSI 2 K erases entire line', () => {
|
||||
expect(renderTerminalOutput('Hello World\u001B[2K')).toBe('');
|
||||
});
|
||||
|
||||
test('handles large single-line output without quadratic slowdown', () => {
|
||||
const largeLine = 'A'.repeat(50000) + '\u001B[0m';
|
||||
const start = performance.now();
|
||||
const result = renderTerminalOutput(largeLine);
|
||||
const elapsed = performance.now() - start;
|
||||
expect(result).toBe('A'.repeat(50000));
|
||||
expect(elapsed).toBeLessThan(1000);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user