diff --git a/src/cli/cron-cli/register.cron-edit-options.ts b/src/cli/cron-cli/register.cron-edit-options.ts index 2bb8d7728ce8..0ca1facbf7b9 100644 --- a/src/cli/cron-cli/register.cron-edit-options.ts +++ b/src/cli/cron-cli/register.cron-edit-options.ts @@ -25,6 +25,7 @@ export async function resolveCronEditPayloadDeliveryPatch( opts: Record, loadExistingJob: () => Promise, webhookUrl: string | undefined, + commandCwd: string | undefined, ): Promise> { const patch: Record = {}; const hasSystemEventPatch = typeof opts.systemEvent === "string"; @@ -120,11 +121,13 @@ export async function resolveCronEditPayloadDeliveryPatch( throw new Error("Use --account or --clear-account, not both"); } + // Unlike cwd, command stdin intentionally accepts empty and whitespace strings. + const hasCommandInput = typeof opts.commandInput === "string"; const hasCommandSpecificPayloadField = Boolean(commandShell) || Boolean(commandArgv) || - typeof opts.commandCwd === "string" || - typeof opts.commandInput === "string" || + Boolean(commandCwd) || + hasCommandInput || opts.commandEnv !== undefined || noOutputTimeoutSeconds !== undefined || outputMaxBytes !== undefined; @@ -228,14 +231,9 @@ export async function resolveCronEditPayloadDeliveryPatch( const payload: Record = { kind: "command" }; assignIf(payload, "argv", commandArgv, Boolean(commandArgv)); assignIf(payload, "argv", ["sh", "-lc", commandShell], Boolean(commandShell)); - assignIf( - payload, - "cwd", - normalizeOptionalString(opts.commandCwd), - typeof opts.commandCwd === "string", - ); + assignIf(payload, "cwd", commandCwd, Boolean(commandCwd)); assignIf(payload, "env", parseCronCommandEnv(opts.commandEnv), opts.commandEnv !== undefined); - assignIf(payload, "input", opts.commandInput, typeof opts.commandInput === "string"); + assignIf(payload, "input", opts.commandInput, hasCommandInput); assignIf(payload, "timeoutSeconds", timeoutSeconds, hasTimeoutSeconds); assignIf( payload, diff --git a/src/cli/cron-cli/register.cron-edit.test.ts b/src/cli/cron-cli/register.cron-edit.test.ts index b39e85b84741..58e5b63ba1dd 100644 --- a/src/cli/cron-cli/register.cron-edit.test.ts +++ b/src/cli/cron-cli/register.cron-edit.test.ts @@ -1033,6 +1033,28 @@ describe("cron edit command", () => { exitSpy.mockRestore(); }); + it.each(["", " "])("rejects blank --command-cwd %j", async (value) => { + await expectCronEditRejection(["--command-cwd", value], "--command-cwd must not be blank"); + }); + + it("rejects blank --command-cwd before loading an existing job", async () => { + await expectCronEditRejection( + ["--pacing-min", "30m", "--command-cwd", " "], + "--command-cwd must not be blank", + ); + }); + + it.each(["", " "])("preserves --command-input %j as command stdin", async (value) => { + await createCronProgram().parseAsync(["edit", "job-1", "--command-input", value], { + from: "user", + }); + + expect(callGatewayFromCli).toHaveBeenCalledWith("cron.update", expect.anything(), { + id: "job-1", + patch: { payload: { kind: "command", input: value } }, + }); + }); + it("rejects --webhook combined with a delivery clear flag", async () => { const errorSpy = vi.spyOn(defaultRuntime, "error").mockImplementation(() => {}); const exitSpy = vi.spyOn(defaultRuntime, "exit").mockImplementation((() => undefined) as never); diff --git a/src/cli/cron-cli/register.cron-edit.ts b/src/cli/cron-cli/register.cron-edit.ts index ce94efd9ef07..7e1d6fddbf02 100644 --- a/src/cli/cron-cli/register.cron-edit.ts +++ b/src/cli/cron-cli/register.cron-edit.ts @@ -171,6 +171,10 @@ export function registerCronEditCommand(cron: Command) { if (opts.clearTools && opts.tools !== undefined) { throw new Error("Use --tools or --clear-tools, not both"); } + const commandCwd = normalizeOptionalString(opts.commandCwd); + if (typeof opts.commandCwd === "string" && !commandCwd) { + throw new Error("--command-cwd must not be blank"); + } let existingJobPromise: Promise | undefined; let expectedConfigRevision: string | undefined; const readExistingCronJob = async (): Promise => { @@ -421,7 +425,12 @@ export function registerCronEditCommand(cron: Command) { Object.assign( patch, - await resolveCronEditPayloadDeliveryPatch(opts, readExistingCronJob, webhookUrl), + await resolveCronEditPayloadDeliveryPatch( + opts, + readExistingCronJob, + webhookUrl, + commandCwd, + ), ); const hasFailureAlertAfter = typeof opts.failureAlertAfter === "string";