location-filesystem.test.ts 2.8 KB

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