From 5b903918622db3a1833ada29d93b4b28cb3b0a4b Mon Sep 17 00:00:00 2001 From: Mark Bennett Date: Tue, 10 Feb 2026 10:04:57 -0700 Subject: [PATCH] Test stdin reading in body-input via process mock Replaces the skipped stdin placeholder with three real tests covering: - reading a single data chunk from stdin - concatenating multiple chunks - propagating stdin error events process.stdin is non-configurable on the real process object, so the tests mock the entire node:process module using a Proxy that intercepts the stdin property and returns a local EventEmitter fixture while forwarding all other property accesses to the real module. The readFromStdin Promise constructor registers handlers synchronously, so events can be emitted immediately after readBodyInput returns its pending Promise without needing setImmediate. Co-Authored-By: Claude Sonnet 4.5 --- tests/utils/body-input.test.ts | 65 +++++++++++++++++++++++++++++----- 1 file changed, 56 insertions(+), 9 deletions(-) diff --git a/tests/utils/body-input.test.ts b/tests/utils/body-input.test.ts index b9c2a0e..14748d9 100644 --- a/tests/utils/body-input.test.ts +++ b/tests/utils/body-input.test.ts @@ -1,9 +1,26 @@ +import { EventEmitter } from 'node:events'; import * as fs from 'node:fs/promises'; import * as path from 'node:path'; -import * as process from 'node:process'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { readBodyInput } from '../../src/utils/body-input.js'; +// Mutable reference updated per stdin test; null means fall through to real stdin +let currentMockStdin: (EventEmitter & { resume: () => void }) | null = null; + +// Mock node:process so process.stdin can be swapped per test without needing +// to redefine the non-configurable property on the real process object +vi.mock('node:process', async (importOriginal) => { + const actual = await importOriginal(); + return new Proxy(actual as object, { + get(target, prop, receiver) { + if (prop === 'stdin' && currentMockStdin !== null) { + return currentMockStdin; + } + return Reflect.get(target, prop, receiver); + }, + }); +}); + describe('readBodyInput', () => { describe('direct string input', () => { it('should return body string when provided', async () => { @@ -82,13 +99,43 @@ describe('readBodyInput', () => { }); describe('stdin input', () => { - // Note: Stdin reading is tested via integration tests - // Mocking process.stdin is complex and unreliable in unit tests - // The implementation is straightforward and covered by: - // 1. File I/O tests (same event-driven patterns) - // 2. Integration tests with real stdin - it.skip('stdin reading is tested via integration tests', () => { - // Placeholder to document testing approach + afterEach(() => { + currentMockStdin = null; + }); + + it('should read content from stdin when - is provided', async () => { + const mockStdin = Object.assign(new EventEmitter(), { resume: vi.fn() }); + currentMockStdin = mockStdin; + + // readBodyInput registers handlers synchronously inside the Promise + // constructor before returning, so we can emit immediately after + const readPromise = readBodyInput(undefined, '-'); + mockStdin.emit('data', Buffer.from('hello from stdin')); + mockStdin.emit('end'); + + expect(await readPromise).toBe('hello from stdin'); + }); + + it('should concatenate multiple chunks from stdin', async () => { + const mockStdin = Object.assign(new EventEmitter(), { resume: vi.fn() }); + currentMockStdin = mockStdin; + + const readPromise = readBodyInput(undefined, '-'); + mockStdin.emit('data', Buffer.from('chunk1')); + mockStdin.emit('data', Buffer.from(' chunk2')); + mockStdin.emit('end'); + + expect(await readPromise).toBe('chunk1 chunk2'); + }); + + it('should throw when stdin emits an error', async () => { + const mockStdin = Object.assign(new EventEmitter(), { resume: vi.fn() }); + currentMockStdin = mockStdin; + + const readPromise = readBodyInput(undefined, '-'); + mockStdin.emit('error', new Error('read error')); + + await expect(readPromise).rejects.toThrow('Failed to read from stdin: read error'); }); }); -- 2.51.2