From 55cd6fe931b90dfd3624a4d03dc0723403845de9 Mon Sep 17 00:00:00 2001 From: Mohammed Alkindi Date: Tue, 25 Aug 2026 12:46:44 +0400 Subject: [PATCH] fix(doctor): resolve macOS cloud paths through existing ancestors (#124299) Co-authored-by: Peter Steinberger --- ...ctor-state-integrity.cloud-storage.test.ts | 57 ++++++++++++++++++- src/commands/doctor-state-integrity.ts | 16 +++--- 2 files changed, 63 insertions(+), 10 deletions(-) diff --git a/src/commands/doctor-state-integrity.cloud-storage.test.ts b/src/commands/doctor-state-integrity.cloud-storage.test.ts index 2ead84fc815a..ad8dc072342b 100644 --- a/src/commands/doctor-state-integrity.cloud-storage.test.ts +++ b/src/commands/doctor-state-integrity.cloud-storage.test.ts @@ -1,9 +1,17 @@ // Doctor state integrity cloud-storage tests cover macOS cloud-synced state directory detection. +import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { createTempDirTracker } from "../../test/helpers/temp-dir.js"; import { detectMacCloudSyncedStateDir } from "./doctor-state-integrity.js"; +const tempDirs = createTempDirTracker(); + +afterEach(() => { + tempDirs.cleanup(); +}); + describe("detectMacCloudSyncedStateDir", () => { const home = "/Users/tester"; @@ -85,6 +93,53 @@ describe("detectMacCloudSyncedStateDir", () => { expect(result).toBeNull(); }); + it("follows a real symlink out of the sync root when the state dir leaf is absent", () => { + const sandbox = fs.realpathSync(tempDirs.make("openclaw-cloud-storage-symlink-")); + const realHome = path.join(sandbox, "home"); + const cloudStorage = path.join(realHome, "Library", "CloudStorage"); + const localTarget = path.join(sandbox, "local-openclaw"); + fs.mkdirSync(cloudStorage, { recursive: true }); + fs.mkdirSync(localTarget, { recursive: true }); + const syncedLink = path.join(cloudStorage, "OneDrive-Personal"); + fs.symlinkSync(localTarget, syncedLink, process.platform === "win32" ? "junction" : "dir"); + + const stateDir = path.join(syncedLink, "OpenClaw", ".openclaw"); + expect(fs.existsSync(stateDir)).toBe(false); + + expect( + detectMacCloudSyncedStateDir(stateDir, { + platform: "darwin", + homedir: realHome, + }), + ).toBeNull(); + }); + + it("still warns for a real absent leaf that stays inside the sync root", () => { + const sandbox = fs.realpathSync(tempDirs.make("openclaw-cloud-storage-real-")); + const realHome = path.join(sandbox, "home"); + const syncedDir = path.join( + realHome, + "Library", + "CloudStorage", + "OneDrive-Personal", + "OpenClaw", + ); + fs.mkdirSync(syncedDir, { recursive: true }); + + const stateDir = path.join(syncedDir, ".openclaw"); + expect(fs.existsSync(stateDir)).toBe(false); + + expect( + detectMacCloudSyncedStateDir(stateDir, { + platform: "darwin", + homedir: realHome, + }), + ).toEqual({ + path: path.resolve(stateDir), + storage: "CloudStorage provider", + }); + }); + it("anchors cloud detection to OS homedir when OPENCLAW_HOME is overridden", () => { const stateDir = path.join(home, "Library", "CloudStorage", "iCloud Drive", ".openclaw"); const originalOpenClawHome = process.env.OPENCLAW_HOME; diff --git a/src/commands/doctor-state-integrity.ts b/src/commands/doctor-state-integrity.ts index 8c0a6a631ff4..9ae02559a866 100644 --- a/src/commands/doctor-state-integrity.ts +++ b/src/commands/doctor-state-integrity.ts @@ -697,16 +697,14 @@ export function detectMacCloudSyncedStateDir( root: path.join(homedir, "Library", "CloudStorage"), }, ]; - const realPath = (deps?.resolveRealPath ?? tryResolveRealPath)(stateDir); - // Prefer the resolved target path when available so symlink prefixes do not - // misclassify local state dirs as cloud-synced. - const candidates = realPath ? [path.resolve(realPath)] : [path.resolve(stateDir)]; + const resolveRealPath = deps?.resolveRealPath ?? tryResolveRealPath; + // Missing state leaves must still follow existing symlink ancestors, like the Linux detectors. + const resolvedStatePath = + resolvePathThroughExistingAncestor(stateDir, resolveRealPath, path) ?? path.resolve(stateDir); - for (const candidate of candidates) { - for (const { storage, root } of roots) { - if (isPathUnderRoot(candidate, root)) { - return { path: candidate, storage }; - } + for (const { storage, root } of roots) { + if (isPathUnderRoot(resolvedStatePath, root)) { + return { path: resolvedStatePath, storage }; } }