Skip to content

Commit a970ce5

Browse files
rohitjavvadivincentkoc
authored andcommitted
fix(canvas): validate CLI numeric options
1 parent 47759c3 commit a970ce5

2 files changed

Lines changed: 98 additions & 4 deletions

File tree

extensions/canvas/src/cli.test.ts

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
// Canvas tests cover cli plugin behavior.
22
import { Command } from "commander";
33
import { describe, expect, it, vi } from "vitest";
4-
import { registerNodesCanvasCommands, type CanvasCliDependencies } from "./cli.js";
4+
import {
5+
createDefaultCanvasCliDependencies,
6+
registerNodesCanvasCommands,
7+
type CanvasCliDependencies,
8+
} from "./cli.js";
59

610
function createCanvasCliDeps() {
711
const writtenFiles: Array<{ filePath: string; base64: string }> = [];
@@ -47,6 +51,26 @@ function createCanvasCliDeps() {
4751
return { deps, runtime, writtenFiles };
4852
}
4953

54+
function createCanvasCliDepsWithDefaultParsers() {
55+
const baseDeps = createDefaultCanvasCliDependencies();
56+
const harness = createCanvasCliDeps();
57+
return {
58+
...harness,
59+
deps: {
60+
...baseDeps,
61+
defaultRuntime: harness.runtime,
62+
nodesCallOpts: harness.deps.nodesCallOpts,
63+
runNodesCommand: harness.deps.runNodesCommand,
64+
getNodesTheme: harness.deps.getNodesTheme,
65+
resolveNodeId: harness.deps.resolveNodeId,
66+
buildNodeInvokeParams: harness.deps.buildNodeInvokeParams,
67+
callGatewayCli: harness.deps.callGatewayCli,
68+
writeBase64ToFile: harness.deps.writeBase64ToFile,
69+
shortenHomePath: harness.deps.shortenHomePath,
70+
},
71+
};
72+
}
73+
5074
describe("canvas CLI", () => {
5175
it("registers under nodes and captures a snapshot media path", async () => {
5276
const program = new Command();
@@ -135,6 +159,8 @@ describe("canvas CLI", () => {
135159
it.each([
136160
["--max-width", "640px", "--max-width must be a positive integer."],
137161
["--quality", "0.8x", "--quality must be a number."],
162+
["--quality", "-0.1", "--quality must be between 0 and 1."],
163+
["--quality", "5", "--quality must be between 0 and 1."],
138164
])("rejects partial numeric snapshot %s values", async (flag, value, message) => {
139165
const program = new Command();
140166
program.exitOverride();
@@ -151,6 +177,62 @@ describe("canvas CLI", () => {
151177
expect(deps.callGatewayCli).not.toHaveBeenCalled();
152178
});
153179

180+
it.each(["0", "1"])("accepts snapshot --quality boundary value %s", async (quality) => {
181+
const program = new Command();
182+
program.exitOverride();
183+
const nodes = program.command("nodes");
184+
const { deps } = createCanvasCliDeps();
185+
186+
registerNodesCanvasCommands(nodes, deps);
187+
188+
await program.parseAsync(
189+
["nodes", "canvas", "snapshot", "--node", "ios-node", "--quality", quality],
190+
{
191+
from: "user",
192+
},
193+
);
194+
expect(deps.callGatewayCli).toHaveBeenCalledWith(
195+
"node.invoke",
196+
expect.any(Object),
197+
expect.objectContaining({
198+
params: expect.objectContaining({
199+
quality: Number(quality),
200+
}),
201+
}),
202+
);
203+
});
204+
205+
it.each([
206+
["snapshot"],
207+
["present"],
208+
["hide"],
209+
["navigate", "https://example.com"],
210+
["eval", "1 + 1"],
211+
["a2ui", "push", "--text", "hello"],
212+
["a2ui", "reset"],
213+
])("rejects invalid %s invoke timeouts before invoking the node", async (...args) => {
214+
const program = new Command();
215+
program.exitOverride();
216+
const nodes = program.command("nodes");
217+
const { deps } = createCanvasCliDepsWithDefaultParsers();
218+
deps.resolveNodeId = vi.fn(async () => {
219+
throw new Error("resolveNodeId should not be called");
220+
});
221+
222+
registerNodesCanvasCommands(nodes, deps);
223+
224+
await expect(
225+
program.parseAsync(
226+
["nodes", "canvas", ...args, "--node", "ios-node", "--invoke-timeout", "20ms"],
227+
{
228+
from: "user",
229+
},
230+
),
231+
).rejects.toThrow("--invoke-timeout must be a positive integer.");
232+
expect(deps.resolveNodeId).not.toHaveBeenCalled();
233+
expect(deps.callGatewayCli).not.toHaveBeenCalled();
234+
});
235+
154236
it.each([
155237
["--x", "1x"],
156238
["--y", "2px"],

extensions/canvas/src/cli.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,11 @@ function parseTimeoutMs(raw: unknown): number | undefined {
9797
if (raw === undefined || raw === null) {
9898
return undefined;
9999
}
100-
return parseStrictPositiveInteger(raw);
100+
const parsed = parseStrictPositiveInteger(raw);
101+
if (parsed === undefined) {
102+
throw new Error("--invoke-timeout must be a positive integer.");
103+
}
104+
return parsed;
101105
}
102106

103107
function parseCanvasPositiveIntOption(raw: string | undefined, flag: string): number | undefined {
@@ -122,6 +126,14 @@ function parseCanvasFiniteNumberOption(raw: string | undefined, flag: string): n
122126
return parsed;
123127
}
124128

129+
function parseCanvasSnapshotQualityOption(raw: string | undefined): number | undefined {
130+
const parsed = parseCanvasFiniteNumberOption(raw, "--quality");
131+
if (parsed !== undefined && (parsed < 0 || parsed > 1)) {
132+
throw new Error("--quality must be between 0 and 1.");
133+
}
134+
return parsed;
135+
}
136+
125137
function parseNodeCandidates(raw: unknown): CanvasNodeCandidate[] {
126138
const payload =
127139
raw && typeof raw === "object" ? (raw as { nodes?: unknown; paired?: unknown }) : {};
@@ -245,8 +257,8 @@ async function invokeCanvas(
245257
command: string,
246258
params?: Record<string, unknown>,
247259
) {
248-
const nodeId = await deps.resolveNodeId(opts, normalizeOptionalString(opts.node) ?? "");
249260
const timeoutMs = deps.parseTimeoutMs(opts.invokeTimeout);
261+
const nodeId = await deps.resolveNodeId(opts, normalizeOptionalString(opts.node) ?? "");
250262
return await deps.callGatewayCli(
251263
"node.invoke",
252264
opts,
@@ -278,7 +290,7 @@ export function registerNodesCanvasCommands(nodes: Command, deps: CanvasCliDepen
278290
await deps.runNodesCommand("canvas snapshot", async () => {
279291
const format = parseCanvasSnapshotRequestFormat(opts.format);
280292
const maxWidth = parseCanvasPositiveIntOption(opts.maxWidth, "--max-width");
281-
const quality = parseCanvasFiniteNumberOption(opts.quality, "--quality");
293+
const quality = parseCanvasSnapshotQualityOption(opts.quality);
282294
const raw = await invokeCanvas(deps, opts, "canvas.snapshot", {
283295
format,
284296
maxWidth: Number.isFinite(maxWidth) ? maxWidth : undefined,

0 commit comments

Comments
 (0)