Merge pull request #66 from frankbria/feature/session-continuity
feat(session): add session lifecycle management with auto-reset triggers
This commit is contained in:
commit
6ace046ef8
4 changed files with 622 additions and 5 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -3,6 +3,9 @@
|
||||||
.last_reset
|
.last_reset
|
||||||
.exit_signals
|
.exit_signals
|
||||||
status.json
|
status.json
|
||||||
|
.ralph_session
|
||||||
|
.ralph_session_history
|
||||||
|
.claude_session_id
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
logs/*
|
logs/*
|
||||||
|
|
|
||||||
28
CLAUDE.md
28
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.
|
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.6 | **Tests**: 239 passing (100% pass rate) | **CI/CD**: GitHub Actions
|
**Version**: v0.9.7 | **Tests**: 265 passing (100% pass rate) | **CI/CD**: GitHub Actions
|
||||||
|
|
||||||
## Core Architecture
|
## Core Architecture
|
||||||
|
|
||||||
|
|
@ -37,6 +37,9 @@ The system uses a modular architecture with reusable components in the `lib/` di
|
||||||
- Extracts structured fields: status, exit_signal, work_type, files_modified
|
- Extracts structured fields: status, exit_signal, work_type, files_modified
|
||||||
- **Session management**: `store_session_id()`, `get_last_session_id()`, `should_resume_session()`
|
- **Session management**: `store_session_id()`, `get_last_session_id()`, `should_resume_session()`
|
||||||
- Automatic session persistence to `.claude_session_id` file with 24-hour expiration
|
- Automatic session persistence to `.claude_session_id` file with 24-hour expiration
|
||||||
|
- Session lifecycle: `get_session_id()`, `reset_session()`, `log_session_transition()`, `init_session_tracking()`
|
||||||
|
- Session history tracked in `.ralph_session_history` (last 50 transitions)
|
||||||
|
- Session auto-reset on: circuit breaker open, manual interrupt, project completion
|
||||||
- Detects test-only loops and stuck error patterns
|
- Detects test-only loops and stuck error patterns
|
||||||
- Two-stage error filtering to eliminate false positives
|
- Two-stage error filtering to eliminate false positives
|
||||||
- Multi-line error matching for accurate stuck loop detection
|
- Multi-line error matching for accurate stuck loop detection
|
||||||
|
|
@ -81,6 +84,9 @@ ralph --status
|
||||||
# Circuit breaker management
|
# Circuit breaker management
|
||||||
ralph --reset-circuit
|
ralph --reset-circuit
|
||||||
ralph --circuit-status
|
ralph --circuit-status
|
||||||
|
|
||||||
|
# Session management
|
||||||
|
ralph --reset-session # Reset session state manually
|
||||||
```
|
```
|
||||||
|
|
||||||
### Monitoring
|
### Monitoring
|
||||||
|
|
@ -275,13 +281,14 @@ Ralph uses advanced error detection with two-stage filtering to eliminate false
|
||||||
|
|
||||||
## Test Suite
|
## Test Suite
|
||||||
|
|
||||||
### Test Files (239 tests total)
|
### Test Files (265 tests total)
|
||||||
|
|
||||||
| File | Tests | Description |
|
| File | Tests | Description |
|
||||||
|------|-------|-------------|
|
|------|-------|-------------|
|
||||||
| `test_cli_parsing.bats` | 27 | CLI argument parsing for all 12 flags |
|
| `test_cli_parsing.bats` | 27 | CLI argument parsing for all 12 flags |
|
||||||
| `test_cli_modern.bats` | 29 | Modern CLI commands (Phase 1.1) + build_claude_command fix |
|
| `test_cli_modern.bats` | 29 | Modern CLI commands (Phase 1.1) + build_claude_command fix |
|
||||||
| `test_json_parsing.bats` | 36 | JSON output format parsing + Claude CLI format + session management |
|
| `test_json_parsing.bats` | 36 | JSON output format parsing + Claude CLI format + session management |
|
||||||
|
| `test_session_continuity.bats` | 26 | Session lifecycle management + circuit breaker integration |
|
||||||
| `test_exit_detection.bats` | 20 | Exit signal detection |
|
| `test_exit_detection.bats` | 20 | Exit signal detection |
|
||||||
| `test_rate_limiting.bats` | 15 | Rate limiting behavior |
|
| `test_rate_limiting.bats` | 15 | Rate limiting behavior |
|
||||||
| `test_loop_execution.bats` | 20 | Integration tests |
|
| `test_loop_execution.bats` | 20 | Integration tests |
|
||||||
|
|
@ -304,6 +311,23 @@ bats tests/unit/test_cli_parsing.bats
|
||||||
|
|
||||||
## Recent Improvements
|
## Recent Improvements
|
||||||
|
|
||||||
|
### Session Lifecycle Management (v0.9.7)
|
||||||
|
- Added complete session lifecycle management with automatic reset triggers:
|
||||||
|
- `get_session_id()` - Retrieves current session from `.ralph_session`
|
||||||
|
- `reset_session(reason)` - Clears session with reason logging
|
||||||
|
- `log_session_transition()` - Records transitions to `.ralph_session_history`
|
||||||
|
- `init_session_tracking()` - Initializes session file with validation
|
||||||
|
- Session auto-reset integration points:
|
||||||
|
- Circuit breaker open events (stagnation detection)
|
||||||
|
- Manual interrupt (Ctrl+C / SIGINT)
|
||||||
|
- Project completion (graceful exit)
|
||||||
|
- Manual circuit breaker reset (`--reset-circuit`)
|
||||||
|
- Added `--reset-session` CLI flag for manual session reset
|
||||||
|
- Session history tracking (last 50 transitions) for debugging
|
||||||
|
- New configuration constants: `RALPH_SESSION_FILE`, `RALPH_SESSION_HISTORY_FILE`
|
||||||
|
- Added 26 new tests for session continuity features
|
||||||
|
- Test count: 265 (up from 239)
|
||||||
|
|
||||||
### JSON Output & Session Management (v0.9.6)
|
### JSON Output & Session Management (v0.9.6)
|
||||||
- Extended `parse_json_response()` to support Claude Code CLI JSON format
|
- Extended `parse_json_response()` to support Claude Code CLI JSON format
|
||||||
- Supports `result`, `sessionId`, and `metadata` fields alongside existing flat format
|
- Supports `result`, `sessionId`, and `metadata` fields alongside existing flat format
|
||||||
|
|
|
||||||
221
ralph_loop.sh
221
ralph_loop.sh
|
|
@ -33,6 +33,11 @@ CLAUDE_USE_CONTINUE=true # Enable session continuity
|
||||||
CLAUDE_SESSION_FILE=".claude_session_id" # Session ID persistence file
|
CLAUDE_SESSION_FILE=".claude_session_id" # Session ID persistence file
|
||||||
CLAUDE_MIN_VERSION="2.0.76" # Minimum required Claude CLI version
|
CLAUDE_MIN_VERSION="2.0.76" # Minimum required Claude CLI version
|
||||||
|
|
||||||
|
# Session management configuration (Phase 1.2)
|
||||||
|
# Note: SESSION_EXPIRATION_SECONDS is defined in lib/response_analyzer.sh (86400 = 24 hours)
|
||||||
|
RALPH_SESSION_FILE=".ralph_session" # Ralph-specific session tracking (lifecycle)
|
||||||
|
RALPH_SESSION_HISTORY_FILE=".ralph_session_history" # Session transition history
|
||||||
|
|
||||||
# Valid tool patterns for --allowed-tools validation
|
# Valid tool patterns for --allowed-tools validation
|
||||||
# Tools can be exact matches or pattern matches with wildcards in parentheses
|
# Tools can be exact matches or pattern matches with wildcards in parentheses
|
||||||
VALID_TOOL_PATTERNS=(
|
VALID_TOOL_PATTERNS=(
|
||||||
|
|
@ -477,6 +482,196 @@ save_claude_session() {
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# SESSION LIFECYCLE MANAGEMENT FUNCTIONS (Phase 1.2)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Get current session ID from Ralph session file
|
||||||
|
# Returns: session ID string or empty if not found
|
||||||
|
get_session_id() {
|
||||||
|
if [[ ! -f "$RALPH_SESSION_FILE" ]]; then
|
||||||
|
echo ""
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract session_id from JSON file (SC2155: separate declare from assign)
|
||||||
|
local session_id
|
||||||
|
session_id=$(jq -r '.session_id // ""' "$RALPH_SESSION_FILE" 2>/dev/null)
|
||||||
|
local jq_status=$?
|
||||||
|
|
||||||
|
# Handle jq failure or null/empty results
|
||||||
|
if [[ $jq_status -ne 0 || -z "$session_id" || "$session_id" == "null" ]]; then
|
||||||
|
session_id=""
|
||||||
|
fi
|
||||||
|
echo "$session_id"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Reset session with reason logging
|
||||||
|
# Usage: reset_session "reason_for_reset"
|
||||||
|
reset_session() {
|
||||||
|
local reason=${1:-"manual_reset"}
|
||||||
|
|
||||||
|
# Get current timestamp
|
||||||
|
local reset_timestamp
|
||||||
|
reset_timestamp=$(get_iso_timestamp)
|
||||||
|
|
||||||
|
# Always create/overwrite the session file using jq for safe JSON escaping
|
||||||
|
jq -n \
|
||||||
|
--arg session_id "" \
|
||||||
|
--arg created_at "" \
|
||||||
|
--arg last_used "" \
|
||||||
|
--arg reset_at "$reset_timestamp" \
|
||||||
|
--arg reset_reason "$reason" \
|
||||||
|
'{
|
||||||
|
session_id: $session_id,
|
||||||
|
created_at: $created_at,
|
||||||
|
last_used: $last_used,
|
||||||
|
reset_at: $reset_at,
|
||||||
|
reset_reason: $reset_reason
|
||||||
|
}' > "$RALPH_SESSION_FILE"
|
||||||
|
|
||||||
|
# Also clear the Claude session file for consistency
|
||||||
|
rm -f "$CLAUDE_SESSION_FILE" 2>/dev/null
|
||||||
|
|
||||||
|
# Log the session transition (non-fatal to prevent script exit under set -e)
|
||||||
|
log_session_transition "active" "reset" "$reason" "${loop_count:-0}" || true
|
||||||
|
|
||||||
|
log_status "INFO" "Session reset: $reason"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Log session state transitions to history file
|
||||||
|
# Usage: log_session_transition from_state to_state reason loop_number
|
||||||
|
log_session_transition() {
|
||||||
|
local from_state=$1
|
||||||
|
local to_state=$2
|
||||||
|
local reason=$3
|
||||||
|
local loop_number=${4:-0}
|
||||||
|
|
||||||
|
# Get timestamp once (SC2155: separate declare from assign)
|
||||||
|
local ts
|
||||||
|
ts=$(get_iso_timestamp)
|
||||||
|
|
||||||
|
# Create transition entry using jq for safe JSON (SC2155: separate declare from assign)
|
||||||
|
local transition
|
||||||
|
transition=$(jq -n -c \
|
||||||
|
--arg timestamp "$ts" \
|
||||||
|
--arg from_state "$from_state" \
|
||||||
|
--arg to_state "$to_state" \
|
||||||
|
--arg reason "$reason" \
|
||||||
|
--argjson loop_number "$loop_number" \
|
||||||
|
'{
|
||||||
|
timestamp: $timestamp,
|
||||||
|
from_state: $from_state,
|
||||||
|
to_state: $to_state,
|
||||||
|
reason: $reason,
|
||||||
|
loop_number: $loop_number
|
||||||
|
}')
|
||||||
|
|
||||||
|
# Read history file defensively - fallback to empty array on any failure
|
||||||
|
local history
|
||||||
|
if [[ -f "$RALPH_SESSION_HISTORY_FILE" ]]; then
|
||||||
|
history=$(cat "$RALPH_SESSION_HISTORY_FILE" 2>/dev/null)
|
||||||
|
# Validate JSON, fallback to empty array if corrupted
|
||||||
|
if ! echo "$history" | jq empty 2>/dev/null; then
|
||||||
|
history='[]'
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
history='[]'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Append transition and keep only last 50 entries
|
||||||
|
local updated_history
|
||||||
|
updated_history=$(echo "$history" | jq ". += [$transition] | .[-50:]" 2>/dev/null)
|
||||||
|
local jq_status=$?
|
||||||
|
|
||||||
|
# Only write if jq succeeded
|
||||||
|
if [[ $jq_status -eq 0 && -n "$updated_history" ]]; then
|
||||||
|
echo "$updated_history" > "$RALPH_SESSION_HISTORY_FILE"
|
||||||
|
else
|
||||||
|
# Fallback: start fresh with just this transition
|
||||||
|
echo "[$transition]" > "$RALPH_SESSION_HISTORY_FILE"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Generate a unique session ID using timestamp and random component
|
||||||
|
generate_session_id() {
|
||||||
|
local ts
|
||||||
|
ts=$(date +%s)
|
||||||
|
local rand
|
||||||
|
rand=$RANDOM
|
||||||
|
echo "ralph-${ts}-${rand}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Initialize session tracking (called at loop start)
|
||||||
|
init_session_tracking() {
|
||||||
|
local ts
|
||||||
|
ts=$(get_iso_timestamp)
|
||||||
|
|
||||||
|
# Create session file if it doesn't exist
|
||||||
|
if [[ ! -f "$RALPH_SESSION_FILE" ]]; then
|
||||||
|
local new_session_id
|
||||||
|
new_session_id=$(generate_session_id)
|
||||||
|
|
||||||
|
jq -n \
|
||||||
|
--arg session_id "$new_session_id" \
|
||||||
|
--arg created_at "$ts" \
|
||||||
|
--arg last_used "$ts" \
|
||||||
|
--arg reset_at "" \
|
||||||
|
--arg reset_reason "" \
|
||||||
|
'{
|
||||||
|
session_id: $session_id,
|
||||||
|
created_at: $created_at,
|
||||||
|
last_used: $last_used,
|
||||||
|
reset_at: $reset_at,
|
||||||
|
reset_reason: $reset_reason
|
||||||
|
}' > "$RALPH_SESSION_FILE"
|
||||||
|
|
||||||
|
log_status "INFO" "Initialized session tracking (session: $new_session_id)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate existing session file
|
||||||
|
if ! jq empty "$RALPH_SESSION_FILE" 2>/dev/null; then
|
||||||
|
log_status "WARN" "Corrupted session file detected, recreating..."
|
||||||
|
local new_session_id
|
||||||
|
new_session_id=$(generate_session_id)
|
||||||
|
|
||||||
|
jq -n \
|
||||||
|
--arg session_id "$new_session_id" \
|
||||||
|
--arg created_at "$ts" \
|
||||||
|
--arg last_used "$ts" \
|
||||||
|
--arg reset_at "$ts" \
|
||||||
|
--arg reset_reason "corrupted_file_recovery" \
|
||||||
|
'{
|
||||||
|
session_id: $session_id,
|
||||||
|
created_at: $created_at,
|
||||||
|
last_used: $last_used,
|
||||||
|
reset_at: $reset_at,
|
||||||
|
reset_reason: $reset_reason
|
||||||
|
}' > "$RALPH_SESSION_FILE"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Update last_used timestamp in session file (called on each loop iteration)
|
||||||
|
update_session_last_used() {
|
||||||
|
if [[ ! -f "$RALPH_SESSION_FILE" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local ts
|
||||||
|
ts=$(get_iso_timestamp)
|
||||||
|
|
||||||
|
# Update last_used in existing session file
|
||||||
|
local updated
|
||||||
|
updated=$(jq --arg last_used "$ts" '.last_used = $last_used' "$RALPH_SESSION_FILE" 2>/dev/null)
|
||||||
|
local jq_status=$?
|
||||||
|
|
||||||
|
if [[ $jq_status -eq 0 && -n "$updated" ]]; then
|
||||||
|
echo "$updated" > "$RALPH_SESSION_FILE"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# Global array for Claude command arguments (avoids shell injection)
|
# Global array for Claude command arguments (avoids shell injection)
|
||||||
declare -a CLAUDE_CMD_ARGS=()
|
declare -a CLAUDE_CMD_ARGS=()
|
||||||
|
|
||||||
|
|
@ -731,6 +926,7 @@ EOF
|
||||||
# Cleanup function
|
# Cleanup function
|
||||||
cleanup() {
|
cleanup() {
|
||||||
log_status "INFO" "Ralph loop interrupted. Cleaning up..."
|
log_status "INFO" "Ralph loop interrupted. Cleaning up..."
|
||||||
|
reset_session "manual_interrupt"
|
||||||
update_status "$loop_count" "$(cat "$CALL_COUNT_FILE" 2>/dev/null || echo "0")" "interrupted" "stopped"
|
update_status "$loop_count" "$(cat "$CALL_COUNT_FILE" 2>/dev/null || echo "0")" "interrupted" "stopped"
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
|
|
@ -772,12 +968,19 @@ main() {
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Initialize session tracking before entering the loop
|
||||||
|
init_session_tracking
|
||||||
|
|
||||||
log_status "INFO" "Starting main loop..."
|
log_status "INFO" "Starting main loop..."
|
||||||
log_status "INFO" "DEBUG: About to enter while loop, loop_count=$loop_count"
|
log_status "INFO" "DEBUG: About to enter while loop, loop_count=$loop_count"
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
loop_count=$((loop_count + 1))
|
loop_count=$((loop_count + 1))
|
||||||
log_status "INFO" "DEBUG: Successfully incremented loop_count to $loop_count"
|
log_status "INFO" "DEBUG: Successfully incremented loop_count to $loop_count"
|
||||||
|
|
||||||
|
# Update session last_used timestamp
|
||||||
|
update_session_last_used
|
||||||
|
|
||||||
log_status "INFO" "Loop #$loop_count - calling init_call_tracking..."
|
log_status "INFO" "Loop #$loop_count - calling init_call_tracking..."
|
||||||
init_call_tracking
|
init_call_tracking
|
||||||
|
|
||||||
|
|
@ -785,6 +988,7 @@ main() {
|
||||||
|
|
||||||
# Check circuit breaker before attempting execution
|
# Check circuit breaker before attempting execution
|
||||||
if should_halt_execution; then
|
if should_halt_execution; then
|
||||||
|
reset_session "circuit_breaker_open"
|
||||||
update_status "$loop_count" "$(cat "$CALL_COUNT_FILE")" "circuit_breaker_open" "halted" "stagnation_detected"
|
update_status "$loop_count" "$(cat "$CALL_COUNT_FILE")" "circuit_breaker_open" "halted" "stagnation_detected"
|
||||||
log_status "ERROR" "🛑 Circuit breaker has opened - execution halted"
|
log_status "ERROR" "🛑 Circuit breaker has opened - execution halted"
|
||||||
break
|
break
|
||||||
|
|
@ -800,6 +1004,7 @@ main() {
|
||||||
local exit_reason=$(should_exit_gracefully)
|
local exit_reason=$(should_exit_gracefully)
|
||||||
if [[ "$exit_reason" != "" ]]; then
|
if [[ "$exit_reason" != "" ]]; then
|
||||||
log_status "SUCCESS" "🏁 Graceful exit triggered: $exit_reason"
|
log_status "SUCCESS" "🏁 Graceful exit triggered: $exit_reason"
|
||||||
|
reset_session "project_complete"
|
||||||
update_status "$loop_count" "$(cat "$CALL_COUNT_FILE")" "graceful_exit" "completed" "$exit_reason"
|
update_status "$loop_count" "$(cat "$CALL_COUNT_FILE")" "graceful_exit" "completed" "$exit_reason"
|
||||||
|
|
||||||
log_status "SUCCESS" "🎉 Ralph has completed the project! Final stats:"
|
log_status "SUCCESS" "🎉 Ralph has completed the project! Final stats:"
|
||||||
|
|
@ -825,6 +1030,7 @@ main() {
|
||||||
sleep 5
|
sleep 5
|
||||||
elif [ $exec_result -eq 3 ]; then
|
elif [ $exec_result -eq 3 ]; then
|
||||||
# Circuit breaker opened
|
# Circuit breaker opened
|
||||||
|
reset_session "circuit_breaker_trip"
|
||||||
update_status "$loop_count" "$(cat "$CALL_COUNT_FILE")" "circuit_breaker_open" "halted" "stagnation_detected"
|
update_status "$loop_count" "$(cat "$CALL_COUNT_FILE")" "circuit_breaker_open" "halted" "stagnation_detected"
|
||||||
log_status "ERROR" "🛑 Circuit breaker has opened - halting loop"
|
log_status "ERROR" "🛑 Circuit breaker has opened - halting loop"
|
||||||
log_status "INFO" "Run 'ralph --reset-circuit' to reset the circuit breaker after addressing issues"
|
log_status "INFO" "Run 'ralph --reset-circuit' to reset the circuit breaker after addressing issues"
|
||||||
|
|
@ -896,6 +1102,7 @@ Options:
|
||||||
-t, --timeout MIN Set Claude Code execution timeout in minutes (default: $CLAUDE_TIMEOUT_MINUTES)
|
-t, --timeout MIN Set Claude Code execution timeout in minutes (default: $CLAUDE_TIMEOUT_MINUTES)
|
||||||
--reset-circuit Reset circuit breaker to CLOSED state
|
--reset-circuit Reset circuit breaker to CLOSED state
|
||||||
--circuit-status Show circuit breaker status and exit
|
--circuit-status Show circuit breaker status and exit
|
||||||
|
--reset-session Reset session state and exit (clears session continuity)
|
||||||
|
|
||||||
Modern CLI Options (Phase 1.1):
|
Modern CLI Options (Phase 1.1):
|
||||||
--output-format FORMAT Set Claude output format: json or text (default: $CLAUDE_OUTPUT_FORMAT)
|
--output-format FORMAT Set Claude output format: json or text (default: $CLAUDE_OUTPUT_FORMAT)
|
||||||
|
|
@ -906,6 +1113,10 @@ Files created:
|
||||||
- $LOG_DIR/: All execution logs
|
- $LOG_DIR/: All execution logs
|
||||||
- $DOCS_DIR/: Generated documentation
|
- $DOCS_DIR/: Generated documentation
|
||||||
- $STATUS_FILE: Current status (JSON)
|
- $STATUS_FILE: Current status (JSON)
|
||||||
|
- .ralph_session: Session lifecycle tracking
|
||||||
|
- .ralph_session_history: Session transition history (last 50)
|
||||||
|
- .call_count: API call counter for rate limiting
|
||||||
|
- .last_reset: Timestamp of last rate limit reset
|
||||||
|
|
||||||
Example workflow:
|
Example workflow:
|
||||||
ralph-setup my-project # Create project
|
ralph-setup my-project # Create project
|
||||||
|
|
@ -968,7 +1179,17 @@ while [[ $# -gt 0 ]]; do
|
||||||
# Source the circuit breaker library
|
# Source the circuit breaker library
|
||||||
SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")"
|
SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")"
|
||||||
source "$SCRIPT_DIR/lib/circuit_breaker.sh"
|
source "$SCRIPT_DIR/lib/circuit_breaker.sh"
|
||||||
|
source "$SCRIPT_DIR/lib/date_utils.sh"
|
||||||
reset_circuit_breaker "Manual reset via command line"
|
reset_circuit_breaker "Manual reset via command line"
|
||||||
|
reset_session "manual_circuit_reset"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
--reset-session)
|
||||||
|
# Reset session state only
|
||||||
|
SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")"
|
||||||
|
source "$SCRIPT_DIR/lib/date_utils.sh"
|
||||||
|
reset_session "manual_reset_flag"
|
||||||
|
echo -e "\033[0;32m✅ Session state reset successfully\033[0m"
|
||||||
exit 0
|
exit 0
|
||||||
;;
|
;;
|
||||||
--circuit-status)
|
--circuit-status)
|
||||||
|
|
|
||||||
369
tests/unit/test_session_continuity.bats
Normal file
369
tests/unit/test_session_continuity.bats
Normal file
|
|
@ -0,0 +1,369 @@
|
||||||
|
#!/usr/bin/env bats
|
||||||
|
# Unit tests for session continuity enhancements
|
||||||
|
# TDD: Tests for session lifecycle management across Ralph loops
|
||||||
|
|
||||||
|
load '../helpers/test_helper'
|
||||||
|
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
|
||||||
|
export PROMPT_FILE="PROMPT.md"
|
||||||
|
export LOG_DIR="logs"
|
||||||
|
export DOCS_DIR="docs/generated"
|
||||||
|
export STATUS_FILE="status.json"
|
||||||
|
export EXIT_SIGNALS_FILE=".exit_signals"
|
||||||
|
export CALL_COUNT_FILE=".call_count"
|
||||||
|
export TIMESTAMP_FILE=".last_reset"
|
||||||
|
export CLAUDE_SESSION_FILE=".claude_session_id"
|
||||||
|
export RALPH_SESSION_FILE=".ralph_session"
|
||||||
|
export RALPH_SESSION_HISTORY_FILE=".ralph_session_history"
|
||||||
|
export CLAUDE_MIN_VERSION="2.0.76"
|
||||||
|
export CLAUDE_CODE_CMD="claude"
|
||||||
|
export CLAUDE_USE_CONTINUE="true"
|
||||||
|
|
||||||
|
mkdir -p "$LOG_DIR" "$DOCS_DIR"
|
||||||
|
echo "0" > "$CALL_COUNT_FILE"
|
||||||
|
echo "$(date +%Y%m%d%H)" > "$TIMESTAMP_FILE"
|
||||||
|
echo '{"test_only_loops": [], "done_signals": [], "completion_indicators": []}' > "$EXIT_SIGNALS_FILE"
|
||||||
|
|
||||||
|
# Create sample project files
|
||||||
|
create_sample_prompt
|
||||||
|
create_sample_fix_plan "@fix_plan.md" 10 3
|
||||||
|
|
||||||
|
# Source library components
|
||||||
|
source "${BATS_TEST_DIRNAME}/../../lib/date_utils.sh"
|
||||||
|
source "${BATS_TEST_DIRNAME}/../../lib/response_analyzer.sh"
|
||||||
|
source "${BATS_TEST_DIRNAME}/../../lib/circuit_breaker.sh"
|
||||||
|
|
||||||
|
# Define color variables for log_status
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
PURPLE='\033[0;35m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
# Define log_status function for tests
|
||||||
|
log_status() {
|
||||||
|
local level=$1
|
||||||
|
local message=$2
|
||||||
|
echo "[$level] $message"
|
||||||
|
}
|
||||||
|
export -f log_status
|
||||||
|
}
|
||||||
|
|
||||||
|
teardown() {
|
||||||
|
if [[ -n "$TEST_DIR" ]] && [[ -d "$TEST_DIR" ]]; then
|
||||||
|
cd /
|
||||||
|
rm -rf "$TEST_DIR"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# HELPER: Check if function exists in ralph_loop.sh
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
function_exists_in_ralph() {
|
||||||
|
local func_name=$1
|
||||||
|
grep -qE "^${func_name}\s*\(\)|^function\s+${func_name}" "${BATS_TEST_DIRNAME}/../../ralph_loop.sh" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# SESSION RESET FUNCTION TESTS
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@test "reset_session function exists in ralph_loop.sh" {
|
||||||
|
run function_exists_in_ralph "reset_session"
|
||||||
|
[[ $status -eq 0 ]] || skip "reset_session function not yet implemented"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "get_session_id function exists in ralph_loop.sh" {
|
||||||
|
run function_exists_in_ralph "get_session_id"
|
||||||
|
[[ $status -eq 0 ]] || skip "get_session_id function not yet implemented"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "log_session_transition function exists in ralph_loop.sh" {
|
||||||
|
run function_exists_in_ralph "log_session_transition"
|
||||||
|
[[ $status -eq 0 ]] || skip "log_session_transition function not yet implemented"
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# --reset-session CLI FLAG TESTS
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@test "--reset-session flag is recognized in help" {
|
||||||
|
run bash "${BATS_TEST_DIRNAME}/../../ralph_loop.sh" --help
|
||||||
|
|
||||||
|
[[ "$output" == *"reset-session"* ]] || skip "--reset-session flag not yet implemented"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "--reset-session flag in argument parser" {
|
||||||
|
# Check if the flag exists in the argument parsing section
|
||||||
|
run grep -E '\-\-reset-session' "${BATS_TEST_DIRNAME}/../../ralph_loop.sh"
|
||||||
|
|
||||||
|
[[ $status -eq 0 ]] || skip "--reset-session flag not yet implemented"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "--reset-session resets session file" {
|
||||||
|
# Create a session file
|
||||||
|
echo '{"session_id": "session-to-reset", "timestamp": "2026-01-09T10:00:00Z"}' > "$RALPH_SESSION_FILE"
|
||||||
|
echo 'session-to-reset' > "$CLAUDE_SESSION_FILE"
|
||||||
|
|
||||||
|
# Run with --reset-session flag (should exit quickly)
|
||||||
|
run timeout 5 bash "${BATS_TEST_DIRNAME}/../../ralph_loop.sh" --reset-session 2>&1
|
||||||
|
|
||||||
|
# If flag not recognized, skip
|
||||||
|
if [[ "$output" == *"Unknown option"* ]]; then
|
||||||
|
skip "--reset-session flag not yet implemented"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check that session was reset
|
||||||
|
if [[ -f "$RALPH_SESSION_FILE" ]]; then
|
||||||
|
local session=$(jq -r '.session_id // ""' "$RALPH_SESSION_FILE" 2>/dev/null || echo "")
|
||||||
|
[[ -z "$session" || "$session" == "" || "$session" == "null" ]]
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# CIRCUIT BREAKER SESSION INTEGRATION TESTS
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@test "circuit breaker reset code includes session reset" {
|
||||||
|
# Check if reset_circuit_breaker mentions reset_session
|
||||||
|
run grep -A10 'reset_circuit_breaker' "${BATS_TEST_DIRNAME}/../../ralph_loop.sh"
|
||||||
|
|
||||||
|
[[ "$output" == *"reset_session"* ]] || skip "Circuit breaker session integration not yet implemented"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "cleanup function includes session reset" {
|
||||||
|
# Check if cleanup function includes reset_session
|
||||||
|
run grep -A5 'cleanup()' "${BATS_TEST_DIRNAME}/../../ralph_loop.sh"
|
||||||
|
|
||||||
|
[[ "$output" == *"reset_session"* ]] || skip "Cleanup session reset not yet implemented"
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# SESSION HISTORY TESTS
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@test "RALPH_SESSION_HISTORY_FILE constant defined" {
|
||||||
|
run grep 'RALPH_SESSION_HISTORY_FILE' "${BATS_TEST_DIRNAME}/../../ralph_loop.sh"
|
||||||
|
|
||||||
|
[[ $status -eq 0 ]] || skip "Session history file constant not yet defined"
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# RESPONSE ANALYZER SESSION FUNCTIONS (already implemented)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@test "store_session_id writes session to file with timestamp" {
|
||||||
|
run store_session_id "session-test-abc"
|
||||||
|
|
||||||
|
[[ -f "$CLAUDE_SESSION_FILE" ]] || skip "store_session_id not yet implemented"
|
||||||
|
|
||||||
|
local content=$(cat "$CLAUDE_SESSION_FILE")
|
||||||
|
[[ "$content" == *"session-test-abc"* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "get_last_session_id retrieves stored session" {
|
||||||
|
# First store a session
|
||||||
|
echo '{"session_id": "session-retrieve-test", "timestamp": "2026-01-09T10:00:00Z"}' > "$CLAUDE_SESSION_FILE"
|
||||||
|
|
||||||
|
run get_last_session_id
|
||||||
|
|
||||||
|
[[ "$output" == *"session-retrieve-test"* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "get_last_session_id returns empty when no session file" {
|
||||||
|
rm -f "$CLAUDE_SESSION_FILE"
|
||||||
|
|
||||||
|
run get_last_session_id
|
||||||
|
|
||||||
|
# Should return empty string, not error
|
||||||
|
[[ $status -eq 0 ]]
|
||||||
|
[[ -z "$output" || "$output" == "" || "$output" == "null" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "should_resume_session returns true for recent session" {
|
||||||
|
# Store a recent session
|
||||||
|
local now_iso=$(date -Iseconds 2>/dev/null || date +%Y-%m-%dT%H:%M:%S%z)
|
||||||
|
echo "{\"session_id\": \"session-recent\", \"timestamp\": \"$now_iso\"}" > "$CLAUDE_SESSION_FILE"
|
||||||
|
|
||||||
|
run should_resume_session
|
||||||
|
|
||||||
|
# Should indicate session can be resumed
|
||||||
|
[[ "$output" == "true" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "should_resume_session returns false for old session" {
|
||||||
|
# Store an old session (24+ hours ago)
|
||||||
|
echo '{"session_id": "session-old", "timestamp": "2020-01-01T00:00:00Z"}' > "$CLAUDE_SESSION_FILE"
|
||||||
|
|
||||||
|
run should_resume_session
|
||||||
|
|
||||||
|
# Should indicate session expired
|
||||||
|
[[ "$output" == "false" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "should_resume_session returns false when no session file" {
|
||||||
|
rm -f "$CLAUDE_SESSION_FILE"
|
||||||
|
|
||||||
|
run should_resume_session
|
||||||
|
|
||||||
|
# Should indicate no session to resume
|
||||||
|
[[ "$output" == "false" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# SESSION ID EXTRACTION FROM CLAUDE OUTPUT
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@test "parse_json_response extracts sessionId from Claude CLI format" {
|
||||||
|
local output_file="$LOG_DIR/test_output.log"
|
||||||
|
|
||||||
|
cat > "$output_file" << 'EOF'
|
||||||
|
{
|
||||||
|
"result": "Working on feature implementation.",
|
||||||
|
"sessionId": "session-unique-123"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
run parse_json_response "$output_file"
|
||||||
|
local result_file=".json_parse_result"
|
||||||
|
|
||||||
|
[[ -f "$result_file" ]]
|
||||||
|
|
||||||
|
local session_id=$(jq -r '.session_id' "$result_file")
|
||||||
|
assert_equal "$session_id" "session-unique-123"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "analyze_response persists sessionId to session file" {
|
||||||
|
local output_file="$LOG_DIR/test_output.log"
|
||||||
|
|
||||||
|
cat > "$output_file" << 'EOF'
|
||||||
|
{
|
||||||
|
"result": "Working on implementation.",
|
||||||
|
"sessionId": "session-persist-test-456"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
analyze_response "$output_file" 1
|
||||||
|
|
||||||
|
# Session ID should be persisted
|
||||||
|
[[ -f "$CLAUDE_SESSION_FILE" ]]
|
||||||
|
|
||||||
|
local stored=$(cat "$CLAUDE_SESSION_FILE" 2>/dev/null)
|
||||||
|
[[ "$stored" == *"session-persist-test-456"* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# SESSION CONTINUITY IN CLAUDE CLI COMMAND
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@test "--continue flag is added to Claude CLI command" {
|
||||||
|
# Check that --continue is used in build_claude_command
|
||||||
|
run grep -E '\-\-continue' "${BATS_TEST_DIRNAME}/../../ralph_loop.sh"
|
||||||
|
|
||||||
|
[[ $status -eq 0 ]]
|
||||||
|
[[ "$output" == *"--continue"* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "CLAUDE_USE_CONTINUE configuration controls session continuity" {
|
||||||
|
# Check that CLAUDE_USE_CONTINUE is defined and controls --continue
|
||||||
|
run grep 'CLAUDE_USE_CONTINUE' "${BATS_TEST_DIRNAME}/../../ralph_loop.sh"
|
||||||
|
|
||||||
|
[[ $status -eq 0 ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# SESSION EXPIRATION HANDLING
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@test "SESSION_EXPIRATION_SECONDS is defined in response_analyzer" {
|
||||||
|
run grep 'SESSION_EXPIRATION_SECONDS' "${BATS_TEST_DIRNAME}/../../lib/response_analyzer.sh"
|
||||||
|
|
||||||
|
[[ $status -eq 0 ]]
|
||||||
|
[[ "$output" == *"86400"* ]] # 24 hours in seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "expired session (24+ hours) is not resumed" {
|
||||||
|
# Create old session
|
||||||
|
echo '{"session_id": "old-session", "timestamp": "2020-01-01T00:00:00Z"}' > "$CLAUDE_SESSION_FILE"
|
||||||
|
|
||||||
|
run should_resume_session
|
||||||
|
|
||||||
|
[[ "$output" == "false" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# EDGE CASES
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@test "store_session_id handles empty session ID" {
|
||||||
|
run store_session_id ""
|
||||||
|
|
||||||
|
# Should fail or return error status
|
||||||
|
[[ $status -ne 0 ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "get_last_session_id handles corrupted JSON file" {
|
||||||
|
echo "not valid json at all {{{" > "$CLAUDE_SESSION_FILE"
|
||||||
|
|
||||||
|
run get_last_session_id
|
||||||
|
|
||||||
|
# Should not error, should return empty
|
||||||
|
[[ $status -eq 0 ]]
|
||||||
|
[[ -z "$output" || "$output" == "" || "$output" == "null" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "should_resume_session handles corrupted JSON file" {
|
||||||
|
echo "corrupted json {{{" > "$CLAUDE_SESSION_FILE"
|
||||||
|
|
||||||
|
run should_resume_session
|
||||||
|
|
||||||
|
# Should return false, not error
|
||||||
|
[[ $status -eq 0 || $status -eq 1 ]] # Either is acceptable
|
||||||
|
[[ "$output" == "false" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "should_resume_session handles missing timestamp field" {
|
||||||
|
echo '{"session_id": "session-no-time"}' > "$CLAUDE_SESSION_FILE"
|
||||||
|
|
||||||
|
run should_resume_session
|
||||||
|
|
||||||
|
# Should return false since no timestamp to validate
|
||||||
|
[[ "$output" == "false" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# INTEGRATION: FULL SESSION LIFECYCLE
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@test "full session lifecycle: store -> get -> check -> expires" {
|
||||||
|
# 1. Store a session
|
||||||
|
store_session_id "lifecycle-session-001"
|
||||||
|
|
||||||
|
# 2. Get it back
|
||||||
|
local stored=$(get_last_session_id)
|
||||||
|
[[ "$stored" == "lifecycle-session-001" ]]
|
||||||
|
|
||||||
|
# 3. Check if resumable (should be true since just created)
|
||||||
|
run should_resume_session
|
||||||
|
[[ "$output" == "true" ]]
|
||||||
|
|
||||||
|
# 4. Simulate expiration by setting old timestamp
|
||||||
|
echo '{"session_id": "lifecycle-session-001", "timestamp": "2020-01-01T00:00:00Z"}' > "$CLAUDE_SESSION_FILE"
|
||||||
|
|
||||||
|
# 5. Check again (should be expired)
|
||||||
|
run should_resume_session
|
||||||
|
[[ "$output" == "false" ]]
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue