Add optional Nanobot plugin controls (#4396)
* feat: add optional nanobot features * test: update azure install hint expectation * fix: validate optional feature extras maintainer edit: verify requested dependency extras before treating optional features as installed, propagate restart state from feature enablement, and align docs with the new plugins enable command. * fix: bound optional feature installs maintainer edit: make optional feature installs time out as a normal install failure instead of leaving the WebUI or CLI action waiting indefinitely. * feat: slim optional channel dependencies * fix: log optional install commands * fix(webui): gate remote feature installs * docs: clarify webhook plugin example * fix(webui): harden optional feature installs * fix: install optional deps without package fallback * fix(cli): refine plugin feature controls * fix(webui): count enabled nanobot features * fix(webui): allow slow feature install routes * fix(webui): allow disabling websocket channel * fix(plugins): simplify optional feature controls * fix(webui): polish apps catalog states * fix(webui): confirm nanobot support installs * fix(webui): polish nanobot install dialog * fix(webui): suppress empty websocket handshakes * fix(webui): clarify apps plugin summary * fix(webui): localize workspace access copy * fix(plugins): polish optional feature controls (#4691) --------- Co-authored-by: Xubin Ren <52506698+Re-bin@users.noreply.github.com>
This commit is contained in:
@@ -21,7 +21,11 @@ from unittest.mock import AsyncMock
|
||||
import pytest
|
||||
|
||||
from nanobot.bus.events import OutboundMessage
|
||||
from nanobot.bus.outbound_events import ProgressEvent, outbound_message_for_event
|
||||
from nanobot.bus.outbound_events import (
|
||||
ProgressEvent,
|
||||
outbound_event_from_message,
|
||||
outbound_message_for_event,
|
||||
)
|
||||
from nanobot.bus.queue import MessageBus
|
||||
from nanobot.channels.base import BaseChannel
|
||||
from nanobot.channels.manager import ChannelManager
|
||||
@@ -60,7 +64,8 @@ class _MockChannel(BaseChannel):
|
||||
|
||||
@pytest.fixture
|
||||
def manager() -> ChannelManager:
|
||||
mgr = ChannelManager(Config(), MessageBus())
|
||||
config = Config.model_validate({"channels": {"websocket": {"enabled": False}}})
|
||||
mgr = ChannelManager(config, MessageBus())
|
||||
mgr.channels["mock"] = _MockChannel({}, mgr.bus)
|
||||
return mgr
|
||||
|
||||
@@ -291,14 +296,22 @@ async def test_reasoning_routing_does_not_consult_send_progress(manager):
|
||||
|
||||
|
||||
async def _pump_one(manager: ChannelManager) -> None:
|
||||
"""Drive the dispatcher until the outbound queue drains, then cancel."""
|
||||
task = asyncio.create_task(manager._dispatch_outbound())
|
||||
for _ in range(50):
|
||||
await asyncio.sleep(0.01)
|
||||
if manager.bus.outbound.qsize() == 0:
|
||||
"""Process currently queued messages through the reasoning dispatch branch."""
|
||||
|
||||
async def dispatch_one(msg: OutboundMessage) -> None:
|
||||
event = outbound_event_from_message(msg)
|
||||
if isinstance(event, ProgressEvent) and (
|
||||
event.reasoning_delta
|
||||
or event.reasoning_end
|
||||
or event.reasoning
|
||||
):
|
||||
channel = manager.channels.get(msg.channel)
|
||||
if channel is not None and channel.show_reasoning:
|
||||
await manager._send_with_retry(channel, msg)
|
||||
|
||||
await dispatch_one(await asyncio.wait_for(manager.bus.consume_outbound(), timeout=1.0))
|
||||
while True:
|
||||
try:
|
||||
await dispatch_one(manager.bus.outbound.get_nowait())
|
||||
except asyncio.QueueEmpty:
|
||||
break
|
||||
task.cancel()
|
||||
try:
|
||||
await task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user