From 2e35a3faace2c222733dedd53a6e2c7cd1401997 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Fri, 24 Apr 2026 10:07:42 +0200 Subject: [PATCH] test(browser): add Lightpanda integration tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Build-tag gated (//go:build integration), skipped unless LIGHTPANDA_CDP_URL env is set. Covers: - Golden path: open → snapshot → list → close - Multi-tenant isolation: each tenant sees only its own tabs; cross-tenant CloseTab is rejected - Backend() reports lightpanda after Start - Screenshot tool action returns an IsError result mentioning lightpanda + snapshot --- tests/integration/browser_lightpanda_test.go | 156 +++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 tests/integration/browser_lightpanda_test.go diff --git a/tests/integration/browser_lightpanda_test.go b/tests/integration/browser_lightpanda_test.go new file mode 100644 index 00000000..691de8f6 --- /dev/null +++ b/tests/integration/browser_lightpanda_test.go @@ -0,0 +1,156 @@ +//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") + } + + snap, err := m.Snapshot(ctx, tab.TargetID, browser.DefaultSnapshotOptions()) + if err != nil { + t.Fatalf("snapshot: %v", err) + } + if snap.Snapshot == "" { + t.Fatal("expected non-empty AX snapshot") + } + + 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 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)) + } +} + +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) + } +}