mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-11 03:13:24 +00:00
Merge pull request #1028 from krichprollsch/lightpanda
Add Lightpanda as alternative to Chrome for the browser automation tool
This commit is contained in:
11 files changed
+665
-56
No files matched your search
@@ -0,0 +1,192 @@
|
||||
//go:build integration
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/pkg/browser"
|
||||
)
|
||||
|
||||
// These tests require a running Lightpanda CDP sidecar. Set
|
||||
// LIGHTPANDA_CDP_URL=ws://localhost:9222 (or wherever it lives) before running:
|
||||
//
|
||||
// docker run -d --rm -p 9222:9222 lightpanda/browser:latest \
|
||||
// serve --host 0.0.0.0 --port 9222
|
||||
// LIGHTPANDA_CDP_URL=ws://localhost:9222 \
|
||||
// go test -tags integration -run Lightpanda ./tests/integration/
|
||||
|
||||
func newLightpandaManager(t *testing.T) *browser.Manager {
|
||||
t.Helper()
|
||||
url := mustEnv(t, "LIGHTPANDA_CDP_URL")
|
||||
|
||||
m := browser.New(
|
||||
browser.WithRemoteURL(url),
|
||||
browser.WithBackend(browser.BackendLightpanda),
|
||||
browser.WithActionTimeout(15*time.Second),
|
||||
browser.WithIdleTimeout(0), // disable reaper for predictable tests
|
||||
browser.WithMaxPages(10),
|
||||
)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
if err := m.Start(ctx); err != nil {
|
||||
t.Fatalf("manager start: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_ = m.Stop(context.Background())
|
||||
})
|
||||
return m
|
||||
}
|
||||
|
||||
func mustEnv(t *testing.T, key string) string {
|
||||
t.Helper()
|
||||
v := strings.TrimSpace(os.Getenv(key))
|
||||
if v == "" {
|
||||
t.Skipf("%s not set; skipping lightpanda integration test", key)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func TestLightpanda_SingleTenant_Golden(t *testing.T) {
|
||||
m := newLightpandaManager(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
tab, err := m.OpenTab(ctx, "https://example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("open tab: %v", err)
|
||||
}
|
||||
if tab.TargetID == "" {
|
||||
t.Fatal("expected non-empty TargetID")
|
||||
}
|
||||
if tab.URL != "https://example.com" {
|
||||
t.Errorf("expected URL https://example.com, got %q", tab.URL)
|
||||
}
|
||||
if tab.Title == "" {
|
||||
t.Errorf("expected non-empty Title (page.Info() should populate from initial post-open call)")
|
||||
}
|
||||
|
||||
tabs, err := m.ListTabs(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("list tabs: %v", err)
|
||||
}
|
||||
if len(tabs) != 1 {
|
||||
t.Fatalf("expected 1 tab, got %d", len(tabs))
|
||||
}
|
||||
if tabs[0].Title != tab.Title {
|
||||
t.Errorf("ListTabs should return cached title %q, got %q", tab.Title, tabs[0].Title)
|
||||
}
|
||||
|
||||
if err := m.CloseTab(ctx, tab.TargetID); err != nil {
|
||||
t.Fatalf("close tab: %v", err)
|
||||
}
|
||||
tabs, _ = m.ListTabs(ctx)
|
||||
if len(tabs) != 0 {
|
||||
t.Fatalf("expected 0 tabs after close, got %d", len(tabs))
|
||||
}
|
||||
}
|
||||
|
||||
// TestLightpanda_Snapshot_AndEval covers the agent's primary "see the page"
|
||||
// workflow: AX-tree snapshot for structure + Eval (function form) for JS
|
||||
// access. AX-tree decoding required Lightpanda upstream fix
|
||||
// lightpanda-io/browser#2232.
|
||||
func TestLightpanda_Snapshot_AndEval(t *testing.T) {
|
||||
m := newLightpandaManager(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
tab, err := m.OpenTab(ctx, "https://example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("open tab: %v", err)
|
||||
}
|
||||
|
||||
snap, err := m.Snapshot(ctx, tab.TargetID, browser.DefaultSnapshotOptions())
|
||||
if err != nil {
|
||||
t.Fatalf("Snapshot: %v", err)
|
||||
}
|
||||
if snap.Snapshot == "" {
|
||||
t.Error("expected non-empty AX snapshot text")
|
||||
}
|
||||
if snap.Stats.Refs == 0 {
|
||||
t.Error("expected at least one ref in snapshot")
|
||||
}
|
||||
|
||||
v, err := m.Evaluate(ctx, tab.TargetID, "() => document.title")
|
||||
if err != nil {
|
||||
t.Errorf("Evaluate(function form): %v", err)
|
||||
}
|
||||
if v == "" {
|
||||
t.Errorf("Evaluate(() => document.title) returned empty value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLightpanda_MultiTenant_Isolation(t *testing.T) {
|
||||
m := newLightpandaManager(t)
|
||||
|
||||
tenantA := "11111111-1111-1111-1111-111111111111"
|
||||
tenantB := "22222222-2222-2222-2222-222222222222"
|
||||
base := context.Background()
|
||||
ctxA := browser.WithTenantID(base, tenantA)
|
||||
ctxB := browser.WithTenantID(base, tenantB)
|
||||
|
||||
tabA, err := m.OpenTab(ctxA, "https://example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("tenant A open: %v", err)
|
||||
}
|
||||
tabB, err := m.OpenTab(ctxB, "https://example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("tenant B open: %v", err)
|
||||
}
|
||||
|
||||
listA, _ := m.ListTabs(ctxA)
|
||||
if len(listA) != 1 || listA[0].TargetID != tabA.TargetID {
|
||||
t.Errorf("tenant A should see only its own tab; got %v", listA)
|
||||
}
|
||||
listB, _ := m.ListTabs(ctxB)
|
||||
if len(listB) != 1 || listB[0].TargetID != tabB.TargetID {
|
||||
t.Errorf("tenant B should see only its own tab; got %v", listB)
|
||||
}
|
||||
|
||||
// Tenant A must not be able to close tenant B's tab.
|
||||
if err := m.CloseTab(ctxA, tabB.TargetID); err == nil {
|
||||
t.Error("expected error when tenant A closes tenant B's tab; got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLightpanda_Backend_ReportedCorrectly(t *testing.T) {
|
||||
m := newLightpandaManager(t)
|
||||
if got := m.Backend(); got != browser.BackendLightpanda {
|
||||
t.Errorf("expected backend %q, got %q", browser.BackendLightpanda, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLightpanda_Screenshot_BlockedByToolGuard(t *testing.T) {
|
||||
m := newLightpandaManager(t)
|
||||
tool := browser.NewBrowserTool(m)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
tab, err := m.OpenTab(ctx, "https://example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("open tab: %v", err)
|
||||
}
|
||||
|
||||
res := tool.Execute(ctx, map[string]any{
|
||||
"action": "screenshot",
|
||||
"targetId": tab.TargetID,
|
||||
})
|
||||
if res == nil || !res.IsError {
|
||||
t.Fatal("expected screenshot to return an error result on lightpanda")
|
||||
}
|
||||
msg := strings.ToLower(res.ForLLM)
|
||||
if !strings.Contains(msg, "lightpanda") || !strings.Contains(msg, "snapshot") {
|
||||
t.Errorf("expected error to mention lightpanda + snapshot; got %q", res.ForLLM)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user