From 1220123d8799756e16542451989190b7d8c87739 Mon Sep 17 00:00:00 2001 From: IlyaasK <86218345+IlyaasK@users.noreply.github.com> Date: Tue, 7 Jul 2026 10:45:55 +0000 Subject: [PATCH 1/7] Bound Playwright daemon executions --- server/e2e/e2e_display_resize_window_test.go | 28 ++++++++---- server/runtime/playwright-daemon.ts | 46 ++++++++++++++------ 2 files changed, 52 insertions(+), 22 deletions(-) diff --git a/server/e2e/e2e_display_resize_window_test.go b/server/e2e/e2e_display_resize_window_test.go index 527e1202..24ae1d80 100644 --- a/server/e2e/e2e_display_resize_window_test.go +++ b/server/e2e/e2e_display_resize_window_test.go @@ -457,7 +457,6 @@ func TestDisplayResizeChromiumWindow(t *testing.T) { } } - // navigateBlank points the active page at about:blank via playwright so the // renderer is alive before we query window dimensions. func navigateBlank(t *testing.T, ctx context.Context, c *TestContainer) { @@ -465,13 +464,26 @@ func navigateBlank(t *testing.T, ctx context.Context, c *TestContainer) { client, err := c.APIClient() require.NoError(t, err) timeout := 5 - rsp, err := client.ExecutePlaywrightCodeWithResponse(ctx, instanceoapi.ExecutePlaywrightRequest{ - Code: `await page.goto('about:blank'); return true;`, - TimeoutSec: &timeout, - }) - require.NoError(t, err) - require.NotNil(t, rsp.JSON200, "playwright navigate response missing") - require.True(t, rsp.JSON200.Success, "playwright navigate to about:blank failed: %s", string(rsp.Body)) + var lastFailure string + require.Eventually(t, func() bool { + rsp, err := client.ExecutePlaywrightCodeWithResponse(ctx, instanceoapi.ExecutePlaywrightRequest{ + Code: `await page.goto('about:blank'); return true;`, + TimeoutSec: &timeout, + }) + if err != nil { + lastFailure = err.Error() + return false + } + if rsp.JSON200 == nil { + lastFailure = fmt.Sprintf("missing JSON200 status=%s body=%s", rsp.Status(), string(rsp.Body)) + return false + } + if !rsp.JSON200.Success { + lastFailure = string(rsp.Body) + return false + } + return true + }, 30*time.Second, 500*time.Millisecond, "playwright navigate to about:blank failed: %s", lastFailure) } // patchDisplayExpectingOK issues PATCH /display and requires a 200. The diff --git a/server/runtime/playwright-daemon.ts b/server/runtime/playwright-daemon.ts index ed1e6ed5..15fa1516 100644 --- a/server/runtime/playwright-daemon.ts +++ b/server/runtime/playwright-daemon.ts @@ -39,6 +39,17 @@ interface ExecuteResponse { stack?: string; } +function withTimeout(promise: Promise, timeoutMs: number): Promise { + let timeout: ReturnType | undefined; + const timeoutPromise = new Promise((_, reject) => { + timeout = setTimeout(() => reject(new Error(`Execution timed out after ${timeoutMs}ms`)), timeoutMs); + }); + + return Promise.race([promise, timeoutPromise]).finally(() => { + if (timeout) clearTimeout(timeout); + }); +} + async function transformCode(code: string): Promise { // Wrap in async function so top-level await/return are valid for esbuild const wrapped = `async function __userCode__() {\n${code}\n}`; @@ -88,23 +99,26 @@ async function ensureBrowserConnection(): Promise { } console.error(`[playwright-daemon] Connecting to CDP: ${CDP_ENDPOINT}`); - browser = await chromium.connectOverCDP(CDP_ENDPOINT); + const connectedBrowser = await chromium.connectOverCDP(CDP_ENDPOINT); + browser = connectedBrowser; reconnectAttempts = 0; - browser.on('disconnected', () => { + connectedBrowser.on('disconnected', () => { console.error('[playwright-daemon] Browser disconnected'); - browser = null; + if (browser === connectedBrowser) { + browser = null; + } }); console.error('[playwright-daemon] CDP connection established'); - return browser; + return connectedBrowser; } finally { connecting = false; } } async function executeCode(request: ExecuteRequest): Promise { - const { id, code, timeout_ms = 60000 } = request; + const { id, code } = request; try { let jsCode: string; @@ -151,14 +165,7 @@ async function executeCode(request: ExecuteRequest): Promise { const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor; const userFunction = new AsyncFunction('page', 'context', 'browser', jsCode); - const timeoutPromise = new Promise((_, reject) => { - setTimeout(() => reject(new Error(`Execution timed out after ${timeout_ms}ms`)), timeout_ms); - }); - - const result = await Promise.race([ - userFunction(page, context, browserInstance), - timeoutPromise, - ]); + const result = await userFunction(page, context, browserInstance); return { id, @@ -201,7 +208,18 @@ function handleConnection(socket: Socket): void { continue; } - const response = await executeCode(request); + const timeoutMs = request.timeout_ms ?? 60000; + let response: ExecuteResponse; + try { + response = await withTimeout(executeCode(request), timeoutMs); + } catch (error: any) { + response = { + id: request.id, + success: false, + error: error.message, + stack: error.stack, + }; + } socket.write(JSON.stringify(response) + '\n'); } }); From 63dd3b268d81720913bb29925d5d3760f09f5e8d Mon Sep 17 00:00:00 2001 From: IlyaasK <86218345+IlyaasK@users.noreply.github.com> Date: Tue, 7 Jul 2026 10:51:12 +0000 Subject: [PATCH 2/7] Avoid running timed-out Playwright requests --- server/runtime/playwright-daemon.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/server/runtime/playwright-daemon.ts b/server/runtime/playwright-daemon.ts index 15fa1516..e1e4d8ac 100644 --- a/server/runtime/playwright-daemon.ts +++ b/server/runtime/playwright-daemon.ts @@ -50,6 +50,12 @@ function withTimeout(promise: Promise, timeoutMs: number): Promise { }); } +function requireWithinDeadline(deadlineMs: number, timeoutMs: number): void { + if (Date.now() >= deadlineMs) { + throw new Error(`Execution timed out after ${timeoutMs}ms`); + } +} + async function transformCode(code: string): Promise { // Wrap in async function so top-level await/return are valid for esbuild const wrapped = `async function __userCode__() {\n${code}\n}`; @@ -117,7 +123,7 @@ async function ensureBrowserConnection(): Promise { } } -async function executeCode(request: ExecuteRequest): Promise { +async function executeCode(request: ExecuteRequest, deadlineMs: number, timeoutMs: number): Promise { const { id, code } = request; try { @@ -132,6 +138,7 @@ async function executeCode(request: ExecuteRequest): Promise { stack: transformError.stack, }; } + requireWithinDeadline(deadlineMs, timeoutMs); let browserInstance: Browser; try { @@ -156,11 +163,14 @@ async function executeCode(request: ExecuteRequest): Promise { }; } } + requireWithinDeadline(deadlineMs, timeoutMs); const contexts = browserInstance.contexts(); const context = contexts.length > 0 ? contexts[0] : await browserInstance.newContext(); + requireWithinDeadline(deadlineMs, timeoutMs); const pages = context.pages(); const page = pages.length > 0 ? pages[0] : await context.newPage(); + requireWithinDeadline(deadlineMs, timeoutMs); const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor; const userFunction = new AsyncFunction('page', 'context', 'browser', jsCode); @@ -209,9 +219,10 @@ function handleConnection(socket: Socket): void { } const timeoutMs = request.timeout_ms ?? 60000; + const deadlineMs = Date.now() + timeoutMs; let response: ExecuteResponse; try { - response = await withTimeout(executeCode(request), timeoutMs); + response = await withTimeout(executeCode(request, deadlineMs, timeoutMs), timeoutMs); } catch (error: any) { response = { id: request.id, From 4655f31908e3adece695f8d2861c2b63eb28b8dd Mon Sep 17 00:00:00 2001 From: Ilyaas Kapadia <86218345+IlyaasK@users.noreply.github.com> Date: Tue, 7 Jul 2026 10:45:54 -0400 Subject: [PATCH 3/7] Tighten Playwright timeout guards Keep reconnect retries within the request deadline and check the deadline immediately before user code starts. Preserve the final Playwright retry failure in the display resize readiness helper so flaky failures stay diagnosable. --- server/e2e/e2e_display_resize_window_test.go | 19 +++++++++---------- server/runtime/playwright-daemon.ts | 15 ++++++++++++++- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/server/e2e/e2e_display_resize_window_test.go b/server/e2e/e2e_display_resize_window_test.go index 24ae1d80..1e51a950 100644 --- a/server/e2e/e2e_display_resize_window_test.go +++ b/server/e2e/e2e_display_resize_window_test.go @@ -12,6 +12,7 @@ import ( "time" instanceoapi "github.com/kernel/kernel-images/server/lib/oapi" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -464,26 +465,24 @@ func navigateBlank(t *testing.T, ctx context.Context, c *TestContainer) { client, err := c.APIClient() require.NoError(t, err) timeout := 5 - var lastFailure string - require.Eventually(t, func() bool { + require.EventuallyWithT(t, func(collect *assert.CollectT) { rsp, err := client.ExecutePlaywrightCodeWithResponse(ctx, instanceoapi.ExecutePlaywrightRequest{ Code: `await page.goto('about:blank'); return true;`, TimeoutSec: &timeout, }) if err != nil { - lastFailure = err.Error() - return false + require.NoError(collect, err) + return } if rsp.JSON200 == nil { - lastFailure = fmt.Sprintf("missing JSON200 status=%s body=%s", rsp.Status(), string(rsp.Body)) - return false + require.NotNil(collect, rsp.JSON200, "missing JSON200 status=%s body=%s", rsp.Status(), string(rsp.Body)) + return } if !rsp.JSON200.Success { - lastFailure = string(rsp.Body) - return false + require.True(collect, rsp.JSON200.Success, "playwright navigate to about:blank failed: %s", string(rsp.Body)) + return } - return true - }, 30*time.Second, 500*time.Millisecond, "playwright navigate to about:blank failed: %s", lastFailure) + }, 30*time.Second, 500*time.Millisecond) } // patchDisplayExpectingOK issues PATCH /display and requires a 200. The diff --git a/server/runtime/playwright-daemon.ts b/server/runtime/playwright-daemon.ts index e1e4d8ac..4c2b3896 100644 --- a/server/runtime/playwright-daemon.ts +++ b/server/runtime/playwright-daemon.ts @@ -56,6 +56,18 @@ function requireWithinDeadline(deadlineMs: number, timeoutMs: number): void { } } +async function delayWithinDeadline(delayMs: number, deadlineMs: number, timeoutMs: number): Promise { + requireWithinDeadline(deadlineMs, timeoutMs); + + const remainingMs = deadlineMs - Date.now(); + if (remainingMs < delayMs) { + throw new Error(`Execution timed out after ${timeoutMs}ms`); + } + + await new Promise(resolve => setTimeout(resolve, delayMs)); + requireWithinDeadline(deadlineMs, timeoutMs); +} + async function transformCode(code: string): Promise { // Wrap in async function so top-level await/return are valid for esbuild const wrapped = `async function __userCode__() {\n${code}\n}`; @@ -152,7 +164,7 @@ async function executeCode(request: ExecuteRequest, deadlineMs: number, timeoutM error: `Failed to connect to browser after ${MAX_RECONNECT_ATTEMPTS} attempts: ${connError.message}`, }; } - await new Promise(resolve => setTimeout(resolve, RECONNECT_DELAY_MS)); + await delayWithinDeadline(RECONNECT_DELAY_MS, deadlineMs, timeoutMs); try { browserInstance = await ensureBrowserConnection(); } catch (retryError: any) { @@ -174,6 +186,7 @@ async function executeCode(request: ExecuteRequest, deadlineMs: number, timeoutM const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor; const userFunction = new AsyncFunction('page', 'context', 'browser', jsCode); + requireWithinDeadline(deadlineMs, timeoutMs); const result = await userFunction(page, context, browserInstance); From 873572679dac0df2bc2cbfd7ece129015d964de4 Mon Sep 17 00:00:00 2001 From: Ilyaas Kapadia <86218345+IlyaasK@users.noreply.github.com> Date: Wed, 8 Jul 2026 11:35:19 -0400 Subject: [PATCH 4/7] Trim Playwright timeout guard cleanup Keep only the deadline guard that prevents timed-out setup from starting user code, drop the now-unneeded reconnect delay helper, and simplify the display resize retry assertions. --- server/e2e/e2e_display_resize_window_test.go | 15 +++------------ server/runtime/playwright-daemon.ts | 20 +------------------- 2 files changed, 4 insertions(+), 31 deletions(-) diff --git a/server/e2e/e2e_display_resize_window_test.go b/server/e2e/e2e_display_resize_window_test.go index 1e51a950..2162e42e 100644 --- a/server/e2e/e2e_display_resize_window_test.go +++ b/server/e2e/e2e_display_resize_window_test.go @@ -470,18 +470,9 @@ func navigateBlank(t *testing.T, ctx context.Context, c *TestContainer) { Code: `await page.goto('about:blank'); return true;`, TimeoutSec: &timeout, }) - if err != nil { - require.NoError(collect, err) - return - } - if rsp.JSON200 == nil { - require.NotNil(collect, rsp.JSON200, "missing JSON200 status=%s body=%s", rsp.Status(), string(rsp.Body)) - return - } - if !rsp.JSON200.Success { - require.True(collect, rsp.JSON200.Success, "playwright navigate to about:blank failed: %s", string(rsp.Body)) - return - } + require.NoError(collect, err) + require.NotNil(collect, rsp.JSON200, "missing JSON200 status=%s body=%s", rsp.Status(), string(rsp.Body)) + require.True(collect, rsp.JSON200.Success, "playwright navigate to about:blank failed: %s", string(rsp.Body)) }, 30*time.Second, 500*time.Millisecond) } diff --git a/server/runtime/playwright-daemon.ts b/server/runtime/playwright-daemon.ts index 4c2b3896..a605806d 100644 --- a/server/runtime/playwright-daemon.ts +++ b/server/runtime/playwright-daemon.ts @@ -56,18 +56,6 @@ function requireWithinDeadline(deadlineMs: number, timeoutMs: number): void { } } -async function delayWithinDeadline(delayMs: number, deadlineMs: number, timeoutMs: number): Promise { - requireWithinDeadline(deadlineMs, timeoutMs); - - const remainingMs = deadlineMs - Date.now(); - if (remainingMs < delayMs) { - throw new Error(`Execution timed out after ${timeoutMs}ms`); - } - - await new Promise(resolve => setTimeout(resolve, delayMs)); - requireWithinDeadline(deadlineMs, timeoutMs); -} - async function transformCode(code: string): Promise { // Wrap in async function so top-level await/return are valid for esbuild const wrapped = `async function __userCode__() {\n${code}\n}`; @@ -150,8 +138,6 @@ async function executeCode(request: ExecuteRequest, deadlineMs: number, timeoutM stack: transformError.stack, }; } - requireWithinDeadline(deadlineMs, timeoutMs); - let browserInstance: Browser; try { browserInstance = await ensureBrowserConnection(); @@ -164,7 +150,7 @@ async function executeCode(request: ExecuteRequest, deadlineMs: number, timeoutM error: `Failed to connect to browser after ${MAX_RECONNECT_ATTEMPTS} attempts: ${connError.message}`, }; } - await delayWithinDeadline(RECONNECT_DELAY_MS, deadlineMs, timeoutMs); + await new Promise(resolve => setTimeout(resolve, RECONNECT_DELAY_MS)); try { browserInstance = await ensureBrowserConnection(); } catch (retryError: any) { @@ -175,14 +161,10 @@ async function executeCode(request: ExecuteRequest, deadlineMs: number, timeoutM }; } } - requireWithinDeadline(deadlineMs, timeoutMs); - const contexts = browserInstance.contexts(); const context = contexts.length > 0 ? contexts[0] : await browserInstance.newContext(); - requireWithinDeadline(deadlineMs, timeoutMs); const pages = context.pages(); const page = pages.length > 0 ? pages[0] : await context.newPage(); - requireWithinDeadline(deadlineMs, timeoutMs); const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor; const userFunction = new AsyncFunction('page', 'context', 'browser', jsCode); From fb99856d8a01ddc7368ab70ec0500b313cef1568 Mon Sep 17 00:00:00 2001 From: Ilyaas Kapadia <86218345+IlyaasK@users.noreply.github.com> Date: Wed, 8 Jul 2026 14:54:50 -0400 Subject: [PATCH 5/7] Use AbortSignal for Playwright deadline guard Replace the custom deadline timestamp check with AbortSignal.timeout and throwIfAborted so the pre-user-code timeout guard is self-describing and drops the message-only timeout parameter. --- server/runtime/playwright-daemon.ts | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/server/runtime/playwright-daemon.ts b/server/runtime/playwright-daemon.ts index a605806d..6424c355 100644 --- a/server/runtime/playwright-daemon.ts +++ b/server/runtime/playwright-daemon.ts @@ -50,12 +50,6 @@ function withTimeout(promise: Promise, timeoutMs: number): Promise { }); } -function requireWithinDeadline(deadlineMs: number, timeoutMs: number): void { - if (Date.now() >= deadlineMs) { - throw new Error(`Execution timed out after ${timeoutMs}ms`); - } -} - async function transformCode(code: string): Promise { // Wrap in async function so top-level await/return are valid for esbuild const wrapped = `async function __userCode__() {\n${code}\n}`; @@ -123,7 +117,7 @@ async function ensureBrowserConnection(): Promise { } } -async function executeCode(request: ExecuteRequest, deadlineMs: number, timeoutMs: number): Promise { +async function executeCode(request: ExecuteRequest, signal: AbortSignal): Promise { const { id, code } = request; try { @@ -168,7 +162,7 @@ async function executeCode(request: ExecuteRequest, deadlineMs: number, timeoutM const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor; const userFunction = new AsyncFunction('page', 'context', 'browser', jsCode); - requireWithinDeadline(deadlineMs, timeoutMs); + signal.throwIfAborted(); const result = await userFunction(page, context, browserInstance); @@ -214,10 +208,10 @@ function handleConnection(socket: Socket): void { } const timeoutMs = request.timeout_ms ?? 60000; - const deadlineMs = Date.now() + timeoutMs; + const signal = AbortSignal.timeout(timeoutMs); let response: ExecuteResponse; try { - response = await withTimeout(executeCode(request, deadlineMs, timeoutMs), timeoutMs); + response = await withTimeout(executeCode(request, signal), timeoutMs); } catch (error: any) { response = { id: request.id, From 5a8b33d06c53f3ee04e9766ed142f530981b9509 Mon Sep 17 00:00:00 2001 From: Ilyaas Kapadia <86218345+IlyaasK@users.noreply.github.com> Date: Thu, 9 Jul 2026 08:54:08 -0400 Subject: [PATCH 6/7] Drive Playwright timeout race from abort signal Use the same AbortSignal for both the response timeout race and the pre-user-code guard so there is a single deadline authority and no gap between the timeout response and signal abortion. --- server/runtime/playwright-daemon.ts | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/server/runtime/playwright-daemon.ts b/server/runtime/playwright-daemon.ts index 6424c355..6ce6c61d 100644 --- a/server/runtime/playwright-daemon.ts +++ b/server/runtime/playwright-daemon.ts @@ -39,15 +39,17 @@ interface ExecuteResponse { stack?: string; } -function withTimeout(promise: Promise, timeoutMs: number): Promise { - let timeout: ReturnType | undefined; - const timeoutPromise = new Promise((_, reject) => { - timeout = setTimeout(() => reject(new Error(`Execution timed out after ${timeoutMs}ms`)), timeoutMs); - }); +function withTimeout(promise: Promise, signal: AbortSignal): Promise { + if (signal.aborted) { + return Promise.reject(signal.reason); + } - return Promise.race([promise, timeoutPromise]).finally(() => { - if (timeout) clearTimeout(timeout); - }); + return Promise.race([ + promise, + new Promise((_, reject) => { + signal.addEventListener('abort', () => reject(signal.reason), { once: true }); + }), + ]); } async function transformCode(code: string): Promise { @@ -207,11 +209,10 @@ function handleConnection(socket: Socket): void { continue; } - const timeoutMs = request.timeout_ms ?? 60000; - const signal = AbortSignal.timeout(timeoutMs); + const signal = AbortSignal.timeout(request.timeout_ms ?? 60000); let response: ExecuteResponse; try { - response = await withTimeout(executeCode(request, signal), timeoutMs); + response = await withTimeout(executeCode(request, signal), signal); } catch (error: any) { response = { id: request.id, From 3f3b5ad5cd87ce61a662dc3ebc4c93e22173f09f Mon Sep 17 00:00:00 2001 From: Ilyaas Kapadia <86218345+IlyaasK@users.noreply.github.com> Date: Thu, 9 Jul 2026 15:21:51 -0400 Subject: [PATCH 7/7] Add Playwright timeout regression test Cover the request timeout path Raf called out by executing a long-running Playwright snippet with a short timeout and asserting the daemon returns a prompt success=false response. Also verify a follow-up userAgent request succeeds so the test locks in recovery after a timed-out execution. --- server/e2e/e2e_playwright_test.go | 62 +++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/server/e2e/e2e_playwright_test.go b/server/e2e/e2e_playwright_test.go index 14ee03de..03999fb0 100644 --- a/server/e2e/e2e_playwright_test.go +++ b/server/e2e/e2e_playwright_test.go @@ -6,6 +6,7 @@ import ( "fmt" "net/http" "os/exec" + "strings" "testing" "time" @@ -80,6 +81,67 @@ func TestPlaywrightExecuteAPI(t *testing.T) { t.Log("playwright execute API test passed") } +func TestPlaywrightExecuteTimeoutReturnsPromptlyAndRecovers(t *testing.T) { + t.Parallel() + + if _, err := exec.LookPath("docker"); err != nil { + t.Skipf("docker not available: %v", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + + c := NewTestContainer(t, headlessImage) + require.NoError(t, c.Start(ctx, ContainerConfig{}), "failed to start container") + defer c.Stop(ctx) + + require.NoError(t, c.WaitReady(ctx), "api not ready") + + client, err := c.APIClient() + require.NoError(t, err) + + timeoutSec := 2 + timeoutReq := instanceoapi.ExecutePlaywrightCodeJSONRequestBody{ + Code: `await new Promise(r => setTimeout(r, 30000));`, + TimeoutSec: &timeoutSec, + } + + t.Log("executing playwright code expected to exceed timeout") + start := time.Now() + timeoutRsp, err := client.ExecutePlaywrightCodeWithResponse(ctx, timeoutReq) + elapsed := time.Since(start) + + require.NoError(t, err, "playwright timeout request should return an API response") + require.Less(t, elapsed, 5*time.Second, "timeout response should arrive before the API socket read deadline") + require.Equal(t, http.StatusOK, timeoutRsp.StatusCode(), "unexpected status for timed-out playwright execute: %s body=%s", timeoutRsp.Status(), string(timeoutRsp.Body)) + require.NotNil(t, timeoutRsp.JSON200, "expected JSON200 timeout response, got nil") + require.False(t, timeoutRsp.JSON200.Success, "expected success=false for timed-out playwright execution") + require.NotNil(t, timeoutRsp.JSON200.Error, "expected timeout error message") + + errorMsg := strings.ToLower(*timeoutRsp.JSON200.Error) + require.True(t, strings.Contains(errorMsg, "timeout") || strings.Contains(errorMsg, "timed out"), "expected timeout error, got %q", *timeoutRsp.JSON200.Error) + require.NotContains(t, errorMsg, "i/o timeout", "daemon should return a timeout response before the API socket read deadline") + + recoveryReq := instanceoapi.ExecutePlaywrightCodeJSONRequestBody{ + Code: `return await page.evaluate(() => navigator.userAgent);`, + } + + t.Log("executing normal playwright code after timed-out request") + recoveryRsp, err := client.ExecutePlaywrightCodeWithResponse(ctx, recoveryReq) + require.NoError(t, err, "playwright recovery request error: %v", err) + require.Equal(t, http.StatusOK, recoveryRsp.StatusCode(), "unexpected status for playwright recovery execute: %s body=%s", recoveryRsp.Status(), string(recoveryRsp.Body)) + require.NotNil(t, recoveryRsp.JSON200, "expected JSON200 recovery response, got nil") + require.True(t, recoveryRsp.JSON200.Success, "expected recovery request success=true, got error: %s", func() string { + if recoveryRsp.JSON200.Error != nil { + return *recoveryRsp.JSON200.Error + } + return "nil" + }()) + require.NotNil(t, recoveryRsp.JSON200.Result, "expected recovery result to be non-nil") + + t.Log("playwright timeout regression test passed") +} + // TestPlaywrightDaemonRecovery tests that the playwright daemon recovers after chromium is restarted. // The daemon maintains a warm CDP connection, but when chromium restarts, that connection breaks. // The daemon should detect the disconnection and reconnect on the next request.