fix(browser): retry chrome act when target tab is stale

When a Chrome relay targetId becomes stale between snapshot and action,
the browser tool now retries once without targetId so the relay falls
back to the currently attached tab.

Drop the unknown recovered field from the test mock return value
to satisfy tsc strict checking against BrowserActResponse.
This commit is contained in:
SidQin-cyber
2026-03-01 23:56:01 +08:00
committed by Peter Steinberger
parent 910c654807
commit 732c4f3921
2 changed files with 63 additions and 0 deletions

View File

@@ -523,3 +523,43 @@ describe("browser tool external content wrapping", () => {
});
});
});
describe("browser tool act stale target recovery", () => {
afterEach(() => {
vi.clearAllMocks();
configMocks.loadConfig.mockReturnValue({ browser: {} });
nodesUtilsMocks.listNodes.mockResolvedValue([]);
});
it("retries chrome act once without targetId when tab id is stale", async () => {
browserActionsMocks.browserAct
.mockRejectedValueOnce(new Error("404: tab not found"))
.mockResolvedValueOnce({ ok: true });
const tool = createBrowserTool();
const result = await tool.execute?.("call-1", {
action: "act",
profile: "chrome",
request: {
action: "click",
targetId: "stale-tab",
ref: "btn-1",
},
});
expect(browserActionsMocks.browserAct).toHaveBeenCalledTimes(2);
expect(browserActionsMocks.browserAct).toHaveBeenNthCalledWith(
1,
undefined,
expect.objectContaining({ targetId: "stale-tab", action: "click", ref: "btn-1" }),
expect.objectContaining({ profile: "chrome" }),
);
expect(browserActionsMocks.browserAct).toHaveBeenNthCalledWith(
2,
undefined,
expect.not.objectContaining({ targetId: expect.anything() }),
expect.objectContaining({ profile: "chrome" }),
);
expect(result?.details).toMatchObject({ ok: true });
});
});

View File

@@ -862,6 +862,29 @@ export function createBrowserTool(opts?: {
} catch (err) {
const msg = String(err);
if (msg.includes("404:") && msg.includes("tab not found") && profile === "chrome") {
const targetId =
typeof request.targetId === "string" ? request.targetId.trim() : undefined;
// Some Chrome relay targetIds can go stale between snapshots and actions.
// Retry once without targetId to let relay use the currently attached tab.
if (targetId) {
const retryRequest = { ...request };
delete retryRequest.targetId;
try {
const retryResult = proxyRequest
? await proxyRequest({
method: "POST",
path: "/act",
profile,
body: retryRequest,
})
: await browserAct(baseUrl, retryRequest as Parameters<typeof browserAct>[1], {
profile,
});
return jsonResult(retryResult);
} catch {
// Fall through to explicit stale-target guidance.
}
}
const tabs = proxyRequest
? ((
(await proxyRequest({