Skip to content
Closed
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
23 changes: 19 additions & 4 deletions src/integrations/raycast-detect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,19 @@ export interface RaycastDetectDeps {
*/
const RAYCAST_DEFAULTS_DOMAIN = "com.raycast.macos.v1";
const RAYCAST_SUBSCRIPTION_KEY = "subscriptions_active";
const DEFAULTS_PATH = "/usr/bin/defaults";
const DEFAULTS_TIMEOUT_MS = 2_000;

export function realRaycastDetectDeps(): RaycastDetectDeps {
interface RealRaycastDetectRuntime {
platform?: string;
spawnSync?: typeof Bun.spawnSync;
}

export function realRaycastDetectDeps(runtime: RealRaycastDetectRuntime = {}): RaycastDetectDeps {
const platform = runtime.platform ?? process.platform;
const spawnSync = runtime.spawnSync ?? Bun.spawnSync;
return {
platform: process.platform,
platform,
homedir: homedir(),
env: process.env,
exists: path => {
Expand All @@ -59,9 +68,15 @@ export function realRaycastDetectDeps(): RaycastDetectDeps {
},
readDefault: (domain, key) => {
// `defaults` is macOS-only; elsewhere the plan is simply unknown.
if (process.platform !== "darwin") return null;
if (platform !== "darwin") return null;
try {
const result = Bun.spawnSync(["defaults", "read", domain, key], { stdout: "pipe", stderr: "pipe" });
const result = spawnSync([DEFAULTS_PATH, "read", domain, key], {
stdout: "pipe",
stderr: "pipe",
timeout: DEFAULTS_TIMEOUT_MS,
});
// A timed-out or signal-killed probe reports exitCode === null; that
// and any non-zero exit mean the preference was not read.
if (result.exitCode !== 0) return null;
return result.stdout.toString().trim();
} catch {
Expand Down
29 changes: 28 additions & 1 deletion tests/clients/raycast-detect.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { describe, expect, test } from "bun:test";
import { detectRaycast, type RaycastDetectDeps } from "../../src/integrations/raycast-detect";
import {
detectRaycast,
realRaycastDetectDeps,
type RaycastDetectDeps,
} from "../../src/integrations/raycast-detect";

/**
* Stubbed deps only. The real detector spawns `defaults` and reads the
Expand Down Expand Up @@ -29,6 +33,21 @@ function fakeDeps(
}

describe("detectRaycast", () => {
test("the macOS preference probe uses the system binary with a bounded runtime", () => {
let invocation: { command: string[]; timeout?: number } | undefined;
const spawnSync = ((command: string[], options: { timeout?: number }) => {
invocation = { command, timeout: options.timeout };
return { exitCode: 0, stdout: Buffer.from("1") };
}) as typeof Bun.spawnSync;

const deps = realRaycastDetectDeps({ platform: "darwin", spawnSync });
expect(deps.readDefault("com.raycast.macos.v1", "subscriptions_active")).toBe("1");
expect(invocation).toEqual({
command: ["/usr/bin/defaults", "read", "com.raycast.macos.v1", "subscriptions_active"],
timeout: 2_000,
});
});

test("darwin: a Pro subscription, the app bundle and the revealed ai folder", () => {
const deps = fakeDeps("darwin", ["/Applications/Raycast.app", "/home/u/.config/raycast/ai"], { defaultValue: "1" });
expect(detectRaycast(deps)).toEqual({
Expand All @@ -55,6 +74,14 @@ describe("detectRaycast", () => {
expect(detectRaycast(fakeDeps("darwin", [], { defaultValue: "" })).plan).toBe("unknown");
});

test("darwin: a timed-out or killed defaults probe is unknown, not a false positive", () => {
// Bun reports exitCode === null when the 2s timeout kills the process.
const spawnSync = (() => ({ exitCode: null, stdout: Buffer.from("") })) as typeof Bun.spawnSync;
const deps = realRaycastDetectDeps({ platform: "darwin", spawnSync });
expect(deps.readDefault("com.raycast.macos.v1", "subscriptions_active")).toBeNull();
expect(detectRaycast(deps).plan).toBe("unknown");
});

test("win32: LOCALAPPDATA\\Programs\\Raycast is the install path and the plan is unknown", () => {
const local = "C:\\Users\\u\\AppData\\Local";
const deps = fakeDeps("win32", [`${local}\\Programs\\Raycast`, "C:\\Users\\u\\.config\\raycast\\ai"], {
Expand Down
Loading