mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(mcp): normalize browser scenario input errors (#4661)
Co-authored-by: gongfpp <22683417+gongfpp@users.noreply.github.com>
This commit is contained in:
1 parent
9534e42ec8
commit
5a7e8a166a
3 files changed
+149
-5
No files matched your search
@@ -6,11 +6,10 @@ import { withCommandCancellation } from "./cli/commandCancellation.js";
|
||||
import { CLI_COMMANDS } from "./cliCommandNames.js";
|
||||
import { parseCliJsonInput, resolveCliJsonPaths } from "./cliJsonInput.js";
|
||||
import { logCliCommand } from "./cliLogging.js";
|
||||
import { AnalysisInputError } from "./domain/analysisErrorCore.js";
|
||||
import { AnalysisError } from "./domain/analysisErrorBase.js";
|
||||
import { projectAnalysisError } from "./domain/analysisErrorProjection.js";
|
||||
import { BrowserObservationError } from "./domain/browserObservationError.js";
|
||||
import { projectInputIssues } from "./domain/inputIssueProjection.js";
|
||||
import { analysisInputErrorFromIssues } from "./domain/inputIssueProjection.js";
|
||||
import { browserScenarioSchema } from "./domain/browserScenario.js";
|
||||
import type { JsonValue } from "./domain/jsonValue.js";
|
||||
import type { Logger } from "pino";
|
||||
@@ -41,10 +40,11 @@ export const registerBrowserScenarioCommands = (
|
||||
);
|
||||
if (!scenario.success)
|
||||
return cliError(
|
||||
new AnalysisInputError(
|
||||
analysisInputErrorFromIssues(
|
||||
OPERATION,
|
||||
scenario.error.issues,
|
||||
input.value,
|
||||
{ cause: scenario.error },
|
||||
projectInputIssues(scenario.error.issues, input.value),
|
||||
),
|
||||
);
|
||||
// Load Playwright only for this command, not during CLI startup.
|
||||
|
||||
@@ -9,6 +9,7 @@ import { err } from "../domain/result.js";
|
||||
import type { BrowserScenarioCapturePort } from "../application/BrowserScenarioCapturePort.js";
|
||||
import { captureBrowserScenario } from "../application/BrowserScenarioCaptureService.js";
|
||||
import { toolContract } from "../contracts/toolContracts.js";
|
||||
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
|
||||
import { browserScenarioSchema } from "../domain/browserScenario.js";
|
||||
import type { Logger } from "pino";
|
||||
import { logToolExecution } from "./toolLogging.js";
|
||||
@@ -35,7 +36,20 @@ export const registerBrowserScenarioTool = (
|
||||
contract.name,
|
||||
toolRegistrationOptions(contract),
|
||||
async (input, context) => {
|
||||
const scenario = browserScenarioSchema.parse(input);
|
||||
const parsedScenario = browserScenarioSchema.safeParse(input);
|
||||
if (!parsedScenario.success)
|
||||
return server.delivery.toCallToolResult(
|
||||
err(
|
||||
analysisInputErrorFromIssues(
|
||||
contract.name,
|
||||
parsedScenario.error.issues,
|
||||
input,
|
||||
{ cause: parsedScenario.error },
|
||||
),
|
||||
),
|
||||
contract,
|
||||
);
|
||||
const scenario = parsedScenario.data;
|
||||
if (options.loadFailure !== undefined)
|
||||
return server.delivery.toCallToolResult(
|
||||
err(optionalProviderUnavailable(options.loadFailure, contract.name)),
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
import { join, resolve } from "node:path";
|
||||
import { expect } from "vitest";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
|
||||
const scenarios = [
|
||||
{
|
||||
name: "undeclared secret references",
|
||||
issuePath: ["secrets"],
|
||||
issueMessage: "Secret reference pw is not declared",
|
||||
actions: [
|
||||
{
|
||||
step_id: "pw",
|
||||
action: "fill",
|
||||
locator: { kind: "css", selector: "#pw" },
|
||||
value: { source: "secret", secret_id: "pw" },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "duplicate action step IDs",
|
||||
issuePath: ["actions"],
|
||||
issueMessage: "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 },
|
||||
],
|
||||
},
|
||||
] as const;
|
||||
|
||||
cliTest(
|
||||
"projects refined browser scenario validation into CLI-equivalent diagnostics over stdio MCP",
|
||||
async ({ cli }) => {
|
||||
const home = await createTestTempDirectory(
|
||||
"rea-browser-scenario-mcp-home-",
|
||||
);
|
||||
const client = new Client({
|
||||
name: "browser-scenario-input-diagnostics",
|
||||
version: "1",
|
||||
});
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [resolve("scripts/rea.mjs"), "mcp"],
|
||||
cwd: process.cwd(),
|
||||
env: {
|
||||
PATH: process.env.PATH ?? "",
|
||||
HOME: home,
|
||||
REA_LOG_LEVEL: "silent",
|
||||
},
|
||||
stderr: "pipe",
|
||||
});
|
||||
transport.stderr?.on("data", () => undefined);
|
||||
|
||||
try {
|
||||
await client.connect(transport);
|
||||
const tool = (await client.listTools()).tools.find(
|
||||
({ name }) => name === "capture_browser_scenario",
|
||||
);
|
||||
if (tool === undefined) throw new Error("Missing browser scenario tool");
|
||||
const validate = new Ajv2020({
|
||||
strict: false,
|
||||
validateFormats: false,
|
||||
}).compile(tool.inputSchema);
|
||||
|
||||
for (const scenario of scenarios) {
|
||||
const input = {
|
||||
browser: {
|
||||
mode: "launch",
|
||||
executable_path: join(home, "missing-chromium"),
|
||||
},
|
||||
start_url: { url: "http://127.0.0.1:1/", query: [] },
|
||||
actions: scenario.actions,
|
||||
secrets: [],
|
||||
};
|
||||
expect(
|
||||
validate(input),
|
||||
`${scenario.name}: ${JSON.stringify(validate.errors)}`,
|
||||
).toBe(true);
|
||||
|
||||
const cliResult = await cli.run({
|
||||
arguments: [
|
||||
"capture-browser-scenario",
|
||||
JSON.stringify(input),
|
||||
"--json",
|
||||
],
|
||||
environment: { HOME: home, REA_LOG_LEVEL: "silent" },
|
||||
});
|
||||
expect(cliResult.exitCode).toBe(1);
|
||||
const cliDiagnostic = z
|
||||
.record(z.string(), z.unknown())
|
||||
.parse(cliResult.json);
|
||||
expect(typeof cliDiagnostic.error).toBe("string");
|
||||
delete cliDiagnostic.error;
|
||||
expect(cliDiagnostic).toMatchObject({
|
||||
code: "invalid_request",
|
||||
category: "invalid_input",
|
||||
retryable: true,
|
||||
remediation: { action: "Correct the listed arguments and retry." },
|
||||
details: {
|
||||
operation: "capture_browser_scenario",
|
||||
issues: [
|
||||
{
|
||||
path: scenario.issuePath,
|
||||
reason: "invalid_value",
|
||||
message: scenario.issueMessage,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const mcpResult = await client.callTool({
|
||||
name: tool.name,
|
||||
arguments: input,
|
||||
});
|
||||
expect(mcpResult.structuredContent).toBeUndefined();
|
||||
expect(parseMcpToolError(mcpResult).error).toEqual(cliDiagnostic);
|
||||
await expect(client.ping()).resolves.toEqual({});
|
||||
}
|
||||
} finally {
|
||||
await Promise.all([client.close(), transport.close()]);
|
||||
}
|
||||
},
|
||||
60_000,
|
||||
);
|
||||
Reference in new issue
Block a user