hermes-agent/tests/gateway/test_feishu_channel_prompts.py
teknium1 36bfe3a449 fix(anthropic+feishu): model-gate max_tokens fallback; wire Feishu channel_prompt
Two independent fixes salvaged from #12811 (closing it; one of its three
bundled fixes — Discord free_response — is already on main).

Anthropic max_tokens (#12790): the chat-completions max_tokens fallback only
fired for OpenRouter/Nous URLs, so any other proxy serving a Claude model
(AWS Bedrock, NVIDIA, LiteLLM, vLLM, corporate gateways) shipped requests
with no max_tokens and inherited the proxy's low default (Bedrock: 4096),
exhausting on thinking + large tool calls. Changed the gate in
chat_completion_helpers.build_api_kwargs from URL-gated to model-gated:
fires whenever the model matches an _ANTHROPIC_OUTPUT_LIMITS key. This also
fixes a latent miss — the old 'claude' substring gate skipped MiniMax and
Qwen3 even on OpenRouter. Remains a last-resort fallback (build_kwargs only
applies it after ephemeral/user/profile max_tokens), so it never overrides
an explicit value, and only touches the chat-completions transport (native
Anthropic Messages API is a separate path).

Feishu channel_prompt (#12805): the Feishu adapter never resolved
channel_prompts config, unlike Discord/Slack, so per-channel role prompts
were silently ignored. Added _resolve_channel_prompt() (delegating to the
shared gateway.platforms.base.resolve_channel_prompt) and wired it into all
three MessageEvent construction sites — inbound message, reaction routing,
and card-action routing.

Tests: tests/gateway/test_feishu_channel_prompts.py (6 cases) covering exact
match, parent-thread fallback, no-match, missing-config safety, and event
propagation.
2026-06-30 17:20:41 -07:00

88 lines
3.2 KiB
Python

"""Tests for Feishu per-channel prompt resolution.
Feishu previously ignored ``channel_prompts`` config (unlike Discord/Slack).
These tests verify that ``_resolve_channel_prompt`` reads the adapter's
``config.extra`` and that the resolved prompt is attached to the dispatched
``MessageEvent`` for the inbound, reaction, and card-action paths.
"""
import asyncio
import json
from types import SimpleNamespace
from unittest.mock import AsyncMock, Mock
from gateway.config import PlatformConfig
def _build_adapter(extra=None):
from plugins.platforms.feishu.adapter import FeishuAdapter
adapter = FeishuAdapter.__new__(FeishuAdapter)
adapter.config = PlatformConfig(extra=extra or {})
adapter._bot_open_id = "ou_bot"
adapter._bot_user_id = ""
adapter._bot_name = "Hermes"
adapter._download_feishu_message_resources = AsyncMock(return_value=([], []))
adapter._fetch_message_text = AsyncMock(return_value=None)
adapter.get_chat_info = AsyncMock(return_value={"name": "Test Chat"})
adapter._resolve_sender_profile = AsyncMock(
return_value={"user_id": "u1", "user_name": "Alice", "user_id_alt": None}
)
adapter._resolve_source_chat_type = Mock(return_value="group")
adapter.build_source = Mock(return_value=SimpleNamespace(thread_id=None))
adapter._dispatch_inbound_event = AsyncMock()
return adapter
def _run_inbound(adapter, chat_id="oc_chat"):
message = SimpleNamespace(
content=json.dumps({"text": "plain message"}),
message_type="text",
message_id="m",
mentions=[],
chat_id=chat_id,
parent_id=None,
upper_message_id=None,
thread_id=None,
)
asyncio.run(
adapter._process_inbound_message(
data=message, message=message, sender_id=None, chat_type="group", message_id="m",
)
)
return adapter._dispatch_inbound_event.call_args.args[0]
def test_resolve_channel_prompt_exact_match():
adapter = _build_adapter({"channel_prompts": {"oc_chat": "Be terse."}})
assert adapter._resolve_channel_prompt("oc_chat") == "Be terse."
def test_resolve_channel_prompt_parent_fallback():
adapter = _build_adapter({"channel_prompts": {"oc_parent": "Inherit me."}})
assert adapter._resolve_channel_prompt("oc_thread", "oc_parent") == "Inherit me."
def test_resolve_channel_prompt_no_match_returns_none():
adapter = _build_adapter({"channel_prompts": {"oc_other": "Nope."}})
assert adapter._resolve_channel_prompt("oc_chat") is None
def test_resolve_channel_prompt_missing_config_is_safe():
# __new__ adapter without a config attribute (defensive getattr path).
from plugins.platforms.feishu.adapter import FeishuAdapter
bare = FeishuAdapter.__new__(FeishuAdapter)
assert bare._resolve_channel_prompt("oc_chat") is None
def test_inbound_event_carries_channel_prompt():
adapter = _build_adapter({"channel_prompts": {"oc_chat": "Feishu role prompt."}})
event = _run_inbound(adapter, chat_id="oc_chat")
assert event.channel_prompt == "Feishu role prompt."
def test_inbound_event_no_prompt_when_unconfigured():
adapter = _build_adapter({"channel_prompts": {"oc_other": "Different chat."}})
event = _run_inbound(adapter, chat_id="oc_chat")
assert event.channel_prompt is None