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:
huanghuoguoguo
2026-05-11 10:20:34 +08:00
parent adb4b29c94
commit 1a3c73bc05
17 changed files with 3123 additions and 510 deletions

View 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