Files
browser_mcp/tools_lifecycle_test.go
T
egutierrez 1fae6c1df9 feat(browser_mcp): add browser_list/launch_profile/close lifecycle tools
Three MCP tools to manage the user's Chromium instances by profile, distinct
from browser_launch's isolated automation Chrome:

- browser_list: enumerate running Chromium master processes by scanning
  /proc/*/cmdline (has --user-data-dir, no --type=). Returns pid, profile,
  user_data_dir, cdp_port, has_cdp as a JSON array.
- browser_launch_profile: launch a concrete profile using the REAL binary
  /usr/lib/chromium/chromium (bypassing the /usr/bin/chromium wrapper). No CDP
  by default so Google keeps the session for human profiles; cdp=true adds
  --remote-debugging-port + --remote-allow-origins=*. Detects DISPLAY/XAUTHORITY
  from the XFCE session and launches decoupled via setsid.
- browser_close: locate a master by profile/cdp_port/pid, SIGTERM with a 10s
  wait, then SIGKILL as a last resort.

Per-profile instances are NOT registered in the connection pool: they are
user-facing and survive the MCP dying; cleanup is explicit via browser_close.

Unit tests for cmdline master detection, flag parsing, and close-target
matching. Bumps version 0.6.0 -> 0.7.0 (42 -> 45 tools).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 18:23:45 +02:00

153 lines
4.6 KiB
Go

package main
import "testing"
// TestParseChromiumMaster cubre la deteccion de master: solo procesos chromium con
// --user-data-dir y SIN --type= cuentan; el resto (wrapper sin udd, children con
// --type=, no-chromium) se descartan. Tambien valida que profile/cdp_port se
// extraen y que has_cdp refleja la presencia del flag.
func TestParseChromiumMaster(t *testing.T) {
cases := []struct {
name string
args []string
wantOK bool
wantProfile string
wantUDD string
wantPort string
wantHasCDP bool
}{
{
name: "master con CDP y profile",
args: []string{
"/usr/lib/chromium/chromium",
"--user-data-dir=/home/u/.config/chromium-cdp",
"--profile-directory=Personal",
"--remote-debugging-port=9222",
"--remote-allow-origins=*",
},
wantOK: true,
wantProfile: "Personal",
wantUDD: "/home/u/.config/chromium-cdp",
wantPort: "9222",
wantHasCDP: true,
},
{
name: "master humano sin CDP",
args: []string{
"/usr/lib/chromium/chromium",
"--user-data-dir=/home/u/.config/chromium-cdp",
"--profile-directory=Default",
},
wantOK: true,
wantProfile: "Default",
wantUDD: "/home/u/.config/chromium-cdp",
wantPort: "",
wantHasCDP: false,
},
{
name: "child renderer con --type= se descarta",
args: []string{
"/usr/lib/chromium/chromium",
"--type=renderer",
"--user-data-dir=/home/u/.config/chromium-cdp",
},
wantOK: false,
},
{
name: "child gpu-process con --type= se descarta",
args: []string{
"/usr/lib/chromium/chromium",
"--type=gpu-process",
"--user-data-dir=/home/u/.config/chromium-cdp",
"--profile-directory=Personal",
},
wantOK: false,
},
{
name: "chromium sin --user-data-dir se descarta",
args: []string{"/usr/lib/chromium/chromium", "--profile-directory=Personal"},
wantOK: false,
},
{
name: "proceso no-chromium se descarta",
args: []string{"/usr/bin/firefox", "--user-data-dir=/x"},
wantOK: false,
},
{
name: "argv vacio se descarta",
args: nil,
wantOK: false,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
m, ok := parseChromiumMaster(1234, tc.args)
if ok != tc.wantOK {
t.Fatalf("ok = %v, want %v", ok, tc.wantOK)
}
if !ok {
return
}
if m.PID != 1234 {
t.Errorf("PID = %d, want 1234", m.PID)
}
if m.Profile != tc.wantProfile {
t.Errorf("Profile = %q, want %q", m.Profile, tc.wantProfile)
}
if m.UserDataDir != tc.wantUDD {
t.Errorf("UserDataDir = %q, want %q", m.UserDataDir, tc.wantUDD)
}
if m.CDPPort != tc.wantPort {
t.Errorf("CDPPort = %q, want %q", m.CDPPort, tc.wantPort)
}
if m.HasCDP != tc.wantHasCDP {
t.Errorf("HasCDP = %v, want %v", m.HasCDP, tc.wantHasCDP)
}
})
}
}
// TestFlagValue valida el parseo exacto de "--name=value".
func TestFlagValue(t *testing.T) {
args := []string{"--user-data-dir=/x/y", "--profile-directory=Work", "--flag-without-value"}
if v, ok := flagValue(args, "user-data-dir"); !ok || v != "/x/y" {
t.Errorf("user-data-dir = (%q,%v), want (/x/y,true)", v, ok)
}
if v, ok := flagValue(args, "profile-directory"); !ok || v != "Work" {
t.Errorf("profile-directory = (%q,%v), want (Work,true)", v, ok)
}
if _, ok := flagValue(args, "remote-debugging-port"); ok {
t.Errorf("remote-debugging-port should be absent")
}
// Prefijo no debe hacer match parcial: "user-data" != "user-data-dir".
if _, ok := flagValue(args, "user-data"); ok {
t.Errorf("partial prefix user-data should NOT match user-data-dir")
}
}
// TestMatchMaster valida la prioridad pid > cdp_port > profile y el no-match.
func TestMatchMaster(t *testing.T) {
masters := []chromiumMaster{
{PID: 100, Profile: "Personal", CDPPort: ""},
{PID: 200, Profile: "Work", CDPPort: "9222"},
{PID: 300, Profile: "Personal", CDPPort: "9333"},
}
if m, ok := matchMaster(masters, browserCloseArgs{PID: 200}); !ok || m.PID != 200 {
t.Errorf("by pid: got (%d,%v), want (200,true)", m.PID, ok)
}
if m, ok := matchMaster(masters, browserCloseArgs{CDPPort: 9333}); !ok || m.PID != 300 {
t.Errorf("by cdp_port: got (%d,%v), want (300,true)", m.PID, ok)
}
// profile "Personal" tiene dos: gana el primero (PID 100).
if m, ok := matchMaster(masters, browserCloseArgs{Profile: "Personal"}); !ok || m.PID != 100 {
t.Errorf("by profile: got (%d,%v), want (100,true)", m.PID, ok)
}
if _, ok := matchMaster(masters, browserCloseArgs{PID: 999}); ok {
t.Errorf("unknown pid should not match")
}
if _, ok := matchMaster(masters, browserCloseArgs{Profile: "Nope"}); ok {
t.Errorf("unknown profile should not match")
}
}