test(ebook): cover protected phrases and migrate suite to async
Add test_protected_phrases.py covering phrase-matching behavior in the RAG engine, and update the existing ebook_search tests to use the async SQLAlchemy engine/session (create_async_engine, AsyncSession) and async HTTP paths.
This commit is contained in:
@@ -10,8 +10,8 @@ from types import ModuleType
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import create_engine, select
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, create_async_engine
|
||||
|
||||
from python.ebook_search.answer import answer_query
|
||||
from python.ebook_search.bm25_corpus import (
|
||||
@@ -77,10 +77,17 @@ def test_reciprocal_rank_fusion_combines_vector_and_bm25_rankings() -> None:
|
||||
assert fused[0].fused_score == fused[0].score
|
||||
|
||||
|
||||
def test_find_existing_source_matches_path_or_hash() -> None:
|
||||
engine = create_engine("sqlite+pysqlite:///:memory:", future=True)
|
||||
RichieBase.metadata.create_all(engine)
|
||||
with sessionmaker(bind=engine, expire_on_commit=False, future=True)() as session:
|
||||
async def build_async_engine() -> AsyncEngine:
|
||||
"""Create an in-memory async engine with the Richie schema."""
|
||||
engine = create_async_engine("sqlite+aiosqlite:///:memory:")
|
||||
async with engine.begin() as connection:
|
||||
await connection.run_sync(RichieBase.metadata.create_all)
|
||||
return engine
|
||||
|
||||
|
||||
async def test_find_existing_source_matches_path_or_hash() -> None:
|
||||
engine = await build_async_engine()
|
||||
async with AsyncSession(engine, expire_on_commit=False) as session:
|
||||
source = EbookSource(
|
||||
title="Book",
|
||||
author=None,
|
||||
@@ -93,16 +100,15 @@ def test_find_existing_source_matches_path_or_hash() -> None:
|
||||
file_size=10,
|
||||
)
|
||||
session.add(source)
|
||||
session.commit()
|
||||
await session.commit()
|
||||
|
||||
assert find_existing_source(session, Path("/old/book.epub"), "b" * 64) == source
|
||||
assert find_existing_source(session, Path("/new/book.epub"), "a" * 64) == source
|
||||
assert await find_existing_source(session, Path("/old/book.epub"), "b" * 64) == source
|
||||
assert await find_existing_source(session, Path("/new/book.epub"), "a" * 64) == source
|
||||
|
||||
|
||||
def test_bm25_corpus_uses_existing_search_text_without_duplicate_metadata() -> None:
|
||||
engine = create_engine("sqlite+pysqlite:///:memory:", future=True)
|
||||
RichieBase.metadata.create_all(engine)
|
||||
with sessionmaker(bind=engine, expire_on_commit=False, future=True)() as session:
|
||||
async def test_bm25_corpus_uses_existing_search_text_without_duplicate_metadata() -> None:
|
||||
engine = await build_async_engine()
|
||||
async with AsyncSession(engine, expire_on_commit=False) as session:
|
||||
source = EbookSource(
|
||||
title="Book",
|
||||
author="Author",
|
||||
@@ -115,10 +121,10 @@ def test_bm25_corpus_uses_existing_search_text_without_duplicate_metadata() -> N
|
||||
file_size=10,
|
||||
)
|
||||
session.add(source)
|
||||
session.flush()
|
||||
await session.flush()
|
||||
chapter = EbookChapter(source_id=source.id, spine_index=0, title="Chapter", href=None)
|
||||
session.add(chapter)
|
||||
session.flush()
|
||||
await session.flush()
|
||||
session.add(
|
||||
EbookChunk(
|
||||
id=1,
|
||||
@@ -133,9 +139,9 @@ def test_bm25_corpus_uses_existing_search_text_without_duplicate_metadata() -> N
|
||||
search_text="Book Author Chapter content",
|
||||
)
|
||||
)
|
||||
session.commit()
|
||||
await session.commit()
|
||||
|
||||
records, texts = fetch_bm25_corpus_records(session)
|
||||
records, texts = await fetch_bm25_corpus_records(session)
|
||||
|
||||
assert texts == ["Book Author Chapter content"]
|
||||
assert records[0]["chunk_id"] == 1
|
||||
@@ -370,7 +376,7 @@ def test_load_bm25_corpus_raises_when_index_is_missing(mocker: MockerFixture, tm
|
||||
load_bm25_corpus.cache_clear()
|
||||
|
||||
|
||||
def test_ensure_bm25_corpus_refreshes_missing_index(mocker: MockerFixture) -> None:
|
||||
async def test_ensure_bm25_corpus_refreshes_missing_index(mocker: MockerFixture) -> None:
|
||||
refreshed: list[object] = []
|
||||
db_updated_at = datetime.now(tz=UTC)
|
||||
|
||||
@@ -385,12 +391,12 @@ def test_ensure_bm25_corpus_refreshes_missing_index(mocker: MockerFixture) -> No
|
||||
config = EbookSearchConfig(rerank=RerankConfig(enabled=False))
|
||||
session = object()
|
||||
|
||||
ensure_bm25_corpus(session, config)
|
||||
await ensure_bm25_corpus(session, config)
|
||||
|
||||
assert refreshed == [(session, config, db_updated_at)]
|
||||
|
||||
|
||||
def test_ensure_bm25_corpus_refreshes_stale_index(mocker: MockerFixture) -> None:
|
||||
async def test_ensure_bm25_corpus_refreshes_stale_index(mocker: MockerFixture) -> None:
|
||||
refreshed: list[object] = []
|
||||
created_at = datetime(2026, 1, 1, tzinfo=UTC)
|
||||
db_updated_at = datetime(2026, 1, 2, tzinfo=UTC)
|
||||
@@ -407,7 +413,7 @@ def test_ensure_bm25_corpus_refreshes_stale_index(mocker: MockerFixture) -> None
|
||||
config = EbookSearchConfig(rerank=RerankConfig(enabled=False))
|
||||
session = object()
|
||||
|
||||
ensure_bm25_corpus(session, config)
|
||||
await ensure_bm25_corpus(session, config)
|
||||
|
||||
assert refreshed == [(session, config, db_updated_at)]
|
||||
|
||||
@@ -420,14 +426,13 @@ def test_supported_embedding_models_match_service_names() -> None:
|
||||
}
|
||||
|
||||
|
||||
def test_ensure_embedding_models_registers_service_names() -> None:
|
||||
engine = create_engine("sqlite+pysqlite:///:memory:", future=True)
|
||||
RichieBase.metadata.create_all(engine)
|
||||
with sessionmaker(bind=engine, expire_on_commit=False, future=True)() as session:
|
||||
ensure_embedding_models(session)
|
||||
session.commit()
|
||||
async def test_ensure_embedding_models_registers_service_names() -> None:
|
||||
engine = await build_async_engine()
|
||||
async with AsyncSession(engine, expire_on_commit=False) as session:
|
||||
await ensure_embedding_models(session)
|
||||
await session.commit()
|
||||
|
||||
models = list(session.scalars(select(EbookEmbeddingModel).order_by(EbookEmbeddingModel.name)))
|
||||
models = list(await session.scalars(select(EbookEmbeddingModel).order_by(EbookEmbeddingModel.name)))
|
||||
|
||||
assert [(model.name, model.dimension) for model in models] == [
|
||||
("qwen3-embedding-0.6b", 1024),
|
||||
@@ -496,10 +501,10 @@ def test_chat_api_key_falls_back_to_ollama_api_key(mocker: MockerFixture) -> Non
|
||||
assert config.vllm_api_key == "ollama-key"
|
||||
|
||||
|
||||
def test_answer_query_does_not_call_model_when_disabled() -> None:
|
||||
async def test_answer_query_does_not_call_model_when_disabled(mocker: MockerFixture) -> None:
|
||||
config = load_config().model_copy(update={"answer_enabled": False})
|
||||
result = SearchResult(chunk_id=1, text="source text", source_title="Book")
|
||||
|
||||
answer = answer_query("question", [result], config)
|
||||
answer = await answer_query(mocker.Mock(), "question", [result], config)
|
||||
|
||||
assert "Answer generation is disabled" in answer
|
||||
|
||||
Reference in New Issue
Block a user