927437a8d8
Sistema FleetView para centralizar la flota de procesos Claude Code vivos en una sola ventana kitty + tmux (socket aislado -L fleet) con un panel TUI: - list_claude_fleet (+ tipo claude_fleet): escanea ~/.claude/sessions + goals + runtime, valida procesos vivos (anti-PID-reciclado), join por sessionId. - list_resumable_claudes (+ tipo resumable_claude): sesiones cerradas reanudables. - wrappers tmux: tmux_new_claude_window (con --resume), tmux_swap_window_into_console (preserva ancho del sidebar), tmux_map_claude_panes. - launch_kittyclaude: comando entrypoint; instala atajos alt+flechas/enter/n/0/k/r, mouse on, remain-on-exit off; fija el ancho del sidebar con hooks. - docs/capabilities/claude-fleet.md + entrada en el INDEX. Incluye ademas funciones datascience en progreso (excel/duckdb/postgres) y ajustes varios de docs e infra de otra sesion, agrupados aqui para no perderlos. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
85 lines
2.5 KiB
Go
85 lines
2.5 KiB
Go
//go:build !windows && linux
|
|
|
|
package infra
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// tmuxAvailable reports whether the tmux binary is present. Tests skip when it
|
|
// is not (CI without tmux).
|
|
func tmuxAvailable(t *testing.T) {
|
|
t.Helper()
|
|
if _, err := exec.LookPath("tmux"); err != nil {
|
|
t.Skipf("tmux no disponible en PATH: %v", err)
|
|
}
|
|
}
|
|
|
|
// isolatedSocket returns a per-test isolated tmux socket name and registers a
|
|
// cleanup that kills its server. All commands in a test run against
|
|
// `tmux -L <socket> ...`, never the user's default server.
|
|
func isolatedSocket(t *testing.T) string {
|
|
t.Helper()
|
|
socket := fmt.Sprintf("fleettest_%d_%d", os.Getpid(), time.Now().UnixNano())
|
|
t.Cleanup(func() {
|
|
// best-effort: el server puede no existir si el test fallo antes de crearlo
|
|
_ = exec.Command("tmux", "-L", socket, "kill-server").Run()
|
|
})
|
|
return socket
|
|
}
|
|
|
|
// startConsoleSession crea una sesion <session> con una window "console" cuyo
|
|
// pane 0 corre `cat` (simula la TUI fleetview, un proceso que no termina).
|
|
func startConsoleSession(t *testing.T, socket, session string) {
|
|
t.Helper()
|
|
cmd := exec.Command("tmux", "-L", socket,
|
|
"new-session", "-d", "-s", session, "-n", "console", "cat")
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
t.Fatalf("new-session: %v (%s)", err, out)
|
|
}
|
|
}
|
|
|
|
func TestTmuxNewClaudeWindow(t *testing.T) {
|
|
tmuxAvailable(t)
|
|
socket := isolatedSocket(t)
|
|
session := "fleet"
|
|
startConsoleSession(t, socket, session)
|
|
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatalf("getwd: %v", err)
|
|
}
|
|
|
|
// El comando real ("claude ...") no esta disponible en el test, pero
|
|
// new-window devuelve el window_id ANTES de que el comando pueda fallar:
|
|
// tmux crea la window y reporta su id sincronamente. Validamos que el id
|
|
// venga con la forma esperada (@N) y no vacio.
|
|
windowID, err := TmuxNewClaudeWindow(socket, session, cwd)
|
|
if err != nil {
|
|
t.Fatalf("TmuxNewClaudeWindow: %v", err)
|
|
}
|
|
if windowID == "" {
|
|
t.Fatal("window_id vacio")
|
|
}
|
|
if !strings.HasPrefix(windowID, "@") {
|
|
t.Errorf("window_id %q no tiene la forma esperada @N", windowID)
|
|
}
|
|
}
|
|
|
|
func TestTmuxNewClaudeWindowEmptyArgs(t *testing.T) {
|
|
if _, err := TmuxNewClaudeWindow("", "fleet", "/tmp"); err == nil {
|
|
t.Error("socket vacio deberia dar error")
|
|
}
|
|
if _, err := TmuxNewClaudeWindow("sock", "", "/tmp"); err == nil {
|
|
t.Error("session vacia deberia dar error")
|
|
}
|
|
if _, err := TmuxNewClaudeWindow("sock", "fleet", ""); err == nil {
|
|
t.Error("cwd vacio deberia dar error")
|
|
}
|
|
}
|