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);
|
registerCapabilityCommands(cli, logger, analysis);
|
||||||
registerNativeCommands(cli, logger, runProviderAnalysis);
|
registerNativeCommands(cli, logger, runProviderAnalysis);
|
||||||
registerNativeCallCommands(cli, logger, runProviderAnalysis);
|
registerNativeCallCommands(cli, logger, runProviderAnalysis);
|
||||||
for (const [command, operation] of [
|
registerNativeUiCommands(cli, logger, runProviderAnalysis);
|
||||||
[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);
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
registerReferenceSourceCommand(cli, logger, environment);
|
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 = (
|
const invalidNativeUiScenarioInput = (
|
||||||
operation: string,
|
operation: string,
|
||||||
issues: ConstructorParameters<typeof AnalysisInputError>[2],
|
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