Skip to content

Commit fa28c4c

Browse files
committed
test(uninstall): prove Homebrew executable retention
1 parent d10fa00 commit fa28c4c

1 file changed

Lines changed: 59 additions & 34 deletions

File tree

src/lib/actions/uninstall/run-plan-homebrew-openshell.test.ts

Lines changed: 59 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
22
// SPDX-License-Identifier: Apache-2.0
33

4+
import fs from "node:fs";
5+
import os from "node:os";
46
import path from "node:path";
57

68
import { expect, it, vi } from "vitest";
@@ -12,12 +14,12 @@ import {
1214
} from "./run-plan";
1315

1416
const FORMULA = "nvidia/openshell/openshell";
15-
const EXECUTABLE_NAMES = new Set([
17+
const EXECUTABLE_NAMES = [
1618
"openshell",
1719
"openshell-driver-vm",
1820
"openshell-gateway",
1921
"openshell-sandbox",
20-
]);
22+
] as const;
2123

2224
function ok(stdout = ""): RunResult {
2325
return { status: 0, stdout, stderr: "" };
@@ -42,51 +44,62 @@ function runUninstallPlan(deps: UninstallRunDeps) {
4244
);
4345
}
4446

45-
function uninstallOnMacOs(options: { brewAvailable: boolean; brewStatus: number | null }) {
47+
function uninstallOpenShell(options: {
48+
brewAvailable: boolean;
49+
brewStatus: number | null;
50+
platform?: NodeJS.Platform;
51+
}) {
52+
const home = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-uninstall-homebrew-"));
53+
const userBin = path.join(home, ".local", "bin");
54+
const executablePaths = EXECUTABLE_NAMES.map((name) => path.join(userBin, name));
55+
fs.mkdirSync(userBin, { mode: 0o700, recursive: true });
56+
for (const executablePath of executablePaths) {
57+
fs.writeFileSync(executablePath, "", { mode: 0o755 });
58+
}
59+
4660
const calls: string[][] = [];
4761
const logs: string[] = [];
4862
const removed: string[] = [];
63+
const existing = new Set(executablePaths);
4964

50-
const result = runUninstallPlan({
51-
commandExists: (command) =>
52-
command === "openshell" || (command === "brew" && options.brewAvailable),
53-
env: { HOME: "/tmp/nemoclaw-uninstall-homebrew" } as NodeJS.ProcessEnv,
54-
existsSync: (target) =>
55-
EXECUTABLE_NAMES.has(path.basename(String(target))) &&
56-
path.basename(path.dirname(String(target))) === "bin",
57-
hasPortableRuntimeCleanup: () => false,
58-
isTty: false,
59-
log: (line) => logs.push(line),
60-
platform: "darwin",
61-
rmSync: vi.fn((target) => removed.push(String(target))),
62-
run: vi.fn((command, args) => {
63-
calls.push([command, ...args]);
64-
return command === "openshell" && args[0] === "gateway" && args[1] === "list"
65-
? ok(JSON.stringify([{ name: "nemoclaw" }]))
66-
: command === "brew" && args[0] === "list"
67-
? { status: options.brewStatus, stdout: "", stderr: "" }
68-
: ok();
69-
}),
70-
runDocker: () => ok(),
71-
});
72-
73-
return { calls, logs, removed, result };
74-
}
65+
try {
66+
const result = runUninstallPlan({
67+
commandExists: (command) =>
68+
command === "openshell" || (command === "brew" && options.brewAvailable),
69+
env: { HOME: home } as NodeJS.ProcessEnv,
70+
existsSync: (target) => existing.has(target) && fs.existsSync(target),
71+
hasPortableRuntimeCleanup: () => false,
72+
isTty: false,
73+
log: (line) => logs.push(line),
74+
platform: options.platform ?? "darwin",
75+
rmSync: vi.fn((target) => removed.push(String(target))),
76+
run: vi.fn((command, args) => {
77+
calls.push([command, ...args]);
78+
return command === "openshell" && args[0] === "gateway" && args[1] === "list"
79+
? ok(JSON.stringify([{ name: "nemoclaw" }]))
80+
: command === "brew" && args[0] === "list"
81+
? { status: options.brewStatus, stdout: "", stderr: "" }
82+
: ok();
83+
}),
84+
runDocker: () => ok(),
85+
});
7586

76-
function removedOpenShellExecutables(removed: readonly string[]): string[] {
77-
return removed.filter((target) => EXECUTABLE_NAMES.has(path.basename(target)));
87+
return { calls, executablePaths, logs, removed, result };
88+
} finally {
89+
fs.rmSync(home, { force: true, recursive: true });
90+
}
7891
}
7992

8093
it("retains a Homebrew-managed OpenShell and reports its removal command (#8882)", () => {
81-
const { calls, logs, removed, result } = uninstallOnMacOs({
94+
const { calls, logs, removed, result } = uninstallOpenShell({
8295
brewAvailable: true,
8396
brewStatus: 0,
8497
});
8598

8699
expect(result.exitCode).toBe(0);
87100
expect(calls).toContainEqual(["brew", "list", "--formula", FORMULA]);
88101
expect(calls.some((call) => call[0] === "brew" && call[1] === "uninstall")).toBe(false);
89-
expect(removedOpenShellExecutables(removed)).toEqual([]);
102+
expect(removed).toEqual([]);
90103
expect(logs).toContain(
91104
`Kept Homebrew-managed OpenShell. To remove it, run: brew uninstall ${FORMULA}`,
92105
);
@@ -112,12 +125,24 @@ it.each([
112125
report: `Kept OpenShell executables because Homebrew did not confirm ${FORMULA}. Check the formula before removing OpenShell.`,
113126
},
114127
])("retains OpenShell when $label (#8882)", ({ brewAvailable, brewStatus, report }) => {
115-
const { calls, logs, removed, result } = uninstallOnMacOs({ brewAvailable, brewStatus });
128+
const { calls, logs, removed, result } = uninstallOpenShell({ brewAvailable, brewStatus });
116129

117130
expect(result.exitCode).toBe(0);
118131
expect(calls.filter((call) => call[0] === "brew")).toEqual(
119132
brewAvailable ? [["brew", "list", "--formula", FORMULA]] : [],
120133
);
121-
expect(removedOpenShellExecutables(removed)).toEqual([]);
134+
expect(removed).toEqual([]);
122135
expect(logs).toContain(report);
123136
});
137+
138+
it("removes managed OpenShell executables on Linux (#8882)", () => {
139+
const { executablePaths, removed, result } = uninstallOpenShell({
140+
brewAvailable: false,
141+
brewStatus: 0,
142+
platform: "linux",
143+
});
144+
145+
expect(result.exitCode).toBe(0);
146+
expect(new Set(removed)).toEqual(new Set(executablePaths));
147+
expect(removed).toHaveLength(executablePaths.length);
148+
});

0 commit comments

Comments
 (0)