fix(cli): refuse scenario steps on passive native UI observation (#2712)
observe-native-ui and capture-native-ui-scenario were registered in one
loop with a shared option schema and description. The passive command
therefore advertised `--steps` ("Observe or run a scenario"), accepted
any value, and then dispatched observe_native_ui without it, so a
caller who selected the wrong command never learned that no scenario ran.
Register each command against its own contract. Observation declares
only the passive scope options, so `--steps` is rejected as an unknown
flag; the scenario command keeps its JSON step decoding and validation.
Each command now has its own description.
Fixes #2674
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
beca34f522
commit
96d1f0f3fb
|
|
@ -32,89 +32,116 @@ export const registerUtilityCommands = (
|
|||
registerCapabilityCommands(cli, logger, analysis);
|
||||
registerNativeCommands(cli, logger, runProviderAnalysis);
|
||||
registerNativeCallCommands(cli, logger, runProviderAnalysis);
|
||||
for (const [command, operation] of [
|
||||
[CLI_COMMANDS.observeNativeUi, "observe_native_ui"],
|
||||
[CLI_COMMANDS.captureNativeUiScenario, "capture_native_ui_scenario"],
|
||||
] as const) {
|
||||
cli.command(command, {
|
||||
description:
|
||||
"Observe or run a scenario in one exact native application window",
|
||||
args: z.object({
|
||||
path: z.string().describe("Local native executable or app path"),
|
||||
}),
|
||||
options: z.object({
|
||||
pid: z.number().int().positive().describe("Existing application PID"),
|
||||
windowId: z
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.describe("Exact selected application window ID"),
|
||||
steps: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("JSON array of declarative AX actions or waits"),
|
||||
screenshot: z
|
||||
.boolean()
|
||||
.default(true)
|
||||
.describe("Capture only the selected window"),
|
||||
accessibility: z
|
||||
.boolean()
|
||||
.default(true)
|
||||
.describe("Read the selected window accessibility tree"),
|
||||
maxNodes: z
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.safe()
|
||||
.default(500)
|
||||
.describe("Maximum accessibility nodes per capture"),
|
||||
}),
|
||||
run: ({ args, options }) =>
|
||||
logCliCommand(logger, command, async () => {
|
||||
const parameters = {
|
||||
pid: options.pid,
|
||||
window_id: options.windowId,
|
||||
screenshot: options.screenshot,
|
||||
accessibility: options.accessibility,
|
||||
max_nodes: options.maxNodes,
|
||||
};
|
||||
if (operation === "capture_native_ui_scenario") {
|
||||
const decoded =
|
||||
options.steps === undefined
|
||||
? { ok: true as const, value: undefined }
|
||||
: safeParseJson(options.steps);
|
||||
if (!decoded.ok)
|
||||
return invalidNativeUiScenarioInput(operation, [
|
||||
{ path: ["steps"], reason: "invalid_format", expected: "JSON" },
|
||||
]);
|
||||
const parsed = nativeUiScenarioInputSchema.safeParse({
|
||||
...parameters,
|
||||
steps: decoded.value,
|
||||
});
|
||||
if (!parsed.success)
|
||||
return invalidNativeUiScenarioInput(
|
||||
operation,
|
||||
projectInputIssues(parsed.error.issues, {
|
||||
...parameters,
|
||||
...(options.steps === undefined
|
||||
? {}
|
||||
: { steps: decoded.value }),
|
||||
}),
|
||||
);
|
||||
return runProviderAnalysis(
|
||||
args.path,
|
||||
operation,
|
||||
parsed.data,
|
||||
logger,
|
||||
);
|
||||
}
|
||||
return runProviderAnalysis(args.path, operation, parameters, logger);
|
||||
}),
|
||||
});
|
||||
}
|
||||
registerNativeUiCommands(cli, logger, runProviderAnalysis);
|
||||
registerReferenceSourceCommand(cli, logger, environment);
|
||||
};
|
||||
|
||||
const nativeUiScopeOptions = {
|
||||
pid: z.number().int().positive().describe("Existing application PID"),
|
||||
windowId: z
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.describe("Exact selected application window ID"),
|
||||
screenshot: z
|
||||
.boolean()
|
||||
.default(true)
|
||||
.describe("Capture only the selected window"),
|
||||
accessibility: z
|
||||
.boolean()
|
||||
.default(true)
|
||||
.describe("Read the selected window accessibility tree"),
|
||||
maxNodes: z
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.safe()
|
||||
.default(500)
|
||||
.describe("Maximum accessibility nodes per capture"),
|
||||
};
|
||||
|
||||
const nativeUiScope = (options: {
|
||||
readonly pid: number;
|
||||
readonly windowId: number;
|
||||
readonly screenshot: boolean;
|
||||
readonly accessibility: boolean;
|
||||
readonly maxNodes: number;
|
||||
}) => ({
|
||||
pid: options.pid,
|
||||
window_id: options.windowId,
|
||||
screenshot: options.screenshot,
|
||||
accessibility: options.accessibility,
|
||||
max_nodes: options.maxNodes,
|
||||
});
|
||||
|
||||
/**
|
||||
* Passive observation and active scenarios have different input contracts.
|
||||
* Only the scenario command accepts steps, so passive observation rejects
|
||||
* them as an unknown flag instead of silently dropping them.
|
||||
*/
|
||||
const registerNativeUiCommands = (
|
||||
cli: CliInstance,
|
||||
logger: Logger,
|
||||
runProviderAnalysis: DirectAnalysis["runProviderAnalysis"],
|
||||
): void => {
|
||||
const pathArgument = z.object({
|
||||
path: z.string().describe("Local native executable or app path"),
|
||||
});
|
||||
cli.command(CLI_COMMANDS.observeNativeUi, {
|
||||
description:
|
||||
"Observe one already-running native application window by PID and window ID",
|
||||
args: pathArgument,
|
||||
options: z.object(nativeUiScopeOptions),
|
||||
run: ({ args, options }) =>
|
||||
logCliCommand(logger, CLI_COMMANDS.observeNativeUi, () =>
|
||||
runProviderAnalysis(
|
||||
args.path,
|
||||
"observe_native_ui",
|
||||
nativeUiScope(options),
|
||||
logger,
|
||||
),
|
||||
),
|
||||
});
|
||||
const operation = "capture_native_ui_scenario";
|
||||
cli.command(CLI_COMMANDS.captureNativeUiScenario, {
|
||||
description:
|
||||
"Run declarative AX actions or waits in one exact native application window",
|
||||
args: pathArgument,
|
||||
options: z.object({
|
||||
...nativeUiScopeOptions,
|
||||
steps: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("JSON array of declarative AX actions or waits"),
|
||||
}),
|
||||
run: ({ args, options }) =>
|
||||
logCliCommand(logger, CLI_COMMANDS.captureNativeUiScenario, async () => {
|
||||
const parameters = nativeUiScope(options);
|
||||
const decoded =
|
||||
options.steps === undefined
|
||||
? { ok: true as const, value: undefined }
|
||||
: safeParseJson(options.steps);
|
||||
if (!decoded.ok)
|
||||
return invalidNativeUiScenarioInput(operation, [
|
||||
{ path: ["steps"], reason: "invalid_format", expected: "JSON" },
|
||||
]);
|
||||
const parsed = nativeUiScenarioInputSchema.safeParse({
|
||||
...parameters,
|
||||
steps: decoded.value,
|
||||
});
|
||||
if (!parsed.success)
|
||||
return invalidNativeUiScenarioInput(
|
||||
operation,
|
||||
projectInputIssues(parsed.error.issues, {
|
||||
...parameters,
|
||||
...(options.steps === undefined ? {} : { steps: decoded.value }),
|
||||
}),
|
||||
);
|
||||
return runProviderAnalysis(args.path, operation, parsed.data, logger);
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
const invalidNativeUiScenarioInput = (
|
||||
operation: string,
|
||||
issues: ConstructorParameters<typeof AnalysisInputError>[2],
|
||||
|
|
|
|||
|
|
@ -71,4 +71,27 @@ describe("native UI scenario CLI input", () => {
|
|||
});
|
||||
},
|
||||
);
|
||||
|
||||
cliTest(
|
||||
"refuses scenario steps on passive observation instead of dropping them",
|
||||
async ({ cli }) => {
|
||||
const result = await cli.run({
|
||||
arguments: [
|
||||
"observe-native-ui",
|
||||
"/provider-must-not-be-invoked",
|
||||
"--pid",
|
||||
"1",
|
||||
"--window-id",
|
||||
"1",
|
||||
"--steps",
|
||||
'[{"kind":"wait","milliseconds":0}]',
|
||||
"--json",
|
||||
],
|
||||
environment: { REA_LOG_LEVEL: "silent" },
|
||||
});
|
||||
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(result.stdout).toContain("Unknown flag: --steps");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue