fix(onboard): skip Homebrew prompt on unsupported platforms (#93521)

Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
This commit is contained in:
Vincent Koc
2026-06-16 14:30:27 +08:00
committed by GitHub
parent 52280351bb
commit d2439d2f7d
3 changed files with 52 additions and 17 deletions
+1
View File
@@ -23,6 +23,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Onboarding/skills: show the Homebrew install recommendation only on macOS and Linux, so FreeBSD and other unsupported platforms no longer get a misleading brew prompt. Fixes #68893; carries forward #68894, #68910, #68941, #68943, #69002, and #69545. Thanks @yurivict, @Sanjays2402, @Eruditi, @JustInCache, @nnish16, and @Mlightsnow.
- Channels and delivery: preserve account-scoped DM channel send policy, rich Telegram final replies, rich Telegram tables and lists, Telegram thread-create CLI remapping, Slack outbound `message_sent` hooks, contributed message-tool schema optionality, same-channel generated media completions, and channel chunking around surrogate pairs and Infinity limits. (#92788, #92679, #89421, #89943, #91137, #91246, #92735) Thanks @yetval, @obviyus, @spacegeologist, @rishitamrakar, @lundog, @TurboTheTurtle, and @yhterrance.
- iMessage: normalize leading NUL sent-message echo prefixes while preserving interior NUL bytes and the leading attributedBody marker handling from #73942. Carries forward #63581. Thanks @drvoss.
- Discord: give generated auto-thread titles a 60-second timeout and 4,096-token reasoning-model output budget, clamped to the selected model output cap. (#64734) Thanks @hanamizuki.
+44 -16
View File
@@ -1,5 +1,5 @@
// Onboard skills tests cover skill setup prompts, package manager config, and skip behavior.
import { afterEach, describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import type { RuntimeEnv } from "../runtime.js";
import type { WizardPrompter } from "../wizard/prompts.js";
@@ -143,16 +143,30 @@ const runtime: RuntimeEnv = {
}) as RuntimeEnv["exit"],
};
const supportsHomebrewPrompt = process.platform === "darwin" || process.platform === "linux";
async function withPlatform<T>(platform: NodeJS.Platform, fn: () => Promise<T>): Promise<T> {
const originalPlatformDescriptor = Object.getOwnPropertyDescriptor(process, "platform")!;
Object.defineProperty(process, "platform", {
configurable: true,
value: platform,
});
try {
return await fn();
} finally {
Object.defineProperty(process, "platform", originalPlatformDescriptor);
}
}
describe("setupSkills", () => {
afterEach(() => {
beforeEach(() => {
vi.clearAllMocks();
mocks.isContainerEnvironment.mockReset();
mocks.resolveBrewExecutable.mockReset();
});
it("hides brew-only installs in Linux containers when brew is missing", async () => {
const originalPlatformDescriptor = Object.getOwnPropertyDescriptor(process, "platform")!;
Object.defineProperty(process, "platform", { value: "linux", configurable: true });
try {
await withPlatform("linux", async () => {
mockMissingBrewStatus([
createBundledSkill({
name: "video-frames",
@@ -173,15 +187,11 @@ describe("setupSkills", () => {
expect(
notes.find((n) => n.message.includes("No missing skill dependencies to install")),
).toBeUndefined();
} finally {
Object.defineProperty(process, "platform", originalPlatformDescriptor);
}
});
});
it("keeps brew-only installs visible when Linuxbrew is resolved off PATH", async () => {
const originalPlatformDescriptor = Object.getOwnPropertyDescriptor(process, "platform")!;
Object.defineProperty(process, "platform", { value: "linux", configurable: true });
try {
await withPlatform("linux", async () => {
mockMissingBrewStatus([
createBundledSkill({
name: "video-frames",
@@ -202,13 +212,11 @@ describe("setupSkills", () => {
);
expect(notes.find((n) => n.title === "Container skill installs")).toBeUndefined();
expect(notes.find((n) => n.title === "Homebrew recommended")).toBeUndefined();
} finally {
Object.defineProperty(process, "platform", originalPlatformDescriptor);
}
});
});
it("does not recommend Homebrew when user skips installing brew-backed deps", async () => {
if (process.platform === "win32") {
if (!supportsHomebrewPrompt) {
return;
}
@@ -247,7 +255,7 @@ describe("setupSkills", () => {
});
it("recommends Homebrew when user selects a brew-backed install and brew is missing", async () => {
if (process.platform === "win32") {
if (!supportsHomebrewPrompt) {
return;
}
@@ -279,4 +287,24 @@ describe("setupSkills", () => {
expect(emptyStateNote?.message).toContain("openclaw skills list --verbose");
expect(emptyStateNote?.message).toContain("openclaw skills check");
});
it("does not recommend Homebrew on FreeBSD", async () => {
await withPlatform("freebsd", async () => {
mockMissingBrewStatus([
createBundledSkill({
name: "video-frames",
description: "ffmpeg",
bins: ["ffmpeg"],
installLabel: "Install ffmpeg (brew)",
}),
]);
const { prompter, notes } = createPrompter({ multiselect: ["video-frames"] });
await setupSkills({} as OpenClawConfig, "/tmp/ws", runtime, prompter);
const brewNote = notes.find((n) => n.title === "Homebrew recommended");
expect(brewNote).toBeUndefined();
expect(mocks.detectBinary).not.toHaveBeenCalledWith("brew");
});
});
});
+7 -1
View File
@@ -16,6 +16,12 @@ import { t } from "../wizard/i18n/index.js";
import type { WizardPrompter } from "../wizard/prompts.js";
import { detectBinary, resolveNodeManagerOptions } from "./onboard-helpers.js";
const HOMEBREW_PROMPT_PLATFORMS = new Set(["darwin", "linux"]);
function supportsHomebrewPrompt(platform: NodeJS.Platform): boolean {
return HOMEBREW_PROMPT_PLATFORMS.has(platform);
}
function summarizeInstallFailure(message: string): string | undefined {
const cleaned = message.replace(/^Install failed(?:\s*\([^)]*\))?\s*:?\s*/i, "").trim();
if (!cleaned) {
@@ -145,7 +151,7 @@ export async function setupSkills(
.filter((item): item is (typeof installable)[number] => Boolean(item));
const needsBrewPrompt =
process.platform !== "win32" &&
supportsHomebrewPrompt(process.platform) &&
selectedSkills.some((skill) => skill.install.some((option) => option.kind === "brew")) &&
!(await detectBrewOnce());