77 */
88import { beforeEach , describe , expect , it , vi } from "vitest" ;
99import { useChatStore } from "../webview-ui/src/stores/chat.store" ;
10+ import { useApprovalStore } from "../webview-ui/src/stores/approval.store" ;
1011import { deriveWorkflowLanes , maxLaneStepCount } from "../webview-ui/src/lib/workflow-lanes" ;
1112import type { UIStepItem } from "../webview-ui/src/stores/chat.store" ;
1213
@@ -17,6 +18,9 @@ const boundary = vi.hoisted(() => ({
1718 trackFiles : vi . fn ( ) ,
1819 toastError : vi . fn ( ) ,
1920 toastWarning : vi . fn ( ) ,
21+ toastSuccess : vi . fn ( ) ,
22+ setPermissionMode : vi . fn ( ) ,
23+ respondApproval : vi . fn ( ) ,
2024} ) ) ;
2125
2226vi . mock ( "@/services" , ( ) => ( {
@@ -25,10 +29,16 @@ vi.mock("@/services", () => ({
2529 streamChat : boundary . streamChat ,
2630 abortChat : boundary . abortChat ,
2731 trackFiles : boundary . trackFiles ,
32+ setPermissionMode : boundary . setPermissionMode ,
33+ respondApproval : boundary . respondApproval ,
2834 } ,
2935} ) ) ;
3036vi . mock ( "@/components/ui/sonner" , ( ) => ( {
31- toast : { error : boundary . toastError , warning : boundary . toastWarning } ,
37+ toast : {
38+ error : boundary . toastError ,
39+ warning : boundary . toastWarning ,
40+ success : boundary . toastSuccess ,
41+ } ,
3242} ) ) ;
3343
3444beforeEach ( ( ) => {
@@ -270,3 +280,61 @@ describe("workflow lane derivation", () => {
270280 expect ( maxLaneStepCount ( lanes ) ) . toBe ( 2 ) ;
271281 } ) ;
272282} ) ;
283+
284+ describe ( "permission slash commands (control path, not a queued turn)" , ( ) => {
285+ beforeEach ( ( ) => {
286+ boundary . setPermissionMode . mockReset ( ) ;
287+ boundary . respondApproval . mockReset ( ) ;
288+ boundary . toastSuccess . mockReset ( ) ;
289+ boundary . respondApproval . mockResolvedValue ( { ok : true } ) ;
290+ useApprovalStore . setState ( { pending : [ ] } ) ;
291+ } ) ;
292+
293+ it ( "sends /yolo straight through while a turn is streaming instead of queueing it" , async ( ) => {
294+ boundary . setPermissionMode . mockResolvedValue ( {
295+ ok : true ,
296+ mode : "yolo" ,
297+ message : "You only live once!" ,
298+ } ) ;
299+ useChatStore . setState ( { isStreaming : true , queue : [ ] } ) ;
300+
301+ useChatStore . getState ( ) . sendMessage ( "/yolo" ) ;
302+ await vi . waitFor ( ( ) => expect ( boundary . setPermissionMode ) . toHaveBeenCalled ( ) ) ;
303+
304+ expect ( boundary . setPermissionMode ) . toHaveBeenCalledWith ( "yolo" , "toggle" ) ;
305+ expect ( useChatStore . getState ( ) . queue ) . toHaveLength ( 0 ) ;
306+ expect ( boundary . streamChat ) . not . toHaveBeenCalled ( ) ;
307+ } ) ;
308+
309+ it ( "answers the approvals already on screen when a turn is unblocked by /yolo" , async ( ) => {
310+ boundary . setPermissionMode . mockResolvedValue ( { ok : true , mode : "yolo" , message : "on" } ) ;
311+ useApprovalStore . setState ( {
312+ pending : [
313+ {
314+ id : "approval-1" ,
315+ tool_call_id : "call-1" ,
316+ sender : "Bash" ,
317+ action : "run" ,
318+ description : "grep" ,
319+ display : [ ] ,
320+ } ,
321+ ] ,
322+ } ) ;
323+ useChatStore . setState ( { isStreaming : true , queue : [ ] } ) ;
324+
325+ useChatStore . getState ( ) . sendMessage ( "/yolo on" ) ;
326+ await vi . waitFor ( ( ) => expect ( boundary . respondApproval ) . toHaveBeenCalled ( ) ) ;
327+
328+ expect ( boundary . respondApproval ) . toHaveBeenCalledWith ( "approval-1" , "approve_for_session" ) ;
329+ expect ( useApprovalStore . getState ( ) . pending ) . toHaveLength ( 0 ) ;
330+ } ) ;
331+
332+ it ( "still queues an ordinary message while streaming" , ( ) => {
333+ useChatStore . setState ( { isStreaming : true , queue : [ ] } ) ;
334+
335+ useChatStore . getState ( ) . sendMessage ( "/compact" ) ;
336+
337+ expect ( boundary . setPermissionMode ) . not . toHaveBeenCalled ( ) ;
338+ expect ( useChatStore . getState ( ) . queue ) . toHaveLength ( 1 ) ;
339+ } ) ;
340+ } ) ;
0 commit comments