mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-07 09:31:25 +00:00
fix: ensure CLI exits after command completion (#12906)
* fix: ensure CLI exits after command completion The CLI process would hang indefinitely after commands like `openclaw gateway restart` completed successfully. Two root causes: 1. `runCli()` returned without calling `process.exit()` after `program.parseAsync()` resolved, and Commander.js does not force-exit the process. 2. `daemon-cli/register.ts` eagerly called `createDefaultDeps()` which imported all messaging-provider modules, creating persistent event-loop handles that prevented natural Node exit. Changes: - Add `flushAndExit()` helper that drains stdout/stderr before calling `process.exit()`, preventing truncated piped output in CI/scripts. - Call `flushAndExit()` after both `tryRouteCli()` and `program.parseAsync()` resolve. - Remove unnecessary `void createDefaultDeps()` from daemon-cli registration — daemon lifecycle commands never use messaging deps. - Make `serveAcpGateway()` return a promise that resolves on intentional shutdown (SIGINT/SIGTERM), so `openclaw acp` blocks `parseAsync` for the bridge lifetime and exits cleanly on signal. - Handle the returned promise in the standalone main-module entry point to avoid unhandled rejections. Fixes #12904 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: refactor CLI lifecycle and lazy outbound deps (#12906) (thanks @DrCrinkle) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
93
src/cli/deps.test.ts
Normal file
93
src/cli/deps.test.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createDefaultDeps } from "./deps.js";
|
||||
|
||||
const moduleLoads = vi.hoisted(() => ({
|
||||
whatsapp: vi.fn(),
|
||||
telegram: vi.fn(),
|
||||
discord: vi.fn(),
|
||||
slack: vi.fn(),
|
||||
signal: vi.fn(),
|
||||
imessage: vi.fn(),
|
||||
}));
|
||||
|
||||
const sendFns = vi.hoisted(() => ({
|
||||
whatsapp: vi.fn(async () => ({ messageId: "w1", toJid: "whatsapp:1" })),
|
||||
telegram: vi.fn(async () => ({ messageId: "t1", chatId: "telegram:1" })),
|
||||
discord: vi.fn(async () => ({ messageId: "d1", channelId: "discord:1" })),
|
||||
slack: vi.fn(async () => ({ messageId: "s1", channelId: "slack:1" })),
|
||||
signal: vi.fn(async () => ({ messageId: "sg1", conversationId: "signal:1" })),
|
||||
imessage: vi.fn(async () => ({ messageId: "i1", chatId: "imessage:1" })),
|
||||
}));
|
||||
|
||||
vi.mock("../channels/web/index.js", () => {
|
||||
moduleLoads.whatsapp();
|
||||
return { sendMessageWhatsApp: sendFns.whatsapp };
|
||||
});
|
||||
|
||||
vi.mock("../telegram/send.js", () => {
|
||||
moduleLoads.telegram();
|
||||
return { sendMessageTelegram: sendFns.telegram };
|
||||
});
|
||||
|
||||
vi.mock("../discord/send.js", () => {
|
||||
moduleLoads.discord();
|
||||
return { sendMessageDiscord: sendFns.discord };
|
||||
});
|
||||
|
||||
vi.mock("../slack/send.js", () => {
|
||||
moduleLoads.slack();
|
||||
return { sendMessageSlack: sendFns.slack };
|
||||
});
|
||||
|
||||
vi.mock("../signal/send.js", () => {
|
||||
moduleLoads.signal();
|
||||
return { sendMessageSignal: sendFns.signal };
|
||||
});
|
||||
|
||||
vi.mock("../imessage/send.js", () => {
|
||||
moduleLoads.imessage();
|
||||
return { sendMessageIMessage: sendFns.imessage };
|
||||
});
|
||||
|
||||
describe("createDefaultDeps", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("does not load provider modules until a dependency is used", async () => {
|
||||
const deps = createDefaultDeps();
|
||||
|
||||
expect(moduleLoads.whatsapp).not.toHaveBeenCalled();
|
||||
expect(moduleLoads.telegram).not.toHaveBeenCalled();
|
||||
expect(moduleLoads.discord).not.toHaveBeenCalled();
|
||||
expect(moduleLoads.slack).not.toHaveBeenCalled();
|
||||
expect(moduleLoads.signal).not.toHaveBeenCalled();
|
||||
expect(moduleLoads.imessage).not.toHaveBeenCalled();
|
||||
|
||||
const sendTelegram = deps.sendMessageTelegram as unknown as (
|
||||
...args: unknown[]
|
||||
) => Promise<unknown>;
|
||||
await sendTelegram("chat", "hello", { verbose: false });
|
||||
|
||||
expect(moduleLoads.telegram).toHaveBeenCalledTimes(1);
|
||||
expect(sendFns.telegram).toHaveBeenCalledTimes(1);
|
||||
expect(moduleLoads.whatsapp).not.toHaveBeenCalled();
|
||||
expect(moduleLoads.discord).not.toHaveBeenCalled();
|
||||
expect(moduleLoads.slack).not.toHaveBeenCalled();
|
||||
expect(moduleLoads.signal).not.toHaveBeenCalled();
|
||||
expect(moduleLoads.imessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reuses module cache after first dynamic import", async () => {
|
||||
const deps = createDefaultDeps();
|
||||
const sendDiscord = deps.sendMessageDiscord as unknown as (
|
||||
...args: unknown[]
|
||||
) => Promise<unknown>;
|
||||
|
||||
await sendDiscord("channel", "first", { verbose: false });
|
||||
await sendDiscord("channel", "second", { verbose: false });
|
||||
|
||||
expect(moduleLoads.discord).toHaveBeenCalledTimes(1);
|
||||
expect(sendFns.discord).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user