|
2 | 2 |
|
3 | 3 | import threading |
4 | 4 | import time |
5 | | -from unittest.mock import MagicMock |
| 5 | +from unittest.mock import AsyncMock, MagicMock, patch |
6 | 6 |
|
7 | 7 | import pytest |
8 | 8 |
|
@@ -813,6 +813,77 @@ async def swallowed_interrupt_generate(text, preproc=True): |
813 | 813 | mock_io.tool_warning.assert_called_once() |
814 | 814 |
|
815 | 815 |
|
| 816 | +class TestSubAgentCoderSwitch: |
| 817 | + """A sub-agent's queued command signal swaps coders instead of escaping.""" |
| 818 | + |
| 819 | + @pytest.mark.asyncio |
| 820 | + async def test_process_prompt_queue_moves_queue_to_replacement_coder(self, mock_io): |
| 821 | + """A SwitchCoderSignal from a queued command switches only the sub-agent.""" |
| 822 | + from cecli.coders.base_coder import Coder |
| 823 | + |
| 824 | + coder = _make_coder_with_commands(("raises-switch", "second", "third")) |
| 825 | + coder.uuid = "sub-uuid" |
| 826 | + coder.parent_uuid = "primary-uuid" |
| 827 | + coder.io = mock_io |
| 828 | + coder.args = MagicMock() |
| 829 | + |
| 830 | + async def signal_generate(text, preproc=True): |
| 831 | + raise SwitchCoderSignal(edit_format="subagent", from_coder=coder) |
| 832 | + |
| 833 | + coder.generate = AsyncMock(side_effect=signal_generate) |
| 834 | + |
| 835 | + new_coder = MagicMock() |
| 836 | + new_coder.uuid = "sub-uuid" |
| 837 | + new_coder.process_prompt_queue = AsyncMock() |
| 838 | + |
| 839 | + from cecli.helpers.agents.service import AgentService, SubAgentInfo |
| 840 | + |
| 841 | + parent = MagicMock() |
| 842 | + parent.uuid = "primary-uuid" |
| 843 | + service = AgentService(parent) |
| 844 | + info = SubAgentInfo(name="wf:storytime", coder=coder, parent_uuid="primary-uuid") |
| 845 | + service.sub_agents["sub-uuid"] = info |
| 846 | + service._sub_agent_order.append("sub-uuid") |
| 847 | + |
| 848 | + with ( |
| 849 | + patch("cecli.coders.Coder.create", AsyncMock(return_value=new_coder)) as create, |
| 850 | + patch("cecli.helpers.agents.service.AgentService.get_instance", return_value=service), |
| 851 | + ): |
| 852 | + await Coder.process_prompt_queue(coder) |
| 853 | + |
| 854 | + assert [item["text"] for item in new_coder.prompt_queue] == ["second", "third"] |
| 855 | + assert coder.prompt_queue == [] |
| 856 | + assert info.coder is new_coder |
| 857 | + assert service.sub_agents["sub-uuid"] is info |
| 858 | + assert coder._processing_queue is False |
| 859 | + assert command_queue.get_active_prompt(coder) is None |
| 860 | + new_coder.process_prompt_queue.assert_awaited_once_with(True) |
| 861 | + |
| 862 | + create_kwargs = create.call_args.kwargs |
| 863 | + assert create_kwargs["from_coder"] is coder |
| 864 | + |
| 865 | + @pytest.mark.asyncio |
| 866 | + async def test_process_prompt_queue_reraises_switch_for_primary(self, mock_io): |
| 867 | + """A primary coder still propagates its own SwitchCoderSignal.""" |
| 868 | + from cecli.coders.base_coder import Coder |
| 869 | + |
| 870 | + coder = _make_coder_with_commands(("raises-switch",)) |
| 871 | + coder.uuid = "primary-uuid" |
| 872 | + coder.parent_uuid = "" |
| 873 | + coder.io = mock_io |
| 874 | + coder.args = MagicMock() |
| 875 | + |
| 876 | + async def signal_generate(text, preproc=True): |
| 877 | + raise SwitchCoderSignal(edit_format="agent", from_coder=coder) |
| 878 | + |
| 879 | + coder.generate = AsyncMock(side_effect=signal_generate) |
| 880 | + |
| 881 | + with pytest.raises(SwitchCoderSignal): |
| 882 | + await Coder.process_prompt_queue(coder) |
| 883 | + |
| 884 | + assert coder._processing_queue is False |
| 885 | + |
| 886 | + |
816 | 887 | class TestTUICompletionApplication: |
817 | 888 | """Applying nested completions must preserve the host and inner command.""" |
818 | 889 |
|
|
0 commit comments