filesystem.test.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. import { describe, test, expect } from "bun:test"
  2. import { Effect, Layer, FileSystem } from "effect"
  3. import { NodeFileSystem } from "@effect/platform-node"
  4. import { AppFileSystem } from "@opencode-ai/shared/filesystem"
  5. import { testEffect } from "../lib/effect"
  6. import path from "path"
  7. const live = AppFileSystem.layer.pipe(Layer.provideMerge(NodeFileSystem.layer))
  8. const { effect: it } = testEffect(live)
  9. describe("AppFileSystem", () => {
  10. describe("isDir", () => {
  11. it(
  12. "returns true for directories",
  13. Effect.gen(function* () {
  14. const fs = yield* AppFileSystem.Service
  15. const filesys = yield* FileSystem.FileSystem
  16. const tmp = yield* filesys.makeTempDirectoryScoped()
  17. expect(yield* fs.isDir(tmp)).toBe(true)
  18. }),
  19. )
  20. it(
  21. "returns false for files",
  22. Effect.gen(function* () {
  23. const fs = yield* AppFileSystem.Service
  24. const filesys = yield* FileSystem.FileSystem
  25. const tmp = yield* filesys.makeTempDirectoryScoped()
  26. const file = path.join(tmp, "test.txt")
  27. yield* filesys.writeFileString(file, "hello")
  28. expect(yield* fs.isDir(file)).toBe(false)
  29. }),
  30. )
  31. it(
  32. "returns false for non-existent paths",
  33. Effect.gen(function* () {
  34. const fs = yield* AppFileSystem.Service
  35. expect(yield* fs.isDir("/tmp/nonexistent-" + Math.random())).toBe(false)
  36. }),
  37. )
  38. })
  39. describe("isFile", () => {
  40. it(
  41. "returns true for files",
  42. Effect.gen(function* () {
  43. const fs = yield* AppFileSystem.Service
  44. const filesys = yield* FileSystem.FileSystem
  45. const tmp = yield* filesys.makeTempDirectoryScoped()
  46. const file = path.join(tmp, "test.txt")
  47. yield* filesys.writeFileString(file, "hello")
  48. expect(yield* fs.isFile(file)).toBe(true)
  49. }),
  50. )
  51. it(
  52. "returns false for directories",
  53. Effect.gen(function* () {
  54. const fs = yield* AppFileSystem.Service
  55. const filesys = yield* FileSystem.FileSystem
  56. const tmp = yield* filesys.makeTempDirectoryScoped()
  57. expect(yield* fs.isFile(tmp)).toBe(false)
  58. }),
  59. )
  60. })
  61. describe("readJson / writeJson", () => {
  62. it(
  63. "round-trips JSON data",
  64. Effect.gen(function* () {
  65. const fs = yield* AppFileSystem.Service
  66. const filesys = yield* FileSystem.FileSystem
  67. const tmp = yield* filesys.makeTempDirectoryScoped()
  68. const file = path.join(tmp, "data.json")
  69. const data = { name: "test", count: 42, nested: { ok: true } }
  70. yield* fs.writeJson(file, data)
  71. const result = yield* fs.readJson(file)
  72. expect(result).toEqual(data)
  73. }),
  74. )
  75. })
  76. describe("ensureDir", () => {
  77. it(
  78. "creates nested directories",
  79. Effect.gen(function* () {
  80. const fs = yield* AppFileSystem.Service
  81. const filesys = yield* FileSystem.FileSystem
  82. const tmp = yield* filesys.makeTempDirectoryScoped()
  83. const nested = path.join(tmp, "a", "b", "c")
  84. yield* fs.ensureDir(nested)
  85. const info = yield* filesys.stat(nested)
  86. expect(info.type).toBe("Directory")
  87. }),
  88. )
  89. it(
  90. "is idempotent",
  91. Effect.gen(function* () {
  92. const fs = yield* AppFileSystem.Service
  93. const filesys = yield* FileSystem.FileSystem
  94. const tmp = yield* filesys.makeTempDirectoryScoped()
  95. const dir = path.join(tmp, "existing")
  96. yield* filesys.makeDirectory(dir)
  97. yield* fs.ensureDir(dir)
  98. const info = yield* filesys.stat(dir)
  99. expect(info.type).toBe("Directory")
  100. }),
  101. )
  102. })
  103. describe("writeWithDirs", () => {
  104. it(
  105. "creates parent directories if missing",
  106. Effect.gen(function* () {
  107. const fs = yield* AppFileSystem.Service
  108. const filesys = yield* FileSystem.FileSystem
  109. const tmp = yield* filesys.makeTempDirectoryScoped()
  110. const file = path.join(tmp, "deep", "nested", "file.txt")
  111. yield* fs.writeWithDirs(file, "hello")
  112. expect(yield* filesys.readFileString(file)).toBe("hello")
  113. }),
  114. )
  115. it(
  116. "writes directly when parent exists",
  117. Effect.gen(function* () {
  118. const fs = yield* AppFileSystem.Service
  119. const filesys = yield* FileSystem.FileSystem
  120. const tmp = yield* filesys.makeTempDirectoryScoped()
  121. const file = path.join(tmp, "direct.txt")
  122. yield* fs.writeWithDirs(file, "world")
  123. expect(yield* filesys.readFileString(file)).toBe("world")
  124. }),
  125. )
  126. it(
  127. "writes Uint8Array content",
  128. Effect.gen(function* () {
  129. const fs = yield* AppFileSystem.Service
  130. const filesys = yield* FileSystem.FileSystem
  131. const tmp = yield* filesys.makeTempDirectoryScoped()
  132. const file = path.join(tmp, "binary.bin")
  133. const content = new Uint8Array([0x00, 0x01, 0x02, 0x03])
  134. yield* fs.writeWithDirs(file, content)
  135. const result = yield* filesys.readFile(file)
  136. expect(new Uint8Array(result)).toEqual(content)
  137. }),
  138. )
  139. })
  140. describe("findUp", () => {
  141. it(
  142. "finds target in start directory",
  143. Effect.gen(function* () {
  144. const fs = yield* AppFileSystem.Service
  145. const filesys = yield* FileSystem.FileSystem
  146. const tmp = yield* filesys.makeTempDirectoryScoped()
  147. yield* filesys.writeFileString(path.join(tmp, "target.txt"), "found")
  148. const result = yield* fs.findUp("target.txt", tmp)
  149. expect(result).toEqual([path.join(tmp, "target.txt")])
  150. }),
  151. )
  152. it(
  153. "finds target in parent directories",
  154. Effect.gen(function* () {
  155. const fs = yield* AppFileSystem.Service
  156. const filesys = yield* FileSystem.FileSystem
  157. const tmp = yield* filesys.makeTempDirectoryScoped()
  158. yield* filesys.writeFileString(path.join(tmp, "marker"), "root")
  159. const child = path.join(tmp, "a", "b")
  160. yield* filesys.makeDirectory(child, { recursive: true })
  161. const result = yield* fs.findUp("marker", child, tmp)
  162. expect(result).toEqual([path.join(tmp, "marker")])
  163. }),
  164. )
  165. it(
  166. "returns empty array when not found",
  167. Effect.gen(function* () {
  168. const fs = yield* AppFileSystem.Service
  169. const filesys = yield* FileSystem.FileSystem
  170. const tmp = yield* filesys.makeTempDirectoryScoped()
  171. const result = yield* fs.findUp("nonexistent", tmp, tmp)
  172. expect(result).toEqual([])
  173. }),
  174. )
  175. })
  176. describe("up", () => {
  177. it(
  178. "finds multiple targets walking up",
  179. Effect.gen(function* () {
  180. const fs = yield* AppFileSystem.Service
  181. const filesys = yield* FileSystem.FileSystem
  182. const tmp = yield* filesys.makeTempDirectoryScoped()
  183. yield* filesys.writeFileString(path.join(tmp, "a.txt"), "a")
  184. yield* filesys.writeFileString(path.join(tmp, "b.txt"), "b")
  185. const child = path.join(tmp, "sub")
  186. yield* filesys.makeDirectory(child)
  187. yield* filesys.writeFileString(path.join(child, "a.txt"), "a-child")
  188. const result = yield* fs.up({ targets: ["a.txt", "b.txt"], start: child, stop: tmp })
  189. expect(result).toContain(path.join(child, "a.txt"))
  190. expect(result).toContain(path.join(tmp, "a.txt"))
  191. expect(result).toContain(path.join(tmp, "b.txt"))
  192. }),
  193. )
  194. })
  195. describe("glob", () => {
  196. it(
  197. "finds files matching pattern",
  198. Effect.gen(function* () {
  199. const fs = yield* AppFileSystem.Service
  200. const filesys = yield* FileSystem.FileSystem
  201. const tmp = yield* filesys.makeTempDirectoryScoped()
  202. yield* filesys.writeFileString(path.join(tmp, "a.ts"), "a")
  203. yield* filesys.writeFileString(path.join(tmp, "b.ts"), "b")
  204. yield* filesys.writeFileString(path.join(tmp, "c.json"), "c")
  205. const result = yield* fs.glob("*.ts", { cwd: tmp })
  206. expect(result.sort()).toEqual(["a.ts", "b.ts"])
  207. }),
  208. )
  209. it(
  210. "supports absolute paths",
  211. Effect.gen(function* () {
  212. const fs = yield* AppFileSystem.Service
  213. const filesys = yield* FileSystem.FileSystem
  214. const tmp = yield* filesys.makeTempDirectoryScoped()
  215. yield* filesys.writeFileString(path.join(tmp, "file.txt"), "hello")
  216. const result = yield* fs.glob("*.txt", { cwd: tmp, absolute: true })
  217. expect(result).toEqual([path.join(tmp, "file.txt")])
  218. }),
  219. )
  220. })
  221. describe("globMatch", () => {
  222. it(
  223. "matches patterns",
  224. Effect.gen(function* () {
  225. const fs = yield* AppFileSystem.Service
  226. expect(fs.globMatch("*.ts", "foo.ts")).toBe(true)
  227. expect(fs.globMatch("*.ts", "foo.json")).toBe(false)
  228. expect(fs.globMatch("src/**", "src/a/b.ts")).toBe(true)
  229. }),
  230. )
  231. })
  232. describe("globUp", () => {
  233. it(
  234. "finds files walking up directories",
  235. Effect.gen(function* () {
  236. const fs = yield* AppFileSystem.Service
  237. const filesys = yield* FileSystem.FileSystem
  238. const tmp = yield* filesys.makeTempDirectoryScoped()
  239. yield* filesys.writeFileString(path.join(tmp, "root.md"), "root")
  240. const child = path.join(tmp, "a", "b")
  241. yield* filesys.makeDirectory(child, { recursive: true })
  242. yield* filesys.writeFileString(path.join(child, "leaf.md"), "leaf")
  243. const result = yield* fs.globUp("*.md", child, tmp)
  244. expect(result).toContain(path.join(child, "leaf.md"))
  245. expect(result).toContain(path.join(tmp, "root.md"))
  246. }),
  247. )
  248. })
  249. describe("built-in passthrough", () => {
  250. it(
  251. "exists works",
  252. Effect.gen(function* () {
  253. yield* AppFileSystem.Service
  254. const filesys = yield* FileSystem.FileSystem
  255. const tmp = yield* filesys.makeTempDirectoryScoped()
  256. const file = path.join(tmp, "exists.txt")
  257. yield* filesys.writeFileString(file, "yes")
  258. expect(yield* filesys.exists(file)).toBe(true)
  259. expect(yield* filesys.exists(file + ".nope")).toBe(false)
  260. }),
  261. )
  262. it(
  263. "remove works",
  264. Effect.gen(function* () {
  265. yield* AppFileSystem.Service
  266. const filesys = yield* FileSystem.FileSystem
  267. const tmp = yield* filesys.makeTempDirectoryScoped()
  268. const file = path.join(tmp, "delete-me.txt")
  269. yield* filesys.writeFileString(file, "bye")
  270. yield* filesys.remove(file)
  271. expect(yield* filesys.exists(file)).toBe(false)
  272. }),
  273. )
  274. })
  275. describe("pure helpers", () => {
  276. test("mimeType returns correct types", () => {
  277. expect(AppFileSystem.mimeType("file.json")).toBe("application/json")
  278. expect(AppFileSystem.mimeType("image.png")).toBe("image/png")
  279. expect(AppFileSystem.mimeType("unknown.qzx")).toBe("application/octet-stream")
  280. })
  281. test("contains checks path containment", () => {
  282. expect(AppFileSystem.contains("/a/b", "/a/b/c")).toBe(true)
  283. expect(AppFileSystem.contains("/a/b", "/a/c")).toBe(false)
  284. })
  285. test("overlaps detects overlapping paths", () => {
  286. expect(AppFileSystem.overlaps("/a/b", "/a/b/c")).toBe(true)
  287. expect(AppFileSystem.overlaps("/a/b/c", "/a/b")).toBe(true)
  288. expect(AppFileSystem.overlaps("/a", "/b")).toBe(false)
  289. })
  290. })
  291. })