fix(browser): improve chrome relay tab selection
This commit is contained in:
@@ -96,4 +96,106 @@ describe("browser server-context ensureTabAvailable", () => {
|
|||||||
const second = await chrome.ensureTabAvailable();
|
const second = await chrome.ensureTabAvailable();
|
||||||
expect(second.targetId).toBe("A");
|
expect(second.targetId).toBe("A");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("falls back to the only attached tab when an invalid targetId is provided (extension)", async () => {
|
||||||
|
const fetchMock = vi.fn();
|
||||||
|
const responses = [
|
||||||
|
[{ id: "A", type: "page", url: "https://a.example", webSocketDebuggerUrl: "ws://x/a" }],
|
||||||
|
[{ id: "A", type: "page", url: "https://a.example", webSocketDebuggerUrl: "ws://x/a" }],
|
||||||
|
];
|
||||||
|
|
||||||
|
fetchMock.mockImplementation(async (url: unknown) => {
|
||||||
|
const u = String(url);
|
||||||
|
if (!u.includes("/json/list")) throw new Error(`unexpected fetch: ${u}`);
|
||||||
|
const next = responses.shift();
|
||||||
|
if (!next) throw new Error("no more responses");
|
||||||
|
return { ok: true, json: async () => next } as unknown as Response;
|
||||||
|
});
|
||||||
|
|
||||||
|
// @ts-expect-error test override
|
||||||
|
global.fetch = fetchMock;
|
||||||
|
|
||||||
|
const state: BrowserServerState = {
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: test stub
|
||||||
|
server: null as any,
|
||||||
|
port: 0,
|
||||||
|
resolved: {
|
||||||
|
enabled: true,
|
||||||
|
controlUrl: "http://127.0.0.1:18791",
|
||||||
|
controlHost: "127.0.0.1",
|
||||||
|
controlPort: 18791,
|
||||||
|
cdpProtocol: "http",
|
||||||
|
cdpHost: "127.0.0.1",
|
||||||
|
cdpIsLoopback: true,
|
||||||
|
color: "#FF4500",
|
||||||
|
headless: true,
|
||||||
|
noSandbox: false,
|
||||||
|
attachOnly: false,
|
||||||
|
defaultProfile: "chrome",
|
||||||
|
profiles: {
|
||||||
|
chrome: {
|
||||||
|
driver: "extension",
|
||||||
|
cdpUrl: "http://127.0.0.1:18792",
|
||||||
|
cdpPort: 18792,
|
||||||
|
color: "#00AA00",
|
||||||
|
},
|
||||||
|
clawd: { cdpPort: 18800, color: "#FF4500" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
profiles: new Map(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const ctx = createBrowserRouteContext({ getState: () => state });
|
||||||
|
const chrome = ctx.forProfile("chrome");
|
||||||
|
const chosen = await chrome.ensureTabAvailable("NOT_A_TAB");
|
||||||
|
expect(chosen.targetId).toBe("A");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns a descriptive message when no extension tabs are attached", async () => {
|
||||||
|
const fetchMock = vi.fn();
|
||||||
|
const responses = [[]];
|
||||||
|
fetchMock.mockImplementation(async (url: unknown) => {
|
||||||
|
const u = String(url);
|
||||||
|
if (!u.includes("/json/list")) throw new Error(`unexpected fetch: ${u}`);
|
||||||
|
const next = responses.shift();
|
||||||
|
if (!next) throw new Error("no more responses");
|
||||||
|
return { ok: true, json: async () => next } as unknown as Response;
|
||||||
|
});
|
||||||
|
// @ts-expect-error test override
|
||||||
|
global.fetch = fetchMock;
|
||||||
|
|
||||||
|
const state: BrowserServerState = {
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: test stub
|
||||||
|
server: null as any,
|
||||||
|
port: 0,
|
||||||
|
resolved: {
|
||||||
|
enabled: true,
|
||||||
|
controlUrl: "http://127.0.0.1:18791",
|
||||||
|
controlHost: "127.0.0.1",
|
||||||
|
controlPort: 18791,
|
||||||
|
cdpProtocol: "http",
|
||||||
|
cdpHost: "127.0.0.1",
|
||||||
|
cdpIsLoopback: true,
|
||||||
|
color: "#FF4500",
|
||||||
|
headless: true,
|
||||||
|
noSandbox: false,
|
||||||
|
attachOnly: false,
|
||||||
|
defaultProfile: "chrome",
|
||||||
|
profiles: {
|
||||||
|
chrome: {
|
||||||
|
driver: "extension",
|
||||||
|
cdpUrl: "http://127.0.0.1:18792",
|
||||||
|
cdpPort: 18792,
|
||||||
|
color: "#00AA00",
|
||||||
|
},
|
||||||
|
clawd: { cdpPort: 18800, color: "#FF4500" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
profiles: new Map(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const ctx = createBrowserRouteContext({ getState: () => state });
|
||||||
|
const chrome = ctx.forProfile("chrome");
|
||||||
|
await expect(chrome.ensureTabAvailable()).rejects.toThrow(/no attached Chrome tabs/i);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -282,13 +282,16 @@ function createProfileContext(
|
|||||||
const tabs1 = await listTabs();
|
const tabs1 = await listTabs();
|
||||||
if (tabs1.length === 0) {
|
if (tabs1.length === 0) {
|
||||||
if (profile.driver === "extension") {
|
if (profile.driver === "extension") {
|
||||||
throw new Error("tab not found");
|
throw new Error(
|
||||||
|
`tab not found (no attached Chrome tabs for profile "${profile.name}"). ` +
|
||||||
|
"Click the Clawdbot Browser Relay toolbar icon on the tab you want to control (badge ON).",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
await openTab("about:blank");
|
await openTab("about:blank");
|
||||||
}
|
}
|
||||||
|
|
||||||
const tabs = await listTabs();
|
const tabs = await listTabs();
|
||||||
const candidates = tabs.filter((t) => Boolean(t.wsUrl));
|
const candidates = profile.driver === "extension" ? tabs : tabs.filter((t) => Boolean(t.wsUrl));
|
||||||
|
|
||||||
const resolveById = (raw: string) => {
|
const resolveById = (raw: string) => {
|
||||||
const resolved = resolveTargetIdFromTabs(raw, candidates);
|
const resolved = resolveTargetIdFromTabs(raw, candidates);
|
||||||
@@ -308,12 +311,17 @@ function createProfileContext(
|
|||||||
return page ?? candidates.at(0) ?? null;
|
return page ?? candidates.at(0) ?? null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const chosen = targetId ? resolveById(targetId) : pickDefault();
|
let chosen = targetId ? resolveById(targetId) : pickDefault();
|
||||||
|
if (!chosen && profile.driver === "extension" && candidates.length === 1) {
|
||||||
|
// If an agent passes a stale/foreign targetId but we only have a single attached tab,
|
||||||
|
// recover by using that tab instead of failing hard.
|
||||||
|
chosen = candidates[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
if (chosen === "AMBIGUOUS") {
|
if (chosen === "AMBIGUOUS") {
|
||||||
throw new Error("ambiguous target id prefix");
|
throw new Error("ambiguous target id prefix");
|
||||||
}
|
}
|
||||||
if (!chosen?.wsUrl) throw new Error("tab not found");
|
if (!chosen) throw new Error("tab not found");
|
||||||
profileState.lastTargetId = chosen.targetId;
|
profileState.lastTargetId = chosen.targetId;
|
||||||
return chosen;
|
return chosen;
|
||||||
};
|
};
|
||||||
@@ -496,7 +504,7 @@ export function createBrowserRouteContext(opts: ContextOptions): BrowserRouteCon
|
|||||||
return { status: 409, message: "ambiguous target id prefix" };
|
return { status: 409, message: "ambiguous target id prefix" };
|
||||||
}
|
}
|
||||||
if (msg.includes("tab not found")) {
|
if (msg.includes("tab not found")) {
|
||||||
return { status: 404, message: "tab not found" };
|
return { status: 404, message: msg };
|
||||||
}
|
}
|
||||||
if (msg.includes("not found")) {
|
if (msg.includes("not found")) {
|
||||||
return { status: 404, message: msg };
|
return { status: 404, message: msg };
|
||||||
|
|||||||
Reference in New Issue
Block a user