diff --git a/nemoclaw/src/index.ts b/nemoclaw/src/index.ts index 188259f0611..2ab4dcd6694 100644 --- a/nemoclaw/src/index.ts +++ b/nemoclaw/src/index.ts @@ -437,9 +437,9 @@ export default function register(api: OpenClawPluginApi): void { " Slash: /nemoclaw", ]; - api.logger.info(""); + process.stderr.write("\n"); for (const line of renderBox(bannerLines)) { - api.logger.info(line); + process.stderr.write(`${line}\n`); } - api.logger.info(""); + process.stderr.write("\n"); } diff --git a/nemoclaw/src/register.test.ts b/nemoclaw/src/register.test.ts index ed66e916df1..7657d513171 100644 --- a/nemoclaw/src/register.test.ts +++ b/nemoclaw/src/register.test.ts @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawPluginApi } from "./index.js"; vi.mock("node:fs", async (importOriginal) => { @@ -32,6 +32,17 @@ const mockedReadFileSync = vi.mocked(readFileSync); const mockedLoadOnboardConfig = vi.mocked(loadOnboardConfig); const originalReadFileSync = (await vi.importActual("node:fs")) .readFileSync; +let stderrWrite: ReturnType; + +function mockStderrWrite(): void { + stderrWrite = vi + .spyOn(process.stderr, "write") + .mockImplementation((() => true) as typeof process.stderr.write); +} + +function stderrOutput(): string { + return stderrWrite.mock.calls.map(([chunk]) => String(chunk)).join(""); +} function mockMissingOpenClawConfig(): void { mockedReadFileSync.mockReset(); @@ -67,10 +78,15 @@ function createMockApi(): OpenClawPluginApi { describe("plugin registration", () => { beforeEach(() => { vi.clearAllMocks(); + mockStderrWrite(); mockMissingOpenClawConfig(); mockedLoadOnboardConfig.mockReturnValue(null); }); + afterEach(() => { + vi.restoreAllMocks(); + }); + it("registers a slash command", () => { const api = createMockApi(); register(api); @@ -140,8 +156,15 @@ describe("plugin registration", () => { expect(providerArg.models?.chat).toEqual([ expect.objectContaining({ id: "inference/nvidia/live-model", label: "nvidia/live-model" }), ]); - const logLines = vi.mocked(api.logger.info).mock.calls.map(([message]) => message); - expect(logLines.some((line) => line.includes("Model: nvidia/live-model"))).toBe(true); + expect(stderrOutput()).toContain("Model: nvidia/live-model"); + }); + + it("writes the registration banner to stderr instead of plugin info logs", () => { + const api = createMockApi(); + register(api); + + expect(stderrOutput()).toContain("NemoClaw registered"); + expect(api.logger.info).not.toHaveBeenCalled(); }); it("falls back to onboard config when openclaw.json has no primary model", () => { @@ -178,22 +201,25 @@ describe("plugin registration", () => { expect.objectContaining({ id: "nvidia/nemotron-3-nano-30b-a3b" }), ]); - const logLines = vi.mocked(api.logger.info).mock.calls.map(([message]) => message); - expect(logLines.some((line) => line.includes("Endpoint: build.nvidia.com"))).toBe(true); - expect(logLines.some((line) => line.includes("Provider: NVIDIA Endpoints"))).toBe(true); - expect( - logLines.some((line) => line.includes("Model: nvidia/nemotron-3-super-120b-a12b")), - ).toBe(true); + const stderr = stderrOutput(); + expect(stderr).toContain("Endpoint: build.nvidia.com"); + expect(stderr).toContain("Provider: NVIDIA Endpoints"); + expect(stderr).toContain("Model: nvidia/nemotron-3-super-120b-a12b"); }); }); describe("before_tool_call secret scanner hook (#1233)", () => { beforeEach(() => { vi.clearAllMocks(); + mockStderrWrite(); mockMissingOpenClawConfig(); mockedLoadOnboardConfig.mockReturnValue(null); }); + afterEach(() => { + vi.restoreAllMocks(); + }); + function getHookHandler(api: OpenClawPluginApi) { register(api); const onCalls = vi.mocked(api.on).mock.calls;