5c829942d7
* refactor(cli): replace channel module globals with ChannelRuntime Removes _cli_agent / _cli_thread_id from EvoScientist/cli/channel.py and threads a ChannelRuntime via CommandContext.channel_runtime so /model and /channel rebind without poking module-level state. * fix(cli): address coderabbit review - _auto_start_channel: bind ChannelRuntime only after _start_channels_bus_mode succeeds, so a startup failure no longer leaves a stale binding pointing at channels that never started. - _sync_tui_command_completion (TUI) and the Rich CLI command-completion paths: rebind the runtime on thread rotation, not just agent swap, so /new and /resume keep ChannelRuntime in sync with the running thread (matches the serve-mode hook contract). - test_hook_syncs_channel_runtime: pin ctx.thread_id explicitly so a bare MagicMock attribute can't silently mutate runtime.thread_id. - New regression test covering the rebind-on-thread-rotation contract.
204 lines
6.5 KiB
Python
204 lines
6.5 KiB
Python
"""Tests for the /channel command (ChannelCommand)."""
|
|
|
|
from types import SimpleNamespace
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from tests.conftest import run_async as _run
|
|
|
|
|
|
def _ctx():
|
|
from EvoScientist.commands.base import ChannelRuntime, CommandContext
|
|
|
|
ui = MagicMock()
|
|
ui.supports_interactive = True
|
|
runtime = ChannelRuntime()
|
|
ctx = CommandContext(
|
|
agent=object(),
|
|
thread_id="tid-42",
|
|
ui=ui,
|
|
workspace_dir="/ws",
|
|
channel_runtime=runtime,
|
|
)
|
|
return ctx, ui
|
|
|
|
|
|
class TestNeedsAgent:
|
|
"""status + stop must not require the agent (recovery from broken load)."""
|
|
|
|
def test_status_does_not_need_agent(self):
|
|
from EvoScientist.commands.implementation.channel import ChannelCommand
|
|
|
|
assert ChannelCommand().needs_agent(["status"]) is False
|
|
|
|
def test_stop_does_not_need_agent(self):
|
|
from EvoScientist.commands.implementation.channel import ChannelCommand
|
|
|
|
assert ChannelCommand().needs_agent(["stop"]) is False
|
|
|
|
def test_stop_with_target_does_not_need_agent(self):
|
|
from EvoScientist.commands.implementation.channel import ChannelCommand
|
|
|
|
assert ChannelCommand().needs_agent(["stop", "telegram"]) is False
|
|
|
|
def test_start_subcommand_needs_agent(self):
|
|
from EvoScientist.commands.implementation.channel import ChannelCommand
|
|
|
|
assert ChannelCommand().needs_agent(["telegram"]) is True
|
|
|
|
def test_no_args_needs_agent(self):
|
|
from EvoScientist.commands.implementation.channel import ChannelCommand
|
|
|
|
# no subcmd → falls through to start flow, needs agent
|
|
assert ChannelCommand().needs_agent([]) is True
|
|
|
|
|
|
class TestStartPath:
|
|
"""Start flow must propagate agent/thread_id globals."""
|
|
|
|
def test_start_binds_channel_runtime(self):
|
|
from EvoScientist.commands.implementation.channel import ChannelCommand
|
|
|
|
ctx, _ui = _ctx()
|
|
config = SimpleNamespace(
|
|
channel_enabled="telegram",
|
|
channel_send_thinking=True,
|
|
)
|
|
|
|
with (
|
|
patch(
|
|
"EvoScientist.cli.channel._channels_is_running",
|
|
return_value=False,
|
|
),
|
|
patch(
|
|
"EvoScientist.cli.channel._start_channels_bus_mode",
|
|
),
|
|
patch(
|
|
"EvoScientist.config.load_config",
|
|
return_value=config,
|
|
),
|
|
):
|
|
_run(ChannelCommand().execute(ctx, ["telegram"]))
|
|
assert ctx.channel_runtime.agent is ctx.agent
|
|
assert ctx.channel_runtime.thread_id == "tid-42"
|
|
|
|
def test_start_propagates_send_thinking(self):
|
|
"""send_thinking flag must reach _start_channels_bus_mode."""
|
|
from EvoScientist.commands.implementation.channel import ChannelCommand
|
|
|
|
ctx, _ui = _ctx()
|
|
config = SimpleNamespace(
|
|
channel_enabled="telegram",
|
|
channel_send_thinking=False,
|
|
)
|
|
captured = {}
|
|
|
|
def _fake_start(cfg, agent, thread_id, *, send_thinking=None):
|
|
captured["agent"] = agent
|
|
captured["thread_id"] = thread_id
|
|
captured["send_thinking"] = send_thinking
|
|
|
|
with (
|
|
patch(
|
|
"EvoScientist.cli.channel._channels_is_running",
|
|
return_value=False,
|
|
),
|
|
patch(
|
|
"EvoScientist.cli.channel._start_channels_bus_mode",
|
|
_fake_start,
|
|
),
|
|
patch(
|
|
"EvoScientist.config.load_config",
|
|
return_value=config,
|
|
),
|
|
):
|
|
_run(ChannelCommand().execute(ctx, ["telegram"]))
|
|
assert captured["agent"] is ctx.agent
|
|
assert captured["thread_id"] == "tid-42"
|
|
assert captured["send_thinking"] is False
|
|
|
|
|
|
class TestAddToRunningPath:
|
|
def test_add_to_running_binds_channel_runtime(self):
|
|
from EvoScientist.commands.implementation.channel import ChannelCommand
|
|
|
|
ctx, _ui = _ctx()
|
|
config = SimpleNamespace(
|
|
channel_enabled="telegram",
|
|
channel_send_thinking=False,
|
|
)
|
|
|
|
with (
|
|
patch(
|
|
"EvoScientist.cli.channel._channels_is_running",
|
|
return_value=True,
|
|
),
|
|
patch(
|
|
"EvoScientist.cli.channel._add_channel_to_running_bus",
|
|
),
|
|
patch(
|
|
"EvoScientist.config.load_config",
|
|
return_value=config,
|
|
),
|
|
):
|
|
_run(ChannelCommand().execute(ctx, ["discord"]))
|
|
assert ctx.channel_runtime.agent is ctx.agent
|
|
assert ctx.channel_runtime.thread_id == "tid-42"
|
|
|
|
def test_add_to_running_propagates_send_thinking(self):
|
|
"""Adding to a running bus must honor config.channel_send_thinking."""
|
|
from EvoScientist.commands.implementation.channel import ChannelCommand
|
|
|
|
ctx, _ui = _ctx()
|
|
config = SimpleNamespace(
|
|
channel_enabled="telegram",
|
|
channel_send_thinking=True,
|
|
)
|
|
captured = {}
|
|
|
|
def _fake_add(channel_type, cfg, *, send_thinking=None):
|
|
captured["channel_type"] = channel_type
|
|
captured["send_thinking"] = send_thinking
|
|
|
|
with (
|
|
patch(
|
|
"EvoScientist.cli.channel._channels_is_running",
|
|
return_value=True,
|
|
),
|
|
patch(
|
|
"EvoScientist.cli.channel._add_channel_to_running_bus",
|
|
_fake_add,
|
|
),
|
|
patch(
|
|
"EvoScientist.config.load_config",
|
|
return_value=config,
|
|
),
|
|
):
|
|
_run(ChannelCommand().execute(ctx, ["discord"]))
|
|
assert captured["channel_type"] == "discord"
|
|
assert captured["send_thinking"] is True
|
|
|
|
|
|
class TestStatusPath:
|
|
def test_status_without_running_channels(self):
|
|
from EvoScientist.commands.implementation.channel import ChannelCommand
|
|
|
|
ctx, ui = _ctx()
|
|
config = SimpleNamespace(channel_enabled="", channel_send_thinking=False)
|
|
with (
|
|
patch(
|
|
"EvoScientist.cli.channel._channels_is_running",
|
|
return_value=False,
|
|
),
|
|
patch(
|
|
"EvoScientist.cli.channel._channels_running_list",
|
|
return_value=[],
|
|
),
|
|
patch(
|
|
"EvoScientist.config.load_config",
|
|
return_value=config,
|
|
),
|
|
):
|
|
_run(ChannelCommand().execute(ctx, ["status"]))
|
|
msgs = [c.args[0] for c in ui.append_system.call_args_list]
|
|
assert any("No messaging channels" in m for m in msgs)
|