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:
2026-07-12 17:51:06 -04:00
parent a876d71339
commit 114e05f438
8 changed files with 1448 additions and 94 deletions
+35 -30
View File
@@ -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