app-lifecycle.test.tsx 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. import { expect, mock, test } from "bun:test"
  2. import type { TuiPluginApi } from "@opencode-ai/plugin/tui"
  3. import { createTestRenderer } from "@opentui/core/testing"
  4. import { Effect } from "effect"
  5. import { Global } from "@opencode-ai/core/global"
  6. import { createTuiResolvedConfig } from "./fixture/tui-runtime"
  7. import { createEventSource, createFetch, directory, json } from "./fixture/tui-sdk"
  8. test("SIGHUP clears title and disposes scoped resources once", async () => {
  9. const setup = await createTestRenderer({ width: 80, height: 24, useThread: false })
  10. const core = await import("@opentui/core")
  11. mock.module("@opentui/core", () => ({ ...core, createCliRenderer: async () => setup.renderer }))
  12. const titles: string[] = []
  13. const setTitle = setup.renderer.setTerminalTitle.bind(setup.renderer)
  14. setup.renderer.setTerminalTitle = (title) => {
  15. titles.push(title)
  16. setTitle(title)
  17. }
  18. const listeners = new Set(process.listeners("SIGHUP"))
  19. const events = createEventSource()
  20. const calls = createFetch()
  21. let started!: () => void
  22. const ready = new Promise<void>((resolve) => {
  23. started = resolve
  24. })
  25. let disposes = 0
  26. try {
  27. const { run } = await import("../src/app")
  28. const task = Effect.runPromise(
  29. run({
  30. url: "http://test",
  31. directory,
  32. config: createTuiResolvedConfig({ plugin_enabled: {} }),
  33. fetch: calls.fetch,
  34. events: events.source,
  35. args: {},
  36. pluginHost: {
  37. async start() {
  38. started()
  39. },
  40. async dispose() {
  41. disposes++
  42. },
  43. },
  44. }).pipe(Effect.provide(Global.defaultLayer)),
  45. )
  46. await ready
  47. process.emit("SIGHUP")
  48. await task
  49. expect(setup.renderer.isDestroyed).toBe(true)
  50. expect(titles.at(-1)).toBe("")
  51. expect(disposes).toBe(1)
  52. expect(process.listeners("SIGHUP").every((listener) => listeners.has(listener))).toBe(true)
  53. } finally {
  54. if (!setup.renderer.isDestroyed) setup.renderer.destroy()
  55. mock.restore()
  56. }
  57. })
  58. test("app.exit prints the session epilogue after scoped cleanup", async () => {
  59. const setup = await createTestRenderer({ width: 80, height: 24, useThread: false })
  60. const core = await import("@opentui/core")
  61. mock.module("@opentui/core", () => ({ ...core, createCliRenderer: async () => setup.renderer }))
  62. const events = createEventSource()
  63. const calls = createFetch((url) => {
  64. if (url.pathname === "/session")
  65. return json([
  66. {
  67. id: "dummy",
  68. title: "Demo session",
  69. slug: "dummy",
  70. projectID: "project",
  71. directory,
  72. version: "0.0.0-test",
  73. time: { created: 0, updated: 0 },
  74. },
  75. ])
  76. })
  77. const originalWrite = process.stdout.write.bind(process.stdout)
  78. let stdout = ""
  79. let api: TuiPluginApi | undefined
  80. let started!: () => void
  81. const ready = new Promise<void>((resolve) => {
  82. started = resolve
  83. })
  84. process.stdout.write = ((chunk: string | Uint8Array) => {
  85. stdout += String(chunk)
  86. return true
  87. }) as typeof process.stdout.write
  88. try {
  89. const { run } = await import("../src/app")
  90. const task = Effect.runPromise(
  91. run({
  92. url: "http://test",
  93. directory,
  94. config: createTuiResolvedConfig({ plugin_enabled: {} }),
  95. fetch: calls.fetch,
  96. events: events.source,
  97. args: { continue: true },
  98. pluginHost: {
  99. async start(input) {
  100. api = input.api
  101. started()
  102. },
  103. async dispose() {},
  104. },
  105. }).pipe(Effect.provide(Global.defaultLayer)),
  106. )
  107. await ready
  108. await setup.renderOnce()
  109. await setup.renderOnce()
  110. api?.keymap.dispatchCommand("app.exit")
  111. await task
  112. expect(stdout).toContain("Demo session")
  113. expect(stdout).toContain("opencode -s dummy")
  114. } finally {
  115. process.stdout.write = originalWrite
  116. if (!setup.renderer.isDestroyed) setup.renderer.destroy()
  117. mock.restore()
  118. }
  119. })