mikeboone Claude Sonnet 4.6 commited on
Commit
204862d
Β·
1 Parent(s): e6ee6c5

fix: empty table detection + test suite robustness

Browse files

Empty tables (Wells Fargo, Best Buy):
- Extend empty-table check to ALL tables, not just fact tables
- is_fact_table heuristic (>=2 FKs) fails when DDL has no FOREIGN KEY
constraints β€” fact_table_names was empty so the check never triggered
- Now checks all non-date tables; 0-row dim table triggers retry
- Final check also covers dim tables: raises RuntimeError if still empty
- Add Step 3b generation summary (per-table row counts before write)
- Log schema name in populate log_end so fetch_run_diagnostics can find it
- Improved insert_rows logging when filtered_columns is empty

Test suite fixes:
- Liveboard Name selector: try textarea/input/generic aria-label variants
- Custom tab Context selector: 1.5s wait after vertical change + try
placeholder and aria-label selectors with visibility check
- Tag/share checks: guard against empty API response body before .json()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Files changed (2) hide show
  1. legitdata_bridge.py +38 -7
  2. tests/e2e_quality.py +162 -74
legitdata_bridge.py CHANGED
@@ -197,7 +197,14 @@ class KeyPairSnowflakeWriter:
197
  filtered_indices.append(i)
198
 
199
  if not filtered_columns:
200
- print(f"Warning: No columns with values to insert for {table_name}")
 
 
 
 
 
 
 
201
  return 0
202
 
203
  col_list = ', '.join(filtered_columns)
@@ -435,6 +442,11 @@ class DemoLegitGenerator:
435
  # Register PK values for FK references
436
  self._gen._register_pk_values(table, rows)
437
 
 
 
 
 
 
438
  print("\n=== Step 4: Writing to Database ===")
439
  results = self._write_to_database(generated_data, truncate_first)
440
 
@@ -640,11 +652,25 @@ def populate_demo_data(
640
  if _slog:
641
  _slog.log_verbose("populate", "first attempt failed", error=first_attempt_error)
642
 
643
- # Check all tables for 0 rows even if no exception
 
 
 
644
  fact_table_names = {t.name for t in schema.fact_tables}
 
 
 
 
645
  empty_facts = [t for t in fact_table_names if results.get(t, 0) == 0]
646
- if not first_attempt_error and empty_facts:
647
- first_attempt_error = f"Fact table(s) empty after first attempt: {', '.join(empty_facts)}"
 
 
 
 
 
 
 
648
  log(f"⚠️ {first_attempt_error}")
649
 
650
  # Attempt 2 if anything failed
@@ -667,9 +693,12 @@ def populate_demo_data(
667
  log(f" {status} {table_name}: {count:,} rows")
668
 
669
  empty_facts_final = [t for t in fact_table_names if results.get(t, 0) == 0]
670
- if empty_facts_final:
 
 
671
  raise RuntimeError(
672
- f"Fact table(s) still empty after 2 attempts: {', '.join(empty_facts_final)}. "
 
673
  f"First attempt error: {first_attempt_error}"
674
  )
675
 
@@ -688,7 +717,9 @@ Data contextually generated for:
688
  """
689
  log(message)
690
  if _slog and _t_populate is not None:
691
- _slog.log_end("populate", _t_populate, tables=len(results), total_rows=total_rows)
 
 
692
  return True, message, results
693
 
694
  except Exception as e:
 
197
  filtered_indices.append(i)
198
 
199
  if not filtered_columns:
200
+ # Log the first row so we can see what the generator produced
201
+ if rows:
202
+ sample = dict(zip(columns, rows[0]))
203
+ print(f"ERROR: No columns with non-None values to insert for {table_name}")
204
+ print(f" columns expected: {columns[:8]}")
205
+ print(f" first row sample: {str(sample)[:300]}")
206
+ else:
207
+ print(f"Warning: No rows to insert for {table_name}")
208
  return 0
209
 
210
  col_list = ', '.join(filtered_columns)
 
442
  # Register PK values for FK references
443
  self._gen._register_pk_values(table, rows)
444
 
445
+ print("\n=== Step 3b: Generation Summary ===")
446
+ for tname, trows in generated_data.items():
447
+ status = "βœ…" if trows else "❌ EMPTY"
448
+ print(f" {status} {tname}: {len(trows)} rows in memory")
449
+
450
  print("\n=== Step 4: Writing to Database ===")
451
  results = self._write_to_database(generated_data, truncate_first)
452
 
 
652
  if _slog:
653
  _slog.log_verbose("populate", "first attempt failed", error=first_attempt_error)
654
 
655
+ # Check all tables for 0 rows even if no exception.
656
+ # NOTE: is_fact_table uses a FK-count heuristic; if DDL has no explicit FOREIGN KEY
657
+ # constraints (common in Snowflake schemas), ALL tables look like dim tables and
658
+ # fact_table_names is empty β€” so we must also check dim tables.
659
  fact_table_names = {t.name for t in schema.fact_tables}
660
+ all_table_names = {t.name for t in schema.tables}
661
+ date_table_names = {t.name for t in schema.tables
662
+ if t.name.upper() in ('DATES', 'DATE', 'DATE_DIM', 'DIM_DATE', 'CALENDAR')}
663
+
664
  empty_facts = [t for t in fact_table_names if results.get(t, 0) == 0]
665
+ # Also catch dim tables (excluding date tables, which may legitimately be smaller)
666
+ empty_dims = [t for t in (all_table_names - fact_table_names - date_table_names)
667
+ if results.get(t, 0) == 0]
668
+
669
+ if not first_attempt_error and (empty_facts or empty_dims):
670
+ first_attempt_error = (
671
+ f"Empty table(s) after first attempt β€” "
672
+ f"facts: {empty_facts or 'none'}, dims: {empty_dims or 'none'}"
673
+ )
674
  log(f"⚠️ {first_attempt_error}")
675
 
676
  # Attempt 2 if anything failed
 
693
  log(f" {status} {table_name}: {count:,} rows")
694
 
695
  empty_facts_final = [t for t in fact_table_names if results.get(t, 0) == 0]
696
+ empty_dims_final = [t for t in (all_table_names - fact_table_names - date_table_names)
697
+ if results.get(t, 0) == 0]
698
+ if empty_facts_final or empty_dims_final:
699
  raise RuntimeError(
700
+ f"Empty table(s) still present after 2 attempts β€” "
701
+ f"facts: {empty_facts_final or 'none'}, dims: {empty_dims_final or 'none'}. "
702
  f"First attempt error: {first_attempt_error}"
703
  )
704
 
 
717
  """
718
  log(message)
719
  if _slog and _t_populate is not None:
720
+ _slog.log_end("populate", _t_populate,
721
+ tables=len(results), total_rows=total_rows,
722
+ schema=schema_name)
723
  return True, message, results
724
 
725
  except Exception as e:
tests/e2e_quality.py CHANGED
@@ -95,9 +95,14 @@ def _parse_json(text: str) -> dict:
95
  # ---------------------------------------------------------------------------
96
  # Test case generation
97
  # ---------------------------------------------------------------------------
98
- def generate_ai_test_case(config: dict) -> dict:
99
  """AI picks vertical, line, function, and a matching company."""
100
  prompt = config["ai_generated"]["generation_prompt"]
 
 
 
 
 
101
  data = _parse_json(_llm(prompt))
102
  return {
103
  "name": f"AI: {data['company']} β€” {data['vertical']} / {data['line']} / {data['function']}",
@@ -192,10 +197,9 @@ def build_test_suite(config: dict) -> list:
192
  ai_count = config["ai_generated"].get("count", 2)
193
  for i in range(ai_count):
194
  try:
195
- tc = generate_ai_test_case(config)
196
- # Deduplicate: if this company was already picked, regenerate once
197
  if tc["company"] in used_companies:
198
- tc = generate_ai_test_case(config)
199
  used_companies.append(tc["company"])
200
  suite.append(tc)
201
  except Exception as e:
@@ -204,7 +208,9 @@ def build_test_suite(config: dict) -> list:
204
 
205
  for i in range(2):
206
  try:
207
- suite.append(generate_custom_test_case(config, used_companies))
 
 
208
  except Exception as e:
209
  print(f" ⚠️ Custom test case {i+1} failed ({e}), skipping")
210
 
@@ -280,6 +286,34 @@ def submit_job(page: Page, test_case: dict):
280
  page.get_by_role('tab', name='App', exact=True).click(timeout=10000)
281
  page.wait_for_timeout(1000)
282
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
283
  # Select TS environment first (required for deploy)
284
  select_gradio_dropdown(page, "TS Environment", test_case.get("ts_environment", "secloud - primary"))
285
 
@@ -287,8 +321,23 @@ def submit_job(page: Page, test_case: dict):
287
  select_gradio_dropdown(page, "Vertical", test_case["vertical"])
288
 
289
  if test_case["vertical"] == "* CUSTOM *":
290
- # Custom mode: fill the Context textarea, skip Line/Function
291
- ctx_el = page.locator('textarea[aria-label="Context"]').first
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
  ctx_el.click(click_count=3, timeout=5000)
293
  ctx_el.fill(test_case.get("context", ""))
294
  page.wait_for_timeout(300)
@@ -302,17 +351,6 @@ def submit_job(page: Page, test_case: dict):
302
  url_el.fill(test_case["company_url"])
303
  page.wait_for_timeout(300)
304
 
305
- # Set liveboard name so test runs are identifiable in ThoughtSpot
306
- lb_name = f"QA β€” {test_case['company']} {test_case['function']}"
307
- try:
308
- lb_el = page.locator('input[aria-label="Liveboard Name"]').first
309
- lb_el.click(click_count=3, timeout=3000)
310
- lb_el.fill(lb_name)
311
- page.wait_for_timeout(300)
312
- except Exception:
313
- lb_name += " (name not set)"
314
- print(f" ⚠️ Liveboard Name field not found β€” continuing without it")
315
-
316
  # Click GO
317
  page.click('button:has-text("β†’ GO")', timeout=10000)
318
  print(f" βœ… Form submitted: {test_case['vertical']} / {test_case['line']} / {test_case['function']} β€” {test_case['company_url']} | lb: {lb_name}")
@@ -439,18 +477,51 @@ def get_snowflake_sample(db: str, schema: str) -> str:
439
  cursor = conn.cursor()
440
  cursor.execute(f'SHOW TABLES IN SCHEMA "{db}"."{schema}"')
441
  tables = [row[1] for row in cursor.fetchall()]
442
- parts = []
443
- for table in tables[:6]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
444
  try:
445
  cursor.execute(f'SELECT * FROM "{db}"."{schema}"."{table}" LIMIT 30')
446
  cols = [d[0] for d in cursor.description]
447
  rows = cursor.fetchall()
448
- parts.append(f"\nTable: {table} ({len(rows)} sample rows)")
 
 
 
449
  parts.append(f"Columns: {', '.join(cols)}")
450
  for row in rows[:15]:
451
  parts.append(" " + str(dict(zip(cols, row))))
452
  except Exception as e:
453
  parts.append(f"\nTable: {table} β€” error: {e}")
 
454
  cursor.close()
455
  conn.close()
456
  return "\n".join(parts) if parts else "No tables found"
@@ -483,18 +554,23 @@ Analytics function: {function}
483
  The goal is a compelling demo with realistic data, outliers that drive a narrative,
484
  and a schema that supports the key KPIs for this use case.
485
 
486
- MODEL TML:
487
- {model_tml[:3000]}
488
 
489
- SNOWFLAKE SAMPLE DATA:
490
- {sample_data[:3000]}
491
 
492
- Grade 0–100:
493
- 1. REALISM (20 pts): Values look like real {company} data at scale.
494
- 2. STORY POTENTIAL (30 pts): Outliers and trends exist that would anchor a demo conversation.
495
- 3. TIME COVERAGE (20 pts): 12–24 months of history with meaningful trends.
496
- 4. SCHEMA FITNESS (15 pts): Star schema supports the right KPIs for {line} {function}.
497
- 5. COMPLETENESS (15 pts): Tables fully populated, dimensions have 20+ members.
 
 
 
 
 
498
 
499
  Return ONLY valid JSON:
500
  {{"score": 0, "reasoning": "...", "strengths": ["..."], "weaknesses": ["..."]}}"""
@@ -750,22 +826,25 @@ def verify_group1_settings(result: dict) -> dict:
750
  if expected_tag and ts_base and model_guid:
751
  try:
752
  session = ts_authenticate(ts_base)
753
- # v1 API: metadata/list returns tags per object
754
  resp = session.get(
755
  f"{ts_base}/tspublic/v1/metadata/list",
756
  params={"type": "LOGICAL_TABLE", "batchsize": 1,
757
  "offset": 0, "pattern": model_guid},
758
  )
759
- headers_data = resp.json().get("headers", [])
760
- obj_tags = []
761
- for h in headers_data:
762
- if h.get("id") == model_guid:
763
- obj_tags = [t.get("name", "") for t in (h.get("tags") or [])]
764
- break
765
- checks["tag_name"] = {
766
- "expected": expected_tag, "actual": obj_tags,
767
- "pass": expected_tag in obj_tags,
768
- }
 
 
 
 
769
  except Exception as e:
770
  checks["tag_name"] = {"pass": None, "note": f"tag check failed: {e}"}
771
 
@@ -779,15 +858,19 @@ def verify_group1_settings(result: dict) -> dict:
779
  json={"metadata": [{"type": "LOGICAL_TABLE", "identifier": model_guid}]},
780
  timeout=15,
781
  )
782
- perms = resp.json()
783
- principals = []
784
- for item in (perms if isinstance(perms, list) else []):
785
- for p in (item.get("permissions") or []):
786
- principals.append(p.get("principal", {}).get("name", ""))
787
- checks["share_with"] = {
788
- "expected": expected_share, "actual": principals,
789
- "pass": any(expected_share.lower() in p.lower() for p in principals),
790
- }
 
 
 
 
791
  except Exception as e:
792
  checks["share_with"] = {"pass": None, "note": f"share check failed: {e}"}
793
 
@@ -911,29 +994,32 @@ def fetch_run_diagnostics(start_time: float) -> dict:
911
  return {"found": False, "reason": f"Diagnostics query failed: {e}"}
912
 
913
 
914
- def check_snowflake_schema(company: str, start_time: float) -> dict:
915
  """
916
- Look for a Snowflake schema matching this company created around test time.
917
- Schema naming: {PREFIX}_{MMDDHHMI}_{RAND}_sch e.g. WEL_04280450_B10_sch
 
918
  """
919
  try:
920
  from snowflake_auth import get_snowflake_connection
921
  from datetime import datetime
922
 
923
- prefix = company[:3].upper()
924
- date_str = datetime.fromtimestamp(start_time).strftime("%m%d")
925
-
926
  conn = get_snowflake_connection()
927
  cursor = conn.cursor()
928
- cursor.execute("SHOW SCHEMAS IN DATABASE DEMOBUILD")
929
- all_schemas = [row[1] for row in cursor.fetchall()]
930
-
931
- matching = [s for s in all_schemas if s.startswith(prefix) and date_str in s]
932
- if not matching:
933
- cursor.close(); conn.close()
934
- return {"found": False, "prefix": prefix, "date": date_str}
935
 
936
- schema = sorted(matching)[-1] # most recent
 
 
 
 
 
 
 
 
 
 
 
 
937
  cursor.execute(f'SHOW TABLES IN SCHEMA DEMOBUILD."{schema}"')
938
  tables = cursor.fetchall()
939
 
@@ -1063,21 +1149,23 @@ def run_single_test(page: Page, test_case: dict, config: dict) -> dict:
1063
 
1064
  result["duration_seconds"] = round(time.time() - start)
1065
 
1066
- # Always check Snowflake row counts β€” included in JSON every run
1067
- sf = check_snowflake_schema(result["company"], start)
 
 
 
 
 
1068
  result["snowflake_check"] = sf
1069
  if sf.get("found"):
1070
  print(f" πŸ“¦ Snowflake: {sf['schema']} ({sf['total_rows']} rows, {len(sf['tables'])} tables)")
1071
  for t in sf["tables"]:
1072
  print(f" {t['table']}: {t['rows']} rows")
1073
  else:
1074
- print(f" πŸ“¦ Snowflake: no schema found (prefix={sf.get('prefix', '?')}, date={sf.get('date', '?')})")
1075
 
1076
- # Fetch Supabase diagnostics on timeout or error only
1077
  if result["timed_out"] or result["error"]:
1078
- print(" πŸ” Fetching failure diagnostics...")
1079
- diag = fetch_run_diagnostics(start)
1080
- result["diagnostics"] = diag
1081
  print_diagnostics(diag, {}) # sf already printed above
1082
 
1083
  # Stage scoring
@@ -1160,7 +1248,7 @@ def run_quality_suite(max_tests: int = None):
1160
  print("βœ… Logged in\n")
1161
 
1162
  for i, test_case in enumerate(suite, 1):
1163
- label = {"fixed": "πŸ”’", "random": "🎲", "ai_generated": "πŸ€–"}[test_case["type"]]
1164
  print(f"{'─'*62}")
1165
  print(f"[{i}/{len(suite)}] {label} {test_case['name']}")
1166
 
@@ -1207,7 +1295,7 @@ def run_quality_suite(max_tests: int = None):
1207
  print(f"\n{'='*62}")
1208
  print(f" COMPLETE β€” Avg: {avg}/100 Grade: {grade}")
1209
  for r in results:
1210
- label = {"fixed": "πŸ”’", "random": "🎲", "ai_generated": "πŸ€–"}[r["type"]]
1211
  ag = r["ai_grading"]
1212
  ds = f"{ag['data_score']}/100" if ag.get("data_score") is not None else "n/a"
1213
  ls = f"{ag['liveboard_score']}/100" if ag.get("liveboard_score") is not None else "n/a"
 
95
  # ---------------------------------------------------------------------------
96
  # Test case generation
97
  # ---------------------------------------------------------------------------
98
+ def generate_ai_test_case(config: dict, exclude_list: list = None) -> dict:
99
  """AI picks vertical, line, function, and a matching company."""
100
  prompt = config["ai_generated"]["generation_prompt"]
101
+ if exclude_list:
102
+ prompt = prompt.replace(
103
+ "Do NOT pick: Nike, Wells Fargo.",
104
+ f"Do NOT pick: {', '.join(exclude_list)}."
105
+ )
106
  data = _parse_json(_llm(prompt))
107
  return {
108
  "name": f"AI: {data['company']} β€” {data['vertical']} / {data['line']} / {data['function']}",
 
197
  ai_count = config["ai_generated"].get("count", 2)
198
  for i in range(ai_count):
199
  try:
200
+ tc = generate_ai_test_case(config, exclude_list=used_companies)
 
201
  if tc["company"] in used_companies:
202
+ tc = generate_ai_test_case(config, exclude_list=used_companies)
203
  used_companies.append(tc["company"])
204
  suite.append(tc)
205
  except Exception as e:
 
208
 
209
  for i in range(2):
210
  try:
211
+ tc = generate_custom_test_case(config, used_companies)
212
+ used_companies.append(tc["company"]) # prevent second custom from picking same
213
+ suite.append(tc)
214
  except Exception as e:
215
  print(f" ⚠️ Custom test case {i+1} failed ({e}), skipping")
216
 
 
286
  page.get_by_role('tab', name='App', exact=True).click(timeout=10000)
287
  page.wait_for_timeout(1000)
288
 
289
+ # Set liveboard name FIRST β€” right panel is independent of the form tabs
290
+ lb_name = f"QA β€” {test_case['company']} {test_case.get('function', 'Demo')}"
291
+ try:
292
+ # Gradio Textbox with label="Liveboard Name" β€” try multiple selectors
293
+ lb_el = None
294
+ for sel in [
295
+ 'textarea[aria-label="Liveboard Name"]',
296
+ 'input[aria-label="Liveboard Name"]',
297
+ '[aria-label="Liveboard Name"]',
298
+ ]:
299
+ try:
300
+ el = page.locator(sel).first
301
+ if el.is_visible(timeout=2000):
302
+ lb_el = el
303
+ break
304
+ except Exception:
305
+ pass
306
+ if lb_el:
307
+ lb_el.scroll_into_view_if_needed(timeout=3000)
308
+ lb_el.click(click_count=3, timeout=3000)
309
+ lb_el.fill(lb_name)
310
+ page.wait_for_timeout(300)
311
+ else:
312
+ raise Exception("No matching element found")
313
+ except Exception:
314
+ lb_name += " (name not set)"
315
+ print(f" ⚠️ Liveboard Name field not found β€” continuing without it")
316
+
317
  # Select TS environment first (required for deploy)
318
  select_gradio_dropdown(page, "TS Environment", test_case.get("ts_environment", "secloud - primary"))
319
 
 
321
  select_gradio_dropdown(page, "Vertical", test_case["vertical"])
322
 
323
  if test_case["vertical"] == "* CUSTOM *":
324
+ # Custom mode: wait for UI to settle after vertical dropdown change, then fill Context
325
+ page.wait_for_timeout(1500)
326
+ ctx_el = None
327
+ for sel in [
328
+ 'textarea[placeholder="Any extra context for the demo..."]',
329
+ 'textarea[aria-label="Context"]',
330
+ 'input[aria-label="Context"]',
331
+ ]:
332
+ try:
333
+ el = page.locator(sel).first
334
+ if el.is_visible(timeout=3000):
335
+ ctx_el = el
336
+ break
337
+ except Exception:
338
+ pass
339
+ if ctx_el is None:
340
+ raise Exception('Context textarea not found β€” tried placeholder and aria-label="Context"')
341
  ctx_el.click(click_count=3, timeout=5000)
342
  ctx_el.fill(test_case.get("context", ""))
343
  page.wait_for_timeout(300)
 
351
  url_el.fill(test_case["company_url"])
352
  page.wait_for_timeout(300)
353
 
 
 
 
 
 
 
 
 
 
 
 
354
  # Click GO
355
  page.click('button:has-text("β†’ GO")', timeout=10000)
356
  print(f" βœ… Form submitted: {test_case['vertical']} / {test_case['line']} / {test_case['function']} β€” {test_case['company_url']} | lb: {lb_name}")
 
477
  cursor = conn.cursor()
478
  cursor.execute(f'SHOW TABLES IN SCHEMA "{db}"."{schema}"')
479
  tables = [row[1] for row in cursor.fetchall()]
480
+
481
+ # Count rows in every table first β€” so we can prioritize the fact table
482
+ row_counts = {}
483
+ for table in tables:
484
+ try:
485
+ cursor.execute(f'SELECT COUNT(*) FROM "{db}"."{schema}"."{table}"')
486
+ row_counts[table] = cursor.fetchone()[0]
487
+ except Exception:
488
+ row_counts[table] = 0
489
+
490
+ # Sort descending β€” fact table (most rows) sampled first
491
+ tables_sorted = sorted(tables, key=lambda t: row_counts.get(t, 0), reverse=True)
492
+
493
+ # Build row-count summary header so grader knows what's populated
494
+ header = ["Table row counts:"]
495
+ for t in tables_sorted:
496
+ header.append(f" {t}: {row_counts.get(t, 0)} rows")
497
+ empty_tables = [t for t in tables_sorted if row_counts.get(t, 0) == 0]
498
+ if empty_tables:
499
+ header.append(
500
+ f"\n⚠️ WARNING: {len(empty_tables)} table(s) have 0 rows: "
501
+ f"{', '.join(empty_tables)}"
502
+ )
503
+
504
+ parts = ["\n".join(header)]
505
+
506
+ # Sample from tables that actually have data (up to 6); fall back to first 3 if all empty
507
+ tables_with_data = [t for t in tables_sorted if row_counts.get(t, 0) > 0]
508
+ to_sample = tables_with_data[:6] if tables_with_data else tables_sorted[:3]
509
+
510
+ for table in to_sample:
511
  try:
512
  cursor.execute(f'SELECT * FROM "{db}"."{schema}"."{table}" LIMIT 30')
513
  cols = [d[0] for d in cursor.description]
514
  rows = cursor.fetchall()
515
+ parts.append(
516
+ f"\nTable: {table} "
517
+ f"({row_counts.get(table, 0)} total rows, {len(rows)} sampled)"
518
+ )
519
  parts.append(f"Columns: {', '.join(cols)}")
520
  for row in rows[:15]:
521
  parts.append(" " + str(dict(zip(cols, row))))
522
  except Exception as e:
523
  parts.append(f"\nTable: {table} β€” error: {e}")
524
+
525
  cursor.close()
526
  conn.close()
527
  return "\n".join(parts) if parts else "No tables found"
 
554
  The goal is a compelling demo with realistic data, outliers that drive a narrative,
555
  and a schema that supports the key KPIs for this use case.
556
 
557
+ MODEL TML (full schema, column definitions, and relationships):
558
+ {model_tml[:7000]}
559
 
560
+ SNOWFLAKE DATA β€” actual row counts and sample rows:
561
+ {sample_data[:4000]}
562
 
563
+ Grade 0–100 using the actual data above. Do NOT hedge with phrases like "constrained by
564
+ partial TML" or "missing sample data" β€” the full TML and real row counts are provided.
565
+ Score based on what you can observe.
566
+
567
+ 1. REALISM (20 pts): Values look like real {company} data at realistic scale and ranges.
568
+ 2. STORY POTENTIAL (30 pts): Outliers, trends, or anomalies exist that anchor a demo narrative.
569
+ 3. TIME COVERAGE (20 pts): 12–24 months of history with meaningful trends over time.
570
+ 4. SCHEMA FITNESS (15 pts): Star schema design supports the key KPIs for {line} {function}.
571
+ 5. COMPLETENESS (15 pts): Tables are populated. Dimensions have 20+ distinct members.
572
+ RULE: If the row counts above show any key table at 0 rows, score COMPLETENESS = 0 for
573
+ that criteria. If the fact table is 0 rows, also deduct heavily from STORY POTENTIAL.
574
 
575
  Return ONLY valid JSON:
576
  {{"score": 0, "reasoning": "...", "strengths": ["..."], "weaknesses": ["..."]}}"""
 
826
  if expected_tag and ts_base and model_guid:
827
  try:
828
  session = ts_authenticate(ts_base)
 
829
  resp = session.get(
830
  f"{ts_base}/tspublic/v1/metadata/list",
831
  params={"type": "LOGICAL_TABLE", "batchsize": 1,
832
  "offset": 0, "pattern": model_guid},
833
  )
834
+ body = resp.text.strip()
835
+ if not body:
836
+ checks["tag_name"] = {"pass": None, "note": "tag check skipped: empty API response"}
837
+ else:
838
+ headers_data = resp.json().get("headers", [])
839
+ obj_tags = []
840
+ for h in headers_data:
841
+ if h.get("id") == model_guid:
842
+ obj_tags = [t.get("name", "") for t in (h.get("tags") or [])]
843
+ break
844
+ checks["tag_name"] = {
845
+ "expected": expected_tag, "actual": obj_tags,
846
+ "pass": expected_tag in obj_tags,
847
+ }
848
  except Exception as e:
849
  checks["tag_name"] = {"pass": None, "note": f"tag check failed: {e}"}
850
 
 
858
  json={"metadata": [{"type": "LOGICAL_TABLE", "identifier": model_guid}]},
859
  timeout=15,
860
  )
861
+ body = resp.text.strip()
862
+ if not body:
863
+ checks["share_with"] = {"pass": None, "note": "share check skipped: empty API response"}
864
+ else:
865
+ perms = resp.json()
866
+ principals = []
867
+ for item in (perms if isinstance(perms, list) else []):
868
+ for p in (item.get("permissions") or []):
869
+ principals.append(p.get("principal", {}).get("name", ""))
870
+ checks["share_with"] = {
871
+ "expected": expected_share, "actual": principals,
872
+ "pass": any(expected_share.lower() in p.lower() for p in principals),
873
+ }
874
  except Exception as e:
875
  checks["share_with"] = {"pass": None, "note": f"share check failed: {e}"}
876
 
 
994
  return {"found": False, "reason": f"Diagnostics query failed: {e}"}
995
 
996
 
997
+ def check_snowflake_schema(company: str, start_time: float, schema_override: str = None) -> dict:
998
  """
999
+ Get row counts for the Snowflake schema created during this test run.
1000
+ Uses schema_override (from session_logs) if available β€” no guessing needed.
1001
+ Falls back to prefix+date matching only if session_logs didn't record the schema.
1002
  """
1003
  try:
1004
  from snowflake_auth import get_snowflake_connection
1005
  from datetime import datetime
1006
 
 
 
 
1007
  conn = get_snowflake_connection()
1008
  cursor = conn.cursor()
 
 
 
 
 
 
 
1009
 
1010
+ if schema_override:
1011
+ schema = schema_override
1012
+ else:
1013
+ # Fall back: guess from company prefix + date
1014
+ prefix = company[:3].upper()
1015
+ date_str = datetime.fromtimestamp(start_time).strftime("%m%d")
1016
+ cursor.execute("SHOW SCHEMAS IN DATABASE DEMOBUILD")
1017
+ all_schemas = [row[1] for row in cursor.fetchall()]
1018
+ matching = [s for s in all_schemas if s.startswith(prefix) and date_str in s]
1019
+ if not matching:
1020
+ cursor.close(); conn.close()
1021
+ return {"found": False, "prefix": prefix, "date": date_str}
1022
+ schema = sorted(matching)[-1] # most recent
1023
  cursor.execute(f'SHOW TABLES IN SCHEMA DEMOBUILD."{schema}"')
1024
  tables = cursor.fetchall()
1025
 
 
1149
 
1150
  result["duration_seconds"] = round(time.time() - start)
1151
 
1152
+ # Always fetch session_logs β€” gives us the real schema name + any errors
1153
+ diag = fetch_run_diagnostics(start)
1154
+ result["diagnostics"] = diag
1155
+
1156
+ # Use the real schema name from session_logs; fall back to prefix-guessing only if missing
1157
+ known_schema = diag.get("snowflake_schema") if diag.get("found") else None
1158
+ sf = check_snowflake_schema(result["company"], start, schema_override=known_schema)
1159
  result["snowflake_check"] = sf
1160
  if sf.get("found"):
1161
  print(f" πŸ“¦ Snowflake: {sf['schema']} ({sf['total_rows']} rows, {len(sf['tables'])} tables)")
1162
  for t in sf["tables"]:
1163
  print(f" {t['table']}: {t['rows']} rows")
1164
  else:
1165
+ print(f" πŸ“¦ Snowflake: schema not found (logged as: {known_schema or 'not in logs'})")
1166
 
1167
+ # Print diagnostics on timeout or error
1168
  if result["timed_out"] or result["error"]:
 
 
 
1169
  print_diagnostics(diag, {}) # sf already printed above
1170
 
1171
  # Stage scoring
 
1248
  print("βœ… Logged in\n")
1249
 
1250
  for i, test_case in enumerate(suite, 1):
1251
+ label = {"fixed": "πŸ”’", "random": "🎲", "ai_generated": "πŸ€–", "custom": "✏️"}[test_case["type"]]
1252
  print(f"{'─'*62}")
1253
  print(f"[{i}/{len(suite)}] {label} {test_case['name']}")
1254
 
 
1295
  print(f"\n{'='*62}")
1296
  print(f" COMPLETE β€” Avg: {avg}/100 Grade: {grade}")
1297
  for r in results:
1298
+ label = {"fixed": "πŸ”’", "random": "🎲", "ai_generated": "πŸ€–", "custom": "✏️"}[r["type"]]
1299
  ag = r["ai_grading"]
1300
  ds = f"{ag['data_score']}/100" if ag.get("data_score") is not None else "n/a"
1301
  ls = f"{ag['liveboard_score']}/100" if ag.get("liveboard_score") is not None else "n/a"