Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 19 additions & 11 deletions ralph_loop.sh
Original file line number Diff line number Diff line change
Expand Up @@ -482,25 +482,33 @@ setup_tmux_session() {
local ralph_home="${RALPH_HOME:-$HOME/.ralph}"
local project_dir="$(pwd)"

# Get the tmux base-index / pane-base-index to handle custom configurations
# (e.g. `set -g base-index 1`, `setw -g pane-base-index 1` — very common in
# dotfiles). Without this, pane targets like `.0` don't exist and the Ralph
# loop never starts, leaving empty panes. See: tmux pane-base-index.
local base_win base_pane
base_win=$(get_tmux_base_index)
base_pane=$(get_tmux_pane_base_index)
local pane0=$((base_pane + 0)) # Ralph loop (left)
local pane1=$((base_pane + 1)) # Claude output (right-top)
local pane2=$((base_pane + 2)) # Status monitor (right-bottom)
# base-index / pane-base-index are detected AFTER the server starts (below).
# Querying earlier fails on the very first `ralph --monitor` run when no tmux
# server exists yet: `tmux show-options` does NOT auto-start a server, so it
# errors out and detection silently defaults to 0. With a `base-index 1` /
# `pane-base-index 1` config that makes every `window.pane` target off-by-one
# (e.g. `1.0`, window `0`), the loop command is sent to a nonexistent pane,
# and tmux opens to empty idle panes. Starting the server first makes the
# detection reliable. See: tmux pane-base-index.
local base_win base_pane pane0 pane1 pane2

log_status "INFO" "Setting up tmux session: $session_name"

# Initialize live.log file
echo "=== Ralph Live Output - Waiting for first loop... ===" > "$LIVE_LOG_FILE"

# Create new tmux session detached (left pane - Ralph loop)
# Create new tmux session detached (left pane - Ralph loop). This also starts
# the tmux server (sourcing the user's config), so the base-index /
# pane-base-index detection below is reliable even on the very first run.
tmux new-session -d -s "$session_name" -c "$project_dir"

# Detect base-index / pane-base-index now that the server is running.
base_win=$(get_tmux_base_index)
base_pane=$(get_tmux_pane_base_index)
pane0=$((base_pane + 0)) # Ralph loop (left)
pane1=$((base_pane + 1)) # Claude output (right-top)
pane2=$((base_pane + 2)) # Status monitor (right-bottom)

# Split window vertically (right side)
tmux split-window -h -t "$session_name" -c "$project_dir"

Expand Down
30 changes: 18 additions & 12 deletions tests/integration/test_monitor.bats
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
#!/usr/bin/env bats
# Integration tests for ralph_monitor.sh dashboard (Issue #15)
#
# Sourcing strategy: `head -n -1` loads all function definitions from
# ralph_monitor.sh without triggering the unconditional `main` call on
# the last line. This mirrors the inline/source pattern used in
# test_tmux_integration.bats and test_loop_execution.bats.
# Sourcing strategy: strip the final `main` line with `sed '$d'` (portable —
# `head -n -1` is GNU-only and errors on BSD), then source from a TEMP FILE.
# `source <(process-substitution)` does NOT define functions on macOS bash 3.2
# (they silently stay undefined), so the pipe→file→source form is required for
# portability. Mirrors the inline/source pattern in test_tmux_integration.bats.

bats_require_minimum_version 1.5.0

Expand All @@ -18,13 +19,18 @@ setup() {

MONITOR_SCRIPT="${BATS_TEST_DIRNAME}/../../ralph_monitor.sh"

# Load all monitor functions without calling main().
# head -n -1 strips the bare `main` call on the final line.
# Load all monitor functions without calling main(). `sed '$d'` strips the
# bare `main` call on the final line (portable — `head -n -1` is GNU-only).
# The `trap cleanup ...` line is filtered out: sourcing it would REPLACE
# bats' own EXIT trap, silently swallowing failing tests (the file then
# reports "Executed N-1 instead of expected N" with no `not ok` line).
# Write to a temp file and source THAT: `source <(process-substitution)` does
# NOT define functions on macOS bash 3.2 (they silently stay undefined), so
# the pipe→file→source form is required for portability.
_monitor_funcs="$TEST_DIR/.monitor_funcs.sh"
sed '$d' "$MONITOR_SCRIPT" | grep -v '^trap cleanup ' > "$_monitor_funcs"
# shellcheck disable=SC1090
source <(head -n -1 "$MONITOR_SCRIPT" | grep -v '^trap cleanup ')
source "$_monitor_funcs"

# Override clear_screen to suppress terminal-escape side-effects in tests.
clear_screen() { :; }
Expand Down Expand Up @@ -226,12 +232,12 @@ EOF
return 1
}

# Verify ralph_monitor.sh registers an EXIT trap that calls cleanup
# Verify ralph_monitor.sh registers an EXIT trap that calls cleanup.
# Source from a temp file (not `source <(...)` — broken on macOS bash 3.2).
local _trap_src="$TEST_DIR/.mon_trap.sh"
sed '$d' "${BATS_TEST_DIRNAME}/../../ralph_monitor.sh" > "$_trap_src"
local trap_output
trap_output=$(bash -c "
source <(head -n -1 '${BATS_TEST_DIRNAME}/../../ralph_monitor.sh')
trap -p EXIT
" 2>/dev/null)
trap_output=$(bash -c "source '$_trap_src'; trap -p EXIT" 2>/dev/null)
echo "$trap_output" | grep -q "cleanup" || {
echo "Expected EXIT trap to invoke cleanup; trap output: $trap_output"
return 1
Expand Down
56 changes: 48 additions & 8 deletions tests/integration/test_tmux_integration.bats
Original file line number Diff line number Diff line change
Expand Up @@ -61,22 +61,29 @@ setup_tmux_session() {
local ralph_home="${RALPH_HOME:-$HOME/.ralph}"
local project_dir="$(pwd)"

# Get the tmux base-index / pane-base-index to handle custom configurations
local base_win base_pane
base_win=$(get_tmux_base_index)
base_pane=$(get_tmux_pane_base_index)
local pane0=$((base_pane + 0))
local pane1=$((base_pane + 1))
local pane2=$((base_pane + 2))
# base-index / pane-base-index are detected AFTER the server starts (below).
# See ralph_loop.sh: querying earlier fails when no tmux server exists yet
# (first run) — `tmux show-options` does not auto-start a server, so detection
# silently defaults to 0. With a base-index 1 config that yields off-by-one
# pane targets and the loop never starts (tmux shows empty idle panes). Keep
# this inline mirror in sync with ralph_loop.sh.
local base_win base_pane pane0 pane1 pane2

log_status "INFO" "Setting up tmux session: $session_name"

# Initialize live.log file
echo "=== Ralph Live Output - Waiting for first loop... ===" > "$LIVE_LOG_FILE"

# Create new tmux session detached (left pane - Ralph loop)
# Create new tmux session detached (left pane - Ralph loop). Starts the server.
tmux new-session -d -s "$session_name" -c "$project_dir"

# Detect base-index / pane-base-index now that the server is running.
base_win=$(get_tmux_base_index)
base_pane=$(get_tmux_pane_base_index)
pane0=$((base_pane + 0))
pane1=$((base_pane + 1))
pane2=$((base_pane + 2))

# Split window vertically (right side)
tmux split-window -h -t "$session_name" -c "$project_dir"

Expand Down Expand Up @@ -348,6 +355,39 @@ assert_tmux_called_with() {
assert_tmux_called_with "tmux show-options"
}

# ==============================================================================
# TEST 3a: setup_tmux_session detects base-index AFTER starting the server
# Regression: with a base-index 1 / pane-base-index 1 config and no tmux server
# running yet (the first `ralph --monitor`), detecting BEFORE new-session made
# `tmux show-options` fail (it does not auto-start a server), silently defaulting
# detection to 0. Every pane/window target was then off-by-one, so the `ralph
# --live` loop command was sent to a nonexistent pane and tmux opened to empty
# idle panes. The fix detects AFTER `tmux new-session` starts the server. This
# test guards the call ORDER: the first `new-session` must precede the first
# `show-options` in the tmux call log.
# ==============================================================================
@test "setup_tmux_session detects base-index only after new-session starts the server" {
export MOCK_TMUX_BASE_INDEX="1"
export MOCK_TMUX_PANE_BASE_INDEX="1"
run setup_tmux_session
[ "$status" -eq 0 ]

local new_session_line show_options_line
new_session_line=$(grep -nE '^tmux new-session -d' "$TMUX_CALL_LOG" | head -1 | cut -d: -f1)
show_options_line=$(grep -nE '^tmux show-options' "$TMUX_CALL_LOG" | head -1 | cut -d: -f1)

[ -n "$new_session_line" ] || { echo "no new-session call logged:"; cat "$TMUX_CALL_LOG"; return 1; }
[ -n "$show_options_line" ] || { echo "no show-options call logged:"; cat "$TMUX_CALL_LOG"; return 1; }

# The ordering invariant: server starts first, THEN base-index is queried.
if [ "$new_session_line" -ge "$show_options_line" ]; then
echo "FAIL: new-session (line $new_session_line) must precede show-options (line $show_options_line)"
echo "--- tmux call log ---"
cat "$TMUX_CALL_LOG"
return 1
fi
}

# ==============================================================================
# TEST 4: setup_tmux_session creates session with -d flag and ralph- prefix
# ==============================================================================
Expand Down
5 changes: 4 additions & 1 deletion tests/unit/test_log_rotation.bats
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,10 @@ if [[ "\$1" == "-c%s" ]]; then
fi
if [[ "\$1" == "-f%z" ]]; then
shift
exec "$real_stat" -c%s "\$@"
# Portable byte count — real stat's -c%s is GNU-only and fails on BSD/macOS,
# which silently defeated this fallback simulation there. wc -c works everywhere.
wc -c < "\$1"
Comment on lines +84 to +86

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Terminate the BSD stat stub branch after printing size.

On Line 86, the stub prints byte count via wc -c, but then falls through to Line 88 (exec "$real_stat" "$@"). That can append extra output and make the fallback simulation inaccurate.

Proposed fix
 if [[ "$1" == "-f%z" ]]; then
   shift
   # Portable byte count — real stat's -c%s is GNU-only and fails on BSD/macOS,
   # which silently defeated this fallback simulation there. wc -c works everywhere.
   wc -c < "$1"
+  exit 0
 fi
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Portable byte count — real stat's -c%s is GNU-only and fails on BSD/macOS,
# which silently defeated this fallback simulation there. wc -c works everywhere.
wc -c < "\$1"
if [[ "$1" == "-f%z" ]]; then
shift
# Portable byte count — real stat's -c%s is GNU-only and fails on BSD/macOS,
# which silently defeated this fallback simulation there. wc -c works everywhere.
wc -c < "$1"
exit 0
fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/test_log_rotation.bats` around lines 84 - 86, The BSD stat stub
branch prints the byte count using wc -c but then falls through to the real_stat
exec call on the following line, appending extra output. Add a return statement
immediately after the wc -c line to terminate that branch and prevent
fall-through to the exec statement, ensuring the stub outputs only the byte
count without additional output from the real stat command.

exit 0
fi
exec "$real_stat" "\$@"
STUBEOF
Expand Down
4 changes: 4 additions & 0 deletions tests/unit/test_notifications.bats
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,10 @@ EOF
# =============================================================================

@test "send_notification uses notify-send on Linux when osascript unavailable" {
# osascript is always present on macOS (/usr/bin, and /bin symlinks to it),
# so the notify-send branch — which fires only when osascript is absent — is
# unreachable there and cannot be hidden via PATH. Exercise it on Linux only.
[[ "$(uname)" == "Darwin" ]] && skip "notify-send branch unreachable on macOS (osascript always present)"
export ENABLE_NOTIFICATIONS=true

# Create a private bin dir that has notify-send but NOT osascript
Expand Down
14 changes: 10 additions & 4 deletions tests/unit/test_ralph_enable.bats
Original file line number Diff line number Diff line change
Expand Up @@ -294,8 +294,11 @@ EOF
cd "'"$TEST_DIR"'"
NON_INTERACTIVE=true

# Define phase_verification from ralph_enable.sh
source <(sed -n "/^phase_verification()/,/^}/p" "'"${BATS_TEST_DIRNAME}"'/../../ralph_enable.sh")
# Define phase_verification from ralph_enable.sh. Write to a temp file and
# source THAT — `source <(process-substitution)` does not define functions
# on macOS bash 3.2 (they silently stay undefined → "command not found").
sed -n "/^phase_verification()/,/^}/p" "'"${BATS_TEST_DIRNAME}"'/../../ralph_enable.sh" > .pv.sh
source .pv.sh

phase_verification
'
Expand Down Expand Up @@ -323,8 +326,11 @@ EOF
cd "'"$TEST_DIR"'"
NON_INTERACTIVE=true

# Define phase_verification from ralph_enable.sh
source <(sed -n "/^phase_verification()/,/^}/p" "'"${BATS_TEST_DIRNAME}"'/../../ralph_enable.sh")
# Define phase_verification from ralph_enable.sh. Write to a temp file and
# source THAT — `source <(process-substitution)` does not define functions
# on macOS bash 3.2 (they silently stay undefined → "command not found").
sed -n "/^phase_verification()/,/^}/p" "'"${BATS_TEST_DIRNAME}"'/../../ralph_enable.sh" > .pv.sh
source .pv.sh

phase_verification
'
Expand Down
Loading