fix(wizard): redirect prompts to stderr for clean command substitution (#130)
When wizard prompt functions (prompt_text, prompt_number, select_option, select_with_default) were used with command substitution, ANSI-colored prompts were captured along with user responses, corrupting .ralphrc values like: PROJECT_NAME="[0;36mProject name[0m [value]: value" Fix: - Redirect all display output (colored prompts, validation messages) to stderr using >&2, matching the pattern already used by select_multiple() - Keep only the actual response/result on stdout for command substitution Changes: - lib/wizard_utils.sh: Add >&2 to prompt_text (lines 84-87), prompt_number (lines 118-121, 131-151), confirm (lines 44, 60), select_option (lines 190-215), select_with_default (lines 330-364) - tests/unit/test_wizard_utils.bats: Add 20 new tests for stdout/stderr separation and clean command substitution results - CLAUDE.md: Update test count to 420 Test count: 420 (up from 396) Co-authored-by: Test User <test@example.com>
This commit is contained in:
parent
910f794fcc
commit
05c57207f3
3 changed files with 238 additions and 26 deletions
|
|
@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||
|
||||
This is the Ralph for Claude Code repository - an autonomous AI development loop system that enables continuous development cycles with intelligent exit detection and rate limiting.
|
||||
|
||||
**Version**: v0.11.0 | **Tests**: 396 passing (100% pass rate) | **CI/CD**: GitHub Actions
|
||||
**Version**: v0.11.0 | **Tests**: 420 passing (100% pass rate) | **CI/CD**: GitHub Actions
|
||||
|
||||
## Core Architecture
|
||||
|
||||
|
|
@ -167,7 +167,7 @@ tmux attach -t <session-name>
|
|||
|
||||
### Running Tests
|
||||
```bash
|
||||
# Run all tests (396 tests)
|
||||
# Run all tests (420 tests)
|
||||
npm test
|
||||
|
||||
# Run specific test suites
|
||||
|
|
@ -402,7 +402,7 @@ Ralph uses advanced error detection with two-stage filtering to eliminate false
|
|||
|
||||
## Test Suite
|
||||
|
||||
### Test Files (396 tests total)
|
||||
### Test Files (420 tests total)
|
||||
|
||||
| File | Tests | Description |
|
||||
|------|-------|-------------|
|
||||
|
|
@ -420,6 +420,7 @@ Ralph uses advanced error detection with two-stage filtering to eliminate false
|
|||
| `test_enable_core.bats` | 30 | Enable core library (idempotency, project detection, template generation) |
|
||||
| `test_task_sources.bats` | 23 | Task sources (beads, GitHub, PRD extraction, normalization) |
|
||||
| `test_ralph_enable.bats` | 22 | Ralph enable integration tests (wizard, CI version, JSON output) |
|
||||
| `test_wizard_utils.bats` | 20 | Wizard utility functions (stdout/stderr separation, prompt functions) |
|
||||
|
||||
### Running Tests
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -41,7 +41,8 @@ confirm() {
|
|||
fi
|
||||
|
||||
while true; do
|
||||
echo -en "${WIZARD_CYAN}${prompt}${WIZARD_NC} ${yn_hint}: "
|
||||
# Display prompt to stderr for consistency with other prompt functions
|
||||
echo -en "${WIZARD_CYAN}${prompt}${WIZARD_NC} ${yn_hint}: " >&2
|
||||
read -r response
|
||||
|
||||
# Handle empty response (use default)
|
||||
|
|
@ -57,7 +58,7 @@ confirm() {
|
|||
return 1
|
||||
;;
|
||||
*)
|
||||
echo -e "${WIZARD_YELLOW}Please answer yes (y) or no (n)${WIZARD_NC}"
|
||||
echo -e "${WIZARD_YELLOW}Please answer yes (y) or no (n)${WIZARD_NC}" >&2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
|
@ -80,10 +81,11 @@ prompt_text() {
|
|||
local default="${2:-}"
|
||||
local response
|
||||
|
||||
# Display prompt to stderr so command substitution only captures the response
|
||||
if [[ -n "$default" ]]; then
|
||||
echo -en "${WIZARD_CYAN}${prompt}${WIZARD_NC} [${default}]: "
|
||||
echo -en "${WIZARD_CYAN}${prompt}${WIZARD_NC} [${default}]: " >&2
|
||||
else
|
||||
echo -en "${WIZARD_CYAN}${prompt}${WIZARD_NC}: "
|
||||
echo -en "${WIZARD_CYAN}${prompt}${WIZARD_NC}: " >&2
|
||||
fi
|
||||
|
||||
read -r response
|
||||
|
|
@ -114,10 +116,11 @@ prompt_number() {
|
|||
local response
|
||||
|
||||
while true; do
|
||||
# Display prompt to stderr so command substitution only captures the response
|
||||
if [[ -n "$default" ]]; then
|
||||
echo -en "${WIZARD_CYAN}${prompt}${WIZARD_NC} [${default}]: "
|
||||
echo -en "${WIZARD_CYAN}${prompt}${WIZARD_NC} [${default}]: " >&2
|
||||
else
|
||||
echo -en "${WIZARD_CYAN}${prompt}${WIZARD_NC}: "
|
||||
echo -en "${WIZARD_CYAN}${prompt}${WIZARD_NC}: " >&2
|
||||
fi
|
||||
|
||||
read -r response
|
||||
|
|
@ -128,25 +131,25 @@ prompt_number() {
|
|||
echo "$default"
|
||||
return 0
|
||||
else
|
||||
echo -e "${WIZARD_YELLOW}Please enter a number${WIZARD_NC}"
|
||||
echo -e "${WIZARD_YELLOW}Please enter a number${WIZARD_NC}" >&2
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
# Validate it's a number
|
||||
if ! [[ "$response" =~ ^[0-9]+$ ]]; then
|
||||
echo -e "${WIZARD_YELLOW}Please enter a valid number${WIZARD_NC}"
|
||||
echo -e "${WIZARD_YELLOW}Please enter a valid number${WIZARD_NC}" >&2
|
||||
continue
|
||||
fi
|
||||
|
||||
# Check range if specified
|
||||
if [[ -n "$min" && "$response" -lt "$min" ]]; then
|
||||
echo -e "${WIZARD_YELLOW}Value must be at least ${min}${WIZARD_NC}"
|
||||
echo -e "${WIZARD_YELLOW}Value must be at least ${min}${WIZARD_NC}" >&2
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ -n "$max" && "$response" -gt "$max" ]]; then
|
||||
echo -e "${WIZARD_YELLOW}Value must be at most ${max}${WIZARD_NC}"
|
||||
echo -e "${WIZARD_YELLOW}Value must be at most ${max}${WIZARD_NC}" >&2
|
||||
continue
|
||||
fi
|
||||
|
||||
|
|
@ -184,20 +187,21 @@ select_option() {
|
|||
return 1
|
||||
fi
|
||||
|
||||
echo -e "\n${WIZARD_BOLD}${prompt}${WIZARD_NC}"
|
||||
echo ""
|
||||
# Display prompt and options to stderr so command substitution only captures the result
|
||||
echo -e "\n${WIZARD_BOLD}${prompt}${WIZARD_NC}" >&2
|
||||
echo "" >&2
|
||||
|
||||
# Display options
|
||||
local i=1
|
||||
for opt in "${options[@]}"; do
|
||||
echo -e " ${WIZARD_CYAN}${i})${WIZARD_NC} ${opt}"
|
||||
echo -e " ${WIZARD_CYAN}${i})${WIZARD_NC} ${opt}" >&2
|
||||
((i++))
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "" >&2
|
||||
|
||||
while true; do
|
||||
echo -en "Select option [1-${num_options}]: "
|
||||
echo -en "Select option [1-${num_options}]: " >&2
|
||||
read -r response
|
||||
|
||||
# Validate it's a number in range
|
||||
|
|
@ -208,7 +212,7 @@ select_option() {
|
|||
echo "${options[$((response - 1))]}"
|
||||
return 0
|
||||
else
|
||||
echo -e "${WIZARD_YELLOW}Please enter a number between 1 and ${num_options}${WIZARD_NC}"
|
||||
echo -e "${WIZARD_YELLOW}Please enter a number between 1 and ${num_options}${WIZARD_NC}" >&2
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
|
@ -323,24 +327,25 @@ select_with_default() {
|
|||
local options=("$@")
|
||||
local num_options=${#options[@]}
|
||||
|
||||
echo -e "\n${WIZARD_BOLD}${prompt}${WIZARD_NC}"
|
||||
echo ""
|
||||
# Display prompt and options to stderr so command substitution only captures the result
|
||||
echo -e "\n${WIZARD_BOLD}${prompt}${WIZARD_NC}" >&2
|
||||
echo "" >&2
|
||||
|
||||
# Display options with default marked
|
||||
local i=1
|
||||
for opt in "${options[@]}"; do
|
||||
if [[ $i -eq $default_index ]]; then
|
||||
echo -e " ${WIZARD_GREEN}${i})${WIZARD_NC} ${opt} ${WIZARD_GREEN}(recommended)${WIZARD_NC}"
|
||||
echo -e " ${WIZARD_GREEN}${i})${WIZARD_NC} ${opt} ${WIZARD_GREEN}(recommended)${WIZARD_NC}" >&2
|
||||
else
|
||||
echo -e " ${WIZARD_CYAN}${i})${WIZARD_NC} ${opt}"
|
||||
echo -e " ${WIZARD_CYAN}${i})${WIZARD_NC} ${opt}" >&2
|
||||
fi
|
||||
((i++))
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "" >&2
|
||||
|
||||
while true; do
|
||||
echo -en "Select option [1-${num_options}] (default: ${default_index}): "
|
||||
echo -en "Select option [1-${num_options}] (default: ${default_index}): " >&2
|
||||
read -r response
|
||||
|
||||
# Use default if empty
|
||||
|
|
@ -356,7 +361,7 @@ select_with_default() {
|
|||
echo "${options[$((response - 1))]}"
|
||||
return 0
|
||||
else
|
||||
echo -e "${WIZARD_YELLOW}Please enter a number between 1 and ${num_options}${WIZARD_NC}"
|
||||
echo -e "${WIZARD_YELLOW}Please enter a number between 1 and ${num_options}${WIZARD_NC}" >&2
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
|
|
|||
206
tests/unit/test_wizard_utils.bats
Normal file
206
tests/unit/test_wizard_utils.bats
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
#!/usr/bin/env bats
|
||||
# Unit tests for wizard_utils.sh
|
||||
# Tests for stdout/stderr separation to prevent ANSI code capture in command substitution
|
||||
|
||||
load '../helpers/test_helper'
|
||||
|
||||
# Path to the library
|
||||
WIZARD_UTILS="${BATS_TEST_DIRNAME}/../../lib/wizard_utils.sh"
|
||||
|
||||
setup() {
|
||||
# Source the wizard utils
|
||||
source "$WIZARD_UTILS"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# PROMPT_TEXT STDOUT/STDERR SEPARATION (4 tests)
|
||||
# =============================================================================
|
||||
|
||||
@test "prompt_text returns only user input on stdout, no ANSI codes" {
|
||||
# Simulate user typing "my-project" followed by Enter
|
||||
result=$(echo "my-project" | prompt_text "Project name" "default")
|
||||
|
||||
# The captured output should be ONLY "my-project", no ANSI escape codes
|
||||
[[ "$result" == "my-project" ]]
|
||||
# Should NOT contain ANSI escape sequence
|
||||
[[ ! "$result" =~ $'\033' ]]
|
||||
[[ ! "$result" =~ "Project name" ]]
|
||||
}
|
||||
|
||||
@test "prompt_text with default returns clean default value on empty input" {
|
||||
# Simulate user pressing Enter (empty input)
|
||||
result=$(echo "" | prompt_text "Project name" "default-value")
|
||||
|
||||
# Should return only the default value
|
||||
[[ "$result" == "default-value" ]]
|
||||
# Should NOT contain ANSI escape sequence
|
||||
[[ ! "$result" =~ $'\033' ]]
|
||||
[[ ! "$result" =~ "Project name" ]]
|
||||
}
|
||||
|
||||
@test "prompt_text without default returns empty string on empty input" {
|
||||
# Simulate user pressing Enter (empty input) with no default
|
||||
result=$(echo "" | prompt_text "Project name")
|
||||
|
||||
# Should return empty string
|
||||
[[ "$result" == "" ]]
|
||||
}
|
||||
|
||||
@test "prompt_text handles special characters in user input" {
|
||||
# Test that special characters are preserved
|
||||
result=$(echo "my project-name_123" | prompt_text "Name" "default")
|
||||
|
||||
[[ "$result" == "my project-name_123" ]]
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# PROMPT_NUMBER STDOUT/STDERR SEPARATION (5 tests)
|
||||
# =============================================================================
|
||||
|
||||
@test "prompt_number returns only numeric value on stdout, no ANSI codes" {
|
||||
# Simulate user typing "100"
|
||||
result=$(echo "100" | prompt_number "Max calls" "50")
|
||||
|
||||
# The captured output should be ONLY "100", no ANSI codes
|
||||
[[ "$result" == "100" ]]
|
||||
# Should NOT contain ANSI escape sequence
|
||||
[[ ! "$result" =~ $'\033' ]]
|
||||
[[ ! "$result" =~ "Max calls" ]]
|
||||
}
|
||||
|
||||
@test "prompt_number with default returns clean default on empty input" {
|
||||
# Simulate user pressing Enter (empty input)
|
||||
result=$(echo "" | prompt_number "Max calls" "50")
|
||||
|
||||
# Should return only the default
|
||||
[[ "$result" == "50" ]]
|
||||
# Should NOT contain ANSI escape sequence
|
||||
[[ ! "$result" =~ $'\033' ]]
|
||||
}
|
||||
|
||||
@test "prompt_number validates min value without ANSI in result" {
|
||||
# First input is too small, second input is valid
|
||||
result=$(printf "5\n20" | prompt_number "Value" "" "10" "100")
|
||||
|
||||
# Should return only the valid number
|
||||
[[ "$result" == "20" ]]
|
||||
[[ ! "$result" =~ $'\033' ]]
|
||||
}
|
||||
|
||||
@test "prompt_number validates max value without ANSI in result" {
|
||||
# First input is too large, second input is valid
|
||||
result=$(printf "150\n80" | prompt_number "Value" "" "10" "100")
|
||||
|
||||
# Should return only the valid number
|
||||
[[ "$result" == "80" ]]
|
||||
[[ ! "$result" =~ $'\033' ]]
|
||||
}
|
||||
|
||||
@test "prompt_number rejects non-numeric input without ANSI in result" {
|
||||
# First input is not a number, second input is valid
|
||||
result=$(printf "abc\n42" | prompt_number "Value" "")
|
||||
|
||||
# Should return only the valid number
|
||||
[[ "$result" == "42" ]]
|
||||
[[ ! "$result" =~ $'\033' ]]
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# CONFIRM STDOUT/STDERR SEPARATION (3 tests)
|
||||
# =============================================================================
|
||||
|
||||
@test "confirm sends prompt to stderr not stdout" {
|
||||
# Capture stdout only - should be empty
|
||||
stdout_output=$(echo "y" | confirm "Continue?" 2>/dev/null)
|
||||
|
||||
# Stdout should be empty (confirm uses return codes, not echo)
|
||||
[[ -z "$stdout_output" ]]
|
||||
}
|
||||
|
||||
@test "confirm yes response returns 0" {
|
||||
echo "y" | confirm "Continue?" 2>/dev/null
|
||||
[[ $? -eq 0 ]]
|
||||
}
|
||||
|
||||
@test "confirm no response returns 1" {
|
||||
# Run confirm with "n" response, expect return code 1 (no)
|
||||
run bash -c 'source '"$WIZARD_UTILS"' && echo "n" | confirm "Continue?" 2>/dev/null'
|
||||
[[ $status -eq 1 ]]
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# SELECT_OPTION STDOUT/STDERR SEPARATION (3 tests)
|
||||
# =============================================================================
|
||||
|
||||
@test "select_option returns only selected option on stdout, no ANSI codes" {
|
||||
# Simulate user selecting option 2
|
||||
result=$(echo "2" | select_option "Choose one" "first" "second" "third")
|
||||
|
||||
# Should return only "second"
|
||||
[[ "$result" == "second" ]]
|
||||
[[ ! "$result" =~ $'\033' ]]
|
||||
[[ ! "$result" =~ "Choose one" ]]
|
||||
}
|
||||
|
||||
@test "select_option handles first option selection" {
|
||||
result=$(echo "1" | select_option "Choose" "alpha" "beta")
|
||||
|
||||
[[ "$result" == "alpha" ]]
|
||||
[[ ! "$result" =~ $'\033' ]]
|
||||
}
|
||||
|
||||
@test "select_option retries on invalid input without ANSI in result" {
|
||||
# First input is invalid, second input is valid
|
||||
result=$(printf "abc\n2" | select_option "Choose" "one" "two")
|
||||
|
||||
[[ "$result" == "two" ]]
|
||||
[[ ! "$result" =~ $'\033' ]]
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# SELECT_WITH_DEFAULT STDOUT/STDERR SEPARATION (3 tests)
|
||||
# =============================================================================
|
||||
|
||||
@test "select_with_default returns selected option on stdout, no ANSI codes" {
|
||||
# Simulate user selecting option 2
|
||||
result=$(echo "2" | select_with_default "Choose" 1 "default" "other")
|
||||
|
||||
[[ "$result" == "other" ]]
|
||||
[[ ! "$result" =~ $'\033' ]]
|
||||
}
|
||||
|
||||
@test "select_with_default returns default on empty input" {
|
||||
# Simulate user pressing Enter
|
||||
result=$(echo "" | select_with_default "Choose" 1 "default" "other")
|
||||
|
||||
[[ "$result" == "default" ]]
|
||||
[[ ! "$result" =~ $'\033' ]]
|
||||
}
|
||||
|
||||
@test "select_with_default handles non-first default" {
|
||||
# Simulate empty input with default_index=2
|
||||
result=$(echo "" | select_with_default "Choose" 2 "first" "second")
|
||||
|
||||
[[ "$result" == "second" ]]
|
||||
[[ ! "$result" =~ $'\033' ]]
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# INTEGRATION: RALPHRC GENERATION (2 tests)
|
||||
# =============================================================================
|
||||
|
||||
@test "prompt_text output is safe for shell variable assignment" {
|
||||
result=$(echo "test-project" | prompt_text "Project" "default")
|
||||
|
||||
# Result should be safe to assign to a shell variable
|
||||
eval "PROJECT_NAME=\"$result\""
|
||||
[[ "$PROJECT_NAME" == "test-project" ]]
|
||||
}
|
||||
|
||||
@test "prompt_number output is safe for shell arithmetic" {
|
||||
result=$(echo "100" | prompt_number "Calls" "50")
|
||||
|
||||
# Result should be a valid number for arithmetic
|
||||
(( result > 0 ))
|
||||
[[ "$result" -eq 100 ]]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue