From 5d4f4f421ce284b19cab0edb4e989f20e9cf08c9 Mon Sep 17 00:00:00 2001 From: Jacob Richman Date: Mon, 16 Jun 2025 06:25:11 +0000 Subject: feat: text-buffer: input sanitization and delete character handling. (#1031) --- .../src/ui/components/shared/text-buffer.test.ts | 119 ++++++++++++++++++++- 1 file changed, 116 insertions(+), 3 deletions(-) (limited to 'packages/cli/src/ui/components/shared/text-buffer.test.ts') diff --git a/packages/cli/src/ui/components/shared/text-buffer.test.ts b/packages/cli/src/ui/components/shared/text-buffer.test.ts index 56f8e240..c5295fc1 100644 --- a/packages/cli/src/ui/components/shared/text-buffer.test.ts +++ b/packages/cli/src/ui/components/shared/text-buffer.test.ts @@ -585,6 +585,68 @@ describe('useTextBuffer', () => { expect(getBufferState(result).text).toBe(''); }); + it('should handle multiple delete characters in one input', () => { + const { result } = renderHook(() => + useTextBuffer({ + initialText: 'abcde', + viewport, + isValidPath: () => false, + }), + ); + act(() => result.current.move('end')); // cursor at the end + expect(getBufferState(result).cursor).toEqual([0, 5]); + + act(() => { + result.current.applyOperations([ + { type: 'backspace' }, + { type: 'backspace' }, + { type: 'backspace' }, + ]); + }); + expect(getBufferState(result).text).toBe('ab'); + expect(getBufferState(result).cursor).toEqual([0, 2]); + }); + + it('should handle inserts that contain delete characters ', () => { + const { result } = renderHook(() => + useTextBuffer({ + initialText: 'abcde', + viewport, + isValidPath: () => false, + }), + ); + act(() => result.current.move('end')); // cursor at the end + expect(getBufferState(result).cursor).toEqual([0, 5]); + + act(() => { + result.current.applyOperations([ + { type: 'insert', payload: '\x7f\x7f\x7f' }, + ]); + }); + expect(getBufferState(result).text).toBe('ab'); + expect(getBufferState(result).cursor).toEqual([0, 2]); + }); + + it('should handle inserts with a mix of regular and delete characters ', () => { + const { result } = renderHook(() => + useTextBuffer({ + initialText: 'abcde', + viewport, + isValidPath: () => false, + }), + ); + act(() => result.current.move('end')); // cursor at the end + expect(getBufferState(result).cursor).toEqual([0, 5]); + + act(() => { + result.current.applyOperations([ + { type: 'insert', payload: '\x7fI\x7f\x7fNEW' }, + ]); + }); + expect(getBufferState(result).text).toBe('abcNEW'); + expect(getBufferState(result).cursor).toEqual([0, 6]); + }); + it('should handle arrow keys for movement', () => { const { result } = renderHook(() => useTextBuffer({ @@ -632,9 +694,13 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots ); // Simulate pasting the long text multiple times - act(() => result.current.insertStr(longText)); - act(() => result.current.insertStr(longText)); - act(() => result.current.insertStr(longText)); + act(() => { + result.current.applyOperations([ + { type: 'insert', payload: longText }, + { type: 'insert', payload: longText }, + { type: 'insert', payload: longText }, + ]); + }); const state = getBufferState(result); // Check that the text is the result of three concatenations. @@ -792,6 +858,53 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots expect(state.cursor).toEqual([0, 3]); // After 'X' }); }); + + describe('Input Sanitization', () => { + it('should strip ANSI escape codes from input', () => { + const { result } = renderHook(() => + useTextBuffer({ viewport, isValidPath: () => false }), + ); + const textWithAnsi = '\x1B[31mHello\x1B[0m'; + act(() => result.current.handleInput(textWithAnsi, {})); + expect(getBufferState(result).text).toBe('Hello'); + }); + + it('should strip control characters from input', () => { + const { result } = renderHook(() => + useTextBuffer({ viewport, isValidPath: () => false }), + ); + const textWithControlChars = 'H\x07e\x08l\x0Bl\x0Co'; // BELL, BACKSPACE, VT, FF + act(() => result.current.handleInput(textWithControlChars, {})); + expect(getBufferState(result).text).toBe('Hello'); + }); + + it('should strip mixed ANSI and control characters from input', () => { + const { result } = renderHook(() => + useTextBuffer({ viewport, isValidPath: () => false }), + ); + const textWithMixed = '\u001B[4mH\u001B[0mello'; + act(() => result.current.handleInput(textWithMixed, {})); + expect(getBufferState(result).text).toBe('Hello'); + }); + + it('should not strip standard characters or newlines', () => { + const { result } = renderHook(() => + useTextBuffer({ viewport, isValidPath: () => false }), + ); + const validText = 'Hello World\nThis is a test.'; + act(() => result.current.handleInput(validText, {})); + expect(getBufferState(result).text).toBe(validText); + }); + + it('should sanitize pasted text via handleInput', () => { + const { result } = renderHook(() => + useTextBuffer({ viewport, isValidPath: () => false }), + ); + const pastedText = '\u001B[4mPasted\u001B[4m Text'; + act(() => result.current.handleInput(pastedText, {})); + expect(getBufferState(result).text).toBe('Pasted Text'); + }); + }); }); describe('offsetToLogicalPos', () => { -- cgit v1.2.3