|
| 1 | +import { existsSync, statSync as statSyncCb } from "node:fs"; |
1 | 2 | /** |
2 | 3 | * Shared bash-tool helper tests. |
3 | 4 | * Covers strict env parsing and sandbox workdir mapping between container and |
4 | 5 | * host workspace paths. |
5 | 6 | */ |
6 | | -import { mkdir, mkdtemp, rm } from "node:fs/promises"; |
| 7 | +import { mkdir, mkdtemp, rm, statSync } from "node:fs/promises"; |
7 | 8 | import os from "node:os"; |
8 | 9 | import path from "node:path"; |
9 | 10 | import { afterEach, describe, expect, it, vi } from "vitest"; |
10 | | -import { deriveSessionName, readEnvInt, resolveSandboxWorkdir } from "./bash-tools.shared.js"; |
| 11 | +import { |
| 12 | + deriveSessionName, |
| 13 | + expandTilde, |
| 14 | + readEnvInt, |
| 15 | + resolveSandboxWorkdir, |
| 16 | + resolveWorkdir, |
| 17 | +} from "./bash-tools.shared.js"; |
11 | 18 |
|
12 | 19 | async function withTempDir(run: (dir: string) => Promise<void>) { |
13 | 20 | const dir = await mkdtemp(path.join(os.tmpdir(), "openclaw-bash-workdir-")); |
@@ -144,3 +151,121 @@ describe("deriveSessionName", () => { |
144 | 151 | } |
145 | 152 | }); |
146 | 153 | }); |
| 154 | + |
| 155 | +describe("expandTilde", () => { |
| 156 | + const homeDir = os.homedir(); |
| 157 | + |
| 158 | + it("expands ~ to home directory", () => { |
| 159 | + expect(expandTilde("~")).toBe(homeDir); |
| 160 | + }); |
| 161 | + |
| 162 | + it("expands ~/path to homeDir/path", () => { |
| 163 | + expect(expandTilde("~/test/path")).toBe(`${homeDir}/test/path`); |
| 164 | + expect(expandTilde("~/Documents/file.txt")).toBe(`${homeDir}/Documents/file.txt`); |
| 165 | + }); |
| 166 | + |
| 167 | + it("leaves absolute paths unchanged", () => { |
| 168 | + expect(expandTilde("/usr/local/bin")).toBe("/usr/local/bin"); |
| 169 | + expect(expandTilde("/home/user/test")).toBe("/home/user/test"); |
| 170 | + }); |
| 171 | + |
| 172 | + it("leaves relative paths unchanged", () => { |
| 173 | + expect(expandTilde("src/index.ts")).toBe("src/index.ts"); |
| 174 | + expect(expandTilde("./local/path")).toBe("./local/path"); |
| 175 | + expect(expandTilde("../parent/path")).toBe("../parent/path"); |
| 176 | + }); |
| 177 | + |
| 178 | + it("handles empty string", () => { |
| 179 | + expect(expandTilde("")).toBe(""); |
| 180 | + }); |
| 181 | + |
| 182 | + it("does not expand ~user syntax (not supported)", () => { |
| 183 | + expect(expandTilde("~otheruser")).toBe("~otheruser"); |
| 184 | + expect(expandTilde("~otheruser/path")).toBe("~otheruser/path"); |
| 185 | + }); |
| 186 | + |
| 187 | + it("handles tilde in the middle or end of path", () => { |
| 188 | + // These should not be expanded as they're not leading tildes |
| 189 | + expect(expandTilde("/path/to/~cache")).toBe("/path/to/~cache"); |
| 190 | + expect(expandTilde("file~backup.txt")).toBe("file~backup.txt"); |
| 191 | + }); |
| 192 | +}); |
| 193 | + |
| 194 | +describe("resolveWorkdir", () => { |
| 195 | + afterEach(() => { |
| 196 | + vi.unstubAllEnvs(); |
| 197 | + }); |
| 198 | + |
| 199 | + it("returns existing absolute path if valid directory", () => { |
| 200 | + const tmpDir = path.join(os.tmpdir(), `openclaw-test-${Date.now()}`); |
| 201 | + try { |
| 202 | + require("node:fs").mkdirSync(tmpDir, { recursive: true }); |
| 203 | + const warnings: string[] = []; |
| 204 | + const result = resolveWorkdir(tmpDir, warnings); |
| 205 | + expect(result).toBe(tmpDir); |
| 206 | + expect(warnings).toEqual([]); |
| 207 | + } finally { |
| 208 | + require("node:fs").rmSync(tmpDir, { recursive: true, force: true }); |
| 209 | + } |
| 210 | + }); |
| 211 | + |
| 212 | + it("falls back for invalid relative path", async () => { |
| 213 | + const tmpDir = path.join(os.tmpdir(), `openclaw-test-${Date.now()}`); |
| 214 | + try { |
| 215 | + await mkdir(tmpDir, { recursive: true }); |
| 216 | + const warnings: string[] = []; |
| 217 | + // Test with a relative path that doesn't exist from any reasonable cwd |
| 218 | + const result = resolveWorkdir("nonexistent_subdir_12345", warnings); |
| 219 | + // Should fall back to cwd or homedir since the path doesn't exist |
| 220 | + expect([process.cwd(), os.homedir()].includes(result)).toBe(true); |
| 221 | + expect(warnings.length).toBe(1); |
| 222 | + expect(warnings[0]).toMatch(/Warning: workdir "nonexistent_subdir_\d+" is unavailable/); |
| 223 | + } finally { |
| 224 | + await rm(tmpDir, { recursive: true, force: true }); |
| 225 | + } |
| 226 | + }); |
| 227 | + |
| 228 | + it("expands ~ to home directory and returns it if valid", () => { |
| 229 | + const warnings: string[] = []; |
| 230 | + const result = resolveWorkdir("~", warnings); |
| 231 | + expect(result).toBe(os.homedir()); |
| 232 | + expect(warnings).toEqual([]); |
| 233 | + }); |
| 234 | + |
| 235 | + it("expands ~/path and returns it if valid directory", async () => { |
| 236 | + const testSubdir = `openclaw-test-${Date.now()}`; |
| 237 | + const testPath = path.join(os.homedir(), testSubdir); |
| 238 | + try { |
| 239 | + await mkdir(testPath, { recursive: true }); |
| 240 | + const warnings: string[] = []; |
| 241 | + const result = resolveWorkdir(`~/${testSubdir}`, warnings); |
| 242 | + expect(result).toBe(testPath); |
| 243 | + expect(warnings).toEqual([]); |
| 244 | + } finally { |
| 245 | + await rm(testPath, { recursive: true, force: true }); |
| 246 | + } |
| 247 | + }); |
| 248 | + |
| 249 | + it("falls back with warning when expanded ~ path does not exist", () => { |
| 250 | + const warnings: string[] = []; |
| 251 | + const result = resolveWorkdir("~/nonexistent_dir_12345", warnings); |
| 252 | + // Should fall back to cwd or homedir |
| 253 | + expect(result).toBe(process.cwd() ?? os.homedir()); |
| 254 | + expect(warnings.length).toBe(1); |
| 255 | + expect(warnings[0]).toMatch(/Warning: workdir "~\/nonexistent_dir_\d+" is unavailable/); |
| 256 | + }); |
| 257 | + |
| 258 | + it("falls back with warning for invalid absolute path", () => { |
| 259 | + const warnings: string[] = []; |
| 260 | + const result = resolveWorkdir("/nonexistent/path/12345", warnings); |
| 261 | + expect(result).toBe(process.cwd() ?? os.homedir()); |
| 262 | + expect(warnings.length).toBe(1); |
| 263 | + }); |
| 264 | + |
| 265 | + it("falls back with warning for invalid relative path", () => { |
| 266 | + const warnings: string[] = []; |
| 267 | + const result = resolveWorkdir("nonexistent/relative/path", warnings); |
| 268 | + expect(result).toBe(process.cwd() ?? os.homedir()); |
| 269 | + expect(warnings.length).toBe(1); |
| 270 | + }); |
| 271 | +}); |
0 commit comments