filesystem.test.ts 9.5 KB

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