fix(browser): centralize snapshot numeric parsing

This commit is contained in:
Peter Steinberger
2026-05-29 03:15:56 -04:00
parent 0bc591a7d7
commit 4fae13e29e
2 changed files with 81 additions and 17 deletions

View File

@@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest";
import type { ResolvedBrowserProfile } from "../config.js";
import { DEFAULT_AI_SNAPSHOT_MAX_CHARS } from "../constants.js";
import { resolveSnapshotPlan } from "./agent.snapshot.plan.js";
function profile(driver: "existing-session" | "openclaw"): ResolvedBrowserProfile {
@@ -68,6 +69,70 @@ describe("resolveSnapshotPlan", () => {
expect(plan.timeoutMs).toBe(2_147_483_647);
});
it("parses snapshot numeric query options as strict integers", () => {
const plan = resolveSnapshotPlan({
profile: profile("openclaw"),
query: {
limit: "25",
maxChars: "5000",
depth: "2",
timeoutMs: "12345",
},
hasPlaywright: true,
});
expect(plan.limit).toBe(25);
expect(plan.resolvedMaxChars).toBe(5000);
expect(plan.depth).toBe(2);
expect(plan.timeoutMs).toBe(12345);
});
it("accepts structured numeric snapshot query options from proxy dispatch", () => {
const plan = resolveSnapshotPlan({
profile: profile("openclaw"),
query: {
limit: 25,
maxChars: 5000,
depth: 2,
timeoutMs: 12345,
},
hasPlaywright: true,
});
expect(plan.limit).toBe(25);
expect(plan.resolvedMaxChars).toBe(5000);
expect(plan.depth).toBe(2);
expect(plan.timeoutMs).toBe(12345);
});
it("rejects loose snapshot numeric query tokens", () => {
const plan = resolveSnapshotPlan({
profile: profile("openclaw"),
query: {
limit: "0x10",
maxChars: "1.5",
depth: "1e0",
timeoutMs: "1000ms",
},
hasPlaywright: true,
});
expect(plan.limit).toBeUndefined();
expect(plan.resolvedMaxChars).toBe(DEFAULT_AI_SNAPSHOT_MAX_CHARS);
expect(plan.depth).toBeUndefined();
expect(plan.timeoutMs).toBeUndefined();
});
it("keeps maxChars zero as an explicit uncapped snapshot request", () => {
const plan = resolveSnapshotPlan({
profile: profile("openclaw"),
query: { maxChars: "0" },
hasPlaywright: true,
});
expect(plan.resolvedMaxChars).toBeUndefined();
});
it("ignores non-positive timeoutMs values", () => {
expect(
resolveSnapshotPlan({

View File

@@ -1,7 +1,8 @@
import {
normalizeOptionalString,
readStringValue,
} from "openclaw/plugin-sdk/string-coerce-runtime";
parseStrictNonNegativeInteger,
parseStrictPositiveInteger,
} from "openclaw/plugin-sdk/number-runtime";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import type { ResolvedBrowserProfile } from "../config.js";
import {
DEFAULT_AI_SNAPSHOT_EFFICIENT_DEPTH,
@@ -14,7 +15,7 @@ import {
shouldUsePlaywrightForScreenshot,
} from "../profile-capabilities.js";
import { normalizeBrowserTimerDelayMs } from "../timer-delay.js";
import { toBoolean, toNumber, toStringOrEmpty } from "./utils.js";
import { toBoolean, toStringOrEmpty } from "./utils.js";
type BrowserSnapshotPlan = {
format: "ai" | "aria";
@@ -49,25 +50,25 @@ export function resolveSnapshotPlan(params: {
explicitFormat,
mode,
});
const limitRaw = readStringValue(params.query.limit);
const limit = parseStrictPositiveInteger(params.query.limit);
const hasMaxChars = Object.hasOwn(params.query, "maxChars");
const maxCharsRaw = readStringValue(params.query.maxChars);
const limit = Number.isFinite(Number(limitRaw)) ? Number(limitRaw) : undefined;
const maxChars =
Number.isFinite(Number(maxCharsRaw)) && Number(maxCharsRaw) > 0
? Math.floor(Number(maxCharsRaw))
: undefined;
const maxCharsRaw = parseStrictNonNegativeInteger(params.query.maxChars);
const maxChars = maxCharsRaw !== undefined && maxCharsRaw > 0 ? maxCharsRaw : undefined;
const resolvedMaxChars =
format === "ai"
? hasMaxChars
? maxChars
? maxCharsRaw === undefined
? mode === "efficient"
? DEFAULT_AI_SNAPSHOT_EFFICIENT_MAX_CHARS
: DEFAULT_AI_SNAPSHOT_MAX_CHARS
: maxChars
: mode === "efficient"
? DEFAULT_AI_SNAPSHOT_EFFICIENT_MAX_CHARS
: DEFAULT_AI_SNAPSHOT_MAX_CHARS
: undefined;
const interactiveRaw = toBoolean(params.query.interactive);
const compactRaw = toBoolean(params.query.compact);
const depthRaw = toNumber(params.query.depth);
const depthRaw = parseStrictNonNegativeInteger(params.query.depth);
const refsModeRaw = toStringOrEmpty(params.query.refs).trim();
const refsMode: "aria" | "role" | undefined =
refsModeRaw === "aria" ? "aria" : refsModeRaw === "role" ? "role" : undefined;
@@ -77,11 +78,9 @@ export function resolveSnapshotPlan(params: {
depthRaw ?? (mode === "efficient" ? DEFAULT_AI_SNAPSHOT_EFFICIENT_DEPTH : undefined);
const selectorValue = normalizeOptionalString(toStringOrEmpty(params.query.selector));
const frameSelectorValue = normalizeOptionalString(toStringOrEmpty(params.query.frame));
const timeoutMsRaw = toNumber(params.query.timeoutMs);
const timeoutMsRaw = parseStrictPositiveInteger(params.query.timeoutMs);
const timeoutMs =
timeoutMsRaw !== undefined && Number.isFinite(timeoutMsRaw) && timeoutMsRaw > 0
? normalizeBrowserTimerDelayMs(timeoutMsRaw)
: undefined;
timeoutMsRaw !== undefined ? normalizeBrowserTimerDelayMs(timeoutMsRaw) : undefined;
return {
format,