fix(browser): accept targetId prefixes
This commit is contained in:
@@ -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);
|
||||
|
||||
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