chore(merge): sync master and preserve monitoring test coverage

This commit is contained in:
BiFangKNT
2026-10-18 12:43:44 +08:00
174 changed files with 7645 additions and 3218 deletions
@@ -580,3 +580,4 @@ class TestChatHandlerAsyncBehavior:
assert len(results) == 1
assert results[0].result_type == entities.ResultType.CONTINUE
assert len(query.resp_messages) == 1
assert query.resp_messages[0].content == 'Reply from plugin'
@@ -303,6 +303,10 @@ class TestRunnerContextProtocolV1:
assert ctx.event is not None
assert ctx.event.event_type == 'message.received'
from pydantic import ValidationError
with pytest.raises(ValidationError, match='event'):
RunnerContext.model_validate(ctx.model_dump(exclude={'event'}))
def test_sdk_context_has_no_history_message_fields(self):
"""RunnerContext should not expose inline history message fields."""
@@ -332,16 +336,6 @@ class TestRunnerContextProtocolV1:
assert not hasattr(ctx, 'bootstrap')
class TestHostManagedHistoryNotInProtocol:
"""Test that Host-managed history payloads are not in Protocol v1."""
def test_messages_not_in_sdk_context_top_level(self):
"""RunnerContext should not expose top-level history messages."""
ctx_fields = RunnerContext.model_fields.keys()
assert 'messages' not in ctx_fields
class TestSDKResultProtocolV1:
"""Test SDK RunnerResult for Protocol v1."""
@@ -355,6 +349,8 @@ class TestSDKResultProtocolV1:
)
assert result.run_id == 'run_1'
with pytest.raises(TypeError, match='run_id'):
RunnerResult.message_completed(message=Message(role='assistant', content='Hello'))
# Fixtures
@@ -139,30 +139,31 @@ class TestTranscriptStore:
"""Test TranscriptStore operations."""
@pytest.mark.asyncio
async def test_append_transcript(self, mock_db_engine):
async def test_append_transcript(self):
"""Test appending a transcript item."""
from unittest.mock import AsyncMock, MagicMock, patch
from sqlalchemy.ext.asyncio import create_async_engine
from langbot.pkg.entity.persistence.base import Base
from uuid import UUID
store = TranscriptStore(mock_db_engine)
mock_session = AsyncMock()
mock_session.add = MagicMock()
mock_session.commit = AsyncMock()
# Mock _get_next_seq
with patch.object(store, '_get_next_seq', return_value=1):
with patch.object(store, '_session_factory') as mock_factory:
mock_factory.return_value.__aenter__.return_value = mock_session
transcript_id = await store.append_transcript(
transcript_id=None, # Auto-generate
event_id='evt_1',
conversation_id='conv_1',
role='user',
content='Hello',
)
assert transcript_id is not None
engine = create_async_engine('sqlite+aiosqlite:///:memory:')
try:
async with engine.begin() as connection:
await connection.run_sync(Base.metadata.create_all)
store = TranscriptStore(engine)
transcript_id = await store.append_transcript(
transcript_id=None,
event_id='evt_1',
conversation_id='conv_1',
role='user',
content='Hello',
)
assert UUID(transcript_id).version == 4
items, _, _, _ = await store.page_transcript('conv_1')
assert [(item['transcript_id'], item['event_id'], item['role'], item['content']) for item in items] == [
(transcript_id, 'evt_1', 'user', 'Hello')
]
finally:
await engine.dispose()
class TestEventLogStoreRealSQLite:
@@ -258,6 +259,7 @@ class TestEventLogStoreRealSQLite:
async def test_get_latest_cursor(self, db_engine):
"""Test get_latest_cursor with real DB."""
store = EventLogStore(db_engine)
assert await store.get_latest_cursor('conv_cursor') is None
# Append events
for i in range(3):
@@ -270,8 +272,12 @@ class TestEventLogStoreRealSQLite:
# Get latest cursor
cursor = await store.get_latest_cursor('conv_cursor')
assert cursor is not None
assert int(cursor) > 0
assert cursor == '3'
await store.append_event(
event_id='other', event_type='message.received', source='platform', conversation_id='other'
)
assert await store.get_latest_cursor('conv_cursor') == '3'
assert await store.get_latest_cursor('other') == '4'
@pytest.mark.asyncio
async def test_cleanup_events_older_than(self, db_engine):
@@ -205,14 +205,43 @@ def official_plugin_root():
@pytest.mark.parametrize(
'runner,folder,method,error_name,local_identity',
[
('DifyAgent', 'dify-agent', '_get_user_tag', 'DifyConfigError', 'person_Session_007'),
('N8nAgent', 'n8n-agent', '_get_user_tag', 'N8nConfigError', 'person_Session_007'),
('CozeAgent', 'coze-agent', '_get_user_id', 'CozeConfigError', 'person_launcher-123'),
('TboxAgent', 'tbox-agent', '_get_user_id', 'TboxConfigError', 'bot-uuid-123'),
pytest.param(
'DifyAgent',
'dify-agent',
'_get_user_tag',
'DifyConfigError',
'person_Session_007',
marks=pytest.mark.contract('dify_identity'),
),
pytest.param(
'N8nAgent',
'n8n-agent',
'_get_user_tag',
'N8nConfigError',
'person_Session_007',
marks=pytest.mark.contract('n8n_identity'),
),
pytest.param(
'CozeAgent',
'coze-agent',
'_get_user_id',
'CozeConfigError',
'person_launcher-123',
marks=pytest.mark.contract('coze_identity'),
),
pytest.param(
'TboxAgent',
'tbox-agent',
'_get_user_id',
'TboxConfigError',
'bot-uuid-123',
marks=pytest.mark.contract('tbox_identity'),
),
],
)
@pytest.mark.parametrize('missing', [False, True], ids=['trusted-identity', 'missing-identity'])
@pytest.mark.parametrize('profile', ['dedicated', 'shared'])
@pytest.mark.plugin_identity
async def test_real_plugin_identity_helper_with_host_payload(
mock_query, monkeypatch, official_plugin_root, runner, folder, method, error_name, local_identity, missing, profile
):
@@ -789,6 +789,13 @@ async def test_orchestrator_applies_state_updates_and_suppresses_protocol_event(
assert [message.content for message in messages] == ['state saved']
# State is persisted to the database via PersistentStateStore.
from langbot.pkg.agent.runner.persistent_state_store import get_persistent_state_store
event = QueryEntryAdapter.query_to_event(query)
config = QueryEntryAdapter.config_to_agent_config(query, RUNNER_ID)
binding = AgentBindingResolver().resolve_one(event, [config])
snapshot = await get_persistent_state_store(db_engine).build_snapshot_from_event(event, binding, descriptor)
assert snapshot['conversation'].get('external.conversation_id') == 'external_conv_123'
@pytest.mark.asyncio
+6
View File
@@ -124,6 +124,7 @@ class TestRegistryDiscovery:
async def test_discover_caches_results(self):
"""Discovery should cache results."""
ap = FakeApplication()
ap.plugin_connector.list_runners = AsyncMock(wraps=ap.plugin_connector.list_runners)
registry = RunnerRegistry(ap)
# First discovery
@@ -134,6 +135,11 @@ class TestRegistryDiscovery:
assert registry._cache is not None
assert len(runners1) == len(runners2)
ap.plugin_connector.list_runners.assert_awaited_once()
assert [runner.id for runner in runners2] == [
'plugin:langbot-team/LocalAgent/default',
'plugin:alice/my-agent/custom',
]
@pytest.mark.asyncio
async def test_discover_handles_plugin_disabled(self):
@@ -156,6 +156,7 @@ class TestNormalizeRunCompleted:
assert result is not None
assert isinstance(result, provider_message.Message)
assert (result.role, result.content) == ('assistant', 'Final response')
@pytest.mark.asyncio
async def test_normalize_run_completed_without_message(self):
@@ -175,6 +175,22 @@ async def _create_run(
)
@pytest.fixture
async def ledger_snapshot(db_engine):
await _create_run(
db_engine, run_id='workspace_guard', workspace_id='workspace_guard', plugin_identity='guard/runner'
)
async def snapshot():
async with AsyncSession(db_engine) as session:
return [
(await session.execute(sqlalchemy.select(*model.__table__.c).order_by(model.id))).mappings().all()
for model in (agent_run_model.AgentRun, agent_run_model.AgentRunEvent)
]
return snapshot
@pytest.mark.asyncio
async def test_run_get_returns_current_run(session_registry, db_engine):
await _register_session(session_registry, available_apis={'run_get': True})
@@ -264,6 +280,7 @@ async def test_run_list_filters_same_scope_different_runner_owner(session_regist
async def test_non_admin_target_run_actions_reject_same_scope_different_runner_owner(
session_registry,
db_engine,
ledger_snapshot,
):
await _register_session(
session_registry,
@@ -351,10 +368,12 @@ async def test_non_admin_target_run_actions_reject_same_scope_different_runner_o
),
]
before = await ledger_snapshot()
for action, payload in calls:
result = await handler.actions[action](payload)
assert result.code != 0, action
assert 'not accessible' in result.message.lower()
assert await ledger_snapshot() == before
@pytest.mark.asyncio
@@ -1092,11 +1111,12 @@ async def test_run_append_result_uses_cas_and_redacts_metadata(session_registry,
@pytest.mark.asyncio
async def test_run_append_result_rejects_side_effecting_result_type(session_registry, db_engine):
async def test_run_append_result_rejects_side_effecting_result_type(session_registry, db_engine, ledger_snapshot):
await _register_session(session_registry, available_apis={'run_append_result': True})
await _create_run(db_engine)
handler = _handler(db_engine)
run_append_result = handler.actions[PluginToRuntimeAction.RUN_APPEND_RESULT.value]
before = await ledger_snapshot()
result = await run_append_result(
{
@@ -1112,14 +1132,16 @@ async def test_run_append_result_rejects_side_effecting_result_type(session_regi
assert result.code != 0
assert 'canonical runner result path' in result.message
assert await ledger_snapshot() == before
@pytest.mark.asyncio
async def test_run_append_result_rejects_unknown_result_type(session_registry, db_engine):
async def test_run_append_result_rejects_unknown_result_type(session_registry, db_engine, ledger_snapshot):
await _register_session(session_registry, available_apis={'run_append_result': True})
await _create_run(db_engine)
handler = _handler(db_engine)
run_append_result = handler.actions[PluginToRuntimeAction.RUN_APPEND_RESULT.value]
before = await ledger_snapshot()
result = await run_append_result(
{
@@ -1135,14 +1157,16 @@ async def test_run_append_result_rejects_unknown_result_type(session_registry, d
assert result.code != 0
assert 'unknown result type' in result.message
assert await ledger_snapshot() == before
@pytest.mark.asyncio
async def test_run_append_result_validates_known_payloads(session_registry, db_engine):
async def test_run_append_result_validates_known_payloads(session_registry, db_engine, ledger_snapshot):
await _register_session(session_registry, available_apis={'run_append_result': True})
await _create_run(db_engine)
handler = _handler(db_engine)
run_append_result = handler.actions[PluginToRuntimeAction.RUN_APPEND_RESULT.value]
before = await ledger_snapshot()
result = await run_append_result(
{
@@ -1158,10 +1182,13 @@ async def test_run_append_result_validates_known_payloads(session_registry, db_e
assert result.code != 0
assert 'invalid action.requested payload' in result.message
assert await ledger_snapshot() == before
@pytest.mark.asyncio
async def test_run_append_and_finalize_claimed_target_require_active_claim(session_registry, db_engine):
async def test_run_append_and_finalize_claimed_target_require_active_claim(
session_registry, db_engine, ledger_snapshot
):
await _register_session(
session_registry,
available_apis={'run_append_result': True, 'run_finalize': True},
@@ -1185,6 +1212,7 @@ async def test_run_append_and_finalize_claimed_target_require_active_claim(sessi
handler = _handler(db_engine)
run_append_result = handler.actions[PluginToRuntimeAction.RUN_APPEND_RESULT.value]
run_finalize = handler.actions[PluginToRuntimeAction.RUN_FINALIZE.value]
before = await ledger_snapshot()
missing_claim = await run_append_result(
{
@@ -1198,6 +1226,9 @@ async def test_run_append_and_finalize_claimed_target_require_active_claim(sessi
},
}
)
assert missing_claim.code != 0
assert 'active claim ownership' in missing_claim.message
assert await ledger_snapshot() == before
wrong_claim = await run_finalize(
{
'run_id': 'run_1',
@@ -1208,6 +1239,9 @@ async def test_run_append_and_finalize_claimed_target_require_active_claim(sessi
'status': 'completed',
}
)
assert wrong_claim.code != 0
assert 'claim ownership is not active' in wrong_claim.message
assert await ledger_snapshot() == before
appended = await run_append_result(
{
'run_id': 'run_1',
@@ -1233,10 +1267,6 @@ async def test_run_append_and_finalize_claimed_target_require_active_claim(sessi
}
)
assert missing_claim.code != 0
assert 'active claim ownership' in missing_claim.message
assert wrong_claim.code != 0
assert 'claim ownership is not active' in wrong_claim.message
assert appended.code == 0
assert finalized.code == 0
assert finalized.data['status'] == 'completed'
@@ -1471,7 +1501,7 @@ async def test_non_admin_run_claim_is_scoped_to_session_runner_and_conversation(
@pytest.mark.asyncio
async def test_non_admin_renew_and_release_reject_cross_runner_claim(session_registry, db_engine):
async def test_non_admin_renew_and_release_reject_cross_runner_claim(session_registry, db_engine, ledger_snapshot):
await _register_session(
session_registry,
available_apis={'run_renew_claim': True, 'run_release_claim': True},
@@ -1493,6 +1523,7 @@ async def test_non_admin_renew_and_release_reject_cross_runner_claim(session_reg
handler = _handler(db_engine)
run_renew_claim = handler.actions[PluginToRuntimeAction.RUN_RENEW_CLAIM.value]
run_release_claim = handler.actions[PluginToRuntimeAction.RUN_RELEASE_CLAIM.value]
before = await ledger_snapshot()
renewed = await run_renew_claim(
{
@@ -1503,6 +1534,9 @@ async def test_non_admin_renew_and_release_reject_cross_runner_claim(session_reg
'claim_token': claim['claim_token'],
}
)
assert renewed.code != 0
assert 'not accessible' in renewed.message.lower()
assert await ledger_snapshot() == before
released = await run_release_claim(
{
'run_id': 'run_1',
@@ -1513,14 +1547,13 @@ async def test_non_admin_renew_and_release_reject_cross_runner_claim(session_reg
}
)
assert renewed.code != 0
assert 'not accessible' in renewed.message.lower()
assert released.code != 0
assert 'not accessible' in released.message.lower()
assert await ledger_snapshot() == before
@pytest.mark.asyncio
async def test_non_admin_release_claim_cannot_finalize_run(session_registry, db_engine):
async def test_non_admin_release_claim_cannot_finalize_run(session_registry, db_engine, ledger_snapshot):
await _register_session(
session_registry,
available_apis={'run_claim': True, 'run_release_claim': True},
@@ -1549,6 +1582,7 @@ async def test_non_admin_release_claim_cannot_finalize_run(session_registry, db_
}
)
assert claimed.code == 0
before = await ledger_snapshot()
released = await run_release_claim(
{
@@ -1563,3 +1597,4 @@ async def test_non_admin_release_claim_cannot_finalize_run(session_registry, db_
assert released.code != 0
assert 'use run_finalize' in released.message
assert await ledger_snapshot() == before
@@ -5,6 +5,7 @@ from __future__ import annotations
import asyncio
import os
import tempfile
from copy import deepcopy
import pytest
from sqlalchemy.ext.asyncio import create_async_engine
@@ -132,6 +133,24 @@ class TestStateScopeHelpers:
assert keys['subject'].startswith('subject:v2:')
assert keys['runner'].startswith('runner:v2:')
assert len(set(keys.values())) == len(keys)
for dimension, value in [
('workspace_id', 'ws_other'),
('bot_id', 'bot_other'),
]:
other = deepcopy(event)
setattr(other, dimension, value)
for scope in VALID_STATE_SCOPES:
assert build_state_scope_key(scope, other, binding, descriptor) != keys[scope]
for scope, dimension, value in [
('conversation', 'conversation_id', 'conv_other'),
('conversation', 'thread_id', 'thread_other'),
('actor', 'actor_id', 'user_other'),
('subject', 'subject_id', 'msg_other'),
]:
other = deepcopy(event)
owner = other.actor if scope == 'actor' else other.subject if scope == 'subject' else other
setattr(owner, dimension, value)
assert build_state_scope_key(scope, other, binding, descriptor) != keys[scope]
def test_scope_key_missing_identity_returns_none(self):
descriptor = make_descriptor()