opencode/packages/tui/test/prompt/clipboard.test.ts
2026-08-03 08:15:58 +02:00

97 lines
3.4 KiB
TypeScript

import { afterAll, expect, mock, test } from "bun:test"
import { TextareaRenderable, type HostClipboardService } from "@opentui/core"
import { createTestRenderer } from "@opentui/core/testing"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { Global } from "@opencode-ai/util/global"
import { Effect, FileSystem } from "effect"
import { createComponent } from "solid-js"
import { Prompt } from "../../src/component/prompt"
import { createEventStream, createFetch } from "../fixture/tui-client"
const openTui = { ...(await import("@opentui/core")) }
let activeSetup: Awaited<ReturnType<typeof createTestRenderer>> | undefined
let reads = 0
await mock.module("@opentui/core", () => ({
...openTui,
createCliRenderer: async () => {
if (!activeSetup) throw new Error("Prompt renderer is not mounted")
return activeSetup.renderer
},
createHostClipboard: () =>
({
maxWriteBytes: 8 * 1024 * 1024,
async read() {
reads++
return { status: "empty" }
},
async writeText() {
return { status: "written" }
},
async clear() {
return { status: "cleared" }
},
async dispose() {},
}) satisfies HostClipboardService,
}))
await mock.module("../../src/routes/home", () => ({
Home: () => createComponent(Prompt, { showPlaceholder: false }),
}))
const { run } = await import("../../src/app")
afterAll(() => mock.restore())
test("only zero-byte terminal pastes read the host clipboard", async () => {
const setup = await createTestRenderer({ width: 80, height: 24, useThread: false })
activeSetup = setup
reads = 0
const mounted = Promise.withResolvers<void>()
const setTitle = setup.renderer.setTerminalTitle.bind(setup.renderer)
setup.renderer.setTerminalTitle = (title) => {
if (title === "OpenCode") mounted.resolve()
setTitle(title)
}
const events = createEventStream()
const calls = createFetch(undefined, events)
const preloaded = Promise.withResolvers<void>()
const server = Bun.serve({
port: 0,
fetch: async (request) => {
const response = await calls.fetch(request)
if (new URL(request.url).pathname === "/api/session/active") preloaded.resolve()
return response
},
})
const task = Effect.runPromise(
run({
app: { name: "test", version: "test", channel: "test" },
server: { endpoint: { url: server.url.toString() } },
config: { get: async () => ({ prompt: { paste: "full" as const } }), update: async () => ({}) },
packages: { resolve: async () => undefined },
args: {},
log: () => {},
}).pipe(Effect.provide(AppNodeBuilder.build(Global.node)), Effect.provide(FileSystem.layerNoop({}))),
)
try {
await mounted.promise
await setup.waitFor(() => setup.renderer.currentFocusedEditor instanceof TextareaRenderable)
await preloaded.promise
await Bun.sleep(0)
const input = setup.renderer.currentFocusedEditor
if (!(input instanceof TextareaRenderable)) throw new Error("Prompt textarea is not focused")
await setup.mockInput.pasteBracketedText(" \t\n")
await setup.waitFor(() => input.plainText === " \t\n")
expect(reads).toBe(0)
setup.renderer.keyInput.processPaste(new Uint8Array())
await setup.waitFor(() => reads === 1)
expect(input.plainText).toBe(" \t\n")
} finally {
setup.renderer.destroy()
await task
await server.stop()
activeSetup = undefined
}
})