@@ -11,6 +11,7 @@ const mocked = vi.hoisted(() => ({
1111 } as { id : string ; title : string ; directory : string } | null ,
1212 sessionStatusMock : vi . fn ( ) ,
1313 sessionPromptMock : vi . fn ( ) ,
14+ sessionPromptAsyncMock : vi . fn ( ) ,
1415 sessionCreateMock : vi . fn ( ) ,
1516 suppressionRegisterMock : vi . fn ( ) ,
1617 safeBackgroundTaskMock : vi . fn ( ) ,
@@ -24,6 +25,7 @@ vi.mock("../../../src/opencode/client.js", () => ({
2425 session : {
2526 status : mocked . sessionStatusMock ,
2627 prompt : mocked . sessionPromptMock ,
28+ promptAsync : mocked . sessionPromptAsyncMock ,
2729 create : mocked . sessionCreateMock ,
2830 } ,
2931 } ,
@@ -144,11 +146,25 @@ function createContext(): Context {
144146
145147function createDeps ( ) : ProcessPromptDeps {
146148 return {
147- bot : { api : { sendMessage : vi . fn ( ) } } as unknown as Bot < Context > ,
149+ bot : { api : { sendMessage : vi . fn ( ) . mockResolvedValue ( undefined ) } } as unknown as Bot < Context > ,
148150 ensureEventSubscription : vi . fn ( ) . mockResolvedValue ( undefined ) ,
149151 } ;
150152}
151153
154+ function getScheduledBackgroundTask ( ) : {
155+ task : ( ) => Promise < unknown > ;
156+ onSuccess ?: ( value : { error : unknown | null } ) => void ;
157+ onError ?: ( error : unknown ) => void ;
158+ } {
159+ const [ [ options ] ] = mocked . safeBackgroundTaskMock . mock . calls as [ [ {
160+ task : ( ) => Promise < unknown > ;
161+ onSuccess ?: ( value : { error : unknown | null } ) => void ;
162+ onError ?: ( error : unknown ) => void ;
163+ } ] ] ;
164+
165+ return options ;
166+ }
167+
152168describe ( "bot/handlers/prompt" , ( ) => {
153169 beforeEach ( ( ) => {
154170 mocked . currentProject = { id : "project-1" , worktree : "D:\\Projects\\Repo" } ;
@@ -159,6 +175,7 @@ describe("bot/handlers/prompt", () => {
159175 } ;
160176 mocked . sessionStatusMock . mockReset ( ) ;
161177 mocked . sessionPromptMock . mockReset ( ) ;
178+ mocked . sessionPromptAsyncMock . mockReset ( ) ;
162179 mocked . sessionCreateMock . mockReset ( ) ;
163180 mocked . suppressionRegisterMock . mockReset ( ) ;
164181 mocked . safeBackgroundTaskMock . mockReset ( ) ;
@@ -179,6 +196,7 @@ describe("bot/handlers/prompt", () => {
179196 error : null ,
180197 } ) ;
181198 mocked . sessionPromptMock . mockResolvedValue ( { data : { } , error : null } ) ;
199+ mocked . sessionPromptAsyncMock . mockResolvedValue ( { data : { } , error : null } ) ;
182200 } ) ;
183201
184202 it ( "registers suppression entry for text prompts" , async ( ) => {
@@ -198,6 +216,67 @@ describe("bot/handlers/prompt", () => {
198216 expect ( mocked . suppressionRegisterMock ) . toHaveBeenCalledWith ( "session-1" , "Review README" ) ;
199217 } ) ;
200218
219+ it ( "starts prompts through promptAsync instead of the streaming prompt endpoint" , async ( ) => {
220+ const handled = await processUserPrompt ( createContext ( ) , "Review README" , createDeps ( ) ) ;
221+
222+ expect ( handled ) . toBe ( true ) ;
223+
224+ const backgroundTask = getScheduledBackgroundTask ( ) ;
225+ await backgroundTask . task ( ) ;
226+
227+ expect ( mocked . sessionPromptAsyncMock ) . toHaveBeenCalledWith ( {
228+ sessionID : "session-1" ,
229+ directory : "D:\\Projects\\Repo" ,
230+ parts : [ { type : "text" , text : "Review README" } ] ,
231+ agent : "build" ,
232+ model : {
233+ providerID : "openai" ,
234+ modelID : "gpt-5" ,
235+ } ,
236+ variant : "default" ,
237+ } ) ;
238+ expect ( mocked . sessionPromptMock ) . not . toHaveBeenCalled ( ) ;
239+ } ) ;
240+
241+ it ( "still notifies the user when promptAsync reports a real start error" , async ( ) => {
242+ const ctx = createContext ( ) ;
243+ const deps = createDeps ( ) ;
244+
245+ const handled = await processUserPrompt ( ctx , "Review README" , deps ) ;
246+
247+ expect ( handled ) . toBe ( true ) ;
248+
249+ const backgroundTask = getScheduledBackgroundTask ( ) ;
250+ backgroundTask . onSuccess ?.( { error : new Error ( "request start failed" ) } ) ;
251+
252+ expect ( deps . bot . api . sendMessage ) . toHaveBeenCalledWith (
253+ 777 ,
254+ "Failed to send request to OpenCode." ,
255+ ) ;
256+ } ) ;
257+
258+ it ( "still notifies the user when promptAsync rejects before the run starts" , async ( ) => {
259+ const ctx = createContext ( ) ;
260+ const deps = createDeps ( ) ;
261+
262+ const handled = await processUserPrompt ( ctx , "Review README" , deps ) ;
263+
264+ expect ( handled ) . toBe ( true ) ;
265+
266+ const backgroundTask = getScheduledBackgroundTask ( ) ;
267+ const startError = new Error ( "network down" ) ;
268+ mocked . sessionPromptAsyncMock . mockRejectedValueOnce ( startError ) ;
269+
270+ await backgroundTask . task ( ) . catch ( ( error ) => {
271+ backgroundTask . onError ?.( error ) ;
272+ } ) ;
273+
274+ expect ( deps . bot . api . sendMessage ) . toHaveBeenCalledWith (
275+ 777 ,
276+ "Failed to send request to OpenCode." ,
277+ ) ;
278+ } ) ;
279+
201280 it ( "does not register suppression entry for file-only prompts" , async ( ) => {
202281 const handled = await processUserPrompt ( createContext ( ) , "" , createDeps ( ) , [
203282 {
0 commit comments