| |
| |
| |
| |
| |
|
|
| import { vi, describe, expect, it, beforeEach, afterEach } from 'vitest'; |
| import { readStdin } from './readStdin.js'; |
| import { debugLogger } from '@google/gemini-cli-core'; |
|
|
| vi.mock('@google/gemini-cli-core', () => ({ |
| debugLogger: { |
| warn: vi.fn(), |
| }, |
| })); |
|
|
| |
| const mockStdin = { |
| setEncoding: vi.fn(), |
| read: vi.fn(), |
| on: vi.fn(), |
| removeListener: vi.fn(), |
| destroy: vi.fn(), |
| listeners: vi.fn().mockReturnValue([]), |
| listenerCount: vi.fn().mockReturnValue(0), |
| }; |
|
|
| describe('readStdin', () => { |
| let originalStdin: typeof process.stdin; |
| let onReadableHandler: () => void; |
| let onEndHandler: () => void; |
| let onErrorHandler: (err: Error) => void; |
|
|
| beforeEach(() => { |
| vi.clearAllMocks(); |
| originalStdin = process.stdin; |
|
|
| |
| Object.defineProperty(process, 'stdin', { |
| value: mockStdin, |
| writable: true, |
| configurable: true, |
| }); |
|
|
| |
| mockStdin.on.mockImplementation( |
| (event: string, handler: (...args: unknown[]) => void) => { |
| if (event === 'readable') onReadableHandler = handler as () => void; |
| if (event === 'end') onEndHandler = handler as () => void; |
| if (event === 'error') onErrorHandler = handler as (err: Error) => void; |
| }, |
| ); |
| mockStdin.listeners.mockReturnValue([]); |
| mockStdin.listenerCount.mockReturnValue(0); |
| }); |
|
|
| afterEach(() => { |
| vi.restoreAllMocks(); |
| Object.defineProperty(process, 'stdin', { |
| value: originalStdin, |
| writable: true, |
| configurable: true, |
| }); |
| }); |
|
|
| it('should read and accumulate data from stdin', async () => { |
| mockStdin.read |
| .mockReturnValueOnce('I love ') |
| .mockReturnValueOnce('Gemini!') |
| .mockReturnValueOnce(null); |
|
|
| const promise = readStdin(); |
|
|
| |
| onReadableHandler(); |
|
|
| |
| onEndHandler(); |
|
|
| await expect(promise).resolves.toBe('I love Gemini!'); |
| }); |
|
|
| it('should handle empty stdin input', async () => { |
| mockStdin.read.mockReturnValue(null); |
|
|
| const promise = readStdin(); |
|
|
| |
| onEndHandler(); |
|
|
| await expect(promise).resolves.toBe(''); |
| }); |
|
|
| |
| it('should timeout and resolve with empty string when no input is available', async () => { |
| vi.useFakeTimers(); |
|
|
| const promise = readStdin(); |
|
|
| |
| vi.advanceTimersByTime(500); |
|
|
| await expect(promise).resolves.toBe(''); |
|
|
| vi.useRealTimers(); |
| }); |
|
|
| it('should clear timeout once when data is received and resolve with data', async () => { |
| const clearTimeoutSpy = vi.spyOn(global, 'clearTimeout'); |
| mockStdin.read |
| .mockReturnValueOnce('chunk1') |
| .mockReturnValueOnce('chunk2') |
| .mockReturnValueOnce(null); |
|
|
| const promise = readStdin(); |
|
|
| |
| onReadableHandler(); |
|
|
| expect(clearTimeoutSpy).toHaveBeenCalledOnce(); |
|
|
| |
| onEndHandler(); |
|
|
| await expect(promise).resolves.toBe('chunk1chunk2'); |
| }); |
|
|
| it('should truncate input if it exceeds MAX_STDIN_SIZE', async () => { |
| const MAX_STDIN_SIZE = 8 * 1024 * 1024; |
| const largeChunk = 'a'.repeat(MAX_STDIN_SIZE + 100); |
| mockStdin.read.mockReturnValueOnce(largeChunk).mockReturnValueOnce(null); |
|
|
| const promise = readStdin(); |
| onReadableHandler(); |
|
|
| await expect(promise).resolves.toBe('a'.repeat(MAX_STDIN_SIZE)); |
| expect(debugLogger.warn).toHaveBeenCalledWith( |
| `Warning: stdin input truncated to ${MAX_STDIN_SIZE} bytes.`, |
| ); |
| expect(mockStdin.destroy).toHaveBeenCalled(); |
| }); |
|
|
| it('should truncate multi-byte characters at byte boundary', async () => { |
| const MAX_STDIN_SIZE = 8 * 1024 * 1024; |
| |
| |
| const charCount = Math.floor(MAX_STDIN_SIZE / 3) + 1; |
| const multiByteChunk = '한'.repeat(charCount); |
|
|
| mockStdin.read |
| .mockReturnValueOnce(multiByteChunk) |
| .mockReturnValueOnce(null); |
|
|
| const promise = readStdin(); |
| onReadableHandler(); |
|
|
| const result = await promise; |
| const resultBytes = Buffer.byteLength(result, 'utf8'); |
|
|
| expect(resultBytes).toBeLessThanOrEqual(MAX_STDIN_SIZE); |
| expect(resultBytes).toBe(Math.floor(MAX_STDIN_SIZE / 3) * 3); |
| expect(result).not.toContain('\uFFFD'); |
| }); |
|
|
| it('should use byte length instead of string length for limit', async () => { |
| const MAX_STDIN_SIZE = 8 * 1024 * 1024; |
| |
| |
| const charCount = MAX_STDIN_SIZE; |
| const multiByteChunk = '한'.repeat(charCount); |
|
|
| mockStdin.read |
| .mockReturnValueOnce(multiByteChunk) |
| .mockReturnValueOnce(null); |
|
|
| const promise = readStdin(); |
| onReadableHandler(); |
|
|
| const result = await promise; |
| expect(Buffer.byteLength(result, 'utf8')).toBeLessThanOrEqual( |
| MAX_STDIN_SIZE, |
| ); |
| expect(result.length).toBeLessThan(charCount); |
| }); |
|
|
| it('should handle stdin error', async () => { |
| const promise = readStdin(); |
| const error = new Error('stdin error'); |
| onErrorHandler(error); |
| await expect(promise).rejects.toThrow('stdin error'); |
| }); |
| }); |
|
|