mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-30 12:25:29 +08:00
112 lines
4.2 KiB
Python
112 lines
4.2 KiB
Python
"""Two installations drive one HelloPlugin object graph under shared-runtime-v1.
|
|
|
|
HelloPlugin relies on the stateless component model: one ``HelloPlugin`` and one
|
|
object per declared component serve every installation of the artifact digest.
|
|
These tests instantiate the real plugin/component classes, attach the same
|
|
component object to a task-local invocation for two distinct
|
|
``InstallationBinding`` values, and assert that each call builds everything from
|
|
its arguments and the invocation, retaining no tenant state on the shared object.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import sys
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
from langbot_plugin.api.entities import events
|
|
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.default as default_mod # noqa: E402
|
|
import components.tools.get_weather_alerts as weather_mod # noqa: E402
|
|
from main import HelloPlugin # 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(component):
|
|
"""Mimic the runtime object graph: one plugin and one shared component."""
|
|
plugin = HelloPlugin()
|
|
handler = SimpleNamespace()
|
|
plugin.plugin_runtime_handler = handler
|
|
component.plugin = plugin
|
|
return plugin, handler, component
|
|
|
|
|
|
def test_tool_is_stateless_and_resolves_config_per_invocation(monkeypatch):
|
|
_plugin, handler, tool = _wire(weather_mod.GetWeatherAlerts())
|
|
seen_urls: list[str] = []
|
|
|
|
async def fake_request(url: str):
|
|
seen_urls.append(url)
|
|
return {"features": [{"properties": {"event": url.rsplit("/", 1)[-1]}}]}
|
|
|
|
monkeypatch.setattr(weather_mod, "make_nws_request", fake_request)
|
|
|
|
async def call(state: str, binding: InstallationBinding, config: dict) -> str:
|
|
with bind_invocation(handler, config=config, binding=binding):
|
|
# The same shared object yields the config of the active installation.
|
|
assert tool.get_plugin_config() == config
|
|
return await tool.call({"state": state})
|
|
|
|
first = asyncio.run(call("CA", _binding("installation-a"), {"marker": "a"}))
|
|
second = asyncio.run(call("NY", _binding("installation-b"), {"marker": "b"}))
|
|
|
|
assert seen_urls == [
|
|
"https://api.weather.gov/alerts/active/area/CA",
|
|
"https://api.weather.gov/alerts/active/area/NY",
|
|
]
|
|
assert "CA" in first and "NY" in second
|
|
assert "NY" not in first and "CA" not in second
|
|
# No per-invocation or tenant value was copied onto the shared component.
|
|
assert set(vars(tool)) == {"plugin"}
|
|
|
|
|
|
def test_listener_handlers_mutate_only_the_per_event_context():
|
|
_plugin, handler, listener = _wire(default_mod.DefaultEventListener())
|
|
|
|
def make_context() -> SimpleNamespace:
|
|
return SimpleNamespace(event=SimpleNamespace(user_message_alter=None))
|
|
|
|
async def dispatch(event_context: SimpleNamespace, binding: InstallationBinding) -> None:
|
|
with bind_invocation(handler, config={}, binding=binding):
|
|
for registered in listener.registered_handlers[events.GroupNormalMessageReceived]:
|
|
await registered(event_context)
|
|
|
|
context_a = make_context()
|
|
context_b = make_context()
|
|
asyncio.run(dispatch(context_a, _binding("installation-a")))
|
|
asyncio.run(dispatch(context_b, _binding("installation-b")))
|
|
|
|
assert context_a.event.user_message_alter.text == "Hello from LangBot Plugin!"
|
|
assert context_b.event.user_message_alter.text == "Hello from LangBot Plugin!"
|
|
assert context_a.event.user_message_alter is not context_b.event.user_message_alter
|
|
# The listener holds only its handler registry and plugin reference.
|
|
assert set(vars(listener)) == {"registered_handlers", "plugin"}
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import pytest
|
|
|
|
raise SystemExit(pytest.main([__file__, "-q"]))
|