* feat(channels): add guided setup flows * test(channels): preserve setup config values * fix(channels): reflect saved setup state * refactor(channels): simplify setup state metadata * fix(channels): harden setup lifecycle * refactor(channels): centralize setup contracts * fix(channels): route setup actions through webui shim * fix(channels): adapt settings for compact screens * fix(models): preserve default preset display * feat(models): add curated Codex catalog * fix(webui): stop attached gateway on interrupt * fix(webui): simplify apps catalog * docs(webui): clarify apps and runtime features * feat(settings): add guided capability setup * fix(webui): harden setup and managed services * test: keep managed runtime checks portable * test: scope POSIX runtime coverage * fix(webui): simplify file settings * feat(files): bundle document reading * fix(webui): harden setup request boundaries * fix(webui): prevent channel setup status squeeze * fix(settings): group provider compatibility aliases * refactor(settings): remove redundant setup surfaces * fix(webui): harden guided setup lifecycle * fix(webui): preserve channel setup compatibility
115 lines
3.6 KiB
Python
115 lines
3.6 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
|
|
import pytest
|
|
|
|
from nanobot.bus.queue import MessageBus
|
|
from nanobot.channels.base import BaseChannel
|
|
from nanobot.channels.manager import ChannelManager
|
|
from nanobot.config.schema import Config
|
|
|
|
|
|
class _HotChannel(BaseChannel):
|
|
name = "hot"
|
|
display_name = "Hot"
|
|
|
|
def __init__(self, config, bus):
|
|
super().__init__(config, bus)
|
|
self.started = asyncio.Event()
|
|
self.stopped = asyncio.Event()
|
|
|
|
async def start(self):
|
|
self._running = True
|
|
self.started.set()
|
|
await self.stopped.wait()
|
|
|
|
async def stop(self):
|
|
self._running = False
|
|
self.stopped.set()
|
|
|
|
async def send(self, msg): # pragma: no cover - not used by this test
|
|
raise AssertionError("send should not be called")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_apply_channel_feature_action_starts_and_stops_channel(monkeypatch):
|
|
disabled = Config.model_validate({
|
|
"channels": {
|
|
"websocket": {"enabled": False},
|
|
"hot": {"enabled": False},
|
|
}
|
|
})
|
|
enabled = Config.model_validate({
|
|
"channels": {
|
|
"websocket": {"enabled": False},
|
|
"hot": {"enabled": True},
|
|
}
|
|
})
|
|
|
|
import nanobot.channels.registry as registry
|
|
|
|
def discover_enabled(enabled_names, **_kwargs):
|
|
return {"hot": _HotChannel} if "hot" in enabled_names else {}
|
|
|
|
configs = iter([enabled, disabled])
|
|
monkeypatch.setattr(registry, "discover_channel_names", lambda: ["hot"])
|
|
monkeypatch.setattr(registry, "discover_plugins", lambda enabled_names=None: {})
|
|
monkeypatch.setattr(registry, "discover_enabled", discover_enabled)
|
|
monkeypatch.setattr("nanobot.config.loader.load_config", lambda: next(configs))
|
|
|
|
manager = ChannelManager(disabled, MessageBus())
|
|
manager._started = True
|
|
|
|
enabled_result = await manager.apply_channel_feature_action("enable", "hot")
|
|
|
|
assert enabled_result["handled"] is True
|
|
assert enabled_result["requires_restart"] is False
|
|
channel = manager.channels["hot"]
|
|
await asyncio.wait_for(channel.started.wait(), timeout=1)
|
|
assert channel.is_running is True
|
|
|
|
disabled_result = await manager.apply_channel_feature_action("disable", "hot")
|
|
|
|
assert disabled_result["handled"] is True
|
|
assert disabled_result["requires_restart"] is False
|
|
assert "hot" not in manager.channels
|
|
assert channel.is_running is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_apply_channel_feature_action_keeps_running_channel_when_rebuild_fails(monkeypatch):
|
|
enabled = Config.model_validate({
|
|
"channels": {
|
|
"websocket": {"enabled": False},
|
|
"hot": {"enabled": True},
|
|
}
|
|
})
|
|
|
|
import nanobot.channels.registry as registry
|
|
|
|
monkeypatch.setattr(registry, "discover_channel_names", lambda: ["hot"])
|
|
monkeypatch.setattr(registry, "discover_plugins", lambda enabled_names=None: {})
|
|
monkeypatch.setattr(
|
|
registry,
|
|
"discover_enabled",
|
|
lambda enabled_names, **_kwargs: {"hot": _HotChannel},
|
|
)
|
|
monkeypatch.setattr("nanobot.config.loader.load_config", lambda: enabled)
|
|
|
|
manager = ChannelManager(enabled, MessageBus())
|
|
old_channel = manager.channels["hot"]
|
|
old_channel._running = True
|
|
|
|
def fail_build(*_args, **_kwargs):
|
|
raise RuntimeError("invalid replacement config")
|
|
|
|
monkeypatch.setattr(manager, "_build_channel", fail_build)
|
|
|
|
result = await manager.apply_channel_feature_action("enable", "hot")
|
|
|
|
assert result["requires_restart"] is True
|
|
assert manager.channels["hot"] is old_channel
|
|
assert old_channel.is_running is True
|
|
assert not old_channel.stopped.is_set()
|