File size: 3,258 Bytes
f00d669
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b34c92a
 
 
 
 
 
 
 
f00d669
 
 
 
b34c92a
 
 
 
f00d669
 
 
b34c92a
 
 
 
 
 
 
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
"""Brick 4 tests: vibe -> preferences and prompt sensitivity (P0-5).

The embedding model is downloaded on first run; tests are skipped if it (or the
data) is unavailable so the suite still runs in a minimal environment.
"""
from __future__ import annotations

import pytest

from discoverroute import config

st = pytest.importorskip("sentence_transformers")

data_ready = pytest.mark.skipif(
    not (config.GRAPH_WALK_PATH.exists() and config.POIS_PATH.exists()),
    reason="Graph or POI table not built",
)


def test_contrasting_vibes_differ():
    from discoverroute.interpret.vibe import interpret
    green = interpret("quiet green wander")
    lively = interpret("lively café crawl and bar hopping")
    # the top category should differ, and green should rank parks above bars
    assert green.top_categories[0] != lively.top_categories[0]
    assert green.affinity["park_garden"] > green.affinity["bar_pub"]
    assert lively.affinity["bar_pub"] > lively.affinity["park_garden"]


def test_affinity_in_range_and_floored():
    from discoverroute.interpret.vibe import interpret
    r = interpret("museums and historic monuments")
    assert all(config.AFFINITY_FLOOR - 1e-6 <= a <= 1.0 + 1e-6
               for a in r.affinity.values())
    assert max(r.affinity.values()) == pytest.approx(1.0)


def test_empty_vibe_is_neutral():
    from discoverroute.interpret.vibe import interpret
    r = interpret("")
    assert set(r.affinity.values()) == {1.0}


def test_budget_and_posture_hints():
    from discoverroute.interpret.vibe import interpret
    quick = interpret("quick direct ride")
    long = interpret("long scenic meander, no rush")
    assert quick.budget_hint is not None and quick.budget_hint < 0.5
    assert long.budget_hint is not None and long.budget_hint > 0.5
    # "ride" is a pass cue -> everything becomes pass-by
    assert set(quick.posture.values()) == {"pass"}


@data_ready
def test_vibe_changes_route_categories():
    """Same A/B, contrasting vibes -> each route serves ITS OWN vibe best (P0-5).

    Property-based rather than exact category counts (which are brittle across
    embedder backends): scoring each route's categories under a vibe's affinity,
    the route planned FOR that vibe must fit it at least as well as the route
    planned for the contrasting vibe.
    """
    from discoverroute.interpret.embed import vibe_to_affinity
    from discoverroute.pipeline import plan_route

    a = "Place de la République, Paris"
    b = "Jardin du Luxembourg, Paris"
    v_green, v_lively = "quiet green park wander", "lively bar and café crawl"
    green = plan_route(a, b, budget=0.7, vibe=v_green)
    lively = plan_route(a, b, budget=0.7, vibe=v_lively)
    assert green.pois and lively.pois

    # the two routes should not select an identical set of waypoints
    assert {p.osm_id for p in green.pois} != {p.osm_id for p in lively.pois}

    def fit(route, affinity):  # mean affinity of the route's categories
        return sum(affinity.get(p.category, 0.0) for p in route.pois) / len(route.pois)

    aff_green, aff_lively = vibe_to_affinity(v_green), vibe_to_affinity(v_lively)
    assert fit(green, aff_green) >= fit(lively, aff_green)
    assert fit(lively, aff_lively) >= fit(green, aff_lively)