Files
wehub-resource-sync a789495a98
FreeBSD Smoke / FreeBSD Smoke (x86_64) (push) Has been cancelled
CI / Quality Guardrails (push) Has been cancelled
CI / Build & Test (macos-latest) (push) Has been cancelled
CI / Build & Test (ubuntu-latest) (push) Has been cancelled
CI / Build & Test (windows-latest) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / PowerShell Syntax (push) Has been cancelled
CI / Windows Cross-Target Check (Linux) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:34 +08:00

242 lines
8.6 KiB
Python
Executable File

#!/usr/bin/env python3
"""End-to-end protocol smoke test against the mock gateway.
Exercises: /health, /pair, ws upgrade, subscribe, get_history, message stream.
Pure stdlib so it runs anywhere. Asserts the full happy-path event sequence.
"""
import argparse
import base64
import http.client
import json
import os
import socket
import struct
import sys
def http_get(host, port, path):
conn = http.client.HTTPConnection(host, port, timeout=5)
conn.request("GET", path)
resp = conn.getresponse()
data = resp.read().decode()
conn.close()
return resp.status, data
def http_post(host, port, path, body):
conn = http.client.HTTPConnection(host, port, timeout=5)
conn.request("POST", path, json.dumps(body), {"Content-Type": "application/json"})
resp = conn.getresponse()
data = resp.read().decode()
conn.close()
return resp.status, data
def ws_connect(host, port, token):
s = socket.create_connection((host, port), timeout=5)
key = base64.b64encode(os.urandom(16)).decode()
req = (
f"GET /ws HTTP/1.1\r\n"
f"Host: {host}:{port}\r\n"
"Upgrade: websocket\r\n"
"Connection: Upgrade\r\n"
f"Sec-WebSocket-Key: {key}\r\n"
"Sec-WebSocket-Version: 13\r\n"
f"Authorization: Bearer {token}\r\n"
"\r\n"
)
s.sendall(req.encode())
# Read handshake response headers.
buf = b""
while b"\r\n\r\n" not in buf:
buf += s.recv(1024)
assert b"101" in buf.split(b"\r\n")[0], buf
return s
def ws_send(s, text):
payload = text.encode()
mask = os.urandom(4)
masked = bytes(payload[i] ^ mask[i % 4] for i in range(len(payload)))
header = bytearray([0x81])
length = len(payload)
if length < 126:
header.append(0x80 | length)
elif length < 65536:
header.append(0x80 | 126)
header += struct.pack(">H", length)
else:
header.append(0x80 | 127)
header += struct.pack(">Q", length)
s.sendall(bytes(header) + mask + masked)
def ws_recv(s):
def recv_exact(n):
data = b""
while len(data) < n:
chunk = s.recv(n - len(data))
if not chunk:
raise ConnectionError("closed")
data += chunk
return data
b = recv_exact(2)
opcode = b[0] & 0x0F
length = b[1] & 0x7F
if length == 126:
length = struct.unpack(">H", recv_exact(2))[0]
elif length == 127:
length = struct.unpack(">Q", recv_exact(8))[0]
data = recv_exact(length) if length else b""
return opcode, data
def collect_events(s, until_type, max_events=200):
events = []
for _ in range(max_events):
opcode, data = ws_recv(s)
if opcode == 0x9: # ping -> ignore (client would pong)
continue
if opcode == 0x8:
break
text = data.decode("utf-8", errors="replace")
for line in text.split("\n"):
line = line.strip()
if not line:
continue
obj = json.loads(line)
events.append(obj)
if obj.get("type") == until_type:
return events
return events
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--host", default="127.0.0.1")
ap.add_argument("--port", type=int, default=7643)
ap.add_argument("--code", default="123456")
args = ap.parse_args()
failures = []
def check(name, cond):
status = "PASS" if cond else "FAIL"
print(f" [{status}] {name}")
if not cond:
failures.append(name)
# 1. health
st, body = http_get(args.host, args.port, "/health")
h = json.loads(body)
check("health 200", st == 200)
check("health status ok", h.get("status") == "ok")
check("health gateway flag", h.get("gateway") is True)
# 2. pair bad code
st, body = http_post(args.host, args.port, "/pair", {"code": "000000", "device_id": "d", "device_name": "Test"})
check("pair bad code 401", st == 401)
# 3. pair good code
st, body = http_post(args.host, args.port, "/pair", {"code": args.code, "device_id": "d", "device_name": "Test"})
p = json.loads(body)
check("pair 200", st == 200)
token = p.get("token", "")
check("pair returns token", bool(token))
check("pair server_name", p.get("server_name") == "mock-jcode")
# 4. ws connect + subscribe + history
s = ws_connect(args.host, args.port, token)
ws_send(s, json.dumps({"id": 1, "type": "subscribe"}))
evs = collect_events(s, until_type="state")
types = [e["type"] for e in evs]
check("subscribe -> ack", "ack" in types)
check("subscribe -> session", "session" in types)
check("subscribe -> state", "state" in types)
ws_send(s, json.dumps({"id": 2, "type": "get_history"}))
evs = collect_events(s, until_type="history")
hist = next((e for e in evs if e["type"] == "history"), None)
check("get_history -> history", hist is not None)
if hist:
check("history available_models", len(hist.get("available_models", [])) >= 1)
check("history all_sessions", len(hist.get("all_sessions", [])) >= 1)
# 5. message -> full stream
ws_send(s, json.dumps({"id": 3, "type": "message", "content": "hi there"}))
evs = collect_events(s, until_type="done")
types = [e["type"] for e in evs]
check("message -> ack", "ack" in types)
check("message -> reasoning_delta", "reasoning_delta" in types)
check("message -> text_delta", "text_delta" in types)
check("message -> tool_start", "tool_start" in types)
check("message -> tool_exec", "tool_exec" in types)
check("message -> tool_done", "tool_done" in types)
check("message -> message_end", "message_end" in types)
check("message -> tokens", "tokens" in types)
check("message -> done", "done" in types)
# reconstruct streamed text
streamed = "".join(e.get("text", "") for e in evs if e["type"] == "text_delta")
check("streamed text contains echo of input", "hi there" in streamed)
check("streamed text has code fence", "```" in streamed)
# 6. set_model
ws_send(s, json.dumps({"id": 4, "type": "set_model", "model": "openai:gpt-5"}))
evs = collect_events(s, until_type="available_models_updated")
mc = next((e for e in evs if e["type"] == "model_changed"), None)
check("set_model -> model_changed", mc is not None and mc.get("model") == "openai:gpt-5")
# 7. history carries display_title + reasoning_effort (session titles UI)
if hist:
check("history display_title", bool(hist.get("display_title")))
check("history reasoning_effort", bool(hist.get("reasoning_effort")))
# 8. soft_interrupt -> injection ack precedes the streamed response
ws_send(s, json.dumps({"id": 5, "type": "soft_interrupt",
"content": "queued mid-run", "urgent": False}))
evs = collect_events(s, until_type="done")
types = [e["type"] for e in evs]
check("soft_interrupt -> soft_interrupt_injected", "soft_interrupt_injected" in types)
inj = next((e for e in evs if e["type"] == "soft_interrupt_injected"), None)
check("injected content echoes request",
inj is not None and inj.get("content") == "queued mid-run")
if "soft_interrupt_injected" in types and "text_delta" in types:
check("injected before stream",
types.index("soft_interrupt_injected") < types.index("text_delta"))
# 9. set_reasoning_effort
ws_send(s, json.dumps({"id": 6, "type": "set_reasoning_effort", "effort": "low"}))
evs = collect_events(s, until_type="reasoning_effort_changed")
rc = next((e for e in evs if e["type"] == "reasoning_effort_changed"), None)
check("set_reasoning_effort -> reasoning_effort_changed",
rc is not None and rc.get("effort") == "low" and rc.get("error") is None)
# 10. compact
ws_send(s, json.dumps({"id": 7, "type": "compact"}))
evs = collect_events(s, until_type="compact_result")
cr = next((e for e in evs if e["type"] == "compact_result"), None)
check("compact -> compact_result success",
cr is not None and cr.get("success") is True and bool(cr.get("message")))
# 11. rename_session -> session_renamed broadcast (titles map)
ws_send(s, json.dumps({"id": 8, "type": "rename_session", "title": "Renamed by smoke"}))
evs = collect_events(s, until_type="session_renamed")
rn = next((e for e in evs if e["type"] == "session_renamed"), None)
check("rename_session -> session_renamed",
rn is not None and rn.get("display_title") == "Renamed by smoke"
and bool(rn.get("session_id")))
s.close()
print()
if failures:
print(f"FAILED ({len(failures)}): {failures}")
sys.exit(1)
print("ALL CHECKS PASSED")
if __name__ == "__main__":
main()