Files
simplex-orchestrate/smoke_test.py
Jon 38ff96c576 Scaffold SimpleX Orchestrate: supervisor over official binaries
A standalone control-plane app that spawns and drives the official SimpleX
binaries (never modifies simplex source). Validated against simplex-chat
built from source (stable v6.5.4, GHC 9.6.3).

- CLAUDE.md: architecture notes mined from the upstream docs (WebSocket bot API,
  per-profile DBs, directory/broadcast bot config)
- supervisor/: process registry + port allocation (supervisor.py), corrId/cmd<->resp
  WebSocket client (ws_client.py), binary locator (binaries.py), FastAPI front with
  REST control + /events stream (server.py)
- smoke_test.py: Pattern-1 handshake (spawn simplex-chat -p, create+read user) — PASS
- group_test.py: two accounts, invitation connect + group invite/join, verified
  membership over the real SMP network — PASS
- build_chat.sh / install_ghc.sh: reproducible toolchain + from-source build

Key finding: fresh DB prompts for a display name on stdin; spawn with
--create-bot-display-name to start the WebSocket server non-interactively.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 12:31:37 +01:00

68 lines
2.3 KiB
Python

"""Pattern-1 smoke test: spawn a real `simplex-chat -p` process, drive it over the
WebSocket corrId/cmd↔resp handshake, and confirm a structured response comes back.
Validates the supervisor + ws_client against a real binary. Local commands only
(/user, /_create user, /users) — no network needed.
Run (with ./bin/simplex-chat present): python smoke_test.py
"""
import asyncio
import json
import shutil
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from supervisor.supervisor import Supervisor # noqa: E402
async def main() -> int:
events: list[tuple[str, str]] = []
async def on_event(name: str, resp: dict) -> None:
events.append((name, resp.get("type", "?")))
sup = Supervisor(data_dir="data", base_port=5400, on_event=on_event)
ok = False
try:
print("→ spawning simplex-chat -p (profile 'smoke') …")
m = await sup.start_cli("smoke")
print(f" process pid={m.proc.pid} port={m.port}, WebSocket connected")
print("→ /user (show active user)")
r = await sup.send("smoke", "/user")
print(" resp.type =", r.get("type"))
if r.get("type") != "activeUser":
new_user = {
"profile": {"displayName": "smoke", "fullName": ""},
"pastTimestamp": False,
"userChatRelay": False,
}
print("→ /_create user")
rc = await sup.send("smoke", "/_create user " + json.dumps(new_user))
print(" resp.type =", rc.get("type"))
r = await sup.send("smoke", "/user")
print(" /user resp.type =", r.get("type"))
user = r.get("user") or {}
print(" active user displayName =", user.get("localDisplayName"))
rl = await sup.send("smoke", "/users")
print("→ /users resp.type =", rl.get("type"),
"count =", len(rl.get("users", []) if isinstance(rl, dict) else []))
ok = r.get("type") == "activeUser"
finally:
await sup.stop("smoke")
print("→ stopped process")
print(" events observed:", events[:5])
print("\nRESULT:", "PASS — corrId/cmd↔resp handshake works" if ok else "FAIL")
return 0 if ok else 1
if __name__ == "__main__":
raise SystemExit(asyncio.run(main()))