feat(enable): add ralph-enable wizard for existing projects (v0.11.0) (#124)
* feat(enable): add ralph-enable wizard for existing projects (v0.11.0) Add interactive wizard and CI version for enabling Ralph in existing projects. New commands: - ralph-enable: Interactive 5-phase wizard for humans - ralph-enable-ci: Non-interactive version with JSON output for CI/automation New library components: - lib/enable_core.sh: Shared logic for idempotency, project detection, templates - lib/wizard_utils.sh: Interactive prompt utilities - lib/task_sources.sh: Task import from beads, GitHub Issues, PRD documents Features: - Auto-detects project type (TypeScript, Python, Rust, Go) - Auto-detects framework (Next.js, FastAPI, Django, Express) - Imports tasks from beads, GitHub Issues, or PRD documents - Generates .ralphrc project configuration file - Idempotent: safe to run multiple times, respects existing files - Exit codes: 0 (success), 1 (error), 2 (already enabled) Updated: - install.sh: Added new commands to global installation - ralph_loop.sh: Loads .ralphrc configuration at startup Tests: 75 new tests (30 enable_core + 23 task_sources + 22 integration) Total: 396 tests passing (100% pass rate) Closes #85, #121, #64, #87, #99 * fix(enable): address code review feedback Fixes from PR #124 review: 1. sed -i portability (ralph_enable.sh:456) - Use portable sed + mv pattern instead of GNU-only sed -i 2. sed regex portability (lib/task_sources.sh) - Replace \s with POSIX [[:space:]] character class - Add sed -E flag for extended regex 3. jq availability check (ralph_enable_ci.sh:177) - Add check for jq when --json flag is used 4. Unused filter parameter (lib/task_sources.sh:44) - Pass filter to bd list --filter command 5. Word-splitting in select_multiple (ralph_enable.sh:322) - Return comma-separated indices instead of space-separated text - Update caller to parse indices correctly 6. Missing || true for check_existing_ralph (ralph_enable.sh:185) - Prevent set -e from exiting on non-zero return 7. select_multiple stdout corruption (lib/wizard_utils.sh) - Redirect interactive output to stderr - Only final result goes to stdout 8. Color variables not exported (lib/wizard_utils.sh:12) - Export WIZARD_* color variables for subshells 9. select_option infinite loop (lib/wizard_utils.sh:179) - Add guard for empty options array * fix(tests): add missing mocks and exports for new enable feature - Add RESPONSE_ANALYSIS_FILE export to test_session_continuity.bats setup - Add mock ralph_enable.sh and ralph_enable_ci.sh to test_installation.bats - Add mock lib files: enable_core.sh, wizard_utils.sh, task_sources.sh, timeout_utils.sh All 396 tests now pass. * fix(config): fix critical issues from PR review 1. .ralphrc Configuration Loading Fix: - Captured env var state BEFORE setting defaults with _env_* variables - load_ralphrc now only restores values explicitly set by environment - .ralphrc settings are now properly applied (not overwritten by defaults) 2. sed Command Injection Fix: - Replaced sed with awk for .ralphrc updates in ralph_enable.sh - awk -v pattern safely handles user input without shell injection risk 3. Shell Injection Fix in safe_create_file(): - Replaced echo with printf '%s\n' for safer content handling - Prevents issues with backslashes, -n, and special characters 4. Specific Error Codes: - Added ENABLE_INVALID_ARGS=3 for argument errors - Added ENABLE_FILE_NOT_FOUND=4 for missing files - Added ENABLE_DEPENDENCY_MISSING=5 for missing deps (e.g., jq) - Added ENABLE_PERMISSION_DENIED=6 for permission errors - Updated ralph_enable.sh and ralph_enable_ci.sh to use specific codes 5. Added tests for .ralphrc loading pattern verification Test count: 398 (up from 396) * fix(enable): make --force flag actually overwrite existing files The --force flag was accepted but safe_create_file() always skipped existing files regardless of ENABLE_FORCE value. Changes: - safe_create_file() now checks ENABLE_FORCE environment variable - When ENABLE_FORCE="true", overwrites existing files instead of skipping - Added proper logging for overwrite operations Added tests: - Verify enable_ralph_in_directory actually changes file contents with --force - Test safe_create_file overwrites when ENABLE_FORCE is true - Test safe_create_file skips when ENABLE_FORCE is false Test count: 400 (up from 398) --------- Co-authored-by: Test User <test@example.com>
This commit is contained in:
parent
019b8c738a
commit
910f794fcc
15 changed files with 4169 additions and 18 deletions
|
|
@ -80,6 +80,43 @@ EOF
|
|||
#!/bin/bash
|
||||
# Mock migrate_to_ralph_folder.sh
|
||||
echo "Migration running"
|
||||
EOF
|
||||
|
||||
cat > "$MOCK_SOURCE_DIR/ralph_enable.sh" << 'EOF'
|
||||
#!/bin/bash
|
||||
# Mock ralph_enable.sh
|
||||
echo "Ralph enable running"
|
||||
EOF
|
||||
|
||||
cat > "$MOCK_SOURCE_DIR/ralph_enable_ci.sh" << 'EOF'
|
||||
#!/bin/bash
|
||||
# Mock ralph_enable_ci.sh
|
||||
echo "Ralph enable CI running"
|
||||
EOF
|
||||
|
||||
# Create mock lib files for new enable functionality
|
||||
cat > "$MOCK_SOURCE_DIR/lib/enable_core.sh" << 'EOF'
|
||||
#!/bin/bash
|
||||
# Mock enable_core.sh
|
||||
check_existing_ralph() { :; }
|
||||
EOF
|
||||
|
||||
cat > "$MOCK_SOURCE_DIR/lib/wizard_utils.sh" << 'EOF'
|
||||
#!/bin/bash
|
||||
# Mock wizard_utils.sh
|
||||
confirm() { :; }
|
||||
EOF
|
||||
|
||||
cat > "$MOCK_SOURCE_DIR/lib/task_sources.sh" << 'EOF'
|
||||
#!/bin/bash
|
||||
# Mock task_sources.sh
|
||||
fetch_beads_tasks() { :; }
|
||||
EOF
|
||||
|
||||
cat > "$MOCK_SOURCE_DIR/lib/timeout_utils.sh" << 'EOF'
|
||||
#!/bin/bash
|
||||
# Mock timeout_utils.sh
|
||||
portable_timeout() { timeout "$@"; }
|
||||
EOF
|
||||
|
||||
chmod +x "$MOCK_SOURCE_DIR"/*.sh
|
||||
|
|
|
|||
|
|
@ -157,8 +157,9 @@ teardown() {
|
|||
|
||||
@test "CLAUDE_OUTPUT_FORMAT defaults to json" {
|
||||
# Verify by checking the default in ralph_loop.sh via grep
|
||||
# The default is set via ${CLAUDE_OUTPUT_FORMAT:-json} pattern
|
||||
run grep 'CLAUDE_OUTPUT_FORMAT=' "${BATS_TEST_DIRNAME}/../../ralph_loop.sh"
|
||||
[[ "$output" == *'"json"'* ]]
|
||||
[[ "$output" == *"json"* ]]
|
||||
}
|
||||
|
||||
@test "CLAUDE_ALLOWED_TOOLS has sensible defaults" {
|
||||
|
|
@ -622,3 +623,29 @@ EOF
|
|||
|
||||
[[ "$found_prompt" == "true" ]]
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# .RALPHRC CONFIGURATION LOADING TESTS
|
||||
# Tests for the environment variable precedence fix
|
||||
# =============================================================================
|
||||
|
||||
@test "load_ralphrc uses env var capture pattern for precedence" {
|
||||
# Verify the implementation pattern: _env_* variables capture state before defaults
|
||||
# This test validates the pattern is correctly implemented in ralph_loop.sh
|
||||
|
||||
run grep '_env_MAX_CALLS_PER_HOUR=' "${BATS_TEST_DIRNAME}/../../ralph_loop.sh"
|
||||
|
||||
# Should capture env var state BEFORE setting defaults
|
||||
[[ "$output" == *'${MAX_CALLS_PER_HOUR:-}'* ]]
|
||||
}
|
||||
|
||||
@test "load_ralphrc restores only env var overrides, not defaults" {
|
||||
# Verify that load_ralphrc uses _env_* pattern for restoration
|
||||
# This ensures .ralphrc values are not overwritten by script defaults
|
||||
|
||||
run grep -A5 'Restore ONLY values' "${BATS_TEST_DIRNAME}/../../ralph_loop.sh"
|
||||
|
||||
# Should check _env_* variables (not saved_* which would always have values)
|
||||
[[ "$output" == *'_env_MAX_CALLS_PER_HOUR'* ]]
|
||||
[[ "$output" == *'_env_CLAUDE_TIMEOUT_MINUTES'* ]]
|
||||
}
|
||||
|
|
|
|||
388
tests/unit/test_enable_core.bats
Normal file
388
tests/unit/test_enable_core.bats
Normal file
|
|
@ -0,0 +1,388 @@
|
|||
#!/usr/bin/env bats
|
||||
# Unit tests for lib/enable_core.sh
|
||||
# Tests idempotency, safe file creation, project detection, and template generation
|
||||
|
||||
load '../helpers/test_helper'
|
||||
load '../helpers/fixtures'
|
||||
|
||||
# Path to enable_core.sh
|
||||
ENABLE_CORE="${BATS_TEST_DIRNAME}/../../lib/enable_core.sh"
|
||||
|
||||
setup() {
|
||||
# Create temporary test directory
|
||||
TEST_DIR="$(mktemp -d)"
|
||||
cd "$TEST_DIR"
|
||||
|
||||
# Source the library (disable set -e for testing)
|
||||
set +e
|
||||
source "$ENABLE_CORE"
|
||||
set -e
|
||||
}
|
||||
|
||||
teardown() {
|
||||
if [[ -n "$TEST_DIR" ]] && [[ -d "$TEST_DIR" ]]; then
|
||||
cd /
|
||||
rm -rf "$TEST_DIR"
|
||||
fi
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# IDEMPOTENCY CHECKS (5 tests)
|
||||
# =============================================================================
|
||||
|
||||
@test "check_existing_ralph returns 'none' when no .ralph directory exists" {
|
||||
check_existing_ralph || true
|
||||
|
||||
assert_equal "$RALPH_STATE" "none"
|
||||
}
|
||||
|
||||
@test "check_existing_ralph returns 'complete' when all required files exist" {
|
||||
mkdir -p .ralph
|
||||
echo "# PROMPT" > .ralph/PROMPT.md
|
||||
echo "# Fix Plan" > .ralph/@fix_plan.md
|
||||
echo "# Agent" > .ralph/@AGENT.md
|
||||
|
||||
check_existing_ralph || true
|
||||
|
||||
assert_equal "$RALPH_STATE" "complete"
|
||||
}
|
||||
|
||||
@test "check_existing_ralph returns 'partial' when some files are missing" {
|
||||
mkdir -p .ralph
|
||||
echo "# PROMPT" > .ralph/PROMPT.md
|
||||
# Missing @fix_plan.md and @AGENT.md
|
||||
|
||||
check_existing_ralph || true
|
||||
|
||||
assert_equal "$RALPH_STATE" "partial"
|
||||
[[ " ${RALPH_MISSING_FILES[*]} " =~ ".ralph/@fix_plan.md" ]]
|
||||
[[ " ${RALPH_MISSING_FILES[*]} " =~ ".ralph/@AGENT.md" ]]
|
||||
}
|
||||
|
||||
@test "is_ralph_enabled returns 0 when fully enabled" {
|
||||
mkdir -p .ralph
|
||||
echo "# PROMPT" > .ralph/PROMPT.md
|
||||
echo "# Fix Plan" > .ralph/@fix_plan.md
|
||||
echo "# Agent" > .ralph/@AGENT.md
|
||||
|
||||
run is_ralph_enabled
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "is_ralph_enabled returns 1 when not enabled" {
|
||||
run is_ralph_enabled
|
||||
assert_failure
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# SAFE FILE OPERATIONS (5 tests)
|
||||
# =============================================================================
|
||||
|
||||
@test "safe_create_file creates file that doesn't exist" {
|
||||
run safe_create_file "test.txt" "test content"
|
||||
|
||||
assert_success
|
||||
[[ -f "test.txt" ]]
|
||||
[[ "$(cat test.txt)" == "test content" ]]
|
||||
}
|
||||
|
||||
@test "safe_create_file skips existing file" {
|
||||
echo "original content" > existing.txt
|
||||
|
||||
run safe_create_file "existing.txt" "new content"
|
||||
|
||||
assert_failure # Returns 1 for skip
|
||||
assert_equal "$(cat existing.txt)" "original content"
|
||||
[[ "$output" =~ "SKIP" ]] || [[ "$output" =~ "already exists" ]]
|
||||
}
|
||||
|
||||
@test "safe_create_file creates parent directories" {
|
||||
run safe_create_file "nested/dir/file.txt" "nested content"
|
||||
|
||||
assert_success
|
||||
[[ -f "nested/dir/file.txt" ]]
|
||||
[[ "$(cat nested/dir/file.txt)" == "nested content" ]]
|
||||
}
|
||||
|
||||
@test "safe_create_dir creates directory that doesn't exist" {
|
||||
run safe_create_dir "new_dir"
|
||||
|
||||
assert_success
|
||||
[[ -d "new_dir" ]]
|
||||
}
|
||||
|
||||
@test "safe_create_dir succeeds when directory already exists" {
|
||||
mkdir existing_dir
|
||||
|
||||
run safe_create_dir "existing_dir"
|
||||
|
||||
assert_success
|
||||
[[ -d "existing_dir" ]]
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# DIRECTORY STRUCTURE (2 tests)
|
||||
# =============================================================================
|
||||
|
||||
@test "create_ralph_structure creates all required directories" {
|
||||
run create_ralph_structure
|
||||
|
||||
assert_success
|
||||
[[ -d ".ralph" ]]
|
||||
[[ -d ".ralph/specs" ]]
|
||||
[[ -d ".ralph/examples" ]]
|
||||
[[ -d ".ralph/logs" ]]
|
||||
[[ -d ".ralph/docs/generated" ]]
|
||||
}
|
||||
|
||||
@test "create_ralph_structure is idempotent" {
|
||||
create_ralph_structure
|
||||
echo "test" > .ralph/specs/test.txt
|
||||
|
||||
run create_ralph_structure
|
||||
|
||||
assert_success
|
||||
[[ -f ".ralph/specs/test.txt" ]]
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# PROJECT DETECTION (6 tests)
|
||||
# =============================================================================
|
||||
|
||||
@test "detect_project_context identifies TypeScript from package.json" {
|
||||
cat > package.json << 'EOF'
|
||||
{
|
||||
"name": "my-ts-project",
|
||||
"devDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
detect_project_context
|
||||
|
||||
assert_equal "$DETECTED_PROJECT_TYPE" "typescript"
|
||||
assert_equal "$DETECTED_PROJECT_NAME" "my-ts-project"
|
||||
}
|
||||
|
||||
@test "detect_project_context identifies JavaScript from package.json" {
|
||||
cat > package.json << 'EOF'
|
||||
{
|
||||
"name": "my-js-project"
|
||||
}
|
||||
EOF
|
||||
|
||||
detect_project_context
|
||||
|
||||
assert_equal "$DETECTED_PROJECT_TYPE" "javascript"
|
||||
}
|
||||
|
||||
@test "detect_project_context identifies Python from pyproject.toml" {
|
||||
cat > pyproject.toml << 'EOF'
|
||||
[project]
|
||||
name = "my-python-project"
|
||||
EOF
|
||||
|
||||
detect_project_context
|
||||
|
||||
assert_equal "$DETECTED_PROJECT_TYPE" "python"
|
||||
}
|
||||
|
||||
@test "detect_project_context identifies Next.js framework" {
|
||||
cat > package.json << 'EOF'
|
||||
{
|
||||
"name": "nextjs-app",
|
||||
"dependencies": {
|
||||
"next": "^14.0.0"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
detect_project_context
|
||||
|
||||
assert_equal "$DETECTED_FRAMEWORK" "nextjs"
|
||||
}
|
||||
|
||||
@test "detect_project_context identifies FastAPI framework" {
|
||||
cat > pyproject.toml << 'EOF'
|
||||
[project]
|
||||
name = "fastapi-app"
|
||||
dependencies = ["fastapi>=0.100.0"]
|
||||
EOF
|
||||
|
||||
detect_project_context
|
||||
|
||||
assert_equal "$DETECTED_FRAMEWORK" "fastapi"
|
||||
}
|
||||
|
||||
@test "detect_project_context falls back to folder name" {
|
||||
detect_project_context
|
||||
|
||||
# Should use the temp directory name
|
||||
[[ -n "$DETECTED_PROJECT_NAME" ]]
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# GIT DETECTION (3 tests)
|
||||
# =============================================================================
|
||||
|
||||
@test "detect_git_info detects git repository" {
|
||||
git init >/dev/null 2>&1
|
||||
|
||||
detect_git_info
|
||||
|
||||
assert_equal "$DETECTED_GIT_REPO" "true"
|
||||
}
|
||||
|
||||
@test "detect_git_info detects non-git directory" {
|
||||
detect_git_info
|
||||
|
||||
assert_equal "$DETECTED_GIT_REPO" "false"
|
||||
}
|
||||
|
||||
@test "detect_git_info detects GitHub remote" {
|
||||
git init >/dev/null 2>&1
|
||||
git remote add origin git@github.com:user/repo.git 2>/dev/null || true
|
||||
|
||||
detect_git_info
|
||||
|
||||
assert_equal "$DETECTED_GIT_GITHUB" "true"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# TASK SOURCE DETECTION (2 tests)
|
||||
# =============================================================================
|
||||
|
||||
@test "detect_task_sources detects .beads directory" {
|
||||
mkdir -p .beads
|
||||
|
||||
detect_task_sources
|
||||
|
||||
assert_equal "$DETECTED_BEADS_AVAILABLE" "true"
|
||||
}
|
||||
|
||||
@test "detect_task_sources finds PRD files" {
|
||||
mkdir -p docs
|
||||
echo "# Requirements" > docs/requirements.md
|
||||
|
||||
detect_task_sources
|
||||
|
||||
[[ ${#DETECTED_PRD_FILES[@]} -gt 0 ]]
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# TEMPLATE GENERATION (4 tests)
|
||||
# =============================================================================
|
||||
|
||||
@test "generate_prompt_md includes project name" {
|
||||
output=$(generate_prompt_md "my-project" "typescript")
|
||||
|
||||
[[ "$output" =~ "my-project" ]]
|
||||
}
|
||||
|
||||
@test "generate_prompt_md includes project type" {
|
||||
output=$(generate_prompt_md "my-project" "python")
|
||||
|
||||
[[ "$output" =~ "python" ]]
|
||||
}
|
||||
|
||||
@test "generate_agent_md includes build command" {
|
||||
output=$(generate_agent_md "npm run build" "npm test" "npm start")
|
||||
|
||||
[[ "$output" =~ "npm run build" ]]
|
||||
[[ "$output" =~ "npm test" ]]
|
||||
}
|
||||
|
||||
@test "generate_ralphrc includes project configuration" {
|
||||
output=$(generate_ralphrc "my-project" "typescript" "local,beads")
|
||||
|
||||
[[ "$output" =~ "PROJECT_NAME=\"my-project\"" ]]
|
||||
[[ "$output" =~ "PROJECT_TYPE=\"typescript\"" ]]
|
||||
[[ "$output" =~ "TASK_SOURCES=\"local,beads\"" ]]
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# FULL ENABLE FLOW (3 tests)
|
||||
# =============================================================================
|
||||
|
||||
@test "enable_ralph_in_directory creates all required files" {
|
||||
export ENABLE_FORCE="false"
|
||||
export ENABLE_SKIP_TASKS="true"
|
||||
export ENABLE_PROJECT_NAME="test-project"
|
||||
|
||||
run enable_ralph_in_directory
|
||||
|
||||
assert_success
|
||||
[[ -f ".ralph/PROMPT.md" ]]
|
||||
[[ -f ".ralph/@fix_plan.md" ]]
|
||||
[[ -f ".ralph/@AGENT.md" ]]
|
||||
[[ -f ".ralphrc" ]]
|
||||
}
|
||||
|
||||
@test "enable_ralph_in_directory returns ALREADY_ENABLED when complete and no force" {
|
||||
mkdir -p .ralph
|
||||
echo "# PROMPT" > .ralph/PROMPT.md
|
||||
echo "# Fix Plan" > .ralph/@fix_plan.md
|
||||
echo "# Agent" > .ralph/@AGENT.md
|
||||
|
||||
export ENABLE_FORCE="false"
|
||||
|
||||
run enable_ralph_in_directory
|
||||
|
||||
assert_equal "$status" "$ENABLE_ALREADY_ENABLED"
|
||||
}
|
||||
|
||||
@test "enable_ralph_in_directory overwrites with force flag" {
|
||||
mkdir -p .ralph
|
||||
echo "old content" > .ralph/PROMPT.md
|
||||
echo "old fix plan" > .ralph/@fix_plan.md
|
||||
echo "old agent" > .ralph/@AGENT.md
|
||||
|
||||
export ENABLE_FORCE="true"
|
||||
export ENABLE_PROJECT_NAME="new-project"
|
||||
|
||||
run enable_ralph_in_directory
|
||||
|
||||
assert_success
|
||||
|
||||
# Verify files were actually overwritten, not just skipped
|
||||
local prompt_content
|
||||
prompt_content=$(cat .ralph/PROMPT.md)
|
||||
|
||||
# Should contain new project name, not "old content"
|
||||
[[ "$prompt_content" != "old content" ]]
|
||||
[[ "$prompt_content" == *"new-project"* ]]
|
||||
}
|
||||
|
||||
@test "safe_create_file overwrites existing file when ENABLE_FORCE is true" {
|
||||
# Create existing file with old content
|
||||
echo "original content" > test_file.txt
|
||||
|
||||
export ENABLE_FORCE="true"
|
||||
|
||||
run safe_create_file "test_file.txt" "new content"
|
||||
|
||||
assert_success
|
||||
|
||||
# Verify file was overwritten
|
||||
local content
|
||||
content=$(cat test_file.txt)
|
||||
[[ "$content" == "new content" ]]
|
||||
}
|
||||
|
||||
@test "safe_create_file skips existing file when ENABLE_FORCE is false" {
|
||||
# Create existing file with old content
|
||||
echo "original content" > test_file.txt
|
||||
|
||||
export ENABLE_FORCE="false"
|
||||
|
||||
run safe_create_file "test_file.txt" "new content"
|
||||
|
||||
# Should return 1 (skipped)
|
||||
assert_failure
|
||||
|
||||
# Verify file was NOT overwritten
|
||||
local content
|
||||
content=$(cat test_file.txt)
|
||||
[[ "$content" == "original content" ]]
|
||||
}
|
||||
268
tests/unit/test_ralph_enable.bats
Normal file
268
tests/unit/test_ralph_enable.bats
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
#!/usr/bin/env bats
|
||||
# Integration tests for ralph_enable.sh and ralph_enable_ci.sh
|
||||
# Tests the full enable wizard flow and CI version
|
||||
|
||||
load '../helpers/test_helper'
|
||||
load '../helpers/fixtures'
|
||||
|
||||
# Paths to scripts
|
||||
RALPH_ENABLE="${BATS_TEST_DIRNAME}/../../ralph_enable.sh"
|
||||
RALPH_ENABLE_CI="${BATS_TEST_DIRNAME}/../../ralph_enable_ci.sh"
|
||||
|
||||
setup() {
|
||||
# Create temporary test directory
|
||||
TEST_DIR="$(mktemp -d)"
|
||||
cd "$TEST_DIR"
|
||||
|
||||
# Initialize git repo (required by some detection)
|
||||
git init > /dev/null 2>&1
|
||||
git config user.email "test@example.com"
|
||||
git config user.name "Test User"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
if [[ -n "$TEST_DIR" ]] && [[ -d "$TEST_DIR" ]]; then
|
||||
cd /
|
||||
rm -rf "$TEST_DIR"
|
||||
fi
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# HELP AND VERSION (4 tests)
|
||||
# =============================================================================
|
||||
|
||||
@test "ralph enable --help shows usage information" {
|
||||
run bash "$RALPH_ENABLE" --help
|
||||
|
||||
assert_success
|
||||
[[ "$output" =~ "Usage:" ]]
|
||||
[[ "$output" =~ "--from" ]]
|
||||
[[ "$output" =~ "--force" ]]
|
||||
}
|
||||
|
||||
@test "ralph enable --version shows version" {
|
||||
run bash "$RALPH_ENABLE" --version
|
||||
|
||||
assert_success
|
||||
[[ "$output" =~ "version" ]]
|
||||
}
|
||||
|
||||
@test "ralph enable-ci --help shows usage information" {
|
||||
run bash "$RALPH_ENABLE_CI" --help
|
||||
|
||||
assert_success
|
||||
[[ "$output" =~ "Usage:" ]]
|
||||
[[ "$output" =~ "Exit Codes:" ]]
|
||||
}
|
||||
|
||||
@test "ralph enable-ci --version shows version" {
|
||||
run bash "$RALPH_ENABLE_CI" --version
|
||||
|
||||
assert_success
|
||||
[[ "$output" =~ "version" ]]
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# CI VERSION TESTS (8 tests)
|
||||
# =============================================================================
|
||||
|
||||
@test "ralph enable-ci creates .ralph structure in empty directory" {
|
||||
run bash "$RALPH_ENABLE_CI" --from none
|
||||
|
||||
assert_success
|
||||
[[ -d ".ralph" ]]
|
||||
[[ -f ".ralph/PROMPT.md" ]]
|
||||
[[ -f ".ralph/@fix_plan.md" ]]
|
||||
[[ -f ".ralph/@AGENT.md" ]]
|
||||
}
|
||||
|
||||
@test "ralph enable-ci creates .ralphrc configuration" {
|
||||
run bash "$RALPH_ENABLE_CI" --from none
|
||||
|
||||
assert_success
|
||||
[[ -f ".ralphrc" ]]
|
||||
}
|
||||
|
||||
@test "ralph enable-ci detects TypeScript project" {
|
||||
cat > package.json << 'EOF'
|
||||
{
|
||||
"name": "test-ts-project",
|
||||
"devDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
run bash "$RALPH_ENABLE_CI" --from none
|
||||
|
||||
assert_success
|
||||
grep -q "PROJECT_TYPE=\"typescript\"" .ralphrc
|
||||
}
|
||||
|
||||
@test "ralph enable-ci detects Python project" {
|
||||
cat > pyproject.toml << 'EOF'
|
||||
[project]
|
||||
name = "test-python-project"
|
||||
EOF
|
||||
|
||||
run bash "$RALPH_ENABLE_CI" --from none
|
||||
|
||||
assert_success
|
||||
grep -q "PROJECT_TYPE=\"python\"" .ralphrc
|
||||
}
|
||||
|
||||
@test "ralph enable-ci respects --project-name override" {
|
||||
run bash "$RALPH_ENABLE_CI" --from none --project-name "custom-name"
|
||||
|
||||
assert_success
|
||||
grep -q "PROJECT_NAME=\"custom-name\"" .ralphrc
|
||||
}
|
||||
|
||||
@test "ralph enable-ci respects --project-type override" {
|
||||
run bash "$RALPH_ENABLE_CI" --from none --project-type "rust"
|
||||
|
||||
assert_success
|
||||
grep -q "PROJECT_TYPE=\"rust\"" .ralphrc
|
||||
}
|
||||
|
||||
@test "ralph enable-ci returns exit code 2 when already enabled" {
|
||||
# First enable
|
||||
bash "$RALPH_ENABLE_CI" --from none >/dev/null 2>&1
|
||||
|
||||
# Second enable without force
|
||||
run bash "$RALPH_ENABLE_CI" --from none
|
||||
|
||||
assert_equal "$status" 2
|
||||
}
|
||||
|
||||
@test "ralph enable-ci --force overwrites existing configuration" {
|
||||
# First enable
|
||||
bash "$RALPH_ENABLE_CI" --from none --project-name "old-name" >/dev/null 2>&1
|
||||
|
||||
# Second enable with force
|
||||
run bash "$RALPH_ENABLE_CI" --from none --force --project-name "new-name"
|
||||
|
||||
assert_success
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# JSON OUTPUT TESTS (3 tests)
|
||||
# =============================================================================
|
||||
|
||||
@test "ralph enable-ci --json outputs valid JSON on success" {
|
||||
run bash "$RALPH_ENABLE_CI" --from none --json
|
||||
|
||||
assert_success
|
||||
# Validate JSON structure
|
||||
echo "$output" | jq -e '.success == true'
|
||||
echo "$output" | jq -e '.project_name'
|
||||
echo "$output" | jq -e '.files_created'
|
||||
}
|
||||
|
||||
@test "ralph enable-ci --json includes project info" {
|
||||
cat > package.json << 'EOF'
|
||||
{"name": "json-test"}
|
||||
EOF
|
||||
|
||||
run bash "$RALPH_ENABLE_CI" --from none --json
|
||||
|
||||
assert_success
|
||||
echo "$output" | jq -e '.project_name == "json-test"'
|
||||
}
|
||||
|
||||
@test "ralph enable-ci --json returns proper structure when already enabled" {
|
||||
bash "$RALPH_ENABLE_CI" --from none >/dev/null 2>&1
|
||||
|
||||
run bash "$RALPH_ENABLE_CI" --from none --json
|
||||
|
||||
assert_equal "$status" 2
|
||||
echo "$output" | jq -e '.code == 2'
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# PRD IMPORT TESTS (2 tests)
|
||||
# =============================================================================
|
||||
|
||||
@test "ralph enable-ci imports tasks from PRD file" {
|
||||
mkdir -p docs
|
||||
cat > docs/requirements.md << 'EOF'
|
||||
# Project Requirements
|
||||
|
||||
- [ ] Implement user authentication
|
||||
- [ ] Add API endpoints
|
||||
- [ ] Create database schema
|
||||
EOF
|
||||
|
||||
run bash "$RALPH_ENABLE_CI" --from prd --prd docs/requirements.md
|
||||
|
||||
assert_success
|
||||
# Check that tasks were imported
|
||||
grep -q "authentication\|API\|database" .ralph/@fix_plan.md
|
||||
}
|
||||
|
||||
@test "ralph enable-ci fails gracefully with missing PRD file" {
|
||||
run bash "$RALPH_ENABLE_CI" --from prd --prd nonexistent.md
|
||||
|
||||
assert_failure
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# IDEMPOTENCY TESTS (3 tests)
|
||||
# =============================================================================
|
||||
|
||||
@test "ralph enable-ci is idempotent with force flag" {
|
||||
bash "$RALPH_ENABLE_CI" --from none >/dev/null 2>&1
|
||||
|
||||
# Add a file to .ralph
|
||||
echo "custom file" > .ralph/custom.txt
|
||||
|
||||
run bash "$RALPH_ENABLE_CI" --from none --force
|
||||
|
||||
assert_success
|
||||
# Custom file should still exist (we don't delete extra files)
|
||||
[[ -f ".ralph/custom.txt" ]]
|
||||
}
|
||||
|
||||
@test "ralph enable-ci preserves existing .ralph subdirectories" {
|
||||
bash "$RALPH_ENABLE_CI" --from none >/dev/null 2>&1
|
||||
|
||||
# Add custom content
|
||||
echo "spec content" > .ralph/specs/custom_spec.md
|
||||
|
||||
run bash "$RALPH_ENABLE_CI" --from none --force
|
||||
|
||||
assert_success
|
||||
[[ -f ".ralph/specs/custom_spec.md" ]]
|
||||
}
|
||||
|
||||
@test "ralph enable-ci does not overwrite existing files without force" {
|
||||
mkdir -p .ralph
|
||||
echo "original prompt" > .ralph/PROMPT.md
|
||||
echo "original fix plan" > .ralph/@fix_plan.md
|
||||
echo "original agent" > .ralph/@AGENT.md
|
||||
|
||||
run bash "$RALPH_ENABLE_CI" --from none
|
||||
|
||||
assert_equal "$status" 2
|
||||
# Verify original content preserved
|
||||
assert_equal "$(cat .ralph/PROMPT.md)" "original prompt"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# QUIET MODE TESTS (2 tests)
|
||||
# =============================================================================
|
||||
|
||||
@test "ralph enable-ci --quiet suppresses output" {
|
||||
run bash "$RALPH_ENABLE_CI" --from none --quiet
|
||||
|
||||
assert_success
|
||||
# Output should be minimal
|
||||
[[ -z "$output" ]] || [[ ! "$output" =~ "Detected" ]]
|
||||
}
|
||||
|
||||
@test "ralph enable-ci --quiet still creates files" {
|
||||
run bash "$RALPH_ENABLE_CI" --from none --quiet
|
||||
|
||||
assert_success
|
||||
[[ -f ".ralph/PROMPT.md" ]]
|
||||
}
|
||||
|
|
@ -27,6 +27,7 @@ setup() {
|
|||
export CLAUDE_SESSION_FILE="$RALPH_DIR/.claude_session_id"
|
||||
export RALPH_SESSION_FILE="$RALPH_DIR/.ralph_session"
|
||||
export RALPH_SESSION_HISTORY_FILE="$RALPH_DIR/.ralph_session_history"
|
||||
export RESPONSE_ANALYSIS_FILE="$RALPH_DIR/.response_analysis"
|
||||
export CLAUDE_MIN_VERSION="2.0.76"
|
||||
export CLAUDE_CODE_CMD="claude"
|
||||
export CLAUDE_USE_CONTINUE="true"
|
||||
|
|
|
|||
269
tests/unit/test_task_sources.bats
Normal file
269
tests/unit/test_task_sources.bats
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
#!/usr/bin/env bats
|
||||
# Unit tests for lib/task_sources.sh
|
||||
# Tests beads integration, GitHub integration, PRD extraction, and task normalization
|
||||
|
||||
load '../helpers/test_helper'
|
||||
load '../helpers/fixtures'
|
||||
|
||||
# Path to task_sources.sh
|
||||
TASK_SOURCES="${BATS_TEST_DIRNAME}/../../lib/task_sources.sh"
|
||||
|
||||
setup() {
|
||||
# Create temporary test directory
|
||||
TEST_DIR="$(mktemp -d)"
|
||||
cd "$TEST_DIR"
|
||||
|
||||
# Source the library
|
||||
source "$TASK_SOURCES"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
if [[ -n "$TEST_DIR" ]] && [[ -d "$TEST_DIR" ]]; then
|
||||
cd /
|
||||
rm -rf "$TEST_DIR"
|
||||
fi
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# BEADS DETECTION (3 tests)
|
||||
# =============================================================================
|
||||
|
||||
@test "check_beads_available returns false when no .beads directory" {
|
||||
run check_beads_available
|
||||
assert_failure
|
||||
}
|
||||
|
||||
@test "check_beads_available returns false when bd command not found" {
|
||||
mkdir -p .beads
|
||||
# bd command likely won't exist in test environment
|
||||
if command -v bd &>/dev/null; then
|
||||
skip "bd command is available"
|
||||
fi
|
||||
run check_beads_available
|
||||
assert_failure
|
||||
}
|
||||
|
||||
@test "get_beads_count returns 0 when beads unavailable" {
|
||||
run get_beads_count
|
||||
assert_output "0"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# GITHUB DETECTION (3 tests)
|
||||
# =============================================================================
|
||||
|
||||
@test "check_github_available returns false when no gh command" {
|
||||
# gh command may not exist in test environment
|
||||
if ! command -v gh &>/dev/null; then
|
||||
run check_github_available
|
||||
assert_failure
|
||||
else
|
||||
skip "gh command is available"
|
||||
fi
|
||||
}
|
||||
|
||||
@test "check_github_available returns false when not in git repo" {
|
||||
run check_github_available
|
||||
assert_failure
|
||||
}
|
||||
|
||||
@test "get_github_issue_count returns 0 when GitHub unavailable" {
|
||||
run get_github_issue_count
|
||||
assert_output "0"
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# PRD EXTRACTION (6 tests)
|
||||
# =============================================================================
|
||||
|
||||
@test "extract_prd_tasks extracts checkbox items" {
|
||||
cat > prd.md << 'EOF'
|
||||
# Requirements
|
||||
|
||||
- [ ] Implement user authentication
|
||||
- [x] Set up database
|
||||
- [ ] Add API endpoints
|
||||
EOF
|
||||
|
||||
run extract_prd_tasks "prd.md"
|
||||
|
||||
assert_success
|
||||
[[ "$output" =~ "Implement user authentication" ]]
|
||||
[[ "$output" =~ "Add API endpoints" ]]
|
||||
}
|
||||
|
||||
@test "extract_prd_tasks extracts numbered list items" {
|
||||
cat > prd.md << 'EOF'
|
||||
# Requirements
|
||||
|
||||
1. Implement user authentication
|
||||
2. Set up database
|
||||
3. Add API endpoints
|
||||
EOF
|
||||
|
||||
run extract_prd_tasks "prd.md"
|
||||
|
||||
assert_success
|
||||
[[ "$output" =~ "Implement user authentication" ]]
|
||||
}
|
||||
|
||||
@test "extract_prd_tasks returns empty for file without tasks" {
|
||||
cat > prd.md << 'EOF'
|
||||
# Empty Document
|
||||
|
||||
This document has no tasks.
|
||||
EOF
|
||||
|
||||
run extract_prd_tasks "prd.md"
|
||||
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "extract_prd_tasks returns error for missing file" {
|
||||
run extract_prd_tasks "nonexistent.md"
|
||||
assert_failure
|
||||
}
|
||||
|
||||
@test "extract_prd_tasks normalizes checked items to unchecked" {
|
||||
cat > prd.md << 'EOF'
|
||||
- [x] Completed task
|
||||
- [X] Another completed
|
||||
EOF
|
||||
|
||||
run extract_prd_tasks "prd.md"
|
||||
|
||||
assert_success
|
||||
[[ "$output" =~ "[ ]" ]]
|
||||
[[ ! "$output" =~ "[x]" ]]
|
||||
[[ ! "$output" =~ "[X]" ]]
|
||||
}
|
||||
|
||||
@test "extract_prd_tasks limits output to 30 tasks" {
|
||||
# Create PRD with 40 tasks
|
||||
{
|
||||
echo "# Tasks"
|
||||
for i in {1..40}; do
|
||||
echo "- [ ] Task $i"
|
||||
done
|
||||
} > prd.md
|
||||
|
||||
run extract_prd_tasks "prd.md"
|
||||
|
||||
# Count the number of task lines
|
||||
task_count=$(echo "$output" | grep -c '^\- \[' || echo "0")
|
||||
[[ "$task_count" -le 30 ]]
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# TASK NORMALIZATION (5 tests)
|
||||
# =============================================================================
|
||||
|
||||
@test "normalize_tasks converts bullet points to checkboxes" {
|
||||
input="- First task
|
||||
* Second task"
|
||||
|
||||
run normalize_tasks "$input"
|
||||
|
||||
assert_success
|
||||
[[ "$output" =~ "- [ ] First task" ]]
|
||||
[[ "$output" =~ "- [ ] Second task" ]]
|
||||
}
|
||||
|
||||
@test "normalize_tasks converts numbered items to checkboxes" {
|
||||
input="1. First task
|
||||
2. Second task"
|
||||
|
||||
run normalize_tasks "$input"
|
||||
|
||||
assert_success
|
||||
[[ "$output" =~ "- [ ]" ]]
|
||||
}
|
||||
|
||||
@test "normalize_tasks preserves existing checkboxes" {
|
||||
input="- [ ] Already a task"
|
||||
|
||||
run normalize_tasks "$input"
|
||||
|
||||
assert_success
|
||||
[[ "$output" =~ "- [ ] Already a task" ]]
|
||||
}
|
||||
|
||||
@test "normalize_tasks handles plain text lines" {
|
||||
input="Plain text task"
|
||||
|
||||
run normalize_tasks "$input"
|
||||
|
||||
assert_success
|
||||
[[ "$output" =~ "- [ ] Plain text task" ]]
|
||||
}
|
||||
|
||||
@test "normalize_tasks handles empty input" {
|
||||
run normalize_tasks ""
|
||||
assert_success
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# TASK PRIORITIZATION (3 tests)
|
||||
# =============================================================================
|
||||
|
||||
@test "prioritize_tasks puts critical tasks in High Priority" {
|
||||
input="- [ ] Critical bug fix
|
||||
- [ ] Normal task"
|
||||
|
||||
output=$(prioritize_tasks "$input" || true)
|
||||
|
||||
[[ "$output" =~ "## High Priority" ]]
|
||||
# Critical should be before Medium
|
||||
high_section="${output%%## Medium*}"
|
||||
[[ "$high_section" =~ "Critical bug fix" ]]
|
||||
}
|
||||
|
||||
@test "prioritize_tasks puts optional tasks in Low Priority" {
|
||||
input="- [ ] Nice to have feature
|
||||
- [ ] Normal task"
|
||||
|
||||
run prioritize_tasks "$input"
|
||||
|
||||
assert_success
|
||||
[[ "$output" =~ "## Low Priority" ]]
|
||||
low_section="${output##*## Low Priority}"
|
||||
[[ "$low_section" =~ "Nice to have" ]]
|
||||
}
|
||||
|
||||
@test "prioritize_tasks puts regular tasks in Medium Priority" {
|
||||
input="- [ ] Regular task"
|
||||
|
||||
output=$(prioritize_tasks "$input" || true)
|
||||
|
||||
[[ "$output" =~ "## Medium Priority" ]]
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# COMBINED IMPORT (3 tests)
|
||||
# =============================================================================
|
||||
|
||||
@test "import_tasks_from_sources handles prd source" {
|
||||
mkdir -p docs
|
||||
cat > docs/prd.md << 'EOF'
|
||||
# Requirements
|
||||
- [ ] Test task
|
||||
EOF
|
||||
|
||||
run import_tasks_from_sources "prd" "docs/prd.md" ""
|
||||
|
||||
assert_success
|
||||
[[ "$output" =~ "Test task" ]]
|
||||
}
|
||||
|
||||
@test "import_tasks_from_sources handles empty sources" {
|
||||
run import_tasks_from_sources "" "" ""
|
||||
|
||||
assert_failure
|
||||
}
|
||||
|
||||
@test "import_tasks_from_sources handles none source" {
|
||||
run import_tasks_from_sources "none" "" ""
|
||||
|
||||
# 'none' doesn't import anything, so fails
|
||||
assert_failure
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue