Co-authored-by: opencode <opencode@sst.dev> Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com> Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com> Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com> Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Co-authored-by: Victor Navarro <vn4varro@gmail.com> Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Co-authored-by: Frank <frank@anoma.ly> Co-authored-by: Dax Raad <d@ironbay.co> Co-authored-by: Dax <mail@thdxr.com> Co-authored-by: Nabs <nabil@instafork.com> Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com> Co-authored-by: Brendan Allan <git@brendonovich.dev> Co-authored-by: Vladimir Glafirov <vglafirov@gitlab.com> Co-authored-by: AidenGeunGeun <eastlandwyvern@gmail.com> Co-authored-by: Mark <geraint0923@users.noreply.github.com> Co-authored-by: Aiden Cline <aidenpcline@gmail.com> Co-authored-by: David Hill <1879069+iamdavidhill@users.noreply.github.com> Co-authored-by: Jay <air@live.ca> Co-authored-by: Jay <53023+jayair@users.noreply.github.com> Co-authored-by: BB84 <110078428+BB-84C@users.noreply.github.com> Co-authored-by: Dustin Deus <deusdustin@gmail.com> Co-authored-by: Jack <jack@anoma.ly> Co-authored-by: Sebastian <hasta84@gmail.com> Co-authored-by: Jérôme Benoit <jerome.benoit@sap.com> Co-authored-by: Test User <test@test.com> Co-authored-by: Simon Klee <hello@simonklee.dk> Co-authored-by: Rahul A Mistry <149420892+ProdigyRahul@users.noreply.github.com> Co-authored-by: Qiping Li <liqiping1991@gmail.com> Co-authored-by: liqiping <liqiping@msh.team> Co-authored-by: OpeOginni <107570612+OpeOginni@users.noreply.github.com> Co-authored-by: Matthias Reso <13337103+mreso@users.noreply.github.com> Co-authored-by: tobwen <1864057+tobwen@users.noreply.github.com>
493 lines
15 KiB
TypeScript
493 lines
15 KiB
TypeScript
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])
|
|
})
|
|
})
|