Files
fn_registry/bash/functions/infra/fleet_send_text_test.sh
T
egutierrez 3be8b28a8f feat(orchestration): fleet_send_text — nudge fiable por pane_id estable
El nudge del orquestador apuntaba al window_id (@N) de tmux, que migra cuando
el focus-swap de FleetView recrea windows (break-pane/join-pane): el texto
acababa en el window equivocado o en otro agente (a veces no llega). Ademas,
texto y Enter en la misma invocacion hacian que el TUI no interpretara el submit.

Nueva funcion fleet_send_text_bash_infra (grupo orchestration) que:
- resuelve el pane_id (%N) estable fresco justo antes de enviar (sessionId/PID
  a pane via tmux list-panes -a + walk de ancestros /proc), no el @N volatil;
- manda texto literal y Enter en invocaciones separadas;
- verifica con capture-pane que el texto llego antes del submit, con reintento;
- guards anti-self y error claro si el target no resuelve a un pane vivo.

Test (19/19) sobre socket tmux propio: confirma que tras break-pane el pane_id
no migra y el reenvio sigue llegando. orchestration.md (seccion Nudge + catalogo)
actualizado para usar la funcion en lugar del send-keys -t <@N> manual.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 21:08:47 +02:00

159 lines
6.2 KiB
Bash

#!/usr/bin/env bash
# Tests para fleet_send_text. Levanta un socket tmux PROPIO de test
# (fleet_test_<pid>, nunca el socket "fleet" real) con un pane `cat` vivo, y
# verifica: envio + verificacion via capture-pane (golden), supervivencia al
# focus-swap (break-pane preserva el pane_id), resolucion por sessionId fake,
# y los paths de error/guard. No toca la flota real ni ningun agente.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/fleet_send_text.sh"
PASS=0
FAIL=0
assert_contains() {
local test_name="$1" needle="$2" haystack="$3"
if echo "$haystack" | grep -qF "$needle"; then
echo "PASS: $test_name"
PASS=$((PASS+1))
else
echo "FAIL: $test_name — expected to contain '$needle'"
echo " got: $haystack"
FAIL=$((FAIL+1))
fi
}
assert_not_contains() {
local test_name="$1" needle="$2" haystack="$3"
if echo "$haystack" | grep -qF "$needle"; then
echo "FAIL: $test_name — should NOT contain '$needle'"
echo " got: $haystack"
FAIL=$((FAIL+1))
else
echo "PASS: $test_name"
PASS=$((PASS+1))
fi
}
assert_rc() {
local test_name="$1" expected="$2" actual="$3"
if [[ "$actual" == "$expected" ]]; then
echo "PASS: $test_name (rc=$actual)"
PASS=$((PASS+1))
else
echo "FAIL: $test_name — expected rc=$expected, got rc=$actual"
FAIL=$((FAIL+1))
fi
}
command -v tmux >/dev/null 2>&1 || { echo "SKIP: tmux no instalado"; exit 0; }
# --- Socket de test PROPIO + pane `cat` vivo (con echo de tty) ---
SOCK="fleet_test_$$"
TMP="$(mktemp -d)"
SESS="$TMP/sessions"
mkdir -p "$SESS"
cleanup() {
tmux -L "$SOCK" kill-server 2>/dev/null || true
rm -rf "$TMP"
}
trap cleanup EXIT
tmux -L "$SOCK" new-session -d -s t -x 120 -y 30 'cat'
sleep 0.4
PANE_PID="$(tmux -L "$SOCK" list-panes -a -F '#{pane_pid}' | head -n1)"
PANE_ID0="$(tmux -L "$SOCK" list-panes -a -F '#{pane_pid} #{pane_id}' | awk -v p="$PANE_PID" '$1==p{print $2}')"
WIN_ID0="$(tmux -L "$SOCK" list-panes -a -F '#{pane_pid} #{window_id}' | awk -v p="$PANE_PID" '$1==p{print $2}')"
echo "INFO: socket=$SOCK pane_pid=$PANE_PID pane_id=$PANE_ID0 window_id=$WIN_ID0"
# self_pid forzado a un PID que nunca sera target en los tests golden.
export FN_FLEET_SELF_PID=1
export FN_FLEET_SESSIONS_DIR="$SESS"
# --- Test 1 (golden): enviar por PID, verificar via capture-pane ---
set +e
out=$(fleet_send_text "$PANE_PID" "HOLA_FLEET_123" --socket "$SOCK" --no-enter --retries 1 2>&1); rc=$?
set -e
assert_rc "golden: envio por PID sale 0" 0 "$rc"
assert_contains "golden: reporta status=ok" "status=ok" "$out"
assert_contains "golden: reporta el pane_id estable" "pane=$PANE_ID0" "$out"
cap="$(tmux -L "$SOCK" capture-pane -p -t "$PANE_ID0")"
assert_contains "golden: el texto llego al pane (capture-pane)" "HOLA_FLEET_123" "$cap"
# limpiar input del cat
tmux -L "$SOCK" send-keys -t "$PANE_ID0" C-u; sleep 0.2
tmux -L "$SOCK" send-keys -t "$PANE_ID0" C-l 2>/dev/null || true; sleep 0.2
# --- Test 2 (edge focus-swap): mover el pane a otra window, pane_id NO migra ---
# Anadimos un segundo pane para poder break-pane el nuestro a una window nueva.
tmux -L "$SOCK" split-window -t "$WIN_ID0" -d 'cat'; sleep 0.3
tmux -L "$SOCK" break-pane -d -s "$PANE_ID0"; sleep 0.3
WIN_ID1="$(tmux -L "$SOCK" list-panes -a -F '#{pane_pid} #{window_id}' | awk -v p="$PANE_PID" '$1==p{print $2}')"
PANE_ID1="$(tmux -L "$SOCK" list-panes -a -F '#{pane_pid} #{pane_id}' | awk -v p="$PANE_PID" '$1==p{print $2}')"
echo "INFO: tras break-pane: pane_id=$PANE_ID1 (era $PANE_ID0) window_id=$WIN_ID1 (era $WIN_ID0)"
assert_contains "edge: pane_id NO cambia tras mover de window" "$PANE_ID0" "$PANE_ID1"
set +e
out=$(fleet_send_text "$PANE_PID" "TRAS_MOVER_456" --socket "$SOCK" --no-enter --retries 1 2>&1); rc=$?
set -e
assert_rc "edge: reenvio tras focus-swap sale 0" 0 "$rc"
cap="$(tmux -L "$SOCK" capture-pane -p -t "$PANE_ID1")"
assert_contains "edge: el texto sigue llegando tras mover de window" "TRAS_MOVER_456" "$cap"
tmux -L "$SOCK" send-keys -t "$PANE_ID1" C-u; sleep 0.2
# --- Test 3 (edge): resolver por sessionId (sessions/<pid>.json fake) ---
echo "{\"sessionId\":\"test-sid-aaa-111\",\"cwd\":\"/tmp/x\"}" > "$SESS/$PANE_PID.json"
set +e
out=$(fleet_send_text "test-sid-aaa" "VIA_SID_789" --socket "$SOCK" --no-enter --retries 1 2>&1); rc=$?
set -e
assert_rc "edge: resolucion por prefijo de sessionId sale 0" 0 "$rc"
cap="$(tmux -L "$SOCK" capture-pane -p -t "$PANE_ID1")"
assert_contains "edge: texto llego resolviendo por sessionId" "VIA_SID_789" "$cap"
tmux -L "$SOCK" send-keys -t "$PANE_ID1" C-u; sleep 0.2
# --- Test 4 (edge): --dry-run no envia nada ---
set +e
out=$(fleet_send_text "$PANE_PID" "NO_DEBE_APARECER_000" --socket "$SOCK" --no-enter --dry-run 2>&1); rc=$?
set -e
assert_rc "edge: dry-run sale 0" 0 "$rc"
assert_contains "edge: dry-run reporta status=dry-run" "status=dry-run" "$out"
cap="$(tmux -L "$SOCK" capture-pane -p -t "$PANE_ID1")"
assert_not_contains "edge: dry-run NO inyecto texto" "NO_DEBE_APARECER_000" "$cap"
# --- Test 5 (error): sessionId que no resuelve a PID -> rc 2 ---
set +e
out=$(fleet_send_text "sid-inexistente-zzz" "x" --socket "$SOCK" 2>&1); rc=$?
set -e
assert_rc "error: sessionId no resuelto sale 2" 2 "$rc"
assert_contains "error: mensaje de target no resuelto" "no se pudo resolver" "$out"
# --- Test 6 (error): falta el texto -> rc 2 ---
set +e
out=$(fleet_send_text "$PANE_PID" --socket "$SOCK" 2>&1); rc=$?
set -e
assert_rc "error: falta texto sale 2" 2 "$rc"
# --- Test 7 (guard anti-self): target == self_pid -> rc 3 ---
set +e
out=$(FN_FLEET_SELF_PID="$PANE_PID" fleet_send_text "$PANE_PID" "x" --socket "$SOCK" 2>&1); rc=$?
set -e
assert_rc "guard: enviar a la sesion actual sale 3" 3 "$rc"
assert_contains "guard: mensaje anti-self" "No me autoenvio" "$out"
# --- Test 8 (error): PID sin pane vivo -> rc 4 ---
set +e
out=$(fleet_send_text 999999 "x" --socket "$SOCK" 2>&1); rc=$?
set -e
assert_rc "error: PID sin pane vivo sale 4" 4 "$rc"
assert_contains "error: mensaje no pane vivo" "no se encontro un pane vivo" "$out"
# --- Resumen ---
echo ""
echo "================================"
echo "PASS: $PASS FAIL: $FAIL"
echo "================================"
[[ "$FAIL" -eq 0 ]]