Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 181 additions & 0 deletions src/lib/actions/sandbox/destroy-flow.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { createRequire } from "node:module";

import { afterEach, beforeEach, describe, expect, it, type MockInstance, vi } from "vitest";

type DestroySandbox =
typeof import("../../../../dist/lib/actions/sandbox/destroy")["destroySandbox"];

const requireDist = createRequire(import.meta.url);
const destroyModulePath = "../../../../dist/lib/actions/sandbox/destroy.js";

type DestroyHarness = {
cleanupGatewaySpy: MockInstance;
destroySandbox: DestroySandbox;
killStaleProxySpy: MockInstance;
logSpy: MockInstance;
removeSandboxSpy: MockInstance;
runOpenshellSpy: MockInstance;
selectGatewaySpy: MockInstance;
stopNimByNameSpy: MockInstance;
unloadOllamaModelsSpy: MockInstance;
};

type DestroyHarnessOptions = {
deleteStatus?: number;
deleteOutput?: string;
};

const sandboxEntry = {
name: "alpha",
provider: "ollama-local",
model: "nvidia/nemotron",
imageTag: null,
nimContainer: "alpha-nim",
gatewayName: "nemoclaw-19080",
gatewayPort: 19080,
};

function createDestroyHarness(options: DestroyHarnessOptions = {}): DestroyHarness {
delete require.cache[requireDist.resolve(destroyModulePath)];

const logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined);
vi.spyOn(console, "error").mockImplementation(() => undefined);
vi.spyOn(console, "warn").mockImplementation(() => undefined);

const resolve = requireDist("../../../../dist/lib/adapters/openshell/resolve.js");
const runtime = requireDist("../../../../dist/lib/adapters/openshell/runtime.js");
const destroyGateway = requireDist("../../../../dist/lib/actions/sandbox/destroy-gateway.js");
const sandboxProviderCleanup = requireDist(
"../../../../dist/lib/onboard/sandbox-provider-cleanup.js",
);
const nim = requireDist("../../../../dist/lib/inference/nim.js");
const ollamaProxy = requireDist("../../../../dist/lib/inference/ollama/proxy.js");
const tunnelServices = requireDist("../../../../dist/lib/tunnel/services.js");
const onboardSession = requireDist("../../../../dist/lib/state/onboard-session.js");
const registry = requireDist("../../../../dist/lib/state/registry.js");
const sandboxSession = requireDist("../../../../dist/lib/state/sandbox-session.js");
const timerControl = requireDist("../../../../dist/lib/shields/timer-control.js");

vi.spyOn(resolve, "resolveOpenshell").mockReturnValue("/usr/bin/openshell");
vi.spyOn(sandboxSession, "getActiveSandboxSessions").mockReturnValue({
detected: true,
sessions: [{ pid: 1 }],
});
vi.spyOn(registry, "getSandbox").mockReturnValue(sandboxEntry);
vi.spyOn(registry, "listSandboxes").mockReturnValue({ sandboxes: [] });
const removeSandboxSpy = vi.spyOn(registry, "removeSandbox").mockReturnValue(true);
vi.spyOn(onboardSession, "loadSession").mockReturnValue({ sandboxName: "alpha" });
vi.spyOn(onboardSession, "updateSession").mockImplementation((mutator: unknown) => {
const session = { sandboxName: "alpha" };
if (typeof mutator === "function") (mutator as (value: typeof session) => void)(session);
return session;
});
const runOpenshellSpy = vi.spyOn(runtime, "runOpenshell").mockImplementation((args: unknown) => {
const argv = Array.isArray(args) ? args : [];
if (argv[0] === "sandbox" && argv[1] === "delete") {
return { status: options.deleteStatus ?? 0, stdout: options.deleteOutput ?? "", stderr: "" };
}
return { status: 0, stdout: "", stderr: "" };
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
vi.spyOn(runtime, "captureOpenshell").mockReturnValue({ status: 0, output: "" });
const selectGatewaySpy = vi
.spyOn(destroyGateway, "selectGatewayForSandboxDestroy")
.mockImplementation(() => undefined);
const cleanupGatewaySpy = vi
.spyOn(destroyGateway, "cleanupGatewayAfterLastSandbox")
.mockImplementation(() => undefined);
vi.spyOn(sandboxProviderCleanup, "runSandboxProviderPreDeleteCleanup").mockReturnValue({
failures: [],
});
vi.spyOn(sandboxProviderCleanup, "emitProviderDetachResidualHint").mockImplementation(
() => undefined,
);
const stopNimByNameSpy = vi
.spyOn(nim, "stopNimContainerByName")
.mockImplementation(() => undefined);
vi.spyOn(nim, "stopNimContainer").mockImplementation(() => undefined);
const killStaleProxySpy = vi
.spyOn(ollamaProxy, "killStaleProxy")
.mockImplementation(() => undefined);
const unloadOllamaModelsSpy = vi
.spyOn(ollamaProxy, "unloadOllamaModels")
.mockImplementation(() => undefined);
vi.spyOn(tunnelServices, "stopAll").mockImplementation(() => undefined);
vi.spyOn(timerControl, "killTimer").mockReturnValue({ warnings: [] });

logSpy.mockClear();

return {
cleanupGatewaySpy,
destroySandbox: requireDist(destroyModulePath).destroySandbox,
killStaleProxySpy,
logSpy,
removeSandboxSpy,
runOpenshellSpy,
selectGatewaySpy,
stopNimByNameSpy,
unloadOllamaModelsSpy,
};
}

describe("destroySandbox flow", () => {
let exitSpy: MockInstance;

beforeEach(() => {
exitSpy = vi.spyOn(process, "exit").mockImplementation(((code?: number | string | null) => {
throw new Error(`process.exit(${code ?? 0})`);
}) as never);
});

afterEach(() => {
vi.restoreAllMocks();
delete require.cache[requireDist.resolve(destroyModulePath)];
});

it("selects the sandbox gateway, deletes live resources, cleans host state, and removes registry state", async () => {
const harness = createDestroyHarness();

await expect(
harness.destroySandbox("alpha", { yes: true, cleanupGateway: true }),
).resolves.toBeUndefined();

expect(harness.selectGatewaySpy).toHaveBeenCalledWith(
"alpha",
"nemoclaw-19080",
harness.runOpenshellSpy,
);
expect(harness.stopNimByNameSpy).toHaveBeenCalledWith("alpha-nim");
expect(harness.killStaleProxySpy).toHaveBeenCalledTimes(1);
expect(harness.runOpenshellSpy).toHaveBeenCalledWith(
["sandbox", "delete", "alpha"],
expect.objectContaining({ ignoreError: true }),
);
expect(harness.unloadOllamaModelsSpy).toHaveBeenCalledTimes(1);
expect(harness.removeSandboxSpy).toHaveBeenCalledWith("alpha");
expect(harness.cleanupGatewaySpy).toHaveBeenCalledWith(
"nemoclaw-19080",
harness.runOpenshellSpy,
);
expect(harness.logSpy.mock.calls.map((call) => String(call[0])).join("\n")).toContain(
"Sandbox 'alpha' destroyed",
);
expect(exitSpy).not.toHaveBeenCalled();
});

it("stops before local cleanup when OpenShell fails to delete the live sandbox", async () => {
const harness = createDestroyHarness({ deleteStatus: 7, deleteOutput: "delete failed" });

await expect(harness.destroySandbox("alpha", { yes: true })).rejects.toThrow("process.exit(7)");

expect(harness.runOpenshellSpy).toHaveBeenCalledWith(
["sandbox", "delete", "alpha"],
expect.objectContaining({ ignoreError: true }),
);
expect(harness.removeSandboxSpy).not.toHaveBeenCalled();
expect(harness.cleanupGatewaySpy).not.toHaveBeenCalled();
expect(exitSpy).toHaveBeenCalledWith(7);
});
});