Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions src/handlers/runtime/invoke/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { AppIO } from "../../../io";
import type { Core } from "../../types";
import { coreOptsFromCtx } from "../../utils";
import { JsonKey } from "../../keys";
import { ExitCode } from "../../../runnable";
import { RuntimeInvokeInterruptedError } from "./errors";
import {
normalizeRuntimeInvokeRequest,
Expand All @@ -19,8 +20,8 @@ export const createInvokeRuntimeHandler = (core: Core, io: AppIO) =>
name: "invoke",
description: "invoke a Runtime",
flags: [
flag("id", "the ID of the Runtime", runtimeIdSchema),
flag("payload", "the inline payload to send", z.string(), {
flag("id", "the ID of the Runtime", runtimeIdSchema.optional()),
flag("payload", "the inline payload to send", z.string().optional(), {
sensitive: true,
}),
flag("qualifier", "the Runtime endpoint qualifier", z.string().optional()),
Expand Down Expand Up @@ -49,6 +50,17 @@ export const createInvokeRuntimeHandler = (core: Core, io: AppIO) =>
),
],
handle: async (ctx, flags) => {
if (flags.id === undefined) {
throw new InputValidationError("required option '--id <id>' not specified", {
exitCode: ExitCode.USAGE,

@Hweinstock Hweinstock Jul 30, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't realize that 2 was the standard for validation exceptions. Should we make this the default on the input validation error class? (could be a follow-up)

});
}
if (flags.payload === undefined) {
throw new InputValidationError("required option '--payload <payload>' not specified", {
exitCode: ExitCode.USAGE,
});
}

const jsonOutput = ctx.require(JsonKey);
if (jsonOutput && flags["output-file"] !== undefined) {
throw new InputValidationError("--json cannot be used with --output-file");
Expand Down
17 changes: 15 additions & 2 deletions src/handlers/runtime/invoke/invoke.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -340,12 +340,15 @@ describe("runtime invoke", () => {
expect(core.runtime.calls).toEqual([]);
});

test("classifies required local validation as usage before Core calls", async () => {
test.each<[string, string[]]>([
["--id", ["--payload", "{}"]],
["--payload", ["--id", RUNTIME_ID]],
])("classifies a missing %s as usage before Core calls", async (_flag, args) => {
const core = new TestCoreClient();
const output = captureIO();

const code = await runWithExitCode(async () =>
runCommand(core, output.io, ["runtime", "invoke", "--payload", "{}"]),
runCommand(core, output.io, ["runtime", "invoke", ...args]),
);

expect(code).toBe(ExitCode.USAGE);
Expand Down Expand Up @@ -391,6 +394,16 @@ describe("runtime invoke", () => {
expect(core.runtime.calls.map((call) => call.method)).toEqual(["getRuntime"]);
});

test("a bare command reaches the Runtime TUI middleware without Core calls", async () => {
const core = new TestCoreClient();
const output = captureIO();

await expect(runCommand(core, output.io, ["runtime", "invoke"])).rejects.toThrow(
"interactive mode requires a TTY on stdin and stdout",
);
expect(core.runtime.calls).toEqual([]);
});

test.each<[string, ...string[]]>([
["malformed", "missing separator"],
["duplicate", "X-Test: one", "x-test: two"],
Expand Down
Loading