environment.test.ts 3.5 KB

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