mikeboone Claude Fable 5 commited on
Commit
9a955a5
·
1 Parent(s): e7c6ebb

test(mcp): repeatability burn-in — fire the latest brief N times, report pass rate

Browse files
Files changed (1) hide show
  1. 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()