diff --git a/pkg/tui/messages.go b/pkg/tui/messages.go index e473b9a..7ab1f78 100644 --- a/pkg/tui/messages.go +++ b/pkg/tui/messages.go @@ -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{} diff --git a/pkg/tui/model.go b/pkg/tui/model.go index 3c1d338..6a3dccf 100644 --- a/pkg/tui/model.go +++ b/pkg/tui/model.go @@ -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"}, } } diff --git a/pkg/tui/update.go b/pkg/tui/update.go index 6b07418..f16b424 100644 --- a/pkg/tui/update.go +++ b/pkg/tui/update.go @@ -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 { diff --git a/pkg/tui/view.go b/pkg/tui/view.go index 2a431c1..8751715 100644 --- a/pkg/tui/view.go +++ b/pkg/tui/view.go @@ -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 { diff --git a/shell/process/manager.go b/shell/process/manager.go index f21a2a8..49f53d0 100644 --- a/shell/process/manager.go +++ b/shell/process/manager.go @@ -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 diff --git a/shell/tui/adapter.go b/shell/tui/adapter.go index 4e257a0..1cf5709 100644 --- a/shell/tui/adapter.go +++ b/shell/tui/adapter.go @@ -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{}