ScholarX Pytest Coverage Analysis & Remediation Report¶
This report provides a comprehensive analysis of the overall code coverage in scholarx, maps specific testing gaps, and outlines highly actionable opportunities and code patterns to achieve the 100% test coverage North Star goal.
📊 Current Coverage Dashboard¶
[!NOTE] Currently, the overall test coverage stands at 27% across 1,749 statements, with 1,282 statements missed. While core utilities like
models.pyanddeduplication.pyhave excellent test suites, high-impact modules likemcp_server.py,kg_integration.py,queue.py, and the CLI interface remain largely untested.
Coverage Breakdown by Module¶
| Module | Statements | Missed | Current Coverage | Key Missing Lines / Areas |
|---|---|---|---|---|
scholarx/models.py |
89 | 0 | 100% | None (Fully tested!) |
scholarx/providers/__init__.py |
8 | 0 | 100% | None (Fully tested!) |
scholarx/deduplication.py |
117 | 18 | 85% | 40, 44, 48, 58, 67-69, 109-110, 113-115, 162-187 |
scholarx/providers/base.py |
58 | 34 | 41% | Rate limiters, 429 retries (38-48, 54-59, 63-66, 83-103) |
scholarx/cli.py |
318 | 233 | 27% | Argument parser, subprocess runner (323-609, 628-693, 702-897) |
scholarx/paper_storage.py |
74 | 56 | 24% | PDF downloads, metadata JSON writes (30-33, 44-75, 86-95, 103-155) |
scholarx/providers/rss.py |
115 | 88 | 23% | Parsing feeds, fallback RSS client (121-123, 138-171, 175-269) |
scholarx/providers/osf.py |
69 | 53 | 23% | Search filters, error handlers (24-25, 28-48, 51-59, 62-87) |
scholarx/providers/biorxiv.py |
73 | 57 | 22% | bioRxiv/medRxiv details requests (31-32, 40-61, 65-99, 105-122) |
scholarx/providers/semantic_scholar.py |
58 | 46 | 21% | REST query mapping (26-45, 48-55, 58-68, 71-82) |
scholarx/api_client.py |
115 | 93 | 19% | Concurrency handling, client search fan-out (33-52, 83-149, 168-297) |
scholarx/providers/arxiv.py |
130 | 112 | 14% | Atom XML parsing (54-88, 92-101, 118-158, 172-257) |
scholarx/providers/pmc.py |
107 | 94 | 12% | XML tree traversal (26-41, 45-52, 56-83, 89-189) |
scholarx/kg_integration.py |
108 | 108 | 0% | Knowledge Graph integration (10-251 - Untested) |
scholarx/mcp_server.py |
185 | 185 | 0% | FastMCP tools, registry (8-375 - Untested) |
scholarx/queue.py |
39 | 39 | 0% | Multi-threaded download workers (3-59 - Untested) |
scholarx/agent_server.py |
38 | 38 | 0% | Startup configurations, CLI router (8-100 - Untested) |
scholarx/__main__.py |
3 | 3 | 0% | Executable module entrypoint (4-7 - Untested) |
| TOTAL | 1749 | 1282 | 27% | Overall Codebase Metrics |
🎯 Architecture Diagram¶
Below is the workflow of the ScholarX ecosystem, showing where mock tests are required to cover key operational boundaries:
graph TD
subgraph "CLI & Server Interfaces"
CLI[scholarx/cli.py]
MCPServer[scholarx/mcp_server.py]
AgentServer[scholarx/agent_server.py]
end
subgraph "Core Client Orchestration"
Client[scholarx/api_client.py]
Queue[scholarx/queue.py]
end
subgraph "Data & Infrastructure Bridges"
Storage[scholarx/paper_storage.py]
KGBridg[scholarx/kg_integration.py]
end
subgraph "External Providers (Needs HTTP Mocking via respx)"
Arxiv[providers/arxiv.py]
Biorxiv[providers/biorxiv.py]
SemScholar[providers/semantic_scholar.py]
PMC[providers/pmc.py]
OSF[providers/osf.py]
RSS[providers/rss.py]
end
CLI --> Client
MCPServer --> Client
AgentServer --> KGBridg
Client --> KGBridg
Client --> Storage
Client --> Queue
Client --> Arxiv
Client --> Biorxiv
Client --> SemScholar
Client --> PMC
Client --> OSF
Client --> RSS
Queue -.->|Async Download| Storage
🛠️ Step-by-Step Remediation Plan¶
To achieve the North Star goal of 100% test coverage, we have designed highly optimized unit test suites for each under-tested module. These tests utilize fixtures, pytest-asyncio, respx for network mocking, and unittest.mock for dependency isolation.
Component 1: scholarx/paper_storage.py (Current: 24% Coverage)¶
Gaps: Untested async file writes, status validation, directory scans, and JSON metadata logs.
Solution: Create tests/test_paper_storage.py utilizing a dynamic temporary directory and mock client.
import json
from pathlib import Path
import pytest
from unittest.mock import AsyncMock, MagicMock
from scholarx.paper_storage import PaperStorage
from scholarx.models import Paper, PaperSource
@pytest.fixture
def temp_storage(tmp_path):
return PaperStorage(storage_dir=tmp_path)
@pytest.mark.asyncio
async def test_download_paper_success(temp_storage):
# Setup paper and mock data
paper = Paper(
id="arxiv:1234.5678",
title="Mock Paper",
authors=["Alice"],
pdf_url="https://arxiv.org/pdf/1234.5678.pdf",
source=PaperSource.ARXIV
)
# Mock client and download stream
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.iter_bytes = MagicMock(return_value=[b"pdf_content_chunk"])
mock_client = AsyncMock()
mock_client.stream.return_value.__aenter__.return_value = mock_response
temp_storage.client = mock_client
path = await temp_storage.download_paper(paper)
assert path is not None
assert Path(path).exists()
assert Path(path).read_bytes() == b"pdf_content_chunk"
# Verify metadata JSON log
meta_path = Path(path).with_suffix(".json")
assert meta_path.exists()
with open(meta_path, "r") as f:
meta = json.load(f)
assert meta["title"] == "Mock Paper"
@pytest.mark.asyncio
async def test_list_stored_papers(temp_storage):
# Seed storage directory
pdf_dir = temp_storage.storage_dir
pdf_dir.mkdir(parents=True, exist_ok=True)
pdf_file = pdf_dir / "paper.pdf"
pdf_file.write_bytes(b"content")
meta_file = pdf_dir / "paper.json"
meta_file.write_text(json.dumps({"title": "Stored Paper", "id": "arxiv:1234"}))
stored = temp_storage.list_stored_papers()
assert len(stored) == 1
assert stored[0]["id"] == "arxiv:1234"
Component 2: scholarx/queue.py (Current: 0% Coverage)¶
Gaps: Background worker loop, lock primitives, dynamic queue management, and concurrent task synchronization.
Solution: Create tests/test_queue.py using short-delay tests to verify daemon threads.
import time
import pytest
from unittest.mock import AsyncMock, patch
from scholarx.queue import JOB_QUEUE, JOB_STATUS, queue_download, _download_worker
from scholarx.models import Paper, PaperSource
@pytest.mark.asyncio
async def test_queue_download_execution():
paper = Paper(
id="arxiv:9999",
title="Queue Test",
authors=["Bob"],
source=PaperSource.ARXIV
)
# Patch client storage download to execute instantly
with patch("scholarx.api_client.ScholarXClient") as MockClient:
mock_instance = MockClient.return_value
mock_instance.storage.download_paper = AsyncMock(return_value="/tmp/test.pdf")
# Enqueue item
job_id = queue_download(paper, mock_instance)
assert job_id is not None
assert job_id in JOB_STATUS
assert JOB_STATUS[job_id]["status"] == "queued"
# Manually trigger one queue processing cycle
from scholarx.queue import JOB_QUEUE
# The background thread should pick it up automatically, but we can verify status transition
timeout = 5.0
start = time.time()
while JOB_STATUS[job_id]["status"] == "queued" and (time.time() - start) < timeout:
time.sleep(0.1)
assert JOB_STATUS[job_id]["status"] in ("completed", "processing")
Component 3: scholarx/mcp_server.py (Current: 0% Coverage)¶
Gaps: Tool routes, parameters validations, format converters, schema declarations.
Solution: Create tests/test_mcp_server.py by calling mcp tools programmatically.
import pytest
from unittest.mock import AsyncMock, patch
from scholarx.mcp_server import mcp
from scholarx.models import Paper, PaperSource
@pytest.mark.asyncio
@patch("scholarx.mcp_server.client")
async def test_mcp_tool_search(mock_client):
# Setup mock return value
mock_paper = Paper(
id="arxiv:123",
title="MCP Paper",
authors=["Charlie"],
source=PaperSource.ARXIV,
abstract="Snippet",
url="http://test.com"
)
mock_client.search = AsyncMock(return_value=[mock_paper])
# Setup list tools check
tools = mcp.list_tools()
search_tool = next(t for t in tools if t.name == "search")
assert search_tool is not None
# Execute tool function directly
result = await mcp.call_tool("search", arguments={"query": "quantum computing"})
assert "MCP Paper" in result
assert "arxiv:123" in result
Component 4: scholarx/kg_integration.py (Current: 0% Coverage)¶
Gaps: Graph schema integrations, Node/Edge constructions, dynamic deduplication.
Solution: Create tests/test_kg_integration.py isolating graph transactions.
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from scholarx.kg_integration import ScholarXKGBridge
from scholarx.models import Paper, PaperSource
@pytest.fixture
def mock_graph_engine():
engine = MagicMock()
engine.graph = MagicMock()
# Mock nodes and edges methods
engine.graph.nodes = []
engine.graph.has_edge = MagicMock(return_value=False)
return engine
@pytest.mark.asyncio
async def test_ingest_abstract_only(mock_graph_engine):
bridge = ScholarXKGBridge(knowledge_engine=mock_graph_engine)
paper = Paper(
id="arxiv:kg-test-1",
title="KG Reasoning",
authors=["David"],
source=PaperSource.ARXIV,
abstract="KG logic representation abstract.",
doi="10.1234/kg"
)
# Mock abstract-only fallback
result = await bridge._ingest_abstract_only(paper, kb_name="test-kb")
assert result["status"] == "ingested"
assert "arxiv-kg-test-1" in result["article_id"]
# Verify graph node addition calls
assert mock_graph_engine.graph.add_node.call_count >= 2
assert mock_graph_engine.graph.add_edge.call_count >= 1
Component 5: HTTP Provider Mocking (respx & pytest-asyncio)¶
Gaps: Raw response builders, status code handling, JSON/XML parser robust code blocks in arxiv.py, biorxiv.py, osf.py, pmc.py, rss.py, and semantic_scholar.py.
Solution: Use respx for elegant, clean, HTTP isolation.
import pytest
import respx
from httpx import Response
from scholarx.providers.biorxiv import BiorxivProvider
from scholarx.models import SearchQuery
@pytest.mark.asyncio
@respx.mock
async def test_biorxiv_provider_search():
provider = BiorxivProvider()
query = SearchQuery(query="cancer research", max_results=5)
# Setup mock response body
mock_payload = {
"collection": [
{
"doi": "10.1101/2026.05.01",
"title": "A Cancer Research Discovery",
"authors": "Dr. John Doe; Dr. Jane Smith",
"category": "Cancer Biology",
"date": "2026-05-01",
"abstract": "This study shows significant findings."
}
]
}
# Setup endpoint matching
respx.get(url__contains="/details/biorxiv").mock(
return_value=Response(status_code=200, json=mock_payload)
)
papers = await provider.search(query)
assert len(papers) == 1
assert papers[0].title == "A Cancer Research Discovery"
assert "Jane Smith" in papers[0].authors
Component 6: scholarx/cli.py & Startup routing (Current: 27% Coverage)¶
Gaps: Subprocess wrapper execution, system prompt parser arguments, status routers.
Solution: Leverage pytest fixtures for system outputs and argument configurations.
import pytest
from unittest.mock import MagicMock, patch
from scholarx.cli import main
def test_cli_argument_routing():
# Mock CLI arguments
with patch("sys.argv", ["scholarx", "status"]):
with patch("scholarx.cli.list_stored_papers") as mock_list:
mock_list.return_value = [{"id": "arxiv:1", "title": "Stored Paper"}]
with pytest.raises(SystemExit) as exc_info:
main()
assert exc_info.value.code == 0
🚀 Conclusion & Recommendations¶
Action Items for Fast Remediation¶
- Prioritize 0% Modules first: Implement
tests/test_queue.py,tests/test_kg_integration.py, andtests/test_mcp_server.py. These files have the highest potential to jump-start coverage by ~30%. - Implement Respx HTTP Provider Tests: Leverage the mock templates shown above to implement mock test runs for
biorxiv.py,arxiv.py,pmc.py,osf.py,rss.py, andsemantic_scholar.py. - Execute coverage tracking during test runs: Use the following CLI arguments to continuously verify improvement:
With these tests built using the template blueprints above, achieving the 100% test coverage goal is completely within reach.