From daad68ce16d28643db3a14b694abdd73004ab1ab Mon Sep 17 00:00:00 2001 From: WilliamK112 <164879897+WilliamK112@users.noreply.github.com> Date: Tue, 23 Jun 2026 10:46:42 -0400 Subject: [PATCH 1/2] fix(plugin): route registration banner to stderr Signed-off-by: WilliamK112 <164879897+WilliamK112@users.noreply.github.com> --- nemoclaw/src/index.ts | 6 +++--- nemoclaw/src/register.test.ts | 34 ++++++++++++++++++++++++++-------- 2 files changed, 29 insertions(+), 11 deletions(-) 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..93bc57ec027 100644 --- a/nemoclaw/src/register.test.ts +++ b/nemoclaw/src/register.test.ts @@ -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,6 +78,7 @@ function createMockApi(): OpenClawPluginApi { describe("plugin registration", () => { beforeEach(() => { vi.clearAllMocks(); + mockStderrWrite(); mockMissingOpenClawConfig(); mockedLoadOnboardConfig.mockReturnValue(null); }); @@ -140,8 +152,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,18 +197,17 @@ 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); }); From 62cb55ca888e81dca9ed04280338f31bd3b4435d Mon Sep 17 00:00:00 2001 From: WilliamK112 <164879897+WilliamK112@users.noreply.github.com> Date: Tue, 23 Jun 2026 11:15:15 -0400 Subject: [PATCH 2/2] test(plugin): restore stderr spy between tests Signed-off-by: WilliamK112 <164879897+WilliamK112@users.noreply.github.com> --- nemoclaw/src/register.test.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/nemoclaw/src/register.test.ts b/nemoclaw/src/register.test.ts index 93bc57ec027..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) => { @@ -83,6 +83,10 @@ describe("plugin registration", () => { mockedLoadOnboardConfig.mockReturnValue(null); }); + afterEach(() => { + vi.restoreAllMocks(); + }); + it("registers a slash command", () => { const api = createMockApi(); register(api); @@ -212,6 +216,10 @@ describe("before_tool_call secret scanner hook (#1233)", () => { mockedLoadOnboardConfig.mockReturnValue(null); }); + afterEach(() => { + vi.restoreAllMocks(); + }); + function getHookHandler(api: OpenClawPluginApi) { register(api); const onCalls = vi.mocked(api.on).mock.calls;