diff --git a/acp_adapter/gateway_server.py b/acp_adapter/gateway_server.py index 6c569d6216..ff78c7e0f3 100644 --- a/acp_adapter/gateway_server.py +++ b/acp_adapter/gateway_server.py @@ -300,22 +300,26 @@ class GatewayACPAgent(GatewayReplyMixin, acp.Agent): """Main's new/load/resume/fork reply fields (model picker, modes, provenance ``_meta``); the command list and context indicator follow once the reply is queued, as main scheduled them.""" from acp_adapter.gateway_session_meta import model_state, provenance_meta, session_config_options, session_modes - from acp_adapter.modes import MODE_TO_EDIT_APPROVAL_POLICY + from acp_adapter.modes import EDIT_APPROVAL_POLICY_CONFIG_ID, MODE_TO_EDIT_APPROVAL_POLICY title = info.get("title") if isinstance(title, str) and title.strip(): self._titles[session_id] = title models = await asyncio.to_thread(model_state, info) meta = await asyncio.to_thread(provenance_meta, self._home / "state.db", editor_id, session_id) + mode = self._modes.setdefault(session_id, session_modes(info).current_mode_id) + options = await session_config_options(self._gateway, session_id, models, info, mode) + # ``_adopt`` seeded the mode from the snapshot: a change the pump projected since is what the reply shows. + policy = MODE_TO_EDIT_APPROVAL_POLICY[self._modes[session_id]] + modes = session_modes({"edit_approval_policy": policy}) + for option in options: + if option.id == EDIT_APPROVAL_POLICY_CONFIG_ID: + option.current_value = policy if self._conn: task = asyncio.get_running_loop().create_task(self._after_attach(session_id, editor_id)) self._background.add(task) task.add_done_callback(self._background.discard) - # ``_adopt`` seeded the mode from the snapshot: a change the pump projected since is what the reply shows. - modes = session_modes({"edit_approval_policy": MODE_TO_EDIT_APPROVAL_POLICY[self._modes.setdefault( - session_id, session_modes(info).current_mode_id)]}) return {"models": models, "modes": modes, "field_meta": meta, - "config_options": await session_config_options( - self._gateway, session_id, models, info, modes.current_mode_id)} + "config_options": options} async def _after_attach(self, session_id, editor_id): from acp_adapter.gateway_session_meta import available_commands_update diff --git a/tests/acp_adapter/test_gateway_attach_notice_order.py b/tests/acp_adapter/test_gateway_attach_notice_order.py new file mode 100644 index 0000000000..808dae62ef --- /dev/null +++ b/tests/acp_adapter/test_gateway_attach_notice_order.py @@ -0,0 +1,56 @@ +"""Attach command notices follow the reply even when speed discovery yields.""" +import asyncio +from unittest.mock import AsyncMock + +import pytest + +from acp_adapter.gateway_server import GatewayACPAgent + + +@pytest.mark.asyncio +async def test_speed_discovery_does_not_publish_commands_before_the_attach_reply(monkeypatch): + monkeypatch.setattr('acp_adapter.gateway_session_meta.model_state', lambda info: None) + monkeypatch.setattr('acp_adapter.gateway_session_meta.provenance_meta', lambda *args: None) + agent = GatewayACPAgent() + reply_returned = False + command_order = [] + + async def session_update(*, session_id, update): + if update.session_update == 'available_commands_update': + command_order.append(reply_returned) + + async def rpc(method, **params): + if method == 'config.get': + await asyncio.gather(*tuple(agent._background)) + return {'value': 'normal', 'options': ['normal', 'fast']} + return {} + + owner = AsyncMock() + owner.rpc.side_effect = rpc + agent._gateway = owner + monkeypatch.setattr(agent, '_client', AsyncMock(return_value=owner)) + agent._conn = AsyncMock() + agent._conn.session_update.side_effect = session_update + fields = await agent._session_fields('session', {'model': 'gpt-6.1-sol'}, 'session') + reply_returned = True + await asyncio.gather(*tuple(agent._background)) + assert any(option.id == 'service_tier' for option in fields['config_options']) + assert command_order == [True] + + +@pytest.mark.asyncio +async def test_mode_changed_during_speed_discovery_is_used_by_the_attach_reply(monkeypatch): + monkeypatch.setattr('acp_adapter.gateway_session_meta.model_state', lambda info: None) + monkeypatch.setattr('acp_adapter.gateway_session_meta.provenance_meta', lambda *args: None) + agent = GatewayACPAgent() + + async def rpc(method, **params): + await agent._project_mode('session', 'session', {'edit_approval_policy': 'session'}) + return {'value': 'normal', 'options': ['normal', 'fast']} + + owner = AsyncMock() + owner.rpc.side_effect = rpc + agent._gateway = owner + fields = await agent._session_fields('session', {'model': 'gpt-6.1-sol'}, 'session') + assert fields['modes'].current_mode_id == 'dont_ask' + assert next(option for option in fields['config_options'] if option.category == 'mode').current_value == 'session'