Files
openclaw/src/entry.version-fast-path.test.ts
maweibin 84cd3aa7f5 fix(cli): call process.exit(1) in root help and version fast path error handlers (#97793) (#97807)
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) <noreply@anthropic.com>
2026-06-29 10:47:55 -07:00

128 lines
3.8 KiB
TypeScript

// Tests version fast-path output before the full entrypoint loads.
import { describe, expect, it, vi } from "vitest";
import { tryHandleRootVersionFastPath } from "./entry.version-fast-path.js";
vi.mock("./cli/argv.js", () => ({
isRootHelpInvocation: () => false,
isRootVersionInvocation: (argv: string[]) => argv.includes("--version"),
}));
vi.mock("./cli/container-target.js", () => ({
parseCliContainerArgs: (argv: string[]) => ({ ok: true, container: null, argv }),
resolveCliContainerTarget: (argv: string[], env: NodeJS.ProcessEnv = process.env) =>
argv.includes("--container") ? "demo" : (env.OPENCLAW_CONTAINER ?? null),
}));
async function flushVersionFastPath() {
await Promise.resolve();
await Promise.resolve();
}
describe("entry root version fast path", () => {
it("prints version output and skips host handling when container-targeted", async () => {
const output = vi.fn();
const exit = vi.fn();
const resolveVersion = vi.fn<
() => Promise<{
VERSION: string;
resolveCommitHash: (params: { moduleUrl: string }) => string | null;
}>
>(async () => ({
VERSION: "9.9.9-test",
resolveCommitHash: vi.fn(() => "abc1234"),
}));
expect(
tryHandleRootVersionFastPath(["node", "openclaw", "--version"], {
output,
exit,
resolveVersion,
}),
).toBe(true);
await flushVersionFastPath();
expect(output).toHaveBeenCalledWith("OpenClaw 9.9.9-test (abc1234)");
expect(exit).toHaveBeenCalledWith(0);
output.mockClear();
exit.mockClear();
resolveVersion.mockResolvedValueOnce({
VERSION: "9.9.9-test",
resolveCommitHash: vi.fn(() => null),
});
expect(
tryHandleRootVersionFastPath(["node", "openclaw", "--version"], {
output,
exit,
resolveVersion,
}),
).toBe(true);
await flushVersionFastPath();
expect(output).toHaveBeenCalledWith("OpenClaw 9.9.9-test");
expect(exit).toHaveBeenCalledWith(0);
output.mockClear();
exit.mockClear();
expect(
tryHandleRootVersionFastPath(["node", "openclaw", "--container", "demo", "--version"], {
output,
exit,
resolveVersion,
}),
).toBe(false);
expect(resolveVersion).toHaveBeenCalledTimes(2);
expect(output).not.toHaveBeenCalled();
expect(exit).not.toHaveBeenCalled();
expect(
tryHandleRootVersionFastPath(["node", "openclaw", "--version"], {
env: { OPENCLAW_CONTAINER: "demo" },
output,
exit,
resolveVersion,
}),
).toBe(false);
});
it("calls exit(1) via injected exit hook when resolveVersion rejects", async () => {
const exit = vi.fn();
const output = vi.fn();
const resolveVersion = vi
.fn<() => Promise<never>>()
.mockRejectedValue(new Error("version resolution failed"));
expect(
tryHandleRootVersionFastPath(["node", "openclaw", "--version"], {
output,
exit,
resolveVersion,
}),
).toBe(true);
await flushVersionFastPath();
expect(resolveVersion).toHaveBeenCalledTimes(1);
expect(exit).toHaveBeenCalledWith(1);
expect(output).not.toHaveBeenCalled();
expect(exit).toHaveBeenCalledTimes(1);
});
it("calls injected onError when provided and resolveVersion rejects", async () => {
const exit = vi.fn();
const onError = vi.fn();
const resolveVersion = vi
.fn<() => Promise<never>>()
.mockRejectedValue(new Error("version resolution failed"));
expect(
tryHandleRootVersionFastPath(["node", "openclaw", "--version"], {
exit,
onError,
resolveVersion,
}),
).toBe(true);
await flushVersionFastPath();
expect(resolveVersion).toHaveBeenCalledTimes(1);
expect(onError).toHaveBeenCalledTimes(1);
expect(exit).not.toHaveBeenCalled();
});
});