process.test.ts 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. import { describe, expect } from "bun:test"
  2. import { Effect, Exit, Stream } from "effect"
  3. import { ChildProcess } from "effect/unstable/process"
  4. import { AppProcess } from "@opencode-ai/core/process"
  5. import { testEffect } from "../lib/effect"
  6. const it = testEffect(AppProcess.defaultLayer)
  7. const NODE = process.execPath
  8. const cmd = (...args: string[]) => ChildProcess.make(NODE, args)
  9. describe("AppProcess", () => {
  10. describe("run", () => {
  11. it.effect(
  12. "captures stdout and exit code zero",
  13. Effect.gen(function* () {
  14. const svc = yield* AppProcess.Service
  15. const result = yield* svc.run(cmd("-e", "process.stdout.write('hi\\n')"))
  16. expect(result.exitCode).toBe(0)
  17. expect(result.stdout.toString("utf8")).toBe("hi\n")
  18. expect(result.truncated).toBe(false)
  19. }),
  20. )
  21. it.effect(
  22. "non-zero exit returns RunResult; caller can require success",
  23. Effect.gen(function* () {
  24. const svc = yield* AppProcess.Service
  25. const result = yield* svc.run(cmd("-e", "process.exit(1)"))
  26. expect(result.exitCode).toBe(1)
  27. }),
  28. )
  29. it.effect(
  30. "requireSuccess fails on non-zero exit",
  31. Effect.gen(function* () {
  32. const svc = yield* AppProcess.Service
  33. const exit = yield* Effect.exit(
  34. svc.run(cmd("-e", "process.exit(1)")).pipe(Effect.flatMap(AppProcess.requireSuccess)),
  35. )
  36. expect(Exit.isFailure(exit)).toBe(true)
  37. if (Exit.isFailure(exit)) {
  38. const reason = exit.cause.reasons[0]
  39. if (reason && reason._tag === "Fail") {
  40. expect(reason.error).toBeInstanceOf(AppProcess.AppProcessError)
  41. expect((reason.error as AppProcess.AppProcessError).exitCode).toBe(1)
  42. } else {
  43. throw new Error("expected fail reason")
  44. }
  45. }
  46. }),
  47. )
  48. it.effect(
  49. "requireSuccess succeeds on exit 0",
  50. Effect.gen(function* () {
  51. const svc = yield* AppProcess.Service
  52. const result = yield* svc.run(cmd("-e", "process.exit(0)")).pipe(Effect.flatMap(AppProcess.requireSuccess))
  53. expect(result.exitCode).toBe(0)
  54. }),
  55. )
  56. it.effect(
  57. "requireExitIn allowlists multiple exit codes",
  58. Effect.gen(function* () {
  59. const svc = yield* AppProcess.Service
  60. const requireZeroOrOne = AppProcess.requireExitIn([0, 1])
  61. const okZero = yield* svc.run(cmd("-e", "process.exit(0)")).pipe(Effect.flatMap(requireZeroOrOne))
  62. expect(okZero.exitCode).toBe(0)
  63. const okOne = yield* svc.run(cmd("-e", "process.exit(1)")).pipe(Effect.flatMap(requireZeroOrOne))
  64. expect(okOne.exitCode).toBe(1)
  65. const exit = yield* Effect.exit(svc.run(cmd("-e", "process.exit(2)")).pipe(Effect.flatMap(requireZeroOrOne)))
  66. expect(Exit.isFailure(exit)).toBe(true)
  67. if (Exit.isFailure(exit)) {
  68. const reason = exit.cause.reasons[0]
  69. if (reason && reason._tag === "Fail") {
  70. expect(reason.error).toBeInstanceOf(AppProcess.AppProcessError)
  71. expect((reason.error as AppProcess.AppProcessError).exitCode).toBe(2)
  72. }
  73. }
  74. }),
  75. )
  76. it.effect(
  77. "truncates output when maxOutputBytes is set",
  78. Effect.gen(function* () {
  79. const svc = yield* AppProcess.Service
  80. const result = yield* svc.run(cmd("-e", "process.stdout.write('0123456789')"), { maxOutputBytes: 5 })
  81. expect(result.exitCode).toBe(0)
  82. expect(result.truncated).toBe(true)
  83. expect(result.stdout.length).toBe(5)
  84. expect(result.stdout.toString("utf8")).toBe("01234")
  85. }),
  86. )
  87. it.effect(
  88. "result includes command description",
  89. Effect.gen(function* () {
  90. const svc = yield* AppProcess.Service
  91. const result = yield* svc.run(cmd("-e", "process.stdout.write('hi')"))
  92. expect(result.command).toBe(`${NODE} -e process.stdout.write('hi')`)
  93. }),
  94. )
  95. })
  96. describe("inherited platform methods", () => {
  97. it.effect(
  98. "string returns stdout as string",
  99. Effect.gen(function* () {
  100. const svc = yield* AppProcess.Service
  101. const out = yield* svc.string(cmd("-e", "process.stdout.write('hi\\n')"))
  102. expect(out).toBe("hi\n")
  103. }),
  104. )
  105. it.effect(
  106. "lines returns the platform's array of lines",
  107. Effect.gen(function* () {
  108. const svc = yield* AppProcess.Service
  109. const out = yield* svc.lines(cmd("-e", "process.stdout.write('a\\nb\\n')"))
  110. expect(Array.from(out)).toEqual(["a", "b"])
  111. }),
  112. )
  113. })
  114. describe("runStream", () => {
  115. it.live(
  116. "emits lines incrementally and ends cleanly on exit 0",
  117. Effect.gen(function* () {
  118. const svc = yield* AppProcess.Service
  119. const result = yield* svc
  120. .runStream(cmd("-e", "console.log('one'); console.log('two'); console.log('three')"))
  121. .pipe(Stream.runCollect)
  122. expect(Array.from(result)).toEqual(["one", "two", "three"])
  123. }),
  124. )
  125. it.live(
  126. "fails with AppProcessError when exit not in okExitCodes",
  127. Effect.gen(function* () {
  128. const svc = yield* AppProcess.Service
  129. const exit = yield* Effect.exit(
  130. svc.runStream(cmd("-e", "console.log('a'); process.exit(2)"), { okExitCodes: [0] }).pipe(Stream.runCollect),
  131. )
  132. expect(Exit.isFailure(exit)).toBe(true)
  133. if (Exit.isFailure(exit)) {
  134. const reason = exit.cause.reasons[0]
  135. if (reason && reason._tag === "Fail") {
  136. expect(reason.error).toBeInstanceOf(AppProcess.AppProcessError)
  137. }
  138. }
  139. }),
  140. )
  141. it.live(
  142. "okExitCodes allowlist treats non-zero as success",
  143. Effect.gen(function* () {
  144. const svc = yield* AppProcess.Service
  145. const result = yield* svc
  146. .runStream(cmd("-e", "console.log('only'); process.exit(1)"), { okExitCodes: [0, 1] })
  147. .pipe(Stream.runCollect)
  148. expect(Array.from(result)).toEqual(["only"])
  149. }),
  150. )
  151. it.live(
  152. "without okExitCodes, never fails on exit code",
  153. Effect.gen(function* () {
  154. const svc = yield* AppProcess.Service
  155. const result = yield* svc.runStream(cmd("-e", "console.log('only'); process.exit(7)")).pipe(Stream.runCollect)
  156. expect(Array.from(result)).toEqual(["only"])
  157. }),
  158. )
  159. it.live(
  160. "AbortSignal interrupts the stream",
  161. Effect.gen(function* () {
  162. const svc = yield* AppProcess.Service
  163. const controller = new AbortController()
  164. setTimeout(() => controller.abort(), 50)
  165. const exit = yield* Effect.exit(
  166. svc
  167. .runStream(cmd("-e", "setInterval(() => console.log('tick'), 100); setTimeout(() => {}, 60_000)"), {
  168. signal: controller.signal,
  169. })
  170. .pipe(Stream.runCollect),
  171. )
  172. expect(Exit.isFailure(exit)).toBe(true)
  173. }),
  174. )
  175. })
  176. describe("spawn (inherited)", () => {
  177. it.live(
  178. "returns the platform ChildProcessHandle for advanced use",
  179. Effect.scoped(
  180. Effect.gen(function* () {
  181. const svc = yield* AppProcess.Service
  182. const handle = yield* svc.spawn(cmd("-e", "setInterval(() => {}, 1_000)"))
  183. expect(yield* handle.isRunning).toBe(true)
  184. yield* handle.kill()
  185. }),
  186. ),
  187. )
  188. })
  189. })