path.test.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. import { describe, expect, test } from "bun:test"
  2. import { createPathHelpers, stripQueryAndHash, unquoteGitPath, encodeFilePath } from "./path"
  3. describe("file path helpers", () => {
  4. test("normalizes file inputs against workspace root", () => {
  5. const path = createPathHelpers(() => "/repo")
  6. expect(path.normalize("file:///repo/src/app.ts?x=1#h")).toBe("src/app.ts")
  7. expect(path.normalize("/repo/src/app.ts")).toBe("src/app.ts")
  8. expect(path.normalize("./src/app.ts")).toBe("src/app.ts")
  9. expect(path.normalizeDir("src/components///")).toBe("src/components")
  10. expect(path.tab("src/app.ts")).toBe("file://src/app.ts")
  11. expect(path.pathFromTab("file://src/app.ts")).toBe("src/app.ts")
  12. expect(path.pathFromTab("other://src/app.ts")).toBeUndefined()
  13. })
  14. test("normalizes Windows absolute paths with mixed separators", () => {
  15. const path = createPathHelpers(() => "C:\\repo")
  16. expect(path.normalize("C:\\repo\\src\\app.ts")).toBe("src\\app.ts")
  17. expect(path.normalize("C:/repo/src/app.ts")).toBe("src/app.ts")
  18. expect(path.normalize("file://C:/repo/src/app.ts")).toBe("src/app.ts")
  19. expect(path.normalize("c:\\repo\\src\\app.ts")).toBe("src\\app.ts")
  20. })
  21. test("keeps query/hash stripping behavior stable", () => {
  22. expect(stripQueryAndHash("a/b.ts#L12?x=1")).toBe("a/b.ts")
  23. expect(stripQueryAndHash("a/b.ts?x=1#L12")).toBe("a/b.ts")
  24. expect(stripQueryAndHash("a/b.ts")).toBe("a/b.ts")
  25. })
  26. test("unquotes git escaped octal path strings", () => {
  27. expect(unquoteGitPath('"a/\\303\\251.txt"')).toBe("a/\u00e9.txt")
  28. expect(unquoteGitPath('"plain\\nname"')).toBe("plain\nname")
  29. expect(unquoteGitPath("a/b/c.ts")).toBe("a/b/c.ts")
  30. })
  31. })
  32. describe("encodeFilePath", () => {
  33. describe("Linux/Unix paths", () => {
  34. test("should handle Linux absolute path", () => {
  35. const linuxPath = "/home/user/project/README.md"
  36. const result = encodeFilePath(linuxPath)
  37. const fileUrl = `file://${result}`
  38. // Should create a valid URL
  39. expect(() => new URL(fileUrl)).not.toThrow()
  40. expect(result).toBe("/home/user/project/README.md")
  41. const url = new URL(fileUrl)
  42. expect(url.protocol).toBe("file:")
  43. expect(url.pathname).toBe("/home/user/project/README.md")
  44. })
  45. test("should handle Linux path with special characters", () => {
  46. const linuxPath = "/home/user/file#name with spaces.txt"
  47. const result = encodeFilePath(linuxPath)
  48. const fileUrl = `file://${result}`
  49. expect(() => new URL(fileUrl)).not.toThrow()
  50. expect(result).toBe("/home/user/file%23name%20with%20spaces.txt")
  51. })
  52. test("should handle Linux relative path", () => {
  53. const relativePath = "src/components/App.tsx"
  54. const result = encodeFilePath(relativePath)
  55. expect(result).toBe("src/components/App.tsx")
  56. })
  57. test("should handle Linux root directory", () => {
  58. const result = encodeFilePath("/")
  59. expect(result).toBe("/")
  60. })
  61. test("should handle Linux path with all special chars", () => {
  62. const path = "/path/to/file#with?special%chars&more.txt"
  63. const result = encodeFilePath(path)
  64. const fileUrl = `file://${result}`
  65. expect(() => new URL(fileUrl)).not.toThrow()
  66. expect(result).toContain("%23") // #
  67. expect(result).toContain("%3F") // ?
  68. expect(result).toContain("%25") // %
  69. expect(result).toContain("%26") // &
  70. })
  71. })
  72. describe("macOS paths", () => {
  73. test("should handle macOS absolute path", () => {
  74. const macPath = "/Users/kelvin/Projects/opencode/README.md"
  75. const result = encodeFilePath(macPath)
  76. const fileUrl = `file://${result}`
  77. expect(() => new URL(fileUrl)).not.toThrow()
  78. expect(result).toBe("/Users/kelvin/Projects/opencode/README.md")
  79. })
  80. test("should handle macOS path with spaces", () => {
  81. const macPath = "/Users/kelvin/My Documents/file.txt"
  82. const result = encodeFilePath(macPath)
  83. const fileUrl = `file://${result}`
  84. expect(() => new URL(fileUrl)).not.toThrow()
  85. expect(result).toContain("My%20Documents")
  86. })
  87. })
  88. describe("Windows paths", () => {
  89. test("should handle Windows absolute path with backslashes", () => {
  90. const windowsPath = "D:\\dev\\projects\\opencode\\README.bs.md"
  91. const result = encodeFilePath(windowsPath)
  92. const fileUrl = `file://${result}`
  93. // Should create a valid, parseable URL
  94. expect(() => new URL(fileUrl)).not.toThrow()
  95. const url = new URL(fileUrl)
  96. expect(url.protocol).toBe("file:")
  97. expect(url.pathname).toContain("README.bs.md")
  98. expect(result).toBe("/D:/dev/projects/opencode/README.bs.md")
  99. })
  100. test("should handle mixed separator path (Windows + Unix)", () => {
  101. // This is what happens in build-request-parts.ts when concatenating paths
  102. const mixedPath = "D:\\dev\\projects\\opencode/README.bs.md"
  103. const result = encodeFilePath(mixedPath)
  104. const fileUrl = `file://${result}`
  105. expect(() => new URL(fileUrl)).not.toThrow()
  106. expect(result).toBe("/D:/dev/projects/opencode/README.bs.md")
  107. })
  108. test("should handle Windows path with spaces", () => {
  109. const windowsPath = "C:\\Program Files\\MyApp\\file with spaces.txt"
  110. const result = encodeFilePath(windowsPath)
  111. const fileUrl = `file://${result}`
  112. expect(() => new URL(fileUrl)).not.toThrow()
  113. expect(result).toContain("Program%20Files")
  114. expect(result).toContain("file%20with%20spaces.txt")
  115. })
  116. test("should handle Windows path with special chars in filename", () => {
  117. const windowsPath = "D:\\projects\\file#name with ?marks.txt"
  118. const result = encodeFilePath(windowsPath)
  119. const fileUrl = `file://${result}`
  120. expect(() => new URL(fileUrl)).not.toThrow()
  121. expect(result).toContain("file%23name%20with%20%3Fmarks.txt")
  122. })
  123. test("should handle Windows root directory", () => {
  124. const windowsPath = "C:\\"
  125. const result = encodeFilePath(windowsPath)
  126. const fileUrl = `file://${result}`
  127. expect(() => new URL(fileUrl)).not.toThrow()
  128. expect(result).toBe("/C:/")
  129. })
  130. test("should handle Windows relative path with backslashes", () => {
  131. const windowsPath = "src\\components\\App.tsx"
  132. const result = encodeFilePath(windowsPath)
  133. // Relative paths shouldn't get the leading slash
  134. expect(result).toBe("src/components/App.tsx")
  135. })
  136. test("should NOT create invalid URL like the bug report", () => {
  137. // This is the exact scenario from bug report by @alexyaroshuk
  138. const windowsPath = "D:\\dev\\projects\\opencode\\README.bs.md"
  139. const result = encodeFilePath(windowsPath)
  140. const fileUrl = `file://${result}`
  141. // The bug was creating: file://D%3A%5Cdev%5Cprojects%5Copencode/README.bs.md
  142. expect(result).not.toContain("%5C") // Should not have encoded backslashes
  143. expect(result).not.toBe("D%3A%5Cdev%5Cprojects%5Copencode/README.bs.md")
  144. // Should be valid
  145. expect(() => new URL(fileUrl)).not.toThrow()
  146. })
  147. test("should handle lowercase drive letters", () => {
  148. const windowsPath = "c:\\users\\test\\file.txt"
  149. const result = encodeFilePath(windowsPath)
  150. const fileUrl = `file://${result}`
  151. expect(() => new URL(fileUrl)).not.toThrow()
  152. expect(result).toBe("/c:/users/test/file.txt")
  153. })
  154. })
  155. describe("Cross-platform compatibility", () => {
  156. test("should preserve Unix paths unchanged (except encoding)", () => {
  157. const unixPath = "/usr/local/bin/app"
  158. const result = encodeFilePath(unixPath)
  159. expect(result).toBe("/usr/local/bin/app")
  160. })
  161. test("should normalize Windows paths for cross-platform use", () => {
  162. const windowsPath = "C:\\Users\\test\\file.txt"
  163. const result = encodeFilePath(windowsPath)
  164. // Should convert to forward slashes and add leading /
  165. expect(result).not.toContain("\\")
  166. expect(result).toMatch(/^\/[A-Za-z]:\//)
  167. })
  168. test("should handle relative paths the same on all platforms", () => {
  169. const unixRelative = "src/app.ts"
  170. const windowsRelative = "src\\app.ts"
  171. const unixResult = encodeFilePath(unixRelative)
  172. const windowsResult = encodeFilePath(windowsRelative)
  173. // Both should normalize to forward slashes
  174. expect(unixResult).toBe("src/app.ts")
  175. expect(windowsResult).toBe("src/app.ts")
  176. })
  177. })
  178. describe("Edge cases", () => {
  179. test("should handle empty path", () => {
  180. const result = encodeFilePath("")
  181. expect(result).toBe("")
  182. })
  183. test("should handle path with multiple consecutive slashes", () => {
  184. const result = encodeFilePath("//path//to///file.txt")
  185. // Multiple slashes should be preserved (backend handles normalization)
  186. expect(result).toBe("//path//to///file.txt")
  187. })
  188. test("should encode Unicode characters", () => {
  189. const unicodePath = "/home/user/文档/README.md"
  190. const result = encodeFilePath(unicodePath)
  191. const fileUrl = `file://${result}`
  192. expect(() => new URL(fileUrl)).not.toThrow()
  193. // Unicode should be encoded
  194. expect(result).toContain("%E6%96%87%E6%A1%A3")
  195. })
  196. test("should handle already normalized Windows path", () => {
  197. // Path that's already been normalized (has / before drive letter)
  198. const alreadyNormalized = "/D:/path/file.txt"
  199. const result = encodeFilePath(alreadyNormalized)
  200. // Should not add another leading slash
  201. expect(result).toBe("/D:/path/file.txt")
  202. expect(result).not.toContain("//D")
  203. })
  204. test("should handle just drive letter", () => {
  205. const justDrive = "D:"
  206. const result = encodeFilePath(justDrive)
  207. const fileUrl = `file://${result}`
  208. expect(result).toBe("/D:")
  209. expect(() => new URL(fileUrl)).not.toThrow()
  210. })
  211. test("should handle Windows path with trailing backslash", () => {
  212. const trailingBackslash = "C:\\Users\\test\\"
  213. const result = encodeFilePath(trailingBackslash)
  214. const fileUrl = `file://${result}`
  215. expect(() => new URL(fileUrl)).not.toThrow()
  216. expect(result).toBe("/C:/Users/test/")
  217. })
  218. test("should handle very long paths", () => {
  219. const longPath = "C:\\Users\\test\\" + "verylongdirectoryname\\".repeat(20) + "file.txt"
  220. const result = encodeFilePath(longPath)
  221. const fileUrl = `file://${result}`
  222. expect(() => new URL(fileUrl)).not.toThrow()
  223. expect(result).not.toContain("\\")
  224. })
  225. test("should handle paths with dots", () => {
  226. const pathWithDots = "C:\\Users\\..\\test\\.\\file.txt"
  227. const result = encodeFilePath(pathWithDots)
  228. const fileUrl = `file://${result}`
  229. expect(() => new URL(fileUrl)).not.toThrow()
  230. // Dots should be preserved (backend normalizes)
  231. expect(result).toContain("..")
  232. expect(result).toContain("/./")
  233. })
  234. })
  235. describe("Regression tests for PR #12424", () => {
  236. test("should handle file with # in name", () => {
  237. const path = "/path/to/file#name.txt"
  238. const result = encodeFilePath(path)
  239. const fileUrl = `file://${result}`
  240. expect(() => new URL(fileUrl)).not.toThrow()
  241. expect(result).toBe("/path/to/file%23name.txt")
  242. })
  243. test("should handle file with ? in name", () => {
  244. const path = "/path/to/file?name.txt"
  245. const result = encodeFilePath(path)
  246. const fileUrl = `file://${result}`
  247. expect(() => new URL(fileUrl)).not.toThrow()
  248. expect(result).toBe("/path/to/file%3Fname.txt")
  249. })
  250. test("should handle file with % in name", () => {
  251. const path = "/path/to/file%name.txt"
  252. const result = encodeFilePath(path)
  253. const fileUrl = `file://${result}`
  254. expect(() => new URL(fileUrl)).not.toThrow()
  255. expect(result).toBe("/path/to/file%25name.txt")
  256. })
  257. })
  258. describe("Integration with file:// URL construction", () => {
  259. test("should work with query parameters (Linux)", () => {
  260. const path = "/home/user/file.txt"
  261. const encoded = encodeFilePath(path)
  262. const fileUrl = `file://${encoded}?start=10&end=20`
  263. const url = new URL(fileUrl)
  264. expect(url.searchParams.get("start")).toBe("10")
  265. expect(url.searchParams.get("end")).toBe("20")
  266. expect(url.pathname).toBe("/home/user/file.txt")
  267. })
  268. test("should work with query parameters (Windows)", () => {
  269. const path = "C:\\Users\\test\\file.txt"
  270. const encoded = encodeFilePath(path)
  271. const fileUrl = `file://${encoded}?start=10&end=20`
  272. const url = new URL(fileUrl)
  273. expect(url.searchParams.get("start")).toBe("10")
  274. expect(url.searchParams.get("end")).toBe("20")
  275. })
  276. test("should parse correctly in URL constructor (Linux)", () => {
  277. const path = "/var/log/app.log"
  278. const fileUrl = `file://${encodeFilePath(path)}`
  279. const url = new URL(fileUrl)
  280. expect(url.protocol).toBe("file:")
  281. expect(url.pathname).toBe("/var/log/app.log")
  282. })
  283. test("should parse correctly in URL constructor (Windows)", () => {
  284. const path = "D:\\logs\\app.log"
  285. const fileUrl = `file://${encodeFilePath(path)}`
  286. const url = new URL(fileUrl)
  287. expect(url.protocol).toBe("file:")
  288. expect(url.pathname).toContain("app.log")
  289. })
  290. })
  291. })