Skip to content
Merged
Show file tree
Hide file tree
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
35 changes: 24 additions & 11 deletions src/lib/actions/sandbox/destroy-gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,19 +96,32 @@ export function cleanupGatewayAfterLastSandbox(
stopOptions.pidFile = path.join(perGatewayStateDir, "openshell-gateway.pid");
}
stopHostGatewayProcesses({}, stopOptions);
const removeResult = openshell(["gateway", "remove", gatewayName], {
ignoreError: true,
stdio: ["ignore", "pipe", "pipe"],
});
if (removeResult.status !== 0) {
openshell(["gateway", "destroy", "-g", gatewayName], {
ignoreError: true,
stdio: ["ignore", "pipe", "pipe"],
});
}
} else {
}
/**
* SOURCE_OF_TRUTH
* Invalid state: a pre-0.0.44 OpenShell CLI does not expose `gateway remove`.
* Source boundary: the installed CLI may predate the blueprint floor while
* an existing installation is being recovered or removed.
* Source-fix constraint: NemoClaw cannot add the modern verb to historical
* OpenShell builds, so cleanup tries their legacy verb best-effort.
* Regression proof: destroy-gateway-cleanup.test.ts covers successful remove
* and remove-nonzero fallback while preserving Docker-volume cleanup.
* Removal condition: remove the fallback when every supported recovery and
* teardown entry point upgrades OpenShell to the blueprint minimum (currently
* 0.0.72) before this function can run.
*
* macOS previously ran only `gateway destroy`, which current OpenShell
* rejects as an unrecognized subcommand (#6569). The host-process stop above
* remains Linux-only.
*/
const removeResult = openshell(["gateway", "remove", gatewayName], {
ignoreError: true,
stdio: ["ignore", "pipe", "pipe"],
});
if (removeResult.status !== 0) {
openshell(["gateway", "destroy", "-g", gatewayName], {
ignoreError: true,
stdio: ["ignore", "pipe", "pipe"],
});
}
dockerRemoveVolumesByPrefix(`openshell-cluster-${gatewayName}`, {
Expand Down
8 changes: 4 additions & 4 deletions src/lib/actions/sandbox/destroy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -439,10 +439,10 @@ async function destroySandboxUnlocked(
if (shouldCleanupGateway) {
cleanupGatewayAfterLastSandbox(cleanupGatewayName, runOpenshell);
} else {
const gatewayRemovalHint =
process.platform === "linux"
? `openshell gateway remove ${cleanupGatewayName}`
: `openshell gateway destroy -g ${cleanupGatewayName}`;
// `gateway remove <name>` is the modern OpenShell subcommand on every
// platform; the old `gateway destroy -g` was pre-0.0.44 only and current
// OpenShell rejects it as unrecognized, so never recommend it (#6569).
const gatewayRemovalHint = `openshell gateway remove ${cleanupGatewayName}`;
console.log(
` Shared NemoClaw gateway preserved. Re-run '${gatewayRemovalHint}' to remove it,`,
);
Expand Down
27 changes: 19 additions & 8 deletions test/cli/destroy-gateway-cleanup.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { describe, it, expect } from "vitest";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";

import { runWithEnv, testTimeoutOptions } from "./helpers";

Expand Down Expand Up @@ -74,10 +74,14 @@ describe("CLI dispatch", () => {
expect(openshellOutput).not.toContain("gateway destroy -g nemoclaw");
expect(openshellOutput).not.toContain("gateway remove nemoclaw");
expect(fs.readFileSync(bashLog, "utf8")).not.toContain("volume ls -q --filter");
// The preserved-gateway hint must recommend the real subcommand
// (`gateway remove`), never the nonexistent `gateway destroy` (#6569).
expect(r.out).toContain("openshell gateway remove nemoclaw");
expect(r.out).not.toContain("gateway destroy");
});

it(
"tears down the gateway runtime when --cleanup-gateway is passed (#2166)",
"falls back to legacy gateway destroy and still cleans volumes when remove fails (#6569)",
testTimeoutOptions(30_000),
() => {
const home = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-cli-destroy-last-cleanup-"));
Expand Down Expand Up @@ -115,6 +119,9 @@ describe("CLI dispatch", () => {
" exit 0",
"fi",
'printf \'%s\\n\' "$*" >> "$log_file"',
'if [ "$1" = "gateway" ] && [ "$2" = "remove" ]; then',
" exit 1",
"fi",
"exit 0",
].join("\n"),
{ mode: 0o755 },
Expand Down Expand Up @@ -145,10 +152,11 @@ describe("CLI dispatch", () => {
const openshellOutput = fs.readFileSync(openshellLog, "utf8");
expect(openshellOutput).toContain("sandbox delete alpha");
expect(openshellOutput).toContain("forward stop 18789");
expect(openshellOutput).toContain(
process.platform === "linux"
? "gateway remove nemoclaw-8081"
: "gateway destroy -g nemoclaw-8081",
// `gateway remove` is the modern subcommand on every platform (#6569).
expect(openshellOutput).toContain("gateway remove nemoclaw-8081");
expect(openshellOutput).toContain("gateway destroy -g nemoclaw-8081");
expect(openshellOutput.indexOf("gateway remove nemoclaw-8081")).toBeLessThan(
openshellOutput.indexOf("gateway destroy -g nemoclaw-8081"),
);
expect(fs.readFileSync(bashLog, "utf8")).toContain(
"volume ls -q --filter name=openshell-cluster-nemoclaw-8081",
Expand Down Expand Up @@ -223,8 +231,11 @@ describe("CLI dispatch", () => {
expect(r.code, r.out).toBe(0);
const openshellOutput = fs.readFileSync(openshellLog, "utf8");
expect(openshellOutput).toContain("forward stop 18789");
expect(openshellOutput).toContain(
process.platform === "linux" ? "gateway remove nemoclaw" : "gateway destroy -g nemoclaw",
// `gateway remove` is the modern subcommand on every platform (#6569).
expect(openshellOutput).toContain("gateway remove nemoclaw");
expect(openshellOutput).not.toContain("gateway destroy -g nemoclaw");
expect(fs.readFileSync(bashLog, "utf8")).toContain(
"volume ls -q --filter name=openshell-cluster-nemoclaw",
);
},
);
Expand Down
Loading