File size: 5,524 Bytes
7421850
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

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(),
  },
}));

// Mock process.stdin
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;

    // Replace process.stdin with our mock
    Object.defineProperty(process, 'stdin', {
      value: mockStdin,
      writable: true,
      configurable: true,
    });

    // Capture event handlers
    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();

    // Trigger readable event
    onReadableHandler();

    // Trigger end to resolve
    onEndHandler();

    await expect(promise).resolves.toBe('I love Gemini!');
  });

  it('should handle empty stdin input', async () => {
    mockStdin.read.mockReturnValue(null);

    const promise = readStdin();

    // Trigger end immediately
    onEndHandler();

    await expect(promise).resolves.toBe('');
  });

  // Emulate terminals where stdin is not TTY (eg: git bash)
  it('should timeout and resolve with empty string when no input is available', async () => {
    vi.useFakeTimers();

    const promise = readStdin();

    // Fast-forward past the timeout (to run test faster)
    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();

    // Trigger readable event
    onReadableHandler();

    expect(clearTimeoutSpy).toHaveBeenCalledOnce();

    // Trigger end to resolve
    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;
    // '한' is 3 bytes. 2,796,202 * 3 = 8,388,606 bytes.
    // 2,796,203 * 3 = 8,388,609 bytes.
    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'); // No replacement characters
  });

  it('should use byte length instead of string length for limit', async () => {
    const MAX_STDIN_SIZE = 8 * 1024 * 1024;
    // '한' is 3 bytes. If we use string length, we'd allow 8M characters = 24MB.
    // We want to ensure it stops at 8MB.
    const charCount = MAX_STDIN_SIZE; // 8M characters = 24MB
    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');
  });
});