diff --git a/CLAUDE.md b/CLAUDE.md index 8efdc92..0c546c6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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.9.2 | **Tests**: 151 passing (100% pass rate) | **CI/CD**: GitHub Actions +**Version**: v0.9.3 | **Tests**: 165 passing (100% pass rate) | **CI/CD**: GitHub Actions ## Core Architecture @@ -95,7 +95,7 @@ tmux attach -t ### Running Tests ```bash -# Run all tests (151 tests) +# Run all tests (165 tests) npm test # Run specific test suites @@ -272,7 +272,7 @@ Ralph uses advanced error detection with two-stage filtering to eliminate false ## Test Suite -### Test Files (151 tests total) +### Test Files (165 tests total) | File | Tests | Description | |------|-------|-------------| @@ -283,6 +283,7 @@ Ralph uses advanced error detection with two-stage filtering to eliminate false | `test_rate_limiting.bats` | 15 | Rate limiting behavior | | `test_loop_execution.bats` | 20 | Integration tests | | `test_edge_cases.bats` | 20 | Edge case handling | +| `test_installation.bats` | 14 | Global installation/uninstall workflows | ### Running Tests ```bash @@ -298,6 +299,17 @@ bats tests/unit/test_cli_parsing.bats ## Recent Improvements +### Installation Tests (v0.9.3) +- Added 14 comprehensive tests for `install.sh` global installation script +- Tests cover: directory creation, command installation, template copying, lib copying +- Dependency detection tests (jq, git, node) with mocked failures +- PATH detection and warning system tests +- Uninstallation cleanup verification +- Idempotency testing (run twice without errors) +- End-to-end installation workflow validation +- All tests use isolated temp directories (no system modifications) +- Test count: 165 (up from 151) + ### Prompt File Fix (v0.9.2) - Fixed critical bug: replaced non-existent `--prompt-file` CLI flag with `-p` flag - Modern CLI mode now correctly passes prompt content via `CLAUDE_CMD_ARGS+=("-p" "$prompt_content")` diff --git a/package.json b/package.json index 9c8281b..1cda569 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "test": "tests" }, "scripts": { - "test": "bats tests/", + "test": "bats tests/unit/ tests/integration/", "test:unit": "bats tests/unit/", "test:integration": "bats tests/integration/", "test:e2e": "bats tests/e2e/" diff --git a/tests/integration/test_installation.bats b/tests/integration/test_installation.bats new file mode 100644 index 0000000..0ce17a5 --- /dev/null +++ b/tests/integration/test_installation.bats @@ -0,0 +1,542 @@ +#!/usr/bin/env bats +# Integration tests for Ralph install.sh - Global Installation Script + +load '../helpers/test_helper' +load '../helpers/mocks' +load '../helpers/fixtures' + +# Store original values +ORIGINAL_HOME="$HOME" +ORIGINAL_PATH="$PATH" +PROJECT_ROOT="" + +setup() { + # Save project root for sourcing install.sh + PROJECT_ROOT="${BATS_TEST_DIRNAME}/../.." + + # Create unique temp directories for isolated testing + export TEST_HOME="$(mktemp -d)" + export TEST_INSTALL_DIR="$TEST_HOME/.local/bin" + export TEST_RALPH_HOME="$TEST_HOME/.ralph" + + # Override HOME to isolate tests + export HOME="$TEST_HOME" + + # Create mock source directories with required files + export MOCK_SOURCE_DIR="$(mktemp -d)" + mkdir -p "$MOCK_SOURCE_DIR/templates/specs" + mkdir -p "$MOCK_SOURCE_DIR/lib" + + # Create mock template files + echo "# Mock PROMPT.md" > "$MOCK_SOURCE_DIR/templates/PROMPT.md" + echo "# Mock fix_plan.md" > "$MOCK_SOURCE_DIR/templates/fix_plan.md" + echo "# Mock AGENT.md" > "$MOCK_SOURCE_DIR/templates/AGENT.md" + + # Create mock lib files + cat > "$MOCK_SOURCE_DIR/lib/circuit_breaker.sh" << 'EOF' +#!/bin/bash +# Mock circuit_breaker.sh +init_circuit_breaker() { :; } +EOF + + cat > "$MOCK_SOURCE_DIR/lib/response_analyzer.sh" << 'EOF' +#!/bin/bash +# Mock response_analyzer.sh +analyze_response() { :; } +EOF + + cat > "$MOCK_SOURCE_DIR/lib/date_utils.sh" << 'EOF' +#!/bin/bash +# Mock date_utils.sh +get_iso_timestamp() { date -Iseconds; } +EOF + + # Create mock main scripts + cat > "$MOCK_SOURCE_DIR/ralph_loop.sh" << 'EOF' +#!/bin/bash +# Mock ralph_loop.sh +echo "Ralph loop running" +EOF + + cat > "$MOCK_SOURCE_DIR/ralph_monitor.sh" << 'EOF' +#!/bin/bash +# Mock ralph_monitor.sh +echo "Ralph monitor running" +EOF + + cat > "$MOCK_SOURCE_DIR/ralph_import.sh" << 'EOF' +#!/bin/bash +# Mock ralph_import.sh +echo "Ralph import running" +EOF + + chmod +x "$MOCK_SOURCE_DIR"/*.sh + chmod +x "$MOCK_SOURCE_DIR/lib"/*.sh +} + +teardown() { + # Restore original environment + export HOME="$ORIGINAL_HOME" + export PATH="$ORIGINAL_PATH" + + # Clean up test directories + if [[ -n "$TEST_HOME" && -d "$TEST_HOME" ]]; then + rm -rf "$TEST_HOME" + fi + + if [[ -n "$MOCK_SOURCE_DIR" && -d "$MOCK_SOURCE_DIR" ]]; then + rm -rf "$MOCK_SOURCE_DIR" + fi +} + +# Helper: Source install.sh functions for testing +# This overrides SCRIPT_DIR to use our mock source +source_install_functions() { + # Override SCRIPT_DIR before sourcing + export SCRIPT_DIR="$MOCK_SOURCE_DIR" + export INSTALL_DIR="$TEST_INSTALL_DIR" + export RALPH_HOME="$TEST_RALPH_HOME" + + # Source only functions, not main execution + source <(grep -E '^(log|check_dependencies|create_install_dirs|install_scripts|install_ralph_loop|install_setup|check_path)\s*\(\)|^(log|check_dependencies|create_install_dirs|install_scripts|install_ralph_loop|install_setup|check_path)\(\)|^[A-Z_]+=|^function ' "$PROJECT_ROOT/install.sh" | head -100) + + # Re-source with sed to extract functions + eval "$(sed -n '/^log()/,/^}/p' "$PROJECT_ROOT/install.sh")" + eval "$(sed -n '/^create_install_dirs()/,/^}/p' "$PROJECT_ROOT/install.sh")" + eval "$(sed -n '/^check_path()/,/^}/p' "$PROJECT_ROOT/install.sh")" +} + +# Helper: Run install.sh in isolated environment +run_install() { + local action="${1:-install}" + + # Set up environment for isolated install + export SCRIPT_DIR="$MOCK_SOURCE_DIR" + + # Create a modified install.sh that uses our mock paths + local temp_install="$(mktemp)" + sed -e "s|INSTALL_DIR=\"\$HOME/.local/bin\"|INSTALL_DIR=\"$TEST_INSTALL_DIR\"|g" \ + -e "s|RALPH_HOME=\"\$HOME/.ralph\"|RALPH_HOME=\"$TEST_RALPH_HOME\"|g" \ + -e "s|SCRIPT_DIR=\"\$(cd \"\$(dirname \"\${BASH_SOURCE\[0\]}\")\" && pwd)\"|SCRIPT_DIR=\"$MOCK_SOURCE_DIR\"|g" \ + "$PROJECT_ROOT/install.sh" > "$temp_install" + + chmod +x "$temp_install" + + # Run with specified action + if [[ "$action" == "install" ]]; then + bash "$temp_install" install 2>&1 + else + bash "$temp_install" "$action" 2>&1 + fi + local exit_code=$? + + rm -f "$temp_install" + return $exit_code +} + +# ============================================================================= +# Test 1-2: Directory Creation Tests +# ============================================================================= + +@test "install.sh creates ~/.ralph directory" { + run run_install + + # Check main ralph directory was created + assert_dir_exists "$TEST_RALPH_HOME" + + # Check subdirectories + assert_dir_exists "$TEST_RALPH_HOME/templates" + assert_dir_exists "$TEST_RALPH_HOME/lib" +} + +@test "install.sh creates ~/.local/bin directory" { + run run_install + + # Check bin directory was created + assert_dir_exists "$TEST_INSTALL_DIR" + + # Verify directory has correct permissions (should be accessible) + [[ -x "$TEST_INSTALL_DIR" ]] +} + +# ============================================================================= +# Test 3-4: Command Installation Tests +# ============================================================================= + +@test "install.sh creates ~/.local/bin commands" { + run run_install + + # Check all four wrapper commands exist + assert_file_exists "$TEST_INSTALL_DIR/ralph" + assert_file_exists "$TEST_INSTALL_DIR/ralph-monitor" + assert_file_exists "$TEST_INSTALL_DIR/ralph-setup" + assert_file_exists "$TEST_INSTALL_DIR/ralph-import" + + # Verify each command contains proper shebang + grep -q "#!/bin/bash" "$TEST_INSTALL_DIR/ralph" + grep -q "#!/bin/bash" "$TEST_INSTALL_DIR/ralph-monitor" + grep -q "#!/bin/bash" "$TEST_INSTALL_DIR/ralph-setup" + grep -q "#!/bin/bash" "$TEST_INSTALL_DIR/ralph-import" +} + +@test "install.sh sets executable permissions" { + run run_install + + # Verify executable bit on all commands + [[ -x "$TEST_INSTALL_DIR/ralph" ]] + [[ -x "$TEST_INSTALL_DIR/ralph-monitor" ]] + [[ -x "$TEST_INSTALL_DIR/ralph-setup" ]] + [[ -x "$TEST_INSTALL_DIR/ralph-import" ]] + + # Verify executable bit on main scripts + [[ -x "$TEST_RALPH_HOME/ralph_loop.sh" ]] + [[ -x "$TEST_RALPH_HOME/ralph_monitor.sh" ]] + [[ -x "$TEST_RALPH_HOME/setup.sh" ]] + [[ -x "$TEST_RALPH_HOME/ralph_import.sh" ]] + + # Verify lib scripts are executable + [[ -x "$TEST_RALPH_HOME/lib/circuit_breaker.sh" ]] + [[ -x "$TEST_RALPH_HOME/lib/response_analyzer.sh" ]] + [[ -x "$TEST_RALPH_HOME/lib/date_utils.sh" ]] +} + +# ============================================================================= +# Test 5-6: Template and Library Copying Tests +# ============================================================================= + +@test "install.sh copies templates correctly" { + run run_install + + # Check template files were copied + assert_file_exists "$TEST_RALPH_HOME/templates/PROMPT.md" + assert_file_exists "$TEST_RALPH_HOME/templates/fix_plan.md" + assert_file_exists "$TEST_RALPH_HOME/templates/AGENT.md" + + # Verify content matches source + diff -q "$MOCK_SOURCE_DIR/templates/PROMPT.md" "$TEST_RALPH_HOME/templates/PROMPT.md" + diff -q "$MOCK_SOURCE_DIR/templates/fix_plan.md" "$TEST_RALPH_HOME/templates/fix_plan.md" + diff -q "$MOCK_SOURCE_DIR/templates/AGENT.md" "$TEST_RALPH_HOME/templates/AGENT.md" +} + +@test "install.sh copies lib/ directory" { + run run_install + + # Check lib files were copied + assert_file_exists "$TEST_RALPH_HOME/lib/circuit_breaker.sh" + assert_file_exists "$TEST_RALPH_HOME/lib/response_analyzer.sh" + assert_file_exists "$TEST_RALPH_HOME/lib/date_utils.sh" + + # Verify files are executable + [[ -x "$TEST_RALPH_HOME/lib/circuit_breaker.sh" ]] + [[ -x "$TEST_RALPH_HOME/lib/response_analyzer.sh" ]] + [[ -x "$TEST_RALPH_HOME/lib/date_utils.sh" ]] +} + +# ============================================================================= +# Test 7-8: Dependency Detection Tests +# ============================================================================= + +@test "install.sh detects missing dependencies (jq, git, node)" { + # Create a modified install.sh with mocked command -v + local temp_script="$(mktemp)" + + cat > "$temp_script" << 'EOF' +#!/bin/bash +set -e + +# Override command to simulate missing jq +command() { + if [[ "$1" == "-v" && "$2" == "jq" ]]; then + return 1 + fi + builtin command "$@" +} + +# Mock check_dependencies from install.sh +check_dependencies() { + local missing_deps=() + + if ! command -v node &> /dev/null && ! command -v npx &> /dev/null; then + missing_deps+=("Node.js/npm") + fi + + if ! command -v jq &> /dev/null; then + missing_deps+=("jq") + fi + + if ! command -v git &> /dev/null; then + missing_deps+=("git") + fi + + if [ ${#missing_deps[@]} -ne 0 ]; then + echo "ERROR: Missing required dependencies: ${missing_deps[*]}" + exit 1 + fi + + echo "SUCCESS: Dependencies check completed" +} + +check_dependencies +EOF + chmod +x "$temp_script" + + # Run and expect failure + run bash "$temp_script" + + # Should fail + [[ "$status" -ne 0 ]] + + # Should mention missing dependencies + [[ "$output" =~ "Missing required dependencies" ]] || [[ "$output" =~ "jq" ]] + + rm -f "$temp_script" +} + +@test "install.sh detects all dependencies present" { + # Skip if actual dependencies are missing + if ! command -v node &> /dev/null && ! command -v npx &> /dev/null; then + skip "Node.js not available" + fi + if ! command -v jq &> /dev/null; then + skip "jq not available" + fi + if ! command -v git &> /dev/null; then + skip "git not available" + fi + + # Create a test script that checks dependencies + local temp_script="$(mktemp)" + + cat > "$temp_script" << 'EOF' +#!/bin/bash + +check_dependencies() { + local missing_deps=() + + if ! command -v node &> /dev/null && ! command -v npx &> /dev/null; then + missing_deps+=("Node.js/npm") + fi + + if ! command -v jq &> /dev/null; then + missing_deps+=("jq") + fi + + if ! command -v git &> /dev/null; then + missing_deps+=("git") + fi + + if [ ${#missing_deps[@]} -ne 0 ]; then + echo "ERROR: Missing required dependencies: ${missing_deps[*]}" + exit 1 + fi + + echo "Dependencies OK" + exit 0 +} + +check_dependencies +EOF + chmod +x "$temp_script" + + run bash "$temp_script" + + # Should succeed + assert_success + + rm -f "$temp_script" +} + +# ============================================================================= +# Test 9-10: PATH Detection Tests +# ============================================================================= + +@test "install.sh PATH detection warns when not in PATH" { + # Set PATH to exclude install directory + export PATH="/usr/bin:/bin" + + # Extract and test check_path function + local temp_script="$(mktemp)" + + cat > "$temp_script" << EOF +#!/bin/bash +INSTALL_DIR="$TEST_INSTALL_DIR" + +check_path() { + if [[ ":\$PATH:" != *":\$INSTALL_DIR:"* ]]; then + echo "WARN: \$INSTALL_DIR is not in your PATH" + echo "Add this to your ~/.bashrc:" + echo " export PATH=\"\\\$HOME/.local/bin:\\\$PATH\"" + return 0 + else + echo "SUCCESS: \$INSTALL_DIR is already in PATH" + return 0 + fi +} + +check_path +EOF + chmod +x "$temp_script" + + run bash "$temp_script" + + # Should warn about PATH + [[ "$output" =~ "not in your PATH" ]] || [[ "$output" =~ "WARN" ]] + + rm -f "$temp_script" +} + +@test "install.sh PATH detection succeeds when already in PATH" { + # Set PATH to include install directory + export PATH="$TEST_INSTALL_DIR:/usr/bin:/bin" + + # Extract and test check_path function + local temp_script="$(mktemp)" + + cat > "$temp_script" << EOF +#!/bin/bash +INSTALL_DIR="$TEST_INSTALL_DIR" + +check_path() { + if [[ ":\$PATH:" != *":\$INSTALL_DIR:"* ]]; then + echo "WARN: \$INSTALL_DIR is not in your PATH" + return 0 + else + echo "SUCCESS: \$INSTALL_DIR is already in PATH" + return 0 + fi +} + +check_path +EOF + chmod +x "$temp_script" + + run bash "$temp_script" + + # Should succeed + [[ "$output" =~ "SUCCESS" ]] || [[ "$output" =~ "already in PATH" ]] + + rm -f "$temp_script" +} + +# ============================================================================= +# Test 11-12: Uninstallation Tests +# ============================================================================= + +@test "install.sh uninstall removes all files" { + # First run installation + run run_install install + assert_success + + # Verify files exist + assert_file_exists "$TEST_INSTALL_DIR/ralph" + assert_file_exists "$TEST_INSTALL_DIR/ralph-monitor" + assert_file_exists "$TEST_INSTALL_DIR/ralph-setup" + assert_file_exists "$TEST_INSTALL_DIR/ralph-import" + + # Run uninstall + run run_install uninstall + assert_success + + # Verify command files are removed + assert_file_not_exists "$TEST_INSTALL_DIR/ralph" + assert_file_not_exists "$TEST_INSTALL_DIR/ralph-monitor" + assert_file_not_exists "$TEST_INSTALL_DIR/ralph-setup" + assert_file_not_exists "$TEST_INSTALL_DIR/ralph-import" +} + +@test "install.sh uninstall cleans up directories" { + # First run installation + run run_install install + assert_success + + # Verify ralph home exists + assert_dir_exists "$TEST_RALPH_HOME" + + # Run uninstall + run run_install uninstall + assert_success + + # Verify ralph home is removed + [[ ! -d "$TEST_RALPH_HOME" ]] +} + +# ============================================================================= +# Test 13-14: Idempotency and Integration Tests +# ============================================================================= + +@test "installation idempotency (run twice without errors)" { + # First installation + run run_install install + assert_success + + # Capture file counts after first install + local ralph_count_1=$(ls "$TEST_INSTALL_DIR" | wc -l) + local template_count_1=$(ls "$TEST_RALPH_HOME/templates" | wc -l) + + # Second installation (should overwrite cleanly) + run run_install install + assert_success + + # Capture file counts after second install + local ralph_count_2=$(ls "$TEST_INSTALL_DIR" | wc -l) + local template_count_2=$(ls "$TEST_RALPH_HOME/templates" | wc -l) + + # Counts should be the same (no duplicates or missing files) + assert_equal "$ralph_count_1" "$ralph_count_2" + assert_equal "$template_count_1" "$template_count_2" + + # All files should still exist and be valid + assert_file_exists "$TEST_INSTALL_DIR/ralph" + assert_file_exists "$TEST_RALPH_HOME/templates/PROMPT.md" + assert_file_exists "$TEST_RALPH_HOME/lib/circuit_breaker.sh" +} + +@test "complete installation workflow end-to-end" { + # Skip if dependencies missing + if ! command -v jq &> /dev/null; then + skip "jq not available" + fi + if ! command -v git &> /dev/null; then + skip "git not available" + fi + + # Run full installation + run run_install install + assert_success + + # Verify all directories created + assert_dir_exists "$TEST_INSTALL_DIR" + assert_dir_exists "$TEST_RALPH_HOME" + assert_dir_exists "$TEST_RALPH_HOME/templates" + assert_dir_exists "$TEST_RALPH_HOME/lib" + + # Verify all commands installed + assert_file_exists "$TEST_INSTALL_DIR/ralph" + assert_file_exists "$TEST_INSTALL_DIR/ralph-monitor" + assert_file_exists "$TEST_INSTALL_DIR/ralph-setup" + assert_file_exists "$TEST_INSTALL_DIR/ralph-import" + + # Verify all templates copied + assert_file_exists "$TEST_RALPH_HOME/templates/PROMPT.md" + assert_file_exists "$TEST_RALPH_HOME/templates/fix_plan.md" + assert_file_exists "$TEST_RALPH_HOME/templates/AGENT.md" + + # Verify all lib files copied + assert_file_exists "$TEST_RALPH_HOME/lib/circuit_breaker.sh" + assert_file_exists "$TEST_RALPH_HOME/lib/response_analyzer.sh" + assert_file_exists "$TEST_RALPH_HOME/lib/date_utils.sh" + + # Verify all scripts in ralph home + assert_file_exists "$TEST_RALPH_HOME/ralph_loop.sh" + assert_file_exists "$TEST_RALPH_HOME/ralph_monitor.sh" + assert_file_exists "$TEST_RALPH_HOME/setup.sh" + assert_file_exists "$TEST_RALPH_HOME/ralph_import.sh" + + # Verify all permissions correct + [[ -x "$TEST_INSTALL_DIR/ralph" ]] + [[ -x "$TEST_RALPH_HOME/ralph_loop.sh" ]] + [[ -x "$TEST_RALPH_HOME/lib/circuit_breaker.sh" ]] + + # Verify output contains success message + [[ "$output" =~ "installed" ]] || [[ "$output" =~ "SUCCESS" ]] || [[ "$output" =~ "success" ]] +}