Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions src/untether/telegram/commands/media.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

if TYPE_CHECKING:
from ..bridge import TelegramBridgeConfig
from ..chat_prefs import ChatPrefsStore


async def _handle_media_group(
Expand All @@ -37,6 +38,7 @@ async def _handle_media_group(
Awaitable[ResolvedMessage | None],
]
| None = None,
chat_prefs: ChatPrefsStore | None = None,
) -> None:
if not messages:
return
Expand All @@ -47,15 +49,25 @@ async def _handle_media_group(
)
reply = make_reply(cfg, command_msg)
topic_key = _topic_key(command_msg, cfg) if topic_store is not None else None
# keep in sync with loop.py:build_message_context — same ambient-context
# fallback ladder: topic-bound → chat_prefs chat-bound → topic-merged default.
chat_project = _topics_chat_project(cfg, command_msg.chat_id)
bound_context = (
await topic_store.get_context(*topic_key)
if topic_store is not None and topic_key is not None
else None
)
ambient_context = _merge_topic_context(
chat_project=chat_project, bound=bound_context
)
chat_bound_context = None
if chat_prefs is not None:
chat_bound_context = await chat_prefs.get_context(command_msg.chat_id)
if bound_context is not None:
ambient_context = _merge_topic_context(
chat_project=chat_project, bound=bound_context
)
elif chat_bound_context is not None:
ambient_context = chat_bound_context
else:
ambient_context = _merge_topic_context(chat_project=chat_project, bound=None)
command_id, args_text = _parse_slash_command(command_msg.text)
if command_id == "file":
command, rest, error = parse_file_command(args_text)
Expand Down
1 change: 1 addition & 0 deletions src/untether/telegram/loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -1102,6 +1102,7 @@ async def _flush_media_group(self, key: tuple[int, str]) -> None:
self._topic_store,
self._run_prompt_from_upload,
self._resolve_prompt_message,
chat_prefs=self._chat_prefs,
)
logger.debug(
"media_group.flush.ok",
Expand Down
43 changes: 43 additions & 0 deletions tests/test_telegram_media_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,49 @@ async def _fake_handle(*_args, **_kwargs) -> None:
assert calls["count"] == 1


class _FakeChatPrefs:
"""Minimal ChatPrefsStore stub — only ``get_context`` is exercised."""

def __init__(self, context: RunContext | None) -> None:
self._context = context

async def get_context(self, _chat_id: int) -> RunContext | None:
return self._context


@pytest.mark.anyio
async def test_media_group_uses_chat_prefs_bound_context(monkeypatch) -> None:
"""Media-group uploads must fall back to chat_prefs-bound context.

Regression for the channelo bug: with no topic binding, no default_project
and no chat_map entry, a media group resolved ambient_context=None and
failed with "no project context available". The single-file path already
consulted chat_prefs; the media-group path did not.
"""
transport = FakeTransport()
cfg = replace(make_cfg(transport), files=TelegramFilesSettings(enabled=True))
msg = _msg("")
captured: dict[str, RunContext | None] = {}

async def _fake_handle(
_cfg, _msg, _rest, _ordered, ambient_context, _topic_store
) -> None:
captured["ambient_context"] = ambient_context

monkeypatch.setattr(media_commands, "_handle_file_put_group", _fake_handle)

await media_commands._handle_media_group(
cfg,
[msg],
topic_store=None,
chat_prefs=_FakeChatPrefs(RunContext(project="auditor-toolkit")),
)

ambient = captured["ambient_context"]
assert ambient is not None
assert ambient.project == "auditor-toolkit"


@pytest.mark.anyio
async def test_media_group_auto_put_prompt_resolve_none(monkeypatch) -> None:
transport = FakeTransport()
Expand Down
Loading