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')" run: uv run python -c "from transformers import AutoTokenizer; AutoTokenizer.from_pretrained('Qwen/Qwen3-Embedding-0.6B')"
- name: Run tests with coverage - name: Run tests with coverage
run: uv run pytest -m "not integration" --cov=haiku --cov-report=xml run: uv run pytest -m "not integration" --cov=haiku --cov-report=xml
env:
HF_HUB_OFFLINE: "1"
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@v5 uses: codecov/codecov-action@v5
with: with:

View file

@ -1,6 +1,11 @@
# Changelog # Changelog
## [Unreleased] ## [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 ## [0.33.0] - 2026-03-04
### Added ### Added

View file

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

File diff suppressed because one or more lines are too long

View file

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

View file

@ -1,17 +1,9 @@
from pathlib import Path
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
import pytest
from haiku.rag.agents.research.models import ResearchReport from haiku.rag.agents.research.models import ResearchReport
from haiku.rag.client import HaikuRAG 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): async def test_client_research_report(temp_db_path):
"""Test client.research() delegates to research graph in report mode.""" """Test client.research() delegates to research graph in report mode."""
mock_report = ResearchReport( 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}" 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() @pytest.mark.vcr()
async def test_expand_context_radius_zero(temp_db_path): async def test_expand_context_radius_zero(temp_db_path):
"""Test expand_context with radius 0 returns original results.""" """Test expand_context with radius 0 returns original results."""
@ -696,6 +642,10 @@ First paragraph of results.
assert len(r.content) > 0 assert len(r.content) > 0
# Score should be preserved (best score) # Score should be preserved (best score)
assert r.score in [0.9, 0.8] 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: def create_picture_document() -> DoclingDocument:

View file

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

View file

@ -1,5 +1,4 @@
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from pathlib import Path
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
import httpx import httpx
@ -8,11 +7,6 @@ import pytest
from haiku.rag.client import HaikuRAG 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 @pytest.fixture
def mock_to_thread(): def mock_to_thread():
"""Patch asyncio.to_thread to skip docling/tokenizer downloads.""" """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 assert embedded[0] is not original
@pytest.mark.vcr() async def test_embed_chunks_empty_list():
async def test_embed_chunks_empty_list(allow_model_requests):
"""Test that embed_chunks handles empty list.""" """Test that embed_chunks handles empty list."""
result = await embed_chunks([]) result = await embed_chunks([])
assert result == [] assert result == []

View file

@ -60,11 +60,11 @@ async def test_search_qa_corpus(qa_corpus: Dataset, temp_db_path):
@pytest.mark.vcr() @pytest.mark.vcr()
async def test_chunks_include_document_info(temp_db_path): async def test_search_chunk_includes_document_provenance(temp_db_path):
"""Test that search results include document URI and metadata.""" """Test that raw chunk search results include document URI, metadata, and ID."""
client = HaikuRAG(db_path=temp_db_path, config=Config, create=True) 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( created_document = await client.create_document(
content="This is a test document with some content for searching.", content="This is a test document with some content for searching.",
uri="https://example.com/test.html", 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_uri == "https://example.com/test.html"
assert chunk.document_meta == {"title": "Test Document", "author": "Test Author"} assert chunk.document_meta == {"title": "Test Document", "author": "Test Author"}
assert chunk.document_id == created_document.id assert chunk.document_id == created_document.id
assert chunk.document_title is None
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"
client.close() client.close()
@ -207,9 +182,12 @@ async def test_search_returns_search_result(temp_db_path):
assert result.score > 0 assert result.score > 0
assert result.document_uri == "https://example.com/ml.html" assert result.document_uri == "https://example.com/ml.html"
assert result.document_title == "ML Guide" 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 # page_numbers and headings come from chunk metadata
assert isinstance(result.page_numbers, list) assert isinstance(result.page_numbers, list)
assert isinstance(result.labels, list) assert isinstance(result.labels, list)
assert len(result.labels) > 0
client.close() client.close()
@ -272,3 +250,56 @@ async def test_search_result_format_includes_metadata(temp_db_path):
# Should include content # Should include content
assert "Content:" in formatted assert "Content:" in formatted
assert "machine learning" in formatted.lower() 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" }, { 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]] [[package]]
name = "executing" name = "executing"
version = "2.2.1" version = "2.2.1"
@ -1365,6 +1374,7 @@ dev = [
{ name = "pytest-asyncio" }, { name = "pytest-asyncio" },
{ name = "pytest-cov" }, { name = "pytest-cov" },
{ name = "pytest-recording" }, { name = "pytest-recording" },
{ name = "pytest-xdist" },
{ name = "ruff" }, { name = "ruff" },
{ name = "ty" }, { name = "ty" },
] ]
@ -1391,6 +1401,7 @@ dev = [
{ name = "pytest-asyncio", specifier = ">=1.3.0" }, { name = "pytest-asyncio", specifier = ">=1.3.0" },
{ name = "pytest-cov", specifier = ">=7.0.0" }, { name = "pytest-cov", specifier = ">=7.0.0" },
{ name = "pytest-recording", specifier = ">=0.13.4" }, { name = "pytest-recording", specifier = ">=0.13.4" },
{ name = "pytest-xdist", specifier = ">=3.0" },
{ name = "ruff", specifier = ">=0.14.13" }, { name = "ruff", specifier = ">=0.14.13" },
{ name = "ty", specifier = ">=0.0.16" }, { 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" }, { 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]] [[package]]
name = "python-dateutil" name = "python-dateutil"
version = "2.9.0.post0" version = "2.9.0.post0"