feat(infra): kill_fleet_agent — cierre dirigido de un ejecutor de la flota (auto-kill)
Cierra UN ejecutor por sessionId (exacto/prefijo) o PID: SIGTERM al proceso claude (cierre limpio, recuperable con --resume) + kill-window de su window tmux. Lo usa el orquestador para liberar el slot idle de cada ejecutor en cuanto verifica que su DoD-contrato esta met. Guards: NO mata a un role=orchestrator (leido del goal.json) ni a la sesion que invoca (PID propio por ancestros /proc). --dry-run para inspeccionar sin tocar nada. Overrides FN_FLEET_* para tests. Tag grupo orchestration. Tests: 17 asserts (golden por sessionId/PID/prefijo, guards orchestrator/self rc=3, errores rc=2). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tests para kill_fleet_agent. Usa fixtures en dirs temporales (FN_FLEET_*) y
|
||||
# --dry-run para no matar procesos ni cerrar windows reales.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/kill_fleet_agent.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_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
|
||||
}
|
||||
|
||||
# --- Fixtures: sessions/<pid>.json + goals/<sid>.json en dirs temporales ---
|
||||
TMP="$(mktemp -d)"
|
||||
SESS="$TMP/sessions"
|
||||
GOALS="$TMP/goals"
|
||||
mkdir -p "$SESS" "$GOALS"
|
||||
|
||||
# Ejecutor: PID 4242, sessionId executor-aaa-111, role=executor.
|
||||
echo '{"sessionId":"executor-aaa-111","cwd":"/tmp/x"}' > "$SESS/4242.json"
|
||||
echo '{"goal":"hacer X","role":"executor","dod_contract":"golden..."}' > "$GOALS/executor-aaa-111.json"
|
||||
|
||||
# Orquestador: PID 5555, sessionId orchestrator-bbb-222, role=orchestrator.
|
||||
echo '{"sessionId":"orchestrator-bbb-222","cwd":"/tmp/y"}' > "$SESS/5555.json"
|
||||
echo '{"goal":"orquestar","role":"orchestrator"}' > "$GOALS/orchestrator-bbb-222.json"
|
||||
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
|
||||
export FN_FLEET_SESSIONS_DIR="$SESS"
|
||||
export FN_FLEET_GOALS_DIR="$GOALS"
|
||||
# Forzar self_pid a un valor que NO colisione con los fixtures (salvo el test self).
|
||||
export FN_FLEET_SELF_PID=999999
|
||||
|
||||
# --- Test 1 (golden): resolver ejecutor por sessionId, dry-run imprime plan ---
|
||||
set +e
|
||||
out=$(kill_fleet_agent executor-aaa-111 --socket nope --dry-run 2>&1); rc=$?
|
||||
set -e
|
||||
assert_rc "golden: ejecutor por sessionId sale 0" 0 "$rc"
|
||||
assert_contains "golden: plan muestra el PID resuelto" "PID: 4242" "$out"
|
||||
assert_contains "golden: plan muestra el sessionId" "executor-aaa-111" "$out"
|
||||
assert_contains "golden: plan muestra role executor" "role: executor" "$out"
|
||||
assert_contains "golden: dry-run no mata" "DRY-RUN" "$out"
|
||||
|
||||
# --- Test 2 (golden por PID + prefijo de sessionId) ---
|
||||
set +e
|
||||
out=$(kill_fleet_agent 4242 --dry-run 2>&1); rc=$?
|
||||
set -e
|
||||
assert_rc "golden: target por PID sale 0" 0 "$rc"
|
||||
assert_contains "golden: PID resuelve su sessionId" "executor-aaa-111" "$out"
|
||||
|
||||
set +e
|
||||
out=$(kill_fleet_agent executor-aaa --dry-run 2>&1); rc=$?
|
||||
set -e
|
||||
assert_rc "edge: prefijo de sessionId resuelve" 0 "$rc"
|
||||
assert_contains "edge: prefijo resuelve al PID 4242" "PID: 4242" "$out"
|
||||
|
||||
# --- Test 3 (EDGE guard role): negar matar a un orchestrator ---
|
||||
set +e
|
||||
out=$(kill_fleet_agent orchestrator-bbb-222 --dry-run 2>&1); rc=$?
|
||||
set -e
|
||||
assert_rc "guard: matar orchestrator devuelve rc=3" 3 "$rc"
|
||||
assert_contains "guard: mensaje menciona role=orchestrator" "role=orchestrator" "$out"
|
||||
|
||||
# --- Test 4 (EDGE guard self): negar matar a la sesion actual ---
|
||||
set +e
|
||||
out=$(FN_FLEET_SELF_PID=4242 kill_fleet_agent executor-aaa-111 --dry-run 2>&1); rc=$?
|
||||
set -e
|
||||
assert_rc "guard: matar self devuelve rc=3" 3 "$rc"
|
||||
assert_contains "guard: mensaje self menciona no suicidarse" "No me suicido" "$out"
|
||||
|
||||
# --- Test 5 (ERROR): target no resuelto a un PID ---
|
||||
set +e
|
||||
out=$(kill_fleet_agent sesion-inexistente-zzz --dry-run 2>&1); rc=$?
|
||||
set -e
|
||||
assert_rc "error: target inexistente devuelve rc=2" 2 "$rc"
|
||||
assert_contains "error: mensaje de no resuelto" "no se pudo resolver" "$out"
|
||||
|
||||
# --- Test 6 (ERROR): falta el target ---
|
||||
set +e
|
||||
out=$(kill_fleet_agent --dry-run 2>&1); rc=$?
|
||||
set -e
|
||||
assert_rc "error: sin target devuelve rc=2" 2 "$rc"
|
||||
assert_contains "error: mensaje falta target" "falta el target" "$out"
|
||||
|
||||
echo "---"
|
||||
echo "Results: $PASS passed, $FAIL failed"
|
||||
[[ $FAIL -eq 0 ]] || exit 1
|
||||
Reference in New Issue
Block a user