Repository navigation
refactor(cli): centralize sandbox runtime bridge #2816
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
116111c
5e997cb
88cd958
3f748d6
0bc877a
3fd15fc
81c62bb
7d6a6fb
3f17a4f
0fb3c41
d11eeee
74806ce
e637e84
ee20dfb
8d3a568
b447f36
02431db
37ce2ff
d3e218e
df05a3e
3edcf2e
47df3b5
460173c
31c0195
481c758
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| import fs from "node:fs"; | ||
| import os from "node:os"; | ||
| import path from "node:path"; | ||
| import { describe, expect, it } from "vitest"; | ||
|
|
||
| import { parseDuration } from "./duration"; | ||
| import { parseGatewayTokenArgs, runGatewayTokenCommand } from "./gateway-token-command"; | ||
| import { resolveDefaultSandboxName, runStartCommand, runStopCommand } from "./services-command"; | ||
| import { getVersion } from "./version"; | ||
|
|
||
| // Narrow coverage guard for small helper modules that are otherwise only | ||
| // exercised through subprocess CLI flows in this migration stack. | ||
| describe("small CLI helper coverage", () => { | ||
| it("parses durations and rejects invalid values", () => { | ||
| expect(parseDuration("5m")).toBe(300); | ||
| expect(() => parseDuration("31m")).toThrow(/exceeds maximum/); | ||
| }); | ||
|
|
||
| it("parses and runs gateway-token helpers", () => { | ||
| expect(parseGatewayTokenArgs(["--quiet", "extra"])).toEqual({ | ||
| options: { quiet: true }, | ||
| unknown: ["extra"], | ||
| }); | ||
|
|
||
| const output: string[] = []; | ||
| const warnings: string[] = []; | ||
| expect( | ||
| runGatewayTokenCommand( | ||
| "alpha", | ||
| { quiet: false }, | ||
| { | ||
| fetchToken: () => "token-123", | ||
| log: (message) => output.push(message), | ||
| error: (message) => warnings.push(message), | ||
| }, | ||
| ), | ||
| ).toBe(0); | ||
| expect(output).toEqual(["token-123"]); | ||
| expect(warnings.join("\n")).toContain("Treat this token like a password"); | ||
|
|
||
| expect( | ||
| runGatewayTokenCommand( | ||
| "alpha", | ||
| {}, | ||
| { fetchToken: () => null, log: () => undefined, error: (message) => warnings.push(message) }, | ||
| ), | ||
| ).toBe(1); | ||
| }); | ||
|
|
||
| it("resolves service command sandbox names", async () => { | ||
| expect(resolveDefaultSandboxName(() => ({ defaultSandbox: "alpha" }))).toBe("alpha"); | ||
| expect(resolveDefaultSandboxName(() => ({ defaultSandbox: "bad name" }))).toBeUndefined(); | ||
|
|
||
| const startCalls: Array<{ sandboxName?: string }> = []; | ||
| await runStartCommand({ | ||
| listSandboxes: () => ({ defaultSandbox: "alpha" }), | ||
| startAll: async (options) => { | ||
| startCalls.push(options); | ||
| }, | ||
| }); | ||
| expect(startCalls).toEqual([{ sandboxName: "alpha" }]); | ||
|
|
||
| const stopCalls: Array<{ sandboxName?: string }> = []; | ||
| runStopCommand({ | ||
| listSandboxes: () => ({ defaultSandbox: null }), | ||
| stopAll: (options) => { | ||
| stopCalls.push(options); | ||
| }, | ||
| }); | ||
| expect(stopCalls).toEqual([{}]); | ||
| }); | ||
|
|
||
| it("reads version fallback files", () => { | ||
| const root = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-version-")); | ||
| fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ version: "9.8.7" })); | ||
| expect(getVersion({ rootDir: root })).toBe("9.8.7"); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,8 @@ | ||
| // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| /* v8 ignore start -- table-driven dispatch covered through CLI integration tests. */ | ||
|
|
||
|
Comment on lines
+4
to
+5
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Description: Verify V8 coverage directive usage patterns in the codebase
# Check for v8 ignore directives and their pairing
echo "=== Checking v8 ignore directive patterns ==="
rg -n "v8 ignore (start|stop)" --type ts --type js
# Check for c8 ignore directives (alternative syntax)
echo -e "\n=== Checking c8 ignore directive patterns ==="
rg -n "c8 ignore (start|stop)" --type ts --type js
# Look for files with start but no stop
echo -e "\n=== Files with potentially unpaired directives ==="
for file in $(rg -l "v8 ignore start" --type ts --type js); do
starts=$(rg -c "v8 ignore start" "$file")
stops=$(rg -c "v8 ignore stop" "$file" || echo "0")
if [ "$starts" != "$stops" ]; then
echo "$file: $starts start(s), $stops stop(s)"
fi
doneRepository: NVIDIA/NemoClaw Length of output: 4918 Add the missing The V8 coverage directive Note: This pattern affects all 27 files in 🔧 Proposed fixAdd the closing directive at the end of the file: default:
return { kind: "unknownAction", action };
}
}
+
+/* v8 ignore stop */🤖 Prompt for AI Agents |
||
| export type OclifDispatch = { | ||
| kind: "oclif"; | ||
| commandId: string; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| /* v8 ignore start -- transitional bridge until command actions are extracted from src/nemoclaw.ts. */ | ||
|
|
||
| export interface SpawnLikeResult { | ||
| status: number | null; | ||
| stdout?: string | Buffer; | ||
| stderr?: string | Buffer; | ||
| } | ||
|
|
||
| export interface GatewayRecoveryResult { | ||
| recovered: boolean; | ||
| } | ||
|
|
||
| export interface SandboxConnectOptions { | ||
| probeOnly?: boolean; | ||
| } | ||
|
|
||
| export interface NemoClawRuntimeBridge { | ||
| backupAll: () => void; | ||
| deploy: (instanceName?: string) => Promise<void>; | ||
| garbageCollectImages: (args?: string[]) => Promise<void>; | ||
| help: () => void; | ||
| onboard: (args?: string[]) => Promise<void>; | ||
| recoverNamedGatewayRuntime: () => Promise<GatewayRecoveryResult>; | ||
| recoverRegistryEntries: (options?: { requestedSandboxName?: string | null }) => Promise<unknown>; | ||
| runOpenshell: ( | ||
| args: string[], | ||
| opts?: { ignoreError?: boolean; stdio?: import("node:child_process").StdioOptions }, | ||
| ) => SpawnLikeResult; | ||
| sandboxChannelsAdd: (sandboxName: string, args?: string[]) => Promise<void>; | ||
| sandboxChannelsList: (sandboxName: string) => void; | ||
| sandboxChannelsRemove: (sandboxName: string, args?: string[]) => Promise<void>; | ||
| sandboxChannelsStart: (sandboxName: string, args?: string[]) => Promise<void>; | ||
| sandboxChannelsStop: (sandboxName: string, args?: string[]) => Promise<void>; | ||
| sandboxConnect: (sandboxName: string, options?: SandboxConnectOptions) => Promise<void>; | ||
| sandboxDestroy: (sandboxName: string, args?: string[]) => Promise<void>; | ||
| sandboxLogs: (sandboxName: string, follow: boolean) => void; | ||
| sandboxPolicyAdd: (sandboxName: string, args?: string[]) => Promise<void>; | ||
| sandboxPolicyList: (sandboxName: string) => void; | ||
| sandboxPolicyRemove: (sandboxName: string, args?: string[]) => Promise<void>; | ||
| sandboxRebuild: (sandboxName: string, args?: string[]) => Promise<void>; | ||
| sandboxSkillInstall: (sandboxName: string, args?: string[]) => Promise<void>; | ||
| sandboxSnapshot: (sandboxName: string, subArgs: string[]) => Promise<void>; | ||
| sandboxStatus: (sandboxName: string) => Promise<void>; | ||
| setup: (args?: string[]) => Promise<void>; | ||
| setupSpark: (args?: string[]) => Promise<void>; | ||
| upgradeSandboxes: (args?: string[]) => Promise<void>; | ||
| version: () => void; | ||
| } | ||
|
|
||
| let runtimeFactory = (): NemoClawRuntimeBridge => require("../nemoclaw") as NemoClawRuntimeBridge; | ||
|
|
||
| export function setNemoClawRuntimeBridgeFactoryForTest( | ||
| factory: () => NemoClawRuntimeBridge, | ||
| ): void { | ||
| runtimeFactory = factory; | ||
| } | ||
|
|
||
| export function getNemoClawRuntimeBridge(): NemoClawRuntimeBridge { | ||
| return runtimeFactory(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clean up temporary directory after test.
The test creates a temporary directory (line 77) but never removes it. This leaks filesystem resources on each test run.
🧹 Proposed fix to add cleanup
it("reads version fallback files", () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-version-")); - fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ version: "9.8.7" })); - expect(getVersion({ rootDir: root })).toBe("9.8.7"); + try { + fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ version: "9.8.7" })); + expect(getVersion({ rootDir: root })).toBe("9.8.7"); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } });📝 Committable suggestion
🤖 Prompt for AI Agents