Skip to content

Commit 2cfba46

Browse files
maweibinclaude
andcommitted
fix(cli): call process.exit(1) in root help and version fast path error handlers (#97793)
The error handler in tryHandleRootHelpFastPath only set process.exitCode=1 without calling process.exit(). When dangling async handles exist (timers, connections, promises), the Node event loop stays open and the CLI hangs indefinitely instead of returning to the terminal. In tryHandleRootVersionFastPath, the default onError handler called process.exit(1) directly, bypassing the injected exit seam that the success path already uses. Changed to exit(1) (deps.exit ?? process.exit) so tests and embedded callers can observe and control exit on failure. Added rejection-path tests for version fast path: injected exit called on resolveVersion rejection, and caller-supplied onError honored. Fixes #97793. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
1 parent be94853 commit 2cfba46

3 files changed

Lines changed: 43 additions & 2 deletions

File tree

src/entry.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ export async function tryHandleRootHelpFastPath(
159159
"[openclaw] Failed to display help:",
160160
error instanceof Error ? (error.stack ?? error.message) : error,
161161
);
162-
process.exitCode = 1;
162+
process.exit(1);
163163
});
164164
try {
165165
const loadRootHelpRenderOptionsForConfigSensitivePlugins =

src/entry.version-fast-path.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,4 +83,45 @@ describe("entry root version fast path", () => {
8383
}),
8484
).toBe(false);
8585
});
86+
87+
it("calls exit(1) via injected exit hook when resolveVersion rejects", async () => {
88+
const exit = vi.fn();
89+
const output = vi.fn();
90+
const resolveVersion = vi
91+
.fn<() => Promise<never>>()
92+
.mockRejectedValue(new Error("version resolution failed"));
93+
94+
expect(
95+
tryHandleRootVersionFastPath(["node", "openclaw", "--version"], {
96+
output,
97+
exit,
98+
resolveVersion,
99+
}),
100+
).toBe(true);
101+
await flushVersionFastPath();
102+
expect(resolveVersion).toHaveBeenCalledTimes(1);
103+
expect(exit).toHaveBeenCalledWith(1);
104+
expect(output).not.toHaveBeenCalled();
105+
expect(exit).toHaveBeenCalledTimes(1);
106+
});
107+
108+
it("calls injected onError when provided and resolveVersion rejects", async () => {
109+
const exit = vi.fn();
110+
const onError = vi.fn();
111+
const resolveVersion = vi
112+
.fn<() => Promise<never>>()
113+
.mockRejectedValue(new Error("version resolution failed"));
114+
115+
expect(
116+
tryHandleRootVersionFastPath(["node", "openclaw", "--version"], {
117+
exit,
118+
onError,
119+
resolveVersion,
120+
}),
121+
).toBe(true);
122+
await flushVersionFastPath();
123+
expect(resolveVersion).toHaveBeenCalledTimes(1);
124+
expect(onError).toHaveBeenCalledTimes(1);
125+
expect(exit).not.toHaveBeenCalled();
126+
});
86127
});

src/entry.version-fast-path.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export function tryHandleRootVersionFastPath(
3131
"[openclaw] Failed to resolve version:",
3232
error instanceof Error ? (error.stack ?? error.message) : error,
3333
);
34-
process.exitCode = 1;
34+
exit(1);
3535
});
3636
const resolveVersion =
3737
deps.resolveVersion ??

0 commit comments

Comments
 (0)