Fix agent runner host migration and runtime guards

Migrates legacy runner blocks into plugin runner configs, preserves run-scoped history boundaries, enforces operation/file authorization, and sanitizes inline attachment persistence. Also fixes plugin runner form dirty handling and adds regression coverage.
This commit is contained in:
huanghuoguoguo
2026-06-12 18:41:20 +08:00
parent c9ef788072
commit 2094993afb
33 changed files with 1017 additions and 141 deletions
+28
View File
@@ -77,6 +77,33 @@ def make_session(
'skill': {s.get('skill_name') for s in res.get('skills', [])},
'file': {f.get('file_id') for f in res.get('files', [])},
}
authorized_operations: dict[str, dict[str, set[str]]] = {
'model': {
m.get('model_id'): set(m.get('operations') or ['invoke', 'stream', 'rerank'])
for m in res.get('models', [])
if m.get('model_id')
},
'tool': {
t.get('tool_name'): set(t.get('operations') or ['detail', 'call'])
for t in res.get('tools', [])
if t.get('tool_name')
},
'knowledge_base': {
kb.get('kb_id'): set(kb.get('operations') or ['list', 'retrieve'])
for kb in res.get('knowledge_bases', [])
if kb.get('kb_id')
},
'skill': {
s.get('skill_name'): set(s.get('operations') or ['activate'])
for s in res.get('skills', [])
if s.get('skill_name')
},
'file': {
f.get('file_id'): set(f.get('operations') or ['config', 'knowledge'])
for f in res.get('files', [])
if f.get('file_id')
},
}
return {
'run_id': run_id,
@@ -90,6 +117,7 @@ def make_session(
'state_policy': policy,
'state_context': context,
'authorized_ids': authorized_ids,
'authorized_operations': authorized_operations,
},
'status': {
'started_at': now,
+5 -2
View File
@@ -129,6 +129,9 @@ class MockAgentRunOrchestrator:
for chunk in self._chunks:
yield chunk
async def try_claim_steering_from_query(self, query):
return False
def resolve_runner_id_for_telemetry(self, query):
return 'plugin:langbot/local-agent/default'
@@ -240,7 +243,7 @@ class TestConfigMigrationInChatHandler:
assert runner_id == 'plugin:langbot/local-agent/default'
def test_resolve_runner_id_from_old_format(self):
"""ConfigMigration should not resolve removed runner aliases."""
"""ConfigMigration resolves old runner aliases for compatibility."""
pipeline_config = {
'ai': {
'runner': {
@@ -250,7 +253,7 @@ class TestConfigMigrationInChatHandler:
}
runner_id = ConfigMigration.resolve_runner_id(pipeline_config)
assert runner_id is None
assert runner_id == 'plugin:langbot/local-agent/default'
class TestErrorHandling:
@@ -20,7 +20,7 @@ class TestResolveRunnerId:
runner_id = ConfigMigration.resolve_runner_id(pipeline_config)
assert runner_id == 'plugin:langbot/local-agent/default'
def test_does_not_resolve_old_runner_field(self):
def test_resolves_old_runner_field(self):
pipeline_config = {
'ai': {
'runner': {
@@ -30,7 +30,7 @@ class TestResolveRunnerId:
}
runner_id = ConfigMigration.resolve_runner_id(pipeline_config)
assert runner_id is None
assert runner_id == 'plugin:langbot/local-agent/default'
def test_resolve_no_runner_config(self):
runner_id = ConfigMigration.resolve_runner_id({})
@@ -58,7 +58,7 @@ class TestResolveRunnerConfig:
)
assert config == {'model': 'uuid-123', 'custom_option': 10}
def test_does_not_read_old_runner_block(self):
def test_reads_old_runner_block(self):
pipeline_config = {
'ai': {
'local-agent': {
@@ -71,7 +71,7 @@ class TestResolveRunnerConfig:
pipeline_config,
'plugin:langbot/local-agent/default',
)
assert config == {}
assert config == {'model': {'primary': 'uuid-123', 'fallbacks': []}}
def test_resolve_no_config(self):
config = ConfigMigration.resolve_runner_config(
@@ -138,15 +138,20 @@ class TestNormalizePipelineConfig:
assert migrated['ai']['runner']['id'] == 'plugin:test/my-runner/default'
assert migrated['ai']['runner_config']['plugin:test/my-runner/default']['custom-option'] == 20
def test_does_not_migrate_old_runner_blocks(self):
def test_migrates_old_runner_blocks(self):
config = {
'ai': {
'runner': {'runner': 'local-agent'},
'local-agent': {'model': 'old-model'},
'local-agent': {'model': 'old-model', 'knowledge-base': 'kb_1'},
},
}
migrated = ConfigMigration.migrate_pipeline_config(config)
assert 'id' not in migrated['ai']['runner']
assert migrated['ai']['local-agent'] == {'model': 'old-model'}
assert migrated['ai']['runner']['id'] == 'plugin:langbot/local-agent/default'
assert 'runner' not in migrated['ai']['runner']
assert 'local-agent' not in migrated['ai']
assert migrated['ai']['runner_config']['plugin:langbot/local-agent/default'] == {
'model': {'primary': 'old-model', 'fallbacks': []},
'knowledge-bases': ['kb_1'],
}
@@ -31,7 +31,7 @@ class TestMigratePipelineConfig:
assert migrated['ai']['runner']['id'] == 'plugin:langbot/local-agent/default'
assert migrated['ai']['runner_config']['plugin:langbot/local-agent/default']['custom-option'] == 10
def test_old_runner_field_is_not_mapped(self):
def test_old_runner_field_is_mapped(self):
config = {
'ai': {
'runner': {
@@ -47,11 +47,13 @@ class TestMigratePipelineConfig:
migrated = ConfigMigration.migrate_pipeline_config(config)
assert migrated['ai']['runner'] == {
'runner': 'local-agent',
'expire-time': 3600,
'id': 'plugin:langbot/local-agent/default',
}
assert migrated['ai']['runner_config'] == {}
assert migrated['ai']['local-agent'] == {'model': 'old-model'}
assert migrated['ai']['runner_config']['plugin:langbot/local-agent/default'] == {
'model': {'primary': 'old-model', 'fallbacks': []},
}
assert 'local-agent' not in migrated['ai']
def test_empty_config_is_unchanged(self):
config = {}
@@ -95,14 +97,14 @@ class TestResolveRunnerId:
runner_id = ConfigMigration.resolve_runner_id(config)
assert runner_id == 'plugin:test/my-runner/default'
def test_old_runner_field_is_ignored(self):
def test_old_runner_field_is_mapped(self):
config = {
'ai': {
'runner': {'runner': 'local-agent'},
},
}
runner_id = ConfigMigration.resolve_runner_id(config)
assert runner_id is None
assert runner_id == 'plugin:langbot/local-agent/default'
class TestResolveRunnerConfig:
@@ -119,11 +121,11 @@ class TestResolveRunnerConfig:
runner_config = ConfigMigration.resolve_runner_config(config, 'plugin:langbot/local-agent/default')
assert runner_config['custom-option'] == 20
def test_old_runner_block_is_ignored(self):
def test_old_runner_block_is_read(self):
config = {
'ai': {
'local-agent': {'custom-option': 20},
},
}
runner_config = ConfigMigration.resolve_runner_config(config, 'plugin:langbot/local-agent/default')
assert runner_config == {}
assert runner_config == {'custom-option': 20}
+44 -3
View File
@@ -576,8 +576,8 @@ class TestRETRIEVEKNOWLEDGEBASEBugFix:
assert 'kb_custom' in allowed_kbs
def test_retrieve_kb_ignores_old_runner_format(self):
"""Old runner format is not resolved by current AgentRunner helpers."""
def test_retrieve_kb_reads_old_runner_format(self):
"""Old runner format is resolved for migration compatibility."""
from langbot.pkg.agent.runner.config_migration import ConfigMigration
pipeline_config = {
@@ -585,11 +585,16 @@ class TestRETRIEVEKNOWLEDGEBASEBugFix:
'runner': {
'runner': 'local-agent',
},
'local-agent': {
'knowledge-bases': ['kb_legacy'],
},
},
}
runner_id = ConfigMigration.resolve_runner_id(pipeline_config)
assert runner_id is None
runner_config = ConfigMigration.resolve_runner_config(pipeline_config, runner_id)
assert runner_id == 'plugin:langbot/local-agent/default'
assert runner_config.get('knowledge-bases') == ['kb_legacy']
class TestHandlerActionAuthorization:
@@ -1870,6 +1875,42 @@ class TestFilePermissionValidation:
await registry.unregister('run_file_denied')
class TestOperationPermissionValidation:
"""Tests operation-level Host-side run authorization."""
@pytest.mark.asyncio
async def test_model_operation_denied_when_resource_only_allows_invoke(self):
from langbot.pkg.agent.runner.session_registry import get_session_registry
from langbot.pkg.plugin.handler import _validate_run_authorization
registry = get_session_registry()
await registry.register(
run_id='run_model_operation_denied',
runner_id='plugin:test/runner/default',
query_id=1,
plugin_identity='test/runner',
resources=make_resources(models=[{'model_id': 'model_001', 'operations': ['invoke']}]),
)
mock_ap = MagicMock()
mock_ap.logger = MagicMock()
session, error = await _validate_run_authorization(
'run_model_operation_denied',
'model',
'model_001',
mock_ap,
caller_plugin_identity='test/runner',
operation='stream',
)
assert session is None
assert error is not None
assert 'operation stream' in error.message
await registry.unregister('run_model_operation_denied')
class TestCallerPluginIdentityValidation:
"""Tests for caller_plugin_identity cross-plugin validation.
@@ -64,6 +64,9 @@ async def _register_session(
*,
run_id='run_1',
conversation_id='conv_1',
bot_id=None,
workspace_id=None,
thread_id=None,
available_apis=None,
):
await session_registry.register(
@@ -73,6 +76,9 @@ async def _register_session(
plugin_identity='test/runner',
resources=make_resources(),
conversation_id=conversation_id,
bot_id=bot_id,
workspace_id=workspace_id,
thread_id=thread_id,
available_apis=available_apis or {},
)
@@ -220,3 +226,98 @@ async def test_event_page_returns_sdk_page_projection(session_registry, db_engin
assert 'input_json' not in item
assert 'run_id' not in item
assert 'runner_id' not in item
@pytest.mark.asyncio
async def test_history_page_filters_run_scope_thread_and_bot(session_registry, db_engine):
from langbot.pkg.agent.runner.transcript_store import TranscriptStore
await _register_session(
session_registry,
bot_id='bot_1',
thread_id='thread_1',
available_apis={'history_page': True},
)
store = TranscriptStore(db_engine)
await store.append_transcript(
transcript_id='tr_visible',
event_id='evt_visible',
conversation_id='conv_1',
role='user',
bot_id='bot_1',
thread_id='thread_1',
content='visible',
)
await store.append_transcript(
transcript_id='tr_other_bot',
event_id='evt_other_bot',
conversation_id='conv_1',
role='user',
bot_id='bot_2',
thread_id='thread_1',
content='hidden bot',
)
await store.append_transcript(
transcript_id='tr_other_thread',
event_id='evt_other_thread',
conversation_id='conv_1',
role='user',
bot_id='bot_1',
thread_id='thread_2',
content='hidden thread',
)
handler = _handler(db_engine, session_registry)
history_page = handler.actions[PluginToRuntimeAction.HISTORY_PAGE.value]
result = await history_page({
'run_id': 'run_1',
'caller_plugin_identity': 'test/runner',
})
assert result.code == 0
assert [item['content'] for item in result.data['items']] == ['visible']
@pytest.mark.asyncio
async def test_event_page_filters_run_scope_thread_and_bot(session_registry, db_engine):
await _register_session(
session_registry,
bot_id='bot_1',
thread_id='thread_1',
available_apis={'event_page': True},
)
store = EventLogStore(db_engine)
await store.append_event(
event_id='evt_visible',
event_type='message.received',
source='platform',
bot_id='bot_1',
conversation_id='conv_1',
thread_id='thread_1',
)
await store.append_event(
event_id='evt_other_bot',
event_type='message.received',
source='platform',
bot_id='bot_2',
conversation_id='conv_1',
thread_id='thread_1',
)
await store.append_event(
event_id='evt_other_thread',
event_type='message.received',
source='platform',
bot_id='bot_1',
conversation_id='conv_1',
thread_id='thread_2',
)
handler = _handler(db_engine, session_registry)
event_page = handler.actions[PluginToRuntimeAction.EVENT_PAGE.value]
result = await event_page({
'run_id': 'run_1',
'caller_plugin_identity': 'test/runner',
})
assert result.code == 0
assert [item['event_id'] for item in result.data['items']] == ['evt_visible']
@@ -427,6 +427,37 @@ async def test_orchestrator_streams_fake_plugin_deltas(clean_agent_state):
assert [chunk.content for chunk in chunks] == ["hel", "hello"]
@pytest.mark.asyncio
async def test_orchestrator_persists_run_completed_message_transcript(clean_agent_state):
"""run.completed(message=...) should be treated as the final assistant transcript."""
from langbot.pkg.agent.runner.transcript_store import TranscriptStore
db_engine = clean_agent_state
descriptor = make_descriptor()
plugin_connector = FakePluginConnector(
results=[
{
"type": "run.completed",
"data": {
"finish_reason": "stop",
"message": {"role": "assistant", "content": "final response"},
},
},
]
)
orchestrator = AgentRunOrchestrator(FakeApplication(plugin_connector, db_engine), FakeRegistry(descriptor))
query = make_query()
messages = [message async for message in orchestrator.run_from_query(query)]
assert [message.content for message in messages] == ["final response"]
transcript_store = TranscriptStore(db_engine)
transcripts, _, _, _ = await transcript_store.page_transcript(query.session.using_conversation.uuid, limit=10)
assistant_items = [item for item in transcripts if item["role"] == "assistant"]
assert len(assistant_items) == 1
assert assistant_items[0]["content"] == "final response"
@pytest.mark.asyncio
async def test_orchestrator_drops_duplicate_result_sequence(clean_agent_state):
"""Duplicate runner result sequences are idempotently ignored."""
@@ -560,6 +591,14 @@ class TestQueryEntrySessionQueryId:
ap = FakeApplication(plugin_connector, db_engine)
orchestrator = AgentRunOrchestrator(ap, FakeRegistry(descriptor))
query = make_query()
query.user_message = provider_message.Message(
role="user",
content=[
provider_message.ContentElement.from_text("hello"),
provider_message.ContentElement.from_image_base64("data:image/png;base64,aGVsbG8="),
provider_message.ContentElement.from_file_base64("data:text/plain;base64,aGVsbG8=", "hello.txt"),
],
)
messages = [message async for message in orchestrator.run_from_query(query)]
@@ -815,6 +854,13 @@ class TestQueryEntryAdapterHostCapabilities:
ap = FakeApplication(plugin_connector, db_engine)
orchestrator = AgentRunOrchestrator(ap, FakeRegistry(descriptor))
query = make_query()
query.user_message = provider_message.Message(
role="user",
content=[
provider_message.ContentElement.from_text("hello"),
provider_message.ContentElement.from_image_base64("data:image/png;base64,aGVsbG8="),
],
)
messages = [message async for message in orchestrator.run_from_query(query)]
@@ -896,6 +942,13 @@ class TestQueryEntryAdapterHostCapabilities:
ap = FakeApplication(plugin_connector, db_engine)
orchestrator = AgentRunOrchestrator(ap, FakeRegistry(descriptor))
query = make_query()
query.user_message = provider_message.Message(
role="user",
content=[
provider_message.ContentElement.from_text("hello"),
provider_message.ContentElement.from_image_base64("data:image/png;base64,aGVsbG8="),
],
)
messages = [message async for message in orchestrator.run_from_query(query)]
@@ -910,18 +963,26 @@ class TestQueryEntryAdapterHostCapabilities:
assert len(event_logs) >= 1
# First event should be the incoming message.received
assert event_logs[0]["event_type"] == "message.received"
assert event_logs[0]["input_json"]["contents"][1]["image_base64"] is None
assert event_logs[0]["input_json"]["contents"][1]["content_redacted"] is True
assert "aGVsbG8=" not in str(event_logs[0]["input_json"])
# Check Transcript has user and assistant messages
transcript_store = TranscriptStore(db_engine)
transcripts, _, _, _ = await transcript_store.page_transcript(
conversation_id=query.session.using_conversation.uuid,
limit=10,
include_artifacts=True,
)
assert len(transcripts) >= 2
# Find user and assistant messages
roles = [t["role"] for t in transcripts]
assert "user" in roles
assert "assistant" in roles
user_item = next(t for t in transcripts if t["role"] == "user")
assert user_item["content_json"]["content"][1]["image_base64"] is None
assert user_item["artifact_refs"][0]["content"] is None
assert "aGVsbG8=" not in str(user_item)
@pytest.mark.asyncio
async def test_artifact_created_via_event_first_path(self, clean_agent_state):
@@ -138,10 +138,10 @@ async def test_build_models_authorizes_config_declared_llm_and_rerank_models(app
resources = await build_resources(app, query, descriptor)
assert resources['models'] == [
{'model_id': 'primary', 'model_type': 'llm', 'provider': 'test-provider'},
{'model_id': 'fallback', 'model_type': 'llm', 'provider': 'test-provider'},
{'model_id': 'aux', 'model_type': 'llm', 'provider': 'aux-provider'},
{'model_id': 'rerank', 'model_type': 'rerank', 'provider': 'rerank-provider'},
{'model_id': 'primary', 'model_type': 'llm', 'provider': 'test-provider', 'operations': ['invoke', 'stream']},
{'model_id': 'fallback', 'model_type': 'llm', 'provider': 'test-provider', 'operations': ['invoke', 'stream']},
{'model_id': 'aux', 'model_type': 'llm', 'provider': 'aux-provider', 'operations': ['invoke', 'stream']},
{'model_id': 'rerank', 'model_type': 'rerank', 'provider': 'rerank-provider', 'operations': ['rerank']},
]
@@ -188,8 +188,8 @@ async def test_build_models_authorizes_rerank_and_llm_refs_from_config(app):
resources = await build_resources(app, query, descriptor)
assert resources['models'] == [
{'model_id': 'llm', 'model_type': 'llm', 'provider': 'test-provider'},
{'model_id': 'rerank', 'model_type': 'rerank', 'provider': 'rerank-provider'},
{'model_id': 'llm', 'model_type': 'llm', 'provider': 'test-provider', 'operations': ['invoke', 'stream']},
{'model_id': 'rerank', 'model_type': 'rerank', 'provider': 'rerank-provider', 'operations': ['rerank']},
]
@@ -218,7 +218,7 @@ async def test_build_models_manifest_permission_narrows_binding(app):
resources = await build_resources(app, query, descriptor)
assert resources['models'] == [
{'model_id': 'rerank', 'model_type': 'rerank', 'provider': 'rerank-provider'},
{'model_id': 'rerank', 'model_type': 'rerank', 'provider': 'rerank-provider', 'operations': ['rerank']},
]
@@ -264,11 +264,13 @@ async def test_build_tools_authorizes_query_declared_tools(app):
'tool_name': 'qa_plugin_echo',
'tool_type': None,
'description': None,
'operations': ['detail', 'call'],
},
{
'tool_name': 'qa_mcp_echo',
'tool_type': None,
'description': None,
'operations': ['detail', 'call'],
},
]
@@ -320,8 +322,8 @@ async def test_build_knowledge_bases_unions_config_and_policy_grants(app):
resources = await build_resources(app, query, descriptor)
assert resources['knowledge_bases'] == [
{'kb_id': 'kb_config', 'kb_name': 'name-kb_config', 'kb_type': 'default'},
{'kb_id': 'kb_policy', 'kb_name': 'name-kb_policy', 'kb_type': 'default'},
{'kb_id': 'kb_config', 'kb_name': 'name-kb_config', 'kb_type': 'default', 'operations': ['list', 'retrieve']},
{'kb_id': 'kb_policy', 'kb_name': 'name-kb_policy', 'kb_type': 'default', 'operations': ['list', 'retrieve']},
]
@@ -347,6 +349,42 @@ async def test_build_knowledge_bases_manifest_permission_denies_binding_kbs(app)
assert resources['knowledge_bases'] == []
@pytest.mark.asyncio
async def test_build_files_authorizes_config_declared_file_fields(app):
descriptor = make_descriptor(
config_schema=[
{'name': 'avatar', 'type': 'file'},
{'name': 'references', 'type': 'array[file]'},
],
)
query = make_query({
'avatar': {'file_key': 'plugin_config_avatar.png', 'mimetype': 'image/png'},
'references': [
{'file_key': 'plugin_config_doc.txt', 'mimetype': 'text/plain', 'file_name': 'doc.txt'},
{'file_key': 'plugin_config_doc.txt', 'mimetype': 'text/plain', 'file_name': 'doc.txt'},
],
})
resources = await build_resources(app, query, descriptor)
assert resources['files'] == [
{
'file_id': 'plugin_config_avatar.png',
'file_name': None,
'mime_type': 'image/png',
'source': 'config',
'operations': ['config'],
},
{
'file_id': 'plugin_config_doc.txt',
'file_name': 'doc.txt',
'mime_type': 'text/plain',
'source': 'config',
'operations': ['config'],
},
]
@pytest.mark.asyncio
async def test_build_storage_intersects_manifest_and_binding_policy(app):
descriptor = make_descriptor(
@@ -330,6 +330,19 @@ class TestIsResourceAllowed:
assert registry.is_resource_allowed(session, 'model', 'model_001') is True
assert registry.is_resource_allowed(session, 'model', 'model_002') is True
def test_model_operation_denied(self):
"""Model resources should enforce operation-level grants."""
registry = AgentRunSessionRegistry()
resources = make_resources(
models=[
{'model_id': 'model_001', 'operations': ['invoke']},
]
)
session = make_session(resources=resources)
assert registry.is_resource_allowed(session, 'model', 'model_001', 'invoke') is True
assert registry.is_resource_allowed(session, 'model', 'model_001', 'stream') is False
def test_model_not_allowed(self):
"""Model not in resources should be denied."""
registry = AgentRunSessionRegistry()
@@ -360,6 +373,19 @@ class TestIsResourceAllowed:
assert registry.is_resource_allowed(session, 'tool', 'web_search') is True
assert registry.is_resource_allowed(session, 'tool', 'code_exec') is True
def test_tool_operation_denied(self):
"""Tool resources should enforce detail/call grants."""
registry = AgentRunSessionRegistry()
resources = make_resources(
tools=[
{'tool_name': 'web_search', 'operations': ['detail']},
]
)
session = make_session(resources=resources)
assert registry.is_resource_allowed(session, 'tool', 'web_search', 'detail') is True
assert registry.is_resource_allowed(session, 'tool', 'web_search', 'call') is False
def test_tool_not_allowed(self):
"""Tool not in resources should be denied."""
registry = AgentRunSessionRegistry()
@@ -138,6 +138,7 @@ class TestStateAPIHandlerAuthorization:
query_id=1,
plugin_identity='test/runner',
resources=make_resources(),
available_apis={'state': True},
state_policy={'enable_state': True, 'state_scopes': ['conversation']},
state_context={'scope_keys': {'conversation': 'conv_key'}, 'binding_identity': 'binding_1'},
)
@@ -173,6 +174,7 @@ class TestStateAPIHandlerAuthorization:
query_id=1,
plugin_identity='test/runner',
resources=make_resources(),
available_apis={'state': True},
state_policy={'enable_state': True, 'state_scopes': ['conversation']},
state_context={'scope_keys': {'conversation': 'conv_key'}, 'binding_identity': 'binding_1'},
)
@@ -209,6 +211,7 @@ class TestStateAPIHandlerAuthorization:
query_id=1,
plugin_identity='test/runner',
resources=make_resources(),
available_apis={'state': True},
state_policy={'enable_state': False, 'state_scopes': []},
state_context={'scope_keys': {}, 'binding_identity': 'binding_1'},
)
@@ -244,6 +247,7 @@ class TestStateAPIHandlerAuthorization:
query_id=1,
plugin_identity='test/runner',
resources=make_resources(),
available_apis={'state': True},
state_policy={'enable_state': True, 'state_scopes': ['conversation']},
state_context={'scope_keys': {'conversation': 'conv_key', 'actor': 'actor_key'}, 'binding_identity': 'binding_1'},
)
@@ -280,6 +284,7 @@ class TestStateAPIHandlerAuthorization:
query_id=1,
plugin_identity='test/runner',
resources=make_resources(),
available_apis={'state': True},
state_policy={'enable_state': True, 'state_scopes': ['conversation']},
state_context={'scope_keys': {}, 'binding_identity': 'binding_1'}, # No scope_keys
)
@@ -320,6 +325,7 @@ class TestStateAPIFullFlowWithRealDB:
query_id=1,
plugin_identity='test/runner',
resources=make_resources(),
available_apis={'state': True},
state_policy={'enable_state': True, 'state_scopes': ['conversation', 'runner']},
state_context={
'scope_keys': {
@@ -426,6 +432,7 @@ class TestStateHandlerReadsFromAuthorizationSnapshot:
query_id=1,
plugin_identity='test/runner',
resources=make_resources(),
available_apis={'state': True},
state_policy={'enable_state': False, 'state_scopes': []},
state_context={'scope_keys': {}, 'binding_identity': 'binding_1'},
)
@@ -469,6 +476,7 @@ class TestStateHandlerReadsFromAuthorizationSnapshot:
query_id=1,
plugin_identity='test/runner',
resources=make_resources(),
available_apis={'state': True},
state_policy={'enable_state': True, 'state_scopes': ['conversation']},
state_context={'scope_keys': {'conversation': 'conv_key_xyz'}, 'binding_identity': 'binding_xyz'},
)
+10 -12
View File
@@ -119,18 +119,16 @@ class TestStateScopeHelpers:
thread_id='thread_001',
)
assert build_state_scope_key('conversation', event, binding, descriptor) == (
'conversation:plugin:test/my-runner/default:binding_a:conv_001:thread_001'
)
assert build_state_scope_key('actor', event, binding, descriptor) == (
'actor:plugin:test/my-runner/default:binding_a:user:user_001'
)
assert build_state_scope_key('subject', event, binding, descriptor) == (
'subject:plugin:test/my-runner/default:binding_a:message:msg_001'
)
assert build_state_scope_key('runner', event, binding, descriptor) == (
'runner:plugin:test/my-runner/default:binding_a'
)
keys = {
scope: build_state_scope_key(scope, event, binding, descriptor)
for scope in VALID_STATE_SCOPES
}
assert keys['conversation'].startswith('conversation:v2:')
assert keys['actor'].startswith('actor:v2:')
assert keys['subject'].startswith('subject:v2:')
assert keys['runner'].startswith('runner:v2:')
assert len(set(keys.values())) == len(keys)
def test_scope_key_missing_identity_returns_none(self):
descriptor = make_descriptor()