diff --git a/src/media/image-ops.helpers.test.ts b/src/media/image-ops.helpers.test.ts index 09e908362e6..e90f4a13d4e 100644 --- a/src/media/image-ops.helpers.test.ts +++ b/src/media/image-ops.helpers.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from "vitest"; -import { buildImageResizeSideGrid, IMAGE_REDUCE_QUALITY_STEPS } from "./image-ops.js"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { buildImageResizeSideGrid, IMAGE_REDUCE_QUALITY_STEPS, prefersSips } from "./image-ops.js"; describe("buildImageResizeSideGrid", () => { it("returns descending unique sides capped by maxSide", () => { @@ -16,3 +16,136 @@ describe("IMAGE_REDUCE_QUALITY_STEPS", () => { expect([...IMAGE_REDUCE_QUALITY_STEPS]).toEqual([85, 75, 65, 55, 45, 35]); }); }); + +// --------------------------------------------------------------------------- +// prefersSips() +// +// Logic: +// OPENCLAW_IMAGE_BACKEND === "sips" → true (any platform) +// OPENCLAW_IMAGE_BACKEND === "sharp" → false (any platform) +// OPENCLAW_IMAGE_BACKEND unset / empty + darwin → true (← fix: was Node.js-broken) +// OPENCLAW_IMAGE_BACKEND unset / empty + linux | win32 → false +// --------------------------------------------------------------------------- +describe("prefersSips", () => { + let platformSpy: ReturnType; + + beforeEach(() => { + // Allow overriding process.platform per test + platformSpy = vi.spyOn(process, "platform", "get"); + // Restore env vars automatically after each test + vi.stubEnv("OPENCLAW_IMAGE_BACKEND", ""); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + platformSpy.mockRestore(); + }); + + // Branch 1 — explicit sips, any platform + describe("OPENCLAW_IMAGE_BACKEND=sips", () => { + it("returns true on darwin", () => { + vi.stubEnv("OPENCLAW_IMAGE_BACKEND", "sips"); + platformSpy.mockReturnValue("darwin" as NodeJS.Platform); + expect(prefersSips()).toBe(true); + }); + + it("returns true on linux", () => { + vi.stubEnv("OPENCLAW_IMAGE_BACKEND", "sips"); + platformSpy.mockReturnValue("linux" as NodeJS.Platform); + expect(prefersSips()).toBe(true); + }); + + it("returns true on win32", () => { + vi.stubEnv("OPENCLAW_IMAGE_BACKEND", "sips"); + platformSpy.mockReturnValue("win32" as NodeJS.Platform); + expect(prefersSips()).toBe(true); + }); + }); + + // Branch 2 — explicit sharp wins even on darwin + describe("OPENCLAW_IMAGE_BACKEND=sharp", () => { + it("returns false on darwin (sharp override takes priority)", () => { + vi.stubEnv("OPENCLAW_IMAGE_BACKEND", "sharp"); + platformSpy.mockReturnValue("darwin" as NodeJS.Platform); + expect(prefersSips()).toBe(false); + }); + + it("returns false on linux", () => { + vi.stubEnv("OPENCLAW_IMAGE_BACKEND", "sharp"); + platformSpy.mockReturnValue("linux" as NodeJS.Platform); + expect(prefersSips()).toBe(false); + }); + + // Branch 5 — explicit sharp on darwin even if runtime resembles Bun + it("returns false on darwin when env=sharp (Bun-like runtime, regression guard)", () => { + // Simulates: OPENCLAW_IMAGE_BACKEND=sharp + darwin + Bun process + vi.stubEnv("OPENCLAW_IMAGE_BACKEND", "sharp"); + // Fake a Bun version string to confirm sharp env still wins + const origBun = (process.versions as Record).bun; + (process.versions as Record).bun = "1.0.0"; + platformSpy.mockReturnValue("darwin" as NodeJS.Platform); + try { + expect(prefersSips()).toBe(false); + } finally { + if (origBun === undefined) { + delete (process.versions as Record).bun; + } else { + (process.versions as Record).bun = origBun; + } + } + }); + }); + + // Branch 3 — THE FIX: no env var + darwin → must be true for both Node.js and Bun + describe("no OPENCLAW_IMAGE_BACKEND env var + darwin", () => { + it("returns true on darwin (Node.js runtime — core fix, was false before)", () => { + vi.stubEnv("OPENCLAW_IMAGE_BACKEND", ""); + platformSpy.mockReturnValue("darwin" as NodeJS.Platform); + expect(prefersSips()).toBe(true); + }); + + it("returns true on darwin when Bun is detected (no regression)", () => { + vi.stubEnv("OPENCLAW_IMAGE_BACKEND", ""); + const origBun = (process.versions as Record).bun; + (process.versions as Record).bun = "1.0.0"; + platformSpy.mockReturnValue("darwin" as NodeJS.Platform); + try { + expect(prefersSips()).toBe(true); + } finally { + if (origBun === undefined) { + delete (process.versions as Record).bun; + } else { + (process.versions as Record).bun = origBun; + } + } + }); + + // Empty string is distinct from "sips" — falls through to platform check + it("returns true on darwin when env var is empty string", () => { + vi.stubEnv("OPENCLAW_IMAGE_BACKEND", ""); + platformSpy.mockReturnValue("darwin" as NodeJS.Platform); + expect(prefersSips()).toBe(true); + }); + }); + + // Branch 4 — no env var + non-darwin → false + describe("no OPENCLAW_IMAGE_BACKEND env var + non-darwin", () => { + it("returns false on linux", () => { + vi.stubEnv("OPENCLAW_IMAGE_BACKEND", ""); + platformSpy.mockReturnValue("linux" as NodeJS.Platform); + expect(prefersSips()).toBe(false); + }); + + it("returns false on win32", () => { + vi.stubEnv("OPENCLAW_IMAGE_BACKEND", ""); + platformSpy.mockReturnValue("win32" as NodeJS.Platform); + expect(prefersSips()).toBe(false); + }); + + it("returns false on linux when env var is empty string", () => { + vi.stubEnv("OPENCLAW_IMAGE_BACKEND", ""); + platformSpy.mockReturnValue("linux" as NodeJS.Platform); + expect(prefersSips()).toBe(false); + }); + }); +}); diff --git a/src/media/image-ops.ts b/src/media/image-ops.ts index cbf0ed20ffc..8467e9dadaf 100644 --- a/src/media/image-ops.ts +++ b/src/media/image-ops.ts @@ -19,14 +19,10 @@ export function buildImageResizeSideGrid(maxSide: number, sideStart: number): nu .toSorted((a, b) => b - a); } -function isBun(): boolean { - return typeof (process.versions as { bun?: unknown }).bun === "string"; -} - -function prefersSips(): boolean { +export function prefersSips(): boolean { return ( process.env.OPENCLAW_IMAGE_BACKEND === "sips" || - (process.env.OPENCLAW_IMAGE_BACKEND !== "sharp" && isBun() && process.platform === "darwin") + (process.env.OPENCLAW_IMAGE_BACKEND !== "sharp" && process.platform === "darwin") ); }