Skip to content

Commit a1171a1

Browse files
committed
test(e2e): add agent-browser CDP proxy connectivity tests
Add comprehensive e2e tests verifying that agent-browser can connect to Chrome through the CDP proxy on port 9222. Tests validate: - /json endpoint returns targets with URLs rewritten to proxy port - /json/list endpoint works correctly with URL rewriting - /json/version endpoint continues to work - agent-browser works with various --cdp argument formats: - port only (9222) - http URL (http://127.0.0.1:9222) - localhost:port - 127.0.0.1:port - agent-browser snapshot and navigation commands work via proxy
1 parent c62b34c commit a1171a1

File tree

1 file changed

+231
-0
lines changed

1 file changed

+231
-0
lines changed
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
package e2e
2+
3+
import (
4+
"context"
5+
"encoding/base64"
6+
"net/http"
7+
"os/exec"
8+
"strings"
9+
"testing"
10+
"time"
11+
12+
instanceoapi "github.com/onkernel/kernel-images/server/lib/oapi"
13+
"github.com/stretchr/testify/require"
14+
)
15+
16+
// TestAgentBrowserCDPProxy tests that agent-browser can connect to Chrome via the CDP proxy on port 9222.
17+
// This validates the /json and /json/list endpoints that the proxy exposes for target discovery,
18+
// which is required for tools like agent-browser and Playwright's connectOverCDP.
19+
func TestAgentBrowserCDPProxy(t *testing.T) {
20+
t.Parallel()
21+
22+
if _, err := exec.LookPath("docker"); err != nil {
23+
t.Skipf("docker not available: %v", err)
24+
}
25+
26+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
27+
defer cancel()
28+
29+
c := NewTestContainer(t, headlessImage)
30+
require.NoError(t, c.Start(ctx, ContainerConfig{}), "failed to start container")
31+
defer c.Stop(ctx)
32+
33+
require.NoError(t, c.WaitReady(ctx), "api not ready")
34+
require.NoError(t, c.WaitDevTools(ctx), "devtools not ready")
35+
36+
client, err := c.APIClient()
37+
require.NoError(t, err)
38+
39+
// Install agent-browser globally inside the container
40+
t.Log("Installing agent-browser...")
41+
timeoutSec := 120 // npm install can take a while
42+
installResult := execCommandWithTimeout(t, ctx, client, "npm", []string{"install", "-g", "agent-browser"}, &timeoutSec)
43+
require.Zero(t, installResult.exitCode, "failed to install agent-browser: %s", installResult.output)
44+
t.Log("agent-browser installed successfully")
45+
46+
// First test the /json endpoints via curl to verify the proxy is working correctly
47+
// before we test agent-browser
48+
49+
// Test that /json endpoint returns proper target list with rewritten URLs
50+
t.Run("json endpoint returns targets with rewritten URLs", func(t *testing.T) {
51+
t.Log("Testing /json endpoint via curl")
52+
53+
result := execCommand(t, ctx, client, "curl", []string{"-s", "http://127.0.0.1:9222/json"})
54+
require.Zero(t, result.exitCode, "curl /json failed: %s", result.output)
55+
56+
// The response should be a JSON array containing targets
57+
require.True(t, strings.HasPrefix(strings.TrimSpace(result.output), "["),
58+
"expected JSON array from /json, got: %s", result.output)
59+
60+
// The URLs should point to the proxy (port 9222), not Chrome directly (port 9223)
61+
require.Contains(t, result.output, "9222",
62+
"expected target URLs to be rewritten to proxy port 9222, got: %s", result.output)
63+
require.NotContains(t, result.output, "9223",
64+
"target URLs should not contain Chrome port 9223, got: %s", result.output)
65+
})
66+
67+
// Test that /json/list endpoint also works
68+
t.Run("json/list endpoint returns targets with rewritten URLs", func(t *testing.T) {
69+
t.Log("Testing /json/list endpoint via curl")
70+
71+
result := execCommand(t, ctx, client, "curl", []string{"-s", "http://127.0.0.1:9222/json/list"})
72+
require.Zero(t, result.exitCode, "curl /json/list failed: %s", result.output)
73+
74+
// The response should be a JSON array containing targets
75+
require.True(t, strings.HasPrefix(strings.TrimSpace(result.output), "["),
76+
"expected JSON array from /json/list, got: %s", result.output)
77+
78+
// The URLs should point to the proxy (port 9222), not Chrome directly (port 9223)
79+
require.Contains(t, result.output, "9222",
80+
"expected target URLs to be rewritten to proxy port 9222, got: %s", result.output)
81+
require.NotContains(t, result.output, "9223",
82+
"target URLs should not contain Chrome port 9223, got: %s", result.output)
83+
})
84+
85+
// Test that /json/version endpoint works (this was already there)
86+
t.Run("json/version endpoint works", func(t *testing.T) {
87+
t.Log("Testing /json/version endpoint via curl")
88+
89+
result := execCommand(t, ctx, client, "curl", []string{"-s", "http://127.0.0.1:9222/json/version"})
90+
require.Zero(t, result.exitCode, "curl /json/version failed: %s", result.output)
91+
92+
// The response should be a JSON object with browser info
93+
require.True(t, strings.HasPrefix(strings.TrimSpace(result.output), "{"),
94+
"expected JSON object from /json/version, got: %s", result.output)
95+
96+
// Should contain webSocketDebuggerUrl pointing to proxy
97+
require.Contains(t, result.output, "webSocketDebuggerUrl",
98+
"expected webSocketDebuggerUrl in response, got: %s", result.output)
99+
require.Contains(t, result.output, "9222",
100+
"expected webSocketDebuggerUrl to point to proxy port 9222, got: %s", result.output)
101+
})
102+
103+
// Now test agent-browser with different CDP connection variations
104+
// Each test connects to the browser, navigates, and gets the URL to verify connectivity
105+
106+
testCases := []struct {
107+
name string
108+
cdpArg string
109+
}{
110+
{
111+
name: "port only (9222)",
112+
cdpArg: "9222",
113+
},
114+
{
115+
name: "http URL",
116+
cdpArg: "http://127.0.0.1:9222",
117+
},
118+
{
119+
name: "localhost:port",
120+
cdpArg: "localhost:9222",
121+
},
122+
{
123+
name: "127.0.0.1:port",
124+
cdpArg: "127.0.0.1:9222",
125+
},
126+
}
127+
128+
for _, tc := range testCases {
129+
tc := tc // capture range variable
130+
t.Run("agent-browser connect "+tc.name, func(t *testing.T) {
131+
t.Logf("Testing agent-browser with --cdp %s", tc.cdpArg)
132+
133+
// Navigate to example.com - this is the key test that verifies:
134+
// 1. agent-browser can connect to the CDP proxy on port 9222
135+
// 2. The proxy's /json endpoint works for target discovery
136+
// 3. The WebSocket connection through the proxy works
137+
navResult := execCommand(t, ctx, client, "agent-browser", []string{"--cdp", tc.cdpArg, "open", "https://example.com"})
138+
t.Logf("Navigate result: exit=%d, output=%s", navResult.exitCode, navResult.output)
139+
require.Zero(t, navResult.exitCode, "agent-browser --cdp %s open failed: %s", tc.cdpArg, navResult.output)
140+
141+
// Get the current URL to verify we're connected and navigation worked
142+
urlResult := execCommand(t, ctx, client, "agent-browser", []string{"--cdp", tc.cdpArg, "get", "url", "--json"})
143+
t.Logf("Get URL result: exit=%d, output=%s", urlResult.exitCode, urlResult.output)
144+
require.Zero(t, urlResult.exitCode, "agent-browser --cdp %s get url failed: %s", tc.cdpArg, urlResult.output)
145+
146+
// Verify we got a valid response containing example.com
147+
require.Contains(t, urlResult.output, "example.com",
148+
"expected URL to contain example.com, got: %s", urlResult.output)
149+
})
150+
}
151+
152+
// Test agent-browser snapshot command which uses /json for target discovery
153+
t.Run("agent-browser snapshot via proxy", func(t *testing.T) {
154+
t.Log("Testing agent-browser snapshot via CDP proxy")
155+
156+
// Get snapshot - this exercises the /json endpoint to discover targets
157+
result := execCommand(t, ctx, client, "agent-browser", []string{"--cdp", "9222", "snapshot", "-i", "--json"})
158+
t.Logf("Snapshot exit code: %d, Output length: %d", result.exitCode, len(result.output))
159+
160+
require.Zero(t, result.exitCode, "agent-browser snapshot failed: %s", result.output)
161+
// Verify we got a valid snapshot response
162+
require.True(t, strings.Contains(result.output, "success") || strings.Contains(result.output, "snapshot") || strings.Contains(result.output, "data"),
163+
"expected valid snapshot response, got: %s", result.output)
164+
})
165+
166+
// Test agent-browser get title to further verify connectivity
167+
t.Run("agent-browser get title via proxy", func(t *testing.T) {
168+
t.Log("Testing agent-browser get title via CDP proxy")
169+
170+
result := execCommand(t, ctx, client, "agent-browser", []string{"--cdp", "9222", "get", "title", "--json"})
171+
t.Logf("Get title result: exit=%d, output=%s", result.exitCode, result.output)
172+
173+
require.Zero(t, result.exitCode, "agent-browser get title failed: %s", result.output)
174+
// Should contain "Example Domain" from previous navigation
175+
require.Contains(t, result.output, "Example",
176+
"expected title to contain 'Example', got: %s", result.output)
177+
})
178+
179+
t.Log("All agent-browser CDP proxy tests passed")
180+
}
181+
182+
// execResult holds the result of a command execution
183+
type execResult struct {
184+
exitCode int
185+
output string
186+
}
187+
188+
// execCommand runs a command via the container's process exec API and returns the result
189+
func execCommand(t *testing.T, ctx context.Context, client *instanceoapi.ClientWithResponses, command string, args []string) execResult {
190+
t.Helper()
191+
return execCommandWithTimeout(t, ctx, client, command, args, nil)
192+
}
193+
194+
// execCommandWithTimeout runs a command with an optional timeout
195+
func execCommandWithTimeout(t *testing.T, ctx context.Context, client *instanceoapi.ClientWithResponses, command string, args []string, timeoutSec *int) execResult {
196+
t.Helper()
197+
198+
req := instanceoapi.ProcessExecJSONRequestBody{
199+
Command: command,
200+
Args: &args,
201+
TimeoutSec: timeoutSec,
202+
}
203+
204+
rsp, err := client.ProcessExecWithResponse(ctx, req)
205+
require.NoError(t, err, "process exec request error for %s", command)
206+
require.Equal(t, http.StatusOK, rsp.StatusCode(), "unexpected status for %s: %s body=%s",
207+
command, rsp.Status(), string(rsp.Body))
208+
require.NotNil(t, rsp.JSON200, "expected JSON200 response for %s", command)
209+
210+
var stdout, stderr string
211+
if rsp.JSON200.StdoutB64 != nil && *rsp.JSON200.StdoutB64 != "" {
212+
if b, decErr := base64.StdEncoding.DecodeString(*rsp.JSON200.StdoutB64); decErr == nil {
213+
stdout = string(b)
214+
}
215+
}
216+
if rsp.JSON200.StderrB64 != nil && *rsp.JSON200.StderrB64 != "" {
217+
if b, decErr := base64.StdEncoding.DecodeString(*rsp.JSON200.StderrB64); decErr == nil {
218+
stderr = string(b)
219+
}
220+
}
221+
222+
exitCode := 0
223+
if rsp.JSON200.ExitCode != nil {
224+
exitCode = *rsp.JSON200.ExitCode
225+
}
226+
227+
return execResult{
228+
exitCode: exitCode,
229+
output: stdout + stderr,
230+
}
231+
}

0 commit comments

Comments
 (0)