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

543 lines
19 KiB
Python
Executable File

#!/usr/bin/env python3
"""Deterministic mock jcode gateway for end-to-end iOS app testing.
Speaks the exact wire protocol from `crates/jcode-base/src/gateway.rs` on a
single TCP port, peeking the request line to route like the real gateway:
- GET /health -> {status, version, gateway}
- POST /pair -> {token, server_name, server_version}
- GET /ws -> WebSocket upgrade; newline-delimited JSON event protocol
It does NOT call an LLM. A `message` request triggers a scripted, deterministic
stream (reasoning, text deltas, a tool-call lifecycle, tokens, done) so the app
can be exercised and visually validated without network or provider cost. This
is the iOS equivalent of the removed Rust simulator: one source of honest,
repeatable behavior to develop the client against.
Self-contained: no third-party deps. Minimal hand-rolled WebSocket framing.
Run: python3 mock_gateway.py [--port 7643] [--code 123456]
"""
import argparse
import asyncio
import base64
import hashlib
import json
import struct
import sys
WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
SERVER_VERSION = "mock-0.32.0"
SERVER_NAME = "mock-jcode"
DEFAULT_MODELS = [
"claude-api:claude-fable-5",
"claude-api:claude-sonnet-4",
"openai:gpt-5",
"gemini:gemini-2.5-pro",
]
class GatewayState:
def __init__(self, code, token):
self.code = code
self.token = token
self.session_id = "mock-session-0001"
self.title = "Mock session"
self.model = DEFAULT_MODELS[0]
self.messages = []
self.token_input = 0
self.token_output = 0
self.reasoning_effort = "high"
self.push_demo = False
def scenario_messages(name):
"""Pre-seeded transcripts for the layout matrix. Each is a deterministic
content state so UI efficiency can be measured across the real range."""
bash_tool = {
"id": "t1", "name": "bash",
"input": '{"command": "echo hello"}',
"output": "hello\n", "error": None,
}
if name == "empty":
return []
if name == "short":
return [
{"role": "user", "content": "hi"},
{"role": "assistant", "content": "Hello! How can I help?"},
]
if name == "tool":
return [
{"role": "user", "content": "run echo hello"},
{"role": "assistant", "content": "Done. Output above.", "tool_data": bash_tool},
]
if name == "long":
turns = []
for i in range(6):
turns.append({"role": "user", "content": f"Question number {i + 1} about the codebase?"})
turns.append({
"role": "assistant",
"content": (
f"Answer {i + 1}: here is a reasonably detailed paragraph that "
"wraps across multiple lines to simulate a real assistant reply "
"with enough text to fill vertical space and exercise scrolling."
),
"tool_data": bash_tool if i % 2 == 0 else None,
})
return turns
if name == "code":
return [
{"role": "user", "content": "show me a python snippet"},
{"role": "assistant", "content": (
"Sure:\n\n```python\ndef fib(n):\n a, b = 0, 1\n for _ in range(n):\n"
" a, b = b, a + b\n return a\n```\n\nThat is iterative and O(n)."
)},
]
return []
# ---------------------------------------------------------------------------
# WebSocket framing (server side)
# ---------------------------------------------------------------------------
def ws_accept_key(key: str) -> str:
digest = hashlib.sha1((key + WS_GUID).encode()).digest()
return base64.b64encode(digest).decode()
def encode_text_frame(text: str) -> bytes:
payload = text.encode("utf-8")
header = bytearray([0x81]) # FIN + text opcode
length = len(payload)
if length < 126:
header.append(length)
elif length < 65536:
header.append(126)
header += struct.pack(">H", length)
else:
header.append(127)
header += struct.pack(">Q", length)
return bytes(header) + payload
def encode_control_frame(opcode: int, payload: bytes = b"") -> bytes:
header = bytearray([0x80 | opcode, len(payload)])
return bytes(header) + payload
async def read_frame(reader: asyncio.StreamReader):
"""Returns (opcode, payload_bytes) or None on EOF/close."""
try:
b = await reader.readexactly(2)
except asyncio.IncompleteReadError:
return None
opcode = b[0] & 0x0F
masked = (b[1] & 0x80) != 0
length = b[1] & 0x7F
if length == 126:
ext = await reader.readexactly(2)
length = struct.unpack(">H", ext)[0]
elif length == 127:
ext = await reader.readexactly(8)
length = struct.unpack(">Q", ext)[0]
mask = await reader.readexactly(4) if masked else b"\x00\x00\x00\x00"
data = await reader.readexactly(length) if length else b""
if masked:
data = bytes(data[i] ^ mask[i % 4] for i in range(len(data)))
return opcode, data
class WSConn:
"""Minimal server-side WebSocket connection wrapper."""
def __init__(self, writer: asyncio.StreamWriter):
self.writer = writer
self._lock = asyncio.Lock()
async def send(self, text: str):
async with self._lock:
self.writer.write(encode_text_frame(text))
await self.writer.drain()
async def pong(self, payload: bytes):
async with self._lock:
self.writer.write(encode_control_frame(0xA, payload))
await self.writer.drain()
async def close(self):
try:
async with self._lock:
self.writer.write(encode_control_frame(0x8))
await self.writer.drain()
except Exception:
pass
# ---------------------------------------------------------------------------
# Protocol behavior
# ---------------------------------------------------------------------------
def jline(obj):
return json.dumps(obj)
def chunk_text(text, size):
for i in range(0, len(text), size):
yield text[i : i + size]
async def send_event(ws: WSConn, obj):
await ws.send(jline(obj))
async def stream_response(ws, state, user_text, req_id):
await send_event(ws, {"type": "ack", "id": req_id})
for chunk in ["Looking at ", "the request", "..."]:
await send_event(ws, {"type": "reasoning_delta", "text": chunk})
await asyncio.sleep(0.05)
await send_event(ws, {"type": "reasoning_done", "duration_secs": 0.4})
intro = f"You said: {user_text}\n\nRunning a quick tool to demonstrate.\n\n"
for ch in chunk_text(intro, 6):
await send_event(ws, {"type": "text_delta", "text": ch})
await asyncio.sleep(0.02)
tool_id = f"tool-{req_id}"
await send_event(ws, {"type": "tool_start", "id": tool_id, "name": "bash"})
for piece in ['{"command":', ' "echo ', 'hello"}']:
await send_event(ws, {"type": "tool_input", "delta": piece})
await asyncio.sleep(0.03)
await send_event(ws, {"type": "tool_exec", "id": tool_id, "name": "bash"})
await asyncio.sleep(0.2)
await send_event(ws, {
"type": "tool_done", "id": tool_id, "name": "bash",
"output": "hello\n", "error": None,
})
answer = (
"Done. Here is a code block:\n\n"
"```python\nprint('hello from mock gateway')\n```\n\n"
"And a **bold** word plus `inline code`."
)
for ch in chunk_text(answer, 8):
await send_event(ws, {"type": "text_delta", "text": ch})
await asyncio.sleep(0.02)
await send_event(ws, {"type": "message_end"})
state.token_input += 120 + len(user_text)
state.token_output += 240
await send_event(ws, {"type": "tokens", "input": state.token_input, "output": state.token_output})
state.messages.append({"role": "user", "content": user_text})
state.messages.append({
"role": "assistant", "content": answer,
"tool_data": {
"id": tool_id, "name": "bash",
"input": '{"command": "echo hello"}',
"output": "hello\n", "error": None,
},
})
await send_event(ws, {"type": "done", "id": req_id})
def history_payload(state, req_id):
return {
"type": "history",
"id": req_id,
"session_id": state.session_id,
"messages": state.messages,
"provider_name": "anthropic-api",
"provider_model": state.model,
"available_models": DEFAULT_MODELS,
"total_tokens": [state.token_input, state.token_output],
"all_sessions": [state.session_id, "mock-session-0002"],
"server_version": SERVER_VERSION,
"display_title": state.title,
"reasoning_effort": state.reasoning_effort,
}
async def handle_request(ws, state, raw):
try:
msg = json.loads(raw)
except json.JSONDecodeError:
return
req_type = msg.get("type")
req_id = int(msg.get("id", 0))
print(f"[ws] <- {req_type} id={req_id}", file=sys.stderr)
if req_type == "subscribe":
await send_event(ws, {"type": "ack", "id": req_id})
await send_event(ws, {"type": "session", "session_id": state.session_id})
await send_event(ws, {
"type": "state", "id": req_id, "session_id": state.session_id,
"message_count": len(state.messages), "is_processing": False,
})
elif req_type == "get_history":
await send_event(ws, history_payload(state, req_id))
elif req_type == "message":
await stream_response(ws, state, msg.get("content", ""), req_id)
elif req_type == "soft_interrupt":
await send_event(ws, {"type": "ack", "id": req_id})
# Mirror the real server: confirm the queued message was injected
# before streaming the (echoed) response it participates in.
await send_event(ws, {
"type": "soft_interrupt_injected",
"content": msg.get("content", ""),
"display_role": "user",
"point": "immediate",
"tools_skipped": 0,
})
await stream_response(ws, state, msg.get("content", ""), req_id)
elif req_type == "cancel":
await send_event(ws, {"type": "interrupted"})
await send_event(ws, {"type": "done", "id": req_id})
elif req_type == "ping":
await send_event(ws, {"type": "pong", "id": req_id})
elif req_type == "set_model":
state.model = msg.get("model", state.model)
await send_event(ws, {"type": "model_changed", "id": req_id, "model": state.model, "error": None})
await send_event(ws, {"type": "available_models_updated", "available_models": DEFAULT_MODELS, "provider_model": state.model})
elif req_type == "set_reasoning_effort":
state.reasoning_effort = msg.get("effort", state.reasoning_effort)
await send_event(ws, {"type": "reasoning_effort_changed", "id": req_id, "effort": state.reasoning_effort, "error": None})
elif req_type == "compact":
await send_event(ws, {"type": "compact_result", "id": req_id, "message": "Compacted context (2048 tokens saved)", "success": True})
elif req_type == "rename_session":
state.title = msg.get("title") or "Untitled"
await send_event(ws, {"type": "session_renamed", "session_id": state.session_id, "display_title": state.title})
elif req_type == "resume_session":
sid = msg.get("session_id", state.session_id)
state.session_id = sid
state.messages = []
await send_event(ws, {"type": "session", "session_id": sid})
await send_event(ws, history_payload(state, req_id))
elif req_type == "clear":
state.messages = []
await send_event(ws, history_payload(state, req_id))
elif req_type == "cancel_soft_interrupts":
await send_event(ws, {"type": "ack", "id": req_id})
elif req_type == "_notify":
# Test-only: synthesize a push notification + a compaction notice.
await send_event(ws, {"type": "notification", "from_name": "swarm", "message": "build finished"})
await send_event(ws, {"type": "compaction", "trigger": "manual", "tokens_saved": 4096})
else:
print(f"[ws] (ignored unknown request {req_type})", file=sys.stderr)
# ---------------------------------------------------------------------------
# HTTP + routing
# ---------------------------------------------------------------------------
def http_response(status_line, body):
body_bytes = body.encode()
head = (
f"HTTP/1.1 {status_line}\r\n"
"Content-Type: application/json\r\n"
f"Content-Length: {len(body_bytes)}\r\n"
"Connection: close\r\n"
"Access-Control-Allow-Origin: *\r\n"
"\r\n"
)
return head.encode() + body_bytes
async def read_http_request(reader):
"""Reads headers (and body per Content-Length). Returns (method, path, headers, body)."""
header_data = b""
while b"\r\n\r\n" not in header_data:
chunk = await reader.read(4096)
if not chunk:
break
header_data += chunk
if len(header_data) > 65536:
break
if b"\r\n\r\n" not in header_data:
return None
head, _, rest = header_data.partition(b"\r\n\r\n")
lines = head.decode("latin1").split("\r\n")
request_line = lines[0]
parts = request_line.split()
method, path = (parts[0], parts[1]) if len(parts) >= 2 else ("", "")
headers = {}
for line in lines[1:]:
if ":" in line:
k, v = line.split(":", 1)
headers[k.strip().lower()] = v.strip()
body = rest
content_length = int(headers.get("content-length", "0") or "0")
while len(body) < content_length:
chunk = await reader.read(content_length - len(body))
if not chunk:
break
body += chunk
return method, path, headers, body
async def handle_connection(reader, writer, state):
parsed = await read_http_request(reader)
if parsed is None:
writer.close()
return
method, path, headers, body = parsed
path_base = path.split("?")[0]
print(f"[http] {method} {path_base}", file=sys.stderr)
if headers.get("upgrade", "").lower() == "websocket" and path_base == "/ws":
await serve_websocket(reader, writer, headers, state)
return
if method == "GET" and path_base == "/health":
body_str = jline({"status": "ok", "version": SERVER_VERSION, "gateway": True})
writer.write(http_response("200 OK", body_str))
elif method == "OPTIONS":
writer.write(
b"HTTP/1.1 204 No Content\r\nAccess-Control-Allow-Origin: *\r\n"
b"Access-Control-Allow-Methods: GET, POST, OPTIONS\r\n"
b"Access-Control-Allow-Headers: Content-Type, Authorization\r\n"
b"Content-Length: 0\r\nConnection: close\r\n\r\n"
)
elif method == "POST" and path_base == "/pair":
try:
payload = json.loads(body.decode() or "{}")
except Exception:
payload = {}
if payload.get("code", "") == state.code:
resp = jline({"token": state.token, "server_name": SERVER_NAME, "server_version": SERVER_VERSION})
writer.write(http_response("200 OK", resp))
else:
resp = jline({"error": "Invalid or expired pairing code"})
writer.write(http_response("401 Unauthorized", resp))
else:
writer.write(http_response("404 Not Found", jline({"error": "Not found"})))
try:
await writer.drain()
except Exception:
pass
writer.close()
async def serve_websocket(reader, writer, headers, state):
key = headers.get("sec-websocket-key")
auth = headers.get("authorization", "")
if not key:
writer.close()
return
accept = ws_accept_key(key)
handshake = (
"HTTP/1.1 101 Switching Protocols\r\n"
"Upgrade: websocket\r\n"
"Connection: Upgrade\r\n"
f"Sec-WebSocket-Accept: {accept}\r\n"
"\r\n"
)
writer.write(handshake.encode())
await writer.drain()
ws = WSConn(writer)
ok = auth == f"Bearer {state.token}"
print(f"[ws] client connected (auth_ok={ok})", file=sys.stderr)
keepalive = asyncio.create_task(keepalive_loop(ws))
push_demo = None
if getattr(state, "push_demo", False):
push_demo = asyncio.create_task(push_demo_loop(ws))
try:
buffered = ""
while True:
frame = await read_frame(reader)
if frame is None:
break
opcode, data = frame
if opcode == 0x8: # close
break
if opcode == 0x9: # ping
await ws.pong(data)
continue
if opcode in (0x1, 0x2):
buffered += data.decode("utf-8", errors="replace")
while "\n" in buffered:
line, buffered = buffered.split("\n", 1)
line = line.strip()
if line:
await handle_request(ws, state, line)
if buffered.strip():
await handle_request(ws, state, buffered.strip())
buffered = ""
except (asyncio.IncompleteReadError, ConnectionResetError):
pass
finally:
keepalive.cancel()
if push_demo:
push_demo.cancel()
await ws.close()
writer.close()
print("[ws] client disconnected", file=sys.stderr)
async def push_demo_loop(ws):
"""Spontaneously push out-of-band notices to validate the toast UI."""
try:
await asyncio.sleep(2.5)
await send_event(ws, {"type": "notification", "from_name": "swarm", "message": "build finished"})
await asyncio.sleep(1.5)
await send_event(ws, {"type": "compaction", "trigger": "manual", "tokens_saved": 4096})
except asyncio.CancelledError:
pass
except Exception:
pass
async def keepalive_loop(ws):
try:
while True:
await asyncio.sleep(20)
async with ws._lock:
ws.writer.write(encode_control_frame(0x9)) # ping
await ws.writer.drain()
except asyncio.CancelledError:
pass
except Exception:
pass
async def main():
parser = argparse.ArgumentParser()
parser.add_argument("--port", type=int, default=7643)
parser.add_argument("--host", default="127.0.0.1")
parser.add_argument("--code", default="123456")
parser.add_argument("--token", default="mocktoken0123456789abcdef")
parser.add_argument("--push-demo", action="store_true",
help="spontaneously push notification + compaction notices after connect")
parser.add_argument("--scenario", default="",
help="pre-seed transcript: empty|short|tool|long|code")
args = parser.parse_args()
state = GatewayState(args.code, args.token)
state.push_demo = args.push_demo
if args.scenario:
state.messages = scenario_messages(args.scenario)
server = await asyncio.start_server(
lambda r, w: handle_connection(r, w, state),
args.host,
args.port,
)
print(
f"mock gateway: http+ws on {args.host}:{args.port} "
f"(code={args.code}, token={args.token})",
file=sys.stderr,
)
async with server:
await server.serve_forever()
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
pass