Merge pull request #2672 from langbot-app/test/quality-runtime-oracles-20261017

test(runtime): assert filter and message delivery semantics
This commit is contained in:
huanghuoguoguo
2026-10-17 20:10:09 +08:00
committed by GitHub
5 changed files with 158 additions and 122 deletions
+8 -2
View File
@@ -201,8 +201,14 @@ class TestBotEndpoints:
assert response.status_code == 200 assert response.status_code == 200
data = await response.get_json() data = await response.get_json()
assert data['code'] == 0 assert data['code'] == 0
assert 'data' in data assert data['data']['bots'] == [
assert 'bots' in data['data'] {
'uuid': 'test-bot-uuid',
'name': 'Test Bot',
'platform': 'telegram',
'pipeline_uuid': 'test-pipeline-uuid',
}
]
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_create_bot_success(self, quart_test_client): async def test_create_bot_success(self, quart_test_client):
+10 -3
View File
@@ -560,19 +560,18 @@ class TestMessageAggregatorAddMessage:
agg = aggregator.MessageAggregator(app) agg = aggregator.MessageAggregator(app)
chain = text_chain('hello')
event = friend_message_event(chain)
adapter = mock_adapter() adapter = mock_adapter()
# Add messages up to MAX_BUFFER_MESSAGES # Add messages up to MAX_BUFFER_MESSAGES
for i in range(aggregator.MAX_BUFFER_MESSAGES): for i in range(aggregator.MAX_BUFFER_MESSAGES):
chain = text_chain(f'message {i}')
await agg.add_message( await agg.add_message(
execution_context=execution_context(pipeline_uuid='test-pipeline'), execution_context=execution_context(pipeline_uuid='test-pipeline'),
bot_uuid='test-bot', bot_uuid='test-bot',
launcher_type=provider_session.LauncherTypes.PERSON, launcher_type=provider_session.LauncherTypes.PERSON,
launcher_id=12345, launcher_id=12345,
sender_id=12345, sender_id=12345,
message_event=event, message_event=friend_message_event(chain),
message_chain=chain, message_chain=chain,
adapter=adapter, adapter=adapter,
pipeline_uuid='test-pipeline', pipeline_uuid='test-pipeline',
@@ -588,6 +587,14 @@ class TestMessageAggregatorAddMessage:
'test-pipeline', 'test-pipeline',
) )
assert session_id not in agg.buffers or len(agg.buffers[session_id].messages) == 0 assert session_id not in agg.buffers or len(agg.buffers[session_id].messages) == 0
app.query_pool.add_query.assert_awaited_once()
forwarded = app.query_pool.add_query.call_args.kwargs
assert str(forwarded['message_chain']) == '\n'.join(
f'message {i}' for i in range(aggregator.MAX_BUFFER_MESSAGES)
)
assert forwarded['execution_context'] == context
assert forwarded['bot_uuid'] == 'test-bot'
assert forwarded['pipeline_uuid'] == 'test-pipeline'
class TestMessageAggregatorMerge: class TestMessageAggregatorMerge:
@@ -189,7 +189,7 @@ class TestChatMessageHandlerReal:
async def test_user_message_alter_string(self, fake_app, mock_event_ctx, set_runner): async def test_user_message_alter_string(self, fake_app, mock_event_ctx, set_runner):
"""user_message_alter as string updates query.user_message.""" """user_message_alter as string updates query.user_message."""
from tests.factories import text_query from tests.factories import text_query
from langbot_plugin.api.entities.builtin.provider.message import Message from langbot_plugin.api.entities.builtin.provider.message import Message, ContentElement
chat = get_chat_handler() chat = get_chat_handler()
@@ -200,7 +200,8 @@ class TestChatMessageHandlerReal:
query = text_query('original') query = text_query('original')
query.adapter = Mock() query.adapter = Mock()
query.adapter.is_stream_output_supported = AsyncMock(return_value=False) query.adapter.is_stream_output_supported = AsyncMock(return_value=False)
query.user_message = Message(role='user', content=[]) query.user_message = Message(role='user', content=[ContentElement.from_text('original')])
received_contents = []
class QuickRunner: class QuickRunner:
name = 'local-agent' name = 'local-agent'
@@ -210,6 +211,7 @@ class TestChatMessageHandlerReal:
self.config = config self.config = config
async def run(self, query): async def run(self, query):
received_contents.append(query.user_message.content)
yield Message(role='assistant', content='ok') yield Message(role='assistant', content='ok')
set_runner(QuickRunner) set_runner(QuickRunner)
@@ -220,7 +222,9 @@ class TestChatMessageHandlerReal:
async for result in handler.handle(query): async for result in handler.handle(query):
results.append(result) results.append(result)
assert query.user_message.content is not None expected_content = [ContentElement.from_text('altered text')]
assert query.user_message.content == expected_content
assert received_contents == [expected_content]
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_adapter_without_stream_method_defaults_non_stream(self, fake_app, mock_event_ctx, set_runner): async def test_adapter_without_stream_method_defaults_non_stream(self, fake_app, mock_event_ctx, set_runner):
+72 -44
View File
@@ -1,61 +1,89 @@
"""Regression tests for the production Matrix ``!relogin`` command."""
from __future__ import annotations from __future__ import annotations
"""Regression test for the Matrix ``!relogin`` command (``_handle_relogin_command``). from types import SimpleNamespace
from unittest.mock import AsyncMock, Mock, call
The old code placed ``logout_cmd = ...`` on a line after an unconditional import pytest
``continue`` inside the ``if not bridge.login_command or not bridge.dm_room_id``
branch. Because the ``continue`` always fired, ``logout_cmd`` was never assigned
on the configured path, so any bridge with both ``login_command`` and
``dm_room_id`` raised ``UnboundLocalError``. The fix moves the assignment above
the ``if`` so it runs for configured bridges.
These tests replicate the loop's control flow with a minimal fake bridge so they from langbot.pkg.platform.sources import matrix
run without the Matrix SDK / langbot_plugin dependency.
"""
class FakeBridge: @pytest.fixture
def __init__(self, user_id: str, login_command: str, logout_command: str = '', dm_room_id: str | None = None): def bridge():
self.user_id = user_id bridge = matrix.BridgeState('@u:example.org', 'login', '', '', '')
self.login_command = login_command bridge.dm_room_id = '!bridge:example.org'
self.logout_command = logout_command bridge.logged_in = True
self.dm_room_id = dm_room_id return bridge
def _relogin_commands(bridges: list[FakeBridge]) -> list[str]: @pytest.fixture
"""Return the logout commands the fixed loop would send for each bridge.""" def adapter(monkeypatch):
commands: list[str] = [] adapter = matrix.MatrixAdapter.model_construct(
for bridge in bridges: client=SimpleNamespace(room_send=AsyncMock()),
if not bridge.login_command or not bridge.dm_room_id: logger=None,
continue )
# Use configured logout command, fallback to deriving from login command. monkeypatch.setattr(matrix.asyncio, 'sleep', AsyncMock())
logout_cmd = bridge.logout_command or bridge.login_command.replace('login', 'logout') monkeypatch.setattr(matrix.MatrixAdapter, '_restart_bridge_login', Mock())
commands.append(logout_cmd) return adapter
return commands
def test_configured_bridge_produces_logout_command_without_error() -> None: async def test_configured_bridge_produces_logout_command_without_error(adapter, bridge) -> None:
bridges = [FakeBridge('@u:example.org', 'login', dm_room_id='!room:example.org')] adapter._bridges = [bridge]
# Old code raised UnboundLocalError here; the fix must return the derived
# logout command (no configured logout_command -> derive from login). await adapter._handle_relogin_command('!admin:example.org')
assert _relogin_commands(bridges) == ['logout']
assert adapter.client.room_send.await_count == 2
assert adapter.client.room_send.await_args_list[0] == call(
room_id='!bridge:example.org',
message_type='m.room.message',
content={'msgtype': 'm.text', 'body': 'logout'},
)
adapter._restart_bridge_login.assert_called_once_with(bridge)
assert bridge.logged_in is False
def test_configured_logout_command_is_used_verbatim() -> None: async def test_configured_logout_command_is_used_verbatim(adapter, bridge) -> None:
bridges = [FakeBridge('@u:example.org', 'login', logout_command='leave', dm_room_id='!room:example.org')] bridge.logout_command = 'leave'
assert _relogin_commands(bridges) == ['leave'] adapter._bridges = [bridge]
await adapter._handle_relogin_command('!admin:example.org')
assert adapter.client.room_send.await_count == 2
assert adapter.client.room_send.await_args_list[0] == call(
room_id='!bridge:example.org',
message_type='m.room.message',
content={'msgtype': 'm.text', 'body': 'leave'},
)
adapter._restart_bridge_login.assert_called_once_with(bridge)
assert bridge.logged_in is False
def test_skipped_bridge_is_ignored() -> None: async def test_skipped_bridge_is_ignored(adapter, bridge) -> None:
# Missing dm_room_id -> skipped, no command emitted. bridge.dm_room_id = None
bridges = [FakeBridge('@u:example.org', 'login', dm_room_id=None)] adapter._bridges = [bridge]
assert _relogin_commands(bridges) == []
await adapter._handle_relogin_command('!admin:example.org')
adapter.client.room_send.assert_awaited_once()
assert adapter.client.room_send.await_args.kwargs['room_id'] == '!admin:example.org'
adapter._restart_bridge_login.assert_not_called()
assert bridge.logged_in is True
def test_relogin_never_raises_for_mixed_configurations() -> None: async def test_relogin_never_raises_for_mixed_configurations(adapter, bridge) -> None:
bridges = [ missing_login = matrix.BridgeState('@skip:example.org', '', '', '', '')
FakeBridge('@skip:example.org', '', dm_room_id='!room:example.org'), # no login_command missing_login.dm_room_id = '!skip:example.org'
FakeBridge('@ok:example.org', 'login', dm_room_id='!room:example.org'), missing_room = matrix.BridgeState('@skip2:example.org', 'login', '', '', '')
FakeBridge('@skip2:example.org', 'login', dm_room_id=None), # no dm_room_id adapter._bridges = [missing_login, bridge, missing_room]
await adapter._handle_relogin_command('!admin:example.org')
assert [sent.kwargs['room_id'] for sent in adapter.client.room_send.await_args_list] == [
'!bridge:example.org',
'!admin:example.org',
] ]
assert _relogin_commands(bridges) == ['logout'] assert adapter.client.room_send.await_args_list[0].kwargs['content']['body'] == 'logout'
adapter._restart_bridge_login.assert_called_once_with(bridge)
assert bridge.logged_in is False
@@ -10,6 +10,9 @@ from __future__ import annotations
from importlib import import_module from importlib import import_module
import pytest
from sqlalchemy import create_engine, select, text
def get_qdrant_module(): def get_qdrant_module():
"""Lazy import qdrant module.""" """Lazy import qdrant module."""
@@ -261,6 +264,37 @@ class TestPgVectorFilterConversion:
Tests use only these supported fields. Tests use only these supported fields.
""" """
@pytest.fixture
def matching_file_ids(self):
"""Execute generated predicates against fixed rows, including the range boundary."""
pgvector_module = get_pgvector_module()
engine = create_engine('sqlite://')
try:
with engine.begin() as connection:
connection.execute(text('CREATE TABLE langbot_vectors (file_id TEXT, chunk_uuid TEXT, text TEXT)'))
connection.execute(
text('INSERT INTO langbot_vectors VALUES (:file_id, :chunk_uuid, :text)'),
[
{'file_id': 'abc', 'chunk_uuid': 'uuid_099', 'text': 'keep'},
{'file_id': 'deleted', 'chunk_uuid': 'uuid_100', 'text': ''},
{'file_id': 'def', 'chunk_uuid': 'uuid_101', 'text': 'keep'},
{'file_id': 'outside', 'chunk_uuid': 'uuid_102', 'text': 'keep'},
],
)
def matching(filter_dict):
entry = pgvector_module.PgVectorEntry
statement = (
select(entry.file_id)
.where(*pgvector_module._build_pg_conditions(filter_dict))
.order_by(entry.chunk_uuid)
)
return connection.execute(statement).scalars().all()
yield matching
finally:
engine.dispose()
def test_empty_filter_returns_empty_list(self): def test_empty_filter_returns_empty_list(self):
"""Empty filter dict returns empty list.""" """Empty filter dict returns empty list."""
pgvector_module = get_pgvector_module() pgvector_module = get_pgvector_module()
@@ -268,73 +302,40 @@ class TestPgVectorFilterConversion:
result = pgvector_module._build_pg_conditions({}) result = pgvector_module._build_pg_conditions({})
assert result == [] assert result == []
def test_eq_operator_creates_equality_condition(self): def test_eq_operator_creates_equality_condition(self, matching_file_ids):
"""$eq operator creates SQLAlchemy == condition.""" """$eq operator creates SQLAlchemy == condition."""
pgvector_module = get_pgvector_module() assert matching_file_ids({'file_id': 'abc'}) == ['abc']
result = pgvector_module._build_pg_conditions({'file_id': 'abc'}) def test_ne_operator_creates_inequality_condition(self, matching_file_ids):
assert len(result) == 1
# Verify it's a SQLAlchemy BinaryExpression
from sqlalchemy.sql.expression import BinaryExpression
assert isinstance(result[0], BinaryExpression)
def test_ne_operator_creates_inequality_condition(self):
"""$ne operator creates SQLAlchemy != condition.""" """$ne operator creates SQLAlchemy != condition."""
pgvector_module = get_pgvector_module() assert matching_file_ids({'file_id': {'$ne': 'deleted'}}) == ['abc', 'def', 'outside']
result = pgvector_module._build_pg_conditions({'file_id': {'$ne': 'deleted'}}) def test_comparison_operators(self, matching_file_ids):
assert len(result) == 1
# Operator should be ne (not equals)
assert '!=' in str(result[0]) or 'ne' in str(result[0].operator)
def test_comparison_operators(self):
"""$gt, $gte, $lt, $lte create comparison conditions.""" """$gt, $gte, $lt, $lte create comparison conditions."""
pgvector_module = get_pgvector_module() for op, expected_ids in [
('$gt', ['def', 'outside']),
# Test all comparison operators with supported field ('$gte', ['deleted', 'def', 'outside']),
for op, expected_op in [ ('$lt', ['abc']),
('$gt', '>'), ('$lte', ['abc', 'deleted']),
('$gte', '>='),
('$lt', '<'),
('$lte', '<='),
]: ]:
result = pgvector_module._build_pg_conditions({'chunk_uuid': {op: 'uuid_100'}}) assert matching_file_ids({'chunk_uuid': {op: 'uuid_100'}}) == expected_ids
assert len(result) == 1
assert expected_op in str(result[0])
def test_in_operator_creates_in_condition(self): def test_in_operator_creates_in_condition(self, matching_file_ids):
"""$in operator creates SQLAlchemy in_ condition.""" """$in operator creates SQLAlchemy in_ condition."""
pgvector_module = get_pgvector_module() assert matching_file_ids({'file_id': {'$in': ['abc', 'deleted']}}) == ['abc', 'deleted']
result = pgvector_module._build_pg_conditions({'file_id': {'$in': ['a', 'b', 'c']}}) def test_nin_operator_creates_notin_condition(self, matching_file_ids):
assert len(result) == 1
assert 'IN' in str(result[0]).upper()
def test_nin_operator_creates_notin_condition(self):
"""$nin operator creates SQLAlchemy notin_ condition.""" """$nin operator creates SQLAlchemy notin_ condition."""
pgvector_module = get_pgvector_module() assert matching_file_ids({'file_id': {'$nin': ['abc', 'deleted']}}) == ['def', 'outside']
result = pgvector_module._build_pg_conditions({'file_id': {'$nin': ['a', 'b']}}) def test_multiple_conditions_list(self, matching_file_ids):
assert len(result) == 1
assert 'NOT IN' in str(result[0]).upper()
def test_multiple_conditions_list(self):
"""Multiple conditions return list of conditions.""" """Multiple conditions return list of conditions."""
pgvector_module = get_pgvector_module() assert matching_file_ids(
result = pgvector_module._build_pg_conditions(
{ {
'file_id': 'abc', 'file_id': {'$in': ['abc', 'deleted']},
'chunk_uuid': {'$ne': 'def'}, 'chunk_uuid': {'$ne': 'uuid_100'},
} }
) ) == ['abc']
assert len(result) == 2
def test_unsupported_field_dropped(self): def test_unsupported_field_dropped(self):
"""Unsupported fields are dropped (not in _PG_SUPPORTED_FIELDS).""" """Unsupported fields are dropped (not in _PG_SUPPORTED_FIELDS)."""
@@ -343,27 +344,17 @@ class TestPgVectorFilterConversion:
result = pgvector_module._build_pg_conditions({'unknown_field': 'value'}) result = pgvector_module._build_pg_conditions({'unknown_field': 'value'})
assert result == [] assert result == []
def test_uuid_alias_resolved(self): def test_uuid_alias_resolved(self, matching_file_ids):
"""'uuid' alias is resolved to 'chunk_uuid'.""" """'uuid' alias is resolved to 'chunk_uuid'."""
pgvector_module = get_pgvector_module() assert matching_file_ids({'uuid': 'uuid_100'}) == ['deleted']
result = pgvector_module._build_pg_conditions({'uuid': 'abc'}) def test_supported_fields_only(self, matching_file_ids):
assert len(result) == 1
# Should reference chunk_uuid column
assert 'chunk_uuid' in str(result[0])
def test_supported_fields_only(self):
"""Only supported fields (text, file_id, chunk_uuid) are kept.""" """Only supported fields (text, file_id, chunk_uuid) are kept."""
pgvector_module = get_pgvector_module() assert matching_file_ids(
result = pgvector_module._build_pg_conditions(
{ {
'text': {'$ne': ''}, 'text': {'$ne': ''},
'file_id': 'abc', 'file_id': {'$in': ['abc', 'deleted', 'outside']},
'chunk_uuid': {'$in': ['x', 'y']}, 'chunk_uuid': {'$in': ['uuid_099', 'uuid_100', 'uuid_101']},
'unsupported': 'value', 'unsupported': 'value',
} }
) ) == ['abc']
assert len(result) == 3 # Only supported fields