mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-09 04:40:57 +00:00
chore(merge): sync master into dev/4.11.x
This commit is contained in:
@@ -3,6 +3,47 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from langbot_plugin.entities.io.context import InstallationBinding
|
||||
|
||||
|
||||
TEST_RUNTIME_BINDING = InstallationBinding(
|
||||
instance_uuid='instance-test',
|
||||
workspace_uuid='workspace-test',
|
||||
placement_generation=1,
|
||||
installation_uuid='00000000-0000-4000-8000-000000000001',
|
||||
runtime_revision=1,
|
||||
artifact_digest='a' * 64,
|
||||
)
|
||||
|
||||
|
||||
def bind_runtime_action_context(
|
||||
handler,
|
||||
application,
|
||||
*,
|
||||
plugin_identity: str = 'test/runner',
|
||||
):
|
||||
"""Simulate the trusted Runtime envelope used around direct action calls."""
|
||||
|
||||
application.workspace_service = SimpleNamespace(
|
||||
get_execution_binding=AsyncMock(
|
||||
return_value=SimpleNamespace(
|
||||
instance_uuid=TEST_RUNTIME_BINDING.instance_uuid,
|
||||
workspace_uuid=TEST_RUNTIME_BINDING.workspace_uuid,
|
||||
placement_generation=TEST_RUNTIME_BINDING.placement_generation,
|
||||
)
|
||||
)
|
||||
)
|
||||
plugin_author, plugin_name = plugin_identity.split('/', 1)
|
||||
handler.register_installation_binding(
|
||||
TEST_RUNTIME_BINDING,
|
||||
plugin_author=plugin_author,
|
||||
plugin_name=plugin_name,
|
||||
)
|
||||
handler._current_action_context.set(TEST_RUNTIME_BINDING)
|
||||
return handler
|
||||
|
||||
|
||||
def make_resources(
|
||||
|
||||
@@ -350,6 +350,7 @@ def mock_query():
|
||||
"""Create a mock query for testing."""
|
||||
query = Mock()
|
||||
query.query_id = 123
|
||||
query.workspace_uuid = 'workspace-test'
|
||||
query.bot_uuid = 'bot-uuid-123'
|
||||
query.pipeline_uuid = 'pipeline-uuid-456'
|
||||
query.launcher_type = Mock(value='person')
|
||||
@@ -398,6 +399,7 @@ def mock_query_no_session():
|
||||
"""Create a mock Query without session."""
|
||||
query = Mock()
|
||||
query.query_id = 456
|
||||
query.workspace_uuid = 'workspace-test'
|
||||
query.bot_uuid = 'bot-uuid-456'
|
||||
query.pipeline_uuid = 'pipeline-uuid-789'
|
||||
query.launcher_type = Mock(value='person')
|
||||
|
||||
@@ -19,6 +19,7 @@ from unittest.mock import AsyncMock, MagicMock
|
||||
from langbot.pkg.agent.runner.descriptor import AgentRunnerDescriptor
|
||||
from langbot.pkg.agent.runner.session_registry import AgentRunSessionRegistry
|
||||
from langbot.pkg.plugin.handler import _get_pipeline_knowledge_base_uuids
|
||||
from langbot.pkg.api.http.context import ExecutionContext
|
||||
|
||||
# Import shared test fixtures from conftest.py
|
||||
from .conftest import make_resources, make_session
|
||||
@@ -69,6 +70,16 @@ class MockQuery:
|
||||
self.session.launcher_id = 'group_123'
|
||||
self.sender_id = 'user_001'
|
||||
self.bot_uuid = 'bot_001'
|
||||
self.pipeline_uuid = 'pipeline-001'
|
||||
self.query_uuid = f'query-{query_id}'
|
||||
self._execution_context = ExecutionContext(
|
||||
instance_uuid='instance-test',
|
||||
workspace_uuid='workspace-test',
|
||||
placement_generation=1,
|
||||
bot_uuid=self.bot_uuid,
|
||||
pipeline_uuid=self.pipeline_uuid,
|
||||
query_uuid=self.query_uuid,
|
||||
)
|
||||
self.pipeline_config = {
|
||||
'ai': {
|
||||
'runner': {
|
||||
@@ -112,7 +123,7 @@ class MockApplication:
|
||||
|
||||
|
||||
class FakeAgentRunnerRegistry:
|
||||
async def get(self, runner_id, bound_plugins=None):
|
||||
async def get(self, context, runner_id, bound_plugins=None):
|
||||
return AgentRunnerDescriptor(
|
||||
id=runner_id,
|
||||
source='plugin',
|
||||
@@ -306,19 +317,19 @@ async def test_tool_manager_get_tool_detail_returns_uniform_schema():
|
||||
|
||||
mgr = ToolManager.__new__(ToolManager)
|
||||
|
||||
async def fake_get_tool_by_name(name):
|
||||
async def fake_get_tool_by_name(context, name):
|
||||
return tool if name == 'search' else None
|
||||
|
||||
mgr.get_tool_by_name = fake_get_tool_by_name
|
||||
|
||||
detail = await mgr.get_tool_detail('search')
|
||||
detail = await mgr.get_tool_detail('workspace-test', 'search')
|
||||
assert detail == {
|
||||
'name': 'search',
|
||||
'description': 'Search test data',
|
||||
'human_desc': 'Search public data',
|
||||
'parameters': {'type': 'object', 'properties': {'q': {'type': 'string'}}},
|
||||
}
|
||||
assert await mgr.get_tool_detail('missing') is None
|
||||
assert await mgr.get_tool_detail('workspace-test', 'missing') is None
|
||||
|
||||
|
||||
class TestCallToolAuthorization:
|
||||
|
||||
@@ -17,7 +17,7 @@ from langbot_plugin.api.entities.builtin.agent_runner.page_results import (
|
||||
)
|
||||
from langbot_plugin.entities.io.actions.enums import PluginToRuntimeAction
|
||||
|
||||
from .conftest import make_resources
|
||||
from .conftest import bind_runtime_action_context, make_resources
|
||||
|
||||
|
||||
class FakeConnection:
|
||||
@@ -56,7 +56,10 @@ def _handler(db_engine, session_registry):
|
||||
return True
|
||||
|
||||
fake_app = FakeApplication(db_engine)
|
||||
return RuntimeConnectionHandler(FakeConnection(), fake_disconnect, fake_app)
|
||||
return bind_runtime_action_context(
|
||||
RuntimeConnectionHandler(FakeConnection(), fake_disconnect, fake_app),
|
||||
fake_app,
|
||||
)
|
||||
|
||||
|
||||
async def _register_session(
|
||||
|
||||
@@ -19,6 +19,7 @@ from langbot.pkg.agent.runner.session_registry import get_session_registry
|
||||
from langbot.pkg.agent.runner.run_ledger_store import RunLedgerStore
|
||||
from langbot.pkg.agent.runner.interaction_store import InteractionStore
|
||||
from langbot.pkg.agent.runner.persistent_state_store import reset_persistent_state_store
|
||||
from langbot.pkg.api.http.context import ExecutionContext
|
||||
from langbot_plugin.api.entities.builtin.platform import entities as platform_entities
|
||||
from langbot_plugin.api.entities.builtin.platform import events as platform_events
|
||||
from langbot_plugin.api.entities.builtin.platform import message as platform_message
|
||||
@@ -28,6 +29,11 @@ from langbot_plugin.api.entities.builtin.resource import tool as resource_tool
|
||||
|
||||
|
||||
RUNNER_ID = 'plugin:langbot-team/LocalAgent/default'
|
||||
TEST_CONTEXT = ExecutionContext(
|
||||
instance_uuid='instance-test',
|
||||
workspace_uuid='workspace-test',
|
||||
placement_generation=1,
|
||||
)
|
||||
|
||||
|
||||
class FakeLogger:
|
||||
@@ -103,8 +109,15 @@ class FakeRegistry:
|
||||
self.descriptor = descriptor
|
||||
self.calls: list[dict] = []
|
||||
|
||||
async def get(self, runner_id, bound_plugins=None):
|
||||
self.calls.append({'runner_id': runner_id, 'bound_plugins': bound_plugins})
|
||||
async def get(self, context, runner_id, bound_plugins=None):
|
||||
self.calls.append(
|
||||
{
|
||||
'context': context,
|
||||
'runner_id': runner_id,
|
||||
'bound_plugins': bound_plugins,
|
||||
}
|
||||
)
|
||||
assert context.workspace_uuid == TEST_CONTEXT.workspace_uuid
|
||||
assert runner_id == self.descriptor.id
|
||||
return self.descriptor
|
||||
|
||||
@@ -129,7 +142,7 @@ class FakeApplication:
|
||||
get_knowledge_base_by_uuid=AsyncMock(return_value=FakeKnowledgeBase('kb_001'))
|
||||
)
|
||||
self.skill_mgr = types.SimpleNamespace(
|
||||
skills={
|
||||
get_skills=lambda context: {
|
||||
'demo': {
|
||||
'name': 'demo',
|
||||
'display_name': 'Demo Skill',
|
||||
@@ -201,7 +214,7 @@ def make_query():
|
||||
using_conversation=FakeConversation(),
|
||||
)
|
||||
|
||||
return types.SimpleNamespace(
|
||||
query = types.SimpleNamespace(
|
||||
query_id=1001,
|
||||
launcher_type=provider_session.LauncherTypes.PERSON,
|
||||
launcher_id='user_001',
|
||||
@@ -254,6 +267,8 @@ def make_query():
|
||||
)
|
||||
],
|
||||
)
|
||||
query._execution_context = TEST_CONTEXT
|
||||
return query
|
||||
|
||||
|
||||
def test_context_builder_includes_consumable_base64_attachments():
|
||||
@@ -903,6 +918,7 @@ class TestQueryEntrySessionQueryId:
|
||||
|
||||
def __init__(self):
|
||||
self.resolver = object.__new__(BoxService)
|
||||
self.resolver._cloud_managed = False
|
||||
self.materialize_session_id = None
|
||||
|
||||
async def materialize_inbound_attachments(self, query):
|
||||
@@ -1043,7 +1059,14 @@ class TestQueryEntrySessionQueryId:
|
||||
enabled=True,
|
||||
)
|
||||
|
||||
messages = [message async for message in orchestrator.run(event, binding)]
|
||||
messages = [
|
||||
message
|
||||
async for message in orchestrator.run(
|
||||
event,
|
||||
binding,
|
||||
adapter_context={'_execution_context': TEST_CONTEXT},
|
||||
)
|
||||
]
|
||||
|
||||
assert len(messages) == 1
|
||||
# Verify session during run has query_id=None
|
||||
|
||||
@@ -7,6 +7,14 @@ import pytest
|
||||
from langbot.pkg.agent.runner.registry import AgentRunnerRegistry
|
||||
from langbot.pkg.agent.runner.descriptor import AgentRunnerDescriptor
|
||||
from langbot.pkg.agent.runner.errors import RunnerNotFoundError, RunnerNotAuthorizedError
|
||||
from langbot.pkg.api.http.context import ExecutionContext
|
||||
|
||||
|
||||
TEST_CONTEXT = ExecutionContext(
|
||||
instance_uuid='instance-test',
|
||||
workspace_uuid='workspace-test',
|
||||
placement_generation=1,
|
||||
)
|
||||
|
||||
|
||||
class FakeApplication:
|
||||
@@ -31,6 +39,9 @@ class FakeApplication:
|
||||
class FakePluginConnector:
|
||||
is_enable_plugin = True
|
||||
|
||||
async def require_workspace_context(self, context):
|
||||
return context
|
||||
|
||||
async def list_agent_runners(self, bound_plugins=None):
|
||||
# Return sample runner data
|
||||
return [
|
||||
@@ -96,7 +107,7 @@ class TestRegistryDiscovery:
|
||||
ap = FakeApplication()
|
||||
registry = AgentRunnerRegistry(ap)
|
||||
|
||||
runners = await registry.list_runners(use_cache=False)
|
||||
runners = await registry.list_runners(TEST_CONTEXT, use_cache=False)
|
||||
|
||||
# Should find 2 valid runners (langbot-team/LocalAgent and alice/my-agent)
|
||||
assert len(runners) == 2
|
||||
@@ -112,10 +123,10 @@ class TestRegistryDiscovery:
|
||||
registry = AgentRunnerRegistry(ap)
|
||||
|
||||
# First discovery
|
||||
runners1 = await registry.list_runners(use_cache=True)
|
||||
runners1 = await registry.list_runners(TEST_CONTEXT, use_cache=True)
|
||||
|
||||
# Second call should use cache
|
||||
runners2 = await registry.list_runners(use_cache=True)
|
||||
runners2 = await registry.list_runners(TEST_CONTEXT, use_cache=True)
|
||||
|
||||
assert registry._cache is not None
|
||||
assert len(runners1) == len(runners2)
|
||||
@@ -127,7 +138,7 @@ class TestRegistryDiscovery:
|
||||
ap.plugin_connector.is_enable_plugin = False
|
||||
registry = AgentRunnerRegistry(ap)
|
||||
|
||||
runners = await registry.list_runners(use_cache=False)
|
||||
runners = await registry.list_runners(TEST_CONTEXT, use_cache=False)
|
||||
|
||||
assert runners == []
|
||||
|
||||
@@ -143,23 +154,28 @@ class TestRegistryDiscovery:
|
||||
|
||||
# First: get with bound_plugins filter (should not pollute cache)
|
||||
descriptor = await registry.get(
|
||||
TEST_CONTEXT,
|
||||
'plugin:langbot-team/LocalAgent/default',
|
||||
bound_plugins=['langbot-team/LocalAgent'],
|
||||
)
|
||||
assert descriptor.id == 'plugin:langbot-team/LocalAgent/default'
|
||||
|
||||
# Cache should contain ALL runners (both langbot and alice)
|
||||
assert registry._cache is not None
|
||||
assert len(registry._cache) == 2 # Both runners in cache
|
||||
assert 'plugin:langbot-team/LocalAgent/default' in registry._cache
|
||||
assert 'plugin:alice/my-agent/custom' in registry._cache
|
||||
scoped_cache = registry._cache[('instance-test', 'workspace-test', 1)]
|
||||
assert len(scoped_cache) == 2
|
||||
assert 'plugin:langbot-team/LocalAgent/default' in scoped_cache
|
||||
assert 'plugin:alice/my-agent/custom' in scoped_cache
|
||||
|
||||
# Second: list_runners without filter should return ALL runners
|
||||
all_runners = await registry.list_runners(bound_plugins=None, use_cache=True)
|
||||
all_runners = await registry.list_runners(TEST_CONTEXT, bound_plugins=None, use_cache=True)
|
||||
assert len(all_runners) == 2 # Both runners returned
|
||||
|
||||
# Third: list_runners with different filter should work correctly
|
||||
alice_runners = await registry.list_runners(bound_plugins=['alice/my-agent'], use_cache=True)
|
||||
alice_runners = await registry.list_runners(
|
||||
TEST_CONTEXT,
|
||||
bound_plugins=['alice/my-agent'],
|
||||
use_cache=True,
|
||||
)
|
||||
assert len(alice_runners) == 1
|
||||
assert alice_runners[0].id == 'plugin:alice/my-agent/custom'
|
||||
|
||||
@@ -173,7 +189,10 @@ class TestRegistryGet:
|
||||
ap = FakeApplication()
|
||||
registry = AgentRunnerRegistry(ap)
|
||||
|
||||
descriptor = await registry.get('plugin:langbot-team/LocalAgent/default')
|
||||
descriptor = await registry.get(
|
||||
TEST_CONTEXT,
|
||||
'plugin:langbot-team/LocalAgent/default',
|
||||
)
|
||||
|
||||
assert descriptor.id == 'plugin:langbot-team/LocalAgent/default'
|
||||
assert descriptor.plugin_author == 'langbot-team'
|
||||
@@ -187,7 +206,7 @@ class TestRegistryGet:
|
||||
registry = AgentRunnerRegistry(ap)
|
||||
|
||||
with pytest.raises(RunnerNotFoundError) as exc_info:
|
||||
await registry.get('plugin:notexist/unknown/default')
|
||||
await registry.get(TEST_CONTEXT, 'plugin:notexist/unknown/default')
|
||||
|
||||
assert exc_info.value.runner_id == 'plugin:notexist/unknown/default'
|
||||
|
||||
@@ -199,6 +218,7 @@ class TestRegistryGet:
|
||||
|
||||
# Authorized - langbot plugin in bound list
|
||||
descriptor = await registry.get(
|
||||
TEST_CONTEXT,
|
||||
'plugin:langbot-team/LocalAgent/default',
|
||||
bound_plugins=['langbot-team/LocalAgent'],
|
||||
)
|
||||
@@ -207,6 +227,7 @@ class TestRegistryGet:
|
||||
# Not authorized - plugin not in bound list
|
||||
with pytest.raises(RunnerNotAuthorizedError):
|
||||
await registry.get(
|
||||
TEST_CONTEXT,
|
||||
'plugin:alice/my-agent/custom',
|
||||
bound_plugins=['langbot-team/LocalAgent'],
|
||||
)
|
||||
@@ -221,7 +242,7 @@ class TestRegistryMetadataForPipeline:
|
||||
ap = FakeApplication()
|
||||
registry = AgentRunnerRegistry(ap)
|
||||
|
||||
options, stages = await registry.get_runner_metadata_for_pipeline()
|
||||
options, stages = await registry.get_runner_metadata_for_pipeline(TEST_CONTEXT)
|
||||
|
||||
# Should have options for each runner
|
||||
assert len(options) == 2
|
||||
|
||||
@@ -12,9 +12,15 @@ from langbot.pkg.agent.runner.binding_resolver import AgentBindingResolver
|
||||
from langbot.pkg.agent.runner.query_entry_adapter import QueryEntryAdapter
|
||||
from langbot.pkg.agent.runner.resource_builder import AgentResourceBuilder
|
||||
from langbot.pkg.agent.runner.host_models import AgentBinding, BindingScope, ResourcePolicy
|
||||
from langbot.pkg.api.http.context import ExecutionContext
|
||||
|
||||
|
||||
RUNNER_ID = 'plugin:test/runner/default'
|
||||
TEST_CONTEXT = ExecutionContext(
|
||||
instance_uuid='instance-test',
|
||||
workspace_uuid='workspace-test',
|
||||
placement_generation=1,
|
||||
)
|
||||
FULL_PERMISSIONS = {
|
||||
'models': ['count_tokens', 'invoke', 'stream', 'rerank'],
|
||||
'tools': ['detail', 'call'],
|
||||
@@ -86,6 +92,7 @@ async def build_resources(app, query, descriptor):
|
||||
agent_config = QueryEntryAdapter.config_to_agent_config(query, descriptor.id)
|
||||
binding = AgentBindingResolver().resolve_one(event, [agent_config])
|
||||
return await AgentResourceBuilder(app).build_resources_from_binding(
|
||||
execution_context=TEST_CONTEXT,
|
||||
event=event,
|
||||
binding=binding,
|
||||
descriptor=descriptor,
|
||||
@@ -118,10 +125,12 @@ async def test_build_models_authorizes_config_declared_llm_and_rerank_models(app
|
||||
'rerank': make_model(model_type='rerank', provider='rerank-provider'),
|
||||
}
|
||||
|
||||
async def get_model_by_uuid(model_uuid):
|
||||
async def get_model_by_uuid(context, model_uuid):
|
||||
assert context == TEST_CONTEXT
|
||||
return llm_models.get(model_uuid)
|
||||
|
||||
async def get_rerank_model_by_uuid(model_uuid):
|
||||
async def get_rerank_model_by_uuid(context, model_uuid):
|
||||
assert context == TEST_CONTEXT
|
||||
return rerank_models.get(model_uuid)
|
||||
|
||||
app.model_mgr.get_model_by_uuid = AsyncMock(side_effect=get_model_by_uuid)
|
||||
@@ -228,7 +237,8 @@ async def test_build_resources_accepts_dynamic_form_type_aliases(app):
|
||||
"""Frontend DynamicForm aliases should resolve to runtime resource grants."""
|
||||
app.model_mgr.get_model_by_uuid = AsyncMock(return_value=make_model())
|
||||
|
||||
async def get_kb(kb_uuid):
|
||||
async def get_kb(context, kb_uuid):
|
||||
assert context == TEST_CONTEXT
|
||||
return SimpleNamespace(
|
||||
uuid=kb_uuid,
|
||||
get_name=lambda: f'name-{kb_uuid}',
|
||||
@@ -322,7 +332,7 @@ async def test_build_tools_authorizes_query_declared_tools(app):
|
||||
"""Tools discovered by Pipeline preprocessing become run-scoped authorized
|
||||
resources, with full parameters schema prefilled by the host."""
|
||||
app.tool_mgr.get_tool_schema = AsyncMock(
|
||||
side_effect=lambda name, source_ref=None: {
|
||||
side_effect=lambda context, name, source_ref=None: {
|
||||
'qa_plugin_echo': (
|
||||
'Echo test tool',
|
||||
{'type': 'object', 'properties': {'text': {'type': 'string'}}},
|
||||
@@ -411,6 +421,7 @@ async def test_build_tools_materializes_independent_agent_all_tools_policy(app):
|
||||
)
|
||||
|
||||
resources = await AgentResourceBuilder(app).build_resources_from_binding(
|
||||
execution_context=TEST_CONTEXT,
|
||||
event=QueryEntryAdapter.query_to_event(make_query({})),
|
||||
binding=binding,
|
||||
descriptor=descriptor,
|
||||
@@ -418,6 +429,7 @@ async def test_build_tools_materializes_independent_agent_all_tools_policy(app):
|
||||
|
||||
assert [tool['tool_name'] for tool in resources['tools']] == ['exec', 'plugin_tool']
|
||||
app.tool_mgr.get_resolved_tool_catalog.assert_awaited_once_with(
|
||||
TEST_CONTEXT,
|
||||
include_skill_authoring=True,
|
||||
include_mcp_resource_tools=True,
|
||||
)
|
||||
@@ -442,6 +454,7 @@ async def test_build_tools_denies_mcp_resource_tools_when_agent_reads_disabled(a
|
||||
)
|
||||
|
||||
resources = await AgentResourceBuilder(app).build_resources_from_binding(
|
||||
execution_context=TEST_CONTEXT,
|
||||
event=QueryEntryAdapter.query_to_event(make_query({})),
|
||||
binding=binding,
|
||||
descriptor=descriptor,
|
||||
@@ -471,6 +484,7 @@ async def test_build_tools_keeps_plugin_using_synthetic_mcp_tool_name_when_reads
|
||||
)
|
||||
|
||||
resources = await AgentResourceBuilder(app).build_resources_from_binding(
|
||||
execution_context=TEST_CONTEXT,
|
||||
event=QueryEntryAdapter.query_to_event(make_query({})),
|
||||
binding=binding,
|
||||
descriptor=descriptor,
|
||||
@@ -502,7 +516,8 @@ async def test_build_knowledge_bases_unions_config_and_policy_grants(app):
|
||||
variables={'_knowledge_base_uuids': ['kb_policy']},
|
||||
)
|
||||
|
||||
async def get_kb(kb_uuid):
|
||||
async def get_kb(context, kb_uuid):
|
||||
assert context == TEST_CONTEXT
|
||||
return SimpleNamespace(
|
||||
uuid=kb_uuid,
|
||||
get_name=lambda: f'name-{kb_uuid}',
|
||||
|
||||
@@ -25,7 +25,7 @@ from langbot_plugin.api.entities.builtin.agent_runner.run_ledger import (
|
||||
)
|
||||
from langbot_plugin.entities.io.actions.enums import PluginToRuntimeAction
|
||||
|
||||
from .conftest import make_resources
|
||||
from .conftest import bind_runtime_action_context, make_resources
|
||||
|
||||
|
||||
class FakeConnection:
|
||||
@@ -72,17 +72,32 @@ class FakeRunnerRegistry:
|
||||
self.runners = runners
|
||||
self.calls = []
|
||||
|
||||
async def list_runners(self, *, bound_plugins=None, use_cache=True):
|
||||
self.calls.append({'bound_plugins': bound_plugins, 'use_cache': use_cache})
|
||||
async def list_runners(self, context, *, bound_plugins=None, use_cache=True):
|
||||
self.calls.append(
|
||||
{
|
||||
'workspace_uuid': context.workspace_uuid,
|
||||
'bound_plugins': bound_plugins,
|
||||
'use_cache': use_cache,
|
||||
}
|
||||
)
|
||||
return self.runners
|
||||
|
||||
|
||||
def _handler(db_engine, admin_plugins=None, runner_registry=None):
|
||||
def _handler(
|
||||
db_engine,
|
||||
admin_plugins=None,
|
||||
runner_registry=None,
|
||||
plugin_identity='test/runner',
|
||||
):
|
||||
async def fake_disconnect():
|
||||
return True
|
||||
|
||||
fake_app = FakeApplication(db_engine, admin_plugins=admin_plugins, runner_registry=runner_registry)
|
||||
return RuntimeConnectionHandler(FakeConnection(), fake_disconnect, fake_app)
|
||||
return bind_runtime_action_context(
|
||||
RuntimeConnectionHandler(FakeConnection(), fake_disconnect, fake_app),
|
||||
fake_app,
|
||||
plugin_identity=plugin_identity,
|
||||
)
|
||||
|
||||
|
||||
async def _register_session(
|
||||
@@ -505,6 +520,7 @@ async def test_agent_run_admin_can_list_runner_registry_without_run_id(db_engine
|
||||
}
|
||||
],
|
||||
runner_registry=runner_registry,
|
||||
plugin_identity='langbot/control',
|
||||
)
|
||||
runner_list = handler.actions['runner_list']
|
||||
|
||||
@@ -519,6 +535,7 @@ async def test_agent_run_admin_can_list_runner_registry_without_run_id(db_engine
|
||||
assert result.data['items'][0]['id'] == 'plugin:test/runner/default'
|
||||
assert runner_registry.calls == [
|
||||
{
|
||||
'workspace_uuid': 'workspace-test',
|
||||
'bound_plugins': ['test/runner'],
|
||||
'use_cache': True,
|
||||
}
|
||||
@@ -602,6 +619,7 @@ async def test_agent_run_admin_can_get_and_page_cross_scope_without_run_id(db_en
|
||||
'permissions': ['agent_run:admin'],
|
||||
}
|
||||
],
|
||||
plugin_identity='langbot/control',
|
||||
)
|
||||
run_get = handler.actions[PluginToRuntimeAction.RUN_GET.value]
|
||||
run_events_page = handler.actions[PluginToRuntimeAction.RUN_EVENTS_PAGE.value]
|
||||
@@ -720,7 +738,7 @@ async def test_configured_admin_identity_cannot_be_spoofed_with_other_run_sessio
|
||||
)
|
||||
|
||||
assert result.code != 0
|
||||
assert 'mismatch' in result.message.lower()
|
||||
assert 'does not match' in result.message.lower()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -834,6 +852,7 @@ async def test_runtime_admin_can_register_list_and_claim_without_run_id(db_engin
|
||||
'permissions': ['runtime:admin'],
|
||||
}
|
||||
],
|
||||
plugin_identity='langbot/control',
|
||||
)
|
||||
runtime_register = handler.actions[PluginToRuntimeAction.RUNTIME_REGISTER.value]
|
||||
runtime_list = handler.actions[PluginToRuntimeAction.RUNTIME_LIST.value]
|
||||
@@ -917,6 +936,7 @@ async def test_runtime_admin_can_reconcile_without_run_id(db_engine):
|
||||
'permissions': ['runtime:admin'],
|
||||
}
|
||||
],
|
||||
plugin_identity='langbot/control',
|
||||
)
|
||||
runtime_reconcile = handler.actions['runtime_reconcile']
|
||||
|
||||
|
||||
@@ -25,11 +25,11 @@ from sqlalchemy.ext.asyncio import create_async_engine
|
||||
|
||||
from langbot.pkg.agent.runner.session_registry import AgentRunSessionRegistry
|
||||
from langbot.pkg.agent.runner.persistent_state_store import PersistentStateStore, reset_persistent_state_store
|
||||
from langbot.pkg.plugin.handler import RuntimeConnectionHandler
|
||||
from langbot.pkg.plugin.handler import RuntimeConnectionHandler as HostRuntimeConnectionHandler
|
||||
from langbot_plugin.entities.io.actions.enums import PluginToRuntimeAction
|
||||
|
||||
# Import shared test fixtures
|
||||
from .conftest import make_resources
|
||||
from .conftest import bind_runtime_action_context, make_resources
|
||||
|
||||
|
||||
class FakeConnection:
|
||||
@@ -48,6 +48,14 @@ class FakeApplication:
|
||||
self.persistence_mgr.get_db_engine = MagicMock(return_value=db_engine)
|
||||
|
||||
|
||||
class RuntimeConnectionHandler(HostRuntimeConnectionHandler):
|
||||
"""Host handler with the trusted Runtime envelope installed for direct calls."""
|
||||
|
||||
def __init__(self, connection, disconnect, application):
|
||||
super().__init__(connection, disconnect, application)
|
||||
bind_runtime_action_context(self, application)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def session_registry():
|
||||
"""Create a fresh session registry for each test."""
|
||||
@@ -126,8 +134,13 @@ class TestStateAPIHandlerAuthorization:
|
||||
assert 'not found' in result.message.lower()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_state_get_missing_caller_plugin_identity_returns_error(self, session_registry, db_engine, persistent_store):
|
||||
"""STATE_GET: missing caller_plugin_identity when session has plugin_identity returns error."""
|
||||
async def test_state_get_uses_installation_identity_when_payload_omits_caller(
|
||||
self,
|
||||
session_registry,
|
||||
db_engine,
|
||||
persistent_store,
|
||||
):
|
||||
"""STATE_GET derives caller identity from the trusted installation binding."""
|
||||
fake_app = FakeApplication(db_engine)
|
||||
fake_app.persistence_mgr.get_db_engine = MagicMock(return_value=db_engine)
|
||||
|
||||
@@ -157,8 +170,8 @@ class TestStateAPIHandlerAuthorization:
|
||||
'key': 'test_key',
|
||||
})
|
||||
|
||||
assert result.code != 0
|
||||
assert 'caller_plugin_identity is required' in result.message
|
||||
assert result.code == 0
|
||||
assert result.data == {'value': None}
|
||||
|
||||
await session_registry.unregister('run_test_missing_identity')
|
||||
|
||||
@@ -195,7 +208,7 @@ class TestStateAPIHandlerAuthorization:
|
||||
})
|
||||
|
||||
assert result.code != 0
|
||||
assert 'mismatch' in result.message.lower()
|
||||
assert 'does not match' in result.message.lower()
|
||||
|
||||
await session_registry.unregister('run_test_mismatch')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user