From d3d43eb6cfcda69c9ad0e4bcaf975f4ecbd084f4 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 26 Aug 2026 19:55:41 -0700 Subject: [PATCH] fix(cli): reject missing repeatable inference inputs (#130608) --- docs/cli/infer.md | 1 + src/cli/capability-cli.test.ts | 40 ++++++++++++++++++++++++++--- src/cli/capability-cli/embedding.ts | 2 +- src/cli/capability-cli/image.ts | 4 +-- 4 files changed, 41 insertions(+), 6 deletions(-) diff --git a/docs/cli/infer.md b/docs/cli/infer.md index 857e872b43ea..91f064a1d709 100644 --- a/docs/cli/infer.md +++ b/docs/cli/infer.md @@ -101,6 +101,7 @@ A good infer-based skill maps common user intents to the right subcommand, inclu - Use `--json` when the output feeds another command or script; text output otherwise. - Use `--provider` or `--model provider/model` to pin a specific backend. +- `image edit` and `image describe-many` require at least one `--file`; `embedding create` requires at least one `--text`. Repeat the flag for multiple inputs. Omitting it is a usage error, not an empty successful result, and no inference request is sent. - Use `model run --thinking ` for a one-shot thinking/reasoning override: `off`, `minimal`, `low`, `medium`, `high`, `adaptive`, `xhigh`, or `max`. - For `image describe`, `audio transcribe`, and `video describe`, `--model` must use the form ``. - For `image describe`, `--file` accepts local paths and HTTP(S) URLs; remote URLs go through the normal media-fetch SSRF policy. diff --git a/src/cli/capability-cli.test.ts b/src/cli/capability-cli.test.ts index 2b4e3dc2cfc7..0add93d74a01 100644 --- a/src/cli/capability-cli.test.ts +++ b/src/cli/capability-cli.test.ts @@ -568,6 +568,32 @@ vi.mock("../plugins/web-search-providers.runtime.js", () => ({ })); describe("capability cli", () => { + it.each( + [ + { args: ["image", "edit", "--prompt", "crop the image"], option: "--file " }, + { args: ["image", "describe-many"], option: "--file " }, + { args: ["embedding", "create"], option: "--text " }, + ].flatMap(({ args, option }) => + ["infer", "capability"].map((root) => ({ args, option, root })), + ), + )("rejects missing required repeatable input for $root $args", async ({ root, args, option }) => { + const argv = [root, ...args, "--json"]; + const program = new Command().exitOverride().configureOutput({ writeErr: () => {} }); + await registerCapabilityCli(program, ["node", "openclaw", ...argv]); + + await expect( + program.parseAsync(argv, { from: "user" }).then(() => undefined), + ).rejects.toMatchObject({ + code: "commander.missingMandatoryOptionValue", + message: `error: required option '${option}' not specified`, + }); + expect(mocks.resolveCommandConfigWithSecrets).not.toHaveBeenCalled(); + expect(mocks.generateImage).not.toHaveBeenCalled(); + expect(mocks.describeImageFile).not.toHaveBeenCalled(); + expect(mocks.createEmbeddingProvider).not.toHaveBeenCalled(); + expect(mocks.runtime.writeJson).not.toHaveBeenCalled(); + }); + afterEach(() => { vi.unstubAllGlobals(); vi.unstubAllEnvs(); @@ -2417,6 +2443,8 @@ describe("capability cli", () => { "edit", "--file", inputPath, + "--file", + inputPath, "--prompt", "make three variants", "--count", @@ -2425,6 +2453,7 @@ describe("capability cli", () => { ); expect(firstImageGenerationCall()?.count).toBe(3); + expect(firstImageGenerationCall()?.inputImages).toHaveLength(2); }); it("rejects unsupported image output format and background hints", async () => { @@ -3678,15 +3707,20 @@ describe("capability cli", () => { ); it("uses only embedding providers for embedding creation", async () => { - await runCapability("embedding", "create", "--text", "hello", "--json"); + await runCapability("embedding", "create", "--text", "hello", "--text", "world", "--json"); expect(firstEmbeddingProviderCall()?.provider).toBe("auto"); expect(firstEmbeddingProviderCall()?.fallback).toBe("none"); expect(firstJsonOutput()?.capability).toBe("embedding.create"); expect(firstJsonOutput()?.provider).toBe("openai"); expect(firstJsonOutput()?.model).toBe("text-embedding-3-small"); - expect(firstJsonOutput()).toMatchObject({ outputs: [{ embedding: [0.1, 0.2] }] }); - expect(mocks.embedBatch).toHaveBeenCalledWith(["hello"], { inputType: "document" }); + expect(firstJsonOutput()).toMatchObject({ + outputs: [ + { text: "hello", embedding: [0.1, 0.2] }, + { text: "world", embedding: [0.1, 0.2] }, + ], + }); + expect(mocks.embedBatch).toHaveBeenCalledWith(["hello", "world"], { inputType: "document" }); expect(closeEmbeddingProviderMock).toHaveBeenCalledTimes(1); }); diff --git a/src/cli/capability-cli/embedding.ts b/src/cli/capability-cli/embedding.ts index 5164485e649f..4d8e11a473d6 100644 --- a/src/cli/capability-cli/embedding.ts +++ b/src/cli/capability-cli/embedding.ts @@ -111,7 +111,7 @@ export function registerEmbeddingCapabilityCommands(capability: Command): void { embedding .command("create") .description("Create embeddings") - .requiredOption("--text ", "Input text", collectOption, []) + .requiredOption("--text ", "Input text", collectOption) .option("--provider ", "Provider id") .option("--model ", "Model override") .option( diff --git a/src/cli/capability-cli/image.ts b/src/cli/capability-cli/image.ts index 64dd9850c38c..7248c2144554 100644 --- a/src/cli/capability-cli/image.ts +++ b/src/cli/capability-cli/image.ts @@ -366,7 +366,7 @@ export function registerImageCapabilityCommands(capability: Command): void { image .command("edit") .description("Edit images with one or more input files") - .requiredOption("--file ", "Input file", collectOption, []) + .requiredOption("--file ", "Input file", collectOption) .requiredOption("--prompt ", "Prompt text"), ).action(async (opts, command) => { await runCommandWithRuntime(defaultRuntime, async () => { @@ -410,7 +410,7 @@ export function registerImageCapabilityCommands(capability: Command): void { image .command("describe-many") .description("Describe multiple image files") - .requiredOption("--file ", "Image file", collectOption, []) + .requiredOption("--file ", "Image file", collectOption) .option("--prompt ", "Prompt hint") .option("--model ", "Model override") .option("--timeout-ms ", "Provider request timeout in milliseconds")