refactor(bus): type outbound runtime events
This commit is contained in:
@@ -8,10 +8,9 @@ channels that opt in via ``channel.show_reasoning``; plugins without a
|
||||
low-emphasis UI primitive keep the base no-op and the content silently
|
||||
drops at dispatch.
|
||||
|
||||
One-shot ``_reasoning`` frames are accepted for back-compat with hooks
|
||||
that haven't migrated yet — ``BaseChannel.send_reasoning`` expands them
|
||||
to a single delta + end pair so plugins only implement the streaming
|
||||
primitives.
|
||||
One-shot reasoning frames are represented as typed progress events and
|
||||
``BaseChannel.send_reasoning`` expands them to a single delta + end pair so
|
||||
plugins only implement the streaming primitives.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -22,6 +21,7 @@ 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.queue import MessageBus
|
||||
from nanobot.channels.base import BaseChannel
|
||||
from nanobot.channels.manager import ChannelManager
|
||||
@@ -48,11 +48,11 @@ class _MockChannel(BaseChannel):
|
||||
async def send(self, msg):
|
||||
return await self._send_mock(msg)
|
||||
|
||||
async def send_reasoning_delta(self, chat_id, delta, metadata=None):
|
||||
return await self._delta_mock(chat_id, delta, metadata)
|
||||
async def send_reasoning_delta(self, chat_id, delta, metadata=None, *, stream_id=None):
|
||||
return await self._delta_mock(chat_id, delta, metadata, stream_id=stream_id)
|
||||
|
||||
async def send_reasoning_end(self, chat_id, metadata=None):
|
||||
return await self._end_mock(chat_id, metadata)
|
||||
async def send_reasoning_end(self, chat_id, metadata=None, *, stream_id=None):
|
||||
return await self._end_mock(chat_id, metadata, stream_id=stream_id)
|
||||
|
||||
async def send_file_edit_events(self, chat_id, edits, metadata=None):
|
||||
return await self._file_edit_mock(chat_id, edits, metadata)
|
||||
@@ -94,17 +94,17 @@ def test_websocket_gateway_uses_configured_workspace_restriction(tmp_path, monke
|
||||
@pytest.mark.asyncio
|
||||
async def test_reasoning_delta_routes_to_send_reasoning_delta(manager):
|
||||
channel = manager.channels["mock"]
|
||||
msg = OutboundMessage(
|
||||
msg = outbound_message_for_event(
|
||||
channel="mock",
|
||||
chat_id="c1",
|
||||
content="step-by-step",
|
||||
metadata={"_progress": True, "_reasoning_delta": True, "_stream_id": "r1"},
|
||||
event=ProgressEvent(content="step-by-step", reasoning_delta=True, stream_id="r1"),
|
||||
)
|
||||
await manager._send_once(channel, msg)
|
||||
channel._delta_mock.assert_awaited_once()
|
||||
args = channel._delta_mock.await_args.args
|
||||
assert args[0] == "c1"
|
||||
assert args[1] == "step-by-step"
|
||||
assert channel._delta_mock.await_args.kwargs["stream_id"] == "r1"
|
||||
channel._send_mock.assert_not_awaited()
|
||||
channel._end_mock.assert_not_awaited()
|
||||
|
||||
@@ -112,11 +112,10 @@ async def test_reasoning_delta_routes_to_send_reasoning_delta(manager):
|
||||
@pytest.mark.asyncio
|
||||
async def test_reasoning_end_routes_to_send_reasoning_end(manager):
|
||||
channel = manager.channels["mock"]
|
||||
msg = OutboundMessage(
|
||||
msg = outbound_message_for_event(
|
||||
channel="mock",
|
||||
chat_id="c1",
|
||||
content="",
|
||||
metadata={"_progress": True, "_reasoning_end": True, "_stream_id": "r1"},
|
||||
event=ProgressEvent(reasoning_end=True, stream_id="r1"),
|
||||
)
|
||||
await manager._send_once(channel, msg)
|
||||
channel._end_mock.assert_awaited_once()
|
||||
@@ -124,16 +123,13 @@ async def test_reasoning_end_routes_to_send_reasoning_end(manager):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_legacy_one_shot_reasoning_expands_to_delta_plus_end(manager):
|
||||
"""`_reasoning` (no delta/end pair) falls back through `send_reasoning`
|
||||
which the base class expands to a single delta + end. Hooks that haven't
|
||||
migrated still surface in WebUI as a complete stream segment."""
|
||||
async def test_one_shot_reasoning_expands_to_delta_plus_end(manager):
|
||||
"""One-shot reasoning expands to a single delta + end."""
|
||||
channel = manager.channels["mock"]
|
||||
msg = OutboundMessage(
|
||||
msg = outbound_message_for_event(
|
||||
channel="mock",
|
||||
chat_id="c1",
|
||||
content="one-shot reasoning",
|
||||
metadata={"_progress": True, "_reasoning": True},
|
||||
event=ProgressEvent(content="one-shot reasoning", reasoning=True),
|
||||
)
|
||||
await manager._send_once(channel, msg)
|
||||
channel._delta_mock.assert_awaited_once()
|
||||
@@ -144,11 +140,10 @@ async def test_legacy_one_shot_reasoning_expands_to_delta_plus_end(manager):
|
||||
async def test_dispatch_drops_reasoning_when_channel_opts_out(manager):
|
||||
channel = manager.channels["mock"]
|
||||
channel.show_reasoning = False
|
||||
msg = OutboundMessage(
|
||||
msg = outbound_message_for_event(
|
||||
channel="mock",
|
||||
chat_id="c1",
|
||||
content="hidden thinking",
|
||||
metadata={"_progress": True, "_reasoning_delta": True},
|
||||
event=ProgressEvent(content="hidden thinking", reasoning_delta=True),
|
||||
)
|
||||
await manager.bus.publish_outbound(msg)
|
||||
|
||||
@@ -164,17 +159,15 @@ async def test_dispatch_delivers_reasoning_when_channel_opts_in(manager):
|
||||
channel = manager.channels["mock"]
|
||||
channel.show_reasoning = True
|
||||
for chunk in ("first ", "second"):
|
||||
await manager.bus.publish_outbound(OutboundMessage(
|
||||
await manager.bus.publish_outbound(outbound_message_for_event(
|
||||
channel="mock",
|
||||
chat_id="c1",
|
||||
content=chunk,
|
||||
metadata={"_progress": True, "_reasoning_delta": True, "_stream_id": "r1"},
|
||||
event=ProgressEvent(content=chunk, reasoning_delta=True, stream_id="r1"),
|
||||
))
|
||||
await manager.bus.publish_outbound(OutboundMessage(
|
||||
await manager.bus.publish_outbound(outbound_message_for_event(
|
||||
channel="mock",
|
||||
chat_id="c1",
|
||||
content="",
|
||||
metadata={"_progress": True, "_reasoning_end": True, "_stream_id": "r1"},
|
||||
event=ProgressEvent(reasoning_end=True, stream_id="r1"),
|
||||
))
|
||||
|
||||
await _pump_one(manager)
|
||||
@@ -185,11 +178,10 @@ async def test_dispatch_delivers_reasoning_when_channel_opts_in(manager):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dispatch_silently_drops_reasoning_for_unknown_channel(manager):
|
||||
msg = OutboundMessage(
|
||||
msg = outbound_message_for_event(
|
||||
channel="ghost",
|
||||
chat_id="c1",
|
||||
content="nobody home",
|
||||
metadata={"_progress": True, "_reasoning_delta": True},
|
||||
event=ProgressEvent(content="nobody home", reasoning_delta=True),
|
||||
)
|
||||
await manager.bus.publish_outbound(msg)
|
||||
|
||||
@@ -229,17 +221,34 @@ async def test_base_channel_reasoning_primitives_are_noop_safe():
|
||||
async def test_file_edit_events_route_to_channel_capability(manager):
|
||||
channel = manager.channels["mock"]
|
||||
edits = [{"version": 1, "phase": "start", "path": "src/app.py"}]
|
||||
msg = OutboundMessage(
|
||||
msg = outbound_message_for_event(
|
||||
channel="mock",
|
||||
chat_id="c1",
|
||||
content="",
|
||||
metadata={"_progress": True, "_file_edit_events": edits},
|
||||
event=ProgressEvent(file_edit_events=edits),
|
||||
)
|
||||
|
||||
await manager._send_once(channel, msg)
|
||||
|
||||
channel._file_edit_mock.assert_awaited_once_with(
|
||||
"c1", edits, {"_progress": True, "_file_edit_events": edits}
|
||||
"c1", edits, msg.metadata
|
||||
)
|
||||
channel._send_mock.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_typed_file_edit_event_routes_to_channel_capability(manager):
|
||||
channel = manager.channels["mock"]
|
||||
edits = [{"version": 1, "phase": "start", "path": "src/app.py"}]
|
||||
msg = outbound_message_for_event(
|
||||
channel="mock",
|
||||
chat_id="c1",
|
||||
event=ProgressEvent(file_edit_events=edits),
|
||||
)
|
||||
|
||||
await manager._send_once(channel, msg)
|
||||
|
||||
channel._file_edit_mock.assert_awaited_once_with(
|
||||
"c1", edits, msg.metadata
|
||||
)
|
||||
channel._send_mock.assert_not_awaited()
|
||||
|
||||
@@ -270,11 +279,10 @@ async def test_reasoning_routing_does_not_consult_send_progress(manager):
|
||||
channel = manager.channels["mock"]
|
||||
channel.send_progress = False
|
||||
channel.show_reasoning = True
|
||||
await manager.bus.publish_outbound(OutboundMessage(
|
||||
await manager.bus.publish_outbound(outbound_message_for_event(
|
||||
channel="mock",
|
||||
chat_id="c1",
|
||||
content="still surfaces",
|
||||
metadata={"_progress": True, "_reasoning_delta": True},
|
||||
event=ProgressEvent(content="still surfaces", reasoning_delta=True),
|
||||
))
|
||||
|
||||
await _pump_one(manager)
|
||||
|
||||
Reference in New Issue
Block a user