fix(browser): accept targetId prefixes
This commit is contained in:
@@ -26,6 +26,7 @@ import {
|
|||||||
DEFAULT_BROWSER_SCREENSHOT_MAX_SIDE,
|
DEFAULT_BROWSER_SCREENSHOT_MAX_SIDE,
|
||||||
normalizeBrowserScreenshot,
|
normalizeBrowserScreenshot,
|
||||||
} from "./screenshot.js";
|
} from "./screenshot.js";
|
||||||
|
import { resolveTargetIdFromTabs } from "./target-id.js";
|
||||||
|
|
||||||
export type BrowserTab = {
|
export type BrowserTab = {
|
||||||
targetId: string;
|
targetId: string;
|
||||||
@@ -270,7 +271,15 @@ export async function startBrowserControlServerFromConfig(
|
|||||||
const reachable = await isChromeReachable(state.cdpPort, 300);
|
const reachable = await isChromeReachable(state.cdpPort, 300);
|
||||||
if (!reachable) return jsonError(res, 409, "browser not running");
|
if (!reachable) return jsonError(res, 409, "browser not running");
|
||||||
try {
|
try {
|
||||||
await activateTab(state.cdpPort, targetId);
|
const tabs = await listTabs(state.cdpPort);
|
||||||
|
const resolved = resolveTargetIdFromTabs(targetId, tabs);
|
||||||
|
if (!resolved.ok) {
|
||||||
|
if (resolved.reason === "ambiguous") {
|
||||||
|
return jsonError(res, 409, "ambiguous target id prefix");
|
||||||
|
}
|
||||||
|
return jsonError(res, 404, "tab not found");
|
||||||
|
}
|
||||||
|
await activateTab(state.cdpPort, resolved.targetId);
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
jsonError(res, 500, String(err));
|
jsonError(res, 500, String(err));
|
||||||
@@ -284,7 +293,15 @@ export async function startBrowserControlServerFromConfig(
|
|||||||
const reachable = await isChromeReachable(state.cdpPort, 300);
|
const reachable = await isChromeReachable(state.cdpPort, 300);
|
||||||
if (!reachable) return jsonError(res, 409, "browser not running");
|
if (!reachable) return jsonError(res, 409, "browser not running");
|
||||||
try {
|
try {
|
||||||
await closeTab(state.cdpPort, targetId);
|
const tabs = await listTabs(state.cdpPort);
|
||||||
|
const resolved = resolveTargetIdFromTabs(targetId, tabs);
|
||||||
|
if (!resolved.ok) {
|
||||||
|
if (resolved.reason === "ambiguous") {
|
||||||
|
return jsonError(res, 409, "ambiguous target id prefix");
|
||||||
|
}
|
||||||
|
return jsonError(res, 404, "tab not found");
|
||||||
|
}
|
||||||
|
await closeTab(state.cdpPort, resolved.targetId);
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
jsonError(res, 500, String(err));
|
jsonError(res, 500, String(err));
|
||||||
@@ -304,8 +321,20 @@ export async function startBrowserControlServerFromConfig(
|
|||||||
try {
|
try {
|
||||||
const tabs = await listTabs(state.cdpPort);
|
const tabs = await listTabs(state.cdpPort);
|
||||||
const chosen = targetId
|
const chosen = targetId
|
||||||
? tabs.find((t) => t.targetId === targetId)
|
? (() => {
|
||||||
: tabs.at(0);
|
const resolved = resolveTargetIdFromTabs(targetId, tabs);
|
||||||
|
if (!resolved.ok) {
|
||||||
|
if (resolved.reason === "ambiguous") {
|
||||||
|
return "AMBIGUOUS" as const;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return tabs.find((t) => t.targetId === resolved.targetId) ?? null;
|
||||||
|
})()
|
||||||
|
: (tabs.at(0) ?? null);
|
||||||
|
if (chosen === "AMBIGUOUS") {
|
||||||
|
return jsonError(res, 409, "ambiguous target id prefix");
|
||||||
|
}
|
||||||
if (!chosen?.wsUrl) return jsonError(res, 404, "tab not found");
|
if (!chosen?.wsUrl) return jsonError(res, 404, "tab not found");
|
||||||
|
|
||||||
let shot: Buffer<ArrayBufferLike> = Buffer.alloc(0);
|
let shot: Buffer<ArrayBufferLike> = Buffer.alloc(0);
|
||||||
|
|||||||
40
src/browser/target-id.test.ts
Normal file
40
src/browser/target-id.test.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { resolveTargetIdFromTabs } from "./target-id.js";
|
||||||
|
|
||||||
|
describe("browser target id resolution", () => {
|
||||||
|
it("resolves exact ids", () => {
|
||||||
|
const res = resolveTargetIdFromTabs("FULL", [
|
||||||
|
{ targetId: "AAA" },
|
||||||
|
{ targetId: "FULL" },
|
||||||
|
]);
|
||||||
|
expect(res).toEqual({ ok: true, targetId: "FULL" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves unique prefixes (case-insensitive)", () => {
|
||||||
|
const res = resolveTargetIdFromTabs("57a01309", [
|
||||||
|
{ targetId: "57A01309E14B5DEE0FB41F908515A2FC" },
|
||||||
|
]);
|
||||||
|
expect(res).toEqual({
|
||||||
|
ok: true,
|
||||||
|
targetId: "57A01309E14B5DEE0FB41F908515A2FC",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fails on ambiguous prefixes", () => {
|
||||||
|
const res = resolveTargetIdFromTabs("57A0", [
|
||||||
|
{ targetId: "57A01309E14B5DEE0FB41F908515A2FC" },
|
||||||
|
{ targetId: "57A0BEEF000000000000000000000000" },
|
||||||
|
]);
|
||||||
|
expect(res.ok).toBe(false);
|
||||||
|
if (!res.ok) {
|
||||||
|
expect(res.reason).toBe("ambiguous");
|
||||||
|
expect(res.matches?.length).toBe(2);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fails when no tab matches", () => {
|
||||||
|
const res = resolveTargetIdFromTabs("NOPE", [{ targetId: "AAA" }]);
|
||||||
|
expect(res).toEqual({ ok: false, reason: "not_found" });
|
||||||
|
});
|
||||||
|
});
|
||||||
24
src/browser/target-id.ts
Normal file
24
src/browser/target-id.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
export type TargetIdResolution =
|
||||||
|
| { ok: true; targetId: string }
|
||||||
|
| { ok: false; reason: "not_found" | "ambiguous"; matches?: string[] };
|
||||||
|
|
||||||
|
export function resolveTargetIdFromTabs(
|
||||||
|
input: string,
|
||||||
|
tabs: Array<{ targetId: string }>,
|
||||||
|
): TargetIdResolution {
|
||||||
|
const needle = input.trim();
|
||||||
|
if (!needle) return { ok: false, reason: "not_found" };
|
||||||
|
|
||||||
|
const exact = tabs.find((t) => t.targetId === needle);
|
||||||
|
if (exact) return { ok: true, targetId: exact.targetId };
|
||||||
|
|
||||||
|
const lower = needle.toLowerCase();
|
||||||
|
const matches = tabs
|
||||||
|
.map((t) => t.targetId)
|
||||||
|
.filter((id) => id.toLowerCase().startsWith(lower));
|
||||||
|
|
||||||
|
const only = matches.length === 1 ? matches[0] : undefined;
|
||||||
|
if (only) return { ok: true, targetId: only };
|
||||||
|
if (matches.length === 0) return { ok: false, reason: "not_found" };
|
||||||
|
return { ok: false, reason: "ambiguous", matches };
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user