From e82ba71911ad971d0be9219e9f8d064c61a57746 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Fri, 13 Mar 2026 18:39:28 -0400 Subject: [PATCH] fix(browser): follow up batch failure and limit handling (#45506) * fix(browser): propagate nested batch failures * fix(browser): validate top-level batch limits * test(browser): cover nested batch failures * test(browser): cover top-level batch limits --- .../pw-tools-core.interactions.batch.test.ts | 19 +++++++++++++++++++ src/browser/pw-tools-core.interactions.ts | 6 +++++- src/browser/routes/agent.act.ts | 3 +++ ...-contract-form-layout-act-commands.test.ts | 16 ++++++++++++++++ 4 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/browser/pw-tools-core.interactions.batch.test.ts b/src/browser/pw-tools-core.interactions.batch.test.ts index 2801ebe8190..fbd2de4cbc6 100644 --- a/src/browser/pw-tools-core.interactions.batch.test.ts +++ b/src/browser/pw-tools-core.interactions.batch.test.ts @@ -82,4 +82,23 @@ describe("batchViaPlaywright", () => { targetId: "tab-1", }); }); + + it("propagates nested batch failures to the parent batch result", async () => { + const result = await batchViaPlaywright({ + cdpUrl: "http://127.0.0.1:9222", + targetId: "tab-1", + actions: [ + { + kind: "batch", + actions: [{ kind: "evaluate", fn: "() => 1" }], + }, + ], + }); + + expect(result).toEqual({ + results: [ + { ok: false, error: "act:evaluate is disabled by config (browser.evaluateEnabled=false)" }, + ], + }); + }); }); diff --git a/src/browser/pw-tools-core.interactions.ts b/src/browser/pw-tools-core.interactions.ts index dee8a03316c..da0efa0c145 100644 --- a/src/browser/pw-tools-core.interactions.ts +++ b/src/browser/pw-tools-core.interactions.ts @@ -845,7 +845,7 @@ async function executeSingleAction( break; case "batch": // Nested batches: delegate recursively - await batchViaPlaywright({ + const nestedResult = await batchViaPlaywright({ cdpUrl, targetId: effectiveTargetId, actions: action.actions, @@ -853,6 +853,10 @@ async function executeSingleAction( evaluateEnabled, depth: depth + 1, }); + const nestedFailure = nestedResult.results.find((result) => !result.ok); + if (nestedFailure) { + throw new Error(nestedFailure.error ?? "Nested batch action failed"); + } break; default: throw new Error(`Unsupported batch action kind: ${(action as { kind: string }).kind}`); diff --git a/src/browser/routes/agent.act.ts b/src/browser/routes/agent.act.ts index 0c4c9e71967..05557fe1129 100644 --- a/src/browser/routes/agent.act.ts +++ b/src/browser/routes/agent.act.ts @@ -1043,6 +1043,9 @@ export function registerBrowserAgentActRoutes( if (!actions.length) { return jsonError(res, 400, "actions are required"); } + if (countBatchActions(actions) > MAX_BATCH_ACTIONS) { + return jsonError(res, 400, `batch exceeds maximum of ${MAX_BATCH_ACTIONS} actions`); + } const targetIdError = validateBatchTargetIds(actions, tab.targetId); if (targetIdError) { return jsonError(res, 403, targetIdError); diff --git a/src/browser/server.agent-contract-form-layout-act-commands.test.ts b/src/browser/server.agent-contract-form-layout-act-commands.test.ts index 912d024916c..16ade600bec 100644 --- a/src/browser/server.agent-contract-form-layout-act-commands.test.ts +++ b/src/browser/server.agent-contract-form-layout-act-commands.test.ts @@ -287,6 +287,22 @@ describe("browser control server", () => { slowTimeoutMs, ); + it( + "rejects oversized top-level batches before dispatch", + async () => { + const base = await startServerAndBase(); + + const batchRes = await postJson<{ error?: string }>(`${base}/act`, { + kind: "batch", + actions: Array.from({ length: 101 }, () => ({ kind: "press", key: "Enter" })), + }); + + expect(batchRes.error).toContain("batch exceeds maximum of 100 actions"); + expect(pwMocks.batchViaPlaywright).not.toHaveBeenCalled(); + }, + slowTimeoutMs, + ); + it("agent contract: hooks + response + downloads + screenshot", async () => { const base = await startServerAndBase();