package main import ( "context" "strings" "testing" "github.com/mark3labs/mcp-go/mcp" ) // resultText concatena el texto de un CallToolResult para asserts. func resultText(r *mcp.CallToolResult) string { var sb strings.Builder for _, c := range r.Content { if tc, ok := c.(mcp.TextContent); ok { sb.WriteString(tc.Text) } } return sb.String() } // TestPoolPIDLifecycle verifica set/get/clear/count del registro de PIDs sin // tocar Chrome real. func TestPoolPIDLifecycle(t *testing.T) { p := newConnPool() if n := p.launchedCount(); n != 0 { t.Fatalf("launchedCount inicial = %d, want 0", n) } p.setPID(9333, 4242) if pid, ok := p.getPID(9333); !ok || pid != 4242 { t.Fatalf("getPID(9333) = (%d,%v), want (4242,true)", pid, ok) } if n := p.launchedCount(); n != 1 { t.Fatalf("launchedCount tras setPID = %d, want 1", n) } p.clearPID(9333) if _, ok := p.getPID(9333); ok { t.Fatalf("getPID(9333) sigue presente tras clearPID") } if n := p.launchedCount(); n != 0 { t.Fatalf("launchedCount tras clearPID = %d, want 0", n) } } // TestInstanceCapRejectsWithoutLaunching verifica el tope duro: con // maxLaunchedChromes PIDs ya registrados, browser_launch en un puerto nuevo // devuelve error de tool y NO intenta lanzar Chrome (el cap se evalúa antes de // ChromeLaunch, por eso este test no necesita Chrome real). Cubre el edge // "superar el tope → error claro". func TestInstanceCapRejectsWithoutLaunching(t *testing.T) { p := newConnPool() for i := 0; i < maxLaunchedChromes; i++ { p.setPID(9500+i, 100000+i) // PIDs ficticios: nunca se matan en este test } d := &deps{pool: p} res, err := d.handleLaunch(context.Background(), mcp.CallToolRequest{}, launchArgs{Port: 9600}) if err != nil { t.Fatalf("handleLaunch err = %v", err) } if !res.IsError { t.Fatalf("esperaba IsError=true por cap, got text=%q", resultText(res)) } if txt := resultText(res); !strings.Contains(txt, "cap") { t.Fatalf("mensaje no menciona el cap: %q", txt) } // El puerto nuevo no debe haberse registrado. if _, ok := p.getPID(9600); ok { t.Fatalf("el puerto rechazado por cap no debe registrarse") } } // TestLaunchReusesRegisteredPort verifica idempotencia: si el MCP ya lanzó un // Chrome en el puerto (PID registrado), un segundo browser_launch lo reusa sin // lanzar otro proceso. No necesita Chrome real (el reuse corta antes de // ChromeLaunch). Cubre el edge "dos browser_launch al mismo puerto no duplica". func TestLaunchReusesRegisteredPort(t *testing.T) { p := newConnPool() p.setPID(9333, 777777) d := &deps{pool: p} res, err := d.handleLaunch(context.Background(), mcp.CallToolRequest{}, launchArgs{Port: 9333}) if err != nil { t.Fatalf("handleLaunch err = %v", err) } if res.IsError { t.Fatalf("no esperaba error, got %q", resultText(res)) } if txt := resultText(res); !strings.Contains(txt, "reused pid=777777") { t.Fatalf("esperaba reuse del pid registrado, got %q", txt) } if n := p.launchedCount(); n != 1 { t.Fatalf("launchedCount = %d, want 1 (no debe duplicar)", n) } } // TestDropClearsMapsNoPID verifica que drop sobre un puerto sin conn ni pid no // panica y deja los mapas limpios (no mata nada — caso del navegador externo // del que solo se soltó el WebSocket). func TestDropClearsMapsNoPID(t *testing.T) { p := newConnPool() p.drop(9222) // puerto externo, sin conn ni pid registrado: no-op seguro if n := p.launchedCount(); n != 0 { t.Fatalf("launchedCount = %d, want 0", n) } }