| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155 |
- import { expect, mock, test } from "bun:test"
- import type { TuiPluginApi } from "@opencode-ai/plugin/tui"
- import { createTestRenderer } from "@opentui/core/testing"
- import { Effect } from "effect"
- import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
- import { Global } from "@opencode-ai/core/global"
- import { createTuiResolvedConfig } from "./fixture/tui-runtime"
- import { createEventStream, createFetch, directory, json } from "./fixture/tui-sdk"
- test("SIGHUP clears title and disposes scoped resources once", async () => {
- const setup = await createTestRenderer({ width: 80, height: 24, useThread: false })
- const core = await import("@opentui/core")
- mock.module("@opentui/core", () => ({ ...core, createCliRenderer: async () => setup.renderer }))
- const titles: string[] = []
- const setTitle = setup.renderer.setTerminalTitle.bind(setup.renderer)
- setup.renderer.setTerminalTitle = (title) => {
- titles.push(title)
- setTitle(title)
- }
- const listeners = new Set(process.listeners("SIGHUP"))
- const events = createEventStream()
- const calls = createFetch(undefined, events)
- const server = Bun.serve({ port: 0, fetch: (request) => calls.fetch(request) })
- let started!: () => void
- const ready = new Promise<void>((resolve) => {
- started = resolve
- })
- let disposes = 0
- try {
- const { run } = await import("../src/app")
- const task = Effect.runPromise(
- run({
- server: { endpoint: { url: server.url.toString() } },
- config: createTuiResolvedConfig({ plugin_enabled: {} }),
- args: {},
- log: () => {},
- pluginHost: {
- async start() {
- started()
- },
- async dispose() {
- disposes++
- },
- },
- }).pipe(Effect.provide(AppNodeBuilder.build(Global.node))),
- )
- await ready
- process.emit("SIGHUP")
- await task
- expect(setup.renderer.isDestroyed).toBe(true)
- expect(titles.at(-1)).toBe("")
- expect(disposes).toBe(1)
- expect(process.listeners("SIGHUP").every((listener) => listeners.has(listener))).toBe(true)
- } finally {
- if (!setup.renderer.isDestroyed) setup.renderer.destroy()
- await server.stop()
- mock.restore()
- }
- })
- test("session lifecycle updates the terminal title and prints the epilogue after cleanup", async () => {
- const setup = await createTestRenderer({ width: 80, height: 24, useThread: false })
- const core = await import("@opentui/core")
- mock.module("@opentui/core", () => ({ ...core, createCliRenderer: async () => setup.renderer }))
- let initialTitle!: () => void
- const initialTitleSet = new Promise<void>((resolve) => {
- initialTitle = resolve
- })
- let renamedTitle!: () => void
- const renamedTitleSet = new Promise<void>((resolve) => {
- renamedTitle = resolve
- })
- const setTitle = setup.renderer.setTerminalTitle.bind(setup.renderer)
- setup.renderer.setTerminalTitle = (title) => {
- if (title === "OC | Demo session") initialTitle()
- if (title === "OC | Renamed session") renamedTitle()
- setTitle(title)
- }
- const events = createEventStream()
- const calls = createFetch((url) => {
- const session = {
- id: "dummy",
- title: "Demo session",
- projectID: "project",
- location: { directory },
- cost: 0,
- tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
- time: { created: 0, updated: 0 },
- }
- if (url.pathname === "/api/session")
- return json({
- data: [session],
- cursor: {},
- })
- if (url.pathname === "/api/session/dummy") return json({ data: session })
- if (url.pathname === "/api/session/dummy/message") return json({ data: [], cursor: {} })
- if (url.pathname === "/api/session/dummy/pending") return json({ data: [] })
- if (url.pathname === "/api/session/dummy/permission") return json({ data: [] })
- }, events)
- const server = Bun.serve({ port: 0, fetch: (request) => calls.fetch(request) })
- const originalWrite = process.stdout.write.bind(process.stdout)
- let stdout = ""
- let api: TuiPluginApi | undefined
- let started!: () => void
- const ready = new Promise<void>((resolve) => {
- started = resolve
- })
- process.stdout.write = ((chunk: string | Uint8Array) => {
- stdout += String(chunk)
- return true
- }) as typeof process.stdout.write
- try {
- const { run } = await import("../src/app")
- const task = Effect.runPromise(
- run({
- server: { endpoint: { url: server.url.toString() } },
- config: createTuiResolvedConfig({ plugin_enabled: {} }),
- args: { sessionID: "dummy" },
- log: () => {},
- pluginHost: {
- async start(input) {
- api = input.api
- started()
- },
- async dispose() {},
- },
- }).pipe(Effect.provide(AppNodeBuilder.build(Global.node))),
- )
- await ready
- await initialTitleSet
- events.emit({
- id: "evt_renamed",
- created: 1,
- type: "session.renamed",
- durable: { aggregateID: "dummy", seq: 1, version: 1 },
- data: { sessionID: "dummy", title: "Renamed session" },
- })
- await renamedTitleSet
- api?.keymap.dispatchCommand("app.exit")
- await task
- expect(stdout).toContain("Renamed session")
- expect(stdout).toContain("opencode -s dummy")
- } finally {
- process.stdout.write = originalWrite
- if (!setup.renderer.isDestroyed) setup.renderer.destroy()
- await server.stop()
- mock.restore()
- }
- })
|