refactor(tui): extract standalone package (#31193)

This commit is contained in:
Dax 2026-06-07 01:24:27 -04:00 committed by GitHub
commit 106f8e94d6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
84 changed files with 1147 additions and 1918 deletions

View file

@ -0,0 +1,59 @@
import { expect, mock, test } from "bun:test"
import { createTestRenderer } from "@opentui/core/testing"
import { Effect } from "effect"
import { Global } from "@opencode-ai/core/global"
import { createTuiResolvedConfig } from "./fixture/tui-runtime"
import { createEventSource, createFetch, directory } 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 = createEventSource()
const calls = createFetch()
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({
url: "http://test",
directory,
config: createTuiResolvedConfig({ plugin_enabled: {} }),
fetch: calls.fetch,
events: events.source,
args: {},
pluginHost: {
async start() {
started()
},
async dispose() {
disposes++
},
},
}).pipe(Effect.provide(Global.defaultLayer)),
)
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()
}
})

View file

@ -2,13 +2,12 @@
import { testRender } from "@opentui/solid"
import { onMount } from "solid-js"
import { ArgsProvider } from "../../../../src/context/args"
import { createExit, ExitProvider } from "../../../../src/context/exit"
import { KVProvider, useKV } from "../../../../src/context/kv"
import { ProjectProvider, useProject } from "../../../../src/context/project"
import { SDKProvider } from "../../../../src/context/sdk"
import { SyncProvider, useSync } from "../../../../src/context/sync"
import { createEventSource, createFetch, type FetchHandler, directory } from "../../../fixture/tui-sdk"
import { TestTuiEnvironmentProvider } from "../../../fixture/tui-environment"
import { TestTuiContexts } from "../../../fixture/tui-environment"
export { createEventSource, createFetch, directory, eventSource, json, worktree } from "../../../fixture/tui-sdk"
export async function wait(fn: () => boolean, timeout = 2000) {
@ -44,32 +43,22 @@ export async function mount(override?: FetchHandler, state?: string) {
}
const app = await testRender(() => (
<TestTuiEnvironmentProvider paths={state ? { state } : undefined}>
<TestTuiContexts paths={state ? { state } : undefined}>
<ArgsProvider>
<ExitProvider exit={createExit(async () => {})}>
<KVProvider>
<SDKProvider url="http://test" directory={directory} fetch={calls.fetch} events={events.source}>
<ProjectProvider>
<SyncFixtureProvider>
<SyncProvider>
<Probe />
</SyncFixtureProvider>
</SyncProvider>
</ProjectProvider>
</SDKProvider>
</KVProvider>
</ExitProvider>
</ArgsProvider>
</TestTuiEnvironmentProvider>
</TestTuiContexts>
))
await ready
await wait(() => sync.status === "complete")
return { app, emit: events.emit, kv, project, sync, session: calls.session }
}
function SyncFixtureProvider(props: { children: import("solid-js").JSX.Element }) {
return (
<SyncProvider kv={useKV()} logger={{ error() {} }}>
{props.children}
</SyncProvider>
)
}

View file

@ -9,7 +9,7 @@ import { onCleanup } from "solid-js"
import { tmpdir } from "../../fixture/fixture"
import { createTuiResolvedConfig } from "../../fixture/tui-runtime"
import type { TuiKeybind } from "../../../src/config/keybind"
import { TestTuiEnvironmentProvider } from "../../fixture/tui-environment"
import { TestTuiContexts } from "../../fixture/tui-environment"
async function wait(fn: () => boolean, timeout = 2000) {
const start = Date.now()
@ -57,7 +57,7 @@ async function mountPrompt(input: {
onCleanup(off)
return (
<TestTuiEnvironmentProvider
<TestTuiContexts
directory={input.root}
paths={{
home: input.root,
@ -78,7 +78,7 @@ async function mountPrompt(input: {
</KVProvider>
</TuiConfigProvider>
</OpencodeKeymapProvider>
</TestTuiEnvironmentProvider>
</TestTuiContexts>
)
}

View file

@ -8,7 +8,7 @@ import { KVProvider } from "../../../src/context/kv"
import { ThemeProvider } from "../../../src/context/theme"
import { TuiConfigProvider } from "../../../src/config"
import { DiffViewerFileTree } from "../../../src/feature-plugins/system/diff-viewer-file-tree"
import { TestTuiEnvironmentProvider } from "../../fixture/tui-environment"
import { TestTuiContexts } from "../../fixture/tui-environment"
import {
allExpandedFileTreeDirectories,
buildFileTree,
@ -180,13 +180,13 @@ async function captureSettledFrame(app: Awaited<ReturnType<typeof testRender>>)
function withTheme(component: () => JSX.Element) {
return (
<TestTuiEnvironmentProvider>
<TestTuiContexts>
<TuiConfigProvider config={createTuiResolvedConfig()}>
<KVProvider>
<ThemeProvider mode="dark">{component()}</ThemeProvider>
</KVProvider>
</TuiConfigProvider>
</TestTuiEnvironmentProvider>
</TestTuiContexts>
)
}

View file

@ -13,7 +13,7 @@ import { OpencodeKeymapProvider } from "../../../src/keymap"
import diffViewerPlugin from "../../../src/feature-plugins/system/diff-viewer"
import { createTuiPluginApi } from "../../fixture/tui-plugin"
import { createTuiResolvedConfig } from "../../fixture/tui-runtime"
import { TestTuiEnvironmentProvider } from "../../fixture/tui-environment"
import { TestTuiContexts } from "../../fixture/tui-environment"
test("closing the diff viewer returns to the route it opened from", async () => {
const viewer = await renderDiffViewer([])
@ -152,7 +152,7 @@ async function renderDiffViewer(vcsDiff: unknown[], height = 20) {
commands.get("diff.open")?.run?.({} as never)
return (
<TestTuiEnvironmentProvider>
<TestTuiContexts>
<OpencodeKeymapProvider keymap={keymap}>
<TuiConfigProvider config={config}>
<KVProvider>
@ -162,7 +162,7 @@ async function renderDiffViewer(vcsDiff: unknown[], height = 20) {
</KVProvider>
</TuiConfigProvider>
</OpencodeKeymapProvider>
</TestTuiEnvironmentProvider>
</TestTuiContexts>
)
}

View file

@ -7,7 +7,7 @@ import { ProjectProvider } from "../../../src/context/project"
import { SDKProvider } from "../../../src/context/sdk"
import { SyncProviderV2, useSyncV2 } from "../../../src/context/sync-v2"
import { createEventSource, createFetch, directory, json } from "../../fixture/tui-sdk"
import { TestTuiEnvironmentProvider } from "../../fixture/tui-environment"
import { TestTuiContexts } from "../../fixture/tui-environment"
async function wait(fn: () => boolean, timeout = 2000) {
const start = Date.now()
@ -43,7 +43,7 @@ test("sync v2 settles pending tools when a live failure arrives", async () => {
}
const app = await testRender(() => (
<TestTuiEnvironmentProvider>
<TestTuiContexts>
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
<ProjectProvider>
<SyncProviderV2>
@ -51,7 +51,7 @@ test("sync v2 settles pending tools when a live failure arrives", async () => {
</SyncProviderV2>
</ProjectProvider>
</SDKProvider>
</TestTuiEnvironmentProvider>
</TestTuiContexts>
))
try {
@ -172,7 +172,7 @@ test("sync v2 renders admitted prompts only after promotion", async () => {
}
const app = await testRender(() => (
<TestTuiEnvironmentProvider>
<TestTuiContexts>
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
<ProjectProvider>
<SyncProviderV2>
@ -180,7 +180,7 @@ test("sync v2 renders admitted prompts only after promotion", async () => {
</SyncProviderV2>
</ProjectProvider>
</SDKProvider>
</TestTuiEnvironmentProvider>
</TestTuiContexts>
))
try {
@ -236,7 +236,7 @@ test("sync v2 renders a promoted prompt when admission was missed", async () =>
}
const app = await testRender(() => (
<TestTuiEnvironmentProvider>
<TestTuiContexts>
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
<ProjectProvider>
<SyncProviderV2>
@ -244,7 +244,7 @@ test("sync v2 renders a promoted prompt when admission was missed", async () =>
</SyncProviderV2>
</ProjectProvider>
</SDKProvider>
</TestTuiEnvironmentProvider>
</TestTuiContexts>
))
try {
@ -284,7 +284,7 @@ test("sync v2 projects live context updates with their message ID", async () =>
}
const app = await testRender(() => (
<TestTuiEnvironmentProvider>
<TestTuiContexts>
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
<ProjectProvider>
<SyncProviderV2>
@ -292,7 +292,7 @@ test("sync v2 projects live context updates with their message ID", async () =>
</SyncProviderV2>
</ProjectProvider>
</SDKProvider>
</TestTuiEnvironmentProvider>
</TestTuiContexts>
))
try {
@ -339,7 +339,7 @@ test("sync v2 preserves live events while snapshot hydration is in flight", asyn
}
const app = await testRender(() => (
<TestTuiEnvironmentProvider>
<TestTuiContexts>
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
<ProjectProvider>
<SyncProviderV2>
@ -347,7 +347,7 @@ test("sync v2 preserves live events while snapshot hydration is in flight", asyn
</SyncProviderV2>
</ProjectProvider>
</SDKProvider>
</TestTuiEnvironmentProvider>
</TestTuiContexts>
))
try {
@ -389,7 +389,7 @@ test("sync v2 replaces stale cached rows while preserving in-flight live rows",
}
const app = await testRender(() => (
<TestTuiEnvironmentProvider>
<TestTuiContexts>
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
<ProjectProvider>
<SyncProviderV2>
@ -397,7 +397,7 @@ test("sync v2 replaces stale cached rows while preserving in-flight live rows",
</SyncProviderV2>
</ProjectProvider>
</SDKProvider>
</TestTuiEnvironmentProvider>
</TestTuiContexts>
))
try {
@ -458,7 +458,7 @@ test("sync v2 preserves snapshot order and metadata for in-flight updates", asyn
}
const app = await testRender(() => (
<TestTuiEnvironmentProvider>
<TestTuiContexts>
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
<ProjectProvider>
<SyncProviderV2>
@ -466,7 +466,7 @@ test("sync v2 preserves snapshot order and metadata for in-flight updates", asyn
</SyncProviderV2>
</ProjectProvider>
</SDKProvider>
</TestTuiEnvironmentProvider>
</TestTuiContexts>
))
try {

View file

@ -7,7 +7,7 @@ import { ProjectProvider, useProject } from "../../../src/context/project"
import { SDKProvider } from "../../../src/context/sdk"
import { useEvent } from "../../../src/context/event"
import { createEventSource, createFetch, directory } from "../../fixture/tui-sdk"
import { TestTuiEnvironmentProvider } from "../../fixture/tui-environment"
import { TestTuiContexts } from "../../fixture/tui-environment"
const projectID = "proj_test"
@ -60,7 +60,7 @@ async function mount() {
})
const app = await testRender(() => (
<TestTuiEnvironmentProvider>
<TestTuiContexts>
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
<ProjectProvider>
<Probe
@ -74,7 +74,7 @@ async function mount() {
/>
</ProjectProvider>
</SDKProvider>
</TestTuiEnvironmentProvider>
</TestTuiContexts>
))
await ready

View file

@ -0,0 +1,19 @@
import { expect, test } from "bun:test"
import { copyCommand } from "../src/clipboard"
test("prefers Wayland clipboard when available", () => {
expect(copyCommand("linux", true, (name) => name === "wl-copy")).toEqual(["wl-copy"])
})
test("uses osascript on macOS", () => {
expect(copyCommand("darwin", false, (name) => name === "osascript")).toEqual(["osascript"])
})
test("falls back through X11 clipboard commands", () => {
expect(copyCommand("linux", true, (name) => name === "xclip")).toEqual(["xclip", "-selection", "clipboard"])
expect(copyCommand("linux", false, (name) => name === "xsel")).toEqual(["xsel", "--clipboard", "--input"])
})
test("returns undefined when native clipboard is unavailable", () => {
expect(copyCommand("linux", false, () => false)).toBeUndefined()
})

View file

@ -0,0 +1,23 @@
import { afterEach, expect, test } from "bun:test"
import { openEditor } from "../src/editor"
const editor = process.env.EDITOR
const visual = process.env.VISUAL
afterEach(() => {
process.env.EDITOR = editor
process.env.VISUAL = visual
})
test("rejects when the external editor cannot start", async () => {
delete process.env.VISUAL
process.env.EDITOR = "opencode-editor-that-does-not-exist"
const renderer = {
suspend() {},
resume() {},
requestRender() {},
currentRenderBuffer: { clear() {} },
}
await expect(openEditor({ value: "original", renderer: renderer as never })).rejects.toThrow()
})

View file

@ -1,42 +1,32 @@
/** @jsxImportSource @opentui/solid */
import { createTuiEnvironment, TuiEnvironmentProvider, type TuiEnvironment } from "../../src/runtime"
import {
TuiPathsProvider,
TuiStartupProvider,
TuiTerminalEnvironmentProvider,
type TuiPaths,
} from "../../src/context/runtime"
import type { ParentProps } from "solid-js"
export function TestTuiEnvironmentProvider(
export function TestTuiContexts(
props: ParentProps<{
cwd?: string
directory?: string
paths?: Partial<TuiEnvironment["paths"]>
capabilities?: Partial<TuiEnvironment["capabilities"]>
editor?: Partial<TuiEnvironment["editor"]>
paths?: Partial<TuiPaths>
}>,
) {
return (
<TuiEnvironmentProvider
value={createTuiEnvironment({
<TuiPathsProvider
value={{
cwd: props.cwd ?? props.directory ?? "/tmp/opencode/packages/tui",
platform: "linux",
paths: {
home: "/tmp/opencode/home",
state: "/tmp/opencode/state",
worktree: "/tmp/opencode",
...props.paths,
},
capabilities: {
mouse: true,
copyOnSelect: true,
terminalTitle: false,
terminalSuspend: false,
workspaces: false,
showTimeToFirstDraw: false,
...props.capabilities,
},
terminal: {},
editor: { zedTerminal: false, ...props.editor },
skipInitialLoading: false,
})}
home: "/tmp/opencode/home",
state: "/tmp/opencode/state",
worktree: "/tmp/opencode",
...props.paths,
}}
>
{props.children}
</TuiEnvironmentProvider>
<TuiTerminalEnvironmentProvider value={{ platform: "linux" }}>
<TuiStartupProvider value={{ skipInitialLoading: false }}>{props.children}</TuiStartupProvider>
</TuiTerminalEnvironmentProvider>
</TuiPathsProvider>
)
}

View file

@ -1,8 +1,6 @@
import { expect, test } from "bun:test"
import { createRenderer, createTuiRenderer, mount, run, tui } from "../src"
import { run } from "../src"
test("exports the canonical application lifecycle", () => {
expect(run).toBe(tui)
expect(createRenderer).toBe(createTuiRenderer)
expect(typeof mount).toBe("function")
expect(typeof run).toBe("function")
})

View file

@ -1,40 +0,0 @@
import { expect, test } from "bun:test"
import { testRender } from "@opentui/solid"
import { TuiPlatformProvider, useTuiPlatform, type TuiPlatform } from "../src/platform"
test("provides host platform operations", async () => {
const platform: TuiPlatform = {
files: {
readText: async (path) => `text:${path}`,
readBytes: async () => new Uint8Array([1, 2, 3]),
mime: async () => "text/plain",
},
}
function Consumer() {
const value = useTuiPlatform()
return <text>{value.clipboard ? "clipboard" : "files-only"}</text>
}
const app = await testRender(
() => (
<TuiPlatformProvider value={platform}>
<Consumer />
</TuiPlatformProvider>
),
{ width: 20, height: 3 },
)
try {
await app.renderOnce()
expect(app.captureCharFrame()).toContain("files-only")
expect(await platform.files.readText("file.txt")).toBe("text:file.txt")
expect(await platform.files.readBytes("file.bin")).toEqual(new Uint8Array([1, 2, 3]))
} finally {
app.renderer.destroy()
}
})
test("requires a platform provider", () => {
expect(() => useTuiPlatform()).toThrow("TuiPlatformProvider is missing")
})

View file

@ -1,8 +1,8 @@
import { describe, expect, test } from "bun:test"
import { readLocalAttachment } from "../../src/component/prompt/local-attachment"
import type { PlatformFiles } from "../../src/platform"
import { readLocalAttachmentWith } from "../../src/component/prompt/local-attachment"
import type { LocalFiles } from "../../src/component/prompt/local-attachment"
function files(input: { mime: string; text?: string; bytes?: Uint8Array }): PlatformFiles {
function files(input: { mime: string; text?: string; bytes?: Uint8Array }): LocalFiles {
return {
mime: async () => input.mime,
readText: async () => input.text ?? "",
@ -12,7 +12,7 @@ function files(input: { mime: string; text?: string; bytes?: Uint8Array }): Plat
describe("prompt local attachments", () => {
test("reads SVG attachments as text", async () => {
expect(await readLocalAttachment(files({ mime: "image/svg+xml", text: "<svg />" }), "/tmp/image.svg")).toEqual({
expect(await readLocalAttachmentWith(files({ mime: "image/svg+xml", text: "<svg />" }), "/tmp/image.svg")).toEqual({
type: "text",
mime: "image/svg+xml",
content: "<svg />",
@ -21,7 +21,7 @@ describe("prompt local attachments", () => {
test("reads image and PDF attachments as bytes", async () => {
const content = new Uint8Array([1, 2, 3])
expect(await readLocalAttachment(files({ mime: "application/pdf", bytes: content }), "/tmp/file.pdf")).toEqual({
expect(await readLocalAttachmentWith(files({ mime: "application/pdf", bytes: content }), "/tmp/file.pdf")).toEqual({
type: "binary",
mime: "application/pdf",
content,
@ -29,9 +29,9 @@ describe("prompt local attachments", () => {
})
test("ignores unsupported and unreadable local files", async () => {
expect(await readLocalAttachment(files({ mime: "text/plain" }), "/tmp/file.txt")).toBeUndefined()
expect(await readLocalAttachmentWith(files({ mime: "text/plain" }), "/tmp/file.txt")).toBeUndefined()
expect(
await readLocalAttachment(
await readLocalAttachmentWith(
{
...files({ mime: "image/png" }),
readBytes: async () => Promise.reject(new Error("missing")),

View file

@ -1,14 +1,10 @@
import { expect, test } from "bun:test"
import { testRender } from "@opentui/solid"
import { abbreviateHome } from "../src/runtime"
import {
abbreviateHome,
createTuiBuildInfo,
createTuiEnvironment,
TuiBuildInfoProvider,
TuiEnvironmentProvider,
useTuiBuildInfo,
useTuiEnvironment,
} from "../src/runtime"
TuiPathsProvider,
useTuiPaths,
} from "../src/context/runtime"
test("abbreviates paths within home boundaries", () => {
expect(abbreviateHome("/home/test", "/home/test")).toBe("~")
@ -17,48 +13,27 @@ test("abbreviates paths within home boundaries", () => {
expect(abbreviateHome("/tmp/project", "/home/test")).toBe("/tmp/project")
})
test("provides immutable runtime inputs", async () => {
const environment = createTuiEnvironment({
cwd: "/work",
platform: "linux",
paths: { home: "/home/test", state: "/state", worktree: "/data/worktree" },
capabilities: {
mouse: true,
copyOnSelect: true,
terminalTitle: true,
terminalSuspend: true,
workspaces: false,
showTimeToFirstDraw: false,
},
terminal: { multiplexer: "tmux", displayServer: "wayland" },
editor: { command: "vim", port: 4242, zedTerminal: false },
skipInitialLoading: false,
})
const build = createTuiBuildInfo({ version: "1.2.3", channel: "beta" })
test("provides focused immutable runtime inputs", async () => {
let paths: ReturnType<typeof useTuiPaths>
function Runtime() {
const runtime = useTuiEnvironment()
const info = useTuiBuildInfo()
return <text>{`${runtime.cwd} ${runtime.editor.command} ${info.version}`}</text>
paths = useTuiPaths()
return <text>{paths.cwd}</text>
}
const app = await testRender(
() => (
<TuiEnvironmentProvider value={environment}>
<TuiBuildInfoProvider value={build}>
<Runtime />
</TuiBuildInfoProvider>
</TuiEnvironmentProvider>
<TuiPathsProvider value={{ cwd: "/work", home: "/home/test", state: "/state", worktree: "/worktree" }}>
<Runtime />
</TuiPathsProvider>
),
{ width: 40, height: 3 },
)
try {
await app.renderOnce()
expect(app.captureCharFrame()).toContain("/work vim 1.2.3")
expect(Object.isFrozen(environment)).toBe(true)
expect(Object.isFrozen(environment.paths)).toBe(true)
expect(Object.isFrozen(build)).toBe(true)
expect(app.captureCharFrame()).toContain("/work")
expect(Object.isFrozen(paths!)).toBe(true)
} finally {
app.renderer.destroy()
}

View file

@ -1,6 +1,10 @@
import { expect, test } from "bun:test"
import { mkdir, writeFile } from "node:fs/promises"
import path from "node:path"
import type { TerminalColors } from "@opentui/core"
import { DEFAULT_THEMES, addTheme, allThemes, hasTheme, resolveTheme, terminalMode } from "../src/theme"
import { discoverThemes } from "../src/context/theme"
import { tmpdir } from "./fixture/fixture"
test("addTheme writes into module theme store", () => {
const name = `plugin-theme-${Date.now()}`
@ -63,3 +67,15 @@ test("terminalMode derives mode from refreshed background", () => {
test("terminalMode does not derive mode from ANSI slot zero", () => {
expect(terminalMode(terminalColors(null, ["#000000"]))).toBeUndefined()
})
test("custom theme precedence follows directory order", async () => {
await using tmp = await tmpdir()
const global = path.join(tmp.path, "global")
const project = path.join(tmp.path, "project")
await mkdir(path.join(global, "themes"), { recursive: true })
await mkdir(path.join(project, "themes"), { recursive: true })
await writeFile(path.join(global, "themes", "custom.json"), JSON.stringify({ source: "global" }))
await writeFile(path.join(project, "themes", "custom.json"), JSON.stringify({ source: "project" }))
await expect(discoverThemes([global, project])).resolves.toEqual({ custom: { source: "project" } })
})

View file

@ -1,11 +1,8 @@
import { describe, expect, test } from "bun:test"
import { sessionExitSummary } from "../../src/util/presentation"
import { expect, test } from "bun:test"
import { sessionEpilogue } from "../../src/util/presentation"
describe("util.presentation", () => {
test("formats the ANSI session exit summary", () => {
const summary = sessionExitSummary({ title: "A session", sessionID: "ses_123" })
expect(summary.split("\n")).toHaveLength(8)
expect(summary).toContain("\x1b[90mSession \x1b[0m\x1b[1mA session\x1b[0m")
expect(summary).toContain("\x1b[1mopencode -s ses_123\x1b[0m")
})
test("formats session continuation summary", () => {
const epilogue = sessionEpilogue({ title: "A session", sessionID: "ses_123" })
expect(epilogue).toContain("A session")
expect(epilogue).toContain("opencode -s ses_123")
})

View file

@ -0,0 +1,30 @@
import { expect, test } from "bun:test"
import { destroyRenderer } from "../../src/util/renderer"
test("clears the terminal title before destroying the renderer", () => {
const calls: string[] = []
destroyRenderer({
isDestroyed: false,
setTerminalTitle(title) {
calls.push(`title:${title}`)
},
destroy() {
calls.push("destroy")
},
})
expect(calls).toEqual(["title:", "destroy"])
})
test("still clears the title after renderer destruction", () => {
const calls: string[] = []
destroyRenderer({
isDestroyed: true,
setTerminalTitle(title) {
calls.push(`title:${title}`)
},
destroy() {
calls.push("destroy")
},
})
expect(calls).toEqual(["title:"])
})