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 sessionCreateInputs: Array<{ agent?: string model?: { id: string; providerID: string; variant?: string } location?: { directory: 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> = {} const promoted: Array<{ directory: string; sessionID: string }> = [] const sentShell: Array<{ sessionID: string; id?: string; command: string }> = [] const syncedDirectories: string[] = [] const promotedDrafts: Array<{ draftID: string; server: string; sessionId: string }> = [] const sentPrompts: string[] = [] const promptInputs: unknown[] = [] const sentCommands: unknown[] = [] const commands: Array<{ name: string }> = [] let serverSessionSyncs = 0 let params: { id?: string } = {} let search: { draftId?: string } = {} let selected = "/repo/worktree-a" let variant: string | undefined let permissionServer = "server-a" let createSessionGate: Promise | undefined let promptValue: Prompt = [{ type: "text", content: "ls", start: 0, end: 2 }] const [promptStore, setPromptStore] = createStore({ 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 { api: { session: { create: async (input: (typeof sessionCreateInputs)[number]) => { await createSessionGate const location = input.location?.directory ?? directory createdSessions.push(location) sessionCreateInputs.push(input) return { id: `session-${createdSessions.length}`, projectID: "project", agent: input.agent, model: input.model, cost: 0, tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, time: { created: 1, updated: 1 }, title: `New session ${createdSessions.length}`, location: { directory: location }, } }, prompt: async (input: unknown) => { sentPrompts.push(directory) promptInputs.push(input) return { data: undefined } }, command: async (input: unknown) => { sentCommands.push(input) }, shell: async (input: { sessionID: string; id?: string; command: string }) => { sentShell.push(input) }, }, }, session: { 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, api: rootClient.api, url: "http://localhost:4096", createClient(opts: any) { return clientFor(opts.directory) }, } return () => sdk }, })) mock.module("@/context/sync", () => ({ useSync: () => () => ({ data: { command: commands }, 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, sync: async () => { serverSessionSyncs++ }, }, 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 sessionCreateInputs.length = 0 enabledAutoAccept.length = 0 optimistic.length = 0 optimisticSeeded.length = 0 promoted.length = 0 promotedDrafts.length = 0 sentPrompts.length = 0 promptInputs.length = 0 sentCommands.length = 0 commands.length = 0 promptValue = [{ type: "text", content: "ls", start: 0, end: 2 }] params = {} search = {} sentShell.length = 0 syncedDirectories.length = 0 selected = "/repo/worktree-a" variant = undefined permissionServer = "server-a" createSessionGate = undefined serverSessionSyncs = 0 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(sessionCreateInputs).toEqual([ { agent: "agent", model: { id: "model", providerID: "provider", variant: undefined }, location: { directory: "/repo/worktree-a" }, }, { agent: "agent", model: { id: "model", providerID: "provider", variant: undefined }, location: { directory: "/repo/worktree-b" }, }, ]) expect(sentShell).toEqual([ expect.objectContaining({ sessionID: "session-1", id: expect.stringMatching(/^evt_/), command: "ls" }), expect.objectContaining({ sessionID: "session-2", id: expect.stringMatching(/^evt_/), command: "ls" }), ]) expect(syncedDirectories).toEqual(["/repo/worktree-a", "/repo/worktree-a", "/repo/worktree-b", "/repo/worktree-b"]) expect(serverSessionSyncs).toBe(0) 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((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) await Bun.sleep(0) expect(optimistic).toHaveLength(1) expect(optimistic[0]).toMatchObject({ message: { agent: "agent", model: { providerID: "provider", modelID: "model", variant: "high" }, }, }) expect(sentPrompts).toEqual(["/repo/main"]) expect(promptInputs[0]).toMatchObject({ sessionID: "session-1", text: "ls", files: [], agents: [], }) expect((promptInputs[0] as { id?: string }).id).toStartWith("msg_") expect((promptInputs[0] as { legacyParts?: { id: string; type: string; text?: string }[] }).legacyParts).toEqual([ { id: expect.stringMatching(/^prt_/), type: "text", text: "ls" }, ]) }) test("submits slash commands through the current session API", async () => { params = { id: "session-1" } variant = "high" commands.push({ name: "review" }) promptValue = [{ type: "text", content: "/review staged changes", start: 0, end: 22 }] 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, }) await submit.handleSubmit({ preventDefault: () => undefined } as unknown as Event) expect(sentCommands).toEqual([ { sessionID: "session-1", id: expect.stringMatching(/^msg_/), command: "review", arguments: "staged changes", agent: "agent", model: { id: "model", providerID: "provider", variant: "high" }, files: [], }, ]) expect(serverSessionSyncs).toBe(0) }) 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"]).toHaveLength(1) expect(storedSessions["/repo/worktree-a"]?.[0]).toMatchObject({ id: "session-1", title: "New session 1" }) expect(optimisticSeeded).toEqual([true]) }) })