fix(gateway): route /background result media by type

Background-task (/background, /btw) result media now routes to the
type-specific sender — TTS clip → voice bubble, video → send_video,
image → send_image_file — instead of forcing everything through
send_document. Mirrors the streaming + kanban delivery paths and
reuses base.should_send_media_as_audio for the Telegram OGG nuance.

Co-authored-by: LJ Li <liliangjya@gmail.com>
Co-authored-by: Kolektori <256073454+Kolektori@users.noreply.github.com>
This commit is contained in:
teknium1
2026-06-02 16:55:25 -07:00
committed by Teknium
co-authored by LJ Li Kolektori
parent 30a7a94120
commit 082025abcd
3 changed files with 116 additions and 6 deletions
+82
View File
@@ -267,6 +267,88 @@ class TestRunBackgroundTask:
mock_agent_instance.shutdown_memory_provider.assert_called_once()
mock_agent_instance.close.assert_called_once()
@pytest.mark.asyncio
async def test_media_files_routed_by_type(self, monkeypatch):
"""Result media is routed to the type-specific sender, not send_document.
A TTS clip should arrive as a voice bubble, a video as a video, an
image as a native image, and everything else as a document.
"""
from gateway import run as gateway_run
runner = _make_runner()
runner._resolve_session_agent_runtime = MagicMock(
return_value=("test-model", {"api_key": "test-key"})
)
runner._resolve_session_reasoning_config = MagicMock(return_value=None)
runner._load_service_tier = MagicMock(return_value=None)
runner._resolve_turn_agent_config = MagicMock(
return_value={
"model": "test-model",
"runtime": {"api_key": "test-key"},
"request_overrides": None,
}
)
runner._run_in_executor_with_context = AsyncMock(
return_value={"final_response": "see attached", "messages": []}
)
monkeypatch.setattr(gateway_run, "_load_gateway_config", lambda: {})
# Four real files so the media-delivery path validator accepts them
# (default mode requires the file to exist as a regular file).
import os as _os
import tempfile as _tempfile
_tmpdir = _tempfile.mkdtemp(prefix="bg_media_")
_ogg = _os.path.join(_tmpdir, "clip.ogg")
_mp4 = _os.path.join(_tmpdir, "render.mp4")
_png = _os.path.join(_tmpdir, "chart.png")
_pdf = _os.path.join(_tmpdir, "report.pdf")
for _p in (_ogg, _mp4, _png, _pdf):
with open(_p, "wb") as _fh:
_fh.write(b"x")
# ogg flagged as voice, mp4 video, png image, pdf doc.
media = [
(_ogg, True),
(_mp4, False),
(_png, False),
(_pdf, False),
]
mock_adapter = AsyncMock()
mock_adapter.send = AsyncMock()
mock_adapter.send_voice = AsyncMock()
mock_adapter.send_video = AsyncMock()
mock_adapter.send_image_file = AsyncMock()
mock_adapter.send_document = AsyncMock()
mock_adapter.send_image = AsyncMock()
# No text, no markdown images — just the four media attachments.
mock_adapter.extract_media = MagicMock(return_value=(media, ""))
mock_adapter.extract_images = MagicMock(return_value=([], ""))
# Non-telegram platform so every audio ext routes through send_voice.
runner.adapters[Platform.DISCORD] = mock_adapter
source = SessionSource(
platform=Platform.DISCORD,
user_id="12345",
chat_id="67890",
user_name="testuser",
)
try:
await runner._run_background_task("make stuff", source, "bg_test")
mock_adapter.send_voice.assert_called_once()
assert mock_adapter.send_voice.call_args.kwargs["audio_path"] == _ogg
mock_adapter.send_video.assert_called_once()
assert mock_adapter.send_video.call_args.kwargs["video_path"] == _mp4
mock_adapter.send_image_file.assert_called_once()
assert mock_adapter.send_image_file.call_args.kwargs["image_path"] == _png
mock_adapter.send_document.assert_called_once()
assert mock_adapter.send_document.call_args.kwargs["file_path"] == _pdf
finally:
import shutil as _shutil
_shutil.rmtree(_tmpdir, ignore_errors=True)
@pytest.mark.asyncio
async def test_telegram_dm_topic_completion_preserves_reply_anchor_metadata(self, monkeypatch):
"""Background completion metadata must let Telegram send thread id plus reply id."""