feat: pantalla de tests en el dashboard TUI
Nueva seccion "Tests" en el menu principal del dashboard que permite ejecutar Go tests, E2E tests (headless y headed), y todos secuencialmente. - ScreenTests con menu de seleccion de tipo de test - TestKind enum para identificar el tipo de test ejecutado - Nuevos intents: IntentRunGoTests, IntentRunE2ETests, IntentRunE2EHeadTests, IntentRunAllTests - LastTestKind en Model para re-ejecucion con "r" - runGoTests, runE2ETests, runAllTests en adapter - "Run Tests" en Server menu reemplazado por navegacion a ScreenTests - Test output muestra tipo de test en titulo y vuelve a ScreenTests con "0" Issue: 0023 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -40,6 +40,7 @@ type MsgRebuildDone struct {
|
||||
|
||||
// MsgTestsDone reports the result of running tests.
|
||||
type MsgTestsDone struct {
|
||||
Kind TestKind // which test suite was executed
|
||||
Passed bool
|
||||
Output []string // lines of test output
|
||||
}
|
||||
|
||||
+28
-2
@@ -11,9 +11,21 @@ const (
|
||||
ScreenAgentActions // actions for a selected agent
|
||||
ScreenLogs // tail log output
|
||||
ScreenServer // server-wide process management
|
||||
ScreenTests // test type selection menu
|
||||
ScreenTestOutput // test run output
|
||||
)
|
||||
|
||||
// TestKind identifies which test suite to run.
|
||||
type TestKind int
|
||||
|
||||
const (
|
||||
TestKindNone TestKind = iota
|
||||
TestKindGo // go test -tags goolm -count=1 ./...
|
||||
TestKindE2E // ./dev-scripts/e2e/run.sh
|
||||
TestKindE2EHead // ./dev-scripts/e2e/run.sh --headed
|
||||
TestKindAll // Go tests + E2E sequential
|
||||
)
|
||||
|
||||
// Model is the complete TUI state — pure data.
|
||||
type Model struct {
|
||||
Screen Screen
|
||||
@@ -33,6 +45,9 @@ type Model struct {
|
||||
LauncherMemory string
|
||||
LauncherCPU string
|
||||
LauncherLogSize string
|
||||
|
||||
// Test state
|
||||
LastTestKind TestKind // which test to re-run with "r"
|
||||
}
|
||||
|
||||
// AgentView is a pre-formatted projection of an agent for display.
|
||||
@@ -62,10 +77,21 @@ func MainMenuOptions() []MenuOption {
|
||||
return []MenuOption{
|
||||
{Label: "Agents", Desc: "Gestionar agentes"},
|
||||
{Label: "Server", Desc: "Gestionar launcher unificado"},
|
||||
{Label: "Tests", Desc: "Ejecutar tests"},
|
||||
{Label: "Quit", Desc: "Salir"},
|
||||
}
|
||||
}
|
||||
|
||||
// TestMenuOptions returns the available test types.
|
||||
func TestMenuOptions() []MenuOption {
|
||||
return []MenuOption{
|
||||
{Label: "Go Tests", Desc: "go test ./..."},
|
||||
{Label: "E2E Tests", Desc: "Playwright headless"},
|
||||
{Label: "E2E Tests (headed)", Desc: "Playwright con browser"},
|
||||
{Label: "All Tests", Desc: "Go + E2E secuencial"},
|
||||
}
|
||||
}
|
||||
|
||||
// ServerMenuOptions returns the available server-wide actions.
|
||||
func ServerMenuOptions(running bool) []MenuOption {
|
||||
if running {
|
||||
@@ -75,13 +101,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"},
|
||||
{Label: "Tests", Desc: "Ir a pantalla de tests"},
|
||||
}
|
||||
}
|
||||
return []MenuOption{
|
||||
{Label: "Start", Desc: "Iniciar el launcher unificado"},
|
||||
{Label: "Rebuild & Restart", Desc: "Build + iniciar"},
|
||||
{Label: "Run Tests", Desc: "Ejecutar todos los tests"},
|
||||
{Label: "Tests", Desc: "Ir a pantalla de tests"},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+101
-7
@@ -23,6 +23,10 @@ const (
|
||||
IntentKillLauncher IntentKind = "kill_launcher"
|
||||
IntentRebuildRestart IntentKind = "rebuild_restart"
|
||||
IntentRunTests IntentKind = "run_tests"
|
||||
IntentRunGoTests IntentKind = "run_go_tests"
|
||||
IntentRunE2ETests IntentKind = "run_e2e_tests"
|
||||
IntentRunE2EHeadTests IntentKind = "run_e2e_head_tests"
|
||||
IntentRunAllTests IntentKind = "run_all_tests"
|
||||
)
|
||||
|
||||
// Intent is pure data describing a side effect to execute.
|
||||
@@ -107,10 +111,12 @@ func Update(model Model, msg interface{}) (Model, []Intent) {
|
||||
model.LogLines = m.Output
|
||||
model.LogScroll = 0
|
||||
model.Cursor = 0
|
||||
model.LastTestKind = m.Kind
|
||||
label := testKindLabel(m.Kind)
|
||||
if m.Passed {
|
||||
model.StatusMsg = "Tests PASSED"
|
||||
model.StatusMsg = label + " PASSED"
|
||||
} else {
|
||||
model.StatusMsg = "Tests FAILED"
|
||||
model.StatusMsg = label + " FAILED"
|
||||
}
|
||||
return model, nil
|
||||
|
||||
@@ -143,6 +149,8 @@ func updateKey(model Model, key KeyMsg) (Model, []Intent) {
|
||||
return updateLogs(model, key)
|
||||
case ScreenServer:
|
||||
return updateServerScreen(model, key)
|
||||
case ScreenTests:
|
||||
return updateTestsScreen(model, key)
|
||||
case ScreenTestOutput:
|
||||
return updateTestOutput(model, key)
|
||||
}
|
||||
@@ -167,6 +175,11 @@ func updateMainScreen(model Model, key KeyMsg) (Model, []Intent) {
|
||||
model.Cursor = 0
|
||||
model.StatusMsg = ""
|
||||
return model, []Intent{{Kind: IntentLoadAgents}}
|
||||
case "Tests":
|
||||
model.Screen = ScreenTests
|
||||
model.Cursor = 0
|
||||
model.StatusMsg = ""
|
||||
return model, nil
|
||||
case "Quit":
|
||||
return model, []Intent{{Kind: IntentQuit}}
|
||||
}
|
||||
@@ -304,9 +317,11 @@ 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 "Tests":
|
||||
model.Screen = ScreenTests
|
||||
model.Cursor = 0
|
||||
model.StatusMsg = ""
|
||||
return model, nil
|
||||
case "Logs":
|
||||
model.Screen = ScreenLogs
|
||||
model.LogLines = nil
|
||||
@@ -321,7 +336,7 @@ func executeServerAction(model Model, action string) (Model, []Intent) {
|
||||
func updateTestOutput(model Model, key KeyMsg) (Model, []Intent) {
|
||||
switch key.Str {
|
||||
case "0":
|
||||
model.Screen = ScreenServer
|
||||
model.Screen = ScreenTests
|
||||
model.Cursor = 0
|
||||
model.LogLines = nil
|
||||
model.LogScroll = 0
|
||||
@@ -332,12 +347,91 @@ func updateTestOutput(model Model, key KeyMsg) (Model, []Intent) {
|
||||
maxScroll := max(0, len(model.LogLines)-visibleLogLines(model))
|
||||
model.LogScroll = min(model.LogScroll+1, maxScroll)
|
||||
case "r":
|
||||
intent := testKindIntent(model.LastTestKind)
|
||||
if intent == "" {
|
||||
intent = IntentRunGoTests
|
||||
}
|
||||
model.StatusMsg = "Running tests..."
|
||||
return model, []Intent{{Kind: IntentRunTests}}
|
||||
model.LogLines = nil
|
||||
model.LogScroll = 0
|
||||
return model, []Intent{{Kind: intent}}
|
||||
}
|
||||
return model, nil
|
||||
}
|
||||
|
||||
func updateTestsScreen(model Model, key KeyMsg) (Model, []Intent) {
|
||||
opts := TestMenuOptions()
|
||||
|
||||
switch key.Str {
|
||||
case "0":
|
||||
model.Screen = ScreenMain
|
||||
model.Cursor = 0
|
||||
model.StatusMsg = ""
|
||||
case "up", "k":
|
||||
model.Cursor = clamp(model.Cursor-1, 0, len(opts)-1)
|
||||
case "down", "j":
|
||||
model.Cursor = clamp(model.Cursor+1, 0, len(opts)-1)
|
||||
case "enter":
|
||||
if model.Cursor < len(opts) {
|
||||
return executeTestAction(model, opts[model.Cursor].Label)
|
||||
}
|
||||
}
|
||||
return model, nil
|
||||
}
|
||||
|
||||
func executeTestAction(model Model, action string) (Model, []Intent) {
|
||||
model.StatusMsg = "Running tests..."
|
||||
model.LogLines = nil
|
||||
model.LogScroll = 0
|
||||
switch action {
|
||||
case "Go Tests":
|
||||
model.LastTestKind = TestKindGo
|
||||
return model, []Intent{{Kind: IntentRunGoTests}}
|
||||
case "E2E Tests":
|
||||
model.LastTestKind = TestKindE2E
|
||||
return model, []Intent{{Kind: IntentRunE2ETests}}
|
||||
case "E2E Tests (headed)":
|
||||
model.LastTestKind = TestKindE2EHead
|
||||
return model, []Intent{{Kind: IntentRunE2EHeadTests}}
|
||||
case "All Tests":
|
||||
model.LastTestKind = TestKindAll
|
||||
return model, []Intent{{Kind: IntentRunAllTests}}
|
||||
}
|
||||
return model, nil
|
||||
}
|
||||
|
||||
// testKindIntent maps a TestKind to its corresponding IntentKind.
|
||||
func testKindIntent(k TestKind) IntentKind {
|
||||
switch k {
|
||||
case TestKindGo:
|
||||
return IntentRunGoTests
|
||||
case TestKindE2E:
|
||||
return IntentRunE2ETests
|
||||
case TestKindE2EHead:
|
||||
return IntentRunE2EHeadTests
|
||||
case TestKindAll:
|
||||
return IntentRunAllTests
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// testKindLabel returns a human-readable label for a TestKind.
|
||||
func testKindLabel(k TestKind) string {
|
||||
switch k {
|
||||
case TestKindGo:
|
||||
return "Go Tests"
|
||||
case TestKindE2E:
|
||||
return "E2E Tests"
|
||||
case TestKindE2EHead:
|
||||
return "E2E Tests (headed)"
|
||||
case TestKindAll:
|
||||
return "All Tests"
|
||||
default:
|
||||
return "Tests"
|
||||
}
|
||||
}
|
||||
|
||||
// ── pure helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
func visibleLogLines(m Model) int {
|
||||
|
||||
+38
-1
@@ -18,6 +18,8 @@ func View(model Model) string {
|
||||
return viewLogs(model)
|
||||
case ScreenServer:
|
||||
return viewServer(model)
|
||||
case ScreenTests:
|
||||
return viewTests(model)
|
||||
case ScreenTestOutput:
|
||||
return viewTestOutput(model)
|
||||
default:
|
||||
@@ -238,10 +240,45 @@ func viewServer(m Model) string {
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func viewTests(m Model) string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString("\n Tests\n")
|
||||
b.WriteString(" " + strings.Repeat("─", 44) + "\n")
|
||||
|
||||
for i, opt := range TestMenuOptions() {
|
||||
cursor := " "
|
||||
if i == m.Cursor {
|
||||
cursor = "> "
|
||||
}
|
||||
b.WriteString(fmt.Sprintf(" %s%-22s %s\n", cursor, opt.Label, opt.Desc))
|
||||
}
|
||||
|
||||
if m.LastTestKind != TestKindNone {
|
||||
b.WriteString(fmt.Sprintf("\n Last run: %s", testKindLabel(m.LastTestKind)))
|
||||
if m.StatusMsg != "" && (strings.HasSuffix(m.StatusMsg, "PASSED") || strings.HasSuffix(m.StatusMsg, "FAILED")) {
|
||||
// Extract result from status
|
||||
if strings.HasSuffix(m.StatusMsg, "PASSED") {
|
||||
b.WriteString(" — PASSED")
|
||||
} else {
|
||||
b.WriteString(" — FAILED")
|
||||
}
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString("\n ↑↓ navegar enter ejecutar 0 volver\n")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func viewTestOutput(m Model) string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString("\n Test Results\n")
|
||||
title := "Test Results"
|
||||
if m.LastTestKind != TestKindNone {
|
||||
title = "Test Results — " + testKindLabel(m.LastTestKind)
|
||||
}
|
||||
b.WriteString("\n " + title + "\n")
|
||||
b.WriteString(" " + strings.Repeat("─", 60) + "\n")
|
||||
|
||||
if m.StatusMsg != "" {
|
||||
|
||||
+78
-5
@@ -59,7 +59,19 @@ func (a *Adapter) RunIntent(intent puretui.Intent) tea.Cmd {
|
||||
return a.rebuildRestart()
|
||||
|
||||
case puretui.IntentRunTests:
|
||||
return a.runTests()
|
||||
return a.runGoTests()
|
||||
|
||||
case puretui.IntentRunGoTests:
|
||||
return a.runGoTests()
|
||||
|
||||
case puretui.IntentRunE2ETests:
|
||||
return a.runE2ETests(false)
|
||||
|
||||
case puretui.IntentRunE2EHeadTests:
|
||||
return a.runE2ETests(true)
|
||||
|
||||
case puretui.IntentRunAllTests:
|
||||
return a.runAllTests()
|
||||
|
||||
case puretui.IntentTick:
|
||||
return a.tick()
|
||||
@@ -236,11 +248,10 @@ func (a *Adapter) loadLogs(id string) tea.Cmd {
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adapter) runTests() tea.Cmd {
|
||||
func (a *Adapter) runGoTests() 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", "./...")
|
||||
@@ -252,9 +263,71 @@ func (a *Adapter) runTests() tea.Cmd {
|
||||
output = "Error: " + err.Error()
|
||||
}
|
||||
lines := strings.Split(output, "\n")
|
||||
passed := err == nil
|
||||
return puretui.MsgTestsDone{Kind: puretui.TestKindGo, Passed: err == nil, Output: lines}
|
||||
}
|
||||
}
|
||||
|
||||
return puretui.MsgTestsDone{Passed: passed, Output: lines}
|
||||
func (a *Adapter) runE2ETests(headed bool) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
args := []string{"./dev-scripts/e2e/run.sh"}
|
||||
if headed {
|
||||
args = append(args, "--headed")
|
||||
}
|
||||
cmd := exec.Command("bash", args...)
|
||||
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")
|
||||
kind := puretui.TestKindE2E
|
||||
if headed {
|
||||
kind = puretui.TestKindE2EHead
|
||||
}
|
||||
return puretui.MsgTestsDone{Kind: kind, Passed: err == nil, Output: lines}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adapter) runAllTests() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
var allLines []string
|
||||
|
||||
// Go tests first
|
||||
goBin, err := exec.LookPath("go")
|
||||
if err != nil {
|
||||
goBin = "/usr/local/go/bin/go"
|
||||
}
|
||||
goCmd := exec.Command(goBin, "test", "-tags", "goolm", "-count=1", "./...")
|
||||
goCmd.Env = a.mgr.BuildEnv()
|
||||
goOut, goErr := goCmd.CombinedOutput()
|
||||
|
||||
allLines = append(allLines, "═══ Go Tests ═══")
|
||||
goOutput := strings.TrimSpace(string(goOut))
|
||||
if goOutput == "" && goErr != nil {
|
||||
goOutput = "Error: " + goErr.Error()
|
||||
}
|
||||
allLines = append(allLines, strings.Split(goOutput, "\n")...)
|
||||
|
||||
if goErr != nil {
|
||||
allLines = append(allLines, "", "Go tests FAILED — skipping E2E")
|
||||
return puretui.MsgTestsDone{Kind: puretui.TestKindAll, Passed: false, Output: allLines}
|
||||
}
|
||||
|
||||
// E2E tests
|
||||
allLines = append(allLines, "", "═══ E2E Tests ═══")
|
||||
e2eCmd := exec.Command("bash", "./dev-scripts/e2e/run.sh")
|
||||
e2eCmd.Env = a.mgr.BuildEnv()
|
||||
e2eOut, e2eErr := e2eCmd.CombinedOutput()
|
||||
|
||||
e2eOutput := strings.TrimSpace(string(e2eOut))
|
||||
if e2eOutput == "" && e2eErr != nil {
|
||||
e2eOutput = "Error: " + e2eErr.Error()
|
||||
}
|
||||
allLines = append(allLines, strings.Split(e2eOutput, "\n")...)
|
||||
|
||||
return puretui.MsgTestsDone{Kind: puretui.TestKindAll, Passed: e2eErr == nil, Output: allLines}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user