@@ -7,6 +7,8 @@ const dispatchPluginInteractiveHandlerMock = vi.fn(async () => ({
77 handled : false ,
88 duplicate : false ,
99} ) ) ;
10+ const resolvePluginConversationBindingApprovalMock = vi . fn ( ) ;
11+ const buildPluginBindingResolvedTextMock = vi . fn ( ( ) => "Binding updated." ) ;
1012
1113vi . mock ( "../../../../../src/infra/system-events.js" , ( ) => ( {
1214 enqueueSystemEvent : ( ...args : unknown [ ] ) =>
@@ -18,6 +20,21 @@ vi.mock("../../../../../src/plugins/interactive.js", () => ({
1820 ( dispatchPluginInteractiveHandlerMock as ( ...innerArgs : unknown [ ] ) => unknown ) ( ...args ) ,
1921} ) ) ;
2022
23+ vi . mock ( "../../../../../src/plugins/conversation-binding.js" , async ( ) => {
24+ const actual = await vi . importActual <
25+ typeof import ( "../../../../../src/plugins/conversation-binding.js" )
26+ > ( "../../../../../src/plugins/conversation-binding.js" ) ;
27+ return {
28+ ...actual ,
29+ resolvePluginConversationBindingApproval : ( ...args : unknown [ ] ) =>
30+ ( resolvePluginConversationBindingApprovalMock as ( ...innerArgs : unknown [ ] ) => unknown ) (
31+ ...args ,
32+ ) ,
33+ buildPluginBindingResolvedText : ( ...args : unknown [ ] ) =>
34+ ( buildPluginBindingResolvedTextMock as ( ...innerArgs : unknown [ ] ) => unknown ) ( ...args ) ,
35+ } ;
36+ } ) ;
37+
2138type RegisteredHandler = ( args : {
2239 ack : ( ) => Promise < void > ;
2340 body : {
@@ -166,6 +183,10 @@ describe("registerSlackInteractionEvents", () => {
166183 beforeEach ( ( ) => {
167184 enqueueSystemEventMock . mockClear ( ) ;
168185 dispatchPluginInteractiveHandlerMock . mockClear ( ) ;
186+ resolvePluginConversationBindingApprovalMock . mockClear ( ) ;
187+ resolvePluginConversationBindingApprovalMock . mockResolvedValue ( { status : "expired" } ) ;
188+ buildPluginBindingResolvedTextMock . mockClear ( ) ;
189+ buildPluginBindingResolvedTextMock . mockReturnValue ( "Binding updated." ) ;
169190 dispatchPluginInteractiveHandlerMock . mockResolvedValue ( {
170191 matched : false ,
171192 handled : false ,
@@ -299,9 +320,11 @@ describe("registerSlackInteractionEvents", () => {
299320 expect . objectContaining ( {
300321 channel : "slack" ,
301322 data : "codex:approve:thread-1" ,
323+ interactionId : "U123:C1:100.200:codex:approve:thread-1" ,
302324 ctx : expect . objectContaining ( {
303325 accountId : ctx . accountId ,
304326 conversationId : "C1" ,
327+ interactionId : "U123:C1:100.200:codex:approve:thread-1" ,
305328 threadId : "100.100" ,
306329 interaction : expect . objectContaining ( {
307330 actionId : "codex" ,
@@ -314,6 +337,74 @@ describe("registerSlackInteractionEvents", () => {
314337 expect ( app . client . chat . update ) . not . toHaveBeenCalled ( ) ;
315338 } ) ;
316339
340+ it ( "resolves plugin binding approvals from shared interactive Slack actions" , async ( ) => {
341+ resolvePluginConversationBindingApprovalMock . mockResolvedValueOnce ( {
342+ status : "approved" ,
343+ decision : "allow-once" ,
344+ request : {
345+ pluginId : "codex" ,
346+ pluginName : "Codex" ,
347+ summary : "for this thread" ,
348+ } ,
349+ } ) ;
350+ const { ctx, app, getHandler } = createContext ( ) ;
351+ registerSlackInteractionEvents ( { ctx : ctx as never } ) ;
352+
353+ const handler = getHandler ( ) ;
354+ expect ( handler ) . toBeTruthy ( ) ;
355+
356+ const ack = vi . fn ( ) . mockResolvedValue ( undefined ) ;
357+ const respond = vi . fn ( ) . mockResolvedValue ( undefined ) ;
358+ await handler ! ( {
359+ ack,
360+ respond,
361+ body : {
362+ user : { id : "U123" } ,
363+ channel : { id : "C1" } ,
364+ container : { channel_id : "C1" , message_ts : "100.200" , thread_ts : "100.100" } ,
365+ message : {
366+ ts : "100.200" ,
367+ text : "Approve this bind?" ,
368+ blocks : [
369+ {
370+ type : "actions" ,
371+ block_id : "bind_actions" ,
372+ elements : [ { type : "button" , action_id : "openclaw:reply_button" } ] ,
373+ } ,
374+ ] ,
375+ } ,
376+ } ,
377+ action : {
378+ type : "button" ,
379+ action_id : "openclaw:reply_button" ,
380+ block_id : "bind_actions" ,
381+ value : "pluginbind:approval-123:o" ,
382+ text : { type : "plain_text" , text : "Allow once" } ,
383+ } ,
384+ } ) ;
385+
386+ expect ( ack ) . toHaveBeenCalled ( ) ;
387+ expect ( resolvePluginConversationBindingApprovalMock ) . toHaveBeenCalledWith ( {
388+ approvalId : "approval-123" ,
389+ decision : "allow-once" ,
390+ senderId : "U123" ,
391+ } ) ;
392+ expect ( dispatchPluginInteractiveHandlerMock ) . not . toHaveBeenCalled ( ) ;
393+ expect ( app . client . chat . update ) . toHaveBeenCalledWith (
394+ expect . objectContaining ( {
395+ channel : "C1" ,
396+ ts : "100.200" ,
397+ text : "Approve this bind?" ,
398+ blocks : [ ] ,
399+ } ) ,
400+ ) ;
401+ expect ( respond ) . toHaveBeenCalledWith ( {
402+ text : "Binding updated." ,
403+ response_type : "ephemeral" ,
404+ } ) ;
405+ expect ( enqueueSystemEventMock ) . not . toHaveBeenCalled ( ) ;
406+ } ) ;
407+
317408 it ( "drops block actions when mismatch guard triggers" , async ( ) => {
318409 enqueueSystemEventMock . mockClear ( ) ;
319410 const { ctx, app, getHandler } = createContext ( {
0 commit comments