fix(restart): show restart completion with elapsed time across channels

This commit is contained in:
imfondof
2026-04-04 02:21:42 +08:00
committed by Xubin Ren
parent ba7c07ccf2
commit 896d578677
8 changed files with 179 additions and 151 deletions
+28
View File
@@ -13,6 +13,7 @@ from nanobot.bus.queue import MessageBus
from nanobot.channels.base import BaseChannel
from nanobot.channels.manager import ChannelManager
from nanobot.config.schema import ChannelsConfig
from nanobot.utils.restart import RestartNotice
# ---------------------------------------------------------------------------
@@ -929,3 +930,30 @@ async def test_start_all_creates_dispatch_task():
# Dispatch task should have been created
assert mgr._dispatch_task is not None
@pytest.mark.asyncio
async def test_notify_restart_done_enqueues_outbound_message():
"""Restart notice should schedule send_with_retry for target channel."""
fake_config = SimpleNamespace(
channels=ChannelsConfig(),
providers=SimpleNamespace(groq=SimpleNamespace(api_key="")),
)
mgr = ChannelManager.__new__(ChannelManager)
mgr.config = fake_config
mgr.bus = MessageBus()
mgr.channels = {"feishu": _StartableChannel(fake_config, mgr.bus)}
mgr._dispatch_task = None
mgr._send_with_retry = AsyncMock()
notice = RestartNotice(channel="feishu", chat_id="oc_123", started_at_raw="100.0")
with patch("nanobot.channels.manager.consume_restart_notice_from_env", return_value=notice):
mgr._notify_restart_done_if_needed()
await asyncio.sleep(0)
mgr._send_with_retry.assert_awaited_once()
sent_channel, sent_msg = mgr._send_with_retry.await_args.args
assert sent_channel is mgr.channels["feishu"]
assert sent_msg.channel == "feishu"
assert sent_msg.chat_id == "oc_123"
assert sent_msg.content.startswith("Restart completed")
+6 -75
View File
@@ -5,7 +5,6 @@ from __future__ import annotations
import asyncio
import os
import time
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
@@ -37,8 +36,12 @@ class TestRestartCommand:
@pytest.mark.asyncio
async def test_restart_sends_message_and_calls_execv(self):
from nanobot.command.builtin import cmd_restart
from nanobot.config.runtime_keys import RESTART_NOTIFY_CHANNEL_ENV, RESTART_NOTIFY_CHAT_ID_ENV
from nanobot.command.router import CommandContext
from nanobot.utils.restart import (
RESTART_NOTIFY_CHANNEL_ENV,
RESTART_NOTIFY_CHAT_ID_ENV,
RESTART_STARTED_AT_ENV,
)
loop, bus = _make_loop()
msg = InboundMessage(channel="cli", sender_id="user", chat_id="direct", content="/restart")
@@ -50,6 +53,7 @@ class TestRestartCommand:
assert "Restarting" in out.content
assert os.environ.get(RESTART_NOTIFY_CHANNEL_ENV) == "cli"
assert os.environ.get(RESTART_NOTIFY_CHAT_ID_ENV) == "direct"
assert os.environ.get(RESTART_STARTED_AT_ENV)
await asyncio.sleep(1.5)
mock_execv.assert_called_once()
@@ -196,76 +200,3 @@ class TestRestartCommand:
assert response is not None
assert response.metadata == {"render_as": "text"}
@pytest.mark.asyncio
async def test_notify_restart_done_waits_until_channel_running() -> None:
from nanobot.bus.queue import MessageBus
from nanobot.cli.commands import _notify_restart_done_when_channel_ready
bus = MessageBus()
channel = SimpleNamespace(is_running=False)
class DummyChannels:
enabled_channels = ["feishu"]
@staticmethod
def get_channel(name: str):
return channel if name == "feishu" else None
async def _mark_running() -> None:
await asyncio.sleep(0.02)
channel.is_running = True
marker = asyncio.create_task(_mark_running())
sent = await _notify_restart_done_when_channel_ready(
bus=bus,
channels=DummyChannels(),
channel="feishu",
chat_id="oc_123",
timeout_s=0.2,
poll_s=0.01,
)
await marker
assert sent is True
out = await asyncio.wait_for(bus.consume_outbound(), timeout=0.1)
assert out.channel == "feishu"
assert out.chat_id == "oc_123"
assert out.content == "Restart completed."
@pytest.mark.asyncio
async def test_notify_restart_done_times_out_when_channel_not_running() -> None:
from nanobot.bus.queue import MessageBus
from nanobot.cli.commands import _notify_restart_done_when_channel_ready
bus = MessageBus()
channel = SimpleNamespace(is_running=False)
class DummyChannels:
enabled_channels = ["feishu"]
@staticmethod
def get_channel(name: str):
return channel if name == "feishu" else None
sent = await _notify_restart_done_when_channel_ready(
bus=bus,
channels=DummyChannels(),
channel="feishu",
chat_id="oc_123",
timeout_s=0.05,
poll_s=0.01,
)
assert sent is False
assert bus.outbound_size == 0
def test_should_show_cli_restart_notice() -> None:
from nanobot.cli.commands import _should_show_cli_restart_notice
assert _should_show_cli_restart_notice("cli", "direct", "cli:direct") is True
assert _should_show_cli_restart_notice("cli", "", "cli:direct") is True
assert _should_show_cli_restart_notice("cli", "other", "cli:direct") is False
assert _should_show_cli_restart_notice("feishu", "oc_123", "cli:direct") is False
+49
View File
@@ -0,0 +1,49 @@
"""Tests for restart notice helpers."""
from __future__ import annotations
import os
from nanobot.utils.restart import (
RestartNotice,
consume_restart_notice_from_env,
format_restart_completed_message,
set_restart_notice_to_env,
should_show_cli_restart_notice,
)
def test_set_and_consume_restart_notice_env_roundtrip(monkeypatch):
monkeypatch.delenv("NANOBOT_RESTART_NOTIFY_CHANNEL", raising=False)
monkeypatch.delenv("NANOBOT_RESTART_NOTIFY_CHAT_ID", raising=False)
monkeypatch.delenv("NANOBOT_RESTART_STARTED_AT", raising=False)
set_restart_notice_to_env(channel="feishu", chat_id="oc_123")
notice = consume_restart_notice_from_env()
assert notice is not None
assert notice.channel == "feishu"
assert notice.chat_id == "oc_123"
assert notice.started_at_raw
# Consumed values should be cleared from env.
assert consume_restart_notice_from_env() is None
assert "NANOBOT_RESTART_NOTIFY_CHANNEL" not in os.environ
assert "NANOBOT_RESTART_NOTIFY_CHAT_ID" not in os.environ
assert "NANOBOT_RESTART_STARTED_AT" not in os.environ
def test_format_restart_completed_message_with_elapsed(monkeypatch):
monkeypatch.setattr("nanobot.utils.restart.time.time", lambda: 102.0)
assert format_restart_completed_message("100.0") == "Restart completed in 2.0s."
def test_should_show_cli_restart_notice():
notice = RestartNotice(channel="cli", chat_id="direct", started_at_raw="100")
assert should_show_cli_restart_notice(notice, "cli:direct") is True
assert should_show_cli_restart_notice(notice, "cli:other") is False
assert should_show_cli_restart_notice(notice, "direct") is True
non_cli = RestartNotice(channel="feishu", chat_id="oc_1", started_at_raw="100")
assert should_show_cli_restart_notice(non_cli, "cli:direct") is False