system-prompts-and-models-o.../salesflow-saas/backend/scripts/full_stack_launch_test.py
Sami Assiri d8bb836614 feat(dealix): ship revenue discovery launch hardening
Add revenue discovery APIs/services, launch verification gates, CI quality checks, and frontend E2E/docs updates to prepare the branch for production go-live.

Made-with: Cursor
2026-04-15 17:51:23 +03:00

288 lines
11 KiB
Python

"""
اختبار إطلاق شامل: pytest اختياري، ثم صحة API، DB، Go-Live، تدفقات أساسية، Marketing/Strategy،
وحالة الإمبراطورية، صحة LangGraph، وتشغيل دورة صفقة CEO عبر LangGraph (واقعي، مهلة أطول).
يشغّل محلياً ضد BASE_URL (افتراضي http://127.0.0.1:8000).
استخدام:
py scripts/full_stack_launch_test.py
py scripts/full_stack_launch_test.py --pytest
py scripts/full_stack_launch_test.py --pytest --soft-ready
py scripts/full_stack_launch_test.py --skip-http
py scripts/full_stack_launch_test.py --http-only --soft-ready
py scripts/full_stack_launch_test.py --http-only --quick --soft-ready
$env:DEALIX_BASE_URL="http://127.0.0.1:8000"; py scripts/full_stack_launch_test.py
PowerShell — مسارات صحيحة:
أنت داخل ...\\salesflow-saas\\backend → لا تكتب cd salesflow-saas\\backend (يضاعف المسار).
للفرونت من الـ backend: cd ..\\frontend
ثم: npm run test:e2e:install
قبل --http-only: شغّل API في طرفية أخرى:
py -m uvicorn app.main:app --host 127.0.0.1 --port 8000
أوامر منفصلة (لا تلصق سطرين بدون مسافة بينهما):
py -m pytest tests -q --tb=line
py scripts/launch_gate_runner.py -- -m launch -q
"""
from __future__ import annotations
import argparse
import asyncio
import os
import subprocess
import sys
from pathlib import Path
from typing import Any, List, Tuple
import httpx
BASE = os.environ.get("DEALIX_BASE_URL", "http://127.0.0.1:8000").rstrip("/")
def _looks_like_no_server_running(detail: str) -> bool:
d = (detail or "").lower()
needles = (
"connection attempts failed",
"connection refused",
"connecterror",
"errno 111",
"name or service not known",
"getaddrinfo failed",
"actively refused",
"no connection could be made",
"failed to establish",
)
return any(n in d for n in needles)
def _safe_console(s: str, max_len: int = 320) -> str:
"""Avoid UnicodeEncodeError on Windows consoles (cp1252)."""
chunk = (s or "")[:max_len]
return chunk.encode("ascii", errors="replace").decode("ascii")
def run_pytest() -> int:
backend = Path(__file__).resolve().parent.parent
return subprocess.call(
[sys.executable, "-m", "pytest", str(backend / "tests"), "-q", "--tb=line"],
cwd=str(backend),
)
async def check(
name: str,
method: str,
path: str,
*,
allow_client_error: bool = False,
allowed_statuses: Tuple[int, ...] | None = None,
timeout: float = 15.0,
**kw: Any,
) -> Tuple[str, bool, str]:
"""Launch checks expect 2xx unless allow_client_error (4xx counts as OK) or allowed_statuses is set."""
url = f"{BASE}{path}"
try:
async with httpx.AsyncClient(timeout=timeout) as client:
r = await client.request(method, url, **kw)
if allowed_statuses is not None:
ok = r.status_code in allowed_statuses
elif allow_client_error:
ok = r.status_code < 500
else:
ok = 200 <= r.status_code < 300
body = _safe_console(r.text or "", 300)
return name, ok, f"{r.status_code} {body}"
except Exception as e:
return name, False, _safe_console(str(e), 300)
async def main() -> int:
parser = argparse.ArgumentParser(description="Dealix full-stack launch verification")
parser.add_argument(
"--pytest",
action="store_true",
help="Run backend/tests with pytest before HTTP checks",
)
parser.add_argument(
"--skip-http",
action="store_true",
help="Only run pytest (if --pytest); skip HTTP checks (CI without running API)",
)
parser.add_argument(
"--soft-ready",
action="store_true",
help="Do not fail the run if /api/v1/ready fails (e.g. Postgres not running locally)",
)
parser.add_argument(
"--http-only",
action="store_true",
help="Only run HTTP checks (no pytest); API must be reachable at DEALIX_BASE_URL",
)
parser.add_argument(
"--quick",
action="store_true",
help="Skip long LangGraph CEO deal cycle POST (~2 min); use for fast HTTP smoke against live server",
)
args = parser.parse_args()
if args.skip_http and not args.pytest:
print("Use --pytest with --skip-http to run tests only without starting the API.", flush=True)
return 2
if args.http_only and (args.pytest or args.skip_http):
print("Do not combine --http-only with --pytest or --skip-http.", flush=True)
return 2
if args.pytest:
print("Running pytest (backend/tests)...", flush=True)
rc = run_pytest()
if rc != 0:
print("pytest failed; fix tests before launch.", flush=True)
return rc
print("pytest OK.\n", flush=True)
if args.skip_http:
print("HTTP checks skipped (--skip-http).")
return 0
if args.http_only:
print("HTTP-only mode (--http-only).\n")
print(f"Full stack launch test -> {BASE}\n")
results: List[Tuple[str, bool, str]] = []
results.append(await check("health", "GET", "/api/v1/health"))
results.append(await check("ready (DB)", "GET", "/api/v1/ready"))
mh = await check("marketing hub", "GET", "/api/v1/marketing/hub")
if not mh[1]:
await asyncio.sleep(0.85)
mh = await check("marketing hub", "GET", "/api/v1/marketing/hub")
results.append(mh)
results.append(await check("strategy summary", "GET", "/api/v1/strategy/summary"))
results.append(await check("value proposition", "GET", "/api/v1/value-proposition/"))
results.append(await check("customer onboarding journey", "GET", "/api/v1/customer-onboarding/journey"))
results.append(await check("sales-os overview", "GET", "/api/v1/sales-os/overview"))
results.append(await check("operations snapshot", "GET", "/api/v1/operations/snapshot"))
results.append(
await check(
"go-live gate",
"GET",
"/api/v1/autonomous-foundation/integrations/go-live-gate",
allowed_statuses=(200, 403),
)
)
results.append(
await check("live readiness report", "GET", "/api/v1/autonomous-foundation/integrations/live-readiness")
)
results.append(
await check(
"executive ROI",
"POST",
"/api/v1/autonomous-foundation/dashboard/executive-roi",
json={"baseline": {"revenue": 100000}, "current": {"revenue": 120000, "win_rate": 0.3, "pipeline_velocity_days": 20, "manual_work_reduction_percent": 75}},
)
)
results.append(
await check(
"MCP ping substitute - autonomous ping",
"POST",
"/api/v1/autonomous-foundation/flows/self-improvement",
json={"tenant_id": "launch_test", "deal": {"signals": []}},
)
)
results.append(await check("affiliates program (public)", "GET", "/api/v1/affiliates/program"))
results.append(await check("affiliates leaderboard", "GET", "/api/v1/affiliates/leaderboard/top"))
results.append(await check("agents list", "GET", "/api/v1/agent-system/list"))
results.append(await check("agents empire status", "GET", "/api/v1/agent-system/empire/status"))
results.append(await check("openclaw safe core health", "GET", "/api/v1/autonomous-foundation/openclaw/health"))
results.append(await check("openclaw runs telemetry", "GET", "/api/v1/autonomous-foundation/openclaw/runs"))
results.append(await check("LangGraph orchestrator health", "GET", "/api/v1/agent-system/langgraph/health"))
results.append(
await check(
"integration connectivity matrix",
"POST",
"/api/v1/autonomous-foundation/integrations/connectivity-test",
json={},
)
)
if not args.quick:
results.append(
await check(
"LangGraph CEO deal cycle (realistic, slow)",
"POST",
"/api/v1/agent-system/ceo/langgraph-deal-cycle",
timeout=120.0,
json={
"company_name": "Launch Verification Co",
"deal_id": "LAUNCH-LG-1",
"tenant_id": "launch_verify",
"industry": "enterprise",
"city": "Riyadh",
},
)
)
else:
results.append(
(
"LangGraph CEO deal cycle (skipped --quick)",
True,
"200 SKIPPED use full_stack_launch_test without --quick for end-to-end graph against live LeadEngine",
)
)
failed = 0
soft_ready = args.soft_ready
for name, ok, detail in results:
status = "OK " if ok else "FAIL"
soft = soft_ready and name == "ready (DB)" and not ok
if soft:
status = "SOFT"
print(f"[{status}] {name}")
if not ok and not soft:
failed += 1
print(f" {detail[:200]}...\n" if len(detail) > 200 else f" {detail}\n")
# Note: /ready may fail if Postgres not running — use --soft-ready for local dev
print("---")
if failed == 0:
print("All launch checks passed (expected status codes, no 5xx).")
return 0
print(f"Some checks failed ({failed}). Fix server/DB or URLs.", flush=True)
failed_rows: List[Tuple[str, str]] = []
for name, ok, detail in results:
if ok:
continue
if soft_ready and name == "ready (DB)":
continue
failed_rows.append((name, detail))
if failed_rows and all(_looks_like_no_server_running(d) for _, d in failed_rows):
print(
"\n>>> تشخيص: يبدو أن لا خادم FastAPI يستمع على هذا العنوان.\n"
f" الهدف الحالي: {BASE}\n"
" رسالة httpx الشائعة: 'All connection attempts failed' = المنفذ فارغ أو جدار ناري.\n\n"
" افتح طرفية جديدة داخل مجلد backend وشغّل:\n"
" py -m uvicorn app.main:app --host 127.0.0.1 --port 8000\n\n"
" إن كان الخادم على منفذ آخر:\n"
' $env:DEALIX_BASE_URL="http://127.0.0.1:PORT"; py scripts/full_stack_launch_test.py --http-only\n\n'
" للتحقق بدون خادم حي استخدم pytest فقط (ASGI):\n"
" py -m pytest tests -q --tb=line\n",
flush=True,
)
for name, ok, detail in results:
if ok or name not in ("marketing hub", "strategy summary"):
continue
if "404" in detail or "Not Found" in detail:
print(
"Hint: 404 on marketing/strategy means the process on "
f"{BASE} is likely an OLD server build. Restart from repo root:\n"
" cd backend && py -m uvicorn app.main:app --host 127.0.0.1 --port 8000\n"
"Or set DEALIX_BASE_URL to a port running the current code.",
flush=True,
)
break
return 1
if __name__ == "__main__":
sys.exit(asyncio.run(main()))