fix: show banner on gateway fast path

This commit is contained in:
Peter Steinberger
2026-04-28 03:46:05 +01:00
parent cfca2d4051
commit fc0a2bc87d
2 changed files with 96 additions and 3 deletions

View File

@@ -23,6 +23,9 @@ const restoreTerminalStateMock = vi.hoisted(() => vi.fn());
const hasEnvHttpProxyAgentConfiguredMock = vi.hoisted(() => vi.fn(() => false));
const ensureGlobalUndiciEnvProxyDispatcherMock = vi.hoisted(() => vi.fn());
const runCrestodianMock = vi.hoisted(() => vi.fn(async () => {}));
const commanderParseAsyncMock = vi.hoisted(() => vi.fn(async () => {}));
const addGatewayRunCommandMock = vi.hoisted(() => vi.fn((command: unknown) => command));
const emitCliBannerMock = vi.hoisted(() => vi.fn());
const progressDoneMock = vi.hoisted(() => vi.fn());
const createCliProgressMock = vi.hoisted(() =>
vi.fn(() => ({
@@ -35,10 +38,49 @@ const maybeRunCliInContainerMock = vi.hoisted(() =>
>((argv: string[]) => ({ handled: false, argv })),
);
vi.mock("commander", () => {
class MockCommanderError extends Error {
exitCode: number;
code: string;
constructor(exitCode: number, code: string, message: string) {
super(message);
this.exitCode = exitCode;
this.code = code;
}
}
class MockCommand {
name = vi.fn(() => this);
enablePositionalOptions = vi.fn(() => this);
exitOverride = vi.fn(() => this);
description = vi.fn(() => this);
command = vi.fn(() => new MockCommand());
parseAsync = commanderParseAsyncMock;
}
return {
Command: MockCommand,
CommanderError: MockCommanderError,
};
});
vi.mock("./route.js", () => ({
tryRouteCli: tryRouteCliMock,
}));
vi.mock("./gateway-cli/run.js", () => ({
addGatewayRunCommand: addGatewayRunCommandMock,
}));
vi.mock("../version.js", () => ({
VERSION: "9.9.9-test",
}));
vi.mock("./banner.js", () => ({
emitCliBanner: emitCliBannerMock,
}));
vi.mock("./container-target.js", () => ({
maybeRunCliInContainer: maybeRunCliInContainerMock,
parseCliContainerArgs: (argv: string[]) => ({ ok: true, container: null, argv }),
@@ -132,6 +174,7 @@ describe("runCli exit behavior", () => {
hasEnvHttpProxyAgentConfiguredMock.mockReturnValue(false);
getProgramContextMock.mockReturnValue(null);
delete process.env.OPENCLAW_DISABLE_CLI_STARTUP_HELP_FAST_PATH;
delete process.env.OPENCLAW_HIDE_BANNER;
});
it("does not force process.exit after successful routed command", async () => {
@@ -151,6 +194,32 @@ describe("runCli exit behavior", () => {
exitSpy.mockRestore();
});
it("emits the startup banner before gateway foreground fast-path startup", async () => {
await runCli(["node", "openclaw", "gateway", "--force"]);
expect(tryRouteCliMock).not.toHaveBeenCalled();
expect(emitCliBannerMock).toHaveBeenCalledWith("9.9.9-test", {
argv: ["node", "openclaw", "gateway", "--force"],
});
expect(addGatewayRunCommandMock).toHaveBeenCalledTimes(2);
expect(commanderParseAsyncMock).toHaveBeenCalledWith([
"node",
"openclaw",
"gateway",
"--force",
]);
});
it("honors banner suppression on the gateway foreground fast path", async () => {
process.env.OPENCLAW_HIDE_BANNER = "1";
await runCli(["node", "openclaw", "gateway"]);
expect(tryRouteCliMock).not.toHaveBeenCalled();
expect(emitCliBannerMock).not.toHaveBeenCalled();
expect(commanderParseAsyncMock).toHaveBeenCalledWith(["node", "openclaw", "gateway"]);
});
it("renders browser help from startup metadata without building the full program", async () => {
outputPrecomputedBrowserHelpTextMock.mockReturnValueOnce(true);
const exitSpy = vi.spyOn(process, "exit").mockImplementation(((code?: number) => {

View File

@@ -83,6 +83,10 @@ export function isGatewayRunFastPathArgv(argv: string[]): boolean {
return invocation.commandPath.length === 1 || invocation.commandPath[1] === "run";
}
function hasJsonOutputFlag(argv: string[]): boolean {
return argv.some((arg) => arg === "--json" || arg.startsWith("--json="));
}
async function tryRunGatewayRunFastPath(
argv: string[],
startupTrace: ReturnType<typeof createGatewayCliMainStartupTrace>,
@@ -90,10 +94,30 @@ async function tryRunGatewayRunFastPath(
if (!isGatewayRunFastPathArgv(argv)) {
return false;
}
const [{ Command }, { addGatewayRunCommand }] = await startupTrace.measure(
"gateway-run-imports",
() => Promise.all([import("commander"), import("./gateway-cli/run.js")]),
const [
{ Command },
{ addGatewayRunCommand },
{ VERSION },
{ emitCliBanner },
{ resolveCliStartupPolicy },
] = await startupTrace.measure("gateway-run-imports", () =>
Promise.all([
import("commander"),
import("./gateway-cli/run.js"),
import("../version.js"),
import("./banner.js"),
import("./command-startup-policy.js"),
]),
);
const invocation = resolveCliArgvInvocation(argv);
const startupPolicy = resolveCliStartupPolicy({
commandPath: invocation.commandPath,
jsonOutputMode: hasJsonOutputFlag(argv),
routeMode: true,
});
if (!startupPolicy.hideBanner) {
emitCliBanner(VERSION, { argv });
}
const program = new Command();
program.name("openclaw");
program.enablePositionalOptions();