From 4c8c6fbd33b45868467cf1e3c1834a0eb06ec06f Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Sun, 8 Mar 2026 16:01:03 -0400 Subject: [PATCH] fix: canonicalize backup cwd fallback --- src/commands/backup.test.ts | 34 ++++++++++++++++++++++++++++++++++ src/commands/backup.ts | 3 ++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/commands/backup.test.ts b/src/commands/backup.test.ts index 6d146221505..349714e4d15 100644 --- a/src/commands/backup.test.ts +++ b/src/commands/backup.test.ts @@ -278,6 +278,40 @@ describe("backup commands", () => { await fs.rm(result.archivePath, { force: true }); }); + it("falls back to the home directory when cwd is a symlink into a backed-up source tree", async () => { + if (process.platform === "win32") { + return; + } + + const stateDir = path.join(tempHome.home, ".openclaw"); + const workspaceDir = path.join(stateDir, "workspace"); + const linkParent = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-backup-cwd-link-")); + const workspaceLink = path.join(linkParent, "workspace-link"); + try { + await fs.writeFile(path.join(stateDir, "openclaw.json"), JSON.stringify({}), "utf8"); + await fs.mkdir(workspaceDir, { recursive: true }); + await fs.writeFile(path.join(workspaceDir, "SOUL.md"), "# soul\n", "utf8"); + await fs.symlink(workspaceDir, workspaceLink); + process.chdir(workspaceLink); + + const runtime = { + log: vi.fn(), + error: vi.fn(), + exit: vi.fn(), + }; + + const nowMs = Date.UTC(2026, 2, 9, 1, 3, 4); + const result = await backupCreateCommand(runtime, { nowMs }); + + expect(result.archivePath).toBe( + path.join(tempHome.home, `${buildBackupArchiveRoot(nowMs)}.tar.gz`), + ); + await fs.rm(result.archivePath, { force: true }); + } finally { + await fs.rm(linkParent, { recursive: true, force: true }); + } + }); + it("allows dry-run preview even when the target archive already exists", async () => { const stateDir = path.join(tempHome.home, ".openclaw"); const existingArchive = path.join(tempHome.home, "existing-backup.tar.gz"); diff --git a/src/commands/backup.ts b/src/commands/backup.ts index 22c426467d7..15f0f505d76 100644 --- a/src/commands/backup.ts +++ b/src/commands/backup.ts @@ -87,8 +87,9 @@ async function resolveOutputPath(params: { const rawOutput = params.output?.trim(); if (!rawOutput) { const cwd = path.resolve(process.cwd()); + const canonicalCwd = await fs.realpath(cwd).catch(() => cwd); const cwdInsideSource = params.includedAssets.some((asset) => - isPathWithin(cwd, asset.sourcePath), + isPathWithin(canonicalCwd, asset.sourcePath), ); const defaultDir = cwdInsideSource ? (resolveHomeDir() ?? path.dirname(params.stateDir)) : cwd; return path.resolve(defaultDir, basename);