Skip to content

Conversation

@ljonesfl
Copy link
Member

@ljonesfl ljonesfl commented Dec 30, 2025

Summary by CodeRabbit

  • New Features

    • Pluggable CLI input handling with prompt, confirm, secret and choice helpers, enabling injectable readers for interactive flows and testing.
  • Documentation

    • Expanded README with thorough examples for testing CLI commands (non-interactive and interactive), input-reader usage, and asserting prompts/exit codes.
  • Tests

    • New comprehensive tests for input readers and command integration, plus a deterministic test input helper with response queue and prompt history.
  • Chores

    • Changelog updated with testability and CLI input handling notes.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Dec 30, 2025

📝 Walkthrough

Walkthrough

Adds a testable CLI input abstraction: a new IInputReader interface, production StdinInputReader, test double TestInputReader, integration into Command with helper methods and setter injection, PHPUnit tests, README examples, and changelog updates.

Changes

Cohort / File(s) Summary
Interface
src/Cli/IO/IInputReader.php
New public interface declaring prompt(string): string, confirm(string, bool): bool, secret(string): string, choice(string, array, ?string): string.
Production reader
src/Cli/IO/StdinInputReader.php
New IInputReader implementation: writes prompts via Output, reads STDIN, confirm parsing, secret input with TTY/echo handling, indexed/text choice selection with iterative retries and MAX_RETRY_ATTEMPTS.
Test reader
src/Cli/IO/TestInputReader.php
New test double implementing IInputReader with queued responses API (addResponse(s)), prompt history, state queries (hasMoreResponses, getRemainingResponseCount), and reset() for deterministic testing.
Base Command integration
src/Cli/Commands/Command.php
Adds protected ?IInputReader $inputReader, setInputReader(IInputReader): self, protected getInputReader(): IInputReader (lazy init), and helper methods prompt(), confirm(), secret(), choice() delegating to the reader (default StdinInputReader).
Tests & test helpers
tests/Cli/IO/TestInputReaderTest.php, tests/Cli/IO/StdinInputReaderTest.php, tests/Cli/Commands/CommandTest.php
Adds PHPUnit tests covering TestInputReader behavior/edge cases, StdinInputReader confirm/choice/secret handling (with mocks), and Command integration. Introduces InteractiveTestCommand test helper exposing protected helpers.
Docs & changelog
readme.md, versionlog.md
Extensive README sections demonstrating testing of CLI commands (interactive and non-interactive) and TestInputReader usage; versionlog.md updated with 0.8.9 entries describing the refactor and safety fixes.

Sequence Diagram

sequenceDiagram
    autonumber
    actor User
    participant Command
    participant IInputReader
    participant StdinInputReader
    participant Output
    participant STDIN
    participant TestInputReader

    rect rgb(230,245,255)
    Note over Command,StdinInputReader: Production flow (default reader)
    User->>Command: execute()
    Command->>Command: getInputReader()
    Command->>IInputReader: prompt("...") 
    IInputReader->>StdinInputReader: prompt()
    StdinInputReader->>Output: write(prompt)
    StdinInputReader->>STDIN: read line
    STDIN-->>StdinInputReader: input
    StdinInputReader->>IInputReader: return trimmed input
    IInputReader->>Command: return response
    end

    rect rgb(255,245,230)
    Note over Command,TestInputReader: Test flow (injected TestInputReader)
    Command->>Command: setInputReader(testReader)
    TestInputReader->>TestInputReader: addResponses(["Alice"])
    Command->>IInputReader: prompt("...")
    IInputReader->>TestInputReader: prompt()
    TestInputReader->>TestInputReader: record promptHistory, dequeue response
    TestInputReader->>Command: return "Alice"
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Poem

🐰
I hop through prompts with answers queued,
Silent secrets kept, no input skewed.
Stdin for humans, TestReader for play,
Commands now ask — and tests obey. ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.94% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title 'testability' is vague and generic, using a non-descriptive single word that doesn't convey the specific changes in the changeset. Use a more descriptive title that specifies the main change, such as 'Add IInputReader interface for testable CLI commands' or 'Introduce CLI input reader abstraction for testing'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings

📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 230bf04 and 7364c84.

📒 Files selected for processing (5)
  • src/Cli/Commands/Command.php
  • src/Cli/IO/StdinInputReader.php
  • tests/Cli/Commands/CommandTest.php
  • tests/Cli/IO/StdinInputReaderTest.php
  • versionlog.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/Cli/IO/StdinInputReaderTest.php
🧰 Additional context used
🧬 Code graph analysis (2)
tests/Cli/Commands/CommandTest.php (2)
src/Cli/IO/TestInputReader.php (7)
  • TestInputReader (28-198)
  • addResponse (59-63)
  • getPromptHistory (84-87)
  • prompt (128-141)
  • confirm (146-157)
  • secret (162-167)
  • choice (172-197)
src/Cli/Commands/Command.php (7)
  • setInputReader (341-345)
  • setInput (61-65)
  • Command (15-431)
  • prompt (360-363)
  • confirm (382-385)
  • secret (401-404)
  • choice (427-430)
src/Cli/IO/StdinInputReader.php (4)
src/Cli/Console/Output.php (4)
  • Output (9-403)
  • writeln (123-131)
  • error (202-205)
  • warning (191-194)
src/Cli/Commands/Command.php (4)
  • prompt (360-363)
  • confirm (382-385)
  • secret (401-404)
  • choice (427-430)
src/Cli/IO/IInputReader.php (4)
  • prompt (22-22)
  • confirm (34-34)
  • secret (45-45)
  • choice (58-58)
src/Cli/IO/TestInputReader.php (4)
  • prompt (128-141)
  • confirm (146-157)
  • secret (162-167)
  • choice (172-197)
🔇 Additional comments (16)
versionlog.md (1)

1-8: Excellent changelog documentation.

The version 0.8.9 entries comprehensively document the testability refactoring and critical fixes. The clear descriptions and appropriate use of CRITICAL labels will help users understand the importance of these changes.

src/Cli/Commands/Command.php (5)

7-8: LGTM! Clean dependency imports.

The new use statements properly import the IInputReader abstraction and its default implementation.


19-19: LGTM! Proper nullable property declaration.

The nullable IInputReader property enables lazy initialization and dependency injection.


308-330: Excellent fix for the uninitialized property concern.

The defensive guard at lines 322-324 properly addresses the previous review comment by lazily initializing a default Output instance when needed. This prevents uninitialized typed property errors while maintaining flexibility.


332-345: LGTM! Clean dependency injection setter.

The fluent interface and comprehensive documentation make this setter easy to use in tests and production code.


347-430: Excellent implementation of input helper methods.

The delegation pattern is clean and consistent across all four methods (prompt, confirm, secret, choice). The comprehensive PHPDoc with practical examples makes these methods easy to understand and use. Protected visibility properly encapsulates the interactive functionality while keeping it accessible to command subclasses.

tests/Cli/Commands/CommandTest.php (3)

8-9: LGTM! Proper test dependencies.

The imports bring in the necessary testing infrastructure for input reader functionality.


174-383: Excellent test coverage for input reader functionality.

The test suite comprehensively covers:

  • Dependency injection and lazy initialization
  • Default creation and defensive Output handling
  • Delegation to input reader for all interactive methods
  • Edge cases like missing Output and empty responses
  • Both direct text and numeric index selection for choices

The tests are well-structured, properly use reflection for protected members, and verify both behavior and integration.


416-456: Clean test helper implementation.

The InteractiveTestCommand wrapper class provides a maintainable way to test protected methods without excessive reflection in test code. The public wrapper methods clearly delegate to their protected counterparts.

src/Cli/IO/StdinInputReader.php (7)

1-29: LGTM! Solid class structure.

The MAX_RETRY_ATTEMPTS constant addresses the stack overflow concern mentioned in the past review. Property promotion in the constructor is clean and modern.


34-39: LGTM! Clean prompt implementation.

Proper handling of fgets() return value and trimming ensures consistent behavior.


44-56: Excellent empty input handling.

The strict equality check $response === '' at line 50 correctly fixes the empty() bug mentioned in the changelog, where '0' was incorrectly treated as empty. This implementation will properly accept '0' as a negative response.


61-88: Excellent implementation with critical safety features.

This method properly addresses two CRITICAL concerns from the changelog:

  1. Exception safety (lines 70-76): The try-finally block ensures terminal echo is always restored, preventing broken terminal states
  2. TTY check (line 66): The isTty() guard prevents stty errors in non-interactive environments like CI/CD pipelines

The defensive fallback for Windows and non-TTY environments ensures the command works across different execution contexts.


98-113: Good defensive TTY detection with proper fallbacks.

The layered approach (posix_isatty → stream_isatty → false) ensures maximum compatibility. The error suppression operators at lines 102 and 107 are acceptable here as they're paired with explicit fallback logic and prevent warnings in edge-case environments.


118-166: Excellent fix for the recursion issue.

The iterative loop with MAX_RETRY_ATTEMPTS completely addresses the unbounded recursion concern from the past review. Additional improvements:

  • Displaying options only on the first attempt (lines 123-135) reduces visual clutter during retries
  • The strict equality check for empty input (line 142) correctly handles '0' as a valid response
  • Error messages are only shown when retries remain (line 162), providing good UX

168-177: LGTM! Proper fallback handling.

The method gracefully falls back to the default with a clear warning, or throws an informative exception if no fallback is available. This provides good user experience and debugging information.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link

codecov bot commented Dec 30, 2025

Welcome to Codecov 🎉

Once you merge this PR into your default branch, you're all set! Codecov will compare coverage reports and display results in all future pull requests.

ℹ️ You can also turn on project coverage checks and project coverage reporting on Pull Request comment

Thanks for integrating Codecov - We've got you covered ☂️

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (4)
versionlog.md (1)

1-2: Consider adding a release date for consistency.

Other version entries include dates (e.g., ## 0.8.8 2025-11-28), but 0.8.9 is missing one. Consider adding the date when this version is released to maintain consistency in the changelog format.

tests/Cli/IO/TestInputReaderTest.php (2)

41-47: Consider testing the full exception message.

The exception message includes additional context about available responses and usage hints. You might want to verify the complete message or use assertStringContainsString for the additional parts to ensure the error guidance is correct.


107-135: Consider adding a test for invalid choice input.

The TestInputReader::choice() method returns the raw response when it doesn't match any option (lines 194-196 in TestInputReader.php). Adding a test to verify this behavior would improve coverage:

public function testChoiceReturnsRawResponseForInvalidInput(): void
{
    $options = ['option1', 'option2'];
    $this->reader->addResponse('invalid');

    $result = $this->reader->choice('Select:', $options);

    $this->assertEquals('invalid', $result);
}
src/Cli/IO/StdinInputReader.php (1)

54-74: Consider error handling for stty commands.

The system() calls don't check for failures. If stty is unavailable or fails, the code continues without indication. Consider capturing the return value or using shell_exec with error handling.

🔎 Possible improvement
 // Only hide input on Unix-like systems
 if( strtoupper( substr( PHP_OS, 0, 3 ) ) !== 'WIN' ) {
 	// Disable terminal echo
-	system( 'stty -echo' );
+	$result = @system( 'stty -echo 2>/dev/null', $retval );
 	$input = fgets( STDIN );
 	// Re-enable terminal echo
-	system( 'stty echo' );
+	@system( 'stty echo 2>/dev/null' );
 	// Add newline since user's enter key wasn't echoed
 	$this->output->writeln( '' );
 } else {
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between da03d32 and eee42c9.

📒 Files selected for processing (7)
  • readme.md
  • src/Cli/Commands/Command.php
  • src/Cli/IO/IInputReader.php
  • src/Cli/IO/StdinInputReader.php
  • src/Cli/IO/TestInputReader.php
  • tests/Cli/IO/TestInputReaderTest.php
  • versionlog.md
🔇 Additional comments (13)
tests/Cli/IO/TestInputReaderTest.php (2)

8-16: Good test structure with proper setup.

The test class properly initializes a fresh TestInputReader instance in setUp(), ensuring test isolation.


167-186: LGTM!

The reset and fluent interface tests properly verify state clearing and method chaining behavior.

readme.md (3)

477-506: Clear documentation for non-interactive command testing.

The example demonstrates proper setup for testing commands without user input, including mock input/output configuration.


508-578: Excellent documentation for interactive command testing.

The examples clearly demonstrate how to use TestInputReader with pre-programmed responses and how to verify prompt history. The cancellation test case is a valuable addition showing negative path testing.


580-638: Good feature documentation for TestInputReader.

The feature list and code example effectively communicate the test double's capabilities including response queue, prompt history, and fluent interface.

src/Cli/IO/IInputReader.php (1)

14-59: Well-designed interface for input abstraction.

The interface is clean, focused, and provides a good abstraction for CLI input operations. The method signatures are consistent and the PHPDoc documentation is thorough.

src/Cli/Commands/Command.php (3)

7-8: LGTM!

Clean integration of the input reader dependency with proper imports and nullable property declaration.

Also applies to: 19-19


334-338: Good dependency injection pattern.

The setter allows test doubles to be injected while maintaining the fluent interface pattern consistent with other setters in this class.


353-423: Well-documented convenience methods.

The helper methods are well-documented with examples and properly delegate to the input reader. This creates a clean API for command authors.

src/Cli/IO/StdinInputReader.php (1)

20-32: LGTM!

Clean constructor with dependency injection and simple prompt implementation.

src/Cli/IO/TestInputReader.php (3)

28-75: Well-designed test double with fluent interface.

The queue-based response mechanism with method chaining provides a clean API for test setup.


128-141: Excellent error messaging.

The exception provides actionable debugging information including the prompt index, message, and available response count. This will help developers quickly identify missing test setup.


172-197: Intentional behavioral difference from production is well-documented.

The comment on lines 194-196 clearly explains why invalid choices return the raw response instead of re-prompting, allowing tests to verify behavior for edge cases. This is a sensible design decision for a test double.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
src/Cli/IO/StdinInputReader.php (1)

115-120: Unbounded recursion on invalid input.

If a user repeatedly enters invalid choices, this recursive call could theoretically cause a stack overflow. Consider converting to an iterative loop or adding a maximum retry limit.

🔎 Iterative alternative
 public function choice( string $message, array $options, ?string $default = null ): string
 {
+	while( true ) {
 		// Display the prompt message
 		$this->output->writeln( $message );
 		$this->output->writeln( '' );

 		// Display options with index numbers
 		foreach( $options as $index => $option ) {
 			$marker = ($default === $option) ? '*' : ' ';
 			$this->output->writeln( "  [{$marker}] {$index}. {$option}" );
 		}

 		$this->output->writeln( '' );

 		// Prompt for selection
 		$prompt = $default !== null ? "Choice [{$default}]: " : "Choice: ";
 		$response = $this->prompt( $prompt );

 		// If user just presses enter and there's a default, use it
 		if( $response === '' && $default !== null ) {
 			return $default;
 		}

 		// Check if response is a numeric index
 		if( is_numeric( $response ) ) {
 			$index = (int)$response;
 			if( isset( $options[$index] ) ) {
 				return $options[$index];
 			}
 		}

 		// Check if response matches an option exactly
 		if( in_array( $response, $options, true ) ) {
 			return $response;
 		}

 		// Invalid choice - ask again
 		$this->output->error( "Invalid choice. Please try again." );
 		$this->output->writeln( '' );
-
-		return $this->choice( $message, $options, $default );
+	}
 }
🧹 Nitpick comments (1)
tests/Cli/IO/StdinInputReaderTest.php (1)

44-90: Consider using PHPUnit data providers for parameterized tests.

The current approach of recreating mocks within loops works correctly but is unconventional. A data provider would be more idiomatic, efficient, and clearer.

🔎 Data provider approach
+	/**
+	 * @dataProvider confirmYesResponseProvider
+	 */
+	public function testConfirmAcceptsYesResponse( string $response ): void
+	{
+		$mock = $this->getMockBuilder( StdinInputReader::class )
+			->setConstructorArgs( [$this->output] )
+			->onlyMethods( ['prompt'] )
+			->getMock();
+
+		$mock->expects( $this->once() )
+			->method( 'prompt' )
+			->willReturn( $response );
+
+		$result = $mock->confirm( 'Continue?', false );
+
+		$this->assertTrue( $result, "Response '{$response}' should be treated as positive" );
+	}
+
+	public function confirmYesResponseProvider(): array
+	{
+		return [
+			'lowercase y' => ['y'],
+			'lowercase yes' => ['yes'],
+			'uppercase YES' => ['YES'],
+			'mixed case Yes' => ['Yes'],
+			'lowercase true' => ['true'],
+			'uppercase TRUE' => ['TRUE'],
+			'numeric 1' => ['1'],
+		];
+	}

Apply the same pattern for testConfirmRejectsNoResponse.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between eee42c9 and 230bf04.

📒 Files selected for processing (5)
  • src/Cli/IO/StdinInputReader.php
  • src/Cli/IO/TestInputReader.php
  • tests/Cli/Commands/CommandTest.php
  • tests/Cli/IO/StdinInputReaderTest.php
  • tests/Cli/IO/TestInputReaderTest.php
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/Cli/IO/TestInputReaderTest.php
  • src/Cli/IO/TestInputReader.php
🧰 Additional context used
🧬 Code graph analysis (3)
tests/Cli/Commands/CommandTest.php (1)
src/Cli/IO/TestInputReader.php (7)
  • TestInputReader (28-198)
  • addResponse (59-63)
  • getPromptHistory (84-87)
  • prompt (128-141)
  • confirm (146-157)
  • secret (162-167)
  • choice (172-197)
tests/Cli/IO/StdinInputReaderTest.php (2)
src/Cli/IO/StdinInputReader.php (3)
  • StdinInputReader (15-121)
  • confirm (37-49)
  • choice (79-120)
src/Cli/IO/TestInputReader.php (2)
  • confirm (146-157)
  • choice (172-197)
src/Cli/IO/StdinInputReader.php (4)
src/Cli/Console/Output.php (3)
  • Output (9-403)
  • writeln (123-131)
  • error (202-205)
src/Cli/IO/TestInputReader.php (4)
  • prompt (128-141)
  • confirm (146-157)
  • secret (162-167)
  • choice (172-197)
src/Cli/IO/IInputReader.php (4)
  • prompt (22-22)
  • confirm (34-34)
  • secret (45-45)
  • choice (58-58)
src/Cli/Commands/Command.php (4)
  • prompt (353-356)
  • confirm (375-378)
  • secret (394-397)
  • choice (420-423)
🔇 Additional comments (13)
tests/Cli/Commands/CommandTest.php (4)

8-9: LGTM!

The new imports support the input reader integration tests added in this file.


174-218: LGTM!

Comprehensive test coverage of the input reader getter/setter functionality with appropriate use of reflection to verify protected members.


220-332: LGTM!

Comprehensive test coverage of the delegation pattern for interactive methods. Tests verify correct delegation to the input reader, response handling, and edge cases including defaults and numeric indices.


365-405: LGTM!

Clean test helper implementation that exposes protected interactive methods for testing. This approach is more maintainable than using reflection in each test method.

src/Cli/IO/StdinInputReader.php (4)

20-22: LGTM!

Clean constructor using PHP 8+ promoted property syntax. The Output dependency is appropriately required for prompt display.


27-32: LGTM!

Correct implementation with proper error handling for fgets() returning false and appropriate trimming of user input.


37-49: LGTM!

Good user experience with clear default indicators and case-insensitive acceptance of common truthy values (y, yes, true, 1).


54-74: LGTM!

The platform-specific echo hiding is implemented securely with hardcoded system() arguments (no injection risk). The Windows fallback is appropriately documented.

tests/Cli/IO/StdinInputReaderTest.php (5)

14-25: LGTM!

Standard test setup and constructor verification.


27-42: LGTM!

Appropriate mocking strategy to test default value handling without STDIN interaction.


92-108: LGTM!

Appropriate verification of the default-based suffix using PHPUnit's stringContains matcher.


110-177: LGTM!

Comprehensive test coverage of choice functionality including default handling, numeric index selection, exact matches, and retry behavior. Good use of willReturnOnConsecutiveCalls for testing the retry flow.


179-192: LGTM!

Limited test scope is appropriate given the challenge of testing terminal echo behavior in unit tests. The comments clearly explain the limitation.

@ljonesfl ljonesfl closed this Dec 30, 2025
@ljonesfl ljonesfl deleted the feature/testability branch December 30, 2025 17:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants