Skip to content

Commit 1ca66c6

Browse files
vvillait88claude
andcommitted
test: add coverage reporting, edge case tests, and thresholds
Add pytest-cov with 94% fail-under threshold. Add 10 edge case tests (None handling, double close, concurrent async, timeout/connection errors) and 2 E2E integration tests. CI now reports coverage. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent cb9424a commit 1ca66c6

5 files changed

Lines changed: 396 additions & 1 deletion

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,4 @@ jobs:
1919
- run: uv run ruff check .
2020
- run: uv run ruff format --check .
2121
- run: uv run vulture . vulture_whitelist.py --min-confidence 80 --exclude .venv
22-
- run: uv run pytest tests/
22+
- run: uv run pytest tests/ --cov=agentscore --cov-report=term-missing

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,15 @@ dev = [
3333
"respx>=0.21",
3434
]
3535

36+
[tool.pytest.ini_options]
37+
addopts = "--cov=agentscore --cov-report=term-missing --cov-fail-under=94"
38+
3639
[tool.hatch.build.targets.wheel]
3740
packages = ["agentscore"]
3841

3942
[dependency-groups]
4043
dev = [
4144
"ruff>=0.11.0",
4245
"vulture>=2.15",
46+
"pytest-cov>=6.0",
4347
]

tests/test_client.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,3 +531,112 @@ def test_user_agent_header_includes_version():
531531
client.get_stats()
532532
request = respx.calls[0].request
533533
assert request.headers["user-agent"] == f"agentscore-py/{version('agentscore-py')}"
534+
535+
536+
# ---------------------------------------------------------------------------
537+
# Edge cases
538+
# ---------------------------------------------------------------------------
539+
540+
541+
@respx.mock
542+
def test_get_agents_none_values_excluded_from_params():
543+
route = respx.get(f"{BASE_URL}/v1/agents").mock(return_value=httpx.Response(200, json=AGENTS_PAYLOAD))
544+
client = AgentScore(api_key=API_KEY)
545+
client.get_agents(chain="base", limit=None)
546+
url_str = str(route.calls.last.request.url)
547+
assert "chain=base" in url_str
548+
assert "limit" not in url_str
549+
550+
551+
@respx.mock
552+
def test_assess_refresh_false_not_included_in_body():
553+
route = respx.post(f"{BASE_URL}/v1/assess").mock(return_value=httpx.Response(200, json=ASSESS_PAYLOAD))
554+
client = AgentScore(api_key=API_KEY)
555+
client.assess(ADDRESS, refresh=False)
556+
body = json.loads(route.calls.last.request.content)
557+
assert "refresh" not in body
558+
559+
560+
@respx.mock
561+
def test_double_close():
562+
respx.get(f"{BASE_URL}/v1/stats").mock(return_value=httpx.Response(200, json=STATS_PAYLOAD))
563+
client = AgentScore(api_key=API_KEY)
564+
client.get_stats()
565+
client.close()
566+
client.close()
567+
assert client._sync_client is None
568+
569+
570+
@pytest.mark.asyncio
571+
@respx.mock
572+
async def test_double_aclose():
573+
respx.get(f"{BASE_URL}/v1/stats").mock(return_value=httpx.Response(200, json=STATS_PAYLOAD))
574+
client = AgentScore(api_key=API_KEY)
575+
await client.aget_stats()
576+
await client.aclose()
577+
await client.aclose()
578+
assert client._async_client is None
579+
580+
581+
@pytest.mark.asyncio
582+
@respx.mock
583+
async def test_concurrent_async_calls():
584+
import asyncio
585+
586+
respx.get(f"{BASE_URL}/v1/reputation/{ADDRESS}").mock(return_value=httpx.Response(200, json=REPUTATION_PAYLOAD))
587+
client = AgentScore(api_key=API_KEY)
588+
results = await asyncio.gather(
589+
client.aget_reputation(ADDRESS),
590+
client.aget_reputation(ADDRESS),
591+
client.aget_reputation(ADDRESS),
592+
)
593+
assert len(results) == 3
594+
for r in results:
595+
assert r["score"]["grade"] == "B"
596+
await client.aclose()
597+
598+
599+
@respx.mock
600+
def test_empty_chain_string_not_included_in_params():
601+
route = respx.get(f"{BASE_URL}/v1/reputation/{ADDRESS}").mock(
602+
return_value=httpx.Response(200, json=REPUTATION_PAYLOAD)
603+
)
604+
client = AgentScore(api_key=API_KEY)
605+
client.get_reputation(ADDRESS, chain="")
606+
assert "chain" not in str(route.calls.last.request.url)
607+
608+
609+
@respx.mock
610+
def test_assess_empty_policy_dict_included_in_body():
611+
route = respx.post(f"{BASE_URL}/v1/assess").mock(return_value=httpx.Response(200, json=ASSESS_PAYLOAD))
612+
client = AgentScore(api_key=API_KEY)
613+
client.assess(ADDRESS, policy={})
614+
body = json.loads(route.calls.last.request.content)
615+
assert "policy" in body
616+
assert body["policy"] == {}
617+
618+
619+
@respx.mock
620+
def test_timeout_error_raises_agentscore_error():
621+
respx.get(f"{BASE_URL}/v1/stats").mock(side_effect=httpx.TimeoutException("timed out"))
622+
client = AgentScore(api_key=API_KEY)
623+
with pytest.raises(httpx.TimeoutException):
624+
client.get_stats()
625+
626+
627+
@respx.mock
628+
def test_connect_error_raises_agentscore_error():
629+
respx.get(f"{BASE_URL}/v1/stats").mock(side_effect=httpx.ConnectError("connection refused"))
630+
client = AgentScore(api_key=API_KEY)
631+
with pytest.raises(httpx.ConnectError):
632+
client.get_stats()
633+
634+
635+
@respx.mock
636+
def test_error_response_no_error_key_fallback():
637+
respx.get(f"{BASE_URL}/v1/stats").mock(return_value=httpx.Response(422, json={"detail": "validation failed"}))
638+
client = AgentScore(api_key=API_KEY)
639+
with pytest.raises(AgentScoreError) as exc_info:
640+
client.get_stats()
641+
assert exc_info.value.status_code == 422
642+
assert exc_info.value.code == "unknown_error"

tests/test_integration.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,3 +112,27 @@ def test_get_reputation_reputation_field():
112112
pytest.skip("no reputation on test address")
113113
assert isinstance(r["feedback_count"], int)
114114
assert isinstance(r["client_count"], int)
115+
116+
117+
def test_assess_then_get_reputation():
118+
client = AgentScore(api_key=API_KEY, base_url=BASE_URL)
119+
assessed = client.assess(TEST_ADDRESS)
120+
assert "value" in assessed["score"]
121+
122+
rep = client.get_reputation(TEST_ADDRESS)
123+
assert "value" in rep["score"]
124+
assert isinstance(rep["score"]["value"], (int, float))
125+
assert rep["subject"]["address"].lower() == TEST_ADDRESS.lower()
126+
127+
128+
def test_get_agents_items_have_expected_fields():
129+
client = AgentScore(api_key=API_KEY, base_url=BASE_URL)
130+
result = client.get_agents()
131+
132+
assert isinstance(result["items"], list)
133+
assert len(result["items"]) > 0
134+
135+
for item in result["items"]:
136+
assert "chain" in item
137+
assert "token_id" in item
138+
assert "owner_address" in item

0 commit comments

Comments
 (0)