#!/usr/bin/env python3 """Launch smoke checks for Customer Agent static surfaces and optional live API.""" from __future__ import annotations import argparse import json import sys import urllib.error import urllib.request from pathlib import Path def check_local_files(root: Path) -> list[str]: required = [ root / "embed" / "agent.js", root / "embed" / "agent.css", root / "admin" / "index.html", root / "test" / "index.html", root / "docs" / "customer-setup-guide.md", root / "docs" / "SHIPPING.md", root / "docs" / "LAUNCH.md", root / "VERSION", root / "integrations" / "wordpress" / "synderesis-customer-agent" / "synderesis-customer-agent.php", root / "integrations" / "wordpress" / "synderesis-customer-agent" / "uninstall.php", root / "evals" / "golden_cases.json", ] missing = [str(path.relative_to(root)) for path in required if not path.is_file()] return missing def http_get(url: str, timeout: float = 10.0) -> tuple[int, str]: req = urllib.request.Request(url, headers={"User-Agent": "SynderesisSmoke/1.5"}) try: with urllib.request.urlopen(req, timeout=timeout) as resp: body = resp.read(200_000).decode("utf-8", errors="replace") return int(resp.status), body except urllib.error.HTTPError as exc: body = exc.read(20_000).decode("utf-8", errors="replace") return int(exc.code), body def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( "--product-root", default=str(Path(__file__).resolve().parents[1]), help="Path to customer-agent/", ) parser.add_argument( "--base-url", default="", help="Optional live base URL, e.g. http://127.0.0.1:8080 or https://www.synderesis.eu", ) parser.add_argument( "--site-key", default="", help="Optional pk_live_ key for public config smoke", ) args = parser.parse_args(argv) root = Path(args.product_root).resolve() errors: list[str] = [] print(f"Product root: {root}") missing = check_local_files(root) if missing: for item in missing: errors.append(f"missing file: {item}") print(f"FAIL missing {item}") else: print("PASS local package files present") version = (root / "VERSION").read_text(encoding="utf-8").strip() print(f"VERSION {version}") # Offline golden evals eval_script = root / "scripts" / "run_evals.py" if eval_script.is_file(): import subprocess proc = subprocess.run( [sys.executable, str(eval_script)], cwd=str(root.parents[0]), capture_output=True, text=True, ) if proc.returncode == 0: print("PASS golden evals") else: errors.append("golden evals failed") print(proc.stdout) print(proc.stderr) print("FAIL golden evals") else: errors.append("run_evals.py missing") base = args.base_url.rstrip("/") static_server = None static_thread = None if not base: # Self-contained static smoke: serve customer-agent/ so HTTP paths can be checked # without a full Synderesis API process. import http.server import socketserver import threading class _Handler(http.server.SimpleHTTPRequestHandler): def __init__(self, *a, **k): super().__init__(*a, directory=str(root), **k) def log_message(self, format: str, *args: object) -> None: # noqa: A003 return # Bind ephemeral port on localhost static_server = socketserver.TCPServer(("127.0.0.1", 0), _Handler) port = static_server.server_address[1] static_thread = threading.Thread(target=static_server.serve_forever, daemon=True) static_thread.start() base = f"http://127.0.0.1:{port}" print(f"Started local static smoke server at {base}") static_paths = [ "/embed/agent.js", "/embed/agent.css", "/admin/index.html", "/test/index.html", "/docs/customer-setup-guide.md", "/docs/SHIPPING.md", "/docs/LAUNCH.md", "/VERSION", ] for path in static_paths: url = base + path try: status, body = http_get(url) except Exception as exc: # noqa: BLE001 errors.append(f"static {path}: {exc}") print(f"FAIL static {path} ({exc})") continue if status != 200: errors.append(f"static {path}: HTTP {status}") print(f"FAIL static {path} HTTP {status}") elif path.endswith("agent.js") and "site-key" not in body and "siteKey" not in body: errors.append(f"static {path}: unexpected body") print(f"FAIL static {path} unexpected body") else: print(f"PASS static {path} HTTP {status}") static_server.shutdown() print("Stopped local static smoke server") else: paths = [ "/health", "/embed/agent.js", "/embed/agent.css", "/customer-agent/admin/", "/customer-agent/test/", "/customer-agent/docs/customer-setup-guide.md", "/customer-agent/docs/SHIPPING.md", "/customer-agent/docs/LAUNCH.md", ] for path in paths: url = base + path try: status, body = http_get(url) except Exception as exc: # noqa: BLE001 errors.append(f"{path}: {exc}") print(f"FAIL {path} ({exc})") continue if status != 200: errors.append(f"{path}: HTTP {status}") print(f"FAIL {path} HTTP {status}") elif path.endswith("agent.js") and "SYNDERESIS" not in body.upper() and "site-key" not in body: errors.append(f"{path}: unexpected body") print(f"FAIL {path} unexpected body") else: print(f"PASS {path} HTTP {status}") if args.site_key: cfg_url = f"{base}/v1/public/agents/{args.site_key}/config" try: status, body = http_get(cfg_url) data = json.loads(body) if body else {} except Exception as exc: # noqa: BLE001 errors.append(f"public config: {exc}") print(f"FAIL public config ({exc})") else: if status == 200 and data.get("name"): print(f"PASS public config agent={data.get('name')!r}") else: errors.append(f"public config HTTP {status}") print(f"FAIL public config HTTP {status}") if errors: print(f"\nSMOKE FAILED ({len(errors)} issue(s))") for item in errors: print(f" - {item}") return 1 print("\nSMOKE PASSED") return 0 if __name__ == "__main__": raise SystemExit(main())