shl-recommender / tests /test_schema.py
Eshit's picture
HF Space deploy snapshot
5733f37
Raw
History Blame Contribute Delete
6.42 kB
"""Tests for the API contract / Pydantic schema (CLAUDE.md §3, §9).
Assert that ChatResponse is always valid: recommendations is [] or 1–10 items,
each item has exactly {name, url, test_type}, and malformed inputs are rejected
(so the agent layer can fall back to a valid response).
"""
from __future__ import annotations
import pytest
from fastapi.testclient import TestClient
from pydantic import ValidationError
from app.main import app
from app.schemas import (
MAX_RECOMMENDATIONS,
ChatRequest,
ChatResponse,
Message,
Recommendation,
)
VALID_URL = "https://www.shl.com/products/product-catalog/view/spring-new/"
def _rec(i: int = 0) -> Recommendation:
return Recommendation(name=f"Test {i}", url=VALID_URL, test_type="K")
# --- Message / ChatRequest ----------------------------------------------------
def test_message_valid_roles():
for role in ("user", "assistant", "system"):
assert Message(role=role, content="hi").role == role
def test_message_bad_role_rejected():
with pytest.raises(ValidationError):
Message(role="tool", content="hi")
def test_chat_request_requires_at_least_one_message():
with pytest.raises(ValidationError):
ChatRequest(messages=[])
def test_chat_request_parses_history():
req = ChatRequest(
messages=[
{"role": "user", "content": "Hiring a Java dev"},
{"role": "assistant", "content": "What seniority?"},
{"role": "user", "content": "Mid-level"},
]
)
assert len(req.messages) == 3
assert req.messages[0].role == "user"
def test_chat_request_ignores_extra_message_keys():
req = ChatRequest(messages=[{"role": "user", "content": "hi", "name": "x"}])
assert req.messages[0].content == "hi"
# --- Recommendation -----------------------------------------------------------
def test_recommendation_valid():
rec = Recommendation(name="Spring (New)", url=VALID_URL, test_type="K")
assert rec.test_type == "K"
def test_recommendation_multi_letter_test_type_ok():
# Traces render multi-key items as comma-joined codes, e.g. "K,S".
rec = Recommendation(name="Microsoft Excel 365", url=VALID_URL, test_type="K,S")
assert rec.test_type == "K,S"
@pytest.mark.parametrize("field", ["name", "url", "test_type"])
def test_recommendation_missing_field_rejected(field):
kwargs = {"name": "X", "url": VALID_URL, "test_type": "K"}
del kwargs[field]
with pytest.raises(ValidationError):
Recommendation(**kwargs)
@pytest.mark.parametrize("field", ["name", "url", "test_type"])
def test_recommendation_blank_field_rejected(field):
kwargs = {"name": "X", "url": VALID_URL, "test_type": "K"}
kwargs[field] = " "
with pytest.raises(ValidationError):
Recommendation(**kwargs)
def test_recommendation_non_http_url_rejected():
with pytest.raises(ValidationError):
Recommendation(name="X", url="www.shl.com/foo", test_type="K")
def test_recommendation_forbids_extra_keys():
with pytest.raises(ValidationError):
Recommendation(
name="X", url=VALID_URL, test_type="K", description="not allowed"
)
def test_recommendation_trims_whitespace():
rec = Recommendation(name=" Spring ", url=VALID_URL, test_type=" K ")
assert rec.name == "Spring"
assert rec.test_type == "K"
# --- ChatResponse -------------------------------------------------------------
def test_response_empty_recommendations_valid():
resp = ChatResponse(reply="What seniority?", recommendations=[])
assert resp.recommendations == []
assert resp.end_of_conversation is False
def test_response_default_recommendations_is_empty():
resp = ChatResponse(reply="hi")
assert resp.recommendations == []
def test_response_ten_recommendations_valid():
resp = ChatResponse(
reply="Here you go",
recommendations=[_rec(i) for i in range(MAX_RECOMMENDATIONS)],
end_of_conversation=True,
)
assert len(resp.recommendations) == MAX_RECOMMENDATIONS
def test_response_too_many_recommendations_rejected():
with pytest.raises(ValidationError):
ChatResponse(
reply="Here you go",
recommendations=[_rec(i) for i in range(MAX_RECOMMENDATIONS + 1)],
)
def test_response_blank_reply_rejected():
with pytest.raises(ValidationError):
ChatResponse(reply="", recommendations=[])
def test_response_forbids_extra_keys():
with pytest.raises(ValidationError):
ChatResponse(reply="hi", recommendations=[], foo="bar")
def test_response_bad_recommendation_item_rejected():
with pytest.raises(ValidationError):
ChatResponse(reply="hi", recommendations=[{"name": "X"}])
# --- Endpoints ----------------------------------------------------------------
def test_health_endpoint():
client = TestClient(app)
r = client.get("/health")
assert r.status_code == 200
assert r.json() == {"status": "ok"}
def test_chat_endpoint_returns_valid_contract(monkeypatch):
# Stub the agent so this exercises the endpoint plumbing / contract shape
# deterministically (no LLM/network). Agent behavior is covered separately.
def fake_handle(messages):
return ChatResponse(
reply="What seniority are you hiring for?",
recommendations=[],
end_of_conversation=False,
)
monkeypatch.setattr("app.main.handle", fake_handle)
client = TestClient(app)
r = client.post(
"/chat", json={"messages": [{"role": "user", "content": "Hiring a Java dev"}]}
)
assert r.status_code == 200
body = r.json()
assert set(body.keys()) == {"reply", "recommendations", "end_of_conversation"}
assert body["recommendations"] == []
assert isinstance(body["reply"], str) and body["reply"]
assert body["end_of_conversation"] is False
# Each returned item (when present) must carry exactly the 3 contract keys.
for item in body["recommendations"]:
assert set(item.keys()) == {"name", "url", "test_type"}
def test_chat_endpoint_rejects_empty_messages():
client = TestClient(app)
r = client.post("/chat", json={"messages": []})
assert r.status_code == 422
def test_chat_endpoint_rejects_bad_role():
client = TestClient(app)
r = client.post("/chat", json={"messages": [{"role": "tool", "content": "x"}]})
assert r.status_code == 422