mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-13 01:06:03 +00:00
test(quality): fix fake tests and add missing coverage
P0 fixes: - telemetry: rewrite fake tests with real behavior verification (25 tests) - config: delete copied-source tests, use proper imports (2 deleted) - persistence: fix try-except pass to verify specific errors P1 fixes: - pipeline: add real FixedWindowAlgo tests instead of mocks (12 tests) - provider: add SessionManager and ToolManager tests (25 tests) - storage: add S3StorageProvider tests with moto mock (16 tests) - plugin: add handler action tests for setting inheritance (15 tests) - rag: add file storage and ZIP processing tests (21 tests) - vector: add VDB filter conversion tests (30 tests) P2 fixes: - pipeline/msgtrun: strengthen assertions for exact message count - api: add response structure validation in integration tests New test files: - provider/test_session_manager.py - provider/test_tool_manager.py - storage/test_s3storage.py - plugin/test_handler_actions.py - rag/test_file_storage.py - vector/test_vdb_filter_conversion.py Source code bugs documented: - provider: TokenManager.next_token() ZeroDivisionError - telemetry: send_tasks class variable shared state - command: empty command IndexError, unused parameters - utils: funcschema KeyError - entity: vector.py independent declarative_base Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
361
tests/unit_tests/vector/test_vdb_filter_conversion.py
Normal file
361
tests/unit_tests/vector/test_vdb_filter_conversion.py
Normal file
@@ -0,0 +1,361 @@
|
||||
"""Tests for VDB backend filter conversion functions.
|
||||
|
||||
Tests cover:
|
||||
- _build_qdrant_filter: Qdrant models.Filter conversion
|
||||
- _build_milvus_expr: Milvus boolean expression string conversion
|
||||
- _build_pg_conditions: PostgreSQL SQLAlchemy conditions conversion
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock
|
||||
from importlib import import_module
|
||||
|
||||
|
||||
def get_qdrant_module():
|
||||
"""Lazy import qdrant module."""
|
||||
return import_module('langbot.pkg.vector.vdbs.qdrant')
|
||||
|
||||
|
||||
def get_milvus_module():
|
||||
"""Lazy import milvus module."""
|
||||
return import_module('langbot.pkg.vector.vdbs.milvus')
|
||||
|
||||
|
||||
def get_pgvector_module():
|
||||
"""Lazy import pgvector module."""
|
||||
return import_module('langbot.pkg.vector.vdbs.pgvector_db')
|
||||
|
||||
|
||||
class TestQdrantFilterConversion:
|
||||
"""Tests for _build_qdrant_filter function."""
|
||||
|
||||
def test_empty_filter_returns_empty_must(self):
|
||||
"""Empty filter dict returns Filter with None must/must_not."""
|
||||
qdrant_module = get_qdrant_module()
|
||||
|
||||
result = qdrant_module._build_qdrant_filter({})
|
||||
assert result.must is None
|
||||
assert result.must_not is None
|
||||
|
||||
def test_eq_operator_creates_must_condition(self):
|
||||
"""$eq operator creates FieldCondition in must list."""
|
||||
qdrant_module = get_qdrant_module()
|
||||
from qdrant_client import models
|
||||
|
||||
result = qdrant_module._build_qdrant_filter({'file_id': 'abc'})
|
||||
|
||||
assert result.must is not None
|
||||
assert len(result.must) == 1
|
||||
condition = result.must[0]
|
||||
assert condition.key == 'file_id'
|
||||
assert isinstance(condition.match, models.MatchValue)
|
||||
assert condition.match.value == 'abc'
|
||||
|
||||
def test_ne_operator_creates_must_not_condition(self):
|
||||
"""$ne operator creates FieldCondition in must_not list."""
|
||||
qdrant_module = get_qdrant_module()
|
||||
from qdrant_client import models
|
||||
|
||||
result = qdrant_module._build_qdrant_filter({'status': {'$ne': 'deleted'}})
|
||||
|
||||
assert result.must_not is not None
|
||||
assert len(result.must_not) == 1
|
||||
condition = result.must_not[0]
|
||||
assert condition.key == 'status'
|
||||
assert isinstance(condition.match, models.MatchValue)
|
||||
assert condition.match.value == 'deleted'
|
||||
|
||||
def test_in_operator_creates_match_any(self):
|
||||
"""$in operator creates MatchAny condition."""
|
||||
qdrant_module = get_qdrant_module()
|
||||
from qdrant_client import models
|
||||
|
||||
result = qdrant_module._build_qdrant_filter({'file_type': {'$in': ['pdf', 'docx']}})
|
||||
|
||||
assert result.must is not None
|
||||
assert len(result.must) == 1
|
||||
condition = result.must[0]
|
||||
assert condition.key == 'file_type'
|
||||
assert isinstance(condition.match, models.MatchAny)
|
||||
assert condition.match.any == ['pdf', 'docx']
|
||||
|
||||
def test_nin_operator_creates_must_not_match_any(self):
|
||||
"""$nin operator creates MatchAny in must_not."""
|
||||
qdrant_module = get_qdrant_module()
|
||||
from qdrant_client import models
|
||||
|
||||
result = qdrant_module._build_qdrant_filter({'status': {'$nin': ['deleted', 'archived']}})
|
||||
|
||||
assert result.must_not is not None
|
||||
assert len(result.must_not) == 1
|
||||
condition = result.must_not[0]
|
||||
assert condition.key == 'status'
|
||||
assert isinstance(condition.match, models.MatchAny)
|
||||
assert condition.match.any == ['deleted', 'archived']
|
||||
|
||||
def test_range_operators_create_range_condition(self):
|
||||
"""$gt, $gte, $lt, $lte create Range conditions."""
|
||||
qdrant_module = get_qdrant_module()
|
||||
from qdrant_client import models
|
||||
|
||||
# Test $gt
|
||||
result = qdrant_module._build_qdrant_filter({'created_at': {'$gt': 100}})
|
||||
condition = result.must[0]
|
||||
assert isinstance(condition.range, models.Range)
|
||||
assert condition.range.gt == 100
|
||||
|
||||
# Test $gte
|
||||
result = qdrant_module._build_qdrant_filter({'created_at': {'$gte': 100}})
|
||||
condition = result.must[0]
|
||||
assert condition.range.gte == 100
|
||||
|
||||
# Test $lt
|
||||
result = qdrant_module._build_qdrant_filter({'created_at': {'$lt': 100}})
|
||||
condition = result.must[0]
|
||||
assert condition.range.lt == 100
|
||||
|
||||
# Test $lte
|
||||
result = qdrant_module._build_qdrant_filter({'created_at': {'$lte': 100}})
|
||||
condition = result.must[0]
|
||||
assert condition.range.lte == 100
|
||||
|
||||
def test_multiple_conditions_combined(self):
|
||||
"""Multiple conditions are combined in must/must_not."""
|
||||
qdrant_module = get_qdrant_module()
|
||||
|
||||
result = qdrant_module._build_qdrant_filter({
|
||||
'file_id': 'abc',
|
||||
'status': {'$ne': 'deleted'},
|
||||
'created_at': {'$gte': 100},
|
||||
})
|
||||
|
||||
assert len(result.must) == 2 # file_id eq + created_at gte
|
||||
assert len(result.must_not) == 1 # status ne
|
||||
|
||||
def test_implicit_eq_handled(self):
|
||||
"""Implicit $eq (bare value) is correctly handled."""
|
||||
qdrant_module = get_qdrant_module()
|
||||
from qdrant_client import models
|
||||
|
||||
result = qdrant_module._build_qdrant_filter({'field': 'value'})
|
||||
|
||||
assert result.must is not None
|
||||
condition = result.must[0]
|
||||
assert isinstance(condition.match, models.MatchValue)
|
||||
|
||||
|
||||
class TestMilvusFilterConversion:
|
||||
"""Tests for _build_milvus_expr function.
|
||||
|
||||
NOTE: Milvus only supports fields: 'text', 'file_id', 'chunk_uuid'
|
||||
Tests use only these supported fields.
|
||||
"""
|
||||
|
||||
def test_empty_filter_returns_empty_string(self):
|
||||
"""Empty filter dict returns empty string."""
|
||||
milvus_module = get_milvus_module()
|
||||
|
||||
result = milvus_module._build_milvus_expr({})
|
||||
assert result == ''
|
||||
|
||||
def test_eq_operator_expression(self):
|
||||
"""$eq operator creates == expression."""
|
||||
milvus_module = get_milvus_module()
|
||||
|
||||
result = milvus_module._build_milvus_expr({'file_id': 'abc'})
|
||||
assert result == 'file_id == "abc"'
|
||||
|
||||
def test_ne_operator_expression(self):
|
||||
"""$ne operator creates != expression."""
|
||||
milvus_module = get_milvus_module()
|
||||
|
||||
result = milvus_module._build_milvus_expr({'file_id': {'$ne': 'deleted'}})
|
||||
assert result == 'file_id != "deleted"'
|
||||
|
||||
def test_comparison_operators(self):
|
||||
"""$gt, $gte, $lt, $lte create comparison expressions."""
|
||||
milvus_module = get_milvus_module()
|
||||
|
||||
assert milvus_module._build_milvus_expr({'chunk_uuid': {'$gt': 'uuid_100'}}) == 'chunk_uuid > "uuid_100"'
|
||||
assert milvus_module._build_milvus_expr({'chunk_uuid': {'$gte': 'uuid_100'}}) == 'chunk_uuid >= "uuid_100"'
|
||||
assert milvus_module._build_milvus_expr({'chunk_uuid': {'$lt': 'uuid_100'}}) == 'chunk_uuid < "uuid_100"'
|
||||
assert milvus_module._build_milvus_expr({'chunk_uuid': {'$lte': 'uuid_100'}}) == 'chunk_uuid <= "uuid_100"'
|
||||
|
||||
def test_in_operator_expression(self):
|
||||
"""$in operator creates in [...] expression."""
|
||||
milvus_module = get_milvus_module()
|
||||
|
||||
result = milvus_module._build_milvus_expr({'file_id': {'$in': ['pdf', 'docx']}})
|
||||
assert result == 'file_id in ["pdf", "docx"]'
|
||||
|
||||
def test_nin_operator_expression(self):
|
||||
"""$nin operator creates not in [...] expression."""
|
||||
milvus_module = get_milvus_module()
|
||||
|
||||
result = milvus_module._build_milvus_expr({'file_id': {'$nin': ['deleted', 'archived']}})
|
||||
assert result == 'file_id not in ["deleted", "archived"]'
|
||||
|
||||
def test_multiple_conditions_joined_with_and(self):
|
||||
"""Multiple conditions are joined with 'and'."""
|
||||
milvus_module = get_milvus_module()
|
||||
|
||||
result = milvus_module._build_milvus_expr({
|
||||
'file_id': 'abc',
|
||||
'chunk_uuid': {'$ne': 'def'},
|
||||
})
|
||||
assert 'and' in result
|
||||
assert 'file_id == "abc"' in result
|
||||
assert 'chunk_uuid != "def"' in result
|
||||
|
||||
def test_string_value_escaped(self):
|
||||
"""String values are properly escaped."""
|
||||
milvus_module = get_milvus_module()
|
||||
|
||||
# Test backslash escape
|
||||
result = milvus_module._build_milvus_expr({'file_id': 'C:\\Users\\test'})
|
||||
assert '\\\\' in result
|
||||
|
||||
# Test quote escape
|
||||
result = milvus_module._build_milvus_expr({'file_id': 'test "quoted"'})
|
||||
assert '\\"' in result
|
||||
|
||||
def test_text_field_supported(self):
|
||||
"""text field is supported."""
|
||||
milvus_module = get_milvus_module()
|
||||
|
||||
result = milvus_module._build_milvus_expr({'text': 'some text'})
|
||||
assert result == 'text == "some text"'
|
||||
|
||||
def test_milvus_literal_function(self):
|
||||
"""Test _milvus_literal helper."""
|
||||
milvus_module = get_milvus_module()
|
||||
|
||||
assert milvus_module._milvus_literal('string') == '"string"'
|
||||
assert milvus_module._milvus_literal(42) == '42'
|
||||
assert milvus_module._milvus_literal(3.14) == '3.14'
|
||||
|
||||
def test_unsupported_field_dropped(self):
|
||||
"""Unsupported fields are dropped (not in _MILVUS_SUPPORTED_FIELDS)."""
|
||||
milvus_module = get_milvus_module()
|
||||
|
||||
result = milvus_module._build_milvus_expr({'unknown_field': 'value'})
|
||||
assert result == ''
|
||||
|
||||
def test_uuid_alias_resolved(self):
|
||||
"""'uuid' alias is resolved to 'chunk_uuid'."""
|
||||
milvus_module = get_milvus_module()
|
||||
|
||||
result = milvus_module._build_milvus_expr({'uuid': 'abc'})
|
||||
assert result.startswith('chunk_uuid')
|
||||
# uuid substring appears in chunk_uuid which is expected
|
||||
|
||||
|
||||
class TestPgVectorFilterConversion:
|
||||
"""Tests for _build_pg_conditions function.
|
||||
|
||||
NOTE: PGVector only supports fields: 'text', 'file_id', 'chunk_uuid'
|
||||
Tests use only these supported fields.
|
||||
"""
|
||||
|
||||
def test_empty_filter_returns_empty_list(self):
|
||||
"""Empty filter dict returns empty list."""
|
||||
pgvector_module = get_pgvector_module()
|
||||
|
||||
result = pgvector_module._build_pg_conditions({})
|
||||
assert result == []
|
||||
|
||||
def test_eq_operator_creates_equality_condition(self):
|
||||
"""$eq operator creates SQLAlchemy == condition."""
|
||||
pgvector_module = get_pgvector_module()
|
||||
|
||||
result = pgvector_module._build_pg_conditions({'file_id': 'abc'})
|
||||
|
||||
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."""
|
||||
pgvector_module = get_pgvector_module()
|
||||
|
||||
result = pgvector_module._build_pg_conditions({'file_id': {'$ne': 'deleted'}})
|
||||
|
||||
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."""
|
||||
pgvector_module = get_pgvector_module()
|
||||
|
||||
# Test all comparison operators with supported field
|
||||
for op, expected_op in [
|
||||
('$gt', '>'),
|
||||
('$gte', '>='),
|
||||
('$lt', '<'),
|
||||
('$lte', '<='),
|
||||
]:
|
||||
result = pgvector_module._build_pg_conditions({'chunk_uuid': {op: 'uuid_100'}})
|
||||
assert len(result) == 1
|
||||
assert expected_op in str(result[0])
|
||||
|
||||
def test_in_operator_creates_in_condition(self):
|
||||
"""$in operator creates SQLAlchemy in_ condition."""
|
||||
pgvector_module = get_pgvector_module()
|
||||
|
||||
result = pgvector_module._build_pg_conditions({'file_id': {'$in': ['a', 'b', 'c']}})
|
||||
|
||||
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."""
|
||||
pgvector_module = get_pgvector_module()
|
||||
|
||||
result = pgvector_module._build_pg_conditions({'file_id': {'$nin': ['a', 'b']}})
|
||||
|
||||
assert len(result) == 1
|
||||
assert 'NOT IN' in str(result[0]).upper()
|
||||
|
||||
def test_multiple_conditions_list(self):
|
||||
"""Multiple conditions return list of conditions."""
|
||||
pgvector_module = get_pgvector_module()
|
||||
|
||||
result = pgvector_module._build_pg_conditions({
|
||||
'file_id': 'abc',
|
||||
'chunk_uuid': {'$ne': 'def'},
|
||||
})
|
||||
|
||||
assert len(result) == 2
|
||||
|
||||
def test_unsupported_field_dropped(self):
|
||||
"""Unsupported fields are dropped (not in _PG_SUPPORTED_FIELDS)."""
|
||||
pgvector_module = get_pgvector_module()
|
||||
|
||||
result = pgvector_module._build_pg_conditions({'unknown_field': 'value'})
|
||||
assert result == []
|
||||
|
||||
def test_uuid_alias_resolved(self):
|
||||
"""'uuid' alias is resolved to 'chunk_uuid'."""
|
||||
pgvector_module = get_pgvector_module()
|
||||
|
||||
result = pgvector_module._build_pg_conditions({'uuid': 'abc'})
|
||||
|
||||
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."""
|
||||
pgvector_module = get_pgvector_module()
|
||||
|
||||
result = pgvector_module._build_pg_conditions({
|
||||
'text': {'$ne': ''},
|
||||
'file_id': 'abc',
|
||||
'chunk_uuid': {'$in': ['x', 'y']},
|
||||
'unsupported': 'value',
|
||||
})
|
||||
|
||||
assert len(result) == 3 # Only supported fields
|
||||
Reference in New Issue
Block a user