From e812ca1626ad2a2d52665430b2776a8048f79203 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sun, 31 May 2026 15:09:58 +0000 Subject: [PATCH] test(tui): cover question prompt keybindings --- .../cli/cmd/tui/routes/session/question.tsx | 1 + .../opencode/test/cli/tui/question.test.tsx | 213 ++++++++++++++++++ 2 files changed, 214 insertions(+) create mode 100644 packages/opencode/test/cli/tui/question.test.tsx diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/question.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/question.tsx index ddb3e84a66..a20b58243b 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/question.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/question.tsx @@ -347,6 +347,7 @@ export function QuestionPrompt(props: { request: QuestionRequest }) { }} + let resolveReady!: () => void + const ready = new Promise((resolve) => { + resolveReady = resolve + }) + + const fetch = (async (requestInput: RequestInfo | URL, init?: RequestInit) => { + const request = requestInput instanceof Request ? requestInput : new Request(requestInput, init) + const url = new URL(request.url) + const match = url.pathname.match(/^\/question\/([^/]+)\/reply$/) + + if (match) { + const body = (await request.json()) as { answers: QuestionAnswer[] } + replies.push({ requestID: match[1]!, answers: body.answers }) + return json({}) + } + + if (/^\/question\/[^/]+\/reject$/.test(url.pathname)) { + return json({}) + } + + throw new Error(`unexpected request: ${url.pathname}`) + }) as typeof globalThis.fetch + + function Harness() { + const renderer = useRenderer() + const keymap = createDefaultOpenTuiKeymap(renderer) + const config = createTuiResolvedConfig() + const offKeymap = registerOpencodeKeymap(keymap, renderer, config) + const [request, set] = createSignal(input.request) + setRequest = set + + onCleanup(offKeymap) + + return ( + + + + + + + + + + + + + + ) + } + + const app = await testRender( + () => ( + + + + ), + { width: 100, height: 20, kittyKeyboard: true }, + ) + await ready + + return { + app, + replies, + setRequest(request: QuestionRequest) { + setRequest(request) + }, + cleanup() { + app.renderer.destroy() + Global.Path.config = previous.config + Global.Path.state = previous.state + }, + } +} + +function Ready(props: ParentProps<{ onReady: () => void }>) { + const kv = useKV() + createEffect(() => { + if (kv.ready) props.onReady() + }) + + return <>{props.children} +} + +test("question prompt answers a new request after a stale custom edit", async () => { + await using tmp = await tmpdir() + const prompt = await mountQuestion({ + root: tmp.path, + request: { + id: "question-1", + sessionID: "session-1", + questions: [ + { + header: "First", + question: "First question?", + options: [{ label: "Preset", description: "Use the preset answer." }], + custom: true, + }, + ], + }, + }) + + try { + await prompt.app.renderOnce() + prompt.app.mockInput.pressKey("2") + await prompt.app.renderOnce() + await prompt.app.waitFor(() => prompt.app.renderer.currentFocusedEditor instanceof TextareaRenderable) + + prompt.setRequest({ + id: "question-2", + sessionID: "session-1", + questions: [ + { + header: "Second", + question: "Second question?", + options: [{ label: "Next", description: "Use the next answer." }], + custom: false, + }, + ], + }) + await prompt.app.renderOnce() + + prompt.app.mockInput.pressKey("1") + await prompt.app.renderOnce() + await prompt.app.waitFor(() => prompt.replies.length === 1) + + expect(prompt.replies).toEqual([{ requestID: "question-2", answers: [["Next"]] }]) + } finally { + prompt.cleanup() + } +}) + +test("question prompt confirm keybinding works after leaving a custom edit by mouse", async () => { + await using tmp = await tmpdir() + const prompt = await mountQuestion({ + root: tmp.path, + request: { + id: "question-1", + sessionID: "session-1", + questions: [ + { + header: "First", + question: "First question?", + options: [{ label: "Preset", description: "Use the preset answer." }], + custom: true, + }, + { + header: "Second", + question: "Second question?", + options: [{ label: "Next", description: "Use the next answer." }], + custom: false, + }, + ], + }, + }) + + try { + await prompt.app.renderOnce() + prompt.app.mockInput.pressKey("2") + await prompt.app.renderOnce() + await prompt.app.waitFor(() => prompt.app.renderer.currentFocusedEditor instanceof TextareaRenderable) + + const confirm = prompt.app.renderer.root.findDescendantById("tui-question-tab-confirm") + if (!confirm) throw new Error("expected confirm tab") + + await prompt.app.mockMouse.click(confirm.screenX + 1, confirm.screenY) + await prompt.app.renderOnce() + prompt.app.mockInput.pressEnter() + await prompt.app.renderOnce() + await prompt.app.waitFor(() => prompt.replies.length === 1) + + expect(prompt.replies).toEqual([{ requestID: "question-1", answers: [[], []] }]) + } finally { + prompt.cleanup() + } +})