# test_rclaude_helpers.sh — unit tests for rclaude's pure helpers. # # Strategy: source rclaude with a guard so the dispatch block doesn't fire, # then call the individual helpers directly. The guard is `RCLAUDE_LIB_ONLY=1` # — rclaude checks it at the top of its dispatch and returns early. # Source rclaude as a library. The dispatch block is bypassed by the guard. RCLAUDE_LIB_ONLY=1 . "$ROOT/bin/rclaude" 2>/dev/null || true # --------------------------------------------------------------------------- # claude_slug # --------------------------------------------------------------------------- test_claude_slug_basic() { assert_eq "-Users-natalie-Code--projects--lilith" \ "$(claude_slug "/Users/natalie/Code/@projects/@lilith")" } test_claude_slug_no_special() { # Leading `/` becomes leading `-` (claude's own behavior — every # non-alphanumeric char is replaced, including the leading slash). assert_eq "-tmp-foo" "$(claude_slug "/tmp/foo")" } test_claude_slug_empty() { assert_eq "" "$(claude_slug "")" } # --------------------------------------------------------------------------- # is_local # --------------------------------------------------------------------------- test_is_local_keywords() { assert_exit 0 is_local "local" assert_exit 0 is_local "localhost" assert_exit 0 is_local "127.0.0.1" assert_exit 0 is_local "::1" } test_is_local_unknown_host() { assert_exit 1 is_local "definitely-not-a-real-host-12345" } # --------------------------------------------------------------------------- # dedupe_sessions (keeps highest-mtime row per uuid) # --------------------------------------------------------------------------- test_dedupe_sessions_keeps_newest() { # Two rows with the same uuid (col 3), different mtimes (col 6). # Should retain only the row with the higher mtime. _in=$(printf 'apricot\tsession\tUUID-A\tsnip\tcwd\t100\nlocal\tsession\tUUID-A\tsnip2\tcwd\t200\n') _out=$(printf '%s' "$_in" | dedupe_sessions) _count=$(printf '%s\n' "$_out" | wc -l | tr -d ' ') assert_eq "1" "$_count" "expected 1 deduped row" || return 1 assert_contains "$_out" "200" "should keep mtime=200 row" || return 1 } test_dedupe_sessions_passes_unique() { _in=$(printf 'apricot\tsession\tA\ts\tc\t100\nlocal\tsession\tB\ts\tc\t100\n') _out=$(printf '%s' "$_in" | dedupe_sessions) _count=$(printf '%s\n' "$_out" | wc -l | tr -d ' ') assert_eq "2" "$_count" } # --------------------------------------------------------------------------- # get_home — always returns 0 even on failure (regression test) # --------------------------------------------------------------------------- test_get_home_unknown_returns_zero() { # Use a clearly invalid host. The function must not abort `set -e` # callers; previously this caused silent exits in cmd_resume. _v=$(get_home "definitely-not-a-real-host-12345-zzz" 2>/dev/null) _rc=$? assert_eq "0" "$_rc" "get_home must return 0 on failure" || return 1 assert_eq "" "$_v" "should produce empty stdout on failure" || return 1 } test_get_home_local_returns_HOME() { assert_eq "$HOME" "$(get_home local)" } # --------------------------------------------------------------------------- # caller_hostname — used by build_inner to tell remote MCPs where to send # things back (audio playback, etc.) # --------------------------------------------------------------------------- test_caller_hostname_env_override() { assert_eq "wg1.10.9.0.3" "$(RCLAUDE_BACK_HOST=wg1.10.9.0.3 caller_hostname)" } test_caller_hostname_default_adds_lan() { # Default behavior should produce a fqdn-ish form (either already # has a dot, or .lan got appended). We don't assert the exact host, # just that the result is non-empty and dotted. _out=$(unset RCLAUDE_BACK_HOST; caller_hostname) assert_contains "$_out" "." "caller_hostname output should be dotted" } # --------------------------------------------------------------------------- # sh_quote — POSIX single-quote escape for safe remote shell interpolation # --------------------------------------------------------------------------- test_sh_quote_empty() { assert_eq "''" "$(sh_quote "")" } test_sh_quote_plain() { assert_eq "'hello'" "$(sh_quote "hello")" } test_sh_quote_spaces() { assert_eq "'hello world'" "$(sh_quote "hello world")" } test_sh_quote_dollar_passthrough() { # `$HOME` inside single quotes must remain literal — that's the whole point. assert_eq "'\$HOME'" "$(sh_quote '$HOME')" } test_sh_quote_embedded_single_quote() { # The classic '\'' escape: close, escape, reopen. assert_eq "'it'\\''s'" "$(sh_quote "it's")" } # --------------------------------------------------------------------------- # filter_targets — selector-based row filter for `rclaude send` # --------------------------------------------------------------------------- # Two tmux rows + one disk row. Disk rows must always be dropped regardless # of selector (can't send-keys to on-disk sessions). Col 5 is populated on # one row to exercise the cwd-match branch. _targets_fixture() { printf 'local\ttmux\tclaude-natalie-lilith-123\t1 windows\n' printf 'apricot\ttmux\tclaude-natalie-scripts-456\t2 windows\t/home/natalie/Code/@scripts\n' printf 'apricot\tdisk\t/home/natalie/Code/@projects/@lilith\tsessions=3\n' } test_filter_targets_all_drops_disk() { _out=$(_targets_fixture | filter_targets all "") _count=$(printf '%s\n' "$_out" | grep -c '^' || true) assert_eq "2" "$_count" "all selector keeps both tmux rows, drops disk" } test_filter_targets_host_exact() { _out=$(_targets_fixture | filter_targets host apricot) _count=$(printf '%s\n' "$_out" | grep -c '^' || true) assert_eq "1" "$_count" "host=apricot matches one row" || return 1 assert_contains "$_out" "claude-natalie-scripts-456" } test_filter_targets_match_session_name() { _out=$(_targets_fixture | filter_targets match lilith) assert_contains "$_out" "claude-natalie-lilith-123" "lilith matches session name" } test_filter_targets_match_cwd_column() { # `@scripts` only appears in col 5 of the apricot row (the session-name # slug strips @ → -). The row must still match via the col-5 cwd branch. _out=$(_targets_fixture | filter_targets match "@scripts") assert_contains "$_out" "claude-natalie-scripts-456" "@scripts matches via cwd col" } test_filter_targets_match_no_match() { _out=$(_targets_fixture | filter_targets match nonexistent-pattern-xyz) assert_eq "" "$_out" "no match → empty output" } test_filter_targets_empty_input() { _out=$(printf '' | filter_targets all "") assert_eq "" "$_out" "empty input → empty output" }