github-actions
Deploy to Hugging Face
c794b6b
Raw
History Blame Contribute Delete
7.73 kB
"""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") == {}