feat(orquestador): feed reactivo FLEET-STATE + fix peek de drain_fleet_events
El orquestador no se enteraba de los cambios de estado de su flota: el drenado
era manual y el peek documentado `./fn run drain_fleet_events --advance false`
devolvia un falso `{total_new:0, cursor:0}` porque `fn run` mapea los argumentos
posicionalmente y no parsea flags `--nombre valor` (events_path acababa valiendo
"--advance", una ruta inexistente).
- drain_fleet_events: nuevo helper _normalize_fn_run_flags que renormaliza el
patron `--advance <bool>` aplanado por `fn run`, de modo que el peek funciona
directo desde la CLI sin tocar el runner de Go. Bump 1.1.0 + growth log + tests
del normalizador (unit y end-to-end por HOME).
- summarize_fleet_transitions (nueva, pure, grupo claude-fleet): resume el dict
by_classification de drain en un bloque de una linea con las tres categorias
accionables (terminados / reclaman / estancados), dedup por session_id y
truncado de objetivo.
- hook_fleet_state_inject.sh (UserPromptSubmit): si la sesion es role=orchestrator
(leido de ~/.claude/goals/<session_id>.json), hace peek de la cola sin mover el
cursor y emite el bloque FLEET-STATE cada turno. Degrada limpio si el watcher
esta caido, la cola no existe o la sesion no es orquestador.
El registro del hook va en .claude/settings.local.json (gitignored, fuera de este
commit). Pendiente, lo integra otro agente: documentar el bloque FLEET-STATE en
.claude/commands/orquestador.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
"""Tests para summarize_fleet_transitions."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from summarize_fleet_transitions import summarize_fleet_transitions
|
||||
|
||||
DRAIN = "(drain con ./fn run drain_fleet_events para consumir)"
|
||||
|
||||
|
||||
def test_golden_tres_categorias_con_datos():
|
||||
by = {
|
||||
"DICE_TERMINADO": [{"session_id": "a1b2c3d4ee", "goal": "Goal terminado", "to": "DICE_TERMINADO"}],
|
||||
"RECLAMA": [{"session_id": "e5f6g7h8ii", "goal": "Goal reclama", "to": "RECLAMA"}],
|
||||
"ESTANCADO": [{"session_id": "99887766zz", "goal": "Goal estanca", "to": "ESTANCADO"}],
|
||||
}
|
||||
out = summarize_fleet_transitions(by)
|
||||
assert out == (
|
||||
"FLEET-STATE: terminados=[a1b2c3d4:Goal terminado] "
|
||||
"reclaman=[e5f6g7h8:Goal reclama] "
|
||||
"estancados=[99887766:Goal estanca] " + DRAIN
|
||||
)
|
||||
|
||||
|
||||
def test_categorias_vacias_sin_cambios():
|
||||
by = {"DICE_TERMINADO": [], "RECLAMA": [], "ESTANCADO": []}
|
||||
assert summarize_fleet_transitions(by) == "FLEET-STATE: sin cambios"
|
||||
|
||||
|
||||
def test_solo_no_accionables_sin_cambios():
|
||||
by = {
|
||||
"TRABAJANDO": [{"session_id": "aaaaaaaa11", "goal": "trabajando", "to": "TRABAJANDO"}],
|
||||
"GONE": [{"session_id": "bbbbbbbb22", "goal": "gone", "to": "GONE"}],
|
||||
"MAL_LANZADO": [{"session_id": "cccccccc33", "goal": "mal", "to": "MAL_LANZADO"}],
|
||||
}
|
||||
assert summarize_fleet_transitions(by) == "FLEET-STATE: sin cambios"
|
||||
|
||||
|
||||
def test_dedup_por_session_id_se_queda_ultima():
|
||||
by = {
|
||||
"RECLAMA": [
|
||||
{"session_id": "dup00000xx", "goal": "primera version", "to": "RECLAMA"},
|
||||
{"session_id": "dup00000xx", "goal": "segunda version", "to": "RECLAMA"},
|
||||
],
|
||||
}
|
||||
out = summarize_fleet_transitions(by)
|
||||
assert out == "FLEET-STATE: reclaman=[dup00000:segunda version] " + DRAIN
|
||||
assert "primera version" not in out
|
||||
|
||||
|
||||
def test_truncado_de_goal_mayor_de_40():
|
||||
goal = "X" * 50
|
||||
by = {"DICE_TERMINADO": [{"session_id": "trunc000yy", "goal": goal, "to": "DICE_TERMINADO"}]}
|
||||
out = summarize_fleet_transitions(by)
|
||||
expected_goal = "X" * 40 + "…"
|
||||
assert f"terminados=[trunc000:{expected_goal}]" in out
|
||||
# El goal mostrado tiene exactamente 40 chars + elipsis.
|
||||
assert ("X" * 41) not in out
|
||||
|
||||
|
||||
def test_max_items_con_overflow():
|
||||
by = {
|
||||
"DICE_TERMINADO": [
|
||||
{"session_id": f"term{i:06d}", "goal": f"t{i}", "to": "DICE_TERMINADO"} for i in range(4)
|
||||
],
|
||||
"RECLAMA": [
|
||||
{"session_id": f"recl{i:06d}", "goal": f"r{i}", "to": "RECLAMA"} for i in range(4)
|
||||
],
|
||||
}
|
||||
out = summarize_fleet_transitions(by, max_items=3)
|
||||
# 8 disponibles, 3 mostrados -> +5 mas. Se llenan en orden terminados primero.
|
||||
assert "(+5 mas)" in out
|
||||
assert out.count(":") >= 3 # al menos los 3 items renderizados
|
||||
# Los 3 primeros mostrados son de terminados (orden de categorias).
|
||||
# session_id truncado a 8 chars: "term000000" -> "term0000".
|
||||
assert "terminados=[term0000:t0, term0000:t1, term0000:t2]" in out
|
||||
assert "reclaman=" not in out
|
||||
|
||||
|
||||
def test_entrada_none_sin_cambios():
|
||||
assert summarize_fleet_transitions(None) == "FLEET-STATE: sin cambios"
|
||||
|
||||
|
||||
def test_entrada_dict_vacio_sin_cambios():
|
||||
assert summarize_fleet_transitions({}) == "FLEET-STATE: sin cambios"
|
||||
|
||||
|
||||
def test_valor_no_lista_se_salta():
|
||||
by = {
|
||||
"DICE_TERMINADO": "no soy una lista",
|
||||
"RECLAMA": [{"session_id": "ok000000aa", "goal": "valido", "to": "RECLAMA"}],
|
||||
}
|
||||
out = summarize_fleet_transitions(by)
|
||||
assert out == "FLEET-STATE: reclaman=[ok000000:valido] " + DRAIN
|
||||
|
||||
|
||||
def test_eventos_no_dict_se_saltan():
|
||||
by = {
|
||||
"ESTANCADO": [
|
||||
"string suelto",
|
||||
42,
|
||||
None,
|
||||
{"session_id": "good0000bb", "goal": "evento bueno", "to": "ESTANCADO"},
|
||||
],
|
||||
}
|
||||
out = summarize_fleet_transitions(by)
|
||||
assert out == "FLEET-STATE: estancados=[good0000:evento bueno] " + DRAIN
|
||||
|
||||
|
||||
def test_session_id_ausente_usa_placeholder():
|
||||
by = {"RECLAMA": [{"goal": "sin sid", "to": "RECLAMA"}]}
|
||||
out = summarize_fleet_transitions(by)
|
||||
assert out == "FLEET-STATE: reclaman=[????????:sin sid] " + DRAIN
|
||||
|
||||
|
||||
def test_goal_ausente_usa_placeholder():
|
||||
by = {"DICE_TERMINADO": [{"session_id": "nogoal00cc", "to": "DICE_TERMINADO"}]}
|
||||
out = summarize_fleet_transitions(by)
|
||||
assert out == "FLEET-STATE: terminados=[nogoal00:(sin objetivo)] " + DRAIN
|
||||
|
||||
|
||||
def test_omite_categorias_vacias_solo_renderiza_con_datos():
|
||||
by = {
|
||||
"DICE_TERMINADO": [],
|
||||
"RECLAMA": [{"session_id": "only0000dd", "goal": "solo reclama", "to": "RECLAMA"}],
|
||||
"ESTANCADO": [],
|
||||
}
|
||||
out = summarize_fleet_transitions(by)
|
||||
assert out == "FLEET-STATE: reclaman=[only0000:solo reclama] " + DRAIN
|
||||
assert "terminados=" not in out
|
||||
assert "estancados=" not in out
|
||||
|
||||
|
||||
def test_varios_eventos_misma_categoria_separados_por_coma():
|
||||
by = {
|
||||
"ESTANCADO": [
|
||||
{"session_id": "aaaaaaaa11", "goal": "uno", "to": "ESTANCADO"},
|
||||
{"session_id": "bbbbbbbb22", "goal": "dos", "to": "ESTANCADO"},
|
||||
],
|
||||
}
|
||||
out = summarize_fleet_transitions(by)
|
||||
assert out == "FLEET-STATE: estancados=[aaaaaaaa:uno, bbbbbbbb:dos] " + DRAIN
|
||||
Reference in New Issue
Block a user