Major feature additions across backend and frontend: Backend: - Unix domain socket admin server with JSON-RPC protocol - CLI subcommands: user/agent management, audit, backup, messages, channels - Managed API keys (sb_ prefix) with permissions, channel limits, expiry - Thread/reply support in messaging core (reply_to column) - Context-based trace owner_id propagation for proper audit filtering - Two-step auth middleware supporting both agent keys and managed API keys Frontend: - Complete Slack-like dark theme redesign with custom CSS properties - Sidebar with Channels, Direct Messages, and Admin sections - Thread panel (slide-in) for viewing message replies - API key management page with create form, key display, and ready-to-use MCP/Claude Code config snippets with copy-to-clipboard - All pages restyled: login, dashboard, agents, conversations, settings E2E Tests: - Fixed test runner binary path resolution - Added pyproject.toml for test dependencies Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
88 lines
2.4 KiB
Python
88 lines
2.4 KiB
Python
"""SynapBus server lifecycle management (start/stop/health)."""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import shutil
|
|
import signal
|
|
import socket
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
|
|
import httpx
|
|
|
|
|
|
def find_free_port() -> int:
|
|
"""Find an available TCP port."""
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
s.bind(("", 0))
|
|
return s.getsockname()[1]
|
|
|
|
|
|
def find_binary() -> str:
|
|
"""Locate the synapbus binary."""
|
|
# Try project root
|
|
repo_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
|
candidate = os.path.join(repo_root, "synapbus")
|
|
if os.path.isfile(candidate) and os.access(candidate, os.X_OK):
|
|
return candidate
|
|
# Try PATH
|
|
from shutil import which
|
|
found = which("synapbus")
|
|
if found:
|
|
return found
|
|
print("ERROR: synapbus binary not found. Run 'make build' first.")
|
|
sys.exit(1)
|
|
|
|
|
|
def start_server(port: int) -> tuple:
|
|
"""Start SynapBus server, return (process, data_dir, port)."""
|
|
binary = find_binary()
|
|
data_dir = tempfile.mkdtemp(prefix="synapbus-e2e-")
|
|
|
|
proc = subprocess.Popen(
|
|
[binary, "serve", "--port", str(port), "--data", data_dir],
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT,
|
|
text=True,
|
|
)
|
|
|
|
# Wait for server to be healthy
|
|
for _ in range(30):
|
|
try:
|
|
resp = httpx.get("http://localhost:{}/health".format(port), timeout=2)
|
|
if resp.status_code == 200:
|
|
return proc, data_dir, port
|
|
except httpx.ConnectError:
|
|
pass
|
|
time.sleep(0.5)
|
|
|
|
proc.terminate()
|
|
shutil.rmtree(data_dir, ignore_errors=True)
|
|
print("ERROR: Server failed to start within 15 seconds")
|
|
sys.exit(1)
|
|
|
|
|
|
def stop_server(proc: subprocess.Popen, data_dir: str) -> None:
|
|
"""Stop server and clean up."""
|
|
try:
|
|
proc.send_signal(signal.SIGTERM)
|
|
proc.wait(timeout=10)
|
|
except Exception:
|
|
proc.kill()
|
|
shutil.rmtree(data_dir, ignore_errors=True)
|
|
|
|
|
|
def wait_for_server(base_url: str, timeout: int = 15) -> bool:
|
|
"""Wait for an already-running server to become healthy."""
|
|
for _ in range(timeout * 2):
|
|
try:
|
|
resp = httpx.get("{}/health".format(base_url), timeout=2)
|
|
if resp.status_code == 200:
|
|
return True
|
|
except httpx.ConnectError:
|
|
pass
|
|
time.sleep(0.5)
|
|
return False
|