mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-13 19:46:16 +03:00
52a10f573f
Give the TUI client timeout: false, share the no-timeout fetch helper across run, mini and the TUI via @opencode/util/no-timeout-fetch, and surface existing-session shell failures with a toast and composer restore. Closes #54974 Co-authored-by: Finn763 <finn763@users.noreply.github.com>
77 lines
3.6 KiB
TypeScript
77 lines
3.6 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"
|
|
|
|
test("a failed shell command in an existing session toasts and restores the composer", async () => {
|
|
await using state = await tmpdir()
|
|
const setup = await createTestRenderer({ width: 100, height: 30, kittyKeyboard: true })
|
|
await setup.renderer.setupTerminal()
|
|
setup.renderer.start()
|
|
const ready = Promise.withResolvers<void>()
|
|
const shell = Promise.withResolvers<string>()
|
|
const events = createEventStream()
|
|
// The draft stash is process-wide, so reruns use a fresh session for an empty composer.
|
|
const sessionID = `ses_shell_failure_${crypto.randomUUID()}`
|
|
const location = { directory, project: { id: "project", directory, canonical: directory } }
|
|
const calls = createFetch(async (url, request) => {
|
|
if (url.pathname === `/api/session/${sessionID}`)
|
|
return json({
|
|
data: {
|
|
id: sessionID,
|
|
projectID: "project",
|
|
title: "Shell fixture",
|
|
agent: "build",
|
|
model: { providerID: "demo", id: "model" },
|
|
location: { directory },
|
|
cost: 0,
|
|
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
|
time: { created: 0, updated: 0 },
|
|
},
|
|
})
|
|
if (url.pathname === `/api/session/${sessionID}/message`) return json({ data: [], cursor: {} })
|
|
if (url.pathname === `/api/session/${sessionID}/inbox` || url.pathname === `/api/session/${sessionID}/permission`)
|
|
return json({ data: [] })
|
|
if (url.pathname === "/api/agent")
|
|
return json({ location, data: [{ id: "build", mode: "primary", hidden: false, permissions: [] }] })
|
|
if (url.pathname === "/api/provider") return json({ location, data: [{ id: "demo", name: "Demo" }] })
|
|
if (url.pathname === "/api/model")
|
|
return json({ location, data: [{ id: "model", providerID: "demo", name: "Demo Model", variants: [] }] })
|
|
if (url.pathname === `/api/session/${sessionID}/shell`) {
|
|
shell.resolve((await request.json()).command)
|
|
return json({ message: "Shell unavailable" }, { status: 400 })
|
|
}
|
|
return undefined
|
|
}, events)
|
|
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 }), update: async () => ({}) },
|
|
packages: { prepare: async () => ({ directory: "" }) },
|
|
terminalHandoff: async () => ({ renderer: setup.renderer, mode: "dark", complete: ready.resolve }),
|
|
args: { sessionID },
|
|
log: () => {},
|
|
}).pipe(Effect.provide(Global.layerWith({ state: state.path })), Effect.provide(FileSystem.layerNoop({}))),
|
|
)
|
|
try {
|
|
await ready.promise
|
|
await setup.waitForFrame((frame) => frame.includes("Demo Model"))
|
|
await setup.mockInput.typeText("!")
|
|
await setup.mockInput.typeText("echo restored")
|
|
setup.mockInput.pressEnter()
|
|
expect(await shell.promise).toBe("echo restored")
|
|
const frame = await setup.waitForFrame((frame) => frame.includes("Shell unavailable"))
|
|
expect(frame).toContain("Failed to run shell command")
|
|
expect(frame).toContain("echo restored")
|
|
} finally {
|
|
setup.renderer.destroy()
|
|
await task
|
|
await server.stop()
|
|
}
|
|
})
|