pty-session.test.ts 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. import { describe, expect, test } from "bun:test"
  2. import { AppRuntime } from "../../src/effect/app-runtime"
  3. import { Bus } from "../../src/bus"
  4. import { Effect } from "effect"
  5. import { Instance } from "../../src/project/instance"
  6. import { Pty } from "../../src/pty"
  7. import type { PtyID } from "../../src/pty/schema"
  8. import { tmpdir } from "../fixture/fixture"
  9. import { setTimeout as sleep } from "node:timers/promises"
  10. const wait = async (fn: () => boolean, ms = 5000) => {
  11. const end = Date.now() + ms
  12. while (Date.now() < end) {
  13. if (fn()) return
  14. await sleep(25)
  15. }
  16. throw new Error("timeout waiting for pty events")
  17. }
  18. const pick = (log: Array<{ type: "created" | "exited" | "deleted"; id: PtyID }>, id: PtyID) => {
  19. return log.filter((evt) => evt.id === id).map((evt) => evt.type)
  20. }
  21. describe("pty", () => {
  22. test("publishes created, exited, deleted in order for a short-lived process", async () => {
  23. if (process.platform === "win32") return
  24. await using dir = await tmpdir({ git: true })
  25. await Instance.provide({
  26. directory: dir.path,
  27. fn: () =>
  28. AppRuntime.runPromise(
  29. Effect.gen(function* () {
  30. const pty = yield* Pty.Service
  31. const log: Array<{ type: "created" | "exited" | "deleted"; id: PtyID }> = []
  32. const off = [
  33. Bus.subscribe(Pty.Event.Created, (evt) => log.push({ type: "created", id: evt.properties.info.id })),
  34. Bus.subscribe(Pty.Event.Exited, (evt) => log.push({ type: "exited", id: evt.properties.id })),
  35. Bus.subscribe(Pty.Event.Deleted, (evt) => log.push({ type: "deleted", id: evt.properties.id })),
  36. ]
  37. let id: PtyID | undefined
  38. try {
  39. const info = yield* pty.create({
  40. command: "/usr/bin/env",
  41. args: ["sh", "-c", "sleep 0.1"],
  42. title: "sleep",
  43. })
  44. id = info.id
  45. yield* Effect.promise(() => wait(() => pick(log, id!).includes("exited")))
  46. yield* pty.remove(id)
  47. yield* Effect.promise(() => wait(() => pick(log, id!).length >= 3))
  48. expect(pick(log, id!)).toEqual(["created", "exited", "deleted"])
  49. } finally {
  50. off.forEach((x) => x())
  51. if (id) yield* pty.remove(id)
  52. }
  53. }),
  54. ),
  55. })
  56. })
  57. test("publishes created, exited, deleted in order for /bin/sh + remove", async () => {
  58. if (process.platform === "win32") return
  59. await using dir = await tmpdir({ git: true })
  60. await Instance.provide({
  61. directory: dir.path,
  62. fn: () =>
  63. AppRuntime.runPromise(
  64. Effect.gen(function* () {
  65. const pty = yield* Pty.Service
  66. const log: Array<{ type: "created" | "exited" | "deleted"; id: PtyID }> = []
  67. const off = [
  68. Bus.subscribe(Pty.Event.Created, (evt) => log.push({ type: "created", id: evt.properties.info.id })),
  69. Bus.subscribe(Pty.Event.Exited, (evt) => log.push({ type: "exited", id: evt.properties.id })),
  70. Bus.subscribe(Pty.Event.Deleted, (evt) => log.push({ type: "deleted", id: evt.properties.id })),
  71. ]
  72. let id: PtyID | undefined
  73. try {
  74. const info = yield* pty.create({ command: "/bin/sh", title: "sh" })
  75. id = info.id
  76. yield* Effect.promise(() => sleep(100))
  77. yield* pty.remove(id)
  78. yield* Effect.promise(() => wait(() => pick(log, id!).length >= 3))
  79. expect(pick(log, id!)).toEqual(["created", "exited", "deleted"])
  80. } finally {
  81. off.forEach((x) => x())
  82. if (id) yield* pty.remove(id)
  83. }
  84. }),
  85. ),
  86. })
  87. })
  88. })