mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 05:56:19 -10:00
fix(cli): reject empty observation duration selections (#7710)
This commit is contained in:
1 parent
c36971953c
commit
fbb0e79354
3 files changed
+148
-5
No files matched your search
@@ -0,0 +1,49 @@
|
||||
import { Parser, Schema, z } from "incur";
|
||||
import { expect, it } from "vitest";
|
||||
import { observationDuration } from "./cliObservationOptions.js";
|
||||
|
||||
const options = z.object({ observationMs: observationDuration(500) });
|
||||
|
||||
it.each(["", " ", "\t"])(
|
||||
"rejects an empty selected duration before SDK numeric coercion (%j)",
|
||||
(value) => {
|
||||
for (const argv of [
|
||||
[`--observation-ms=${value}`],
|
||||
["--observation-ms", value],
|
||||
])
|
||||
expect(() => Parser.parse(argv, { options })).toThrow();
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ argv: [], expected: 500 },
|
||||
{ argv: ["--observation-ms=0"], expected: 0 },
|
||||
{ argv: ["--observation-ms", "0x10"], expected: 16 },
|
||||
{ argv: ["--observation-ms", "1e3"], expected: 1000 },
|
||||
{ argv: ["--observation-ms", " 16 "], expected: 16 },
|
||||
])("preserves caller selection and omission ($argv)", ({ argv, expected }) => {
|
||||
expect(Parser.parse(argv, { options }).options.observationMs).toBe(expected);
|
||||
});
|
||||
|
||||
it.each(["-1", "0.5", "NaN", "Infinity"])(
|
||||
"keeps integer and range validation (%s)",
|
||||
(value) => {
|
||||
expect(() =>
|
||||
Parser.parse(["--observation-ms", value], { options }),
|
||||
).toThrow();
|
||||
},
|
||||
);
|
||||
|
||||
it("advertises numeric bounds, a nonblank text branch and a valid numeric default", () => {
|
||||
expect(Schema.toJsonSchema(options)).toMatchObject({
|
||||
properties: {
|
||||
observationMs: {
|
||||
default: 500,
|
||||
anyOf: [
|
||||
{ type: "integer", minimum: 0 },
|
||||
{ type: "string", pattern: "\\S" },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -5,13 +5,28 @@ import { javaScriptAnalysisResourceControlsSchema } from "./domain/javascript/ja
|
||||
import { javaScriptSourcePathsSchema } from "./domain/javascript/javascriptSourceSelection.js";
|
||||
|
||||
/** Accept an agent-selected browser observation duration without an artificial ceiling. */
|
||||
export const observationDuration = (fallback: number, minimum = 0) =>
|
||||
z
|
||||
.number()
|
||||
.int()
|
||||
.min(minimum)
|
||||
export const observationDuration = (fallback: number, minimum = 0) => {
|
||||
const duration = z.number().int().min(minimum);
|
||||
// Keep argv text until its presence is checked. Incur eagerly converts a
|
||||
// ZodNumber field with Number(), which turns empty/blank selections into 0.
|
||||
const selected = z
|
||||
.union([
|
||||
duration,
|
||||
z
|
||||
.string()
|
||||
.regex(/\S/u, "Expected a nonempty observation duration")
|
||||
.pipe(z.coerce.number<string>())
|
||||
.pipe(duration),
|
||||
])
|
||||
.default(fallback)
|
||||
.describe("Observation duration in milliseconds");
|
||||
// Incur advertises Zod's output view for CLI inputs. Publish this field's
|
||||
// input view so the lexical guard survives the SDK conversion as well.
|
||||
const { $schema: _dialect, ...inputSchema } = z.toJSONSchema(selected, {
|
||||
io: "input",
|
||||
});
|
||||
return selected.meta(inputSchema);
|
||||
};
|
||||
|
||||
/** Shared passive browser origin options. */
|
||||
export const browserScopeOptions = {
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { expect } from "vitest";
|
||||
import { startFakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
|
||||
cliTest(
|
||||
"rejects empty observation selections before connecting to a browser",
|
||||
async ({ cli }) => {
|
||||
for (const selection of [["--observation-ms="], ["--observation-ms", ""]]) {
|
||||
const result = await cli.run({
|
||||
arguments: [
|
||||
"inspect-web-page",
|
||||
"http://127.0.0.1:9",
|
||||
"page",
|
||||
...selection,
|
||||
"--json",
|
||||
],
|
||||
});
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.json).toMatchObject({
|
||||
code: "VALIDATION_ERROR",
|
||||
fieldErrors: expect.arrayContaining([
|
||||
expect.objectContaining({ path: "observationMs" }),
|
||||
]),
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
cliTest(
|
||||
"preserves the default capture window and explicit zero through CDP",
|
||||
async ({ cli }) => {
|
||||
const browser = await startFakeCdpBrowser();
|
||||
try {
|
||||
for (const [selection, expected] of [
|
||||
[[], 500],
|
||||
[["--observation-ms=0"], 0],
|
||||
] as const) {
|
||||
const result = await cli.run({
|
||||
arguments: [
|
||||
"inspect-web-page",
|
||||
browser.endpoint,
|
||||
"allowed-page",
|
||||
...selection,
|
||||
"--json",
|
||||
],
|
||||
});
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
expect(result.json).toMatchObject({
|
||||
normalized_result: { capture_window: { observation_ms: expected } },
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
cliTest(
|
||||
"advertises the lexical guard after SDK conversion",
|
||||
async ({ cli }) => {
|
||||
const result = await cli.run({
|
||||
arguments: ["inspect-web-page", "--schema", "--json"],
|
||||
});
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.json).toMatchObject({
|
||||
options: {
|
||||
properties: {
|
||||
observationMs: {
|
||||
default: 500,
|
||||
anyOf: [
|
||||
{ type: "integer", minimum: 0 },
|
||||
{ type: "string", pattern: "\\S" },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user