From 2209c5c9f5002ff2c0b5d78e335b90fbca618485 Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Thu, 30 Jul 2026 16:59:54 +0000 Subject: [PATCH] fix(runtime): defer invoke required flag validation --- src/handlers/runtime/invoke/index.tsx | 16 ++++++++++++++-- src/handlers/runtime/invoke/invoke.test.tsx | 17 +++++++++++++++-- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/handlers/runtime/invoke/index.tsx b/src/handlers/runtime/invoke/index.tsx index bfba602a8..c97b8e04e 100644 --- a/src/handlers/runtime/invoke/index.tsx +++ b/src/handlers/runtime/invoke/index.tsx @@ -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, @@ -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()), @@ -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 ' not specified", { + exitCode: ExitCode.USAGE, + }); + } + if (flags.payload === undefined) { + throw new InputValidationError("required option '--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"); diff --git a/src/handlers/runtime/invoke/invoke.test.tsx b/src/handlers/runtime/invoke/invoke.test.tsx index 627561b30..af17facd8 100644 --- a/src/handlers/runtime/invoke/invoke.test.tsx +++ b/src/handlers/runtime/invoke/invoke.test.tsx @@ -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); @@ -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"],