Merge pull request #301 from ggozad/chore/test-cleanup

Test suite cleanup & parallelization
This commit is contained in:
Yiorgis Gozadinos 2026-03-05 12:31:14 +02:00 committed by GitHub
commit 24de604678
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 103 additions and 15143 deletions

View file

@ -68,8 +68,6 @@ jobs:
run: uv run python -c "from transformers import AutoTokenizer; AutoTokenizer.from_pretrained('Qwen/Qwen3-Embedding-0.6B')"
- name: Run tests with coverage
run: uv run pytest -m "not integration" --cov=haiku --cov-report=xml
env:
HF_HUB_OFFLINE: "1"
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:

View file

@ -1,6 +1,11 @@
# Changelog
## [Unreleased]
### Changed
- **Test suite cleanup**: Removed stale VCR cassettes, dead fixtures, orphaned directories, and redundant tests. Strengthened weak assertions across search, context enhancement, and converter tests. Relocated misplaced `SearchResult._get_primary_label` test to `test_search.py`
- **Parallel test execution**: Added `pytest-xdist` and enabled parallel test runs by default (`-n auto`), reducing test suite time from ~3.5 min to ~2 min
## [0.33.0] - 2026-03-04
### Added

View file

@ -80,6 +80,7 @@ dev = [
"pytest-asyncio>=1.3.0",
"pytest-cov>=7.0.0",
"pytest-recording>=0.13.4",
"pytest-xdist>=3.0",
"ruff>=0.14.13",
]
@ -120,6 +121,7 @@ python = ".venv"
[tool.pytest.ini_options]
asyncio_default_fixture_loop_scope = "session"
asyncio_mode = "auto"
addopts = "-n auto"
testpaths = ["tests"]
norecursedirs = ["examples", "docs", "evaluations", ".git", ".venv"]
markers = [

File diff suppressed because one or more lines are too long

View file

@ -17,13 +17,14 @@ embeddings:
vector_dim: 2560
""")
os.environ["HAIKU_RAG_CONFIG_PATH"] = str(_test_config_path)
os.environ["HF_HUB_OFFLINE"] = "1"
import pydantic_ai.models # noqa: E402
import pytest # noqa: E402
import yaml # noqa: E402
from datasets import Dataset, load_dataset, load_from_disk # noqa: E402
if TYPE_CHECKING:
from datasets import Dataset
from vcr import VCR
setattr(pydantic_ai.models, "ALLOW_MODEL_REQUESTS", False)
@ -31,7 +32,9 @@ logging.getLogger("vcr.cassette").setLevel(logging.WARNING)
@pytest.fixture(scope="session")
def qa_corpus() -> Dataset:
def qa_corpus() -> "Dataset":
from datasets import Dataset, load_dataset, load_from_disk
ds_path = Path(__file__).parent / "data" / "dataset"
ds_path.mkdir(parents=True, exist_ok=True)
try:

View file

@ -1,17 +1,9 @@
from pathlib import Path
from unittest.mock import AsyncMock, patch
import pytest
from haiku.rag.agents.research.models import ResearchReport
from haiku.rag.client import HaikuRAG
@pytest.fixture(scope="module")
def vcr_cassette_dir():
return str(Path(__file__).parent / "cassettes" / "test_client_research")
async def test_client_research_report(temp_db_path):
"""Test client.research() delegates to research graph in report mode."""
mock_report = ResearchReport(

View file

@ -335,60 +335,6 @@ async def test_max_items_limit_caps_expansion(temp_db_path):
assert item_count <= 2, f"Expected at most 2 items, got {item_count}"
@pytest.mark.vcr()
async def test_search_result_get_primary_label():
"""Test _get_primary_label prioritizes structural labels correctly."""
# Table should be prioritized
result = SearchResult(
content="test",
score=0.5,
chunk_id="c1",
document_id="d1",
labels=["paragraph", "table", "text"],
)
assert result._get_primary_label() == "table"
# Code should be prioritized over paragraph
result = SearchResult(
content="test",
score=0.5,
chunk_id="c2",
document_id="d2",
labels=["paragraph", "code"],
)
assert result._get_primary_label() == "code"
# list_item should be prioritized
result = SearchResult(
content="test",
score=0.5,
chunk_id="c3",
document_id="d3",
labels=["text", "list_item"],
)
assert result._get_primary_label() == "list_item"
# Returns first label when no priority match
result = SearchResult(
content="test",
score=0.5,
chunk_id="c4",
document_id="d4",
labels=["paragraph", "text"],
)
assert result._get_primary_label() == "paragraph"
# Returns None for empty labels
result = SearchResult(
content="test",
score=0.5,
chunk_id="c5",
document_id="d5",
labels=[],
)
assert result._get_primary_label() is None
@pytest.mark.vcr()
async def test_expand_context_radius_zero(temp_db_path):
"""Test expand_context with radius 0 returns original results."""
@ -696,6 +642,10 @@ First paragraph of results.
assert len(r.content) > 0
# Score should be preserved (best score)
assert r.score in [0.9, 0.8]
# Expanded content should have docling refs
assert r.doc_item_refs is not None and len(r.doc_item_refs) > 0
# Document has headings, expanded result should too
assert r.headings is not None and len(r.headings) > 0
def create_picture_document() -> DoclingDocument:

View file

@ -1067,6 +1067,8 @@ class TestDoclingServeConverterIntegration:
doc = await converter.convert_file(pdf_path)
assert isinstance(doc, DoclingDocument)
assert len(doc.pages) > 0
assert len(doc.export_to_markdown().strip()) > 100
@pytest.mark.xfail(
reason="docling-serve does not return picture image data in JSON response "

View file

@ -1,5 +1,4 @@
from contextlib import asynccontextmanager
from pathlib import Path
from unittest.mock import AsyncMock, patch
import httpx
@ -8,11 +7,6 @@ import pytest
from haiku.rag.client import HaikuRAG
@pytest.fixture(scope="module")
def vcr_cassette_dir():
return str(Path(__file__).parent / "cassettes" / "test_download_models")
@pytest.fixture
def mock_to_thread():
"""Patch asyncio.to_thread to skip docling/tokenizer downloads."""

View file

@ -154,8 +154,7 @@ async def test_embed_chunks_returns_new_objects(allow_model_requests):
assert embedded[0] is not original
@pytest.mark.vcr()
async def test_embed_chunks_empty_list(allow_model_requests):
async def test_embed_chunks_empty_list():
"""Test that embed_chunks handles empty list."""
result = await embed_chunks([])
assert result == []

View file

@ -60,11 +60,11 @@ async def test_search_qa_corpus(qa_corpus: Dataset, temp_db_path):
@pytest.mark.vcr()
async def test_chunks_include_document_info(temp_db_path):
"""Test that search results include document URI and metadata."""
async def test_search_chunk_includes_document_provenance(temp_db_path):
"""Test that raw chunk search results include document URI, metadata, and ID."""
client = HaikuRAG(db_path=temp_db_path, config=Config, create=True)
# Create a document with URI and metadata
# Create a document with URI and metadata but no title
created_document = await client.create_document(
content="This is a test document with some content for searching.",
uri="https://example.com/test.html",
@ -87,32 +87,7 @@ async def test_chunks_include_document_info(temp_db_path):
assert chunk.document_uri == "https://example.com/test.html"
assert chunk.document_meta == {"title": "Test Document", "author": "Test Author"}
assert chunk.document_id == created_document.id
client.close()
@pytest.mark.vcr()
async def test_chunks_include_document_title(temp_db_path):
"""Test that search results include the parent document title when present."""
client = HaikuRAG(db_path=temp_db_path, config=Config, create=True)
# Create a document with URI and title
await client.create_document(
content="This is a test document with a custom title to verify enrichment.",
uri="file:///tmp/title-test.md",
title="My Custom Title",
)
# Perform a search that should find this document
results = await client.chunk_repository.search(
"custom title", limit=3, search_type="hybrid"
)
assert results, "Expected at least one search result"
for chunk, _ in results:
# All returned chunks for this doc should carry the document title
if chunk.document_uri == "file:///tmp/title-test.md":
assert chunk.document_title == "My Custom Title"
assert chunk.document_title is None
client.close()
@ -207,9 +182,12 @@ async def test_search_returns_search_result(temp_db_path):
assert result.score > 0
assert result.document_uri == "https://example.com/ml.html"
assert result.document_title == "ML Guide"
assert result.chunk_id is not None
assert result.document_id is not None
# page_numbers and headings come from chunk metadata
assert isinstance(result.page_numbers, list)
assert isinstance(result.labels, list)
assert len(result.labels) > 0
client.close()
@ -272,3 +250,56 @@ async def test_search_result_format_includes_metadata(temp_db_path):
# Should include content
assert "Content:" in formatted
assert "machine learning" in formatted.lower()
def test_search_result_primary_label_prioritizes_structural_types():
"""Test _get_primary_label prioritizes structural labels correctly."""
# Table should be prioritized
result = SearchResult(
content="test",
score=0.5,
chunk_id="c1",
document_id="d1",
labels=["paragraph", "table", "text"],
)
assert result._get_primary_label() == "table"
# Code should be prioritized over paragraph
result = SearchResult(
content="test",
score=0.5,
chunk_id="c2",
document_id="d2",
labels=["paragraph", "code"],
)
assert result._get_primary_label() == "code"
# list_item should be prioritized
result = SearchResult(
content="test",
score=0.5,
chunk_id="c3",
document_id="d3",
labels=["text", "list_item"],
)
assert result._get_primary_label() == "list_item"
# Returns first label when no priority match
result = SearchResult(
content="test",
score=0.5,
chunk_id="c4",
document_id="d4",
labels=["paragraph", "text"],
)
assert result._get_primary_label() == "paragraph"
# Returns None for empty labels
result = SearchResult(
content="test",
score=0.5,
chunk_id="c5",
document_id="d5",
labels=[],
)
assert result._get_primary_label() is None

24
uv.lock
View file

@ -984,6 +984,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
]
[[package]]
name = "execnet"
version = "2.1.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" },
]
[[package]]
name = "executing"
version = "2.2.1"
@ -1365,6 +1374,7 @@ dev = [
{ name = "pytest-asyncio" },
{ name = "pytest-cov" },
{ name = "pytest-recording" },
{ name = "pytest-xdist" },
{ name = "ruff" },
{ name = "ty" },
]
@ -1391,6 +1401,7 @@ dev = [
{ name = "pytest-asyncio", specifier = ">=1.3.0" },
{ name = "pytest-cov", specifier = ">=7.0.0" },
{ name = "pytest-recording", specifier = ">=0.13.4" },
{ name = "pytest-xdist", specifier = ">=3.0" },
{ name = "ruff", specifier = ">=0.14.13" },
{ name = "ty", specifier = ">=0.0.16" },
]
@ -4063,6 +4074,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/42/c2/ce34735972cc42d912173e79f200fe66530225190c06655c5632a9d88f1e/pytest_recording-0.13.4-py3-none-any.whl", hash = "sha256:ad49a434b51b1c4f78e85b1e6b74fdcc2a0a581ca16e52c798c6ace971f7f439", size = 13723, upload-time = "2025-05-08T10:41:09.684Z" },
]
[[package]]
name = "pytest-xdist"
version = "3.8.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "execnet" },
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" },
]
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"