Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ jobs:
- run: uv run ruff check .
- run: uv run ruff format --check .
- run: uv run vulture . vulture_whitelist.py --min-confidence 80 --exclude .venv
- run: uv run pytest tests/
- run: uv run pytest tests/ --cov=agentscore --cov-report=term-missing
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,15 @@ dev = [
"respx>=0.21",
]

[tool.pytest.ini_options]
addopts = "--cov=agentscore --cov-report=term-missing --cov-fail-under=94"

[tool.hatch.build.targets.wheel]
packages = ["agentscore"]

[dependency-groups]
dev = [
"ruff>=0.11.0",
"vulture>=2.15",
"pytest-cov>=6.0",
]
109 changes: 109 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -531,3 +531,112 @@ def test_user_agent_header_includes_version():
client.get_stats()
request = respx.calls[0].request
assert request.headers["user-agent"] == f"agentscore-py/{version('agentscore-py')}"


# ---------------------------------------------------------------------------
# Edge cases
# ---------------------------------------------------------------------------


@respx.mock
def test_get_agents_none_values_excluded_from_params():
route = respx.get(f"{BASE_URL}/v1/agents").mock(return_value=httpx.Response(200, json=AGENTS_PAYLOAD))
client = AgentScore(api_key=API_KEY)
client.get_agents(chain="base", limit=None)
url_str = str(route.calls.last.request.url)
assert "chain=base" in url_str
assert "limit" not in url_str


@respx.mock
def test_assess_refresh_false_not_included_in_body():
route = respx.post(f"{BASE_URL}/v1/assess").mock(return_value=httpx.Response(200, json=ASSESS_PAYLOAD))
client = AgentScore(api_key=API_KEY)
client.assess(ADDRESS, refresh=False)
body = json.loads(route.calls.last.request.content)
assert "refresh" not in body


@respx.mock
def test_double_close():
respx.get(f"{BASE_URL}/v1/stats").mock(return_value=httpx.Response(200, json=STATS_PAYLOAD))
client = AgentScore(api_key=API_KEY)
client.get_stats()
client.close()
client.close()
assert client._sync_client is None


@pytest.mark.asyncio
@respx.mock
async def test_double_aclose():
respx.get(f"{BASE_URL}/v1/stats").mock(return_value=httpx.Response(200, json=STATS_PAYLOAD))
client = AgentScore(api_key=API_KEY)
await client.aget_stats()
await client.aclose()
await client.aclose()
assert client._async_client is None


@pytest.mark.asyncio
@respx.mock
async def test_concurrent_async_calls():
import asyncio

respx.get(f"{BASE_URL}/v1/reputation/{ADDRESS}").mock(return_value=httpx.Response(200, json=REPUTATION_PAYLOAD))
client = AgentScore(api_key=API_KEY)
results = await asyncio.gather(
client.aget_reputation(ADDRESS),
client.aget_reputation(ADDRESS),
client.aget_reputation(ADDRESS),
)
assert len(results) == 3
for r in results:
assert r["score"]["grade"] == "B"
await client.aclose()


@respx.mock
def test_empty_chain_string_not_included_in_params():
route = respx.get(f"{BASE_URL}/v1/reputation/{ADDRESS}").mock(
return_value=httpx.Response(200, json=REPUTATION_PAYLOAD)
)
client = AgentScore(api_key=API_KEY)
client.get_reputation(ADDRESS, chain="")
assert "chain" not in str(route.calls.last.request.url)


@respx.mock
def test_assess_empty_policy_dict_included_in_body():
route = respx.post(f"{BASE_URL}/v1/assess").mock(return_value=httpx.Response(200, json=ASSESS_PAYLOAD))
client = AgentScore(api_key=API_KEY)
client.assess(ADDRESS, policy={})
body = json.loads(route.calls.last.request.content)
assert "policy" in body
assert body["policy"] == {}


@respx.mock
def test_timeout_error_raises_agentscore_error():
respx.get(f"{BASE_URL}/v1/stats").mock(side_effect=httpx.TimeoutException("timed out"))
client = AgentScore(api_key=API_KEY)
with pytest.raises(httpx.TimeoutException):
client.get_stats()


@respx.mock
def test_connect_error_raises_agentscore_error():
respx.get(f"{BASE_URL}/v1/stats").mock(side_effect=httpx.ConnectError("connection refused"))
client = AgentScore(api_key=API_KEY)
with pytest.raises(httpx.ConnectError):
client.get_stats()


@respx.mock
def test_error_response_no_error_key_fallback():
respx.get(f"{BASE_URL}/v1/stats").mock(return_value=httpx.Response(422, json={"detail": "validation failed"}))
client = AgentScore(api_key=API_KEY)
with pytest.raises(AgentScoreError) as exc_info:
client.get_stats()
assert exc_info.value.status_code == 422
assert exc_info.value.code == "unknown_error"
24 changes: 24 additions & 0 deletions tests/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,27 @@ def test_get_reputation_reputation_field():
pytest.skip("no reputation on test address")
assert isinstance(r["feedback_count"], int)
assert isinstance(r["client_count"], int)


def test_assess_then_get_reputation():
client = AgentScore(api_key=API_KEY, base_url=BASE_URL)
assessed = client.assess(TEST_ADDRESS)
assert "value" in assessed["score"]

rep = client.get_reputation(TEST_ADDRESS)
assert "value" in rep["score"]
assert isinstance(rep["score"]["value"], (int, float))
assert rep["subject"]["address"].lower() == TEST_ADDRESS.lower()


def test_get_agents_items_have_expected_fields():
client = AgentScore(api_key=API_KEY, base_url=BASE_URL)
result = client.get_agents()

assert isinstance(result["items"], list)
assert len(result["items"]) > 0

for item in result["items"]:
assert "chain" in item
assert "token_id" in item
assert "owner_address" in item
Loading
Loading