opencode/packages/tui/test/app-lifecycle.test.tsx
James Long 8c94e9005f
chore: merge dev into v2 (#34788)
Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com>
Co-authored-by: Kit Langton <kit.langton@gmail.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: Affan Ali <93028901+affanali2k3@users.noreply.github.com>
Co-authored-by: affanali2k3 <affanalikhanxx@gmail.com>
Co-authored-by: Frank <frank@anoma.ly>
Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: 𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴 <little-frank@opencord.local>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: Jay V <air@live.ca>
Co-authored-by: Dax Raad <d@ironbay.co>
Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com>
Co-authored-by: OpeOginni <107570612+OpeOginni@users.noreply.github.com>
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
Co-authored-by: Ben Guthrie <benjee.012@gmail.com>
Co-authored-by: Dax <mail@thdxr.com>
Co-authored-by: Filip <34747899+neriousy@users.noreply.github.com>
Co-authored-by: Max Anderson <max.a.anderson95@gmail.com>
Co-authored-by: Brendan Allan <git@brendonovich.dev>
Co-authored-by: Jack <jack@anoma.ly>
Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com>
Co-authored-by: Dustin Deus <deusdustin@gmail.com>
Co-authored-by: starptech <starptech@starptechs-MBP.fritz.box>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com>
Co-authored-by: Jay <53023+jayair@users.noreply.github.com>
Co-authored-by: runvip <164729189+runvip@users.noreply.github.com>
Co-authored-by: opencode <opencode@sst.dev>
Co-authored-by: Julian Coy <julian@ex-machina.co>
Co-authored-by: Vladimir Glafirov <vglafirov@gitlab.com>
2026-07-01 17:12:00 -04:00

127 lines
4.1 KiB
TypeScript

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 { createApi, createClient, 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)
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({
client: createClient(calls.fetch),
api: createApi(calls.fetch),
config: createTuiResolvedConfig({ plugin_enabled: {} }),
args: {},
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()
mock.restore()
}
})
test("app.exit prints the session epilogue after scoped 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 }))
const events = createEventStream()
const calls = createFetch((url) => {
if (url.pathname === "/api/session")
return json({
data: [
{
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 },
},
],
cursor: {},
})
}, events)
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({
client: createClient(calls.fetch),
api: createApi(calls.fetch),
config: createTuiResolvedConfig({ plugin_enabled: {} }),
args: { continue: true },
pluginHost: {
async start(input) {
api = input.api
started()
},
async dispose() {},
},
}).pipe(Effect.provide(AppNodeBuilder.build(Global.node))),
)
await ready
await setup.renderOnce()
await setup.renderOnce()
api?.keymap.dispatchCommand("app.exit")
await task
expect(stdout).toContain("Demo session")
expect(stdout).toContain("opencode -s dummy")
} finally {
process.stdout.write = originalWrite
if (!setup.renderer.isDestroyed) setup.renderer.destroy()
mock.restore()
}
})