File size: 7,725 Bytes
c794b6b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
"""API contract tests — run with CEPHEUS_CLOUD=1 to avoid heavy ML imports."""
import os
import sys
from unittest.mock import MagicMock

os.environ.setdefault("CEPHEUS_CLOUD", "1")
os.environ.setdefault("CEPHEUS_API_KEY", "test-key")
os.environ.pop("CEPHEUS_AUTH_DEV_MODE", None)
os.environ.pop("CEPHEUS_JWT_SECRET", None)

# Stub Google ADK so tests run without the full agent stack installed.
for mod in (
    "google.adk",
    "google.adk.agents",
    "google.adk.runners",
    "google.adk.sessions",
):
    sys.modules.setdefault(mod, MagicMock())

BACKEND_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if BACKEND_DIR not in sys.path:
    sys.path.insert(0, BACKEND_DIR)

from fastapi.testclient import TestClient  # noqa: E402

import main  # noqa: E402

API_HEADERS = {"X-API-Key": "test-key"}
client = TestClient(main.app)


def test_health():
    r = client.get("/health")
    assert r.status_code == 200
    assert r.json()["status"] == "ok"


def test_health_live_wake():
    r = client.get("/health/live")
    assert r.status_code == 200
    body = r.json()
    assert body["status"] in ("warming", "ready")
    assert "message" in body


def test_gossip_start_accepts_payload():
    r = client.post(
        "/gossip/start",
        headers=API_HEADERS,
        json={"staffId": "STAFF-42", "personName": "Alex", "cause": "Suspicious contact"},
    )
    assert r.status_code == 200
    body = r.json()
    assert body["status"] == "started"
    assert body["tracking"]["staffId"] == "STAFF-42"
    assert body["tracking"]["personName"] == "Alex"
    assert body["tracking"]["cause"] == "Suspicious contact"
    assert body["root_person"] == "Alex"

    gossip = client.get("/gossip", headers=API_HEADERS).json()
    assert gossip["is_tracking"] is True
    assert gossip["tracking"]["staffId"] == "STAFF-42"
    assert gossip["root_person"] == "Alex"


def test_gossip_stop_clears_tracking_meta():
    client.post("/gossip/start", headers=API_HEADERS, json={"personName": "Pat"})
    r = client.post("/gossip/stop", headers=API_HEADERS)
    assert r.status_code == 200
    gossip = client.get("/gossip", headers=API_HEADERS).json()
    assert gossip["is_tracking"] is False
    assert gossip["tracking"] == {}


def test_sos_persists_to_get():
    before = len(client.get("/sos", headers=API_HEADERS).json())
    r = client.post(
        "/sos",
        headers=API_HEADERS,
        json={
            "guest_id": "guest-test-1",
            "lat": 12.97,
            "lng": 77.59,
            "location_label": "Gate A",
            "message": "Need help",
        },
    )
    assert r.status_code == 200
    assert r.json()["status"] == "received"
    after = client.get("/sos", headers=API_HEADERS).json()
    assert len(after) == before + 1
    assert after[-1]["guest_id"] == "guest-test-1"


def test_create_issue_defaults_title_and_broadcast_shape():
    r = client.post(
        "/issues",
        headers=API_HEADERS,
        json={"desc": "Smoke in corridor B", "type": "fire"},
    )
    assert r.status_code == 200
    issue = r.json()
    assert issue["title"] == "Smoke in corridor B"
    assert issue["status"] == "ONGOING"

    listed = client.get("/issues", headers=API_HEADERS).json()
    assert any(i["id"] == issue["id"] for i in listed)


def test_unauthorized_without_api_key():
    r = client.post("/issues", json={"title": "x", "desc": "y"})
    assert r.status_code == 401


def test_signage_toggle_sets_explicit_active_state():
    r = client.post(
        "/signage/s1/toggle",
        headers=API_HEADERS,
        json={"active": True},
    )
    assert r.status_code == 200
    body = r.json()
    assert body["id"] == "s1"
    assert body["active"] is True

    state = client.get("/signage", headers=API_HEADERS).json()
    assert state["s1"] is True


def test_files_upload_returns_normalized_path():
    png = (
        b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01"
        b"\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90wS\xde\x00\x00\x00\x0cIDATx\x9cc\x00\x01"
        b"\x00\x00\x05\x00\x01\x0d\n-\xb4\x00\x00\x00\x00IEND\xaeB`\x82"
    )
    r = client.post(
        "/files/upload",
        headers=API_HEADERS,
        files={"file": ("tiny.png", png, "image/png")},
    )
    assert r.status_code == 200
    body = r.json()
    assert body["url"].startswith("/files/uploads/")
    assert body["path"] == body["url"]


def test_emergency_nearby_returns_services(monkeypatch):
    import types

    fake_payload = {
        "elements": [
            {"id": 1, "lat": 12.98, "lon": 77.60, "tags": {"amenity": "hospital", "name": "Test Hospital"}},
            {"id": 2, "lat": 12.96, "lon": 77.58, "tags": {"amenity": "police", "name": "Test PS"}},
            {"id": 3, "lat": 12.97, "lon": 77.59, "tags": {"amenity": "cafe", "name": "Ignore Me"}},
        ]
    }

    class FakeResp:
        def raise_for_status(self):
            return None

        def json(self):
            return fake_payload

    class FakeClient:
        def __init__(self, *a, **k):
            pass

        async def __aenter__(self):
            return self

        async def __aexit__(self, *a):
            return False

        async def post(self, *a, **k):
            return FakeResp()

    monkeypatch.setattr(main, "httpx", types.SimpleNamespace(AsyncClient=FakeClient))
    main._nearby_cache.clear()

    r = client.get("/emergency/nearby?lat=12.9716&lng=77.5946", headers=API_HEADERS)
    assert r.status_code == 200
    body = r.json()
    assert body["source"] == "overpass"
    assert body["services"]["hospital"][0]["name"] == "Test Hospital"
    assert body["services"]["police"][0]["name"] == "Test PS"
    assert "cafe" not in body["services"]
    assert isinstance(body["services"]["hospital"][0]["distKm"], (int, float))


def test_gossip_ingest_frame_co_presence_only(monkeypatch):
    """Single-person frames must NOT create interaction edges; pairs in one frame do."""
    class FakeEngine:
        def reload_db(self):
            return None

        def match_all_faces(self, frame, threshold=None):
            return [
                {"name": "ContactA", "confidence": 0.9, "bbox": [0, 0, 1, 1], "found": True},
                {"name": "ContactB", "confidence": 0.85, "bbox": [2, 2, 3, 3], "found": True},
            ]

    monkeypatch.setattr(main.vision_engine, "face_engine", FakeEngine())
    client.post("/gossip/start", headers=API_HEADERS, json={"personName": "RootGuy"})

    import cv2
    import numpy as np

    frame = np.zeros((16, 16, 3), dtype=np.uint8)
    ok, buf = cv2.imencode(".jpg", frame)
    assert ok
    jpeg_bytes = buf.tobytes()

    r = client.post(
        "/gossip/ingest_frame",
        headers=API_HEADERS,
        data={"cam_id": "cam-01"},
        files={"file": ("frame.jpg", jpeg_bytes, "image/jpeg")},
    )
    assert r.status_code == 200
    body = r.json()
    assert "ContactA" in body["names"]
    assert "ContactB" in body["names"]
    pairs = {(l["source"], l["target"]) for l in body["graph"]["links"]}
    assert ("ContactA", "ContactB") in pairs or ("ContactB", "ContactA") in pairs


def test_tracking_session_reset_clears_history():
    client.post("/gossip/start", headers=API_HEADERS, json={"personName": "ResetTest", "staffId": "S1"})
    r = client.post("/tracking/session/reset", headers=API_HEADERS, json={"broadcast": False})
    assert r.status_code == 200
    body = r.json()
    assert body["status"] == "reset"
    assert body.get("broadcast") is False
    gossip = client.get("/gossip", headers=API_HEADERS).json()
    assert gossip.get("total_interactions", len(gossip.get("links", []))) == 0 or len(gossip.get("links", [])) == 0
    assert gossip.get("tracking") == {}