Files
opencode/packages/tui/test/session-terminal-availability.test.tsx
2026-10-19 16:47:33 +02:00

81 lines
3.5 KiB
TypeScript

import { expect, test } from "bun:test"
import { createTestRenderer } from "@opentui/core/testing"
import { Effect, FileSystem } from "effect"
import { Global } from "@opencode/util/global"
import { createEventStream, createFetch, directory, json } from "./fixture/tui-client"
import { tmpdir } from "./fixture/fixture"
// Each case uses its own session so a prompt draft left by one case cannot satisfy another.
test.each([
{ id: "ses_terminal_reported", reported: true, offered: true },
{ id: "ses_terminal_unsupported", reported: false, offered: false },
{ id: "ses_terminal_unreported", reported: undefined, offered: true },
])("offers persistent terminals when the server reports $reported", async (input) => {
await using state = await tmpdir()
const setup = await createTestRenderer({ width: 120, height: 36, kittyKeyboard: true })
await setup.renderer.setupTerminal()
setup.renderer.start()
const session = {
id: input.id,
title: "Terminal support fixture",
projectID: "project",
location: { directory },
cost: 0,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
time: { created: 0, updated: 0 },
}
const message = { id: "message-0", type: "user", text: "Terminal support message", time: { created: 0 } }
const requests: string[] = []
const calls = createFetch((url) => {
requests.push(url.pathname)
if (url.pathname === "/api/info")
return json({
version: "test",
pid: 0,
urls: [],
paths: { tmp: "/tmp" },
// Servers that predate capabilities omit the whole object.
capabilities: input.reported === undefined ? undefined : { persistentPty: input.reported },
})
if (url.pathname === "/api/session") return json({ data: [session], cursor: {} })
if (url.pathname === `/api/session/${session.id}`) return json({ data: session })
if (url.pathname === `/api/session/${session.id}/message`) return json({ data: [message], cursor: {} })
if (url.pathname === `/api/session/${session.id}/inbox`) return json({ data: [] })
if (url.pathname === `/api/session/${session.id}/permission`) return json({ data: [] })
return undefined
}, createEventStream())
const server = Bun.serve({ port: 0, fetch: (request) => calls.fetch(request) })
const { run } = await import("../src/app")
const task = Effect.runPromise(
run({
app: { name: "test", version: "test", channel: "test" },
server: { endpoint: { url: server.url.toString() } },
config: {
get: async () => ({ animations: false, tabs: { mode: "off" } }),
update: async () => ({}),
},
packages: { prepare: async () => ({ directory: "" }) },
args: { sessionID: session.id },
terminalHandoff: async () => ({ renderer: setup.renderer, mode: "dark", complete: () => {} }),
log: () => {},
}).pipe(Effect.provide(Global.layerWith({ state: state.path })), Effect.provide(FileSystem.layerNoop({}))),
)
try {
await setup.waitForFrame((frame) => frame.includes("Terminal support message"))
await setup.waitFor(() => requests.includes("/api/info"))
await setup.waitForVisualIdle()
await setup.mockInput.typeText("/terminal")
if (input.offered) {
await setup.waitForFrame((frame) => frame.includes("New terminal"))
return
}
await setup.waitForVisualIdle()
expect(setup.captureCharFrame()).not.toContain("New terminal")
expect(requests).not.toContain(`/api/experimental/session/${session.id}/terminal`)
} finally {
setup.renderer.destroy()
await task
await server.stop()
}
})