@@ -672,6 +672,241 @@ T["ACPHandler"]["handles no connection"] = function()
672672 h .eq (" \\ cost" , result .content )
673673end
674674
675+ T [" ACPHandler" ][" Permission Queue" ] = new_set ()
676+
677+ T [" ACPHandler" ][" Permission Queue" ][" queues concurrent requests and presents one at a time" ] = function ()
678+ local result = child .lua ([[
679+ local chat = h.setup_chat_buffer({}, {
680+ name = "test_acp",
681+ config = {
682+ name = "test_acp",
683+ type = "acp",
684+ handlers = { form_messages = function(a, m) return m end }
685+ }
686+ })
687+
688+ local ACPHandler = require("codecompanion.interactions.chat.acp.handler")
689+ local handler = ACPHandler.new(chat)
690+
691+ -- Track which requests reach the permission UI
692+ local confirmed = {}
693+ package.loaded["codecompanion.interactions.chat.acp.request_permission"] = {
694+ confirm = function(chat_arg, request)
695+ table.insert(confirmed, request)
696+ end
697+ }
698+
699+ -- Send three permission requests concurrently
700+ handler:handle_permission_request({
701+ tool_call = { toolCallId = "tool_1", kind = "edit", title = "Edit file A" },
702+ options = { { kind = "allow_once", optionId = "allow", name = "Allow" } },
703+ respond = function() end,
704+ })
705+ handler:handle_permission_request({
706+ tool_call = { toolCallId = "tool_2", kind = "edit", title = "Edit file B" },
707+ options = { { kind = "allow_once", optionId = "allow", name = "Allow" } },
708+ respond = function() end,
709+ })
710+ handler:handle_permission_request({
711+ tool_call = { toolCallId = "tool_3", kind = "edit", title = "Edit file C" },
712+ options = { { kind = "allow_once", optionId = "allow", name = "Allow" } },
713+ respond = function() end,
714+ })
715+
716+ return {
717+ confirmed_count = #confirmed,
718+ first_id = confirmed[1] and confirmed[1].tool_call.toolCallId,
719+ queue_count = handler._permission.queue:count(),
720+ active = handler._permission.active,
721+ }
722+ ]] )
723+
724+ h .eq (1 , result .confirmed_count )
725+ h .eq (" tool_1" , result .first_id )
726+ h .eq (2 , result .queue_count )
727+ h .is_true (result .active )
728+ end
729+
730+ T [" ACPHandler" ][" Permission Queue" ][" presents next request after user responds" ] = function ()
731+ local result = child .lua ([[
732+ local chat = h.setup_chat_buffer({}, {
733+ name = "test_acp",
734+ config = {
735+ name = "test_acp",
736+ type = "acp",
737+ handlers = { form_messages = function(a, m) return m end }
738+ }
739+ })
740+
741+ local ACPHandler = require("codecompanion.interactions.chat.acp.handler")
742+ local handler = ACPHandler.new(chat)
743+
744+ local confirmed = {}
745+ package.loaded["codecompanion.interactions.chat.acp.request_permission"] = {
746+ confirm = function(chat_arg, request)
747+ table.insert(confirmed, request)
748+ end
749+ }
750+
751+ local responses = {}
752+ local make_respond = function(id)
753+ return function(option_id, canceled)
754+ table.insert(responses, { id = id, option_id = option_id, canceled = canceled })
755+ end
756+ end
757+
758+ handler:handle_permission_request({
759+ tool_call = { toolCallId = "tool_1" },
760+ options = { { kind = "allow_once", optionId = "allow", name = "Allow" } },
761+ respond = make_respond("tool_1"),
762+ })
763+ handler:handle_permission_request({
764+ tool_call = { toolCallId = "tool_2" },
765+ options = { { kind = "allow_once", optionId = "allow", name = "Allow" } },
766+ respond = make_respond("tool_2"),
767+ })
768+
769+ -- Simulate user accepting the first request
770+ confirmed[1].respond("allow", false)
771+
772+ return {
773+ confirmed_count = #confirmed,
774+ second_id = confirmed[2] and confirmed[2].tool_call.toolCallId,
775+ responses = responses,
776+ queue_empty = handler._permission.queue:is_empty(),
777+ active = handler._permission.active,
778+ }
779+ ]] )
780+
781+ h .eq (2 , result .confirmed_count )
782+ h .eq (" tool_2" , result .second_id )
783+ h .eq (" tool_1" , result .responses [1 ].id )
784+ h .eq (" allow" , result .responses [1 ].option_id )
785+ h .is_true (result .queue_empty )
786+ h .is_true (result .active )
787+ end
788+
789+ T [" ACPHandler" ][" Permission Queue" ][" clears queue on completion" ] = function ()
790+ local result = child .lua ([[
791+ local chat = h.setup_chat_buffer({}, {
792+ name = "test_acp",
793+ config = {
794+ name = "test_acp",
795+ type = "acp",
796+ handlers = { form_messages = function(a, m) return m end }
797+ }
798+ })
799+
800+ local ACPHandler = require("codecompanion.interactions.chat.acp.handler")
801+ local handler = ACPHandler.new(chat)
802+
803+ local confirmed = {}
804+ package.loaded["codecompanion.interactions.chat.acp.request_permission"] = {
805+ confirm = function(chat_arg, request)
806+ table.insert(confirmed, request)
807+ end
808+ }
809+
810+ local rejected = {}
811+ local make_respond = function(id)
812+ return function(option_id, canceled)
813+ if canceled then
814+ table.insert(rejected, id)
815+ end
816+ end
817+ end
818+
819+ -- Queue up three requests
820+ handler:handle_permission_request({
821+ tool_call = { toolCallId = "tool_1" },
822+ options = {},
823+ respond = make_respond("tool_1"),
824+ })
825+ handler:handle_permission_request({
826+ tool_call = { toolCallId = "tool_2" },
827+ options = {},
828+ respond = make_respond("tool_2"),
829+ })
830+ handler:handle_permission_request({
831+ tool_call = { toolCallId = "tool_3" },
832+ options = {},
833+ respond = make_respond("tool_3"),
834+ })
835+
836+ -- Simulate completion while requests are still queued
837+ chat.done = function() end
838+ handler:handle_completion()
839+
840+ return {
841+ queue_empty = handler._permission.queue:is_empty(),
842+ active = handler._permission.active,
843+ rejected = rejected,
844+ }
845+ ]] )
846+
847+ h .is_true (result .queue_empty )
848+ h .is_false (result .active )
849+ h .eq ({ " tool_2" , " tool_3" }, result .rejected )
850+ end
851+
852+ T [" ACPHandler" ][" Permission Queue" ][" clears queue on error" ] = function ()
853+ local result = child .lua ([[
854+ local chat = h.setup_chat_buffer({}, {
855+ name = "test_acp",
856+ config = {
857+ name = "test_acp",
858+ type = "acp",
859+ handlers = { form_messages = function(a, m) return m end }
860+ }
861+ })
862+
863+ local ACPHandler = require("codecompanion.interactions.chat.acp.handler")
864+ local handler = ACPHandler.new(chat)
865+
866+ local confirmed = {}
867+ package.loaded["codecompanion.interactions.chat.acp.request_permission"] = {
868+ confirm = function(chat_arg, request)
869+ table.insert(confirmed, request)
870+ end
871+ }
872+
873+ local rejected = {}
874+ local make_respond = function(id)
875+ return function(option_id, canceled)
876+ if canceled then
877+ table.insert(rejected, id)
878+ end
879+ end
880+ end
881+
882+ handler:handle_permission_request({
883+ tool_call = { toolCallId = "tool_1" },
884+ options = {},
885+ respond = make_respond("tool_1"),
886+ })
887+ handler:handle_permission_request({
888+ tool_call = { toolCallId = "tool_2" },
889+ options = {},
890+ respond = make_respond("tool_2"),
891+ })
892+
893+ -- Stub add_buf_message and done
894+ chat.add_buf_message = function() end
895+ chat.done = function() end
896+ handler:handle_error("Something went wrong")
897+
898+ return {
899+ queue_empty = handler._permission.queue:is_empty(),
900+ active = handler._permission.active,
901+ rejected = rejected,
902+ }
903+ ]] )
904+
905+ h .is_true (result .queue_empty )
906+ h .is_false (result .active )
907+ h .eq ({ " tool_2" }, result .rejected )
908+ end
909+
675910T [" ACPHandler" ][" Config Options" ] = new_set ()
676911
677912T [" ACPHandler" ][" Config Options" ][" updates metadata with config options" ] = function ()
0 commit comments