From e7a962396817b6ef28533806e7a46ce7840334cb Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 2 May 2026 07:40:12 +0100 Subject: [PATCH] fix(crestodian): fail no-tty startup --- CHANGELOG.md | 1 + src/crestodian/crestodian.test.ts | 43 +++++++++++++++++++++++++++++++ src/crestodian/crestodian.ts | 1 + 3 files changed, 45 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1166b3aa79..1eeeed12f11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ Docs: https://docs.openclaw.ai - Active Memory: use the configured recall timeout as the blocking prompt-build hook budget by default and move cold-start setup grace behind explicit `setupGraceTimeoutMs` config, so the plugin no longer silently extends 15000 ms configs to 45000 ms on the main lane. Fixes #75843. Thanks @vishutdhar. - Plugins/web-provider: reuse the active gateway plugin registry for runtime web provider resolution after deriving the same candidate plugin ids as the loader path, avoiding a redundant `loadOpenClawPlugins` call on every request while preserving origin and scope filters. Fixes #75513. Thanks @jochen. +- Crestodian/CLI: exit non-zero when interactive Crestodian is invoked without a TTY, so scripts and CI no longer treat the setup error as success. Fixes #73646 and supersedes #73928 and #74059. Thanks @bittoby, @luyao618, and @Linux2010. - Agents/sandbox: preserve existing workspace file modes when sandbox edits atomically replace files, so 0644 files do not collapse to 0600 after Write/Edit/apply_patch. Fixes #44077. Thanks @patosullivan. - Agents/models: keep legacy CLI runtime model refs such as `claude-cli/*` in the configured allowlist after canonical runtime migration, so cron `payload.model` overrides keep working. Fixes #75753. Thanks @RyanSandoval. - Codex/app-server: restart the shared Codex app-server client once when it closes during startup thread resume, preserving the existing thread binding instead of retrying `thread/start` on a closed client. Thanks @vincentkoc. diff --git a/src/crestodian/crestodian.test.ts b/src/crestodian/crestodian.test.ts index 67f8ba318c9..b1d17d5f408 100644 --- a/src/crestodian/crestodian.test.ts +++ b/src/crestodian/crestodian.test.ts @@ -112,4 +112,47 @@ describe("runCrestodian", () => { expect(onReadyCalls).toBe(1); expect(lines.join("\n")).not.toContain("Say: status"); }); + + it.each([ + { + name: "stdin is not a TTY", + input: { isTTY: false } as unknown as NodeJS.ReadableStream, + output: { isTTY: true } as unknown as NodeJS.WritableStream, + interactive: true, + }, + { + name: "stdout is not a TTY", + input: { isTTY: true } as unknown as NodeJS.ReadableStream, + output: { isTTY: false } as unknown as NodeJS.WritableStream, + interactive: true, + }, + { + name: "interactive mode is disabled", + input: { isTTY: true } as unknown as NodeJS.ReadableStream, + output: { isTTY: true } as unknown as NodeJS.WritableStream, + interactive: false, + }, + ])("exits non-zero when $name", async ({ input, output, interactive }) => { + const { runtime, lines } = createCrestodianTestRuntime(); + let runInteractiveTuiCalls = 0; + + await expect( + runCrestodian( + { + input, + output, + interactive, + runInteractiveTui: async () => { + runInteractiveTuiCalls += 1; + }, + }, + runtime, + ), + ).rejects.toThrow("exit 1"); + + expect(runInteractiveTuiCalls).toBe(0); + expect(lines.join("\n")).toContain( + "Crestodian needs an interactive TTY. Use --message for one command.", + ); + }); }); diff --git a/src/crestodian/crestodian.ts b/src/crestodian/crestodian.ts index 0aee6a46d5b..550380b39c5 100644 --- a/src/crestodian/crestodian.ts +++ b/src/crestodian/crestodian.ts @@ -92,6 +92,7 @@ export async function runCrestodian( const outputIsTty = (output as { isTTY?: boolean }).isTTY === true; if (!interactive || !inputIsTty || !outputIsTty) { runtime.error("Crestodian needs an interactive TTY. Use --message for one command."); + runtime.exit(1); return; }