Spaces:
Running
Running
test(mcp): repeatability burn-in — fire the latest brief N times, report pass rate
Browse files- tests/mcp_repeat_test.py +166 -0
tests/mcp_repeat_test.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Repeatability burn-in for the MCP server: fire the SAME brief N times.
|
| 3 |
+
|
| 4 |
+
Pulls the newest brief for a company from Supabase demo_history (status
|
| 5 |
+
mcp_intake — i.e. "Jack's last payload" for Tixr) and runs it through
|
| 6 |
+
build_demo_from_brief N times sequentially, polling each to completion.
|
| 7 |
+
Reports a per-run table + pass rate, exits non-zero if any run failed.
|
| 8 |
+
|
| 9 |
+
Every run is a fresh LLM blueprint draw, so N runs measure the real-world
|
| 10 |
+
flake rate (blueprint quirks, provider 529s, TS import flakes) — not just
|
| 11 |
+
whether one lucky draw works.
|
| 12 |
+
|
| 13 |
+
Usage (from either clone; MCP_ACCESS_TOKEN must be in the environment or in
|
| 14 |
+
the demoprep_mcp clone's .env, which is tried explicitly as documented):
|
| 15 |
+
|
| 16 |
+
./demoprep/bin/python tests/mcp_repeat_test.py # 5x Tixr
|
| 17 |
+
./demoprep/bin/python tests/mcp_repeat_test.py --count 3
|
| 18 |
+
./demoprep/bin/python tests/mcp_repeat_test.py --company Tixr --owner jack.rayner@thoughtspot.com
|
| 19 |
+
"""
|
| 20 |
+
import argparse
|
| 21 |
+
import json
|
| 22 |
+
import os
|
| 23 |
+
import sys
|
| 24 |
+
import time
|
| 25 |
+
|
| 26 |
+
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
| 27 |
+
sys.path.insert(0, ROOT)
|
| 28 |
+
|
| 29 |
+
from dotenv import load_dotenv
|
| 30 |
+
|
| 31 |
+
load_dotenv(os.path.join(ROOT, ".env"))
|
| 32 |
+
# The MCP access token historically lives in the demoprep_mcp clone's .env.
|
| 33 |
+
# Load it as a documented, explicit second source (does not override existing env).
|
| 34 |
+
_MCP_CLONE_ENV = "/Users/mike.boone/_source/demoprep_mcp/.env"
|
| 35 |
+
if not os.getenv("MCP_ACCESS_TOKEN") and os.path.exists(_MCP_CLONE_ENV):
|
| 36 |
+
load_dotenv(_MCP_CLONE_ENV)
|
| 37 |
+
|
| 38 |
+
import tests.e2e_mcp as H # noqa: E402
|
| 39 |
+
from supabase_client import SupabaseSettings # noqa: E402
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def log(msg: str) -> None:
|
| 43 |
+
print(f"[{time.strftime('%H:%M:%S')}] {msg}", flush=True)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def fetch_latest_brief(company: str) -> tuple[str, str, str]:
|
| 47 |
+
rows = (
|
| 48 |
+
SupabaseSettings().client.table("demo_history")
|
| 49 |
+
.select("company_name,use_case,results,created_at")
|
| 50 |
+
.eq("status", "mcp_intake")
|
| 51 |
+
.ilike("company_name", f"%{company}%")
|
| 52 |
+
.order("created_at", desc=True)
|
| 53 |
+
.limit(1)
|
| 54 |
+
.execute()
|
| 55 |
+
.data
|
| 56 |
+
or []
|
| 57 |
+
)
|
| 58 |
+
if not rows:
|
| 59 |
+
raise SystemExit(f"No mcp_intake brief for {company!r} in demo_history")
|
| 60 |
+
row = rows[0]
|
| 61 |
+
brief = (row.get("results") or {}).get("brief") or ""
|
| 62 |
+
if not brief:
|
| 63 |
+
raise SystemExit(f"Brief for {company!r} ({row['created_at']}) is empty")
|
| 64 |
+
return brief, row.get("use_case") or "", row["created_at"][:19]
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def run_once(ep: str, tok: str, brief: str, company: str, use_case: str,
|
| 68 |
+
owner: str, run_no: int, timeout_min: int) -> dict:
|
| 69 |
+
run_id = None
|
| 70 |
+
for _ in range(60):
|
| 71 |
+
try:
|
| 72 |
+
resp = H.call(ep, tok, "build_demo_from_brief",
|
| 73 |
+
{"brief": brief, "company_name": company,
|
| 74 |
+
"use_case": use_case, "owner_email": owner})
|
| 75 |
+
except Exception as e:
|
| 76 |
+
log(f"run {run_no}: start call failed ({type(e).__name__}) — retry in 30s")
|
| 77 |
+
time.sleep(30)
|
| 78 |
+
continue
|
| 79 |
+
status = resp.get("status")
|
| 80 |
+
if status == "started":
|
| 81 |
+
run_id = resp["run_id"]
|
| 82 |
+
log(f"run {run_no}: started run_id={run_id}")
|
| 83 |
+
break
|
| 84 |
+
if status == "busy":
|
| 85 |
+
log(f"run {run_no}: server busy — retry in 30s")
|
| 86 |
+
time.sleep(30)
|
| 87 |
+
continue
|
| 88 |
+
return {"run_no": run_no, "run_id": None, "grade": "FAIL",
|
| 89 |
+
"status": f"start failed: {str(resp)[:200]}", "elapsed_s": 0, "errors": []}
|
| 90 |
+
if not run_id:
|
| 91 |
+
return {"run_no": run_no, "run_id": None, "grade": "FAIL",
|
| 92 |
+
"status": "never got a slot", "elapsed_s": 0, "errors": []}
|
| 93 |
+
|
| 94 |
+
deadline = time.time() + timeout_min * 60
|
| 95 |
+
misses = 0
|
| 96 |
+
last_phase = ""
|
| 97 |
+
while time.time() < deadline:
|
| 98 |
+
try:
|
| 99 |
+
st = H.call(ep, tok, "status")
|
| 100 |
+
misses = 0
|
| 101 |
+
except Exception as e:
|
| 102 |
+
misses += 1
|
| 103 |
+
if misses >= 20:
|
| 104 |
+
return {"run_no": run_no, "run_id": run_id, "grade": "FAIL",
|
| 105 |
+
"status": "status endpoint unreachable", "elapsed_s": None, "errors": []}
|
| 106 |
+
time.sleep(30)
|
| 107 |
+
continue
|
| 108 |
+
recent = {x["run_id"]: x for x in st.get("recent_builds", [])}
|
| 109 |
+
if run_id in recent:
|
| 110 |
+
res = recent[run_id]
|
| 111 |
+
graded = H.grade(res)
|
| 112 |
+
return {"run_no": run_no, "run_id": run_id, "grade": graded["grade"],
|
| 113 |
+
"status": res.get("status"), "elapsed_s": res.get("elapsed_seconds"),
|
| 114 |
+
"errors": res.get("errors") or [],
|
| 115 |
+
"liveboard": res.get("liveboard_url"), "schema": res.get("schema")}
|
| 116 |
+
active = {x["run_id"]: x for x in st.get("active_builds", [])}.get(run_id, {})
|
| 117 |
+
phase = active.get("phase", "(waiting)")
|
| 118 |
+
if phase != last_phase:
|
| 119 |
+
log(f"run {run_no}: {phase} {active.get('elapsed_seconds', '')}s")
|
| 120 |
+
last_phase = phase
|
| 121 |
+
time.sleep(25)
|
| 122 |
+
return {"run_no": run_no, "run_id": run_id, "grade": "FAIL",
|
| 123 |
+
"status": f"timed out after {timeout_min} min", "elapsed_s": None, "errors": []}
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def main() -> None:
|
| 127 |
+
ap = argparse.ArgumentParser()
|
| 128 |
+
ap.add_argument("--count", type=int, default=5)
|
| 129 |
+
ap.add_argument("--company", default="Tixr")
|
| 130 |
+
ap.add_argument("--owner", default="jack.rayner@thoughtspot.com")
|
| 131 |
+
ap.add_argument("--timeout-min", type=int, default=30, help="per-run timeout")
|
| 132 |
+
args = ap.parse_args()
|
| 133 |
+
|
| 134 |
+
ep = os.getenv("MCP_ENDPOINT", H.DEFAULT_ENDPOINT)
|
| 135 |
+
tok = os.getenv("MCP_ACCESS_TOKEN")
|
| 136 |
+
if not tok:
|
| 137 |
+
raise SystemExit("MCP_ACCESS_TOKEN not set (env or demoprep_mcp/.env)")
|
| 138 |
+
|
| 139 |
+
brief, use_case, brief_ts = fetch_latest_brief(args.company)
|
| 140 |
+
log(f"{args.company} brief from {brief_ts} | use_case={use_case!r} | len={len(brief)}")
|
| 141 |
+
log(f"Running {args.count}x sequentially against {ep}")
|
| 142 |
+
|
| 143 |
+
results = []
|
| 144 |
+
for i in range(1, args.count + 1):
|
| 145 |
+
log(f"===== RUN {i}/{args.count} =====")
|
| 146 |
+
results.append(run_once(ep, tok, brief, args.company, use_case,
|
| 147 |
+
args.owner, i, args.timeout_min))
|
| 148 |
+
|
| 149 |
+
passes = sum(1 for r in results if r["grade"] == "PASS")
|
| 150 |
+
print("\n" + "=" * 78)
|
| 151 |
+
print(f"{'run':>4} {'run_id':14} {'grade':6} {'status':22} {'elapsed':>9} errors")
|
| 152 |
+
print("-" * 78)
|
| 153 |
+
for r in results:
|
| 154 |
+
el = f"{r['elapsed_s']:.0f}s" if isinstance(r.get("elapsed_s"), (int, float)) else "-"
|
| 155 |
+
err = (r["errors"][0][:60] + "…") if r.get("errors") else ""
|
| 156 |
+
print(f"{r['run_no']:>4} {str(r.get('run_id'))[:12]:14} {r['grade']:6} "
|
| 157 |
+
f"{str(r.get('status'))[:22]:22} {el:>9} {err}")
|
| 158 |
+
print("-" * 78)
|
| 159 |
+
print(f"PASS RATE: {passes}/{len(results)}")
|
| 160 |
+
print("=" * 78)
|
| 161 |
+
print(json.dumps(results, indent=2, default=str))
|
| 162 |
+
sys.exit(0 if passes == len(results) else 1)
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
if __name__ == "__main__":
|
| 166 |
+
main()
|