- main() previously used Config() defaults, silently ignoring config.yaml (port + token); now loads config.yaml from repo root (BRIDGE_CONFIG override) - config.yaml: port 8766 (8765 taken on this host), real sha256 token for xiaozhi-main - smoke_live.py: live WS verification (health, auth rejection x2, handshake, full turn) - README: correct run command (python -m app.main) - HANDOFF/plan: Phase 1.5 live-verified evidence
99 lines
3.4 KiB
Python
99 lines
3.4 KiB
Python
"""Live smoke test against the running bridge (real WS, real auth, mock backends)."""
|
|
import asyncio
|
|
import json
|
|
import sys
|
|
|
|
import httpx
|
|
import websockets
|
|
|
|
BASE = "http://127.0.0.1:8766"
|
|
WS = "ws://127.0.0.1:8766/ws/xiaozhi"
|
|
TOKEN = "8a07643d106f9282a8f2eb1161da4f73657ce99b6602b98df89d011b8caf48a2"
|
|
|
|
results = {}
|
|
|
|
# 1. health
|
|
results["health"] = httpx.get(f"{BASE}/health", timeout=5).json()
|
|
|
|
# 2. bad token must be rejected
|
|
async def bad_token():
|
|
async with websockets.connect(WS) as ws:
|
|
await ws.send(json.dumps({
|
|
"type": "hello",
|
|
"device_id": "xiaozhi-main",
|
|
"authorization": "wrong-token",
|
|
"audio": {"format": "opus", "sample_rate": 16000, "channels": 1, "frame_duration": 20},
|
|
}))
|
|
return await ws.recv()
|
|
|
|
# 3. unknown device must be rejected
|
|
async def bad_device():
|
|
async with websockets.connect(WS) as ws:
|
|
await ws.send(json.dumps({
|
|
"type": "hello",
|
|
"device_id": "intruder",
|
|
"authorization": TOKEN,
|
|
"audio": {"format": "opus", "sample_rate": 16000, "channels": 1, "frame_duration": 20},
|
|
}))
|
|
return await ws.recv()
|
|
|
|
# 4. real handshake + a spoken turn
|
|
async def real_turn():
|
|
frames = []
|
|
audio_bytes = 0
|
|
async with websockets.connect(WS) as ws:
|
|
await ws.send(json.dumps({
|
|
"type": "hello",
|
|
"device_id": "xiaozhi-main",
|
|
"client_id": "esp32-01",
|
|
"authorization": TOKEN,
|
|
"protocol_version": 1,
|
|
"audio": {"format": "opus", "sample_rate": 16000, "channels": 1, "frame_duration": 20},
|
|
}))
|
|
for _ in range(20):
|
|
data = await ws.recv()
|
|
if isinstance(data, bytes):
|
|
audio_bytes += len(data)
|
|
frames.append("audio")
|
|
else:
|
|
frames.append(json.loads(data).get("type"))
|
|
if frames[-1] == "hello": # hello reply → session live
|
|
break
|
|
# one spoken turn: 4800 bytes of PCM (0.15s @ 16k mono 16-bit)
|
|
await ws.send(bytes(range(256)) * 19)
|
|
got_text = False
|
|
got_audio = False
|
|
for _ in range(50):
|
|
data = await ws.recv()
|
|
if isinstance(data, bytes):
|
|
got_audio = True
|
|
audio_bytes += len(data)
|
|
else:
|
|
frames.append(json.loads(data).get("type"))
|
|
if json.loads(data).get("type") == "text":
|
|
got_text = True
|
|
break
|
|
# keep reading TTS audio chunks that follow the text
|
|
while not got_audio:
|
|
data = await ws.recv()
|
|
if isinstance(data, bytes):
|
|
got_audio = True
|
|
audio_bytes += len(data)
|
|
else:
|
|
frames.append(json.loads(data).get("type"))
|
|
return {"frames": frames, "audio_bytes": audio_bytes, "got_text": got_text, "got_audio_out": got_audio}
|
|
|
|
results["bad_token"] = asyncio.run(bad_token())
|
|
results["bad_device"] = asyncio.run(bad_device())
|
|
results["real_turn"] = asyncio.run(real_turn())
|
|
|
|
ok = (
|
|
results["health"].get("status") == "ok"
|
|
and json.loads(results["bad_token"]).get("type") == "error"
|
|
and json.loads(results["bad_device"]).get("type") == "error"
|
|
and results["real_turn"]["got_audio_out"]
|
|
)
|
|
print(json.dumps(results, ensure_ascii=False, indent=2))
|
|
print("LIVE SMOKE:", "PASS" if ok else "FAIL")
|
|
sys.exit(0 if ok else 1)
|