store-paths.test.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  1. /**
  2. * store-paths.test.ts - Comprehensive unit tests for Windows path support
  3. *
  4. * Tests all path-related utility functions for cross-platform compatibility:
  5. * - isAbsolutePath() - Unix, Windows (C:\, C:/), and Git Bash (/c/) paths
  6. * - normalizePathSeparators() - backslash to forward slash conversion
  7. * - getRelativePathFromPrefix() - relative path extraction
  8. * - resolve() - path resolution with Unix and Windows paths
  9. *
  10. * Run with: bun test store-paths.test.ts
  11. */
  12. import { describe, test, expect, beforeEach, afterEach } from "vitest";
  13. import {
  14. isAbsolutePath,
  15. normalizePathSeparators,
  16. getRelativePathFromPrefix,
  17. resolve,
  18. } from "../src/store.js";
  19. // =============================================================================
  20. // Test Utilities
  21. // =============================================================================
  22. let originalPWD: string | undefined;
  23. let originalProcessCwd: () => string;
  24. beforeEach(() => {
  25. // Save original environment
  26. originalPWD = process.env.PWD;
  27. originalProcessCwd = process.cwd;
  28. });
  29. afterEach(() => {
  30. // Restore original environment
  31. if (originalPWD !== undefined) {
  32. process.env.PWD = originalPWD;
  33. } else {
  34. delete process.env.PWD;
  35. }
  36. process.cwd = originalProcessCwd;
  37. });
  38. /**
  39. * Mock the current working directory for testing.
  40. * Sets both process.env.PWD and process.cwd() to simulate different environments.
  41. */
  42. function mockPWD(path: string): void {
  43. process.env.PWD = path;
  44. process.cwd = () => path;
  45. }
  46. // =============================================================================
  47. // Path Utilities - Cross-platform Support
  48. // =============================================================================
  49. describe("Path utilities - Cross-platform support", () => {
  50. // ===========================================================================
  51. // isAbsolutePath
  52. // ===========================================================================
  53. describe("isAbsolutePath", () => {
  54. test("Unix absolute paths", () => {
  55. expect(isAbsolutePath("/path/to/file")).toBe(true);
  56. expect(isAbsolutePath("/")).toBe(true);
  57. expect(isAbsolutePath("/home/user/documents")).toBe(true);
  58. expect(isAbsolutePath("/usr/local/bin")).toBe(true);
  59. });
  60. test("Unix relative paths", () => {
  61. expect(isAbsolutePath("path/to/file")).toBe(false);
  62. expect(isAbsolutePath("./path/to/file")).toBe(false);
  63. expect(isAbsolutePath("../path/to/file")).toBe(false);
  64. expect(isAbsolutePath("./file")).toBe(false);
  65. expect(isAbsolutePath("../file")).toBe(false);
  66. expect(isAbsolutePath("file.txt")).toBe(false);
  67. });
  68. test("Windows absolute paths (native) - forward slash", () => {
  69. expect(isAbsolutePath("C:/path/to/file")).toBe(true);
  70. expect(isAbsolutePath("C:/")).toBe(true);
  71. expect(isAbsolutePath("D:/Users/Documents")).toBe(true);
  72. expect(isAbsolutePath("Z:/")).toBe(true);
  73. expect(isAbsolutePath("c:/lowercase")).toBe(true);
  74. });
  75. test("Windows absolute paths (native) - backslash", () => {
  76. expect(isAbsolutePath("C:\\path\\to\\file")).toBe(true);
  77. expect(isAbsolutePath("C:\\")).toBe(true);
  78. expect(isAbsolutePath("D:\\Users\\Documents")).toBe(true);
  79. expect(isAbsolutePath("Z:\\")).toBe(true);
  80. expect(isAbsolutePath("c:\\lowercase")).toBe(true);
  81. });
  82. test("Windows relative paths", () => {
  83. expect(isAbsolutePath("path\\to\\file")).toBe(false);
  84. expect(isAbsolutePath(".\\path\\to\\file")).toBe(false);
  85. expect(isAbsolutePath("..\\path\\to\\file")).toBe(false);
  86. expect(isAbsolutePath(".\\file")).toBe(false);
  87. expect(isAbsolutePath("..\\file")).toBe(false);
  88. expect(isAbsolutePath("file.txt")).toBe(false);
  89. });
  90. test("Git Bash style paths", () => {
  91. expect(isAbsolutePath("/c/Users/name/file")).toBe(true);
  92. expect(isAbsolutePath("/C/Users/name/file")).toBe(true);
  93. expect(isAbsolutePath("/d/Projects")).toBe(true);
  94. expect(isAbsolutePath("/D/Projects")).toBe(true);
  95. expect(isAbsolutePath("/z/")).toBe(true);
  96. });
  97. test("Edge cases", () => {
  98. expect(isAbsolutePath("")).toBe(false);
  99. expect(isAbsolutePath("C:")).toBe(true); // Drive letter only
  100. expect(isAbsolutePath("C")).toBe(false); // Just a letter
  101. expect(isAbsolutePath(":")).toBe(false);
  102. expect(isAbsolutePath("/a")).toBe(true); // Short Unix path
  103. expect(isAbsolutePath("/1/")).toBe(true); // Number after slash (not Git Bash)
  104. });
  105. });
  106. // ===========================================================================
  107. // normalizePathSeparators
  108. // ===========================================================================
  109. describe("normalizePathSeparators", () => {
  110. test("Windows paths with backslashes", () => {
  111. expect(normalizePathSeparators("C:\\Users\\name\\file.txt"))
  112. .toBe("C:/Users/name/file.txt");
  113. expect(normalizePathSeparators("D:\\Projects\\qmd\\src"))
  114. .toBe("D:/Projects/qmd/src");
  115. expect(normalizePathSeparators("\\path\\to\\file"))
  116. .toBe("/path/to/file");
  117. });
  118. test("Mixed separators", () => {
  119. expect(normalizePathSeparators("C:\\Users/name\\file.txt"))
  120. .toBe("C:/Users/name/file.txt");
  121. expect(normalizePathSeparators("path\\to/file/here"))
  122. .toBe("path/to/file/here");
  123. });
  124. test("Unix paths (should remain unchanged)", () => {
  125. expect(normalizePathSeparators("/path/to/file"))
  126. .toBe("/path/to/file");
  127. expect(normalizePathSeparators("/usr/local/bin"))
  128. .toBe("/usr/local/bin");
  129. expect(normalizePathSeparators("relative/path"))
  130. .toBe("relative/path");
  131. });
  132. test("Multiple consecutive backslashes", () => {
  133. expect(normalizePathSeparators("path\\\\to\\\\file"))
  134. .toBe("path//to//file");
  135. expect(normalizePathSeparators("C:\\\\Users\\\\name"))
  136. .toBe("C://Users//name");
  137. });
  138. test("Edge cases", () => {
  139. expect(normalizePathSeparators("")).toBe("");
  140. expect(normalizePathSeparators("\\")).toBe("/");
  141. expect(normalizePathSeparators("\\\\")).toBe("//");
  142. expect(normalizePathSeparators("file.txt")).toBe("file.txt");
  143. });
  144. });
  145. // ===========================================================================
  146. // getRelativePathFromPrefix
  147. // ===========================================================================
  148. describe("getRelativePathFromPrefix", () => {
  149. test("Exact match (path equals prefix)", () => {
  150. expect(getRelativePathFromPrefix("/home/user", "/home/user")).toBe("");
  151. expect(getRelativePathFromPrefix("C:/Users/name", "C:/Users/name")).toBe("");
  152. expect(getRelativePathFromPrefix("/path", "/path")).toBe("");
  153. });
  154. test("Path under prefix", () => {
  155. expect(getRelativePathFromPrefix("/home/user/documents", "/home/user"))
  156. .toBe("documents");
  157. expect(getRelativePathFromPrefix("/home/user/documents/file.txt", "/home/user"))
  158. .toBe("documents/file.txt");
  159. expect(getRelativePathFromPrefix("C:/Users/name/Documents/file.txt", "C:/Users/name"))
  160. .toBe("Documents/file.txt");
  161. });
  162. test("Path not under prefix", () => {
  163. expect(getRelativePathFromPrefix("/home/other", "/home/user")).toBeNull();
  164. expect(getRelativePathFromPrefix("/usr/local", "/home/user")).toBeNull();
  165. expect(getRelativePathFromPrefix("C:/Users/other", "D:/Users")).toBeNull();
  166. });
  167. test("Windows paths with normalized separators", () => {
  168. // Backslashes should be normalized
  169. expect(getRelativePathFromPrefix("C:\\Users\\name\\Documents", "C:\\Users\\name"))
  170. .toBe("Documents");
  171. expect(getRelativePathFromPrefix("C:\\Users\\name\\Documents\\file.txt", "C:/Users/name"))
  172. .toBe("Documents/file.txt");
  173. });
  174. test("Prefix with trailing slash", () => {
  175. expect(getRelativePathFromPrefix("/home/user/documents", "/home/user/"))
  176. .toBe("documents");
  177. expect(getRelativePathFromPrefix("C:/Users/name/Documents", "C:/Users/name/"))
  178. .toBe("Documents");
  179. });
  180. test("Prefix without trailing slash", () => {
  181. expect(getRelativePathFromPrefix("/home/user/documents", "/home/user"))
  182. .toBe("documents");
  183. expect(getRelativePathFromPrefix("C:/Users/name/Documents", "C:/Users/name"))
  184. .toBe("Documents");
  185. });
  186. test("Edge cases", () => {
  187. // Empty prefix
  188. expect(getRelativePathFromPrefix("/path/to/file", "")).toBeNull();
  189. // Path is prefix substring but not in hierarchy
  190. expect(getRelativePathFromPrefix("/home/username", "/home/user")).toBeNull();
  191. // Root prefix
  192. expect(getRelativePathFromPrefix("/home/user", "/")).toBe("home/user");
  193. });
  194. });
  195. // ===========================================================================
  196. // resolve - Unix environment
  197. // ===========================================================================
  198. describe("resolve - Unix environment", () => {
  199. beforeEach(() => {
  200. mockPWD("/home/user");
  201. });
  202. test("Unix relative paths", () => {
  203. expect(resolve("/base", "relative")).toBe("/base/relative");
  204. expect(resolve("/base", "a/b/c")).toBe("/base/a/b/c");
  205. expect(resolve("/home", "user/documents")).toBe("/home/user/documents");
  206. });
  207. test("Unix absolute paths", () => {
  208. expect(resolve("/base", "/absolute")).toBe("/absolute");
  209. expect(resolve("/home/user", "/usr/local")).toBe("/usr/local");
  210. expect(resolve("/any", "/")).toBe("/");
  211. });
  212. test("Path with .. and .", () => {
  213. expect(resolve("/base", "../other")).toBe("/other");
  214. expect(resolve("/base/sub", "..")).toBe("/base");
  215. expect(resolve("/base", "./file")).toBe("/base/file");
  216. expect(resolve("/base/a/b", "../../c")).toBe("/base/c");
  217. });
  218. test("Multiple path segments", () => {
  219. expect(resolve("/a", "b", "c")).toBe("/a/b/c");
  220. expect(resolve("/a", "b", "../c")).toBe("/a/c");
  221. expect(resolve("/a", "b", "/c")).toBe("/c");
  222. });
  223. test("Relative path without base (uses PWD)", () => {
  224. expect(resolve("relative")).toBe("/home/user/relative");
  225. expect(resolve("a/b/c")).toBe("/home/user/a/b/c");
  226. expect(resolve("./file")).toBe("/home/user/file");
  227. });
  228. test("Absolute path alone", () => {
  229. expect(resolve("/absolute/path")).toBe("/absolute/path");
  230. expect(resolve("/")).toBe("/");
  231. });
  232. });
  233. // ===========================================================================
  234. // resolve - Windows environment
  235. // ===========================================================================
  236. describe("resolve - Windows environment", () => {
  237. beforeEach(() => {
  238. mockPWD("C:/Users/name");
  239. });
  240. test("Windows relative paths", () => {
  241. expect(resolve("C:/base", "relative")).toBe("C:/base/relative");
  242. expect(resolve("C:/base", "a/b/c")).toBe("C:/base/a/b/c");
  243. expect(resolve("D:/Projects", "qmd/src")).toBe("D:/Projects/qmd/src");
  244. });
  245. test("Windows absolute paths", () => {
  246. expect(resolve("C:/base", "D:/other")).toBe("D:/other");
  247. expect(resolve("C:/Users", "C:/Program Files")).toBe("C:/Program Files");
  248. expect(resolve("D:/any", "E:/other")).toBe("E:/other");
  249. });
  250. test("Windows with backslashes", () => {
  251. expect(resolve("C:\\base", "relative")).toBe("C:/base/relative");
  252. expect(resolve("C:\\Users\\name", "Documents")).toBe("C:/Users/name/Documents");
  253. expect(resolve("C:\\base", "a\\b\\c")).toBe("C:/base/a/b/c");
  254. });
  255. test("Path with .. and .", () => {
  256. expect(resolve("C:/base", "../other")).toBe("C:/other");
  257. expect(resolve("C:/base/sub", "..")).toBe("C:/base");
  258. expect(resolve("C:/base", "./file")).toBe("C:/base/file");
  259. expect(resolve("C:/base/a/b", "../../c")).toBe("C:/base/c");
  260. });
  261. test("Multiple path segments", () => {
  262. expect(resolve("C:/a", "b", "c")).toBe("C:/a/b/c");
  263. expect(resolve("C:/a", "b", "../c")).toBe("C:/a/c");
  264. expect(resolve("C:/a", "b", "D:/c")).toBe("D:/c");
  265. });
  266. test("Relative path without base (uses PWD)", () => {
  267. expect(resolve("relative")).toBe("C:/Users/name/relative");
  268. expect(resolve("a/b/c")).toBe("C:/Users/name/a/b/c");
  269. expect(resolve(".\\file")).toBe("C:/Users/name/file");
  270. });
  271. test("Drive letter only", () => {
  272. expect(resolve("C:")).toBe("C:/");
  273. expect(resolve("D:")).toBe("D:/");
  274. });
  275. });
  276. // ===========================================================================
  277. // resolve - Git Bash style paths
  278. // ===========================================================================
  279. describe("resolve - Git Bash style paths", () => {
  280. test("Git Bash to Windows conversion", () => {
  281. expect(resolve("/c/Users/name")).toBe("C:/Users/name");
  282. expect(resolve("/C/Users/name")).toBe("C:/Users/name");
  283. expect(resolve("/d/Projects")).toBe("D:/Projects");
  284. expect(resolve("/D/Projects")).toBe("D:/Projects");
  285. });
  286. test("Git Bash with relative paths", () => {
  287. expect(resolve("/c/base", "relative")).toBe("C:/base/relative");
  288. expect(resolve("/d/Projects", "qmd/src")).toBe("D:/Projects/qmd/src");
  289. });
  290. test("Git Bash with .. and .", () => {
  291. expect(resolve("/c/base", "../other")).toBe("C:/other");
  292. expect(resolve("/c/base/sub", "..")).toBe("C:/base");
  293. expect(resolve("/c/base", "./file")).toBe("C:/base/file");
  294. });
  295. test("Multiple Git Bash segments", () => {
  296. expect(resolve("/c/a", "b", "c")).toBe("C:/a/b/c");
  297. expect(resolve("/c/a", "b", "/d/c")).toBe("D:/c");
  298. });
  299. });
  300. // ===========================================================================
  301. // resolve - Edge cases and mixed scenarios
  302. // ===========================================================================
  303. describe("resolve - Edge cases", () => {
  304. test("Empty path segments are filtered", () => {
  305. expect(resolve("/base", "", "file")).toBe("/base/file");
  306. expect(resolve("C:/base", "", "file")).toBe("C:/base/file");
  307. });
  308. test("Multiple consecutive slashes", () => {
  309. expect(resolve("/base//path///file")).toBe("/base/path/file");
  310. expect(resolve("C:/base//path///file")).toBe("C:/base/path/file");
  311. });
  312. test("Trailing slashes", () => {
  313. expect(resolve("/base/", "file")).toBe("/base/file");
  314. expect(resolve("C:/base/", "file")).toBe("C:/base/file");
  315. });
  316. test("Complex .. navigation", () => {
  317. expect(resolve("/a/b/c/d", "../../../e")).toBe("/a/e");
  318. expect(resolve("C:/a/b/c/d", "../../../e")).toBe("C:/a/e");
  319. });
  320. test("Too many .. (should not go above root)", () => {
  321. expect(resolve("/base", "../../../../other")).toBe("/other");
  322. expect(resolve("C:/base", "../../../../other")).toBe("C:/other");
  323. });
  324. test("Mixed Unix and Windows (normalized)", () => {
  325. mockPWD("C:/Users/name");
  326. expect(resolve("/unix/path")).toBe("/unix/path");
  327. expect(resolve("relative")).toBe("C:/Users/name/relative");
  328. });
  329. test("Error on no arguments", () => {
  330. expect(() => resolve()).toThrow("resolve: at least one path segment is required");
  331. });
  332. });
  333. });