fix(browser): accept targetId prefixes

This commit is contained in:
Peter Steinberger
2025-12-13 18:16:47 +00:00
parent 2a71c20ee4
commit 238afbc2f8
3 changed files with 97 additions and 4 deletions

View File

@@ -26,6 +26,7 @@ import {
DEFAULT_BROWSER_SCREENSHOT_MAX_SIDE,
normalizeBrowserScreenshot,
} from "./screenshot.js";
import { resolveTargetIdFromTabs } from "./target-id.js";
export type BrowserTab = {
targetId: string;
@@ -270,7 +271,15 @@ export async function startBrowserControlServerFromConfig(
const reachable = await isChromeReachable(state.cdpPort, 300);
if (!reachable) return jsonError(res, 409, "browser not running");
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 });
} catch (err) {
jsonError(res, 500, String(err));
@@ -284,7 +293,15 @@ export async function startBrowserControlServerFromConfig(
const reachable = await isChromeReachable(state.cdpPort, 300);
if (!reachable) return jsonError(res, 409, "browser not running");
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 });
} catch (err) {
jsonError(res, 500, String(err));
@@ -304,8 +321,20 @@ export async function startBrowserControlServerFromConfig(
try {
const tabs = await listTabs(state.cdpPort);
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");
let shot: Buffer<ArrayBufferLike> = Buffer.alloc(0);

View 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
View 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 };
}