environment.test.ts 3.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  1. import fs from "node:fs/promises"
  2. import { describe, expect } from "bun:test"
  3. import { Effect } from "effect"
  4. import { ChildProcessSpawner } from "effect/unstable/process"
  5. import { CrossSpawnSpawner } from "@opencode-ai/util/cross-spawn-spawner"
  6. import { LayerNode } from "@opencode-ai/util/effect/layer-node"
  7. import {
  8. execDefaults,
  9. Failed,
  10. makeFiles,
  11. makeLocalDriver,
  12. makeMemoryDriver,
  13. NotFound,
  14. typeFollowing,
  15. } from "../src/environment/index"
  16. import { tmpdir } from "./fixture/tmpdir"
  17. import { environmentConformance } from "./lib/environment-conformance"
  18. import { it } from "./lib/effect"
  19. describe("typeFollowing", () => {
  20. it.effect("follows symlinks without changing stat semantics", () =>
  21. Effect.gen(function* () {
  22. const driver = makeMemoryDriver()
  23. const files = makeFiles(driver)
  24. yield* files.mkdir("/directory")
  25. yield* files.write("/file", new Uint8Array())
  26. yield* driver.symlink("/directory", "/directory-link")
  27. yield* driver.symlink("/file", "/file-link")
  28. yield* driver.symlink("/missing", "/dangling-link")
  29. expect(yield* typeFollowing(files, "/directory-link")).toBe("directory")
  30. expect(yield* typeFollowing(files, "/file-link")).toBe("file")
  31. expect(yield* typeFollowing(files, "/dangling-link").pipe(Effect.flip)).toBeInstanceOf(NotFound)
  32. }),
  33. )
  34. })
  35. environmentConformance("memory environment", () =>
  36. Effect.sync(() => {
  37. const driver = makeMemoryDriver()
  38. return {
  39. files: makeFiles(driver),
  40. root: `/workspace-${crypto.randomUUID()}`,
  41. symlink: driver.symlink,
  42. }
  43. }),
  44. )
  45. environmentConformance("local environment", () =>
  46. Effect.gen(function* () {
  47. const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
  48. const tmp = yield* Effect.promise(() => tmpdir("opencode-local-environment-"))
  49. return {
  50. files: makeFiles(makeLocalDriver(spawner)),
  51. root: tmp.path,
  52. ...(process.platform === "win32"
  53. ? {}
  54. : {
  55. symlink: (target: string, link: string) =>
  56. Effect.tryPromise({
  57. try: () => fs.symlink(target, link),
  58. catch: (cause) => new Failed({ path: link, cause }),
  59. }),
  60. }),
  61. dispose: Effect.promise(() => tmp[Symbol.asyncDispose]()),
  62. }
  63. }).pipe(Effect.provide(LayerNode.compile(CrossSpawnSpawner.node))),
  64. )
  65. environmentConformance(
  66. "GNU exec environment",
  67. () =>
  68. Effect.gen(function* () {
  69. const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
  70. const tmp = yield* Effect.promise(() => tmpdir("opencode-environment-"))
  71. return {
  72. files: execDefaults(spawner),
  73. root: tmp.path,
  74. symlink: (target: string, link: string) =>
  75. Effect.tryPromise({
  76. try: () => fs.symlink(target, link),
  77. catch: (cause) => new Failed({ path: link, cause }),
  78. }),
  79. dispose: Effect.promise(() => tmp[Symbol.asyncDispose]()),
  80. }
  81. }).pipe(Effect.provide(LayerNode.compile(CrossSpawnSpawner.node))),
  82. process.platform !== "linux",
  83. )