Merge remote-tracking branch 'origin/main' into codex/audit-continuation-20261020-refresh20

This commit is contained in:
morluto committed 2026-10-20 22:39:34 +08:00
commit d2a3269d0c
5 files changed
+169 -23

No files matched your search

+5
View File
@@ -107,6 +107,11 @@ Through MCP, pass the same object as the `capture_browser_scenario` arguments.
The final step's DOM and accessibility artifacts retain the page state at that
capture time inline. REA still launches and cleans up the browser profile.
Action `timeout_ms` and `wait_for_timeout.duration_ms` accept integers from 1
through 2,147,483,647 milliseconds, matching the underlying timer representation.
Larger values receive an input error before browser launch. Omit `timeout_ms`
to keep an action pending until completion or request cancellation.
The click's `timeout_ms` covers the click action. A non-navigating `fetch` or
delayed render can finish later, so use `wait_for` to observe a page-specific
ready condition before final capture. A page that updates synchronously can
+57
View File
@@ -196,3 +196,60 @@ it("accepts caller-selected viewport sizes and click counts without ceilings", (
});
expect(parsed.actions[0]).toMatchObject({ click_count: 4 });
});
it.each([
{
field: "duration_ms",
action: { action: "wait_for_timeout", duration_ms: 2_147_483_648 },
},
{
field: "timeout_ms",
action: {
action: "wait_for",
locator: { kind: "css", selector: "#btn" },
state: "visible",
timeout_ms: 2_147_483_648,
},
},
])("rejects $field at its precise owning field", ({ field, action }) => {
const parsed = browserScenarioSchema.safeParse({
browser: { mode: "launch", executable_path: "/opt/chromium" },
start_url: { url: "https://app.example.test/" },
actions: [{ step_id: "overflow", ...action }],
});
if (parsed.success)
throw new Error("Expected unrepresentable timer rejection");
expect(parsed.error.issues).toMatchObject([
{ code: "too_big", maximum: 2_147_483_647, path: ["actions", 0, field] },
]);
});
it("preserves the largest representable timers and an omitted action deadline", () => {
const parsed = browserScenarioSchema.parse({
browser: { mode: "launch", executable_path: "/opt/chromium" },
start_url: { url: "https://app.example.test/" },
actions: [
{
step_id: "max-wait",
action: "wait_for_timeout",
duration_ms: 2_147_483_647,
},
{
step_id: "max-deadline",
action: "wait_for",
locator: { kind: "css", selector: "#btn" },
state: "visible",
timeout_ms: 2_147_483_647,
},
{
step_id: "unbounded",
action: "wait_for",
locator: { kind: "css", selector: "#btn" },
state: "visible",
},
],
});
expect(parsed.actions[0]).toMatchObject({ duration_ms: 2_147_483_647 });
expect(parsed.actions[1]).toMatchObject({ timeout_ms: 2_147_483_647 });
expect(parsed.actions[2]).not.toHaveProperty("timeout_ms");
});
+5
View File
@@ -157,12 +157,16 @@ const locatorSchema = z.discriminatedUnion("kind", [
}),
]);
// Playwright forwards these intervals to Node timers.
const MAX_BROWSER_SCENARIO_TIMEOUT_MS = 2_147_483_647;
const stepBase = {
step_id: scenarioIdentifierSchema,
timeout_ms: z
.number()
.int()
.min(1)
.max(MAX_BROWSER_SCENARIO_TIMEOUT_MS)
.optional()
.describe(
"Optional action deadline in milliseconds; omitted means wait until completion or request cancellation.",
@@ -219,6 +223,7 @@ export const browserScenarioActionSchema = z.discriminatedUnion("action", [
.number()
.int()
.min(1)
.max(MAX_BROWSER_SCENARIO_TIMEOUT_MS)
.describe(
"Wait this many milliseconds; the wait ends early on cancellation.",
),
@@ -50,15 +50,21 @@ const runCli = async (arguments_: readonly string[]) => {
};
const expectMissingInputFailureText = (text: string, path: string): void => {
const normalizedText = text.replace(/\s+/g, " ");
for (const value of [
"invalid_request",
"invalid_input",
"Correct the listed arguments and retry.",
"Read the declared input schema or CLI usage",
"trace-application-feature",
"tools/list",
"Correct the listed fields, then retry.",
"Do not repeat the unchanged call.",
"inspect_input_contract",
path,
"read-failed",
"ENOENT",
])
expect(text).toContain(value);
expect(normalizedText).toContain(value);
};
it.each(["json", "toon", "yaml", "md"] as const)(
@@ -81,13 +87,22 @@ it.each(["json", "toon", "yaml", "md"] as const)(
expect(result.output.failed).toBe(false);
expect(result.fallback).toBe("");
expect(result.output.handled).toBe(true);
if (format === "json") {
expect(JSON.parse(result.text)).toMatchObject({
expectMissingInputFailureText(result.text, missingPath);
if (format === "json" || format === "yaml") {
const document =
format === "json"
? JSON.parse(result.text)
: parseYaml(result.text);
expect(document).toMatchObject({
code: "invalid_request",
category: "invalid_input",
retryable: true,
remediation: {
action: "Correct the listed arguments and retry.",
action: expect.stringContaining("tools/list"),
},
recovery: {
kind: "inspect_input_contract",
operation: "trace-application-feature",
},
details: {
input_path: missingPath,
@@ -99,7 +114,7 @@ it.each(["json", "toon", "yaml", "md"] as const)(
],
},
});
} else expectMissingInputFailureText(result.text, missingPath);
}
} finally {
process.exitCode = previousExitCode;
}
@@ -127,6 +142,13 @@ it.each(["json", "toon", "yaml", "md"] as const)(
code: "invalid_request",
category: "invalid_input",
retryable: true,
remediation: {
action: expect.stringContaining("tools/list"),
},
recovery: {
kind: "inspect_input_contract",
operation: "trace-application-feature",
},
details: {
input_path: missingPath,
input_reason: "read-failed",
@@ -12,8 +12,12 @@ import { cliTest } from "../../support/cli/cliFixture.js";
const scenarios = [
{
name: "undeclared secret references",
issuePath: ["secrets"],
issueMessage: "Secret reference pw is not declared",
fullSchemaValid: true,
issue: {
path: ["secrets"],
reason: "invalid_value",
message: "Secret reference pw is not declared",
},
actions: [
{
step_id: "pw",
@@ -25,18 +29,57 @@ const scenarios = [
},
{
name: "duplicate action step IDs",
issuePath: ["actions"],
issueMessage: "Action step IDs must be unique",
fullSchemaValid: true,
issue: {
path: ["actions"],
reason: "invalid_value",
message: "Action step IDs must be unique",
},
actions: [
{ step_id: "wait", action: "wait_for_timeout", duration_ms: 1 },
{ step_id: "wait", action: "wait_for_timeout", duration_ms: 1 },
],
},
{
name: "overflowing wait duration",
fullSchemaValid: false,
issue: {
path: ["actions", 0, "duration_ms"],
reason: "out_of_range",
maximum: 2_147_483_647,
},
actions: [
{
step_id: "wait",
action: "wait_for_timeout",
duration_ms: 2_147_483_648,
},
],
},
{
name: "overflowing action deadline",
fullSchemaValid: false,
issue: {
path: ["actions", 0, "timeout_ms"],
reason: "out_of_range",
maximum: 2_147_483_647,
},
actions: [
{
step_id: "visible",
action: "wait_for",
locator: { kind: "css", selector: "#btn" },
state: "visible",
timeout_ms: 2_147_483_648,
},
],
},
] as const;
cliTest(
"projects refined browser scenario validation into CLI-equivalent diagnostics over stdio MCP",
async ({ cli }) => {
cliTest.for(["full", "compact"] as const)(
"projects browser scenario input diagnostics over stdio MCP (%s)",
{ timeout: 60_000 },
async (profile, { cli }) => {
const home = await createTestTempDirectory(
"rea-browser-scenario-mcp-home-",
);
@@ -52,6 +95,7 @@ cliTest(
PATH: process.env.PATH ?? "",
HOME: home,
REA_LOG_LEVEL: "silent",
REA_MCP_INPUT_SCHEMA_PROFILE: profile,
},
stderr: "pipe",
});
@@ -81,7 +125,7 @@ cliTest(
expect(
validate(input),
`${scenario.name}: ${JSON.stringify(validate.errors)}`,
).toBe(true);
).toBe(scenario.fullSchemaValid || profile === "compact");
const cliResult = await cli.run({
arguments: [
@@ -111,13 +155,7 @@ cliTest(
},
details: {
operation: "capture_browser_scenario",
issues: [
{
path: scenario.issuePath,
reason: "invalid_value",
message: scenario.issueMessage,
},
],
issues: [scenario.issue],
},
});
@@ -126,12 +164,31 @@ cliTest(
arguments: input,
});
expect(mcpResult.structuredContent).toBeUndefined();
expect(parseMcpToolError(mcpResult).error).toEqual(cliDiagnostic);
const mcpError = parseMcpToolError(mcpResult).error;
if (scenario.fullSchemaValid) expect(mcpError).toEqual(cliDiagnostic);
else {
// The SDK validates the canonical schema before invoking the handler.
expect(mcpError).toMatchObject({
code: "invalid_request",
category: "invalid_input",
retryable: true,
recovery: { kind: "inspect_input_contract", operation: tool.name },
details: {
operation: tool.name,
issues: [
{
message: expect.stringContaining(
`${scenario.issue.path.join(".")}: Too big: expected number to be <=2147483647`,
),
},
],
},
});
}
await expect(client.ping()).resolves.toEqual({});
}
} finally {
await Promise.all([client.close(), transport.close()]);
}
},
60_000,
);