location-filesystem.test.ts 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
  1. import fs from "fs/promises"
  2. import path from "path"
  3. import { describe, expect } from "bun:test"
  4. import { Effect, Exit, Layer } from "effect"
  5. import { LayerNode } from "@opencode-ai/core/effect/layer-node"
  6. import { FileSystem } from "@opencode-ai/core/filesystem"
  7. import { Location } from "@opencode-ai/core/location"
  8. import { AbsolutePath, RelativePath } from "@opencode-ai/core/schema"
  9. import { location } from "./fixture/location"
  10. import { tmpdir } from "./fixture/tmpdir"
  11. import { it } from "./lib/effect"
  12. const provide = (directory: string) =>
  13. Effect.provide(
  14. LayerNode.compile(FileSystem.node, [
  15. [
  16. Location.node,
  17. Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make(directory) }))),
  18. ],
  19. ]),
  20. )
  21. const withTmp = <A, E, R>(f: (directory: string) => Effect.Effect<A, E, R>) =>
  22. Effect.acquireRelease(
  23. Effect.promise(() => tmpdir()),
  24. (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
  25. ).pipe(Effect.flatMap((tmp) => f(tmp.path)))
  26. describe("FileSystem", () => {
  27. it.live("reads text and binary files", () =>
  28. withTmp((directory) =>
  29. Effect.gen(function* () {
  30. yield* Effect.promise(() => fs.writeFile(path.join(directory, "text.txt"), "hello"))
  31. yield* Effect.promise(() => fs.writeFile(path.join(directory, "data.bin"), Buffer.from([0, 1, 2])))
  32. const service = yield* FileSystem.Service
  33. const text = yield* service.read({ path: RelativePath.make("text.txt") })
  34. const binary = yield* service.read({ path: RelativePath.make("data.bin") })
  35. expect(new TextDecoder().decode(text.content)).toBe("hello")
  36. expect(text.mime).toBe("text/plain")
  37. expect(binary.content).toEqual(new Uint8Array([0, 1, 2]))
  38. }).pipe(provide(directory)),
  39. ),
  40. )
  41. it.live("lists direct children", () =>
  42. withTmp((directory) =>
  43. Effect.gen(function* () {
  44. yield* Effect.promise(() => fs.mkdir(path.join(directory, "src")))
  45. yield* Effect.promise(() => fs.writeFile(path.join(directory, "README.md"), "# Test"))
  46. const entries = yield* (yield* FileSystem.Service).list()
  47. expect(entries.map((entry) => ({ path: entry.path, type: entry.type }))).toEqual([
  48. { path: RelativePath.make("src" + path.sep), type: "directory" },
  49. { path: RelativePath.make("README.md"), type: "file" },
  50. ])
  51. }).pipe(provide(directory)),
  52. ),
  53. )
  54. it.live("rejects lexical escapes", () =>
  55. withTmp((directory) =>
  56. Effect.gen(function* () {
  57. const result = yield* (yield* FileSystem.Service)
  58. .read({ path: RelativePath.make("../outside.txt") })
  59. .pipe(Effect.exit)
  60. expect(Exit.isFailure(result)).toBe(true)
  61. }).pipe(provide(directory)),
  62. ),
  63. )
  64. })