mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-30 20:35:22 +08:00
149 lines
5.2 KiB
Python
149 lines
5.2 KiB
Python
"""Two installations drive one AutoTranslate object graph under shared-runtime-v1.
|
|
|
|
AutoTranslate relies on the stateless component model: the ``Translator`` listener
|
|
reads ``plugin.get_config()`` and resolves the LLM model for every event, so one
|
|
shared plugin/listener serves every installation. These tests drive the same
|
|
component object across two distinct ``InstallationBinding`` values.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import sys
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
from langbot_plugin.api.entities.builtin.platform import message as platform_message
|
|
from langbot_plugin.api.proxies.invocation import bind_invocation
|
|
from langbot_plugin.entities.io.context import InstallationBinding
|
|
|
|
PLUGIN_ROOT = Path(__file__).resolve().parents[1]
|
|
if str(PLUGIN_ROOT) not in sys.path:
|
|
sys.path.insert(0, str(PLUGIN_ROOT))
|
|
|
|
import components.event_listener.translator as translator_mod # noqa: E402
|
|
from main import AutoTranslate # noqa: E402
|
|
|
|
|
|
def _binding(
|
|
installation: str,
|
|
*,
|
|
workspace: str = "workspace-1",
|
|
instance: str = "instance-1",
|
|
revision: int = 1,
|
|
) -> InstallationBinding:
|
|
return InstallationBinding(
|
|
instance_uuid=instance,
|
|
workspace_uuid=workspace,
|
|
installation_uuid=installation,
|
|
runtime_revision=revision,
|
|
artifact_digest="a" * 64,
|
|
)
|
|
|
|
|
|
def _wire():
|
|
"""Mimic the runtime object graph: one plugin and one shared listener."""
|
|
plugin = AutoTranslate()
|
|
handler = SimpleNamespace()
|
|
plugin.plugin_runtime_handler = handler
|
|
listener = translator_mod.Translator()
|
|
listener.plugin = plugin
|
|
return plugin, handler, listener
|
|
|
|
|
|
def _make_context(text: str) -> SimpleNamespace:
|
|
context = SimpleNamespace(
|
|
event=SimpleNamespace(message_chain=[platform_message.Plain(text=text)]),
|
|
replies=[],
|
|
)
|
|
|
|
async def reply(message_chain):
|
|
context.replies.append(message_chain)
|
|
|
|
context.reply = reply
|
|
return context
|
|
|
|
|
|
def test_config_and_model_are_resolved_per_event(monkeypatch):
|
|
plugin, handler, listener = _wire()
|
|
calls: list[tuple[str, str, str]] = []
|
|
|
|
async def fake_translate(text: str, model_uuid: str, target_lang: str):
|
|
calls.append((text, model_uuid, target_lang))
|
|
return f"{target_lang}:{text}"
|
|
|
|
monkeypatch.setattr(plugin, "translate", fake_translate)
|
|
config_a = {"target_language": "en_US", "model": "model-a", "min_text_length": 4}
|
|
config_b = {"target_language": "ja_JP", "model": "model-b", "min_text_length": 4}
|
|
|
|
async def dispatch(context: SimpleNamespace, binding: InstallationBinding, config: dict) -> None:
|
|
with bind_invocation(handler, config=config, binding=binding):
|
|
await listener._handle_message(context, is_group=True)
|
|
|
|
context_a = _make_context("Bonjour tout le monde")
|
|
context_b = _make_context("Bonjour tout le monde")
|
|
asyncio.run(dispatch(context_a, _binding("installation-a"), config_a))
|
|
asyncio.run(dispatch(context_b, _binding("installation-b"), config_b))
|
|
|
|
assert calls == [
|
|
("Bonjour tout le monde", "model-a", "en_US"),
|
|
("Bonjour tout le monde", "model-b", "ja_JP"),
|
|
]
|
|
assert context_a.replies[0][0].text == "🌐 en_US:Bonjour tout le monde"
|
|
assert context_b.replies[0][0].text == "🌐 ja_JP:Bonjour tout le monde"
|
|
# Nothing from the invocation was stored on the shared listener or plugin.
|
|
assert set(vars(listener)) == {"registered_handlers", "plugin"}
|
|
assert plugin._legacy_config == {}
|
|
|
|
|
|
def test_private_gating_and_model_fallback_are_per_invocation(monkeypatch):
|
|
plugin, handler, listener = _wire()
|
|
calls: list[tuple[str, str, str]] = []
|
|
|
|
async def fake_translate(text: str, model_uuid: str, target_lang: str):
|
|
calls.append((text, model_uuid, target_lang))
|
|
return f"{target_lang}:{text}"
|
|
|
|
async def fake_models():
|
|
return ["fallback-model"]
|
|
|
|
monkeypatch.setattr(plugin, "translate", fake_translate)
|
|
monkeypatch.setattr(plugin, "get_llm_models", fake_models)
|
|
|
|
async def dispatch(context, binding, config, *, is_group):
|
|
with bind_invocation(handler, config=config, binding=binding):
|
|
await listener._handle_message(context, is_group=is_group)
|
|
|
|
# installation-a disables private chat: a private message is ignored.
|
|
private_a = _make_context("Bonjour tout le monde")
|
|
asyncio.run(
|
|
dispatch(
|
|
private_a,
|
|
_binding("installation-a"),
|
|
{"target_language": "en_US", "enable_private": False, "min_text_length": 4},
|
|
is_group=False,
|
|
)
|
|
)
|
|
assert calls == []
|
|
assert private_a.replies == []
|
|
|
|
# installation-b enables private chat and declares no model: it falls back to
|
|
# the invocation's first available model.
|
|
private_b = _make_context("Bonjour tout le monde")
|
|
asyncio.run(
|
|
dispatch(
|
|
private_b,
|
|
_binding("installation-b"),
|
|
{"target_language": "en_US", "enable_private": True, "min_text_length": 4},
|
|
is_group=False,
|
|
)
|
|
)
|
|
assert calls == [("Bonjour tout le monde", "fallback-model", "en_US")]
|
|
assert private_b.replies[0][0].text == "🌐 en_US:Bonjour tout le monde"
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import pytest
|
|
|
|
raise SystemExit(pytest.main([__file__, "-q"]))
|