File size: 6,134 Bytes
49e9f9d
5d63bd6
49e9f9d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
import pytest
from unittest.mock import AsyncMock
from bson import ObjectId
from app.core.security import create_token
from app.services.verification import compute_verified_score
from app.services import weather as weather_svc
from app.services.ai import triage, detect_language, classify_urgency, generate_headline, similarity, is_duplicate


def _token(role="volunteer"):
    return create_token({"sub": str(ObjectId()), "role": role})


def test_verified_score_caps_at_100():
    assert compute_verified_score(99, 99, True) == 100


def test_verified_score_zero_when_no_signals():
    assert compute_verified_score(0, 0, False) == 0


def test_verified_score_each_source_capped():
    # witnesses alone capped at 40
    assert compute_verified_score(100, 0, False) == 40
    # corroboration alone capped at 40
    assert compute_verified_score(0, 100, False) == 40
    # weather adds exactly 20
    assert compute_verified_score(0, 0, True) == 20


def test_weather_supports_flood_on_heavy_rain():
    assert weather_svc.supports_category("flood", {"precipitation_mm": 8, "wind_kph": 0, "code": 0})
    assert not weather_svc.supports_category("flood", {"precipitation_mm": 0, "wind_kph": 0, "code": 0})


def test_weather_supports_fire_on_dry_wind():
    assert weather_svc.supports_category("fire", {"precipitation_mm": 0, "wind_kph": 30, "code": 0})
    assert not weather_svc.supports_category("fire", {"precipitation_mm": 2, "wind_kph": 30, "code": 0})


def test_weather_supports_returns_false_for_unknown_categories():
    assert not weather_svc.supports_category("medical", {"precipitation_mm": 100, "wind_kph": 100})
    assert not weather_svc.supports_category("flood", None)


# --- AI triage tests (use heuristic fallback via NA_DISABLE_AI_MODEL=1) ---


def test_triage_critical_detects_unconscious():
    t = triage("Man collapsed and is unconscious, not breathing")
    assert t.urgency == "CRITICAL"
    assert "unconscious" in t.triggers
    assert t.priority_score >= 80


def test_triage_detects_child_vulnerability():
    t = triage("A bachcha is trapped inside the flooded building")
    assert t.vulnerability == "child"


def test_triage_detects_elderly_vulnerability():
    t = triage("Elderly man alone and injured after the accident")
    assert t.vulnerability == "elderly"


def test_triage_time_sensitivity_immediate():
    t = triage("Need help immediately, right now")
    assert t.time_sensitivity == "immediate"


def test_triage_time_sensitivity_days():
    t = triage("Can someone come tomorrow to check the power line")
    assert t.time_sensitivity == "days"


def test_detect_language_devanagari():
    assert detect_language("मदद चाहिए जल्दी") == "hi"


def test_detect_language_hinglish():
    assert detect_language("aag lagi hai madad karo") == "hi-Latn"


def test_detect_language_english():
    assert detect_language("fire in the building help") == "en"


def test_backcompat_classify_urgency_signature():
    urgency, reason = classify_urgency("person is bleeding heavily")
    assert urgency == "CRITICAL"
    assert reason  # non-empty


# --- headline + similarity ---


def test_headline_prefers_first_sentence():
    assert generate_headline("Fire in the kitchen. Everyone is safe.") == "Fire in the kitchen."


def test_headline_truncates_long_text():
    long_text = "a very long description " * 20
    h = generate_headline(long_text, max_len=50)
    assert len(h) <= 51  # 50 + ellipsis
    assert h.endswith("…")


def test_headline_empty():
    assert generate_headline("") == ""


def test_similarity_identical_is_one():
    s = "fire near park gate"
    assert similarity(s, s) == 1.0


def test_similarity_disjoint_is_low():
    assert similarity("fire in kitchen", "cat chased mouse") < 0.2


def test_is_duplicate_detects_near_match():
    a = "fire in the building near gate 3"
    b = "there is a fire near gate 3 in the building"
    assert is_duplicate(a, b)


def test_is_duplicate_rejects_unrelated():
    assert not is_duplicate("fire near park", "child lost at market")


@pytest.mark.asyncio
async def test_witness_rejects_own_alert(client):
    c, db = client
    alert_id = ObjectId()
    token = _token("volunteer")
    # The JWT sub becomes the user id; make the alert owned by the same id
    import jose.jwt as jj
    from app.core.config import settings
    sub = jj.decode(token, settings.JWT_SECRET, algorithms=[settings.JWT_ALGORITHM])["sub"]

    db.alerts.find_one = AsyncMock(
        return_value={
            "_id": alert_id,
            "reporter_id": ObjectId(sub),
            "status": "open",
            "location": {"type": "Point", "coordinates": [76.7794, 30.7333]},
        }
    )
    resp = await c.post(
        f"/api/alerts/{alert_id}/witness",
        headers={"Authorization": f"Bearer {token}"},
    )
    assert resp.status_code == 400


@pytest.mark.asyncio
async def test_witness_rejects_far_user(client):
    c, db = client
    alert_id = ObjectId()
    token = _token("volunteer")
    import jose.jwt as jj
    from app.core.config import settings
    sub = jj.decode(token, settings.JWT_SECRET, algorithms=[settings.JWT_ALGORITHM])["sub"]

    db.alerts.find_one = AsyncMock(
        return_value={
            "_id": alert_id,
            "reporter_id": ObjectId(),  # someone else
            "status": "open",
            "location": {"type": "Point", "coordinates": [76.7794, 30.7333]},
        }
    )
    db.users.find_one = AsyncMock(
        return_value={
            "_id": ObjectId(sub),
            # >2 km away
            "location": {"type": "Point", "coordinates": [77.2090, 28.6139]},
        }
    )
    resp = await c.post(
        f"/api/alerts/{alert_id}/witness",
        headers={"Authorization": f"Bearer {token}"},
    )
    assert resp.status_code == 403


@pytest.mark.asyncio
async def test_witness_missing_alert(client):
    c, db = client
    db.alerts.find_one = AsyncMock(return_value=None)
    resp = await c.post(
        f"/api/alerts/{ObjectId()}/witness",
        headers={"Authorization": f"Bearer {_token()}"},
    )
    assert resp.status_code == 404