spawner.test.ts 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
  1. import { describe, expect } from "bun:test"
  2. import { Effect, Layer, Stream } from "effect"
  3. import { ChildProcess } from "effect/unstable/process"
  4. import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
  5. import { InMemoryFs } from "just-bash"
  6. import { SimulationFileSystem } from "../../../src/testing/simulation/filesystem"
  7. import { SimulationSpawner } from "../../../src/testing/simulation/spawner"
  8. import { testEffect } from "../../lib/effect"
  9. const root = "/opencode"
  10. const fs = new InMemoryFs()
  11. const it = testEffect(
  12. Layer.mergeAll(SimulationFileSystem.layer({ root, fs }), SimulationSpawner.layer({ root, fs })),
  13. )
  14. describe("SimulationSpawner", () => {
  15. it.effect("runs shell commands through just-bash", () =>
  16. Effect.gen(function* () {
  17. const spawner = yield* ChildProcessSpawner
  18. const handle = yield* spawner.spawn(
  19. ChildProcess.make("printf 'hello' > file.txt && cat file.txt", [], { cwd: root, shell: "/bin/bash" }),
  20. ).pipe(Effect.scoped)
  21. expect(yield* Stream.mkString(Stream.decodeText(handle.stdout))).toBe("hello")
  22. expect(Number(yield* handle.exitCode)).toBe(0)
  23. expect(yield* Effect.promise(() => fs.readFile("/opencode/file.txt"))).toBe("hello")
  24. }),
  25. )
  26. it.effect("extracts shell command text from -lc", () =>
  27. Effect.gen(function* () {
  28. const spawner = yield* ChildProcessSpawner
  29. const handle = yield* spawner.spawn(ChildProcess.make("bash", ["-lc", "printf ok"], { cwd: root })).pipe(Effect.scoped)
  30. expect(yield* Stream.mkString(Stream.decodeText(handle.stdout))).toBe("ok")
  31. expect(Number(yield* handle.exitCode)).toBe(0)
  32. }),
  33. )
  34. it.effect("fakes git discovery commands", () =>
  35. Effect.gen(function* () {
  36. const spawner = yield* ChildProcessSpawner
  37. const [common, topLevel, revision] = yield* Effect.all(
  38. [
  39. spawner.spawn(ChildProcess.make("git", ["rev-parse", "--git-common-dir"], { cwd: root })).pipe(Effect.scoped),
  40. spawner.spawn(ChildProcess.make("git", ["rev-parse", "--show-toplevel"], { cwd: root })).pipe(Effect.scoped),
  41. spawner.spawn(ChildProcess.make("git", ["rev-list", "--max-parents=0", "HEAD"], { cwd: root })).pipe(Effect.scoped),
  42. ],
  43. { concurrency: 3 },
  44. )
  45. expect(yield* Stream.mkString(Stream.decodeText(common.stdout))).toBe(".git\n")
  46. expect(yield* Stream.mkString(Stream.decodeText(topLevel.stdout))).toBe("/opencode\n")
  47. expect(yield* Stream.mkString(Stream.decodeText(revision.stdout))).toBe("0000000000000000000000000000000000000000\n")
  48. }),
  49. )
  50. it.effect("rejects unsupported non-shell commands", () =>
  51. Effect.gen(function* () {
  52. const spawner = yield* ChildProcessSpawner
  53. const exit = yield* spawner.spawn(ChildProcess.make("git", ["status"], { cwd: root })).pipe(Effect.scoped, Effect.exit)
  54. expect(exit._tag).toBe("Failure")
  55. }),
  56. )
  57. it.effect("rejects shell commands outside the simulation root", () =>
  58. Effect.gen(function* () {
  59. const spawner = yield* ChildProcessSpawner
  60. const exit = yield* spawner.spawn(ChildProcess.make("printf blocked", [], { cwd: "/tmp", shell: "/bin/bash" })).pipe(
  61. Effect.scoped,
  62. Effect.exit,
  63. )
  64. expect(exit._tag).toBe("Failure")
  65. }),
  66. )
  67. })