feat(webui): add guided setup flows
* 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
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user