mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-15 12:00:43 +00:00
* 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>
94 lines
3.0 KiB
TypeScript
94 lines
3.0 KiB
TypeScript
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);
|
|
});
|
|
});
|