Files
LangBot/plugins/misc/HelloPlugin/tests/test_shared_runtime.py
T

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"]))