wanderlust-chatbot / tests /test_predict_helpers.py
Kiriten892's picture
fix(chatbot): asyncio.run() replaces deprecated get_event_loop in tests; update appendix Chapter C figures
5fabc4a
Raw
History Blame Contribute Delete
12.1 kB
"""
Unit tests for pure helper functions extracted into routes.py.
Covers:
- Module-level constants: _TRAVEL_INTENTS, _TRAVEL_KEYWORDS_RE, _TERRAIN_KEYWORDS
- _build_destination_info_card()
- _build_nearby_attractions_card()
- _build_travel_tips_card()
- _classify_intent_safe() — exception path returns safe fallback
- _extract_entities_safe() — exception path returns empty entities dict
"""
import re
import pytest
import asyncio
from app.api.routes import (
_TRAVEL_INTENTS,
_TRAVEL_KEYWORDS_RE,
_TERRAIN_KEYWORDS,
_build_destination_info_card,
_build_nearby_attractions_card,
_build_travel_tips_card,
_classify_intent_safe,
_extract_entities_safe,
)
# ---------------------------------------------------------------------------
# Module-level constants
# ---------------------------------------------------------------------------
class TestModuleConstants:
def test_travel_intents_is_frozenset(self):
assert isinstance(_TRAVEL_INTENTS, frozenset)
def test_travel_intents_contains_core_intents(self):
for intent in ("plan_trip", "find_hotel", "find_flight", "budget_advice"):
assert intent in _TRAVEL_INTENTS, f"Expected '{intent}' in _TRAVEL_INTENTS"
def test_travel_keywords_re_is_compiled_pattern(self):
assert isinstance(_TRAVEL_KEYWORDS_RE, type(re.compile("")))
def test_travel_keywords_re_matches_english_keywords(self):
assert _TRAVEL_KEYWORDS_RE.search("I want to book a hotel") is not None
assert _TRAVEL_KEYWORDS_RE.search("looking for flight tickets") is not None
def test_travel_keywords_re_matches_vietnamese_keywords(self):
assert _TRAVEL_KEYWORDS_RE.search("đặt phòng khách sạn") is not None
def test_travel_keywords_re_no_match_for_unrelated(self):
# Should not match random non-travel text
assert _TRAVEL_KEYWORDS_RE.search("hello world foo bar") is None
def test_terrain_keywords_has_vi_and_en(self):
assert "vi" in _TERRAIN_KEYWORDS
assert "en" in _TERRAIN_KEYWORDS
def test_terrain_keywords_vi_has_terrain_types(self):
vi = _TERRAIN_KEYWORDS["vi"]
for key in ("beach", "mountain", "city"):
assert key in vi, f"Expected terrain type '{key}' in vi terrain keywords"
# ---------------------------------------------------------------------------
# _build_destination_info_card()
# ---------------------------------------------------------------------------
class TestBuildDestinationInfoCard:
def _sample_dest(self, **overrides):
base = {
"id": "hanoi-001",
"name": "Hà Nội",
"name_en": "Hanoi",
"description": "Capital of Vietnam",
"description_en": "Capital of Vietnam",
"best_months": [10, 11, 12],
"tags": ["city", "heritage", "food"],
"highlights": ["Hoan Kiem Lake", "Old Quarter"],
"avg_budget_per_day": 500000,
"lat": 21.028511,
"lon": 105.804817,
"image_url": "https://example.com/hanoi.jpg",
"region": "North Vietnam",
}
base.update(overrides)
return base
def test_returns_correct_type(self):
card = _build_destination_info_card(self._sample_dest())
assert card["type"] == "destination_info"
def test_data_contains_required_keys(self):
card = _build_destination_info_card(self._sample_dest())
data = card["data"]
for key in ("id", "name", "name_en", "description", "best_months", "tags",
"highlights", "avg_budget_per_day", "lat", "lon", "image_url", "region"):
assert key in data, f"Missing key '{key}' in destination_info card data"
def test_tags_limited_to_8(self):
dest = self._sample_dest(tags=["t1", "t2", "t3", "t4", "t5", "t6", "t7", "t8", "t9"])
card = _build_destination_info_card(dest)
assert len(card["data"]["tags"]) <= 8
def test_highlights_limited_to_5(self):
dest = self._sample_dest(highlights=[f"h{i}" for i in range(10)])
card = _build_destination_info_card(dest)
assert len(card["data"]["highlights"]) <= 5
def test_empty_destination_uses_defaults(self):
"""Empty dict should not raise — all fields fall back to defaults."""
card = _build_destination_info_card({})
assert card["type"] == "destination_info"
assert card["data"]["id"] == ""
assert card["data"]["best_months"] == []
def test_fallback_description_uses_description_vi(self):
dest = {"description_vi": "Mô tả tiếng Việt"}
card = _build_destination_info_card(dest)
assert card["data"]["description"] == "Mô tả tiếng Việt"
def test_budget_per_day_fallback(self):
dest = {"budget_per_day": 300000}
card = _build_destination_info_card(dest)
assert card["data"]["avg_budget_per_day"] == 300000
# ---------------------------------------------------------------------------
# _build_nearby_attractions_card()
# ---------------------------------------------------------------------------
class TestBuildNearbyAttractionsCard:
def test_returns_correct_type(self):
card = _build_nearby_attractions_card({"id": "x", "name": "Test"})
assert card["type"] == "nearby_attractions"
def test_data_contains_required_keys(self):
card = _build_nearby_attractions_card({"id": "x", "name": "Test"})
for key in ("destination_id", "destination_name", "attractions", "tags"):
assert key in card["data"], f"Missing key '{key}'"
def test_dict_activities_normalised_correctly(self):
dest = {
"id": "d1",
"name": "Da Nang",
"popular_activities": [
{"name": "Beach walk", "type": "outdoor", "cost_vnd": 0, "duration": "2h",
"description": "Walk along the beach"},
],
}
card = _build_nearby_attractions_card(dest)
attractions = card["data"]["attractions"]
assert len(attractions) == 1
assert attractions[0]["name"] == "Beach walk"
assert attractions[0]["type"] == "outdoor"
def test_string_activities_normalised_to_dict(self):
dest = {
"id": "d2",
"name": "Hoi An",
"popular_activities": ["Lantern festival", "Boat ride"],
}
card = _build_nearby_attractions_card(dest)
attractions = card["data"]["attractions"]
assert all(isinstance(a, dict) for a in attractions)
assert attractions[0]["name"] == "Lantern festival"
def test_attractions_limited_to_8(self):
dest = {
"popular_activities": [{"name": f"act{i}", "type": "tour"} for i in range(12)],
}
card = _build_nearby_attractions_card(dest)
assert len(card["data"]["attractions"]) <= 8
def test_empty_destination_no_crash(self):
card = _build_nearby_attractions_card({})
assert card["type"] == "nearby_attractions"
assert card["data"]["attractions"] == []
# ---------------------------------------------------------------------------
# _build_travel_tips_card()
# ---------------------------------------------------------------------------
class TestBuildTravelTipsCard:
def test_returns_correct_type(self):
card = _build_travel_tips_card({"id": "x", "name": "Test"})
assert card["type"] == "travel_tips"
def test_data_contains_required_keys(self):
card = _build_travel_tips_card({"id": "x", "name": "Test"})
for key in ("destination_id", "destination_name", "best_months", "transport", "visa",
"safety_tips", "currency"):
assert key in card["data"]
def test_transport_as_list_coerced_to_empty_dict(self):
"""If transport is a list (invalid shape), coerce to empty dict."""
dest = {"transport": ["bus", "taxi"]}
card = _build_travel_tips_card(dest)
assert isinstance(card["data"]["transport"], dict)
assert card["data"]["transport"] == {}
def test_transport_as_dict_preserved(self):
dest = {"transport": {"bus": "available", "taxi": "cheap"}}
card = _build_travel_tips_card(dest)
assert card["data"]["transport"] == {"bus": "available", "taxi": "cheap"}
def test_visa_info_string_coerced_to_dict(self):
dest = {"visa_info": "Visa-free for 30 days"}
card = _build_travel_tips_card(dest)
assert isinstance(card["data"]["visa"], dict)
assert card["data"]["visa"] == {"note": "Visa-free for 30 days"}
def test_visa_fallback_to_visa_key(self):
dest = {"visa": {"requirement": "e-visa"}}
card = _build_travel_tips_card(dest)
assert card["data"]["visa"] == {"requirement": "e-visa"}
def test_empty_destination_no_crash(self):
card = _build_travel_tips_card({})
assert card["type"] == "travel_tips"
# ---------------------------------------------------------------------------
# _classify_intent_safe() — async helper
# ---------------------------------------------------------------------------
class TestClassifyIntentSafe:
def _run(self, coro):
return asyncio.run(coro)
def test_normal_classifier_returns_result(self):
class FakeClf:
def predict(self, text, lang):
return {"intent": "find_hotel", "confidence": 0.9, "method": "ml_model"}
result = self._run(_classify_intent_safe("book a hotel", "en", FakeClf()))
assert result["intent"] == "find_hotel"
def test_classifier_raises_returns_fallback(self):
class BrokenClf:
def predict(self, text, lang):
raise RuntimeError("model crashed")
result = self._run(_classify_intent_safe("anything", "en", BrokenClf()))
assert result["intent"] == "fallback"
assert result["confidence"] == 0.0
assert result["method"] == "error_fallback"
def test_fallback_dict_has_required_keys(self):
class BrokenClf:
def predict(self, text, lang):
raise ValueError("bad input")
result = self._run(_classify_intent_safe("text", "vi", BrokenClf()))
assert {"intent", "confidence", "method"} <= set(result.keys())
# ---------------------------------------------------------------------------
# _extract_entities_safe() — async helper
# ---------------------------------------------------------------------------
class TestExtractEntitiesSafe:
def _run(self, coro):
return asyncio.run(coro)
def test_normal_ner_returns_result(self):
class FakeNER:
def extract(self, text, lang):
return {"locations": ["Hanoi"], "destination_locations": []}
result = self._run(_extract_entities_safe("trip to Hanoi", "en", FakeNER()))
assert result["locations"] == ["Hanoi"]
def test_ner_raises_returns_empty_entities(self):
class BrokenNER:
def extract(self, text, lang):
raise RuntimeError("NER model crashed")
result = self._run(_extract_entities_safe("text", "en", BrokenNER()))
# Should return empty entities dict without raising
assert isinstance(result, dict)
assert "locations" in result
assert result["locations"] == []
def test_empty_entities_has_all_expected_keys(self):
class BrokenNER:
def extract(self, text, lang):
raise Exception("any error")
result = self._run(_extract_entities_safe("text", "vi", BrokenNER()))
expected_keys = {
"locations", "origin", "destination_locations",
"duration", "budget", "num_people",
"hotel_type", "dietary_restrictions", "cuisine_preferences",
"travel_style", "budget_priorities", "transport_preference",
"time_preference", "activity_preferences", "is_multi_destination",
}
assert expected_keys <= set(result.keys())