"""AC-002: app imports and the /ws/xiaozhi endpoint is live (handshake + turn).""" from __future__ import annotations import json import pytest from fastapi.testclient import TestClient from app.config import Config from app.main import make_app from app.security import DeviceAuth TOKEN = "test-device-token" def _cfg() -> Config: return Config.from_dict( { "security": {"enabled": True, "devices": [ {"device_id": "d1", "token_hash": DeviceAuth.hash_token(TOKEN)} ]}, } ) def test_health(): client = TestClient(make_app(_cfg())) r = client.get("/health") assert r.status_code == 200 assert r.json()["status"] == "ok" def test_ota_endpoint_delivers_server(): """FIRMWARE: device reads websocket.url/token from /ota POST.""" cfg = _cfg() from app.config import OtaConfig cfg.ota = OtaConfig( server_ws_url="wss://zhi.moreminimore.com/ws/xiaozhi", server_token=TOKEN, ) client = TestClient(make_app(cfg)) r = client.post("/ota", json={"device_id": "d1"}) assert r.status_code == 200 body = r.json() assert body["websocket"]["url"] == "wss://zhi.moreminimore.com/ws/xiaozhi" assert body["websocket"]["token"] == TOKEN assert "server_time" in body def test_ws_auth_via_headers(): """FIRMWARE: token in Authorization header, Device-Id = MAC.""" client = TestClient(make_app(_cfg())) with client.websocket_connect( "/ws/xiaozhi", headers={ "Device-Id": "d1", "Authorization": f"Bearer {TOKEN}", }, ) as ws: # hello frame WITHOUT auth fields — auth came in headers ws.send_text(json.dumps({"device_id": "d1"})) reply = json.loads(ws.receive_text()) assert reply["type"] == "hello" assert reply["transport"] == "websocket" assert reply["session_id"] def test_ws_header_auth_rejects_bad_token(): client = TestClient(make_app(_cfg())) with client.websocket_connect( "/ws/xiaozhi", headers={"Device-Id": "d1", "Authorization": "Bearer wrong"}, ) as ws: ws.send_text(json.dumps({"device_id": "d1"})) reply = json.loads(ws.receive_text()) assert reply["type"] == "error" def test_ws_handshake_and_turn(): """One spoken utterance (burst of speech frames + trailing silence) -> firmware-compatible reply: stt text, tts start/sentence/stop, audio.""" client = TestClient(make_app(_cfg())) with client.websocket_connect("/ws/xiaozhi") as ws: # hello frame ws.send_text(json.dumps({"device_id": "d1", "authorization": TOKEN})) reply = json.loads(ws.receive_text()) assert reply["type"] == "hello" assert reply["session_id"] # one utterance: 6 loud frames (16-bit, RMS >= 300), then silence # frames that push the VAD past min_silence_frames. loud = b"\x10\x0f" * 320 # 20 ms @ 16 kHz mono, amplitude 3856 silent = b"\x00\x00" * 320 for _ in range(6): ws.send_bytes(loud) for _ in range(12): ws.send_bytes(silent) got_stt = got_audio = False tts_stop = False deadline = 0 while not tts_stop and deadline < 256: frame = ws.receive() deadline += 1 if frame.get("text") is not None: payload = json.loads(frame["text"]) if payload.get("type") == "stt": got_stt = True if payload.get("type") == "tts" and payload.get("state") == "stop": tts_stop = True else: got_audio = True assert got_stt and got_audio and tts_stop