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") } } // TestParseCmdline cubre el parsing de /proc//cmdline en sus dos formatos: // el canonico separado por NUL y el colapsado por espacios que produce Chromium // al reescribir su titulo de proceso in-place. El segundo caso es el que rompia // browser_list (los flags quedaban dentro de un unico argv[0] gigante). func TestParseCmdline(t *testing.T) { // Caso canonico: argv separado por NUL (proceso normal). nul := []byte("/usr/lib/chromium/chromium\x00--user-data-dir=/tmp/x\x00--remote-debugging-port=9333\x00") got := parseCmdline(nul) want := []string{"/usr/lib/chromium/chromium", "--user-data-dir=/tmp/x", "--remote-debugging-port=9333"} if len(got) != len(want) { t.Fatalf("NUL: got %v, want %v", got, want) } for i := range want { if got[i] != want[i] { t.Errorf("NUL[%d]: got %q, want %q", i, got[i], want[i]) } } // Caso Chromium: cmdline colapsado a una sola cadena separada por espacios. collapsed := []byte("/usr/lib/chromium/chromium --remote-debugging-port=9333 --user-data-dir=/tmp/browser_mcp_userdata --no-first-run https://www.alsa.es/") args := parseCmdline(collapsed) if len(args) == 1 { t.Fatalf("space-collapsed: parse devolvio un unico elemento gigante: %q", args[0]) } if args[0] != "/usr/lib/chromium/chromium" { t.Errorf("space-collapsed argv[0]: got %q, want chromium binary", args[0]) } // El master debe detectarse a partir del cmdline colapsado (regresion de browser_list). m, ok := parseChromiumMaster(18148, args) if !ok { t.Fatalf("space-collapsed: parseChromiumMaster no detecto el master") } if m.UserDataDir != "/tmp/browser_mcp_userdata" { t.Errorf("space-collapsed udd: got %q, want /tmp/browser_mcp_userdata", m.UserDataDir) } if m.CDPPort != "9333" || !m.HasCDP { t.Errorf("space-collapsed cdp: got port=%q hasCDP=%v, want 9333/true", m.CDPPort, m.HasCDP) } if parseCmdline([]byte("")) != nil || parseCmdline([]byte("\x00\x00")) != nil { t.Errorf("cmdline vacio debe devolver nil") } } // TestIsHeadless valida la deteccion de modo headless por el flag de lanzamiento: // --headless, --headless=new y --headless=old cuentan; su ausencia es headed. func TestIsHeadless(t *testing.T) { cases := []struct { name string args []string want bool }{ {"sin flag (headed)", []string{"/usr/lib/chromium/chromium", "--user-data-dir=/tmp/x"}, false}, {"--headless legacy", []string{"/usr/lib/chromium/chromium", "--headless", "--user-data-dir=/tmp/x"}, true}, {"--headless=new", []string{"/usr/lib/chromium/chromium", "--headless=new"}, true}, {"--headless=old", []string{"/usr/lib/chromium/chromium", "--headless=old"}, true}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { if got := isHeadless(c.args); got != c.want { t.Errorf("isHeadless(%v) = %v, want %v", c.args, got, c.want) } }) } // El master headed real (cmdline colapsado por espacios) debe reportar headless=false. headed := parseCmdline([]byte("/usr/lib/chromium/chromium --remote-debugging-port=9333 --user-data-dir=/tmp/browser_mcp_userdata")) if m, ok := parseChromiumMaster(1, headed); !ok || m.Headless { t.Errorf("master headed: ok=%v headless=%v, want ok=true headless=false", ok, m.Headless) } }