/
Alex_Ural
/
opencode
Обзор
Документация
Войти
/
Alex_Ural
/
opencode
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
dev
packages/app/src/components/prompt-input/submit.test.ts
493 строки
15 KB
Brendan Allan
feat(session-ui): rewrite v2 prompt input (#37102)
17 июл 2026, 13:02
Не верифицирован
17 июл 2026, 13:02
c0a258b
Код
Авторство
О чём код?
import { beforeAll, beforeEach, describe, expect, mock, test } from "bun:test" import { createStore } from "solid-js/store" import type { Prompt, PromptStore } from "@/context/prompt" import type { ModelSelection } from "@/context/local" let createPromptSubmit: typeof import("./submit").createPromptSubmit const createdClients: string[] = [] const createdSessions: string[] = [] const enabledAutoAccept: Array<{ server: string; sessionID: string; directory: string }> = [] const optimistic: Array<{ directory?: string sessionID?: string message: { agent: string model: { providerID: string; modelID: string } variant?: string } }> = [] const optimisticSeeded: boolean[] = [] const storedSessions: Record<string, Array<{ id: string; title?: string }>> = {} const promoted: Array<{ directory: string; sessionID: string }> = [] const sentShell: string[] = [] const syncedDirectories: string[] = [] const promotedDrafts: Array<{ draftID: string; server: string; sessionId: string }> = [] let params: { id?: string } = {} let search: { draftId?: string } = {} let selected = "/repo/worktree-a" let variant: string | undefined let permissionServer = "server-a" let createSessionGate: Promise<void> | undefined const promptValue: Prompt = [{ type: "text", content: "ls", start: 0, end: 2 }] const [promptStore, setPromptStore] = createStore<PromptStore>({ prompt: promptValue, cursor: 0, context: { items: [] }, }) const prompt = { store: [() => promptStore, setPromptStore] as [() => PromptStore, typeof setPromptStore], ready: Object.assign(() => true, { promise: Promise.resolve(true) }), current: () => promptValue, cursor: () => 0, dirty: () => true, model: { current: () => undefined, set: () => undefined, }, reset: () => undefined, set: () => undefined, context: { add: () => undefined, remove: () => undefined, removeComment: () => undefined, updateComment: () => undefined, replaceComments: () => undefined, items: () => [], }, capture: () => prompt, } const clientFor = (directory: string) => { createdClients.push(directory) return { session: { create: async () => { await createSessionGate createdSessions.push(directory) return { data: { id: `session-${createdSessions.length}`, title: `New session ${createdSessions.length}`, }, } }, shell: async () => { sentShell.push(directory) return { data: undefined } }, prompt: async () => ({ data: undefined }), promptAsync: async () => ({ data: undefined }), command: async () => ({ data: undefined }), abort: async () => ({ data: undefined }), }, worktree: { create: async () => ({ data: { directory: `${directory}/new` } }), }, } } beforeAll(async () => { const rootClient = clientFor("/repo/main") mock.module("@solidjs/router", () => ({ useNavigate: () => () => undefined, useParams: () => params, useLocation: () => ({}), useSearchParams: () => [search, () => undefined], })) mock.module("@opencode-ai/sdk/v2/client", () => ({ createOpencodeClient: (input: { directory: string }) => { createdClients.push(input.directory) return clientFor(input.directory) }, })) mock.module("@opencode-ai/ui/toast", () => ({ Toast: { Region: () => null }, showToast: () => 0, })) mock.module("@opencode-ai/core/util/encode", () => ({ base64Encode: (value: string) => value, })) mock.module("@/context/local", () => ({ useLocal: () => ({ model: { current: () => ({ id: "model", provider: { id: "provider" } }), variant: { current: () => variant }, }, agent: { current: () => ({ name: "agent" }), }, session: { promote(directory: string, sessionID: string) { promoted.push({ directory, sessionID }) }, }, }), })) mock.module("@/context/permission", () => { const state = (server: string) => ({ enableAutoAccept(sessionID: string, directory: string) { enabledAutoAccept.push({ server, sessionID, directory }) }, }) return { usePermission: () => ({ currentServerState: () => state(permissionServer) }) } }) mock.module("@/context/server", () => ({ useServer: () => ({ key: "server-key" }), })) mock.module("@/context/tabs", () => ({ useTabs: () => ({ draft: () => ({ server: "project-server" }), promoteDraft: (draftID: string, session: { server: string; sessionId: string }) => { promotedDrafts.push({ draftID, ...session }) }, }), })) mock.module("@/context/prompt", () => ({ usePrompt: () => prompt, })) mock.module("@/context/layout", () => ({ useLayout: () => ({ handoff: { setTabs: () => undefined, }, }), })) mock.module("@/context/sdk", () => ({ useSDK: () => { const sdk = { scope: "local", directory: "/repo/main", client: rootClient, url: "http://localhost:4096", createClient(opts: any) { return clientFor(opts.directory) }, } return () => sdk }, })) mock.module("@/context/sync", () => ({ useSync: () => () => ({ data: { command: [] }, session: { optimistic: { add: (value: { directory?: string sessionID?: string message: { agent: string; model: { providerID: string; modelID: string; variant?: string } } }) => { optimistic.push(value) optimisticSeeded.push( !!value.directory && !!value.sessionID && !!storedSessions[value.directory]?.find((item) => item.id === value.sessionID)?.title, ) }, remove: () => undefined, }, }, set: () => undefined, }), })) mock.module("@/context/server-sync", () => ({ useServerSync: () => () => ({ session: { remember: () => undefined, set: () => undefined, }, child: (directory: string) => { syncedDirectories.push(directory) storedSessions[directory] ??= [] return [ { session: storedSessions[directory] }, (...args: unknown[]) => { if (args[0] !== "session") return const next = args[1] if (typeof next === "function") { storedSessions[directory] = next(storedSessions[directory]) as Array<{ id: string; title?: string }> return } if (Array.isArray(next)) { storedSessions[directory] = next as Array<{ id: string; title?: string }> } }, ] }, }), })) mock.module("@/context/platform", () => ({ usePlatform: () => ({ fetch: fetch, }), })) mock.module("@/context/language", () => ({ useLanguage: () => ({ t: (key: string) => key, }), })) const mod = await import("./submit") createPromptSubmit = mod.createPromptSubmit }) beforeEach(() => { createdClients.length = 0 createdSessions.length = 0 enabledAutoAccept.length = 0 optimistic.length = 0 optimisticSeeded.length = 0 promoted.length = 0 promotedDrafts.length = 0 params = {} search = {} sentShell.length = 0 syncedDirectories.length = 0 selected = "/repo/worktree-a" variant = undefined permissionServer = "server-a" createSessionGate = undefined for (const key of Object.keys(storedSessions)) delete storedSessions[key] }) describe("prompt submit worktree selection", () => { test("reads the latest worktree accessor value per submit", async () => { const submit = createPromptSubmit({ prompt, info: () => undefined, imageAttachments: () => [], commentCount: () => 0, autoAccept: () => false, mode: () => "shell", working: () => false, editor: () => undefined, queueScroll: () => undefined, promptLength: (value) => value.reduce((sum, part) => sum + ("content" in part ? part.content.length : 0), 0), addToHistory: () => undefined, resetHistoryNavigation: () => undefined, setMode: () => undefined, setPopover: () => undefined, newSessionWorktree: () => selected, onNewSessionWorktreeReset: () => undefined, onSubmit: () => undefined, }) const event = { preventDefault: () => undefined } as unknown as Event await submit.handleSubmit(event) selected = "/repo/worktree-b" await submit.handleSubmit(event) expect(createdClients).toEqual(["/repo/worktree-a", "/repo/worktree-b"]) expect(createdSessions).toEqual(["/repo/worktree-a", "/repo/worktree-b"]) expect(sentShell).toEqual(["/repo/worktree-a", "/repo/worktree-b"]) expect(syncedDirectories).toEqual(["/repo/worktree-a", "/repo/worktree-a", "/repo/worktree-b", "/repo/worktree-b"]) expect(promoted).toEqual([ { directory: "/repo/worktree-a", sessionID: "session-1" }, { directory: "/repo/worktree-b", sessionID: "session-2" }, ]) expect(syncedDirectories).toEqual(["/repo/worktree-a", "/repo/worktree-a", "/repo/worktree-b", "/repo/worktree-b"]) }) test("applies auto-accept to newly created sessions", async () => { const submit = createPromptSubmit({ prompt, info: () => undefined, imageAttachments: () => [], commentCount: () => 0, autoAccept: () => true, mode: () => "shell", working: () => false, editor: () => undefined, queueScroll: () => undefined, promptLength: (value) => value.reduce((sum, part) => sum + ("content" in part ? part.content.length : 0), 0), addToHistory: () => undefined, resetHistoryNavigation: () => undefined, setMode: () => undefined, setPopover: () => undefined, newSessionWorktree: () => selected, onNewSessionWorktreeReset: () => undefined, onSubmit: () => undefined, }) const event = { preventDefault: () => undefined } as unknown as Event await submit.handleSubmit(event) expect(enabledAutoAccept).toEqual([{ server: "server-a", sessionID: "session-1", directory: "/repo/worktree-a" }]) }) test("keeps auto-accept bound to the submission server", async () => { let release = () => {} createSessionGate = new Promise<void>((resolve) => { release = resolve }) const submit = createPromptSubmit({ prompt, info: () => undefined, imageAttachments: () => [], commentCount: () => 0, autoAccept: () => true, mode: () => "shell", working: () => false, editor: () => undefined, queueScroll: () => undefined, promptLength: (value) => value.reduce((sum, part) => sum + ("content" in part ? part.content.length : 0), 0), addToHistory: () => undefined, resetHistoryNavigation: () => undefined, setMode: () => undefined, setPopover: () => undefined, newSessionWorktree: () => selected, onNewSessionWorktreeReset: () => undefined, onSubmit: () => undefined, }) const result = submit.handleSubmit({ preventDefault: () => undefined } as unknown as Event) permissionServer = "server-b" release() await result expect(enabledAutoAccept).toEqual([{ server: "server-a", sessionID: "session-1", directory: "/repo/worktree-a" }]) }) test("promotes drafts using the selected project's server", async () => { search = { draftId: "draft-1" } const submit = createPromptSubmit({ prompt, info: () => undefined, imageAttachments: () => [], commentCount: () => 0, autoAccept: () => false, mode: () => "normal", working: () => false, editor: () => undefined, queueScroll: () => undefined, promptLength: (value) => value.reduce((sum, part) => sum + ("content" in part ? part.content.length : 0), 0), addToHistory: () => undefined, resetHistoryNavigation: () => undefined, setMode: () => undefined, setPopover: () => undefined, newSessionWorktree: () => selected, onNewSessionWorktreeReset: () => undefined, onSubmit: () => undefined, }) await submit.handleSubmit({ preventDefault: () => undefined } as unknown as Event) expect(promotedDrafts).toEqual([{ draftID: "draft-1", server: "project-server", sessionId: "session-1" }]) }) test("includes the selected variant on optimistic prompts", async () => { params = { id: "session-1" } variant = "high" const submit = createPromptSubmit({ prompt, info: () => ({ id: "session-1" }), imageAttachments: () => [], commentCount: () => 0, autoAccept: () => false, mode: () => "normal", working: () => false, editor: () => undefined, queueScroll: () => undefined, promptLength: (value) => value.reduce((sum, part) => sum + ("content" in part ? part.content.length : 0), 0), addToHistory: () => undefined, resetHistoryNavigation: () => undefined, setMode: () => undefined, setPopover: () => undefined, onSubmit: () => undefined, }) const event = { preventDefault: () => undefined } as unknown as Event await submit.handleSubmit(event) expect(optimistic).toHaveLength(1) expect(optimistic[0]).toMatchObject({ message: { agent: "agent", model: { providerID: "provider", modelID: "model", variant: "high" }, }, }) }) test("uses an injected model selection", async () => { params = { id: "session-1" } const model = { current: () => ({ id: "draft-model", provider: { id: "draft-provider" } }), variant: { current: () => "draft-variant" }, } as unknown as ModelSelection const submit = createPromptSubmit({ prompt, info: () => ({ id: "session-1" }), imageAttachments: () => [], commentCount: () => 0, autoAccept: () => false, mode: () => "normal", working: () => false, editor: () => undefined, queueScroll: () => undefined, promptLength: (value) => value.reduce((sum, part) => sum + ("content" in part ? part.content.length : 0), 0), addToHistory: () => undefined, resetHistoryNavigation: () => undefined, setMode: () => undefined, setPopover: () => undefined, model, }) await submit.handleSubmit({ preventDefault: () => undefined } as unknown as Event) expect(optimistic[0]).toMatchObject({ message: { model: { providerID: "draft-provider", modelID: "draft-model", variant: "draft-variant" }, }, }) }) test("seeds new sessions before optimistic prompts are added", async () => { const submit = createPromptSubmit({ prompt, info: () => undefined, imageAttachments: () => [], commentCount: () => 0, autoAccept: () => false, mode: () => "normal", working: () => false, editor: () => undefined, queueScroll: () => undefined, promptLength: (value) => value.reduce((sum, part) => sum + ("content" in part ? part.content.length : 0), 0), addToHistory: () => undefined, resetHistoryNavigation: () => undefined, setMode: () => undefined, setPopover: () => undefined, newSessionWorktree: () => selected, onNewSessionWorktreeReset: () => undefined, onSubmit: () => undefined, }) const event = { preventDefault: () => undefined } as unknown as Event await submit.handleSubmit(event) expect(storedSessions["/repo/worktree-a"]).toEqual([{ id: "session-1", title: "New session 1" }]) expect(optimisticSeeded).toEqual([true]) }) })