filesystem.test.ts 9.8 KB

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