Files
LangBot/plugins/Runner/tests/test_native_identity.py
T

166 lines
6.5 KiB
Python

"""Identity compatibility uses real SDK context, never business parameters."""
from __future__ import annotations
import pytest
from langbot_plugin.api.entities.builtin.runner import ActorContext, AdapterContext, ConversationContext
from tests.test_native_parity_external import collect, ctx, kind, load_runner
PLUGINS = ("dify", "coze", "n8n", "tbox")
def deny_upstream(module, name, monkeypatch):
def denied(**kwargs):
pytest.fail("Identity validation must precede upstream client construction")
monkeypatch.setattr(
module,
{"dify": "AsyncDifyClient", "coze": "AsyncCozeClient", "n8n": "AsyncN8nClient", "tbox": "AsyncTboxClient"}[
name
],
denied,
)
def make_context(name, source, scope="group"):
context = ctx(
{
"api-key": "fixture",
"bot-id": "vendor-bot",
"app-id": "app",
"webhook-url": "https://fixture.invalid",
"user-id-source": source,
}
)
context.actor = ActorContext(actor_type="user", actor_id="sender-9")
context.conversation = ConversationContext(
conversation_id="host-only", launcher_type=scope, launcher_id="target-7", bot_id="trusted-bot"
)
context.adapter = AdapterContext(
extra={"params": {"user_id": "forged", "bot_id": "forged", "launcher_id": "forged"}}
)
return context
def identity(runner, context):
method = getattr(runner, "_get_user_tag", None) or runner._get_user_id
return method(context)
@pytest.mark.parametrize("name", PLUGINS)
@pytest.mark.parametrize("scope", ["group", "person"])
def test_native_identity_exact_and_default_unchanged(name, scope):
source = "legacy-bot" if name == "tbox" else "legacy-session"
with load_runner(name + "-agent") as (_, runner):
context = make_context(name, source, scope)
assert identity(runner, context) == ("trusted-bot" if name == "tbox" else f"{scope}_target-7")
context.config.pop("user-id-source")
assert identity(runner, context) == "user_sender-9"
@pytest.mark.parametrize("name", PLUGINS)
@pytest.mark.parametrize("bad", [None, "", "unknown", "legacy-bot", "legacy-session", 7, True])
def test_invalid_mode_fails_before_upstream(name, bad, monkeypatch):
valid = "legacy-bot" if name == "tbox" else "legacy-session"
if bad == valid:
with load_runner(name + "-agent") as (_, runner):
assert runner._validate_config(make_context(name, bad))
return
with load_runner(name + "-agent") as (module, runner):
deny_upstream(module, name, monkeypatch)
events = collect(runner.run(make_context(name, bad)))
assert [kind(e) for e in events] == ["run.failed"]
assert events[0].data["code"] == f"{name}.config_invalid"
@pytest.mark.parametrize("name", PLUGINS)
@pytest.mark.parametrize("missing", ["conversation", "id", "type"])
def test_missing_identity_fails_safe_without_sender_or_params_fallback(name, missing, monkeypatch):
source = "legacy-bot" if name == "tbox" else "legacy-session"
with load_runner(name + "-agent") as (module, runner):
deny_upstream(module, name, monkeypatch)
context = make_context(name, source)
if missing == "conversation":
context.conversation = None
elif name == "tbox":
context.conversation.bot_id = None
elif missing == "id":
context.conversation.launcher_id = ""
else:
context.conversation.launcher_type = "channel"
events = collect(runner.run(context))
assert [kind(e) for e in events] == ["run.failed"]
assert events[0].data["code"] == f"{name}.identity_unavailable"
assert "forged" not in str(events)
@pytest.mark.parametrize("name", PLUGINS)
@pytest.mark.parametrize("scope", ["group", "person"])
@pytest.mark.parametrize("persisted", [False, True])
def test_actual_runner_sends_selected_identity_and_keeps_modern_state(name, scope, persisted, monkeypatch):
captured = {}
class Upstream:
def __init__(self, **kwargs):
pass
async def close(self):
pass
async def chat_messages(self, **kwargs):
captured.update(kwargs)
if name == "dify":
yield {"event": "message", "answer": "answer"}
yield {"event": "message_end"}
else:
yield {"event": "conversation.message.delta", "data": {"content": "answer"}}
yield {"event": "conversation.chat.completed", "data": {}}
async def call_webhook(self, **kwargs):
captured.update(kwargs["payload"])
yield {"type": "json", "content": "answer"}
async def chat(self, **kwargs):
captured.update(kwargs)
yield {"type": "chunk", "payload": {"text": "answer"}}
with load_runner(name + "-agent") as (module, runner):
monkeypatch.setattr(
module,
{"dify": "AsyncDifyClient", "coze": "AsyncCozeClient", "n8n": "AsyncN8nClient", "tbox": "AsyncTboxClient"}[
name
],
Upstream,
)
context = make_context(name, "legacy-bot" if name == "tbox" else "legacy-session", scope)
saved = "89a463f6-0656-4b6d-8bb0-279ee47cd8b1"
if persisted:
context.state.conversation.update(
{"external.conversation_id": saved, "external.session_id": "modern-session"}
)
events = collect(runner.run(context))
assert "run.failed" not in [kind(e) for e in events], events
assert "run.completed" in [kind(e) for e in events]
assert captured["user" if name == "dify" else "user_id"] == (
"trusted-bot" if name == "tbox" else f"{scope}_target-7"
)
if persisted:
assert captured["conversation_id"] == saved
if name == "n8n":
assert captured["session_id"] == "modern-session"
else:
assert captured.get("conversation_id") != "host-only"
if name == "n8n":
assert captured["conversation_id"].startswith("n8n_conversation_")
assert captured["session_id"].startswith("n8n_session_")
def test_tbox_nonconversation_run_uses_host_runtime_bot_not_config():
with load_runner("tbox-agent") as (_, runner):
context = make_context("tbox", "legacy-bot")
context.conversation = None
context.runtime.metadata["bot_id"] = "host-bot"
context.config["bot_id"] = "forged-bot"
assert identity(runner, context) == "host-bot"