From a9f1962a12a5f93b4f9e4a2204c3fb0695a14a71 Mon Sep 17 00:00:00 2001 From: futhgar Date: Wed, 18 Mar 2026 21:51:24 -0400 Subject: [PATCH 1/5] fix(telegram): read model from sandbox registry instead of hardcoding The Telegram bridge banner always showed "nvidia/nemotron-3-super-120b-a12b" regardless of the actual configured inference provider. Users running Ollama or other providers saw a misleading model identity. Read the model from the host-side sandbox registry (~/.nemoclaw/sandboxes.json) which is populated by nemoclaw onboard with the actual provider and model selection. Fall back to the default model only if the registry entry is missing. Fixes #24 Signed-off-by: Josue Gomez Signed-off-by: futhgar --- scripts/telegram-bridge.js | 5 ++++- test/telegram-bridge-banner.test.js | 33 +++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 test/telegram-bridge-banner.test.js diff --git a/scripts/telegram-bridge.js b/scripts/telegram-bridge.js index 27d5d7ba4..0127cb2e9 100755 --- a/scripts/telegram-bridge.js +++ b/scripts/telegram-bridge.js @@ -20,6 +20,7 @@ const https = require("https"); const { execFileSync, spawn } = require("child_process"); const { resolveOpenshell } = require("../bin/lib/resolve-openshell"); const { shellQuote, validateName } = require("../bin/lib/runner"); +const registry = require("../bin/lib/registry"); const OPENSHELL = resolveOpenshell(); if (!OPENSHELL) { @@ -260,9 +261,11 @@ async function main() { console.log(" ┌─────────────────────────────────────────────────────┐"); console.log(" │ NemoClaw Telegram Bridge │"); console.log(" │ │"); + const sandboxInfo = registry.getSandbox(SANDBOX); + const model = sandboxInfo?.model || "nvidia/nemotron-3-super-120b-a12b"; console.log(` │ Bot: @${(me.result.username + " ").slice(0, 37)}│`); console.log(" │ Sandbox: " + (SANDBOX + " ").slice(0, 40) + "│"); - console.log(" │ Model: nvidia/nemotron-3-super-120b-a12b │"); + console.log(` │ Model: ${(model + " ").slice(0, 40)}│`); console.log(" │ │"); console.log(" │ Messages are forwarded to the OpenClaw agent │"); console.log(" │ inside the sandbox. Run 'openshell term' in │"); diff --git a/test/telegram-bridge-banner.test.js b/test/telegram-bridge-banner.test.js new file mode 100644 index 000000000..1971c06e6 --- /dev/null +++ b/test/telegram-bridge-banner.test.js @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +const { describe, it } = require("node:test"); +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); + +describe("telegram bridge banner", () => { + it("reads the model from the sandbox registry instead of hardcoding it", () => { + // The banner source should reference registry.getSandbox, not a hardcoded model string + const bridgeSrc = fs.readFileSync( + path.join(__dirname, "..", "scripts", "telegram-bridge.js"), + "utf-8", + ); + + // Must import and use the registry + assert.match(bridgeSrc, /require\(.*registry.*\)/, "should import the registry module"); + assert.match(bridgeSrc, /getSandbox/, "should call getSandbox to look up the model"); + + // The banner Model line must use a variable, not a hardcoded model name + const bannerLines = bridgeSrc.split("\n").filter((l) => l.includes("Model:") && l.includes("│")); + assert.ok(bannerLines.length > 0, "should have a Model banner line"); + for (const line of bannerLines) { + assert.doesNotMatch( + line, + /["'].*nemotron.*["']/, + "Model banner line should not contain a hardcoded model string literal", + ); + } + }); +}); From 245e9f02cac31421db5b5f329480d13df08a6e46 Mon Sep 17 00:00:00 2001 From: futhgar Date: Wed, 18 Mar 2026 22:04:00 -0400 Subject: [PATCH 2/5] test: add behavioral test for non-default model in banner Address CodeRabbit review: add a test that sets up a registry with a non-default model (ollama/llama3), runs the registry lookup with HOME pointed at the temp dir, and verifies the banner output contains the registry model instead of the hardcoded default. Signed-off-by: Josue Gomez Signed-off-by: futhgar --- test/telegram-bridge-banner.test.js | 66 +++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/test/telegram-bridge-banner.test.js b/test/telegram-bridge-banner.test.js index 1971c06e6..c99c56317 100644 --- a/test/telegram-bridge-banner.test.js +++ b/test/telegram-bridge-banner.test.js @@ -30,4 +30,70 @@ describe("telegram bridge banner", () => { ); } }); + + it("displays a non-default model from the sandbox registry in the banner", () => { + // Behavioral test: set up a registry with a non-default model, + // then require the banner-building logic and verify the output. + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-banner-test-")); + const registryDir = path.join(tmp, ".nemoclaw"); + fs.mkdirSync(registryDir, { recursive: true }); + + // Write a sandbox registry with a non-default model + const registryData = { + sandboxes: { + "test-sandbox": { + name: "test-sandbox", + createdAt: new Date().toISOString(), + model: "ollama/llama3", + provider: "ollama-local", + gpuEnabled: false, + policies: [], + }, + }, + defaultSandbox: "test-sandbox", + }; + fs.writeFileSync( + path.join(registryDir, "sandboxes.json"), + JSON.stringify(registryData, null, 2), + ); + + // Run a script that loads the registry with HOME pointed at our temp dir, + // looks up the sandbox, and prints what the banner model line would be + const { spawnSync } = require("node:child_process"); + const result = spawnSync( + "node", + [ + "-e", + ` + const registry = require("./bin/lib/registry"); + const sandboxInfo = registry.getSandbox("test-sandbox"); + const model = sandboxInfo?.model || "nvidia/nemotron-3-super-120b-a12b"; + const line = " │ Model: " + (model + " ").slice(0, 40) + "│"; + console.log(line); + console.log("MODEL=" + model); + `, + ], + { + cwd: path.join(__dirname, ".."), + encoding: "utf-8", + timeout: 5000, + env: { ...process.env, HOME: tmp }, + }, + ); + + assert.equal(result.status, 0, `script failed: ${result.stderr}`); + const output = result.stdout; + + // The banner must show the registry model, not the hardcoded default + assert.match(output, /MODEL=ollama\/llama3/, "should resolve the model from the registry"); + assert.match(output, /ollama\/llama3/, "banner line should contain the registry model"); + assert.doesNotMatch( + output, + /nemotron-3-super/, + "banner should not fall back to the hardcoded default when registry has a model", + ); + + // Clean up + fs.rmSync(tmp, { recursive: true, force: true }); + }); }); From 8e0c6a8af676cad3feee5e6dce46b2f3301d519b Mon Sep 17 00:00:00 2001 From: futhgar Date: Thu, 19 Mar 2026 21:31:41 -0400 Subject: [PATCH 3/5] fix: surface provider and model from registry in /start response and banner Address kjw3 review: the /start Telegram response still hardcoded "Nemotron 3 Super 120B". Now reads both model and provider from the sandbox registry for both the /start message and the startup banner. Consistent identity across all user-facing output. Signed-off-by: Josue Gomez Signed-off-by: futhgar --- scripts/telegram-bridge.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/telegram-bridge.js b/scripts/telegram-bridge.js index 0127cb2e9..f36a5e6fe 100755 --- a/scripts/telegram-bridge.js +++ b/scripts/telegram-bridge.js @@ -185,9 +185,12 @@ async function poll() { // Handle /start if (msg.text === "/start") { + const info = registry.getSandbox(SANDBOX); + const startModel = info?.model || "nvidia/nemotron-3-super-120b-a12b"; + const startProvider = info?.provider || "nvidia-nim"; await sendMessage( chatId, - "🦀 *NemoClaw* — powered by Nemotron 3 Super 120B\n\n" + + `🦀 *NemoClaw* — ${startProvider} / ${startModel}\n\n` + "Send me a message and I'll run it through the OpenClaw agent " + "inside an OpenShell sandbox.\n\n" + "If the agent needs external access, the TUI will prompt for approval.", @@ -263,8 +266,10 @@ async function main() { console.log(" │ │"); const sandboxInfo = registry.getSandbox(SANDBOX); const model = sandboxInfo?.model || "nvidia/nemotron-3-super-120b-a12b"; + const provider = sandboxInfo?.provider || "nvidia-nim"; console.log(` │ Bot: @${(me.result.username + " ").slice(0, 37)}│`); console.log(" │ Sandbox: " + (SANDBOX + " ").slice(0, 40) + "│"); + console.log(` │ Provider: ${(provider + " ").slice(0, 40)}│`); console.log(` │ Model: ${(model + " ").slice(0, 40)}│`); console.log(" │ │"); console.log(" │ Messages are forwarded to the OpenClaw agent │"); From 1fb288109d139cf79b611797670b90b894dd9c0c Mon Sep 17 00:00:00 2001 From: futhgar Date: Thu, 19 Mar 2026 21:33:37 -0400 Subject: [PATCH 4/5] test: cover /start response uses registry provider and model Add structural test verifying the /start handler has no hardcoded model string, and a behavioral test confirming it reflects configured provider and model from the sandbox registry. Signed-off-by: Josue Gomez Signed-off-by: futhgar --- test/telegram-bridge-banner.test.js | 84 +++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/test/telegram-bridge-banner.test.js b/test/telegram-bridge-banner.test.js index c99c56317..faadf145e 100644 --- a/test/telegram-bridge-banner.test.js +++ b/test/telegram-bridge-banner.test.js @@ -96,4 +96,88 @@ describe("telegram bridge banner", () => { // Clean up fs.rmSync(tmp, { recursive: true, force: true }); }); + + it("/start response uses registry provider and model instead of hardcoding", () => { + const bridgeSrc = fs.readFileSync( + path.join(__dirname, "..", "scripts", "telegram-bridge.js"), + "utf-8", + ); + + // The /start handler must not contain a hardcoded model name string + const startBlock = bridgeSrc.slice( + bridgeSrc.indexOf('"/start"'), + bridgeSrc.indexOf("continue;", bridgeSrc.indexOf('"/start"')), + ); + assert.ok(startBlock.length > 0, "should find the /start handler block"); + assert.doesNotMatch( + startBlock, + /Nemotron 3 Super 120B/, + "/start handler should not contain hardcoded 'Nemotron 3 Super 120B'", + ); + // Must reference the registry for both provider and model + assert.match( + startBlock, + /registry\.getSandbox/, + "/start handler should look up the sandbox from the registry", + ); + assert.match( + startBlock, + /provider/i, + "/start handler should reference the provider", + ); + }); + + it("/start response reflects configured provider and model from registry", () => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-start-test-")); + const registryDir = path.join(tmp, ".nemoclaw"); + fs.mkdirSync(registryDir, { recursive: true }); + + const registryData = { + sandboxes: { + "test-sandbox": { + name: "test-sandbox", + createdAt: new Date().toISOString(), + model: "qwen2.5:14b-instruct", + provider: "ollama-local", + gpuEnabled: false, + policies: [], + }, + }, + defaultSandbox: "test-sandbox", + }; + fs.writeFileSync( + path.join(registryDir, "sandboxes.json"), + JSON.stringify(registryData, null, 2), + ); + + const { spawnSync } = require("node:child_process"); + const result = spawnSync( + "node", + [ + "-e", + ` + const registry = require("./bin/lib/registry"); + const info = registry.getSandbox("test-sandbox"); + const model = info?.model || "nvidia/nemotron-3-super-120b-a12b"; + const provider = info?.provider || "nvidia-nim"; + const startMsg = "\u{1F980} *NemoClaw* — " + provider + " / " + model; + console.log(startMsg); + `, + ], + { + cwd: path.join(__dirname, ".."), + encoding: "utf-8", + timeout: 5000, + env: { ...process.env, HOME: tmp }, + }, + ); + + assert.equal(result.status, 0, `script failed: ${result.stderr}`); + const output = result.stdout; + assert.match(output, /ollama-local/, "/start should contain the configured provider"); + assert.match(output, /qwen2\.5:14b-instruct/, "/start should contain the configured model"); + assert.doesNotMatch(output, /Nemotron/, "/start should not contain the hardcoded default"); + + fs.rmSync(tmp, { recursive: true, force: true }); + }); }); From bdd8fc5cbce81351291ee47edb667c5368dcc690 Mon Sep 17 00:00:00 2001 From: futhgar Date: Thu, 19 Mar 2026 21:39:05 -0400 Subject: [PATCH 5/5] test: add try/finally cleanup and document test boundary Address CodeRabbit review: - Wrap all temp dir tests in try/finally for guaranteed cleanup - Extract createTestRegistry() helper to reduce duplication - Add comment explaining why tests use registry.getSandbox() directly rather than the full bridge script (requires Telegram token + openshell) Signed-off-by: Josue Gomez Signed-off-by: futhgar --- test/telegram-bridge-banner.test.js | 206 ++++++++++++---------------- 1 file changed, 91 insertions(+), 115 deletions(-) diff --git a/test/telegram-bridge-banner.test.js b/test/telegram-bridge-banner.test.js index faadf145e..90aad2daa 100644 --- a/test/telegram-bridge-banner.test.js +++ b/test/telegram-bridge-banner.test.js @@ -6,20 +6,45 @@ const assert = require("node:assert/strict"); const fs = require("node:fs"); const os = require("node:os"); const path = require("node:path"); +const { spawnSync } = require("node:child_process"); + +/** + * Helper: create a temp dir with a sandbox registry containing a non-default + * model and provider. Returns the temp dir path. Caller must clean up. + */ +function createTestRegistry(sandboxName, model, provider) { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-bridge-test-")); + const registryDir = path.join(tmp, ".nemoclaw"); + fs.mkdirSync(registryDir, { recursive: true }); + fs.writeFileSync( + path.join(registryDir, "sandboxes.json"), + JSON.stringify({ + sandboxes: { + [sandboxName]: { + name: sandboxName, + createdAt: new Date().toISOString(), + model, + provider, + gpuEnabled: false, + policies: [], + }, + }, + defaultSandbox: sandboxName, + }, null, 2), + ); + return tmp; +} describe("telegram bridge banner", () => { it("reads the model from the sandbox registry instead of hardcoding it", () => { - // The banner source should reference registry.getSandbox, not a hardcoded model string const bridgeSrc = fs.readFileSync( path.join(__dirname, "..", "scripts", "telegram-bridge.js"), "utf-8", ); - // Must import and use the registry assert.match(bridgeSrc, /require\(.*registry.*\)/, "should import the registry module"); assert.match(bridgeSrc, /getSandbox/, "should call getSandbox to look up the model"); - // The banner Model line must use a variable, not a hardcoded model name const bannerLines = bridgeSrc.split("\n").filter((l) => l.includes("Model:") && l.includes("│")); assert.ok(bannerLines.length > 0, "should have a Model banner line"); for (const line of bannerLines) { @@ -31,70 +56,44 @@ describe("telegram bridge banner", () => { } }); - it("displays a non-default model from the sandbox registry in the banner", () => { - // Behavioral test: set up a registry with a non-default model, - // then require the banner-building logic and verify the output. - const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-banner-test-")); - const registryDir = path.join(tmp, ".nemoclaw"); - fs.mkdirSync(registryDir, { recursive: true }); + // Note: the behavioral tests below use registry.getSandbox() directly rather + // than executing the full telegram-bridge.js script. The bridge requires a + // valid TELEGRAM_BOT_TOKEN and running openshell binary to start, so it + // cannot be launched in a test environment. The structural test above guards + // against regressions in the source, and the behavioral tests verify the + // registry lookup contract that the bridge depends on. - // Write a sandbox registry with a non-default model - const registryData = { - sandboxes: { - "test-sandbox": { - name: "test-sandbox", - createdAt: new Date().toISOString(), - model: "ollama/llama3", - provider: "ollama-local", - gpuEnabled: false, - policies: [], + it("displays a non-default model from the sandbox registry in the banner", () => { + const tmp = createTestRegistry("test-sandbox", "ollama/llama3", "ollama-local"); + try { + const result = spawnSync( + "node", + [ + "-e", + ` + const registry = require("./bin/lib/registry"); + const sandboxInfo = registry.getSandbox("test-sandbox"); + const model = sandboxInfo?.model || "nvidia/nemotron-3-super-120b-a12b"; + const provider = sandboxInfo?.provider || "nvidia-nim"; + console.log("MODEL=" + model); + console.log("PROVIDER=" + provider); + `, + ], + { + cwd: path.join(__dirname, ".."), + encoding: "utf-8", + timeout: 5000, + env: { ...process.env, HOME: tmp }, }, - }, - defaultSandbox: "test-sandbox", - }; - fs.writeFileSync( - path.join(registryDir, "sandboxes.json"), - JSON.stringify(registryData, null, 2), - ); - - // Run a script that loads the registry with HOME pointed at our temp dir, - // looks up the sandbox, and prints what the banner model line would be - const { spawnSync } = require("node:child_process"); - const result = spawnSync( - "node", - [ - "-e", - ` - const registry = require("./bin/lib/registry"); - const sandboxInfo = registry.getSandbox("test-sandbox"); - const model = sandboxInfo?.model || "nvidia/nemotron-3-super-120b-a12b"; - const line = " │ Model: " + (model + " ").slice(0, 40) + "│"; - console.log(line); - console.log("MODEL=" + model); - `, - ], - { - cwd: path.join(__dirname, ".."), - encoding: "utf-8", - timeout: 5000, - env: { ...process.env, HOME: tmp }, - }, - ); - - assert.equal(result.status, 0, `script failed: ${result.stderr}`); - const output = result.stdout; - - // The banner must show the registry model, not the hardcoded default - assert.match(output, /MODEL=ollama\/llama3/, "should resolve the model from the registry"); - assert.match(output, /ollama\/llama3/, "banner line should contain the registry model"); - assert.doesNotMatch( - output, - /nemotron-3-super/, - "banner should not fall back to the hardcoded default when registry has a model", - ); + ); - // Clean up - fs.rmSync(tmp, { recursive: true, force: true }); + assert.equal(result.status, 0, `script failed: ${result.stderr}`); + assert.match(result.stdout, /MODEL=ollama\/llama3/); + assert.match(result.stdout, /PROVIDER=ollama-local/); + assert.doesNotMatch(result.stdout, /nemotron-3-super/); + } finally { + fs.rmSync(tmp, { recursive: true, force: true }); + } }); it("/start response uses registry provider and model instead of hardcoding", () => { @@ -103,7 +102,6 @@ describe("telegram bridge banner", () => { "utf-8", ); - // The /start handler must not contain a hardcoded model name string const startBlock = bridgeSrc.slice( bridgeSrc.indexOf('"/start"'), bridgeSrc.indexOf("continue;", bridgeSrc.indexOf('"/start"')), @@ -114,7 +112,6 @@ describe("telegram bridge banner", () => { /Nemotron 3 Super 120B/, "/start handler should not contain hardcoded 'Nemotron 3 Super 120B'", ); - // Must reference the registry for both provider and model assert.match( startBlock, /registry\.getSandbox/, @@ -128,56 +125,35 @@ describe("telegram bridge banner", () => { }); it("/start response reflects configured provider and model from registry", () => { - const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-start-test-")); - const registryDir = path.join(tmp, ".nemoclaw"); - fs.mkdirSync(registryDir, { recursive: true }); - - const registryData = { - sandboxes: { - "test-sandbox": { - name: "test-sandbox", - createdAt: new Date().toISOString(), - model: "qwen2.5:14b-instruct", - provider: "ollama-local", - gpuEnabled: false, - policies: [], + const tmp = createTestRegistry("test-sandbox", "qwen2.5:14b-instruct", "ollama-local"); + try { + const result = spawnSync( + "node", + [ + "-e", + ` + const registry = require("./bin/lib/registry"); + const info = registry.getSandbox("test-sandbox"); + const model = info?.model || "nvidia/nemotron-3-super-120b-a12b"; + const provider = info?.provider || "nvidia-nim"; + const startMsg = "\u{1F980} *NemoClaw* — " + provider + " / " + model; + console.log(startMsg); + `, + ], + { + cwd: path.join(__dirname, ".."), + encoding: "utf-8", + timeout: 5000, + env: { ...process.env, HOME: tmp }, }, - }, - defaultSandbox: "test-sandbox", - }; - fs.writeFileSync( - path.join(registryDir, "sandboxes.json"), - JSON.stringify(registryData, null, 2), - ); - - const { spawnSync } = require("node:child_process"); - const result = spawnSync( - "node", - [ - "-e", - ` - const registry = require("./bin/lib/registry"); - const info = registry.getSandbox("test-sandbox"); - const model = info?.model || "nvidia/nemotron-3-super-120b-a12b"; - const provider = info?.provider || "nvidia-nim"; - const startMsg = "\u{1F980} *NemoClaw* — " + provider + " / " + model; - console.log(startMsg); - `, - ], - { - cwd: path.join(__dirname, ".."), - encoding: "utf-8", - timeout: 5000, - env: { ...process.env, HOME: tmp }, - }, - ); - - assert.equal(result.status, 0, `script failed: ${result.stderr}`); - const output = result.stdout; - assert.match(output, /ollama-local/, "/start should contain the configured provider"); - assert.match(output, /qwen2\.5:14b-instruct/, "/start should contain the configured model"); - assert.doesNotMatch(output, /Nemotron/, "/start should not contain the hardcoded default"); + ); - fs.rmSync(tmp, { recursive: true, force: true }); + assert.equal(result.status, 0, `script failed: ${result.stderr}`); + assert.match(result.stdout, /ollama-local/); + assert.match(result.stdout, /qwen2\.5:14b-instruct/); + assert.doesNotMatch(result.stdout, /Nemotron/); + } finally { + fs.rmSync(tmp, { recursive: true, force: true }); + } }); });