diff --git a/CHANGELOG.md b/CHANGELOG.md index df5aff694fa8..6e13fdbb1258 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/commands/onboard-skills.test.ts b/src/commands/onboard-skills.test.ts index 905c5e909b50..050da74e197c 100644 --- a/src/commands/onboard-skills.test.ts +++ b/src/commands/onboard-skills.test.ts @@ -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(platform: NodeJS.Platform, fn: () => Promise): Promise { + 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"); + }); + }); }); diff --git a/src/commands/onboard-skills.ts b/src/commands/onboard-skills.ts index 0b8a00834e22..0db3e5a60907 100644 --- a/src/commands/onboard-skills.ts +++ b/src/commands/onboard-skills.ts @@ -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());