feat: ejecutar tests desde el dashboard TUI
Se añade opción "Run Tests" al menú del servidor en el dashboard TUI. Ejecuta `go test -tags goolm -count=1 ./...` y muestra los resultados en una pantalla dedicada (ScreenTestOutput) con scroll y opción de re-ejecutar. Cambios: - pkg/tui: nuevo MsgTestsDone, ScreenTestOutput, IntentRunTests, updateTestOutput - pkg/tui/view.go: viewTestOutput con scroll y controles (↑↓ r 0) - shell/tui/adapter.go: runTests() ejecuta go test con el env del manager - shell/process/manager.go: buildEnv → BuildEnv (exportado) para que el adapter pueda construir el env completo con las variables de .env Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -38,5 +38,11 @@ type MsgRebuildDone struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
// MsgTestsDone reports the result of running tests.
|
||||
type MsgTestsDone struct {
|
||||
Passed bool
|
||||
Output []string // lines of test output
|
||||
}
|
||||
|
||||
// MsgTick triggers a periodic refresh.
|
||||
type MsgTick struct{}
|
||||
|
||||
@@ -11,6 +11,7 @@ const (
|
||||
ScreenAgentActions // actions for a selected agent
|
||||
ScreenLogs // tail log output
|
||||
ScreenServer // server-wide process management
|
||||
ScreenTestOutput // test run output
|
||||
)
|
||||
|
||||
// Model is the complete TUI state — pure data.
|
||||
@@ -74,11 +75,13 @@ func ServerMenuOptions(running bool) []MenuOption {
|
||||
{Label: "Kill", Desc: "SIGKILL forzado"},
|
||||
{Label: "Rebuild & Restart", Desc: "Build + reiniciar"},
|
||||
{Label: "Logs", Desc: "Ver log del launcher"},
|
||||
{Label: "Run Tests", Desc: "Ejecutar todos los tests"},
|
||||
}
|
||||
}
|
||||
return []MenuOption{
|
||||
{Label: "Start", Desc: "Iniciar el launcher unificado"},
|
||||
{Label: "Rebuild & Restart", Desc: "Build + iniciar"},
|
||||
{Label: "Run Tests", Desc: "Ejecutar todos los tests"},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ const (
|
||||
IntentRestartLauncher IntentKind = "restart_launcher"
|
||||
IntentKillLauncher IntentKind = "kill_launcher"
|
||||
IntentRebuildRestart IntentKind = "rebuild_restart"
|
||||
IntentRunTests IntentKind = "run_tests"
|
||||
)
|
||||
|
||||
// Intent is pure data describing a side effect to execute.
|
||||
@@ -98,6 +99,18 @@ func Update(model Model, msg interface{}) (Model, []Intent) {
|
||||
model.LogScroll = max(0, len(m.Lines)-visibleLogLines(model))
|
||||
return model, nil
|
||||
|
||||
case MsgTestsDone:
|
||||
model.Screen = ScreenTestOutput
|
||||
model.LogLines = m.Output
|
||||
model.LogScroll = 0
|
||||
model.Cursor = 0
|
||||
if m.Passed {
|
||||
model.StatusMsg = "Tests PASSED"
|
||||
} else {
|
||||
model.StatusMsg = "Tests FAILED"
|
||||
}
|
||||
return model, nil
|
||||
|
||||
case MsgTick:
|
||||
return model, []Intent{{Kind: IntentLoadAgents}}
|
||||
|
||||
@@ -127,6 +140,8 @@ func updateKey(model Model, key KeyMsg) (Model, []Intent) {
|
||||
return updateLogs(model, key)
|
||||
case ScreenServer:
|
||||
return updateServerScreen(model, key)
|
||||
case ScreenTestOutput:
|
||||
return updateTestOutput(model, key)
|
||||
}
|
||||
return model, nil
|
||||
}
|
||||
@@ -283,6 +298,9 @@ func executeServerAction(model Model, action string) (Model, []Intent) {
|
||||
case "Rebuild & Restart":
|
||||
model.StatusMsg = "Building & restarting..."
|
||||
return model, []Intent{{Kind: IntentRebuildRestart}}
|
||||
case "Run Tests":
|
||||
model.StatusMsg = "Running tests..."
|
||||
return model, []Intent{{Kind: IntentRunTests}}
|
||||
case "Logs":
|
||||
model.Screen = ScreenLogs
|
||||
model.LogLines = nil
|
||||
@@ -294,6 +312,26 @@ func executeServerAction(model Model, action string) (Model, []Intent) {
|
||||
return model, nil
|
||||
}
|
||||
|
||||
func updateTestOutput(model Model, key KeyMsg) (Model, []Intent) {
|
||||
switch key.Str {
|
||||
case "0":
|
||||
model.Screen = ScreenServer
|
||||
model.Cursor = 0
|
||||
model.LogLines = nil
|
||||
model.LogScroll = 0
|
||||
model.StatusMsg = ""
|
||||
case "up", "k":
|
||||
model.LogScroll = max(0, model.LogScroll-1)
|
||||
case "down", "j":
|
||||
maxScroll := max(0, len(model.LogLines)-visibleLogLines(model))
|
||||
model.LogScroll = min(model.LogScroll+1, maxScroll)
|
||||
case "r":
|
||||
model.StatusMsg = "Running tests..."
|
||||
return model, []Intent{{Kind: IntentRunTests}}
|
||||
}
|
||||
return model, nil
|
||||
}
|
||||
|
||||
// ── pure helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
func visibleLogLines(m Model) int {
|
||||
|
||||
@@ -18,6 +18,8 @@ func View(model Model) string {
|
||||
return viewLogs(model)
|
||||
case ScreenServer:
|
||||
return viewServer(model)
|
||||
case ScreenTestOutput:
|
||||
return viewTestOutput(model)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
@@ -236,6 +238,40 @@ func viewServer(m Model) string {
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func viewTestOutput(m Model) string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString("\n Test Results\n")
|
||||
b.WriteString(" " + strings.Repeat("─", 60) + "\n")
|
||||
|
||||
if m.StatusMsg != "" {
|
||||
b.WriteString(" " + m.StatusMsg + "\n\n")
|
||||
}
|
||||
|
||||
if len(m.LogLines) == 0 {
|
||||
b.WriteString(" Running tests...\n")
|
||||
} else {
|
||||
visible := visibleLogLines(m)
|
||||
end := m.LogScroll + visible
|
||||
if end > len(m.LogLines) {
|
||||
end = len(m.LogLines)
|
||||
}
|
||||
start := m.LogScroll
|
||||
if start >= len(m.LogLines) {
|
||||
start = max(0, len(m.LogLines)-1)
|
||||
}
|
||||
for _, line := range m.LogLines[start:end] {
|
||||
if len(line) > m.WindowWidth-4 && m.WindowWidth > 10 {
|
||||
line = line[:m.WindowWidth-7] + "..."
|
||||
}
|
||||
b.WriteString(" " + line + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
b.WriteString("\n ↑↓ scroll r re-ejecutar 0 volver\n")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func countStatuses(agents []AgentView) (running, stopped, disabled int) {
|
||||
for _, a := range agents {
|
||||
switch {
|
||||
|
||||
@@ -174,7 +174,7 @@ func (m *Manager) Start(info AgentInfo) error {
|
||||
cmd = exec.Command(bin, "-c", info.ConfigPath)
|
||||
}
|
||||
|
||||
cmd.Env = m.buildEnv()
|
||||
cmd.Env = m.BuildEnv()
|
||||
cmd.Stdout = logFile
|
||||
cmd.Stderr = logFile
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
||||
@@ -385,7 +385,7 @@ func (m *Manager) LogPath(id string) string { return m.logPath(id) }
|
||||
// Returns the combined output and any error.
|
||||
func (m *Manager) Build() (string, error) {
|
||||
cmd := exec.Command("bash", "build.sh")
|
||||
cmd.Env = m.buildEnv()
|
||||
cmd.Env = m.BuildEnv()
|
||||
out, err := cmd.CombinedOutput()
|
||||
return string(out), err
|
||||
}
|
||||
@@ -416,7 +416,7 @@ func (m *Manager) StartUnified() error {
|
||||
cmd = exec.Command(bin, "--log-level", "info")
|
||||
}
|
||||
|
||||
cmd.Env = m.buildEnv()
|
||||
cmd.Env = m.BuildEnv()
|
||||
cmd.Stdout = logFile
|
||||
cmd.Stderr = logFile
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
||||
@@ -654,8 +654,8 @@ func (m *Manager) removePID(id string) {
|
||||
_ = os.Remove(m.pidPath(id))
|
||||
}
|
||||
|
||||
// buildEnv returns the environment for child processes: current env + .env file vars.
|
||||
func (m *Manager) buildEnv() []string {
|
||||
// BuildEnv returns the environment for child processes: current env + .env file vars.
|
||||
func (m *Manager) BuildEnv() []string {
|
||||
env := os.Environ()
|
||||
if m.envFile == "" {
|
||||
return env
|
||||
|
||||
@@ -4,6 +4,7 @@ package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -54,6 +55,9 @@ func (a *Adapter) RunIntent(intent puretui.Intent) tea.Cmd {
|
||||
case puretui.IntentRebuildRestart:
|
||||
return a.rebuildRestart()
|
||||
|
||||
case puretui.IntentRunTests:
|
||||
return a.runTests()
|
||||
|
||||
case puretui.IntentTick:
|
||||
return a.tick()
|
||||
|
||||
@@ -217,6 +221,28 @@ func (a *Adapter) loadLogs(id string) tea.Cmd {
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adapter) runTests() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
goBin, err := exec.LookPath("go")
|
||||
if err != nil {
|
||||
// Fallback to known location
|
||||
goBin = "/usr/local/go/bin/go"
|
||||
}
|
||||
cmd := exec.Command(goBin, "test", "-tags", "goolm", "-count=1", "./...")
|
||||
cmd.Env = a.mgr.BuildEnv()
|
||||
out, err := cmd.CombinedOutput()
|
||||
|
||||
output := strings.TrimSpace(string(out))
|
||||
if output == "" && err != nil {
|
||||
output = "Error: " + err.Error()
|
||||
}
|
||||
lines := strings.Split(output, "\n")
|
||||
passed := err == nil
|
||||
|
||||
return puretui.MsgTestsDone{Passed: passed, Output: lines}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adapter) tick() tea.Cmd {
|
||||
return tea.Tick(3*time.Second, func(time.Time) tea.Msg {
|
||||
return puretui.MsgTick{}
|
||||
|
||||
Reference in New Issue
Block a user