fix(tui): prevent duplicate message forks
This commit is contained in:
@@ -16,7 +16,7 @@ export function DialogFork(props: { sessionID: string; messageID?: string; onMov
|
||||
const client = useClient()
|
||||
const route = useRoute()
|
||||
const toast = useToast()
|
||||
const [pending, setPending] = createSignal(false)
|
||||
const [pending, setPending] = createSignal(!!props.messageID)
|
||||
|
||||
const fork = async (messageID?: string) => {
|
||||
setPending(true)
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
/** @jsxImportSource @opentui/solid */
|
||||
import { InputRenderable } from "@opentui/core"
|
||||
import { testRender } from "@opentui/solid"
|
||||
import { expect, test } from "bun:test"
|
||||
import { mkdir } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import { onMount } from "solid-js"
|
||||
import { ConfigProvider } from "../../../src/config"
|
||||
import { ClientProvider } from "../../../src/context/client"
|
||||
import { ClipboardProvider } from "../../../src/context/clipboard"
|
||||
import { DataProvider, useData } from "../../../src/context/data"
|
||||
import { Keymap } from "../../../src/context/keymap"
|
||||
import { RouteProvider } from "../../../src/context/route"
|
||||
import { ThemeProvider } from "../../../src/context/theme"
|
||||
import { DialogProvider, useDialog } from "../../../src/ui/dialog"
|
||||
import { ToastProvider } from "../../../src/ui/toast"
|
||||
import { tmpdir } from "../../fixture/fixture"
|
||||
import { createApi, createEventStream, createFetch, json } from "../../fixture/tui-client"
|
||||
import { TestTuiContexts } from "../../fixture/tui-environment"
|
||||
import { createTuiResolvedConfig } from "../../fixture/tui-runtime"
|
||||
|
||||
test("forks a message exactly once from its action dialog", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const state = path.join(tmp.path, "state")
|
||||
await mkdir(state, { recursive: true })
|
||||
const bodies: unknown[] = []
|
||||
let finishFork = () => {}
|
||||
const forkResponse = new Promise<void>((resolve) => {
|
||||
finishFork = resolve
|
||||
})
|
||||
const events = createEventStream()
|
||||
const transport = createFetch(async (url, request) => {
|
||||
if (url.pathname === "/api/session/dummy/message")
|
||||
return json({ data: [{ id: "message", type: "user", text: "fork me", time: { created: 1 } }], cursor: {} })
|
||||
if (request.method === "POST" && url.pathname === "/api/session/dummy/fork") {
|
||||
bodies.push(await request.json())
|
||||
await forkResponse
|
||||
return json({
|
||||
data: {
|
||||
id: "fork",
|
||||
title: "Forked session",
|
||||
projectID: "project",
|
||||
location: { directory: tmp.path },
|
||||
cost: 0,
|
||||
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
time: { created: 0, updated: 0 },
|
||||
},
|
||||
})
|
||||
}
|
||||
}, events)
|
||||
const config = createTuiResolvedConfig()
|
||||
const { DialogMessage } = await import("../../../src/routes/session/dialog-message")
|
||||
|
||||
function Fixture() {
|
||||
const data = useData()
|
||||
const dialog = useDialog()
|
||||
onMount(() => {
|
||||
void data.session.message.sync("dummy").then(() =>
|
||||
dialog.replace(() => <DialogMessage sessionID="dummy" messageID="message" />),
|
||||
)
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
function Harness() {
|
||||
return (
|
||||
<TestTuiContexts directory={tmp.path} paths={{ home: tmp.path, state, worktree: tmp.path }}>
|
||||
<ClipboardProvider value={{}}>
|
||||
<ConfigProvider config={config}>
|
||||
<Keymap.Provider>
|
||||
<ClientProvider api={createApi(transport.fetch)}>
|
||||
<DataProvider>
|
||||
<RouteProvider initialRoute={{ type: "session", sessionID: "dummy" }}>
|
||||
<ThemeProvider mode="dark" source={{ discover: () => Promise.resolve({}) }}>
|
||||
<ToastProvider>
|
||||
<DialogProvider>
|
||||
<Fixture />
|
||||
</DialogProvider>
|
||||
</ToastProvider>
|
||||
</ThemeProvider>
|
||||
</RouteProvider>
|
||||
</DataProvider>
|
||||
</ClientProvider>
|
||||
</Keymap.Provider>
|
||||
</ConfigProvider>
|
||||
</ClipboardProvider>
|
||||
</TestTuiContexts>
|
||||
)
|
||||
}
|
||||
|
||||
const app = await testRender(() => <Harness />, { width: 80, height: 20, kittyKeyboard: true })
|
||||
try {
|
||||
app.renderer.start()
|
||||
await app.waitForFrame((frame) => frame.includes("Message Actions"))
|
||||
await app.waitFor(() => app.renderer.currentFocusedEditor instanceof InputRenderable)
|
||||
await app.mockInput.typeText("Fork")
|
||||
app.mockInput.pressEnter()
|
||||
await app.waitFor(() => bodies.length > 0)
|
||||
await app.renderOnce()
|
||||
await Promise.resolve()
|
||||
await app.renderOnce()
|
||||
|
||||
expect(bodies).toEqual([{ messageID: "message" }])
|
||||
} finally {
|
||||
finishFork()
|
||||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user