fix(webui): render local CLI image artifacts

This commit is contained in:
Xubin Ren
2026-05-24 19:43:20 +08:00
parent 9efdce276f
commit c9ff64fc0f
13 changed files with 461 additions and 10 deletions
+32
View File
@@ -480,6 +480,38 @@ async def test_send_delta_emits_delta_and_stream_end() -> None:
assert second["stream_id"] == "sid"
@pytest.mark.asyncio
async def test_send_delta_stream_end_rewrites_local_markdown_image(monkeypatch, tmp_path) -> None:
bus = MagicMock()
workspace = tmp_path / "workspace"
workspace.mkdir()
(workspace / "diagram.png").write_bytes(b"\x89PNG\r\n\x1a\nimage")
media = tmp_path / "media"
def fake_media_dir(channel: str | None = None):
path = media / channel if channel else media
path.mkdir(parents=True, exist_ok=True)
return path
monkeypatch.setattr("nanobot.channels.websocket.get_media_dir", fake_media_dir)
channel = WebSocketChannel(
{"enabled": True, "allowFrom": ["*"], "streaming": True},
bus,
workspace_path=workspace,
)
mock_ws = AsyncMock()
channel._attach(mock_ws, "chat-1")
await channel.send_delta("chat-1", "![Diagram](", {"_stream_delta": True, "_stream_id": "sid"})
await channel.send_delta("chat-1", "diagram.png)", {"_stream_delta": True, "_stream_id": "sid"})
await channel.send_delta("chat-1", "", {"_stream_end": True, "_stream_id": "sid"})
assert mock_ws.send.await_count == 3
final = json.loads(mock_ws.send.call_args_list[2][0][0])
assert final["event"] == "stream_end"
assert final["text"].startswith("![Diagram](/api/media/")
@pytest.mark.asyncio
async def test_send_reasoning_delta_emits_streaming_frame() -> None:
bus = MagicMock()
@@ -44,6 +44,7 @@ def _ch(
bus: Any,
*,
session_manager: SessionManager | None = None,
workspace_path: Path | None = None,
port: int,
) -> WebSocketChannel:
return WebSocketChannel(
@@ -57,6 +58,7 @@ def _ch(
},
bus,
session_manager=session_manager,
workspace_path=workspace_path,
)
@@ -67,6 +69,15 @@ def bus() -> MagicMock:
return b
def _fake_media_dir(root: Path):
def inner(channel: str | None = None) -> Path:
path = root / channel if channel else root
path.mkdir(parents=True, exist_ok=True)
return path
return inner
async def _http_get(
url: str, headers: dict[str, str] | None = None
) -> httpx.Response:
@@ -123,6 +134,45 @@ def test_sign_media_path_round_trips_via_hmac(
assert _b64url_decode(payload).decode() == "a.png"
def test_local_markdown_image_is_staged_and_rewritten(
bus: MagicMock,
tmp_path: Path,
) -> None:
workspace = tmp_path / "workspace"
workspace.mkdir()
(workspace / "demo_arch.png").write_bytes(_PNG_BYTES)
media = tmp_path / "media"
channel = _ch(bus, workspace_path=workspace, port=0)
with patch("nanobot.channels.websocket.get_media_dir", side_effect=_fake_media_dir(media)):
rewritten = channel._rewrite_local_markdown_images(
"The result:\n![Cloud Architecture Diagram](demo_arch.png)"
)
assert "![Cloud Architecture Diagram](/api/media/" in rewritten
staged = list((media / "websocket").iterdir())
assert len(staged) == 1
assert staged[0].read_bytes() == _PNG_BYTES
def test_local_markdown_image_rejects_workspace_escape(
bus: MagicMock,
tmp_path: Path,
) -> None:
workspace = tmp_path / "workspace"
workspace.mkdir()
outside = tmp_path / "outside.png"
outside.write_bytes(_PNG_BYTES)
media = tmp_path / "media"
channel = _ch(bus, workspace_path=workspace, port=0)
text = "![nope](../outside.png)"
with patch("nanobot.channels.websocket.get_media_dir", side_effect=_fake_media_dir(media)):
assert channel._rewrite_local_markdown_images(text) == text
assert not (media / "websocket").exists()
# ---------------------------------------------------------------------------
# /api/media/<sig>/<payload>: the serving handler
# ---------------------------------------------------------------------------