File size: 3,847 Bytes
2c5a5d1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1a502dd
 
 
 
 
 
 
 
2c5a5d1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
"""Regression tests for the adversarial-review fixes (input robustness + clarity).

These lock in invariants that were previously violated: mode validation, the
budget=0 == plain-route guarantee, non-Paris rejection, nonsense-vibe → neutral,
the unnamed-POI demotion, and place-count consistency across UI surfaces.
"""
from __future__ import annotations

import app
from discoverroute.data import taxonomy
from discoverroute.interpret.affinity import resolve_affinity
from discoverroute.pipeline import plan_route
from discoverroute.routing import scoring

S = "Place de la République, Paris"
D = "Jardin du Luxembourg, Paris"


# ---- input validation ------------------------------------------------------
def test_invalid_mode_rejected():
    assert plan_route(S, D, mode="car").error
    assert plan_route(S, D, mode="xyz").error


def test_uppercase_mode_normalized():
    assert plan_route(S, D, mode="WALK").error is None


def test_far_apart_endpoints_rejected_not_namesake_routed():
    # World support: endpoints on different continents can't form one walkable
    # discovery route. Resolve via lat/lon (no network) so the distance cap fires
    # — and crucially we get an honest error, never a fake namesake route.
    london, tokyo = "51.5079, -0.0877", "35.6586, 139.7454"
    r = plan_route(london, tokyo, vibe="quiet")
    assert r.error and "far" in r.error.lower()
    assert r.discovery is None and not r.pois


def test_budget_zero_is_plain_route_even_with_pace_word():
    # P0-3: an explicit 0 slider must win over a vibe pace hint ("all day").
    r = plan_route(S, D, budget=0.0, vibe="I want to spend all day exploring")
    assert r.discovery is None and not r.pois


# ---- vibe interpretation ---------------------------------------------------
def test_nonsense_vibe_degrades_to_neutral():
    aff, _ = resolve_affinity("quantum physics asdfgh")
    spread = max(aff.values()) - min(aff.values())
    assert spread < 1e-6  # all categories equal → neutral


def test_real_vibe_is_confident():
    aff, _ = resolve_affinity("quiet green park")
    assert (max(aff.values()) - min(aff.values())) > 0.2


# ---- unnamed-POI demotion --------------------------------------------------
class _POI:
    def __init__(self, name, category="park_garden", confidence=0.5):
        self.name, self.category, self.confidence = name, category, confidence


def test_unnamed_poi_scored_below_named():
    w = scoring.Weights(category_affinity={"park_garden": 1.0})
    named = scoring.base_score(_POI("Jardin X"), w, adventurousness=1.0)
    unnamed = scoring.base_score(_POI(None), w, adventurousness=1.0)
    assert unnamed < named  # demoted even at max adventurousness


def test_high_adventurousness_not_mostly_unnamed():
    r = plan_route(S, D, vibe="hidden gems off the beaten path",
                   budget=0.7, adventurousness=1.0)
    if r.pois:
        unnamed = sum(1 for p in r.pois
                      if not (getattr(p, "name", None) and str(p.name).strip()))
        assert unnamed / len(r.pois) <= 0.5


# ---- labels & count consistency --------------------------------------------
def test_display_label_article_and_no_snake_case():
    assert taxonomy.display_label(_POI(None, "artwork")) == "a piece of public art"
    assert taxonomy.display_label(_POI(None, "attraction")) == "a landmark"
    assert "_" not in taxonomy.display_label(_POI(None, "monument_historic"))
    assert taxonomy.display_label(_POI("Louvre")) == "Louvre"


def test_place_count_consistent_across_surfaces():
    r = plan_route(S, D, vibe="quiet green bookshops", budget=0.5, n_alternatives=1)
    if r.alternatives:
        a = r.alternatives[0]
        n = len(a.pois)
        assert f"{n} place" in a.summary_md
        assert f"{n} place" in app._alt_label(0, a, r.plain)
        assert f"{n} place" in a.itinerary_md