Spaces:
Running
feat(reporting): production runs reach Snowflake DEMO_RUN/DEMO_RESULTS the moment a build completes
Browse filesThe DemoPrep reporting liveboard reads DEMOPREP_REPORTING.REPORTING (DEMO_RUN,
DEMO_RESULTS, V_DEMO_RUN), which until now was filled only by the hand-run loader
dev_notes/reporting_load_runs.py.txt. Nobody ran it Oct 22-26 and five days of SE
builds were missing. A production run - a real Salesforce opportunity id (15/18
alphanumerics, never a TEST-/REPLAY-/XXTEST marker) with environment prod - is now
written the moment its build completes; every other run stays Supabase-only for the
loader, as before.
- demoprep_app/reporting/writer.py: the loader's single-run logic, importable. Same
rules verbatim (SFID, TEST_MARKER, INTERNAL, owner skips, IS_TEST_RUN sticky,
COALESCE on the opportunity id, err_bucket/cluster/space_of, derived model URL),
LOAD_LEDGER keyed by SOURCE_INTAKE_ID so the loader updates instead of duplicating,
one transaction, fully-qualified statements, never raises.
- supabase_client: fetch_mcp_intake_row (the intake row with its id + created_at);
mcp_result_record extracted from log_mcp_result so the stored row and the
write-through share one shaping.
- mcp_server: _start_reporting_write_through, called in _run_build.result() right
after log_mcp_result, daemon thread, try/except everywhere; no tool signature changed.
- tests/test_reporting_writer.py (48): the rule, the rows, the statements against a
fake cursor, the orchestration, the hook. SQL proven against real Snowflake in a
rolled-back transaction.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- demoprep_app/reporting/__init__.py +8 -0
- demoprep_app/reporting/writer.py +325 -0
- mcp_server.py +45 -2
- sprint_2026_04.md +70 -0
- supabase_client.py +102 -52
- tests/test_deploy_existing_schema.py +4 -1
- tests/test_portal_url_round_trip.py +23 -8
- tests/test_reporting_writer.py +398 -0
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Reporting: the Snowflake tables behind the DemoPrep reporting liveboard
|
| 2 |
+
(DEMOPREP_REPORTING.REPORTING — DEMO_RUN, DEMO_RESULTS, LOAD_LEDGER, V_DEMO_RUN).
|
| 3 |
+
|
| 4 |
+
`writer` puts ONE completed production build there the moment it finishes. The
|
| 5 |
+
hand-run loader (dev_notes/reporting_load_runs.py.txt) remains the backfill for
|
| 6 |
+
everything else and finds the writer's rows through LOAD_LEDGER, so it updates
|
| 7 |
+
them instead of inserting them a second time.
|
| 8 |
+
"""
|
|
@@ -0,0 +1,325 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Write ONE completed production build into the Snowflake reporting tables now.
|
| 2 |
+
|
| 3 |
+
Until 2026-10-26 the tables the DemoPrep reporting liveboard reads
|
| 4 |
+
(DEMOPREP_REPORTING.REPORTING.DEMO_RUN / DEMO_RESULTS, view V_DEMO_RUN) were filled
|
| 5 |
+
only by the hand-run loader, dev_notes/reporting_load_runs.py.txt, from the build
|
| 6 |
+
log in Supabase (demo_history rows mcp_intake + mcp_result). Nobody ran it for five
|
| 7 |
+
days and five days of SE builds were missing. This module is the loader's
|
| 8 |
+
single-run logic, importable, so the MCP server can write a PRODUCTION run the
|
| 9 |
+
moment its build completes (mcp_server._run_build -> _start_reporting_write_through).
|
| 10 |
+
|
| 11 |
+
A production run (boone's rule) is a request whose salesforce_opportunity_id is a
|
| 12 |
+
real Salesforce id — 15 or 18 alphanumerics; the test markers TEST-, REPLAY- and
|
| 13 |
+
XXTEST never qualify — AND whose environment is prod ("prod" / "production";
|
| 14 |
+
"staging" / "stage" do not qualify). Every other run keeps the old behaviour:
|
| 15 |
+
Supabase only, picked up by the loader later.
|
| 16 |
+
|
| 17 |
+
Everything else is the loader's rule, kept verbatim so the two writers agree:
|
| 18 |
+
* SFID / TEST_MARKER, the INTERNAL company set, the testrunner / no.such.person
|
| 19 |
+
owner skip and the IGNORE company skip (`skip_reason`);
|
| 20 |
+
* IS_TEST_RUN = the owner is mike.boone@thoughtspot.com, and sticky in the MERGE;
|
| 21 |
+
* SALESFORCE_OPPORTUNITY_ID is COALESCEd on update, so a human-entered id is
|
| 22 |
+
never overwritten with NULL;
|
| 23 |
+
* `err_bucket`, `cluster`, `space_of`, and the model URL derived from ts_url +
|
| 24 |
+
model_guid when the result carries none;
|
| 25 |
+
* LOAD_LEDGER keyed by SOURCE_INTAKE_ID = the demo_history intake row id. The
|
| 26 |
+
loader finds that entry on its next run and UPDATEs the same DEMO_RUN_KEY, so
|
| 27 |
+
a write-through row is never counted twice.
|
| 28 |
+
|
| 29 |
+
Not done here, left to the loader's next run: inheriting an opportunity id from a
|
| 30 |
+
prospect's other runs (moot — a write-through run always has one) and syncing
|
| 31 |
+
app-emitted findings into DEMO_FINDINGS.
|
| 32 |
+
|
| 33 |
+
The loader stages rows in _RUN_STG / _RES_STG temp tables and MERGEs from them;
|
| 34 |
+
with exactly one run the same statements bind the row's values directly
|
| 35 |
+
(`USING (SELECT %s AS ...)`), fully qualified, inside one transaction.
|
| 36 |
+
"""
|
| 37 |
+
from __future__ import annotations
|
| 38 |
+
|
| 39 |
+
import re
|
| 40 |
+
from datetime import datetime, timezone
|
| 41 |
+
from typing import Any, Callable, Dict, Optional
|
| 42 |
+
|
| 43 |
+
REPORTING_SCHEMA = "DEMOPREP_REPORTING.REPORTING"
|
| 44 |
+
|
| 45 |
+
# A Salesforce opportunity id is 15 or 18 alphanumerics. Anything else (harness
|
| 46 |
+
# sentinels like 'REPLAY-NOT-A-DEAL-…', 'NONE', free text) is NOT an id.
|
| 47 |
+
SFID = re.compile(r"^[A-Za-z0-9]{15}(?:[A-Za-z0-9]{3})?$")
|
| 48 |
+
# Test builds send a marker in place of an opportunity id (tests/*:
|
| 49 |
+
# TEST_OPPORTUNITY_ID = "TEST-NOT-A-DEAL"; older replays "REPLAY-NOT-A-DEAL-<CO>";
|
| 50 |
+
# XXTEST accepted too). A marked request is a test, never a demo for a deal.
|
| 51 |
+
TEST_MARKER = re.compile(r"^(TEST-|REPLAY-|XXTEST)", re.I)
|
| 52 |
+
# Fictional prospects the harnesses build; never reported.
|
| 53 |
+
INTERNAL = {"vantage capital", "northwind retail", "atlas freight", "cloudscape",
|
| 54 |
+
"streamly", "meridian health", "meridian health system"}
|
| 55 |
+
# Whose runs are flagged IS_TEST_RUN (excluded from SE figures).
|
| 56 |
+
TEST_RUN_OWNER = "mike.boone@thoughtspot.com"
|
| 57 |
+
|
| 58 |
+
_ENVIRONMENTS = {"prod": "prod", "production": "prod", "staging": "staging", "stage": "staging"}
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
# --- the rule ---------------------------------------------------------------- #
|
| 62 |
+
def normalize_environment(value: Any) -> str:
|
| 63 |
+
"""'prod' | 'staging' | '' — the same synonyms build_demo_from_brief accepts."""
|
| 64 |
+
return _ENVIRONMENTS.get(str(value or "").strip().lower(), "")
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def opportunity_id(value: Any) -> Optional[str]:
|
| 68 |
+
"""The id if it is a well-formed Salesforce id, else None (the loader's opp_id)."""
|
| 69 |
+
v = str(value or "").strip()
|
| 70 |
+
return v if v and SFID.match(v) else None
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def is_production_run(opportunity: Any, environment: Any) -> bool:
|
| 74 |
+
"""boone's rule: a real Salesforce opportunity id AND environment prod."""
|
| 75 |
+
v = str(opportunity or "").strip()
|
| 76 |
+
if not v or TEST_MARKER.match(v) or not SFID.match(v):
|
| 77 |
+
return False
|
| 78 |
+
return normalize_environment(environment) == "prod"
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def skip_reason(owner_email: Any, company_name: Any) -> Optional[str]:
|
| 82 |
+
"""Why the loader would skip this request outright, or None. Verbatim from the
|
| 83 |
+
loader's per-request filter (no.such.person = the harness's unprovisioned-user
|
| 84 |
+
probe; IGNORE = a request marked to be left out; INTERNAL = fictional prospects)."""
|
| 85 |
+
o = str(owner_email or "").lower()
|
| 86 |
+
co = str(company_name or "").strip()
|
| 87 |
+
if not o:
|
| 88 |
+
return "no owner"
|
| 89 |
+
if "testrunner" in o:
|
| 90 |
+
return "testrunner owner"
|
| 91 |
+
if o.startswith("no.such.person"):
|
| 92 |
+
return "no.such.person owner"
|
| 93 |
+
if "IGNORE" in co:
|
| 94 |
+
return "IGNORE company"
|
| 95 |
+
if co.lower() in INTERNAL:
|
| 96 |
+
return "internal company"
|
| 97 |
+
return None
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
# --- the loader's helpers, verbatim -------------------------------------------- #
|
| 101 |
+
def err_bucket(t: Any) -> Optional[str]:
|
| 102 |
+
if not t:
|
| 103 |
+
return None
|
| 104 |
+
l = str(t).lower()
|
| 105 |
+
for k, v in (("timed out", "llm_timeout"), ("schema deployment", "schema_deploy"),
|
| 106 |
+
("quality gate", "data_quality_gate"), ("concrete values", "blueprint_cardinality"),
|
| 107 |
+
("placeholder", "blueprint_placeholder_names"),
|
| 108 |
+
("failed validation after repair", "blueprint_validation_other"),
|
| 109 |
+
("liveboard creation", "liveboard_creation"),
|
| 110 |
+
("region not supported", "geo_region_unsupported"), ("tml export", "tml_export"),
|
| 111 |
+
("authentication failed", "thoughtspot_auth"), ("compilation error", "sql_compilation"),
|
| 112 |
+
("permission denied", "filesystem_permission"), ("max_tokens", "blueprint_truncated"),
|
| 113 |
+
("hf_token", "portal_token_missing")):
|
| 114 |
+
if k in l:
|
| 115 |
+
return v
|
| 116 |
+
return "other"
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def cluster(u: Any) -> Optional[str]:
|
| 120 |
+
u = (u or "").lower()
|
| 121 |
+
return ("sebe" if "sebe" in u else
|
| 122 |
+
("se-thoughtspot-cloud" if "se-thoughtspot" in u else
|
| 123 |
+
("techpartners" if "techpartners" in u else None)))
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
TESTKEYS = {"with_portal", "use_case", "resolved_ts_url"}
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def space_of(irf: Dict[str, Any]) -> str:
|
| 130 |
+
"""prod|test from the Space's own stamp when present (since the Sep 22 release
|
| 131 |
+
both Spaces stamp space_id); the key fingerprint is only trusted for older,
|
| 132 |
+
unstamped rows."""
|
| 133 |
+
sid = (irf.get("space_id") or "").split("/")[-1]
|
| 134 |
+
if sid == "test-mcp":
|
| 135 |
+
return "test"
|
| 136 |
+
if sid == "mcp":
|
| 137 |
+
return "prod"
|
| 138 |
+
return "test" if (TESTKEYS & set(irf.keys())) else "prod"
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
# --- the rows ------------------------------------------------------------------- #
|
| 142 |
+
def shape_rows(intake_row: Dict[str, Any], result_record: Dict[str, Any],
|
| 143 |
+
completed_at: Optional[str]) -> tuple[Dict[str, Any], Dict[str, Any]]:
|
| 144 |
+
"""The loader's _RUN_STG and _RES_STG rows for one request, keyed by the staging
|
| 145 |
+
column names.
|
| 146 |
+
|
| 147 |
+
intake_row — the demo_history mcp_intake row as stored: id, created_at,
|
| 148 |
+
company_name, use_case, results (the full call payload).
|
| 149 |
+
result_record — the `results` JSON of the matching mcp_result row
|
| 150 |
+
(supabase_client.mcp_result_record), or {} when the build left none.
|
| 151 |
+
completed_at — the result row's created_at (ISO), or None.
|
| 152 |
+
"""
|
| 153 |
+
irf = intake_row.get("results") or {}
|
| 154 |
+
rr = result_record or {}
|
| 155 |
+
rid = irf.get("run_id") or ""
|
| 156 |
+
o = (irf.get("owner_email") or "").lower()
|
| 157 |
+
co = (intake_row.get("company_name") or "").strip()
|
| 158 |
+
errs = rr.get("errors") or []
|
| 159 |
+
ts = (irf.get("ts_url") or rr.get("ts_url") or "").rstrip("/") or None
|
| 160 |
+
mg = rr.get("model_guid") or None
|
| 161 |
+
run = {
|
| 162 |
+
"INTAKE_ID": str(intake_row["id"]),
|
| 163 |
+
"RUN_ID": rid,
|
| 164 |
+
"SFDC": opportunity_id(irf.get("salesforce_opportunity_id")
|
| 165 |
+
or rr.get("salesforce_opportunity_id")),
|
| 166 |
+
"EMAIL": o,
|
| 167 |
+
"PROSPECT": co,
|
| 168 |
+
}
|
| 169 |
+
res = {
|
| 170 |
+
"INTAKE_ID": str(intake_row["id"]),
|
| 171 |
+
"USE_CASE": intake_row.get("use_case") or None,
|
| 172 |
+
"REQUESTED_AT": intake_row["created_at"],
|
| 173 |
+
"COMPLETED_AT": completed_at if rr else None,
|
| 174 |
+
"ELAPSED": rr.get("elapsed_seconds"),
|
| 175 |
+
"STATUS": rr.get("build_status") or ("no_result" if not rr else "unknown"),
|
| 176 |
+
"ERR_CAT": err_bucket(errs[0] if errs else None),
|
| 177 |
+
"SPACE": space_of(irf),
|
| 178 |
+
"CLUSTER_NAME": cluster(ts),
|
| 179 |
+
"IS_TEST": o == TEST_RUN_OWNER,
|
| 180 |
+
"LB_GUID": rr.get("liveboard_guid") or None,
|
| 181 |
+
"LB_URL": rr.get("liveboard_url") or None,
|
| 182 |
+
"MODEL_GUID": mg,
|
| 183 |
+
"MODEL_URL": (rr.get("model_url")
|
| 184 |
+
or (f"{ts}/#/data/tables/{mg}" if ts and mg else None)) or None,
|
| 185 |
+
"PORTAL_URL": (rr.get("portal_url") or "").strip() or None,
|
| 186 |
+
"SCHEMA_NAME": rr.get("schema") or None,
|
| 187 |
+
"ERR_TXT": (str(errs[0])[:4000] if errs else None),
|
| 188 |
+
}
|
| 189 |
+
return run, res
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
# --- the statements --------------------------------------------------------------- #
|
| 193 |
+
_S = REPORTING_SCHEMA
|
| 194 |
+
|
| 195 |
+
SQL_LEDGER_LOOKUP = f"SELECT DEMO_RUN_KEY FROM {_S}.LOAD_LEDGER WHERE SOURCE_INTAKE_ID=%s"
|
| 196 |
+
|
| 197 |
+
# The loader's WHEN MATCHED branch for DEMO_RUN: a human-entered opportunity id is
|
| 198 |
+
# never overwritten with NULL.
|
| 199 |
+
SQL_RUN_UPDATE = f"""UPDATE {_S}.DEMO_RUN SET RUN_ID=%s,
|
| 200 |
+
SALESFORCE_OPPORTUNITY_ID=COALESCE(SALESFORCE_OPPORTUNITY_ID,%s), RUNNER_EMAIL=%s, PROSPECT_NAME=%s
|
| 201 |
+
WHERE DEMO_RUN_KEY=%s"""
|
| 202 |
+
|
| 203 |
+
SQL_RUN_INSERT = (f"INSERT INTO {_S}.DEMO_RUN (RUN_ID, SALESFORCE_OPPORTUNITY_ID, RUNNER_EMAIL, PROSPECT_NAME) "
|
| 204 |
+
"VALUES (%s,%s,%s,%s)")
|
| 205 |
+
|
| 206 |
+
# The key the insert was given: the loader finds it the same way, by the row's
|
| 207 |
+
# identity among rows the ledger does not know yet.
|
| 208 |
+
SQL_RUN_KEY_OF_INSERT = f"""SELECT MAX(r.DEMO_RUN_KEY) FROM {_S}.DEMO_RUN r
|
| 209 |
+
WHERE r.RUN_ID=%s AND r.RUNNER_EMAIL=%s AND r.PROSPECT_NAME=%s
|
| 210 |
+
AND NOT EXISTS (SELECT 1 FROM {_S}.LOAD_LEDGER l WHERE l.DEMO_RUN_KEY=r.DEMO_RUN_KEY)"""
|
| 211 |
+
|
| 212 |
+
SQL_LEDGER_INSERT = f"INSERT INTO {_S}.LOAD_LEDGER (SOURCE_INTAKE_ID, DEMO_RUN_KEY) VALUES (%s,%s)"
|
| 213 |
+
|
| 214 |
+
# The loader's DEMO_RESULTS MERGE, with the staged row bound in place of _RES_STG.
|
| 215 |
+
# IS_TEST_RUN is sticky: a manual test flag is never un-set by a reload.
|
| 216 |
+
SQL_RESULTS_MERGE = f"""MERGE INTO {_S}.DEMO_RESULTS t USING (
|
| 217 |
+
SELECT %s AS DEMO_RUN_KEY, %s AS USE_CASE, TO_TIMESTAMP_TZ(%s) AS REQUESTED_AT, TO_TIMESTAMP_TZ(%s) AS COMPLETED_AT,
|
| 218 |
+
%s AS ELAPSED, %s AS STATUS, %s AS ERR_CAT, %s AS SPACE, %s AS CLUSTER_NAME, %s AS IS_TEST,
|
| 219 |
+
%s AS LB_GUID, %s AS LB_URL, %s AS MODEL_GUID, %s AS MODEL_URL, %s AS PORTAL_URL, %s AS SCHEMA_NAME, %s AS ERR_TXT
|
| 220 |
+
) s ON t.DEMO_RUN_KEY=s.DEMO_RUN_KEY
|
| 221 |
+
WHEN MATCHED THEN UPDATE SET USE_CASE=s.USE_CASE, REQUESTED_AT=s.REQUESTED_AT, COMPLETED_AT=s.COMPLETED_AT, ELAPSED_SECONDS=s.ELAPSED, STATUS=s.STATUS,
|
| 222 |
+
ERROR_CATEGORY=s.ERR_CAT, SPACE=s.SPACE, CLUSTER_NAME=COALESCE(s.CLUSTER_NAME,t.CLUSTER_NAME), IS_TEST_RUN=(t.IS_TEST_RUN OR s.IS_TEST),
|
| 223 |
+
SOURCE_LOADED_AT=CURRENT_TIMESTAMP(),
|
| 224 |
+
LIVEBOARD_GUID=COALESCE(s.LB_GUID,t.LIVEBOARD_GUID), LIVEBOARD_URL=COALESCE(s.LB_URL,t.LIVEBOARD_URL), MODEL_GUID=COALESCE(s.MODEL_GUID,t.MODEL_GUID),
|
| 225 |
+
MODEL_URL=COALESCE(s.MODEL_URL,t.MODEL_URL), PORTAL_URL=COALESCE(s.PORTAL_URL,t.PORTAL_URL), SNOWFLAKE_SCHEMA=COALESCE(s.SCHEMA_NAME,t.SNOWFLAKE_SCHEMA), ERROR_TEXT=s.ERR_TXT
|
| 226 |
+
WHEN NOT MATCHED THEN INSERT (DEMO_RUN_KEY,USE_CASE,REQUESTED_AT,COMPLETED_AT,ELAPSED_SECONDS,STATUS,ERROR_CATEGORY,SPACE,CLUSTER_NAME,IS_TEST_RUN,SOURCE_LOADED_AT,
|
| 227 |
+
LIVEBOARD_GUID,LIVEBOARD_URL,MODEL_GUID,MODEL_URL,PORTAL_URL,SNOWFLAKE_SCHEMA,ERROR_TEXT)
|
| 228 |
+
VALUES (s.DEMO_RUN_KEY,s.USE_CASE,s.REQUESTED_AT,s.COMPLETED_AT,s.ELAPSED,s.STATUS,s.ERR_CAT,s.SPACE,s.CLUSTER_NAME,s.IS_TEST,CURRENT_TIMESTAMP(),
|
| 229 |
+
s.LB_GUID,s.LB_URL,s.MODEL_GUID,s.MODEL_URL,s.PORTAL_URL,s.SCHEMA_NAME,s.ERR_TXT)"""
|
| 230 |
+
|
| 231 |
+
_RES_ORDER = ("USE_CASE", "REQUESTED_AT", "COMPLETED_AT", "ELAPSED", "STATUS", "ERR_CAT", "SPACE",
|
| 232 |
+
"CLUSTER_NAME", "IS_TEST", "LB_GUID", "LB_URL", "MODEL_GUID", "MODEL_URL", "PORTAL_URL",
|
| 233 |
+
"SCHEMA_NAME", "ERR_TXT")
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
def write_run(cur, run: Dict[str, Any], res: Dict[str, Any]) -> Dict[str, Any]:
|
| 237 |
+
"""MERGE one request into DEMO_RUN / LOAD_LEDGER / DEMO_RESULTS through `cur`
|
| 238 |
+
(a Snowflake cursor; the caller owns the transaction). Returns
|
| 239 |
+
{"demo_run_key": <key>, "action": "inserted" | "updated"}."""
|
| 240 |
+
cur.execute(SQL_LEDGER_LOOKUP, (run["INTAKE_ID"],))
|
| 241 |
+
hit = cur.fetchone()
|
| 242 |
+
if hit and hit[0] is not None:
|
| 243 |
+
key = int(hit[0])
|
| 244 |
+
cur.execute(SQL_RUN_UPDATE, (run["RUN_ID"], run["SFDC"], run["EMAIL"], run["PROSPECT"], key))
|
| 245 |
+
action = "updated"
|
| 246 |
+
else:
|
| 247 |
+
cur.execute(SQL_RUN_INSERT, (run["RUN_ID"], run["SFDC"], run["EMAIL"], run["PROSPECT"]))
|
| 248 |
+
cur.execute(SQL_RUN_KEY_OF_INSERT, (run["RUN_ID"], run["EMAIL"], run["PROSPECT"]))
|
| 249 |
+
got = cur.fetchone()
|
| 250 |
+
if not got or got[0] is None:
|
| 251 |
+
raise RuntimeError("DEMO_RUN insert left no un-ledgered row to key")
|
| 252 |
+
key = int(got[0])
|
| 253 |
+
cur.execute(SQL_LEDGER_INSERT, (run["INTAKE_ID"], key))
|
| 254 |
+
action = "inserted"
|
| 255 |
+
cur.execute(SQL_RESULTS_MERGE, (key, *[res[k] for k in _RES_ORDER]))
|
| 256 |
+
return {"demo_run_key": key, "action": action}
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
# --- the entry point the server calls ------------------------------------------------ #
|
| 260 |
+
def _default_connect():
|
| 261 |
+
from snowflake_auth import get_snowflake_connection
|
| 262 |
+
return get_snowflake_connection()
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
def _default_fetch_intake(run_id: str) -> Optional[Dict[str, Any]]:
|
| 266 |
+
from supabase_client import fetch_mcp_intake_row
|
| 267 |
+
return fetch_mcp_intake_row(run_id)
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
def _default_result_record(payload: Dict[str, Any]) -> Dict[str, Any]:
|
| 271 |
+
from supabase_client import mcp_result_record
|
| 272 |
+
return mcp_result_record(payload)
|
| 273 |
+
|
| 274 |
+
|
| 275 |
+
def write_completed_build(run_id: str, result_payload: Dict[str, Any], *,
|
| 276 |
+
opportunity: Any, environment: Any,
|
| 277 |
+
completed_at: Optional[str] = None,
|
| 278 |
+
connect: Callable[[], Any] = _default_connect,
|
| 279 |
+
fetch_intake: Callable[[str], Optional[Dict[str, Any]]] = _default_fetch_intake,
|
| 280 |
+
result_record: Callable[[Dict[str, Any]], Dict[str, Any]] = _default_result_record,
|
| 281 |
+
) -> Dict[str, Any]:
|
| 282 |
+
"""Put one just-completed build into the reporting tables, if it is a production
|
| 283 |
+
run. `result_payload` is the dict the server hands log_mcp_result. Returns
|
| 284 |
+
{"status": "written" | "skipped" | "failed", ...} and never raises: the server
|
| 285 |
+
calls this on a background thread after the build result is recorded, and
|
| 286 |
+
nothing here may fail, block or slow a build.
|
| 287 |
+
"""
|
| 288 |
+
try:
|
| 289 |
+
if not is_production_run(opportunity, environment):
|
| 290 |
+
return {"status": "skipped", "reason": "not a production run",
|
| 291 |
+
"opportunity": str(opportunity or ""), "environment": str(environment or "")}
|
| 292 |
+
intake = fetch_intake(run_id)
|
| 293 |
+
if not intake or not intake.get("id") or not intake.get("created_at"):
|
| 294 |
+
# No intake row = the loader would never see this run either. Its
|
| 295 |
+
# own failure was already logged by log_mcp_payload.
|
| 296 |
+
return {"status": "skipped", "reason": "no intake row in demo_history"}
|
| 297 |
+
irf = intake.get("results") or {}
|
| 298 |
+
why = skip_reason(irf.get("owner_email"), intake.get("company_name"))
|
| 299 |
+
if why:
|
| 300 |
+
return {"status": "skipped", "reason": why}
|
| 301 |
+
run, res = shape_rows(intake, result_record(result_payload),
|
| 302 |
+
completed_at or datetime.now(timezone.utc).isoformat())
|
| 303 |
+
conn = connect()
|
| 304 |
+
try:
|
| 305 |
+
conn.autocommit(False)
|
| 306 |
+
cur = conn.cursor()
|
| 307 |
+
try:
|
| 308 |
+
out = write_run(cur, run, res)
|
| 309 |
+
finally:
|
| 310 |
+
cur.close()
|
| 311 |
+
conn.commit()
|
| 312 |
+
except Exception:
|
| 313 |
+
try:
|
| 314 |
+
conn.rollback()
|
| 315 |
+
except Exception:
|
| 316 |
+
pass
|
| 317 |
+
raise
|
| 318 |
+
finally:
|
| 319 |
+
try:
|
| 320 |
+
conn.close()
|
| 321 |
+
except Exception:
|
| 322 |
+
pass
|
| 323 |
+
return {"status": "written", "intake_id": run["INTAKE_ID"], **out}
|
| 324 |
+
except Exception as exc:
|
| 325 |
+
return {"status": "failed", "error": f"{type(exc).__name__}: {exc}"}
|
|
@@ -787,6 +787,39 @@ def _complete_brand_from_capture(run_id: str, brand: dict, company_url: str,
|
|
| 787 |
_log_hit(f"BRAND run_id={run_id} {n}")
|
| 788 |
|
| 789 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 790 |
def _run_build(run_id: str, brief: str, company_name: str, use_case: str, company_url: str,
|
| 791 |
ts_target_url: str, ts_auth_key: str, owner: str,
|
| 792 |
with_portal: bool = True, call_payload: dict | None = None,
|
|
@@ -963,7 +996,7 @@ def _run_build(run_id: str, brief: str, company_name: str, use_case: str, compan
|
|
| 963 |
# Durable result record: the in-memory stores above wipe on restart, so
|
| 964 |
# every terminal outcome (success | partial | failed) is also persisted
|
| 965 |
# to demo_history (status='mcp_result', joined to intake by run_id).
|
| 966 |
-
|
| 967 |
# The same Space + commit stamp the intake row and the session_logs
|
| 968 |
# 'run started' event carry, so the three records agree on where
|
| 969 |
# this run happened.
|
|
@@ -985,8 +1018,18 @@ def _run_build(run_id: str, brief: str, company_name: str, use_case: str, compan
|
|
| 985 |
# schema if ThoughtSpot refused it (E3).
|
| 986 |
"ddl": out["ddl"],
|
| 987 |
**_opt_in,
|
| 988 |
-
}
|
|
|
|
| 989 |
_log_hit(f"RESULT_PERSISTED run_id={run_id} status={out['status']} supabase={_persisted_result}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 990 |
return out
|
| 991 |
|
| 992 |
controller = None
|
|
|
|
| 787 |
_log_hit(f"BRAND run_id={run_id} {n}")
|
| 788 |
|
| 789 |
|
| 790 |
+
def _start_reporting_write_through(run_id: str, result_payload: dict, *,
|
| 791 |
+
opportunity: str, environment: str):
|
| 792 |
+
"""Write a just-completed PRODUCTION run to the Snowflake reporting tables on a
|
| 793 |
+
daemon thread (demoprep_app.reporting.writer.write_completed_build). A
|
| 794 |
+
production run is a real Salesforce opportunity id (15/18 alphanumerics, no
|
| 795 |
+
TEST-/REPLAY-/XXTEST marker) with environment prod; anything else is left to
|
| 796 |
+
the hand-run loader, as before. Returns the started Thread, or None when the
|
| 797 |
+
run does not qualify or the thread could not be started. Never raises and
|
| 798 |
+
never waits: the build result is already recorded when this runs.
|
| 799 |
+
"""
|
| 800 |
+
try:
|
| 801 |
+
from demoprep_app.reporting.writer import is_production_run, write_completed_build
|
| 802 |
+
if not is_production_run(opportunity, environment):
|
| 803 |
+
_log_hit(f"REPORTING_SKIP run_id={run_id} not a production run "
|
| 804 |
+
f"(opportunity={opportunity!r} environment={environment!r})")
|
| 805 |
+
return None
|
| 806 |
+
|
| 807 |
+
def _write() -> None:
|
| 808 |
+
try:
|
| 809 |
+
outcome = write_completed_build(run_id, result_payload,
|
| 810 |
+
opportunity=opportunity, environment=environment)
|
| 811 |
+
except Exception as exc: # write_completed_build never raises; belt and braces
|
| 812 |
+
outcome = {"status": "failed", "error": f"{type(exc).__name__}: {exc}"}
|
| 813 |
+
_log_hit(f"REPORTING_WRITE run_id={run_id} {outcome}")
|
| 814 |
+
|
| 815 |
+
t = threading.Thread(target=_write, daemon=True, name=f"reporting-{run_id}")
|
| 816 |
+
t.start()
|
| 817 |
+
return t
|
| 818 |
+
except Exception as exc:
|
| 819 |
+
_log_hit(f"REPORTING_WRITE run_id={run_id} not started: {type(exc).__name__}: {exc}")
|
| 820 |
+
return None
|
| 821 |
+
|
| 822 |
+
|
| 823 |
def _run_build(run_id: str, brief: str, company_name: str, use_case: str, company_url: str,
|
| 824 |
ts_target_url: str, ts_auth_key: str, owner: str,
|
| 825 |
with_portal: bool = True, call_payload: dict | None = None,
|
|
|
|
| 996 |
# Durable result record: the in-memory stores above wipe on restart, so
|
| 997 |
# every terminal outcome (success | partial | failed) is also persisted
|
| 998 |
# to demo_history (status='mcp_result', joined to intake by run_id).
|
| 999 |
+
result_payload = {
|
| 1000 |
# The same Space + commit stamp the intake row and the session_logs
|
| 1001 |
# 'run started' event carry, so the three records agree on where
|
| 1002 |
# this run happened.
|
|
|
|
| 1018 |
# schema if ThoughtSpot refused it (E3).
|
| 1019 |
"ddl": out["ddl"],
|
| 1020 |
**_opt_in,
|
| 1021 |
+
}
|
| 1022 |
+
_persisted_result = log_mcp_result(result_payload)
|
| 1023 |
_log_hit(f"RESULT_PERSISTED run_id={run_id} status={out['status']} supabase={_persisted_result}")
|
| 1024 |
+
# Snowflake reporting write-through (boone 2026-10-26): a PRODUCTION run — a
|
| 1025 |
+
# real Salesforce opportunity id and environment prod — reaches the
|
| 1026 |
+
# reporting tables now, on its own thread, instead of waiting for the
|
| 1027 |
+
# hand-run loader. Every other run stays Supabase-only for the loader.
|
| 1028 |
+
# Starts after the durable records above and can never fail, block or
|
| 1029 |
+
# slow this result.
|
| 1030 |
+
_start_reporting_write_through(run_id, result_payload,
|
| 1031 |
+
opportunity=salesforce_opportunity_id,
|
| 1032 |
+
environment=environment)
|
| 1033 |
return out
|
| 1034 |
|
| 1035 |
controller = None
|
|
@@ -1847,6 +1847,76 @@ settings IN the database through the MCP server, dropdown-constrained.
|
|
| 1847 |
real `TS_USER_DEFAULT` / `TS_ENV_URL_DEFAULT`) and the `OPENAI_API_KEY` check in
|
| 1848 |
`validate_required_settings()`
|
| 1849 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1850 |
---
|
| 1851 |
|
| 1852 |
*Last updated: October 22, 2026 — settings document on test-mcp (0d02061) and folded
|
|
|
|
| 1847 |
real `TS_USER_DEFAULT` / `TS_ENV_URL_DEFAULT`) and the `OPENAI_API_KEY` check in
|
| 1848 |
`validate_required_settings()`
|
| 1849 |
|
| 1850 |
+
## October 26, 2026 — HOTFIX: production runs written to Snowflake reporting the moment a build completes (branch `claude/hotfix-snowflake-writethrough-d22061`, off the test tip `0bcf580`)
|
| 1851 |
+
|
| 1852 |
+
Found tonight: the tables the DemoPrep reporting liveboard reads
|
| 1853 |
+
(`DEMOPREP_REPORTING.REPORTING.DEMO_RUN` / `DEMO_RESULTS`, view `V_DEMO_RUN`) were
|
| 1854 |
+
filled ONLY by the hand-run loader `dev_notes/reporting_load_runs.py.txt` (reads the
|
| 1855 |
+
build log in Supabase: `demo_history` rows `mcp_intake` + `mcp_result`, MERGEs into
|
| 1856 |
+
Snowflake). No scheduler. Nobody ran it Oct 22–26, so five days of SE builds were
|
| 1857 |
+
missing until boone ran it by hand (DEMO_RUN then 570 rows, through Oct 26 20:46 UTC).
|
| 1858 |
+
CEO demo on Oct 27 — hotfixed tonight, through test and then prod (boone's call).
|
| 1859 |
+
|
| 1860 |
+
**The rule (boone):** a *production run* = `salesforce_opportunity_id` is a real
|
| 1861 |
+
Salesforce id (15 or 18 alphanumerics — the loader's `SFID`; `TEST-` / `REPLAY-` /
|
| 1862 |
+
`XXTEST` markers never qualify) AND `environment` is prod (`prod` / `production`;
|
| 1863 |
+
`staging` / `stage` do not). Those reach Snowflake the moment the build completes.
|
| 1864 |
+
Every other run keeps the old behaviour: Supabase only, picked up by the loader later.
|
| 1865 |
+
|
| 1866 |
+
Line decision: prod `b918bd8` is an ancestor of test `0bcf580` (`git merge-base
|
| 1867 |
+
--is-ancestor`), both confirmed live by `ping`; the test tip had completed a full
|
| 1868 |
+
successful build tonight (run `4de504402486`, liveboard + portal) and the sprint
|
| 1869 |
+
entries above record the Oct 22 pieces verified on test — so the normal path: branch
|
| 1870 |
+
from the test tip, deploy to test, verify, push the same SHA to prod.
|
| 1871 |
+
|
| 1872 |
+
### Done
|
| 1873 |
+
- [x] `demoprep_app/reporting/writer.py` ✅ the loader's single-run logic, importable.
|
| 1874 |
+
Every loader rule kept verbatim: `SFID`, `TEST_MARKER`, the `INTERNAL` company set,
|
| 1875 |
+
the `testrunner` / `no.such.person` / `IGNORE` skips, `IS_TEST_RUN` = owner is
|
| 1876 |
+
mike.boone (sticky in the MERGE), `COALESCE` so a human-entered opportunity id is
|
| 1877 |
+
never overwritten with NULL, `err_bucket` / `cluster` / `space_of`, the model URL
|
| 1878 |
+
derived from ts_url + model guid. `LOAD_LEDGER` row keyed by `SOURCE_INTAKE_ID`
|
| 1879 |
+
(the `demo_history` intake row id) so the loader's next run finds it and UPDATEs
|
| 1880 |
+
the same `DEMO_RUN_KEY` — never a second row. One transaction, fully-qualified
|
| 1881 |
+
statements (no `USE SCHEMA`), `get_snowflake_connection()` (keypair from admin
|
| 1882 |
+
settings, which the Space already has). `write_completed_build()` never raises.
|
| 1883 |
+
Left to the loader: opportunity-id inheritance (moot — a write-through run always
|
| 1884 |
+
has one) and the app-findings sync into `DEMO_FINDINGS`.
|
| 1885 |
+
- [x] `supabase_client.fetch_mcp_intake_row(run_id)` ✅ the intake row AS STORED (id +
|
| 1886 |
+
created_at — `fetch_mcp_run` drops both); `mcp_result_record(payload)` ✅ the result
|
| 1887 |
+
row's `results`, now ONE shaping shared by `log_mcp_result` and the writer, so the
|
| 1888 |
+
write-through and the loader read the same record.
|
| 1889 |
+
- [x] `mcp_server._start_reporting_write_through()` ✅ called in `_run_build.result()`
|
| 1890 |
+
right after `log_mcp_result`, only for qualifying runs; daemon thread; try/except
|
| 1891 |
+
around everything; hit-log lines `REPORTING_SKIP` / `REPORTING_WRITE`. No MCP tool
|
| 1892 |
+
signature changed (AgentSpot caches the tool list). `.env` untouched.
|
| 1893 |
+
- [x] Tests ✅ `tests/test_reporting_writer.py` (48): the rule (real id + prod → yes;
|
| 1894 |
+
real id + staging → no; `TEST-NOT-A-DEAL` → no; blank → no …), the rows, the
|
| 1895 |
+
statements against a fake cursor (insert → ledger → merge; ledger hit → update with
|
| 1896 |
+
COALESCE), the orchestration (staging never connects; Snowflake failure → rolled
|
| 1897 |
+
back, reported, never raised), the server hook (own thread; a raising writer never
|
| 1898 |
+
escapes). `tests/test_deploy_existing_schema.py` source check re-pointed at
|
| 1899 |
+
`mcp_result_record`.
|
| 1900 |
+
- [x] SQL proven against real Snowflake in a rolled-back transaction ✅ insert →
|
| 1901 |
+
the row visible in `V_DEMO_RUN` with liveboard / model URL / portal / schema →
|
| 1902 |
+
second pass with the same intake id = `updated`, same key → rollback left nothing.
|
| 1903 |
+
- [ ] test-mcp: deploy, `ping` shows the SHA, e2e (prod-flagged build with a
|
| 1904 |
+
well-formed id + staging control), loader dry-run + real run with no double count
|
| 1905 |
+
- [ ] prod: same SHA, `ping`, one real-shaped prod build as the final proof
|
| 1906 |
+
|
| 1907 |
+
### Open
|
| 1908 |
+
- [ ] **Safety net:** a scheduled loader run (daily) for everything the
|
| 1909 |
+
write-through does not cover — staging / test-marked runs that should still be
|
| 1910 |
+
reported, a run whose Snowflake write failed, the opportunity-id inheritance and
|
| 1911 |
+
the app-findings sync.
|
| 1912 |
+
- [ ] Loader quirk found on the way (not changed — the loader lives outside the repo):
|
| 1913 |
+
in its `DEMO_RESULTS` MERGE the `-- sticky …` comment swallows
|
| 1914 |
+
`SOURCE_LOADED_AT=CURRENT_TIMESTAMP(),` on the same line, so an UPDATE never
|
| 1915 |
+
refreshes `SOURCE_LOADED_AT` (inserts do). The writer's copy has the comment on its
|
| 1916 |
+
own line.
|
| 1917 |
+
- [ ] `dev_notes/reporting_admin_handoff_DEMO_RUN.md` "How it is maintained" should name
|
| 1918 |
+
the write-through beside the loader (the file is untracked in this line).
|
| 1919 |
+
|
| 1920 |
---
|
| 1921 |
|
| 1922 |
*Last updated: October 22, 2026 — settings document on test-mcp (0d02061) and folded
|
|
@@ -823,6 +823,44 @@ def fetch_mcp_run(run_id: str) -> Optional[Dict[str, Any]]:
|
|
| 823 |
return None
|
| 824 |
|
| 825 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 826 |
def log_mcp_payload(payload: Dict[str, Any]) -> bool:
|
| 827 |
"""Durably persist a raw MCP intake payload (the full brief + metadata) so real
|
| 828 |
research payloads survive container restarts / log rotation — i.e. so you can tell
|
|
@@ -874,6 +912,69 @@ def log_mcp_payload(payload: Dict[str, Any]) -> bool:
|
|
| 874 |
return False
|
| 875 |
|
| 876 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 877 |
def log_mcp_result(payload: Dict[str, Any]) -> bool:
|
| 878 |
"""Durably persist an MCP build RESULT (terminal status, object GUIDs/URLs,
|
| 879 |
errors, timing) so outcomes survive container restarts — the in-memory
|
|
@@ -898,61 +999,10 @@ def log_mcp_result(payload: Dict[str, Any]) -> bool:
|
|
| 898 |
"industry": "",
|
| 899 |
"use_case": _history_text(payload.get("use_case")),
|
| 900 |
"demo_config": {"source": "mcp_result"},
|
| 901 |
-
"results":
|
| 902 |
-
"run_id": payload.get("run_id"),
|
| 903 |
-
"build_status": payload.get("status"),
|
| 904 |
-
# Which deployment produced this result: the HF Space and the
|
| 905 |
-
# commit it ran (demoprep_app.deployment_identity). The intake
|
| 906 |
-
# row and the session_logs 'run started' event carry the same
|
| 907 |
-
# four keys, so a run ties to a Space and a build by lookup,
|
| 908 |
-
# not by inference from result shapes.
|
| 909 |
-
"space_id": payload.get("space_id") or "",
|
| 910 |
-
"space_host": payload.get("space_host") or "",
|
| 911 |
-
"code": payload.get("code") or "",
|
| 912 |
-
"code_source": payload.get("code_source") or "",
|
| 913 |
-
"owner_email": payload.get("owner_email"),
|
| 914 |
-
"ts_url": payload.get("ts_url"),
|
| 915 |
-
"schema": payload.get("schema"),
|
| 916 |
-
"model_guid": payload.get("model_guid"),
|
| 917 |
-
"liveboard_guid": payload.get("liveboard_guid"),
|
| 918 |
-
"model_url": payload.get("model_url"),
|
| 919 |
-
"liveboard_url": payload.get("liveboard_url"),
|
| 920 |
-
# The branded customer-facing site built from the liveboard. This
|
| 921 |
-
# is the deliverable an SE actually sends, so it must be
|
| 922 |
-
# recoverable from the durable record and not only from the
|
| 923 |
-
# return value of the call that created it.
|
| 924 |
-
"portal_url": payload.get("portal_url") or "",
|
| 925 |
-
# The editable-source zip beside it. Same reasoning: the SE who
|
| 926 |
-
# wants to tailor the site needs to find this link later, not
|
| 927 |
-
# only in the return value of the call that built it.
|
| 928 |
-
"download_url": payload.get("download_url") or "",
|
| 929 |
-
"warnings": payload.get("warnings") or [],
|
| 930 |
-
# The structured records behind the warnings (code, severity,
|
| 931 |
-
# measure, fix) — see demoprep_app/quality/findings.py.
|
| 932 |
-
"findings": payload.get("findings") or [],
|
| 933 |
-
# Did they reach demo_findings? None = nothing to persist,
|
| 934 |
-
# False = the write failed (RLS 42501 until an insert policy
|
| 935 |
-
# exists) and the records exist only here and in the run log.
|
| 936 |
-
"findings_persisted": payload.get("findings_persisted"),
|
| 937 |
-
"findings_persist_error": payload.get("findings_persist_error") or "",
|
| 938 |
-
"errors": payload.get("errors") or [],
|
| 939 |
-
"elapsed_seconds": payload.get("elapsed_seconds"),
|
| 940 |
-
# The pre-build login check (mcp_server._login_precheck, ISS-22):
|
| 941 |
-
# outcome, HTTP status, reason. None for runs from before it existed.
|
| 942 |
-
"login_precheck": payload.get("login_precheck"),
|
| 943 |
-
# The CREATE TABLE text the build loaded, so a run that died at
|
| 944 |
-
# the ThoughtSpot step can be finished later from its schema
|
| 945 |
-
# (mcp_server.deploy_existing_schema) without regenerating.
|
| 946 |
-
"ddl": payload.get("ddl") or "",
|
| 947 |
-
"source": "mcp",
|
| 948 |
-
},
|
| 949 |
"status": "mcp_result",
|
| 950 |
"created_at": datetime.utcnow().isoformat(),
|
| 951 |
}
|
| 952 |
-
# Opt-in: the custom MCP demo walkthrough (markdown) is stored only for a
|
| 953 |
-
# build that asked for it (custom_mcp_demo), so every other row is unchanged.
|
| 954 |
-
if "custom_mcp_demo_section" in payload:
|
| 955 |
-
row["results"]["custom_mcp_demo_section"] = payload["custom_mcp_demo_section"] or ""
|
| 956 |
client.client.table("demo_history").insert(row).execute()
|
| 957 |
return True
|
| 958 |
except Exception as e:
|
|
|
|
| 823 |
return None
|
| 824 |
|
| 825 |
|
| 826 |
+
def fetch_mcp_intake_row(run_id: str) -> Optional[Dict[str, Any]]:
|
| 827 |
+
"""The newest demo_history intake row (status='mcp_intake') of one MCP run, AS
|
| 828 |
+
STORED: {id, created_at, company_name, use_case, results}. Unlike fetch_mcp_run
|
| 829 |
+
this keeps the row's own id and created_at — the id is what the reporting
|
| 830 |
+
loader's LOAD_LEDGER records as SOURCE_INTAKE_ID, so the Snowflake write-through
|
| 831 |
+
(demoprep_app.reporting.writer) and the loader agree on which request a
|
| 832 |
+
DEMO_RUN row belongs to, and created_at is that row's REQUESTED_AT. None when
|
| 833 |
+
Supabase is off, there is no such row, or the lookup fails — never raises.
|
| 834 |
+
"""
|
| 835 |
+
try:
|
| 836 |
+
client = SupabaseSettings()
|
| 837 |
+
if not getattr(client, "enabled", False) or not (run_id or "").strip():
|
| 838 |
+
return None
|
| 839 |
+
resp = (
|
| 840 |
+
client.client.table("demo_history")
|
| 841 |
+
.select("id,created_at,company_name,use_case,results")
|
| 842 |
+
.eq("status", "mcp_intake")
|
| 843 |
+
.eq("results->>run_id", run_id.strip())
|
| 844 |
+
.order("created_at", desc=True)
|
| 845 |
+
.limit(1)
|
| 846 |
+
.execute()
|
| 847 |
+
)
|
| 848 |
+
rows = getattr(resp, "data", None) or []
|
| 849 |
+
if not rows:
|
| 850 |
+
return None
|
| 851 |
+
row = dict(rows[0])
|
| 852 |
+
if isinstance(row.get("results"), str):
|
| 853 |
+
try:
|
| 854 |
+
row["results"] = json.loads(row["results"])
|
| 855 |
+
except Exception:
|
| 856 |
+
row["results"] = {}
|
| 857 |
+
row["results"] = row.get("results") or {}
|
| 858 |
+
return row
|
| 859 |
+
except Exception as e:
|
| 860 |
+
print(f"⚠️ mcp intake lookup failed for {run_id!r}: {e}", flush=True)
|
| 861 |
+
return None
|
| 862 |
+
|
| 863 |
+
|
| 864 |
def log_mcp_payload(payload: Dict[str, Any]) -> bool:
|
| 865 |
"""Durably persist a raw MCP intake payload (the full brief + metadata) so real
|
| 866 |
research payloads survive container restarts / log rotation — i.e. so you can tell
|
|
|
|
| 912 |
return False
|
| 913 |
|
| 914 |
|
| 915 |
+
def mcp_result_record(payload: Dict[str, Any]) -> Dict[str, Any]:
|
| 916 |
+
"""The `results` JSON of an mcp_result row, shaped from a build's result payload.
|
| 917 |
+
|
| 918 |
+
ONE shaping for its two readers: log_mcp_result stores it, and the Snowflake
|
| 919 |
+
reporting write-through (demoprep_app.reporting.writer) reads this same dict
|
| 920 |
+
the moment a build completes — exactly what the hand-run loader reads back from
|
| 921 |
+
the row later, so the two never disagree about a run.
|
| 922 |
+
"""
|
| 923 |
+
results = {
|
| 924 |
+
"run_id": payload.get("run_id"),
|
| 925 |
+
"build_status": payload.get("status"),
|
| 926 |
+
# Which deployment produced this result: the HF Space and the
|
| 927 |
+
# commit it ran (demoprep_app.deployment_identity). The intake
|
| 928 |
+
# row and the session_logs 'run started' event carry the same
|
| 929 |
+
# four keys, so a run ties to a Space and a build by lookup,
|
| 930 |
+
# not by inference from result shapes.
|
| 931 |
+
"space_id": payload.get("space_id") or "",
|
| 932 |
+
"space_host": payload.get("space_host") or "",
|
| 933 |
+
"code": payload.get("code") or "",
|
| 934 |
+
"code_source": payload.get("code_source") or "",
|
| 935 |
+
"owner_email": payload.get("owner_email"),
|
| 936 |
+
"ts_url": payload.get("ts_url"),
|
| 937 |
+
"schema": payload.get("schema"),
|
| 938 |
+
"model_guid": payload.get("model_guid"),
|
| 939 |
+
"liveboard_guid": payload.get("liveboard_guid"),
|
| 940 |
+
"model_url": payload.get("model_url"),
|
| 941 |
+
"liveboard_url": payload.get("liveboard_url"),
|
| 942 |
+
# The branded customer-facing site built from the liveboard. This
|
| 943 |
+
# is the deliverable an SE actually sends, so it must be
|
| 944 |
+
# recoverable from the durable record and not only from the
|
| 945 |
+
# return value of the call that created it.
|
| 946 |
+
"portal_url": payload.get("portal_url") or "",
|
| 947 |
+
# The editable-source zip beside it. Same reasoning: the SE who
|
| 948 |
+
# wants to tailor the site needs to find this link later, not
|
| 949 |
+
# only in the return value of the call that built it.
|
| 950 |
+
"download_url": payload.get("download_url") or "",
|
| 951 |
+
"warnings": payload.get("warnings") or [],
|
| 952 |
+
# The structured records behind the warnings (code, severity,
|
| 953 |
+
# measure, fix) — see demoprep_app/quality/findings.py.
|
| 954 |
+
"findings": payload.get("findings") or [],
|
| 955 |
+
# Did they reach demo_findings? None = nothing to persist,
|
| 956 |
+
# False = the write failed (RLS 42501 until an insert policy
|
| 957 |
+
# exists) and the records exist only here and in the run log.
|
| 958 |
+
"findings_persisted": payload.get("findings_persisted"),
|
| 959 |
+
"findings_persist_error": payload.get("findings_persist_error") or "",
|
| 960 |
+
"errors": payload.get("errors") or [],
|
| 961 |
+
"elapsed_seconds": payload.get("elapsed_seconds"),
|
| 962 |
+
# The pre-build login check (mcp_server._login_precheck, ISS-22):
|
| 963 |
+
# outcome, HTTP status, reason. None for runs from before it existed.
|
| 964 |
+
"login_precheck": payload.get("login_precheck"),
|
| 965 |
+
# The CREATE TABLE text the build loaded, so a run that died at
|
| 966 |
+
# the ThoughtSpot step can be finished later from its schema
|
| 967 |
+
# (mcp_server.deploy_existing_schema) without regenerating.
|
| 968 |
+
"ddl": payload.get("ddl") or "",
|
| 969 |
+
"source": "mcp",
|
| 970 |
+
}
|
| 971 |
+
# Opt-in: the custom MCP demo walkthrough (markdown) is stored only for a
|
| 972 |
+
# build that asked for it (custom_mcp_demo), so every other row is unchanged.
|
| 973 |
+
if "custom_mcp_demo_section" in payload:
|
| 974 |
+
results["custom_mcp_demo_section"] = payload["custom_mcp_demo_section"] or ""
|
| 975 |
+
return results
|
| 976 |
+
|
| 977 |
+
|
| 978 |
def log_mcp_result(payload: Dict[str, Any]) -> bool:
|
| 979 |
"""Durably persist an MCP build RESULT (terminal status, object GUIDs/URLs,
|
| 980 |
errors, timing) so outcomes survive container restarts — the in-memory
|
|
|
|
| 999 |
"industry": "",
|
| 1000 |
"use_case": _history_text(payload.get("use_case")),
|
| 1001 |
"demo_config": {"source": "mcp_result"},
|
| 1002 |
+
"results": mcp_result_record(payload),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1003 |
"status": "mcp_result",
|
| 1004 |
"created_at": datetime.utcnow().isoformat(),
|
| 1005 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1006 |
client.client.table("demo_history").insert(row).execute()
|
| 1007 |
return True
|
| 1008 |
except Exception as e:
|
|
@@ -66,7 +66,10 @@ def test_the_ddl_is_kept_on_the_run_records_but_left_out_of_status():
|
|
| 66 |
# _last_build (always had it) + _recent_builds + log_mcp_result
|
| 67 |
assert '"ddl": out["ddl"],' in src and src.count('"ddl": out["ddl"]') == 3
|
| 68 |
assert 'if k != "ddl"' in inspect.getsource(mcp_server.status)
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
| 70 |
assert '"ddl": payload.get("ddl") or ""' in sb
|
| 71 |
|
| 72 |
|
|
|
|
| 66 |
# _last_build (always had it) + _recent_builds + log_mcp_result
|
| 67 |
assert '"ddl": out["ddl"],' in src and src.count('"ddl": out["ddl"]') == 3
|
| 68 |
assert 'if k != "ddl"' in inspect.getsource(mcp_server.status)
|
| 69 |
+
# The row's `results` are shaped by mcp_result_record (shared with the Snowflake
|
| 70 |
+
# reporting write-through since 2026-10-26); log_mcp_result stores that dict.
|
| 71 |
+
assert '"results": mcp_result_record(payload)' in inspect.getsource(supabase_client.log_mcp_result)
|
| 72 |
+
sb = inspect.getsource(supabase_client.mcp_result_record)
|
| 73 |
assert '"ddl": payload.get("ddl") or ""' in sb
|
| 74 |
|
| 75 |
|
|
@@ -27,6 +27,25 @@ def _build_result_source() -> str:
|
|
| 27 |
return inspect.getsource(mcp_server._run_build)
|
| 28 |
|
| 29 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
def test_status_view_carries_portal_url():
|
| 31 |
"""_recent_builds is what status() serves; it must include the site."""
|
| 32 |
src = _mcp_source()
|
|
@@ -36,15 +55,13 @@ def test_status_view_carries_portal_url():
|
|
| 36 |
|
| 37 |
def test_durable_result_payload_carries_portal_url():
|
| 38 |
"""The payload handed to log_mcp_result must include the site."""
|
| 39 |
-
|
| 40 |
-
block = src.split("log_mcp_result({", 1)[1].split("})", 1)[0]
|
| 41 |
assert "portal_url" in block, "the durable record would omit the built site"
|
| 42 |
|
| 43 |
|
| 44 |
def test_persisted_row_carries_portal_url():
|
| 45 |
"""log_mcp_result rebuilds `results` from its own key list — check it too."""
|
| 46 |
-
|
| 47 |
-
src = inspect.getsource(log_mcp_result)
|
| 48 |
assert '"portal_url"' in src, "demo_history rows would omit the built site"
|
| 49 |
|
| 50 |
|
|
@@ -73,12 +90,10 @@ def test_status_view_carries_download_url():
|
|
| 73 |
|
| 74 |
|
| 75 |
def test_durable_result_payload_carries_download_url():
|
| 76 |
-
|
| 77 |
-
block = src.split("log_mcp_result({", 1)[1].split("})", 1)[0]
|
| 78 |
assert "download_url" in block, "the durable record would omit the source zip"
|
| 79 |
|
| 80 |
|
| 81 |
def test_persisted_row_carries_download_url():
|
| 82 |
-
|
| 83 |
-
src = inspect.getsource(log_mcp_result)
|
| 84 |
assert '"download_url"' in src, "demo_history rows would omit the source zip"
|
|
|
|
| 27 |
return inspect.getsource(mcp_server._run_build)
|
| 28 |
|
| 29 |
|
| 30 |
+
def _durable_payload_block() -> str:
|
| 31 |
+
"""The dict _run_build hands log_mcp_result. Since the Snowflake reporting
|
| 32 |
+
write-through (2026-10-26) it is built first as `result_payload` and then
|
| 33 |
+
stored — and handed to the write-through — so the block is that literal."""
|
| 34 |
+
src = _build_result_source()
|
| 35 |
+
assert "_persisted_result = log_mcp_result(result_payload)" in src, \
|
| 36 |
+
"the durable record is no longer the result_payload dict"
|
| 37 |
+
return src.split("result_payload = {", 1)[1].split("_persisted_result = log_mcp_result(result_payload)", 1)[0]
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def _persisted_row_source() -> str:
|
| 41 |
+
"""The `results` row log_mcp_result stores is shaped by mcp_result_record
|
| 42 |
+
(shared with the write-through) — check the shaping, and that it is stored."""
|
| 43 |
+
from supabase_client import log_mcp_result, mcp_result_record
|
| 44 |
+
assert '"results": mcp_result_record(payload)' in inspect.getsource(log_mcp_result), \
|
| 45 |
+
"log_mcp_result no longer stores the mcp_result_record row"
|
| 46 |
+
return inspect.getsource(mcp_result_record)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
def test_status_view_carries_portal_url():
|
| 50 |
"""_recent_builds is what status() serves; it must include the site."""
|
| 51 |
src = _mcp_source()
|
|
|
|
| 55 |
|
| 56 |
def test_durable_result_payload_carries_portal_url():
|
| 57 |
"""The payload handed to log_mcp_result must include the site."""
|
| 58 |
+
block = _durable_payload_block()
|
|
|
|
| 59 |
assert "portal_url" in block, "the durable record would omit the built site"
|
| 60 |
|
| 61 |
|
| 62 |
def test_persisted_row_carries_portal_url():
|
| 63 |
"""log_mcp_result rebuilds `results` from its own key list — check it too."""
|
| 64 |
+
src = _persisted_row_source()
|
|
|
|
| 65 |
assert '"portal_url"' in src, "demo_history rows would omit the built site"
|
| 66 |
|
| 67 |
|
|
|
|
| 90 |
|
| 91 |
|
| 92 |
def test_durable_result_payload_carries_download_url():
|
| 93 |
+
block = _durable_payload_block()
|
|
|
|
| 94 |
assert "download_url" in block, "the durable record would omit the source zip"
|
| 95 |
|
| 96 |
|
| 97 |
def test_persisted_row_carries_download_url():
|
| 98 |
+
src = _persisted_row_source()
|
|
|
|
| 99 |
assert '"download_url"' in src, "demo_history rows would omit the source zip"
|
|
@@ -0,0 +1,398 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""The Snowflake reporting write-through: a production run reaches DEMO_RUN /
|
| 2 |
+
DEMO_RESULTS the moment its build completes; everything else is left to the loader.
|
| 3 |
+
|
| 4 |
+
Run: pytest tests/test_reporting_writer.py --noconftest
|
| 5 |
+
|
| 6 |
+
Three layers, none touching Snowflake or Supabase:
|
| 7 |
+
1. the rule (boone 2026-10-26): real Salesforce id AND environment prod;
|
| 8 |
+
2. the rows and statements, against a fake cursor — the same semantics as the
|
| 9 |
+
hand-run loader (dev_notes/reporting_load_runs.py.txt): ledger lookup,
|
| 10 |
+
COALESCE on the opportunity id, sticky IS_TEST_RUN, the derived model URL;
|
| 11 |
+
3. the server hook: it never runs for a non-production run, runs on its own
|
| 12 |
+
thread for a production run, and a failing writer never escapes.
|
| 13 |
+
"""
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
import os
|
| 17 |
+
import threading
|
| 18 |
+
|
| 19 |
+
os.environ.setdefault("MCP_TRANSPORT", "stdio")
|
| 20 |
+
|
| 21 |
+
import pytest
|
| 22 |
+
|
| 23 |
+
from demoprep_app.reporting import writer as W
|
| 24 |
+
|
| 25 |
+
REAL_18 = "006Hs00000AbCdEfQA" # well-formed: 18 alphanumerics
|
| 26 |
+
REAL_15 = "006Hs00000AbCdE" # well-formed: 15 alphanumerics
|
| 27 |
+
MIKE = "mike.boone@thoughtspot.com"
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
# --- 1. the rule ---------------------------------------------------------------- #
|
| 31 |
+
@pytest.mark.parametrize("opportunity, environment, expected", [
|
| 32 |
+
(REAL_18, "prod", True),
|
| 33 |
+
(REAL_15, "prod", True),
|
| 34 |
+
(REAL_18, "production", True),
|
| 35 |
+
(REAL_18, " Prod ", True),
|
| 36 |
+
(REAL_18, "staging", False),
|
| 37 |
+
(REAL_18, "stage", False),
|
| 38 |
+
("TEST-NOT-A-DEAL", "prod", False),
|
| 39 |
+
("REPLAY-NOT-A-DEAL-ACME", "prod", False),
|
| 40 |
+
("XXTEST000000000001", "prod", False),
|
| 41 |
+
("xxtest000000000001", "prod", False), # the marker is case-insensitive
|
| 42 |
+
("", "prod", False),
|
| 43 |
+
(None, "prod", False),
|
| 44 |
+
(" ", "prod", False),
|
| 45 |
+
("006Hs00000", "prod", False), # wrong length
|
| 46 |
+
("006Hs00000AbCdEfQA1", "prod", False), # 19 characters
|
| 47 |
+
("006Hs00000-bCdEfQA", "prod", False), # not alphanumeric
|
| 48 |
+
(REAL_18, "", False),
|
| 49 |
+
(REAL_18, None, False),
|
| 50 |
+
(REAL_18, "dev", False),
|
| 51 |
+
])
|
| 52 |
+
def test_is_production_run(opportunity, environment, expected):
|
| 53 |
+
assert W.is_production_run(opportunity, environment) is expected
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def test_normalize_environment_matches_the_tool_synonyms():
|
| 57 |
+
assert [W.normalize_environment(v) for v in ("prod", "PRODUCTION", "staging", "Stage", "qa", "")] == \
|
| 58 |
+
["prod", "prod", "staging", "staging", "", ""]
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def test_opportunity_id_is_the_loaders_opp_id():
|
| 62 |
+
assert W.opportunity_id(f" {REAL_18} ") == REAL_18
|
| 63 |
+
assert W.opportunity_id("NONE") is None
|
| 64 |
+
assert W.opportunity_id("") is None
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
@pytest.mark.parametrize("owner, company, reason", [
|
| 68 |
+
("", "Acme", "no owner"),
|
| 69 |
+
("testrunner@thoughtspot.com", "Acme", "testrunner owner"),
|
| 70 |
+
("no.such.person.42@thoughtspot.com", "Acme", "no.such.person owner"),
|
| 71 |
+
("sam@thoughtspot.com", "Acme IGNORE", "IGNORE company"),
|
| 72 |
+
("sam@thoughtspot.com", "Northwind Retail", "internal company"),
|
| 73 |
+
("sam@thoughtspot.com", "Acme", None),
|
| 74 |
+
(MIKE, "Acme", None),
|
| 75 |
+
])
|
| 76 |
+
def test_skip_reason_is_the_loaders_filter(owner, company, reason):
|
| 77 |
+
assert W.skip_reason(owner, company) == reason
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
# --- 2. the rows ------------------------------------------------------------------ #
|
| 81 |
+
def _intake(owner=MIKE, company="Acme Logistics", opp=REAL_18, space="thoughtspot-demoprep/mcp",
|
| 82 |
+
ts_url=""):
|
| 83 |
+
return {
|
| 84 |
+
"id": "a76030cc-b070-4653-9264-0a904eed2dae",
|
| 85 |
+
"created_at": "2026-10-27T00:07:22.393086+00:00",
|
| 86 |
+
"company_name": company,
|
| 87 |
+
"use_case": "Fleet utilisation analytics",
|
| 88 |
+
"results": {
|
| 89 |
+
"run_id": "4de504402486", "owner_email": owner, "ts_url": ts_url,
|
| 90 |
+
"salesforce_opportunity_id": opp, "environment": "prod",
|
| 91 |
+
"space_id": space, "code": "0bcf58064368", "brief": "...",
|
| 92 |
+
},
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def _result(status="success", errors=(), model_url="", ts="https://se-thoughtspot-cloud.thoughtspot.cloud"):
|
| 97 |
+
return {
|
| 98 |
+
"run_id": "4de504402486", "build_status": status, "owner_email": MIKE,
|
| 99 |
+
"ts_url": ts, "schema": "ACME_LOGISTICS_FLEET_20261027",
|
| 100 |
+
"model_guid": "11111111-2222-3333-4444-555555555555",
|
| 101 |
+
"liveboard_guid": "66666666-7777-8888-9999-000000000000",
|
| 102 |
+
"model_url": model_url,
|
| 103 |
+
"liveboard_url": f"{ts}/#/pinboard/66666666-7777-8888-9999-000000000000",
|
| 104 |
+
"portal_url": " https://thoughtspot-demoprep-portals.static.hf.space/acme/ ",
|
| 105 |
+
"errors": list(errors), "elapsed_seconds": 912.4,
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def test_shape_rows_mirrors_the_loader_for_a_success():
|
| 110 |
+
run, res = W.shape_rows(_intake(), _result(), "2026-10-27T00:22:40+00:00")
|
| 111 |
+
assert run == {"INTAKE_ID": "a76030cc-b070-4653-9264-0a904eed2dae", "RUN_ID": "4de504402486",
|
| 112 |
+
"SFDC": REAL_18, "EMAIL": MIKE, "PROSPECT": "Acme Logistics"}
|
| 113 |
+
assert res["INTAKE_ID"] == run["INTAKE_ID"]
|
| 114 |
+
assert res["USE_CASE"] == "Fleet utilisation analytics"
|
| 115 |
+
assert res["REQUESTED_AT"] == "2026-10-27T00:07:22.393086+00:00"
|
| 116 |
+
assert res["COMPLETED_AT"] == "2026-10-27T00:22:40+00:00"
|
| 117 |
+
assert res["ELAPSED"] == 912.4
|
| 118 |
+
assert res["STATUS"] == "success" and res["ERR_CAT"] is None and res["ERR_TXT"] is None
|
| 119 |
+
assert res["SPACE"] == "prod" and res["CLUSTER_NAME"] == "se-thoughtspot-cloud"
|
| 120 |
+
assert res["IS_TEST"] is True # Mike's runs are test runs
|
| 121 |
+
assert res["LB_GUID"] == "66666666-7777-8888-9999-000000000000"
|
| 122 |
+
assert res["LB_URL"].endswith("/#/pinboard/66666666-7777-8888-9999-000000000000")
|
| 123 |
+
assert res["MODEL_GUID"] == "11111111-2222-3333-4444-555555555555"
|
| 124 |
+
# No model_url in the result: derived from the resolved ts_url + model guid, as the loader does.
|
| 125 |
+
assert res["MODEL_URL"] == ("https://se-thoughtspot-cloud.thoughtspot.cloud"
|
| 126 |
+
"/#/data/tables/11111111-2222-3333-4444-555555555555")
|
| 127 |
+
assert res["PORTAL_URL"] == "https://thoughtspot-demoprep-portals.static.hf.space/acme/" # stripped
|
| 128 |
+
assert res["SCHEMA_NAME"] == "ACME_LOGISTICS_FLEET_20261027"
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def test_shape_rows_for_another_se_is_not_a_test_run_and_keeps_its_own_cluster():
|
| 132 |
+
run, res = W.shape_rows(_intake(owner="Sam.Jones@thoughtspot.com", space="thoughtspot-demoprep/test-mcp"),
|
| 133 |
+
_result(ts="https://sebe.thoughtspot.cloud"), "2026-10-27T00:22:40+00:00")
|
| 134 |
+
assert run["EMAIL"] == "sam.jones@thoughtspot.com" # lower-cased, as the loader stores it
|
| 135 |
+
assert res["IS_TEST"] is False
|
| 136 |
+
assert res["SPACE"] == "test" and res["CLUSTER_NAME"] == "sebe"
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def test_shape_rows_for_a_failure_buckets_the_first_error():
|
| 140 |
+
_, res = W.shape_rows(_intake(), _result(status="failed", errors=["LLM call timed out after 600s", "x"]),
|
| 141 |
+
"2026-10-27T00:22:40+00:00")
|
| 142 |
+
assert res["STATUS"] == "failed"
|
| 143 |
+
assert res["ERR_CAT"] == "llm_timeout"
|
| 144 |
+
assert res["ERR_TXT"] == "LLM call timed out after 600s"
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def test_shape_rows_with_no_result_record_is_no_result():
|
| 148 |
+
_, res = W.shape_rows(_intake(), {}, "2026-10-27T00:22:40+00:00")
|
| 149 |
+
assert res["STATUS"] == "no_result" and res["COMPLETED_AT"] is None and res["MODEL_URL"] is None
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def test_shape_rows_rejects_a_malformed_opportunity_id_as_null_never_truncated():
|
| 153 |
+
run, _ = W.shape_rows(_intake(opp="006Hs00000AbCdEfQA-EXTRA"), _result(), None)
|
| 154 |
+
assert run["SFDC"] is None
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
# --- the statements, against a fake cursor -------------------------------------- #
|
| 158 |
+
class FakeCursor:
|
| 159 |
+
"""Records every statement; answers the two SELECTs from `answers`."""
|
| 160 |
+
|
| 161 |
+
def __init__(self, ledger_key=None, inserted_key=777):
|
| 162 |
+
self.calls: list[tuple[str, tuple]] = []
|
| 163 |
+
self._ledger_key = ledger_key
|
| 164 |
+
self._inserted_key = inserted_key
|
| 165 |
+
self._next = None
|
| 166 |
+
self.closed = False
|
| 167 |
+
|
| 168 |
+
def execute(self, sql, params=None):
|
| 169 |
+
self.calls.append((sql, tuple(params or ())))
|
| 170 |
+
if sql == W.SQL_LEDGER_LOOKUP:
|
| 171 |
+
self._next = (self._ledger_key,) if self._ledger_key is not None else None
|
| 172 |
+
elif sql == W.SQL_RUN_KEY_OF_INSERT:
|
| 173 |
+
self._next = (self._inserted_key,)
|
| 174 |
+
else:
|
| 175 |
+
self._next = None
|
| 176 |
+
|
| 177 |
+
def fetchone(self):
|
| 178 |
+
return self._next
|
| 179 |
+
|
| 180 |
+
def close(self):
|
| 181 |
+
self.closed = True
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
def _rows():
|
| 185 |
+
return W.shape_rows(_intake(), _result(), "2026-10-27T00:22:40+00:00")
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
def test_a_new_request_is_inserted_then_ledgered_then_merged():
|
| 189 |
+
cur = FakeCursor(ledger_key=None, inserted_key=5131)
|
| 190 |
+
run, res = _rows()
|
| 191 |
+
out = W.write_run(cur, run, res)
|
| 192 |
+
assert out == {"demo_run_key": 5131, "action": "inserted"}
|
| 193 |
+
sqls = [c[0] for c in cur.calls]
|
| 194 |
+
assert sqls == [W.SQL_LEDGER_LOOKUP, W.SQL_RUN_INSERT, W.SQL_RUN_KEY_OF_INSERT,
|
| 195 |
+
W.SQL_LEDGER_INSERT, W.SQL_RESULTS_MERGE]
|
| 196 |
+
assert cur.calls[0][1] == (run["INTAKE_ID"],)
|
| 197 |
+
assert cur.calls[1][1] == ("4de504402486", REAL_18, MIKE, "Acme Logistics")
|
| 198 |
+
assert cur.calls[3][1] == (run["INTAKE_ID"], 5131) # the ledger row the loader will find
|
| 199 |
+
merge_params = cur.calls[4][1]
|
| 200 |
+
assert merge_params[0] == 5131
|
| 201 |
+
assert merge_params[1:] == tuple(res[k] for k in W._RES_ORDER)
|
| 202 |
+
assert len(merge_params) == 1 + 16 == W.SQL_RESULTS_MERGE.count("%s")
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
def test_a_request_the_ledger_knows_is_updated_not_inserted():
|
| 206 |
+
cur = FakeCursor(ledger_key=4242)
|
| 207 |
+
run, res = _rows()
|
| 208 |
+
out = W.write_run(cur, run, res)
|
| 209 |
+
assert out == {"demo_run_key": 4242, "action": "updated"}
|
| 210 |
+
sqls = [c[0] for c in cur.calls]
|
| 211 |
+
assert sqls == [W.SQL_LEDGER_LOOKUP, W.SQL_RUN_UPDATE, W.SQL_RESULTS_MERGE]
|
| 212 |
+
assert cur.calls[1][1] == ("4de504402486", REAL_18, MIKE, "Acme Logistics", 4242)
|
| 213 |
+
assert cur.calls[2][1][0] == 4242
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
def test_the_statements_keep_the_loaders_rules():
|
| 217 |
+
# a human-entered opportunity id is never overwritten with NULL
|
| 218 |
+
assert "SALESFORCE_OPPORTUNITY_ID=COALESCE(SALESFORCE_OPPORTUNITY_ID,%s)" in W.SQL_RUN_UPDATE
|
| 219 |
+
# IS_TEST_RUN is sticky; artifact links are COALESCEd; both branches of the MERGE exist
|
| 220 |
+
m = W.SQL_RESULTS_MERGE
|
| 221 |
+
assert "IS_TEST_RUN=(t.IS_TEST_RUN OR s.IS_TEST)" in m
|
| 222 |
+
for col in ("LIVEBOARD_GUID", "LIVEBOARD_URL", "MODEL_GUID", "MODEL_URL", "PORTAL_URL", "SNOWFLAKE_SCHEMA"):
|
| 223 |
+
assert f"{col}=COALESCE(" in m, col
|
| 224 |
+
assert "WHEN MATCHED THEN UPDATE" in m and "WHEN NOT MATCHED THEN INSERT" in m
|
| 225 |
+
# every statement names the reporting schema outright — no USE SCHEMA, no demo-database guess
|
| 226 |
+
for sql in (W.SQL_LEDGER_LOOKUP, W.SQL_RUN_UPDATE, W.SQL_RUN_INSERT, W.SQL_RUN_KEY_OF_INSERT,
|
| 227 |
+
W.SQL_LEDGER_INSERT, W.SQL_RESULTS_MERGE):
|
| 228 |
+
assert "DEMOPREP_REPORTING.REPORTING." in sql
|
| 229 |
+
# the ledger is keyed by the intake row, the FK the loader joins on
|
| 230 |
+
assert "SOURCE_INTAKE_ID" in W.SQL_LEDGER_LOOKUP and "SOURCE_INTAKE_ID" in W.SQL_LEDGER_INSERT
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
def test_an_insert_that_cannot_be_keyed_is_an_error_not_a_silent_half_row():
|
| 234 |
+
cur = FakeCursor(ledger_key=None, inserted_key=None)
|
| 235 |
+
run, res = _rows()
|
| 236 |
+
with pytest.raises(RuntimeError):
|
| 237 |
+
W.write_run(cur, run, res)
|
| 238 |
+
assert W.SQL_LEDGER_INSERT not in [c[0] for c in cur.calls]
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
# --- write_completed_build: the orchestration --------------------------------- #
|
| 242 |
+
class FakeConn:
|
| 243 |
+
def __init__(self, cur, fail=False):
|
| 244 |
+
self.cur, self.fail = cur, fail
|
| 245 |
+
self.events: list[str] = []
|
| 246 |
+
|
| 247 |
+
def autocommit(self, flag):
|
| 248 |
+
self.events.append(f"autocommit={flag}")
|
| 249 |
+
|
| 250 |
+
def cursor(self):
|
| 251 |
+
if self.fail:
|
| 252 |
+
raise RuntimeError("boom: Snowflake unreachable")
|
| 253 |
+
return self.cur
|
| 254 |
+
|
| 255 |
+
def commit(self):
|
| 256 |
+
self.events.append("commit")
|
| 257 |
+
|
| 258 |
+
def rollback(self):
|
| 259 |
+
self.events.append("rollback")
|
| 260 |
+
|
| 261 |
+
def close(self):
|
| 262 |
+
self.events.append("close")
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
def _payload():
|
| 266 |
+
return {"run_id": "4de504402486", "status": "success", "owner_email": MIKE,
|
| 267 |
+
"ts_url": "https://se-thoughtspot-cloud.thoughtspot.cloud",
|
| 268 |
+
"schema": "ACME", "model_guid": "m", "liveboard_guid": "l",
|
| 269 |
+
"model_url": "mu", "liveboard_url": "lu", "portal_url": "pu",
|
| 270 |
+
"errors": [], "elapsed_seconds": 10.0}
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
def test_a_production_run_is_written_and_committed():
|
| 274 |
+
cur = FakeCursor(ledger_key=None, inserted_key=5131)
|
| 275 |
+
conn = FakeConn(cur)
|
| 276 |
+
calls = {"connect": 0}
|
| 277 |
+
|
| 278 |
+
def connect():
|
| 279 |
+
calls["connect"] += 1
|
| 280 |
+
return conn
|
| 281 |
+
|
| 282 |
+
out = W.write_completed_build("4de504402486", _payload(), opportunity=REAL_18, environment="prod",
|
| 283 |
+
completed_at="2026-10-27T00:22:40+00:00",
|
| 284 |
+
connect=connect, fetch_intake=lambda rid: _intake(),
|
| 285 |
+
result_record=lambda p: _result())
|
| 286 |
+
assert out == {"status": "written", "intake_id": "a76030cc-b070-4653-9264-0a904eed2dae",
|
| 287 |
+
"demo_run_key": 5131, "action": "inserted"}
|
| 288 |
+
assert calls["connect"] == 1
|
| 289 |
+
assert conn.events == ["autocommit=False", "commit", "close"] and cur.closed
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
def test_a_staging_run_never_opens_a_connection():
|
| 293 |
+
out = W.write_completed_build("4de504402486", _payload(), opportunity=REAL_18, environment="staging",
|
| 294 |
+
connect=lambda: pytest.fail("connected for a staging run"),
|
| 295 |
+
fetch_intake=lambda rid: pytest.fail("looked up a staging run"))
|
| 296 |
+
assert out["status"] == "skipped" and out["reason"] == "not a production run"
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
def test_a_test_marked_run_never_opens_a_connection():
|
| 300 |
+
out = W.write_completed_build("4de504402486", _payload(), opportunity="TEST-NOT-A-DEAL", environment="prod",
|
| 301 |
+
connect=lambda: pytest.fail("connected for a test run"),
|
| 302 |
+
fetch_intake=lambda rid: pytest.fail("looked up a test run"))
|
| 303 |
+
assert out["status"] == "skipped"
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
def test_a_run_without_an_intake_row_is_skipped_like_the_loader_would():
|
| 307 |
+
out = W.write_completed_build("4de504402486", _payload(), opportunity=REAL_18, environment="prod",
|
| 308 |
+
connect=lambda: pytest.fail("connected with no intake row"),
|
| 309 |
+
fetch_intake=lambda rid: None)
|
| 310 |
+
assert out == {"status": "skipped", "reason": "no intake row in demo_history"}
|
| 311 |
+
|
| 312 |
+
|
| 313 |
+
def test_a_run_the_loader_would_skip_is_skipped_here_too():
|
| 314 |
+
out = W.write_completed_build("4de504402486", _payload(), opportunity=REAL_18, environment="prod",
|
| 315 |
+
connect=lambda: pytest.fail("connected for a testrunner run"),
|
| 316 |
+
fetch_intake=lambda rid: _intake(owner="testrunner@thoughtspot.com"))
|
| 317 |
+
assert out == {"status": "skipped", "reason": "testrunner owner"}
|
| 318 |
+
|
| 319 |
+
|
| 320 |
+
def test_a_snowflake_failure_is_reported_rolled_back_and_never_raised():
|
| 321 |
+
conn = FakeConn(FakeCursor(), fail=True)
|
| 322 |
+
out = W.write_completed_build("4de504402486", _payload(), opportunity=REAL_18, environment="prod",
|
| 323 |
+
connect=lambda: conn, fetch_intake=lambda rid: _intake(),
|
| 324 |
+
result_record=lambda p: _result())
|
| 325 |
+
assert out["status"] == "failed" and "Snowflake unreachable" in out["error"]
|
| 326 |
+
assert conn.events == ["autocommit=False", "rollback", "close"]
|
| 327 |
+
|
| 328 |
+
|
| 329 |
+
def test_a_connect_failure_is_reported_never_raised():
|
| 330 |
+
def connect():
|
| 331 |
+
raise ConnectionError("no route to Snowflake")
|
| 332 |
+
out = W.write_completed_build("4de504402486", _payload(), opportunity=REAL_18, environment="prod",
|
| 333 |
+
connect=connect, fetch_intake=lambda rid: _intake(),
|
| 334 |
+
result_record=lambda p: _result())
|
| 335 |
+
assert out == {"status": "failed", "error": "ConnectionError: no route to Snowflake"}
|
| 336 |
+
|
| 337 |
+
|
| 338 |
+
# --- 3. the server hook ------------------------------------------------------------ #
|
| 339 |
+
def test_the_hook_skips_a_non_production_run_without_a_thread(monkeypatch):
|
| 340 |
+
import mcp_server as M
|
| 341 |
+
monkeypatch.setattr(W, "write_completed_build",
|
| 342 |
+
lambda *a, **k: pytest.fail("the writer ran for a staging run"))
|
| 343 |
+
lines = []
|
| 344 |
+
monkeypatch.setattr(M, "_log_hit", lines.append)
|
| 345 |
+
t = M._start_reporting_write_through("abc", {"run_id": "abc"}, opportunity=REAL_18, environment="staging")
|
| 346 |
+
assert t is None
|
| 347 |
+
assert any("REPORTING_SKIP run_id=abc" in l for l in lines)
|
| 348 |
+
|
| 349 |
+
|
| 350 |
+
def test_the_hook_writes_a_production_run_on_its_own_thread(monkeypatch):
|
| 351 |
+
import mcp_server as M
|
| 352 |
+
seen = {}
|
| 353 |
+
|
| 354 |
+
def fake_write(run_id, payload, *, opportunity, environment):
|
| 355 |
+
seen.update(run_id=run_id, payload=payload, opportunity=opportunity, environment=environment,
|
| 356 |
+
thread=threading.current_thread().name)
|
| 357 |
+
return {"status": "written", "demo_run_key": 5131, "action": "inserted"}
|
| 358 |
+
|
| 359 |
+
monkeypatch.setattr(W, "write_completed_build", fake_write)
|
| 360 |
+
lines = []
|
| 361 |
+
monkeypatch.setattr(M, "_log_hit", lines.append)
|
| 362 |
+
t = M._start_reporting_write_through("abc", {"run_id": "abc", "status": "success"},
|
| 363 |
+
opportunity=REAL_18, environment="prod")
|
| 364 |
+
assert t is not None
|
| 365 |
+
t.join(5)
|
| 366 |
+
assert not t.is_alive()
|
| 367 |
+
assert seen["run_id"] == "abc" and seen["opportunity"] == REAL_18 and seen["environment"] == "prod"
|
| 368 |
+
assert seen["payload"] == {"run_id": "abc", "status": "success"}
|
| 369 |
+
assert seen["thread"] == "reporting-abc" and seen["thread"] != threading.current_thread().name
|
| 370 |
+
assert any("REPORTING_WRITE run_id=abc" in l and "'demo_run_key': 5131" in l for l in lines)
|
| 371 |
+
|
| 372 |
+
|
| 373 |
+
def test_a_writer_that_raises_never_escapes_the_hook(monkeypatch):
|
| 374 |
+
import mcp_server as M
|
| 375 |
+
|
| 376 |
+
def exploding(*a, **k):
|
| 377 |
+
raise RuntimeError("writer exploded")
|
| 378 |
+
|
| 379 |
+
monkeypatch.setattr(W, "write_completed_build", exploding)
|
| 380 |
+
lines = []
|
| 381 |
+
monkeypatch.setattr(M, "_log_hit", lines.append)
|
| 382 |
+
t = M._start_reporting_write_through("abc", {"run_id": "abc"}, opportunity=REAL_18, environment="prod")
|
| 383 |
+
t.join(5)
|
| 384 |
+
assert not t.is_alive()
|
| 385 |
+
assert any("REPORTING_WRITE run_id=abc" in l and "writer exploded" in l for l in lines)
|
| 386 |
+
|
| 387 |
+
|
| 388 |
+
def test_the_result_record_is_the_one_log_mcp_result_stores():
|
| 389 |
+
"""The write-through reads supabase_client.mcp_result_record; the stored row's
|
| 390 |
+
`results` must be that same dict, or the loader would later disagree with it."""
|
| 391 |
+
import supabase_client as SC
|
| 392 |
+
from tests.test_payload_reaches_the_database import captured_result_row
|
| 393 |
+
payload = {**_payload(), "company_name": "Acme Logistics", "use_case": "Fleet", "warnings": ["w"],
|
| 394 |
+
"findings": [{"code": "F1"}], "custom_mcp_demo_section": "# demo"}
|
| 395 |
+
assert captured_result_row(payload) == SC.mcp_result_record(payload)
|
| 396 |
+
assert SC.mcp_result_record(payload)["build_status"] == "success"
|
| 397 |
+
assert SC.mcp_result_record(payload)["custom_mcp_demo_section"] == "# demo"
|
| 398 |
+
assert "custom_mcp_demo_section" not in SC.mcp_result_record(_payload())
|