Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/sdk-go/browser.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ type Browser struct {
cdp *cdpClient
workerAPIKey *string
workerBrowser *BrowserSessionMetadata
extensionDir string
ownsSource bool
closeSource func(context.Context) error
cleanup func() error
Expand Down
73 changes: 10 additions & 63 deletions packages/sdk-go/browser_accessor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,80 +2,27 @@ package stagehand

import (
"context"
"errors"
"net/http"
"reflect"
"testing"
"time"
)

func TestBrowserRequiresInitialization(t *testing.T) {
t.Parallel()

client := New(StagehandClientInitParams{})
if _, err := client.Browser(); !errors.Is(err, ErrNotInitialized) {
t.Fatalf("Browser() error = %v, want ErrNotInitialized", err)
}
}

func TestBrowserReturnsDetachedResolvedSourceSnapshot(t *testing.T) {
func TestBrowserReturnsExactFactoryHandle(t *testing.T) {
t.Parallel()

rpc := &recordingProtocolClient{responses: map[string]any{
"stagehand.init": StagehandInitResult{Initialized: true},
}}
client := newStagehandWithClient(StagehandClientInitParams{}, rpc)
client.adapters.resolveBrowserSource = func(
context.Context,
StagehandClientInitParams,
) (resolvedBrowserSource, error) {
return resolvedBrowserSource{
cdpURL: "wss://connect.example/session",
cdpHeaders: http.Header{
"Authorization": []string{"Bearer secret"},
},
browserbaseSessionID: "session-123",
extensionDir: "/private/sdk-extension",
preloadedExtension: true,
connectTimeout: 15 * time.Second,
keepAlive: true,
close: func(context.Context) error { return nil },
cleanup: func() error { return nil },
}, nil
}

if err := client.Init(context.Background()); err != nil {
t.Fatalf("Init() error = %v", err)
}
browser, err := client.Browser()
if err != nil {
t.Fatalf("Browser() error = %v", err)
}
if browser.CDPURL != "wss://connect.example/session" ||
browser.CDPHeaders["Authorization"] != "Bearer secret" ||
browser.BrowserbaseSessionID != "session-123" ||
!browser.PreloadedExtension ||
browser.ConnectTimeout != 15*time.Second ||
!browser.KeepAlive {
t.Fatalf("Browser() = %#v", browser)
}

browser.CDPHeaders["Authorization"] = "mutated"
next, err := client.Browser()
browser := &Browser{}
client, err := newStagehandWithClient(CreateOptions{Browser: browser}, rpc)
if err != nil {
t.Fatalf("second Browser() error = %v", err)
t.Fatalf("Create() error = %v", err)
}
if next.CDPHeaders["Authorization"] != "Bearer secret" {
t.Fatalf("Browser() exposed mutable SDK state: %#v", next.CDPHeaders)
if client.Browser() != browser {
t.Fatalf("Browser() = %p, want %p", client.Browser(), browser)
}

publicType := reflect.TypeOf(browser)
for _, forbiddenField := range []string{"ExtensionDir", "Cleanup", "Close"} {
if _, exposed := publicType.FieldByName(forbiddenField); exposed {
t.Errorf("ResolvedBrowserSource exposes forbidden field %s", forbiddenField)
}
if err := client.Close(context.Background()); err != nil {
t.Fatalf("Close() error = %v", err)
}
if _, exposed := publicType.MethodByName("Close"); exposed {
t.Error("ResolvedBrowserSource exposes lifecycle method Close")
if client.Browser() != browser {
t.Fatal("Close() changed the attached Browser handle")
}
}
37 changes: 26 additions & 11 deletions packages/sdk-go/browser_factories.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"fmt"
"strings"
"time"

"github.com/browserbase/stagehand/packages/sdk-go/internal/extensionassets"
)

// LocalBrowserLaunchOptions configures a Chromium process launched by the SDK.
Expand Down Expand Up @@ -63,7 +65,7 @@ type BrowserbaseConnectOptions struct {
}

type browserbaseFactoryClient interface {
createSession(context.Context, BrowserbaseClientBrowserSource) (resolvedBrowserSource, error)
createSession(context.Context, BrowserbaseLaunchOptions) (resolvedBrowserSource, error)
connectSession(context.Context, string) (browserbaseSessionConnection, error)
}

Expand Down Expand Up @@ -121,7 +123,7 @@ func launchLocalBrowserWithDependencies(ctx context.Context, options *LocalBrows
if resolvedOptions.AcceptDownloads != nil && *resolvedOptions.AcceptDownloads && resolvedOptions.DownloadsPath == "" {
return nil, errors.New("downloadsPath is required when acceptDownloads is true")
}
extensionDir, cleanup, err := materializeBrowserExtension(dependencies)
extensionDir, cleanup, err := materializeStagehandExtension(dependencies)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -168,7 +170,7 @@ func connectLocalBrowserWithDependencies(ctx context.Context, options LocalBrows
var cleanup func() error
if options.ExtensionID == "" {
var err error
extensionDir, cleanup, err = materializeBrowserExtension(dependencies)
extensionDir, cleanup, err = materializeStagehandExtension(dependencies)
if err != nil {
return nil, err
}
Expand All @@ -190,11 +192,7 @@ func launchBrowserbaseWithDependencies(ctx context.Context, options BrowserbaseL
if err != nil {
return nil, err
}
source, err := client.createSession(ctx, BrowserbaseClientBrowserSource{
BrowserSettings: options.BrowserSettings, ExtensionID: options.ExtensionID,
KeepAlive: options.KeepAlive, Proxies: options.Proxies, Region: options.Region,
Timeout: options.Timeout, UserMetadata: options.UserMetadata,
})
source, err := client.createSession(ctx, options)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -262,8 +260,25 @@ func browserbaseClientForFactory(apiKey string, dependencies browserFactoryDepen
return client, nil
}

func materializeBrowserExtension(dependencies browserFactoryDependencies) (string, func() error, error) {
return materializeStagehandExtension(browserSourceResolverDependencies{materializeExtension: dependencies.materializeExtension})
func materializeStagehandExtension(dependencies browserFactoryDependencies) (string, func() error, error) {
materialize := dependencies.materializeExtension
if materialize == nil {
materialize = extensionassets.Materialize
}
directory, cleanup, err := materialize()
if err != nil {
return "", nil, fmt.Errorf("materialize bundled Stagehand extension: %w", err)
}
if strings.TrimSpace(directory) == "" || cleanup == nil {
if cleanup != nil {
err = cleanup()
}
return "", nil, errors.Join(
errors.New("materialized Stagehand extension is incomplete"),
err,
)
}
return directory, cleanup, nil
}

func connectBrowser(ctx context.Context, options connectBrowserOptions, dependencies browserFactoryDependencies) (*Browser, error) {
Expand Down Expand Up @@ -311,7 +326,7 @@ func connectBrowser(ctx context.Context, options connectBrowserOptions, dependen
return &Browser{
provider: options.provider, origin: options.origin, cdp: cdp,
workerAPIKey: options.workerAPIKey,
workerBrowser: options.workerBrowser, ownsSource: ownsSource,
workerBrowser: options.workerBrowser, extensionDir: options.extensionDir, ownsSource: ownsSource,
closeSource: options.source.close, cleanup: options.source.cleanup,
}, nil
}
Loading
Loading