refactor(browser): dedupe storage and download route parsing

This commit is contained in:
Peter Steinberger
2026-02-18 22:18:38 +00:00
parent bb00eb2031
commit ac4ae9ed61
3 changed files with 113 additions and 43 deletions

View File

@@ -18,9 +18,34 @@ import {
resolvePathWithinRoot, resolvePathWithinRoot,
resolvePathsWithinRoot, resolvePathsWithinRoot,
} from "./path-output.js"; } from "./path-output.js";
import type { BrowserRouteRegistrar } from "./types.js"; import type { BrowserResponse, BrowserRouteRegistrar } from "./types.js";
import { jsonError, toBoolean, toNumber, toStringArray, toStringOrEmpty } from "./utils.js"; import { jsonError, toBoolean, toNumber, toStringArray, toStringOrEmpty } from "./utils.js";
function resolveDownloadPathOrRespond(res: BrowserResponse, requestedPath: string): string | null {
const downloadPathResult = resolvePathWithinRoot({
rootDir: DEFAULT_DOWNLOAD_DIR,
requestedPath,
scopeLabel: "downloads directory",
});
if (!downloadPathResult.ok) {
res.status(400).json({ error: downloadPathResult.error });
return null;
}
return downloadPathResult.path;
}
function buildDownloadRequestBase(cdpUrl: string, targetId: string, timeoutMs: number | undefined) {
return {
cdpUrl,
targetId,
timeoutMs: timeoutMs ?? undefined,
};
}
function respondWithDownloadResult(res: BrowserResponse, targetId: string, result: unknown) {
res.json({ ok: true, targetId, download: result });
}
export function registerBrowserAgentActRoutes( export function registerBrowserAgentActRoutes(
app: BrowserRouteRegistrar, app: BrowserRouteRegistrar,
ctx: BrowserRouteContext, ctx: BrowserRouteContext,
@@ -443,24 +468,18 @@ export function registerBrowserAgentActRoutes(
run: async ({ cdpUrl, tab, pw }) => { run: async ({ cdpUrl, tab, pw }) => {
let downloadPath: string | undefined; let downloadPath: string | undefined;
if (out.trim()) { if (out.trim()) {
const downloadPathResult = resolvePathWithinRoot({ const resolvedDownloadPath = resolveDownloadPathOrRespond(res, out);
rootDir: DEFAULT_DOWNLOAD_DIR, if (!resolvedDownloadPath) {
requestedPath: out,
scopeLabel: "downloads directory",
});
if (!downloadPathResult.ok) {
res.status(400).json({ error: downloadPathResult.error });
return; return;
} }
downloadPath = downloadPathResult.path; downloadPath = resolvedDownloadPath;
} }
const requestBase = buildDownloadRequestBase(cdpUrl, tab.targetId, timeoutMs);
const result = await pw.waitForDownloadViaPlaywright({ const result = await pw.waitForDownloadViaPlaywright({
cdpUrl, ...requestBase,
targetId: tab.targetId,
path: downloadPath, path: downloadPath,
timeoutMs: timeoutMs ?? undefined,
}); });
res.json({ ok: true, targetId: tab.targetId, download: result }); respondWithDownloadResult(res, tab.targetId, result);
}, },
}); });
}); });
@@ -485,23 +504,17 @@ export function registerBrowserAgentActRoutes(
targetId, targetId,
feature: "download", feature: "download",
run: async ({ cdpUrl, tab, pw }) => { run: async ({ cdpUrl, tab, pw }) => {
const downloadPathResult = resolvePathWithinRoot({ const downloadPath = resolveDownloadPathOrRespond(res, out);
rootDir: DEFAULT_DOWNLOAD_DIR, if (!downloadPath) {
requestedPath: out,
scopeLabel: "downloads directory",
});
if (!downloadPathResult.ok) {
res.status(400).json({ error: downloadPathResult.error });
return; return;
} }
const requestBase = buildDownloadRequestBase(cdpUrl, tab.targetId, timeoutMs);
const result = await pw.downloadViaPlaywright({ const result = await pw.downloadViaPlaywright({
cdpUrl, ...requestBase,
targetId: tab.targetId,
ref, ref,
path: downloadPathResult.path, path: downloadPath,
timeoutMs: timeoutMs ?? undefined,
}); });
res.json({ ok: true, targetId: tab.targetId, download: result }); respondWithDownloadResult(res, tab.targetId, result);
}, },
}); });
}); });

View File

@@ -1,5 +1,9 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { parseStorageKind, parseStorageMutationRequest } from "./agent.storage.js"; import {
parseRequiredStorageMutationRequest,
parseStorageKind,
parseStorageMutationRequest,
} from "./agent.storage.js";
describe("browser storage route parsing", () => { describe("browser storage route parsing", () => {
describe("parseStorageKind", () => { describe("parseStorageKind", () => {
@@ -37,4 +41,25 @@ describe("browser storage route parsing", () => {
}); });
}); });
}); });
describe("parseRequiredStorageMutationRequest", () => {
it("returns parsed request for supported kinds", () => {
expect(
parseRequiredStorageMutationRequest("session", {
targetId: " tab-9 ",
}),
).toEqual({
kind: "session",
targetId: "tab-9",
});
});
it("returns null for unsupported kind", () => {
expect(
parseRequiredStorageMutationRequest("cookie", {
targetId: "tab-1",
}),
).toBeNull();
});
});
}); });

View File

@@ -5,7 +5,7 @@ import {
resolveTargetIdFromQuery, resolveTargetIdFromQuery,
withPlaywrightRouteContext, withPlaywrightRouteContext,
} from "./agent.shared.js"; } from "./agent.shared.js";
import type { BrowserRouteRegistrar } from "./types.js"; import type { BrowserRequest, BrowserResponse, BrowserRouteRegistrar } from "./types.js";
import { jsonError, toBoolean, toNumber, toStringOrEmpty } from "./utils.js"; import { jsonError, toBoolean, toNumber, toStringOrEmpty } from "./utils.js";
type StorageKind = "local" | "session"; type StorageKind = "local" | "session";
@@ -27,6 +27,42 @@ export function parseStorageMutationRequest(
}; };
} }
export function parseRequiredStorageMutationRequest(
kindParam: unknown,
body: Record<string, unknown>,
): { kind: StorageKind; targetId: string | undefined } | null {
const parsed = parseStorageMutationRequest(kindParam, body);
if (!parsed.kind) {
return null;
}
return {
kind: parsed.kind,
targetId: parsed.targetId,
};
}
function parseStorageMutationOrRespond(
res: BrowserResponse,
kindParam: unknown,
body: Record<string, unknown>,
) {
const parsed = parseRequiredStorageMutationRequest(kindParam, body);
if (!parsed) {
jsonError(res, 400, "kind must be local|session");
return null;
}
return parsed;
}
function parseStorageMutationFromRequest(req: BrowserRequest, res: BrowserResponse) {
const body = readBody(req);
const parsed = parseStorageMutationOrRespond(res, req.params.kind, body);
if (!parsed) {
return null;
}
return { body, parsed };
}
export function registerBrowserAgentStorageRoutes( export function registerBrowserAgentStorageRoutes(
app: BrowserRouteRegistrar, app: BrowserRouteRegistrar,
ctx: BrowserRouteContext, ctx: BrowserRouteContext,
@@ -139,29 +175,27 @@ export function registerBrowserAgentStorageRoutes(
}); });
app.post("/storage/:kind/set", async (req, res) => { app.post("/storage/:kind/set", async (req, res) => {
const body = readBody(req); const mutation = parseStorageMutationFromRequest(req, res);
const parsed = parseStorageMutationRequest(req.params.kind, body); if (!mutation) {
if (!parsed.kind) { return;
return jsonError(res, 400, "kind must be local|session");
} }
const kind = parsed.kind; const key = toStringOrEmpty(mutation.body.key);
const key = toStringOrEmpty(body.key);
if (!key) { if (!key) {
return jsonError(res, 400, "key is required"); return jsonError(res, 400, "key is required");
} }
const value = typeof body.value === "string" ? body.value : ""; const value = typeof mutation.body.value === "string" ? mutation.body.value : "";
await withPlaywrightRouteContext({ await withPlaywrightRouteContext({
req, req,
res, res,
ctx, ctx,
targetId: parsed.targetId, targetId: mutation.parsed.targetId,
feature: "storage set", feature: "storage set",
run: async ({ cdpUrl, tab, pw }) => { run: async ({ cdpUrl, tab, pw }) => {
await pw.storageSetViaPlaywright({ await pw.storageSetViaPlaywright({
cdpUrl, cdpUrl,
targetId: tab.targetId, targetId: tab.targetId,
kind, kind: mutation.parsed.kind,
key, key,
value, value,
}); });
@@ -171,24 +205,22 @@ export function registerBrowserAgentStorageRoutes(
}); });
app.post("/storage/:kind/clear", async (req, res) => { app.post("/storage/:kind/clear", async (req, res) => {
const body = readBody(req); const mutation = parseStorageMutationFromRequest(req, res);
const parsed = parseStorageMutationRequest(req.params.kind, body); if (!mutation) {
if (!parsed.kind) { return;
return jsonError(res, 400, "kind must be local|session");
} }
const kind = parsed.kind;
await withPlaywrightRouteContext({ await withPlaywrightRouteContext({
req, req,
res, res,
ctx, ctx,
targetId: parsed.targetId, targetId: mutation.parsed.targetId,
feature: "storage clear", feature: "storage clear",
run: async ({ cdpUrl, tab, pw }) => { run: async ({ cdpUrl, tab, pw }) => {
await pw.storageClearViaPlaywright({ await pw.storageClearViaPlaywright({
cdpUrl, cdpUrl,
targetId: tab.targetId, targetId: tab.targetId,
kind, kind: mutation.parsed.kind,
}); });
res.json({ ok: true, targetId: tab.targetId }); res.json({ ok: true, targetId: tab.targetId });
}, },