Download packages/app/src/components/prompt-input/submit.test.ts from SaylorTwift/opencode: direct link, hf CLI and curl.
- Browser
- Download file 19.6 kB
-
https://huggingface.co/SaylorTwift/opencode/resolve/main/packages/app/src/components/prompt-input/submit.test.ts
- Command line
-
hf download hf://SaylorTwift/opencode/packages/app/src/components/prompt-input/submit.test.ts
-
curl -L -o submit.test.ts https://huggingface.co/SaylorTwift/opencode/resolve/main/packages/app/src/components/prompt-input/submit.test.ts
19.6 kB
| 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<string, Array<{ id: string; title?: string }>> = {} | |
| 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<void> | undefined | |
| let 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 { | |
| 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<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) | |
| 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]) | |
| }) | |
| }) | |