ralph-claude-code/tests/integration/test_edge_cases.bats
Frank Bria 9b19d70e35
feat(structure): migrate Ralph files to .ralph/ subfolder (#109)
* feat(structure): migrate Ralph files to .ralph/ subfolder

BREAKING CHANGE: Ralph configuration files now live in .ralph/ subfolder

This refactoring moves all Ralph-specific files into a hidden .ralph/
directory while keeping src/ at the project root. This improves
compatibility with existing tooling and keeps the project root clean.

Changes:
- Move PROMPT.md, @fix_plan.md, @AGENT.md to .ralph/
- Move specs/, logs/, docs/generated/, examples/ to .ralph/
- Move state files (.response_analysis, .circuit_breaker_state, etc.) to .ralph/
- Keep src/ at project root (unchanged)
- Add RALPH_DIR=".ralph" configuration variable
- Add ralph-migrate command for existing projects
- Create migrate_to_ralph_folder.sh migration script
- Update all path references in scripts and tests
- Update documentation (README.md, CLAUDE.md)

New project structure:
  project/
  ├── .ralph/           # Ralph configuration
  │   ├── PROMPT.md
  │   ├── @fix_plan.md
  │   ├── @AGENT.md
  │   ├── specs/
  │   ├── logs/
  │   └── docs/generated/
  └── src/              # Source code (unchanged)

Migration: Run `ralph-migrate` in existing projects to upgrade.

All 310 tests pass (100% pass rate).

* chore: add .claude/settings.local.json to .gitignore

* fix: address code review feedback for .ralph/ subfolder structure

Fixes multiple path-related issues identified in code review:

Test fixes:
- Fix create_sample_prompt to use $RALPH_DIR/PROMPT.md in test_session_continuity.bats
- Fix result_file path to use $RALPH_DIR/.json_parse_result in test_json_parsing.bats
- Fix @fix_plan.md and .response_analysis paths in test_cli_modern.bats
- Update templates directory missing test to account for global fallback

Template fix:
- Fix @fix_plan.md reference in templates/PROMPT.md to use .ralph/ prefix

Script fixes:
- Fix PROMPT_FILE comparison in ralph_loop.sh to use $RALPH_DIR/PROMPT.md
- Fix examples migration logic in migrate_to_ralph_folder.sh (remove premature mkdir)
- Move templates directory check AFTER cd in setup.sh (was checking wrong location)
- Add template directory validation with fallback to global templates

All 310 tests pass.

* Update migrate_to_ralph_folder.sh

Co-authored-by: macroscopeapp[bot] <170038800+macroscopeapp[bot]@users.noreply.github.com>

* fix: address code review feedback for .ralph/ subfolder structure

Code Review Fixes:
- Fix test_json_parsing.bats: all result_file and session file paths now use $RALPH_DIR prefix
- Fix ralph_loop.sh help text: paths now show .ralph/.ralph_session, .ralph/.call_count, etc.
- Fix migrate_to_ralph_folder.sh:
  - Proper error handling for date command (separate local declaration)
  - Use cp -a source/. dest/ pattern to preserve dotfiles and attributes
  - Remove 2>/dev/null suppression to surface copy errors
- Update create_files.sh to use .ralph/ structure for embedded scripts
- Update .gitignore with all .ralph/ state file paths
- Add old structure detection in ralph_loop.sh with helpful migration message

Version Update:
- Bump to v0.10.0 (breaking change: structural reorganization)
- Update README.md and CLAUDE.md with new version and release notes
- Add ralph-migrate documentation to Key Commands section

All 310 tests pass.

---------

Co-authored-by: macroscopeapp[bot] <170038800+macroscopeapp[bot]@users.noreply.github.com>
2026-01-20 23:22:30 -07:00

610 lines
18 KiB
Bash

#!/usr/bin/env bats
# Edge case tests for Ralph loop execution
# Tests boundary conditions, error scenarios, and unusual inputs
load '../helpers/test_helper'
load '../helpers/mocks'
load '../helpers/fixtures'
setup() {
# Create temporary test directory
TEST_DIR="$(mktemp -d)"
cd "$TEST_DIR"
# Initialize git repo
git init > /dev/null 2>&1
git config user.email "test@example.com"
git config user.name "Test User"
# Set up environment with .ralph/ subfolder structure
export RALPH_DIR=".ralph"
# Create necessary files
create_sample_prd_md
create_sample_fix_plan
# Set up environment
export PROMPT_FILE="$RALPH_DIR/PROMPT.md"
export LOG_DIR="$RALPH_DIR/logs"
export EXIT_SIGNALS_FILE="$RALPH_DIR/.exit_signals"
mkdir -p "$RALPH_DIR" "$LOG_DIR"
echo '{"test_only_loops": [], "done_signals": [], "completion_indicators": []}' > "$EXIT_SIGNALS_FILE"
# Source library components
source "${BATS_TEST_DIRNAME}/../../lib/response_analyzer.sh"
source "${BATS_TEST_DIRNAME}/../../lib/circuit_breaker.sh"
}
teardown() {
if [[ -n "$TEST_DIR" ]] && [[ -d "$TEST_DIR" ]]; then
cd /
rm -rf "$TEST_DIR"
fi
}
# Edge Case 1: Empty output file
@test "analyze_response handles empty output file" {
local output_file="$LOG_DIR/empty_output.log"
touch "$output_file"
analyze_response "$output_file" 1
# Should not crash, should create analysis file
assert_file_exists "$RALPH_DIR/.response_analysis"
local exit_signal=$(jq -r '.analysis.exit_signal' "$RALPH_DIR/.response_analysis")
# Empty output shouldn't trigger exit
assert_equal "$exit_signal" "false"
}
# Edge Case 2: Very large output file
@test "analyze_response handles large output file" {
local output_file="$LOG_DIR/large_output.log"
# Create large output (100KB)
for i in {1..1000}; do
echo "This is line $i with some implementation work and progress..." >> "$output_file"
done
analyze_response "$output_file" 1
# Should handle without error
assert_file_exists "$RALPH_DIR/.response_analysis"
local output_length=$(jq -r '.analysis.output_length' "$RALPH_DIR/.response_analysis")
[[ "$output_length" -gt 50000 ]]
}
# Edge Case 3: Malformed RALPH_STATUS block
@test "analyze_response handles malformed status block" {
local output_file="$LOG_DIR/malformed.log"
cat > "$output_file" << 'EOF'
---RALPH_STATUS---
STATUS COMPLETE
MISSING_COLONS
EXIT_SIGNAL true
---END_RALPH_STATUS---
EOF
analyze_response "$output_file" 1
# Should not crash, may not detect structured output
assert_file_exists "$RALPH_DIR/.response_analysis"
}
# Edge Case 4: Missing exit signals file
@test "update_exit_signals creates file if missing" {
local output_file="$LOG_DIR/test.log"
rm -f "$EXIT_SIGNALS_FILE"
cat > "$output_file" << 'EOF'
Project is complete.
EOF
analyze_response "$output_file" 1
update_exit_signals
# Should create the file
assert_file_exists "$EXIT_SIGNALS_FILE"
# Should be valid JSON
jq '.' "$EXIT_SIGNALS_FILE" > /dev/null
}
# Edge Case 5: Circuit breaker with negative file count
@test "record_loop_result handles invalid file count gracefully" {
init_circuit_breaker
# Try with negative number (should treat as 0)
record_loop_result 1 -1 "false" 1000 || true
# Should not crash
local state=$(jq -r '.state' "$RALPH_DIR/.circuit_breaker_state")
# Should still be valid state
[[ "$state" == "CLOSED" || "$state" == "HALF_OPEN" ]]
}
# Edge Case 6: Very high loop number
@test "circuit breaker handles high loop numbers" {
init_circuit_breaker
# Simulate loop 9999
record_loop_result 9999 5 "false" 1000
local current_loop=$(jq -r '.current_loop' "$RALPH_DIR/.circuit_breaker_state")
assert_equal "$current_loop" "9999"
}
# Edge Case 7: Unicode in output
@test "analyze_response handles unicode characters" {
local output_file="$LOG_DIR/unicode.log"
cat > "$output_file" << 'EOF'
Implementation complete! ✅
Features: 🚀 Authentication, 🔒 Security, 📊 Analytics
Status: Done ✨
EOF
analyze_response "$output_file" 1
assert_file_exists "$RALPH_DIR/.response_analysis"
# Should detect "Done" as completion keyword
local has_completion=$(jq -r '.analysis.has_completion_signal' "$RALPH_DIR/.response_analysis")
assert_equal "$has_completion" "true"
}
# Edge Case 8: Multiple RALPH_STATUS blocks (malformed)
@test "analyze_response handles multiple status blocks" {
local output_file="$LOG_DIR/multiple_blocks.log"
cat > "$output_file" << 'EOF'
First attempt:
---RALPH_STATUS---
STATUS: IN_PROGRESS
EXIT_SIGNAL: false
---END_RALPH_STATUS---
Second attempt:
---RALPH_STATUS---
STATUS: COMPLETE
EXIT_SIGNAL: true
---END_RALPH_STATUS---
EOF
analyze_response "$output_file" 1
# Should detect structured output (picks first or last block)
local exit_signal=$(jq -r '.analysis.exit_signal' "$RALPH_DIR/.response_analysis")
# Should detect completion somehow
[[ "$exit_signal" == "true" || "$exit_signal" == "false" ]]
}
# Edge Case 9: Circuit breaker with corrupted state file
@test "circuit breaker handles corrupted state file" {
init_circuit_breaker
# Corrupt the state file
echo "invalid json{" > "$RALPH_DIR/.circuit_breaker_state"
# Should recover gracefully
init_circuit_breaker
# Should have valid state now
local state=$(jq -r '.state' "$RALPH_DIR/.circuit_breaker_state")
assert_equal "$state" "CLOSED"
}
# Edge Case 10: Response analysis with binary content
@test "analyze_response handles binary-like content" {
local output_file="$LOG_DIR/binary.log"
# Create file with some control characters
printf "Output with\x00null bytes\x01and\x02control chars\n" > "$output_file"
echo "But also normal text: implementation complete" >> "$output_file"
# Should not crash
analyze_response "$output_file" 1 || true
# File should exist even if analysis struggled
[[ -f "$RALPH_DIR/.response_analysis" ]]
}
# Edge Case 11: Simultaneous test-only and completion signals
@test "conflicting signals handled appropriately" {
local output_file="$LOG_DIR/conflicting.log"
cat > "$output_file" << 'EOF'
Running tests...
npm test
All tests passed.
Project is complete and ready for review.
EOF
analyze_response "$output_file" 1
local is_test_only=$(jq -r '.analysis.is_test_only' "$RALPH_DIR/.response_analysis")
local has_completion=$(jq -r '.analysis.has_completion_signal' "$RALPH_DIR/.response_analysis")
# Both can be true - completion signal should take precedence
assert_equal "$has_completion" "true"
}
# Edge Case 12: Circuit breaker rapid state changes
@test "circuit breaker handles rapid state transitions" {
init_circuit_breaker
# No progress
record_loop_result 1 0 "false" 1000 || true
record_loop_result 2 0 "false" 1000 || true
# Sudden progress
record_loop_result 3 5 "false" 2000
# Should recover to CLOSED
local state=$(jq -r '.state' "$RALPH_DIR/.circuit_breaker_state")
assert_equal "$state" "CLOSED"
}
# Edge Case 13: Output length exactly at decline threshold
@test "output length boundary condition" {
local output_file="$LOG_DIR/first.log"
# First output: 1000 chars
printf "%1000s" " " > "$output_file"
echo "content" >> "$output_file"
analyze_response "$output_file" 1
# Second output: exactly 50% (500 chars)
cat > "$output_file" << 'EOF'
Done.
EOF
printf "%495s" " " >> "$output_file"
analyze_response "$output_file" 2
# Should be at boundary
assert_file_exists "$RALPH_DIR/.response_analysis"
}
# Edge Case 14: Missing git repository
@test "analyze_response handles missing git repo" {
# Remove git repo
rm -rf .git
local output_file="$LOG_DIR/test.log"
echo "Implementation work" > "$output_file"
# Should not crash when git commands fail
analyze_response "$output_file" 1
assert_file_exists "$RALPH_DIR/.response_analysis"
# files_modified should be 0 (can't detect without git)
local files_modified=$(jq -r '.analysis.files_modified' "$RALPH_DIR/.response_analysis")
assert_equal "$files_modified" "0"
}
# Edge Case 15: Exit signals array overflow (>100 entries)
@test "exit_signals maintains rolling window limit" {
local output_file="$LOG_DIR/test.log"
# Create 10 test-only loops
for i in {1..10}; do
cat > "$output_file" << 'EOF'
Running tests...
npm test
EOF
analyze_response "$output_file" $i
update_exit_signals
done
# Should only keep last 5
local count=$(jq '.test_only_loops | length' "$EXIT_SIGNALS_FILE")
assert_equal "$count" "5"
# Should be loops 6-10
local first_loop=$(jq '.test_only_loops[0]' "$EXIT_SIGNALS_FILE")
assert_equal "$first_loop" "6"
}
# Edge Case 16: Circuit breaker with same timestamp
@test "circuit breaker handles rapid loops (same second)" {
init_circuit_breaker
# Execute 3 loops in rapid succession (likely same second)
record_loop_result 1 1 "false" 1000
record_loop_result 2 1 "false" 1000
record_loop_result 3 1 "false" 1000
# Should track all 3 correctly
local current_loop=$(jq -r '.current_loop' "$RALPH_DIR/.circuit_breaker_state")
assert_equal "$current_loop" "3"
}
# Edge Case 17: Confidence score overflow
@test "confidence score handles multiple bonuses correctly" {
local output_file="$LOG_DIR/high_confidence.log"
cat > "$output_file" << 'EOF'
Project is complete and finished.
All tasks are done.
Nothing to do.
---RALPH_STATUS---
STATUS: COMPLETE
EXIT_SIGNAL: true
---END_RALPH_STATUS---
EOF
# Create file changes
echo "test" > new_file.txt
git add new_file.txt
analyze_response "$output_file" 1
# Confidence should be very high (100 + bonuses)
local confidence=$(jq -r '.analysis.confidence_score' "$RALPH_DIR/.response_analysis")
[[ "$confidence" -ge 100 ]]
}
# Edge Case 18: Circuit breaker history file corruption
@test "circuit breaker recreates corrupted history" {
init_circuit_breaker
# Corrupt history
echo "not valid json" > "$RALPH_DIR/.circuit_breaker_history"
# Should handle gracefully on next transition
record_loop_result 1 0 "false" 1000 || true
record_loop_result 2 0 "false" 1000 || true
# Depending on implementation, may recreate or skip history logging
# Just verify no crash
[[ -f "$RALPH_DIR/.circuit_breaker_state" ]]
}
# Edge Case 19: Status block with extra fields
@test "analyze_response ignores unknown status fields" {
local output_file="$LOG_DIR/extra_fields.log"
cat > "$output_file" << 'EOF'
---RALPH_STATUS---
STATUS: COMPLETE
EXIT_SIGNAL: true
CUSTOM_FIELD: some_value
UNKNOWN_DATA: 12345
---END_RALPH_STATUS---
EOF
analyze_response "$output_file" 1
# Should successfully parse known fields
local exit_signal=$(jq -r '.analysis.exit_signal' "$RALPH_DIR/.response_analysis")
assert_equal "$exit_signal" "true"
}
# Edge Case 20: Detect stuck loop with varying error messages
@test "detect_stuck_loop with similar but not identical errors" {
mkdir -p logs
# Create outputs with similar errors
cat > "logs/claude_output_1.log" << 'EOF'
Error: Cannot find module 'express' at line 42
EOF
cat > "logs/claude_output_2.log" << 'EOF'
Error: Cannot find module 'express' at line 43
EOF
cat > "logs/claude_output_3.log" << 'EOF'
Error: Cannot find module 'express' at line 42
EOF
# May or may not detect as "stuck" depending on exact match requirements
# Just verify function runs without crashing
if detect_stuck_loop "logs/claude_output_3.log" "logs"; then
result=0
else
result=1
fi
[[ "$result" -eq 0 || "$result" -eq 1 ]]
}
# =============================================================================
# EXIT_SIGNAL INTEGRATION TESTS
# Tests for the fix that ensures completion indicators only trigger exit
# when Claude's explicit EXIT_SIGNAL is true
# =============================================================================
# Edge Case 21: Multiple loops with EXIT_SIGNAL=false should continue
@test "multiple loops continue when confidence high but EXIT_SIGNAL=false" {
local output_file="$LOG_DIR/loop.log"
# Simulate 3 loops with explicit EXIT_SIGNAL: false
for i in {1..3}; do
cat > "$output_file" << 'EOF'
---RALPH_STATUS---
STATUS: IN_PROGRESS
EXIT_SIGNAL: false
WORK_TYPE: IMPLEMENTATION
---END_RALPH_STATUS---
Work complete for this iteration.
Project progressing well, all tasks for this phase done.
Ready for next steps.
EOF
analyze_response "$output_file" $i
update_exit_signals
# After each loop, check that exit_signal is correctly captured as false
local exit_signal=$(jq -r '.analysis.exit_signal' "$RALPH_DIR/.response_analysis")
assert_equal "$exit_signal" "false"
done
# Verify that analyze_response correctly captures EXIT_SIGNAL=false
local final_exit_signal=$(jq -r '.analysis.exit_signal' "$RALPH_DIR/.response_analysis")
assert_equal "$final_exit_signal" "false"
# Key test: Even with high completion indicators set externally,
# the exit_signal should still be false (respecting Claude's explicit intent)
echo '{"test_only_loops": [], "done_signals": [], "completion_indicators": [1,2,3]}' > "$EXIT_SIGNALS_FILE"
local last_exit_signal=$(jq -r '.analysis.exit_signal' "$RALPH_DIR/.response_analysis")
assert_equal "$last_exit_signal" "false"
}
# Edge Case 22: Transition from IN_PROGRESS to COMPLETE
@test "loop exits when transitioning from EXIT_SIGNAL=false to EXIT_SIGNAL=true" {
local output_file="$LOG_DIR/loop.log"
# Loop 1-2: IN_PROGRESS with EXIT_SIGNAL=false
for i in 1 2; do
cat > "$output_file" << 'EOF'
---RALPH_STATUS---
STATUS: IN_PROGRESS
EXIT_SIGNAL: false
---END_RALPH_STATUS---
Feature implementation in progress.
EOF
analyze_response "$output_file" $i
update_exit_signals
local exit_signal=$(jq -r '.analysis.exit_signal' "$RALPH_DIR/.response_analysis")
assert_equal "$exit_signal" "false"
done
# Loop 3: COMPLETE with EXIT_SIGNAL=true
cat > "$output_file" << 'EOF'
---RALPH_STATUS---
STATUS: COMPLETE
EXIT_SIGNAL: true
---END_RALPH_STATUS---
All tasks complete. Project ready for review.
EOF
analyze_response "$output_file" 3
update_exit_signals
# Exit signal should now be true
local exit_signal=$(jq -r '.analysis.exit_signal' "$RALPH_DIR/.response_analysis")
assert_equal "$exit_signal" "true"
# Confidence should be >= 100 (100 from EXIT_SIGNAL: true, plus any natural language bonuses)
local confidence=$(jq -r '.analysis.confidence_score' "$RALPH_DIR/.response_analysis")
[[ "$confidence" -ge 100 ]]
}
# Edge Case 23: Missing .response_analysis mid-loop
@test "graceful handling when .response_analysis deleted mid-loop" {
local output_file="$LOG_DIR/loop.log"
# Create initial analysis
cat > "$output_file" << 'EOF'
---RALPH_STATUS---
STATUS: IN_PROGRESS
EXIT_SIGNAL: false
---END_RALPH_STATUS---
Working on implementation.
EOF
analyze_response "$output_file" 1
update_exit_signals
# Verify file exists
assert_file_exists "$RALPH_DIR/.response_analysis"
# Simulate file deletion (e.g., cleanup script ran)
rm -f "$RALPH_DIR/.response_analysis"
# Add more completion indicators
cat > "$output_file" << 'EOF'
Project complete.
EOF
analyze_response "$output_file" 2
update_exit_signals
# File should be recreated
assert_file_exists "$RALPH_DIR/.response_analysis"
}
# Edge Case 24: STATUS=COMPLETE but EXIT_SIGNAL=false conflict in RALPH_STATUS
@test "analyze_response respects EXIT_SIGNAL=false even when STATUS=COMPLETE" {
local output_file="$LOG_DIR/conflict.log"
# Create output with conflicting signals
# This can happen when Claude completes a phase but has more phases to do
cat > "$output_file" << 'EOF'
---RALPH_STATUS---
STATUS: COMPLETE
EXIT_SIGNAL: false
WORK_TYPE: IMPLEMENTATION
---END_RALPH_STATUS---
Phase 1 implementation complete.
Moving on to Phase 2 next.
EOF
analyze_response "$output_file" 1
# EXIT_SIGNAL: false should take precedence over STATUS: COMPLETE
local exit_signal=$(jq -r '.analysis.exit_signal' "$RALPH_DIR/.response_analysis")
assert_equal "$exit_signal" "false"
# has_completion_signal can still be true (STATUS was COMPLETE)
# but exit_signal must be false per Claude's explicit intent
}
# Edge Case 25: JSON format response with EXIT_SIGNAL handling
@test "JSON format response correctly handles EXIT_SIGNAL" {
local output_file="$LOG_DIR/json_response.log"
# Create JSON format response (Claude CLI format)
cat > "$output_file" << 'EOF'
{
"result": "Implementation in progress, more work needed",
"sessionId": "test-session-123",
"metadata": {
"files_changed": 5,
"has_errors": false,
"completion_status": "in_progress"
}
}
EOF
analyze_response "$output_file" 1
update_exit_signals
# Exit signal should be false (completion_status is in_progress)
local exit_signal=$(jq -r '.analysis.exit_signal' "$RALPH_DIR/.response_analysis")
assert_equal "$exit_signal" "false"
# Now test with complete status
cat > "$output_file" << 'EOF'
{
"result": "All tasks completed successfully",
"sessionId": "test-session-124",
"metadata": {
"files_changed": 0,
"has_errors": false,
"completion_status": "complete"
}
}
EOF
analyze_response "$output_file" 2
update_exit_signals
# Exit signal should be true (completion_status is complete)
local exit_signal=$(jq -r '.analysis.exit_signal' "$RALPH_DIR/.response_analysis")
assert_equal "$exit_signal" "true"
}