fix(agent-runner): harden state and event APIs

This commit is contained in:
huanghuoguoguo
2026-06-10 13:30:29 +08:00
parent 02aa244785
commit 8938ef7412
5 changed files with 216 additions and 85 deletions

View File

@@ -6,8 +6,15 @@ from unittest.mock import MagicMock
import pytest
from sqlalchemy.ext.asyncio import create_async_engine
from langbot.pkg.agent.runner.event_log_store import EventLogStore
from langbot.pkg.agent.runner.session_registry import AgentRunSessionRegistry
from langbot.pkg.entity.persistence import event_log as event_log_model
from langbot.pkg.entity.persistence.base import Base
from langbot.pkg.plugin.handler import RuntimeConnectionHandler
from langbot_plugin.api.entities.builtin.agent_runner.page_results import (
AgentEventRecord,
EventPage,
)
from langbot_plugin.entities.io.actions.enums import PluginToRuntimeAction
from .conftest import make_resources
@@ -37,6 +44,9 @@ def session_registry(monkeypatch):
@pytest.fixture
async def db_engine():
engine = create_async_engine('sqlite+aiosqlite:///:memory:')
assert event_log_model.EventLog.__tablename__ == 'event_log'
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
yield engine
await engine.dispose()
@@ -144,3 +154,69 @@ async def test_event_page_rejects_cross_conversation(session_registry, db_engine
assert result.code != 0
assert 'not accessible' in result.message.lower()
@pytest.mark.asyncio
async def test_event_get_returns_sdk_record_projection(session_registry, db_engine):
await _register_session(session_registry, permissions={'events': ['get']})
store = EventLogStore(db_engine)
event_id = await store.append_event(
event_id='evt_projection_1',
event_type='message.received',
source='platform',
conversation_id='conv_1',
actor_type='user',
actor_id='user_1',
input_summary='hello',
input_json={'internal': 'not part of AgentEventRecord'},
run_id='run_1',
runner_id='plugin:test/runner/default',
)
handler = _handler(db_engine, session_registry)
event_get = handler.actions[PluginToRuntimeAction.EVENT_GET.value]
result = await event_get({
'run_id': 'run_1',
'event_id': event_id,
'caller_plugin_identity': 'test/runner',
})
assert result.code == 0
AgentEventRecord.model_validate(result.data)
assert 'id' not in result.data
assert 'input_json' not in result.data
assert 'run_id' not in result.data
assert 'runner_id' not in result.data
assert result.data['seq'] is not None
assert result.data['cursor'] == str(result.data['seq'])
@pytest.mark.asyncio
async def test_event_page_returns_sdk_page_projection(session_registry, db_engine):
await _register_session(session_registry, permissions={'events': ['page']})
store = EventLogStore(db_engine)
await store.append_event(
event_id='evt_projection_page_1',
event_type='message.received',
source='platform',
conversation_id='conv_1',
input_json={'internal': 'not part of AgentEventRecord'},
run_id='run_other',
runner_id='plugin:test/runner/default',
)
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
page = EventPage.model_validate(result.data)
assert len(page.items) == 1
item = result.data['items'][0]
assert 'id' not in item
assert 'input_json' not in item
assert 'run_id' not in item
assert 'runner_id' not in item

View File

@@ -1,6 +1,7 @@
"""Tests for persistent AgentRunner state store."""
from __future__ import annotations
import asyncio
import os
import tempfile
@@ -212,6 +213,26 @@ class TestPersistentStateStore:
snapshot = await persistent_store.build_snapshot_from_event(event, binding, descriptor)
assert snapshot['conversation']['test_key'] == {'nested': 'value'}
@pytest.mark.asyncio
async def test_concurrent_first_state_set_uses_upsert(self, persistent_store):
scope_key = 'conversation:runner:binding:conv_concurrent'
async def set_value(value: int):
return await persistent_store.state_set(
scope_key=scope_key,
state_key='external.concurrent',
value={'value': value},
runner_id='plugin:test/my-runner/default',
binding_identity='binding_001',
scope='conversation',
)
results = await asyncio.gather(*(set_value(value) for value in range(8)))
assert all(success is True and error is None for success, error in results)
stored = await persistent_store.state_get(scope_key, 'external.concurrent')
assert stored in [{'value': value} for value in range(8)]
@pytest.mark.asyncio
async def test_state_api_methods_normalize_public_key_aliases(self, persistent_store):
scope_key = 'conversation:runner:binding:conv_001'