Files
goodgo-platform/libs/ai-services/tests/test_neighborhood.py
Ho Ngoc Hai 2c1e3771e9 feat(analytics): add Python NeighborhoodScore service + NestJS HTTP proxy (TEC-2756)
- libs/ai-services: new POST /neighborhood/score router computing weighted
  6-axis livability score from per-category POI counts; algorithm versioned
  for future iteration (sigmoid curves, percentile thresholds).
- apps/api: HttpNeighborhoodScoreService proxies to Python first, falls back
  to PrismaNeighborhoodScoreService when AI service unavailable. Mirrors the
  HttpAVMService pattern. Existing GET /analytics/neighborhoods/:district/score
  endpoint and CQRS handler now flow through the proxy.
- AnalyticsModule binds Http variant by default, retains Prisma variant as
  injectable fallback.
- Tests: 5 pytest cases for Python heuristic, 4 vitest cases for HTTP proxy
  fallback behaviour.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-18 15:07:02 +07:00

120 lines
3.2 KiB
Python

from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_zero_counts_yields_zero_score():
resp = client.post(
"/neighborhood/score",
json={
"district": "Quận 7",
"city": "Hồ Chí Minh",
"poi_counts": {
"education": 0,
"healthcare": 0,
"transport": 0,
"shopping": 0,
"greenery": 0,
"safety": 0,
},
},
)
assert resp.status_code == 200
data = resp.json()
assert data["total_score"] == 0
assert data["education_score"] == 0
assert data["algorithm_version"].startswith("neighborhood-heuristic")
def test_saturated_counts_yields_one_hundred():
resp = client.post(
"/neighborhood/score",
json={
"district": "Quận 1",
"city": "Hồ Chí Minh",
"poi_counts": {
"education": 50,
"healthcare": 50,
"transport": 50,
"shopping": 50,
"greenery": 50,
"safety": 50,
},
},
)
assert resp.status_code == 200
data = resp.json()
assert data["total_score"] == 100.0
assert data["education_score"] == 10
assert data["safety_score"] == 10
def test_partial_counts_apply_weighted_average():
# Hit the linear cap on transport+greenery only; others 0.
# transport weight 20 + greenery 15 = 35 → expect total 35.0.
resp = client.post(
"/neighborhood/score",
json={
"district": "Bình Thạnh",
"city": "Hồ Chí Minh",
"poi_counts": {
"education": 0,
"healthcare": 0,
"transport": 12,
"shopping": 0,
"greenery": 6,
"safety": 0,
},
},
)
assert resp.status_code == 200
data = resp.json()
assert data["transport_score"] == 10
assert data["greenery_score"] == 10
assert data["total_score"] == 35.0
assert data["poi_counts"]["transport"] == 12
def test_below_max_uses_linear_scale():
# education max=15, count=3 → 2.0; weight 20 → contributes 4.0
resp = client.post(
"/neighborhood/score",
json={
"district": "Quận 3",
"city": "Hồ Chí Minh",
"poi_counts": {
"education": 3,
"healthcare": 0,
"transport": 0,
"shopping": 0,
"greenery": 0,
"safety": 0,
},
},
)
assert resp.status_code == 200
data = resp.json()
assert data["education_score"] == 2.0
assert data["total_score"] == 4.0
def test_validation_rejects_negative_count():
resp = client.post(
"/neighborhood/score",
json={
"district": "Quận 1",
"city": "Hồ Chí Minh",
"poi_counts": {
"education": -1,
"healthcare": 0,
"transport": 0,
"shopping": 0,
"greenery": 0,
"safety": 0,
},
},
)
assert resp.status_code == 422