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
|
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.
|
// MsgTick triggers a periodic refresh.
|
||||||
type MsgTick struct{}
|
type MsgTick struct{}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const (
|
|||||||
ScreenAgentActions // actions for a selected agent
|
ScreenAgentActions // actions for a selected agent
|
||||||
ScreenLogs // tail log output
|
ScreenLogs // tail log output
|
||||||
ScreenServer // server-wide process management
|
ScreenServer // server-wide process management
|
||||||
|
ScreenTestOutput // test run output
|
||||||
)
|
)
|
||||||
|
|
||||||
// Model is the complete TUI state — pure data.
|
// Model is the complete TUI state — pure data.
|
||||||
@@ -74,11 +75,13 @@ func ServerMenuOptions(running bool) []MenuOption {
|
|||||||
{Label: "Kill", Desc: "SIGKILL forzado"},
|
{Label: "Kill", Desc: "SIGKILL forzado"},
|
||||||
{Label: "Rebuild & Restart", Desc: "Build + reiniciar"},
|
{Label: "Rebuild & Restart", Desc: "Build + reiniciar"},
|
||||||
{Label: "Logs", Desc: "Ver log del launcher"},
|
{Label: "Logs", Desc: "Ver log del launcher"},
|
||||||
|
{Label: "Run Tests", Desc: "Ejecutar todos los tests"},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return []MenuOption{
|
return []MenuOption{
|
||||||
{Label: "Start", Desc: "Iniciar el launcher unificado"},
|
{Label: "Start", Desc: "Iniciar el launcher unificado"},
|
||||||
{Label: "Rebuild & Restart", Desc: "Build + iniciar"},
|
{Label: "Rebuild & Restart", Desc: "Build + iniciar"},
|
||||||
|
{Label: "Run Tests", Desc: "Ejecutar todos los tests"},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const (
|
|||||||
IntentRestartLauncher IntentKind = "restart_launcher"
|
IntentRestartLauncher IntentKind = "restart_launcher"
|
||||||
IntentKillLauncher IntentKind = "kill_launcher"
|
IntentKillLauncher IntentKind = "kill_launcher"
|
||||||
IntentRebuildRestart IntentKind = "rebuild_restart"
|
IntentRebuildRestart IntentKind = "rebuild_restart"
|
||||||
|
IntentRunTests IntentKind = "run_tests"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Intent is pure data describing a side effect to execute.
|
// 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))
|
model.LogScroll = max(0, len(m.Lines)-visibleLogLines(model))
|
||||||
return model, nil
|
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:
|
case MsgTick:
|
||||||
return model, []Intent{{Kind: IntentLoadAgents}}
|
return model, []Intent{{Kind: IntentLoadAgents}}
|
||||||
|
|
||||||
@@ -127,6 +140,8 @@ func updateKey(model Model, key KeyMsg) (Model, []Intent) {
|
|||||||
return updateLogs(model, key)
|
return updateLogs(model, key)
|
||||||
case ScreenServer:
|
case ScreenServer:
|
||||||
return updateServerScreen(model, key)
|
return updateServerScreen(model, key)
|
||||||
|
case ScreenTestOutput:
|
||||||
|
return updateTestOutput(model, key)
|
||||||
}
|
}
|
||||||
return model, nil
|
return model, nil
|
||||||
}
|
}
|
||||||
@@ -283,6 +298,9 @@ func executeServerAction(model Model, action string) (Model, []Intent) {
|
|||||||
case "Rebuild & Restart":
|
case "Rebuild & Restart":
|
||||||
model.StatusMsg = "Building & restarting..."
|
model.StatusMsg = "Building & restarting..."
|
||||||
return model, []Intent{{Kind: IntentRebuildRestart}}
|
return model, []Intent{{Kind: IntentRebuildRestart}}
|
||||||
|
case "Run Tests":
|
||||||
|
model.StatusMsg = "Running tests..."
|
||||||
|
return model, []Intent{{Kind: IntentRunTests}}
|
||||||
case "Logs":
|
case "Logs":
|
||||||
model.Screen = ScreenLogs
|
model.Screen = ScreenLogs
|
||||||
model.LogLines = nil
|
model.LogLines = nil
|
||||||
@@ -294,6 +312,26 @@ func executeServerAction(model Model, action string) (Model, []Intent) {
|
|||||||
return model, nil
|
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 ─────────────────────────────────────────────────────────
|
// ── pure helpers ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
func visibleLogLines(m Model) int {
|
func visibleLogLines(m Model) int {
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ func View(model Model) string {
|
|||||||
return viewLogs(model)
|
return viewLogs(model)
|
||||||
case ScreenServer:
|
case ScreenServer:
|
||||||
return viewServer(model)
|
return viewServer(model)
|
||||||
|
case ScreenTestOutput:
|
||||||
|
return viewTestOutput(model)
|
||||||
default:
|
default:
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -236,6 +238,40 @@ func viewServer(m Model) string {
|
|||||||
return b.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) {
|
func countStatuses(agents []AgentView) (running, stopped, disabled int) {
|
||||||
for _, a := range agents {
|
for _, a := range agents {
|
||||||
switch {
|
switch {
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ func (m *Manager) Start(info AgentInfo) error {
|
|||||||
cmd = exec.Command(bin, "-c", info.ConfigPath)
|
cmd = exec.Command(bin, "-c", info.ConfigPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.Env = m.buildEnv()
|
cmd.Env = m.BuildEnv()
|
||||||
cmd.Stdout = logFile
|
cmd.Stdout = logFile
|
||||||
cmd.Stderr = logFile
|
cmd.Stderr = logFile
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
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.
|
// Returns the combined output and any error.
|
||||||
func (m *Manager) Build() (string, error) {
|
func (m *Manager) Build() (string, error) {
|
||||||
cmd := exec.Command("bash", "build.sh")
|
cmd := exec.Command("bash", "build.sh")
|
||||||
cmd.Env = m.buildEnv()
|
cmd.Env = m.BuildEnv()
|
||||||
out, err := cmd.CombinedOutput()
|
out, err := cmd.CombinedOutput()
|
||||||
return string(out), err
|
return string(out), err
|
||||||
}
|
}
|
||||||
@@ -416,7 +416,7 @@ func (m *Manager) StartUnified() error {
|
|||||||
cmd = exec.Command(bin, "--log-level", "info")
|
cmd = exec.Command(bin, "--log-level", "info")
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.Env = m.buildEnv()
|
cmd.Env = m.BuildEnv()
|
||||||
cmd.Stdout = logFile
|
cmd.Stdout = logFile
|
||||||
cmd.Stderr = logFile
|
cmd.Stderr = logFile
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
||||||
@@ -654,8 +654,8 @@ func (m *Manager) removePID(id string) {
|
|||||||
_ = os.Remove(m.pidPath(id))
|
_ = os.Remove(m.pidPath(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildEnv returns the environment for child processes: current env + .env file vars.
|
// BuildEnv returns the environment for child processes: current env + .env file vars.
|
||||||
func (m *Manager) buildEnv() []string {
|
func (m *Manager) BuildEnv() []string {
|
||||||
env := os.Environ()
|
env := os.Environ()
|
||||||
if m.envFile == "" {
|
if m.envFile == "" {
|
||||||
return env
|
return env
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ package tui
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -54,6 +55,9 @@ func (a *Adapter) RunIntent(intent puretui.Intent) tea.Cmd {
|
|||||||
case puretui.IntentRebuildRestart:
|
case puretui.IntentRebuildRestart:
|
||||||
return a.rebuildRestart()
|
return a.rebuildRestart()
|
||||||
|
|
||||||
|
case puretui.IntentRunTests:
|
||||||
|
return a.runTests()
|
||||||
|
|
||||||
case puretui.IntentTick:
|
case puretui.IntentTick:
|
||||||
return a.tick()
|
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 {
|
func (a *Adapter) tick() tea.Cmd {
|
||||||
return tea.Tick(3*time.Second, func(time.Time) tea.Msg {
|
return tea.Tick(3*time.Second, func(time.Time) tea.Msg {
|
||||||
return puretui.MsgTick{}
|
return puretui.MsgTick{}
|
||||||
|
|||||||
Reference in New Issue
Block a user