mikeboone Claude Sonnet 4.6 commited on
Commit
9cdfe85
Β·
1 Parent(s): fc9ee34

feat: complete settings accordion + 10k test harness update

Browse files

Accordion now includes all runtime knobs: Tag Name, Column Naming
Style, Object Naming Prefix, Share With (in addition to Liveboard
Name, Data Size, Geo Scope already added). All values wired to
defined_go and seeded from Supabase at page load.

- settings_test.py updated: opens accordion, selects Data Size from
panel instead of writing to Supabase β€” no DB write needed
- ThoughtSpot Password orphan field removed from Settings tab
- Sprint doc: Done section populated with sprint 4 + sprint 3 items
- Settings test added to Priority 3 task list

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

Files changed (3) hide show
  1. chat_interface.py +59 -9
  2. sprint_2026_04.md +33 -12
  3. tests/settings_test.py +366 -0
chat_interface.py CHANGED
@@ -5122,6 +5122,10 @@ def create_chat_interface():
5122
  _ft_to_size = {"100": "Small", "1000": "Medium", "5000": "Standard", "10000": "Large"}
5123
  saved_size = _ft_to_size.get(str(settings.get("fact_table_size", "5000")), "Standard")
5124
  saved_geo = str(settings.get("geo_scope", "USA Only")).strip() or "USA Only"
 
 
 
 
5125
  print(f"[LOAD] load_session_state_on_startup OK β€” model={model}, company={company}")
5126
  return (
5127
  "initialization",
@@ -5133,6 +5137,10 @@ def create_chat_interface():
5133
  gr.update(value=liveboard_name),
5134
  gr.update(value=saved_size),
5135
  gr.update(value=saved_geo),
 
 
 
 
5136
  "",
5137
  )
5138
  except Exception as e:
@@ -5141,7 +5149,9 @@ def create_chat_interface():
5141
  return (
5142
  "initialization", DEFAULT_LLM_MODEL, "", "", "",
5143
  gr.update(value=DEFAULT_LLM_MODEL), gr.update(value=""),
5144
- gr.update(value="Standard"), gr.update(value="USA Only"), "",
 
 
5145
  )
5146
 
5147
  # Wire up load handler - outputs follow SETTINGS_SCHEMA order
@@ -5164,6 +5174,10 @@ def create_chat_interface():
5164
  chat_components["liveboard_name_input"],
5165
  chat_components["data_size_dropdown"],
5166
  chat_components["geo_scope_dropdown"],
 
 
 
 
5167
  chat_components["msg"],
5168
  ]
5169
  )
@@ -5309,6 +5323,33 @@ def create_chat_tab(chat_controller_state, settings, current_stage, current_mode
5309
  value=settings.get('geo_scope', 'USA Only'),
5310
  interactive=True,
5311
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5312
 
5313
  gr.Markdown("### πŸ“ˆ Progress")
5314
 
@@ -5460,7 +5501,7 @@ def create_chat_tab(chat_controller_state, settings, current_stage, current_mode
5460
  # Defined tab: GO button handler
5461
  def defined_go(controller, vertical, line, function, url, use_url, additional_info,
5462
  history, stage, model, company, usecase, env_label, lb_name,
5463
- data_size, geo_scope,
5464
  request: gr.Request = None):
5465
  import traceback
5466
  function_clean = (function or "").strip()
@@ -5491,6 +5532,14 @@ def create_chat_tab(chat_controller_state, settings, current_stage, current_mode
5491
  controller.settings['dim_table_size'] = _dt
5492
  if geo_scope:
5493
  controller.settings['geo_scope'] = geo_scope
 
 
 
 
 
 
 
 
5494
 
5495
  # Derive company name from URL or line+function label
5496
  if use_url and url.strip():
@@ -5569,7 +5618,8 @@ def create_chat_tab(chat_controller_state, settings, current_stage, current_mode
5569
  chat_controller_state, vertical_dd, line_dd, function_dd, url_input, use_url_cb,
5570
  additional_info_input, chatbot, current_stage, current_model,
5571
  current_company, current_usecase, ts_env_dropdown, liveboard_name_input,
5572
- data_size_dropdown, geo_scope_dropdown,
 
5573
  ]
5574
  _go_outputs = [
5575
  chat_controller_state, chatbot, current_stage, current_model,
@@ -5643,6 +5693,10 @@ def create_chat_tab(chat_controller_state, settings, current_stage, current_mode
5643
  'liveboard_name_input': liveboard_name_input,
5644
  'data_size_dropdown': data_size_dropdown,
5645
  'geo_scope_dropdown': geo_scope_dropdown,
 
 
 
 
5646
  'progress_html': progress_html,
5647
  'phase_log_display': phase_log_display,
5648
  'phase_log_timer': phase_log_timer,
@@ -5986,12 +6040,8 @@ def create_settings_tab():
5986
  info="Your ThoughtSpot login"
5987
  )
5988
 
5989
- ts_password = gr.Textbox(
5990
- label="ThoughtSpot Password",
5991
- placeholder="β€’β€’β€’β€’β€’β€’β€’β€’",
5992
- type="password",
5993
- info="Your ThoughtSpot password (stored securely)"
5994
- )
5995
 
5996
  gr.Markdown("---")
5997
  gr.Markdown("### πŸ”§ Data Adjuster")
 
5122
  _ft_to_size = {"100": "Small", "1000": "Medium", "5000": "Standard", "10000": "Large"}
5123
  saved_size = _ft_to_size.get(str(settings.get("fact_table_size", "5000")), "Standard")
5124
  saved_geo = str(settings.get("geo_scope", "USA Only")).strip() or "USA Only"
5125
+ saved_tag = str(settings.get("tag_name", "")).strip()
5126
+ saved_col_naming = str(settings.get("column_naming_style", "Regular Case")).strip() or "Regular Case"
5127
+ saved_obj_prefix = str(settings.get("object_naming_prefix", "")).strip()
5128
+ saved_share_with = str(settings.get("share_with", "")).strip()
5129
  print(f"[LOAD] load_session_state_on_startup OK β€” model={model}, company={company}")
5130
  return (
5131
  "initialization",
 
5137
  gr.update(value=liveboard_name),
5138
  gr.update(value=saved_size),
5139
  gr.update(value=saved_geo),
5140
+ gr.update(value=saved_tag),
5141
+ gr.update(value=saved_col_naming),
5142
+ gr.update(value=saved_obj_prefix),
5143
+ gr.update(value=saved_share_with),
5144
  "",
5145
  )
5146
  except Exception as e:
 
5149
  return (
5150
  "initialization", DEFAULT_LLM_MODEL, "", "", "",
5151
  gr.update(value=DEFAULT_LLM_MODEL), gr.update(value=""),
5152
+ gr.update(value="Standard"), gr.update(value="USA Only"),
5153
+ gr.update(value=""), gr.update(value="Regular Case"),
5154
+ gr.update(value=""), gr.update(value=""), "",
5155
  )
5156
 
5157
  # Wire up load handler - outputs follow SETTINGS_SCHEMA order
 
5174
  chat_components["liveboard_name_input"],
5175
  chat_components["data_size_dropdown"],
5176
  chat_components["geo_scope_dropdown"],
5177
+ chat_components["tag_name_input"],
5178
+ chat_components["column_naming_dropdown"],
5179
+ chat_components["object_prefix_input"],
5180
+ chat_components["share_with_input"],
5181
  chat_components["msg"],
5182
  ]
5183
  )
 
5323
  value=settings.get('geo_scope', 'USA Only'),
5324
  interactive=True,
5325
  )
5326
+ tag_name_input = gr.Textbox(
5327
+ label="Tag Name",
5328
+ placeholder="e.g. Sales_Demo (blank = no tag)",
5329
+ value=settings.get('tag_name', ''),
5330
+ lines=1,
5331
+ interactive=True,
5332
+ )
5333
+ column_naming_dropdown = gr.Dropdown(
5334
+ label="Column Naming Style",
5335
+ choices=["Regular Case", "snake_case", "camelCase", "PascalCase", "UPPER_CASE", "original"],
5336
+ value=settings.get('column_naming_style', 'Regular Case'),
5337
+ interactive=True,
5338
+ )
5339
+ object_prefix_input = gr.Textbox(
5340
+ label="Object Naming Prefix",
5341
+ placeholder="e.g. ACME_ (blank = none)",
5342
+ value=settings.get('object_naming_prefix', ''),
5343
+ lines=1,
5344
+ interactive=True,
5345
+ )
5346
+ share_with_input = gr.Textbox(
5347
+ label="Share With",
5348
+ placeholder="user@company.com or group-name (blank = no share)",
5349
+ value=settings.get('share_with', ''),
5350
+ lines=1,
5351
+ interactive=True,
5352
+ )
5353
 
5354
  gr.Markdown("### πŸ“ˆ Progress")
5355
 
 
5501
  # Defined tab: GO button handler
5502
  def defined_go(controller, vertical, line, function, url, use_url, additional_info,
5503
  history, stage, model, company, usecase, env_label, lb_name,
5504
+ data_size, geo_scope, tag_name, col_naming, obj_prefix, share_with,
5505
  request: gr.Request = None):
5506
  import traceback
5507
  function_clean = (function or "").strip()
 
5532
  controller.settings['dim_table_size'] = _dt
5533
  if geo_scope:
5534
  controller.settings['geo_scope'] = geo_scope
5535
+ if tag_name is not None:
5536
+ controller.settings['tag_name'] = tag_name
5537
+ if col_naming:
5538
+ controller.settings['column_naming_style'] = col_naming
5539
+ if obj_prefix is not None:
5540
+ controller.settings['object_naming_prefix'] = obj_prefix
5541
+ if share_with is not None:
5542
+ controller.settings['share_with'] = share_with
5543
 
5544
  # Derive company name from URL or line+function label
5545
  if use_url and url.strip():
 
5618
  chat_controller_state, vertical_dd, line_dd, function_dd, url_input, use_url_cb,
5619
  additional_info_input, chatbot, current_stage, current_model,
5620
  current_company, current_usecase, ts_env_dropdown, liveboard_name_input,
5621
+ data_size_dropdown, geo_scope_dropdown, tag_name_input,
5622
+ column_naming_dropdown, object_prefix_input, share_with_input,
5623
  ]
5624
  _go_outputs = [
5625
  chat_controller_state, chatbot, current_stage, current_model,
 
5693
  'liveboard_name_input': liveboard_name_input,
5694
  'data_size_dropdown': data_size_dropdown,
5695
  'geo_scope_dropdown': geo_scope_dropdown,
5696
+ 'tag_name_input': tag_name_input,
5697
+ 'column_naming_dropdown': column_naming_dropdown,
5698
+ 'object_prefix_input': object_prefix_input,
5699
+ 'share_with_input': share_with_input,
5700
  'progress_html': progress_html,
5701
  'phase_log_display': phase_log_display,
5702
  'phase_log_timer': phase_log_timer,
 
6040
  info="Your ThoughtSpot login"
6041
  )
6042
 
6043
+ # ts_password removed β€” auth is handled by TS Environment dropdown (API key),
6044
+ # not username/password. This field was never wired to SETTINGS_SCHEMA.
 
 
 
 
6045
 
6046
  gr.Markdown("---")
6047
  gr.Markdown("### πŸ”§ Data Adjuster")
sprint_2026_04.md CHANGED
@@ -80,9 +80,9 @@ settings (loaded to screen at page load) β€” the app then reads exclusively from
80
  - [x] Remove save/reload dependency for pipeline settings βœ… β€” panel values override at run time
81
  - [x] `fact_table_size`/`dim_table_size`/`geo_scope` hidden in Settings tab (still saved to Supabase, but UI lives in right panel) βœ…
82
  - [x] Supabase defaults seed right panel at page load via `load_session_state_on_startup` βœ…
83
- - [ ] Auto-derive liveboard name from company URL when field is blank
84
- - [ ] Add "reset defaults" section to Admin page
85
- - [ ] Remove orphaned Password field from Settings tab (never wired to SETTINGS_SCHEMA)
86
 
87
  ---
88
 
@@ -91,16 +91,14 @@ settings (loaded to screen at page load) β€” the app then reads exclusively from
91
  **Goal:** Confirm the "large" preset (10k fact rows) works end-to-end and produces better liveboard quality.
92
  Do this after the settings redesign β€” no sense testing row count behavior against the old settings flow.
93
 
94
- **Known issues to solve in test:**
95
- - Settings cached at controller init β€” test must change setting β†’ trigger fresh session β†’ run pipeline
96
- - 10k runs will be ~3–4x slower β€” bump test timeout for large-preset tests
97
- - Validate that `is_fact_table` name-based detection correctly assigns 10k rows (not 500)
98
 
99
  **Tasks:**
100
- - [ ] Add `fact_table_size=10000` variant to test suite (separate test case or flag)
101
- - [ ] Implement session reset between setting change and run in test harness
102
- - [ ] Increase timeout for large-preset test cases
103
- - [ ] Compare data/liveboard quality scores: 1k vs 10k on same company/use case
 
104
 
105
  ---
106
 
@@ -152,7 +150,30 @@ should tell. KPI targets, growth trends, and anomaly patterns live in the matrix
152
 
153
  ## Done
154
 
155
- *(items move here as completed)*
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
 
157
  ---
158
 
 
80
  - [x] Remove save/reload dependency for pipeline settings βœ… β€” panel values override at run time
81
  - [x] `fact_table_size`/`dim_table_size`/`geo_scope` hidden in Settings tab (still saved to Supabase, but UI lives in right panel) βœ…
82
  - [x] Supabase defaults seed right panel at page load via `load_session_state_on_startup` βœ…
83
+ - [x] Auto-derive liveboard name from company URL when field is blank βœ… β€” already implemented at line 3622; blank field falls through to `"{clean_company} - {use_case}"`
84
+ - [ ] Add "reset defaults" section to Admin page (deferred β€” lower priority)
85
+ - [x] Remove orphaned ThoughtSpot Password field from Settings tab βœ…
86
 
87
  ---
88
 
 
91
  **Goal:** Confirm the "large" preset (10k fact rows) works end-to-end and produces better liveboard quality.
92
  Do this after the settings redesign β€” no sense testing row count behavior against the old settings flow.
93
 
94
+ **Test harness:** `tests/settings_test.py` β€” purpose-built for settings-flow verification. Runs pipeline end-to-end and checks Snowflake row counts. No Supabase write needed β€” now selects "Large" directly from the panel Data Size dropdown.
 
 
 
95
 
96
  **Tasks:**
97
+ - [x] Settings test harness exists (`tests/settings_test.py`) βœ…
98
+ - [x] Updated test to use panel (open accordion β†’ select "Large") instead of Supabase write βœ…
99
+ - [x] Timeout bumped to 90 min for large-preset runs βœ…
100
+ - [ ] Run `tests/settings_test.py --test fact_10k` against test environment and verify ~10k rows in Snowflake
101
+ - [ ] Compare data/liveboard quality scores: Standard (5k) vs Large (10k) on same company/use case
102
 
103
  ---
104
 
 
150
 
151
  ## Done
152
 
153
+ ### Sprint 4 (this sprint)
154
+
155
+ - [x] **Branching / Release Strategy** β€” `main`/`develop`/hotfix workflow documented in CLAUDE.md; `develop` branch confirmed; `v1.0.0` tagged on `main`; `hf-test` remote wired βœ…
156
+ - [x] **Settings Redesign β€” accordion** β€” Right panel: TS Environment + AI Model always visible; collapsible `βš™οΈ Settings` accordion with Liveboard Name, Data Size, Geo Scope, Tag Name, Column Naming Style, Object Naming Prefix, Share With βœ…
157
+ - [x] **Settings Redesign β€” controller wiring** β€” `defined_go` reads all panel values at GO time; Data Size maps to fact/dim row counts; no Supabase call during run βœ…
158
+ - [x] **Settings Redesign β€” load from Supabase once** β€” `load_session_state_on_startup` seeds all accordion fields from saved settings at page load βœ…
159
+ - [x] **Settings tab cleanup** β€” `fact_table_size`, `dim_table_size`, `geo_scope` hidden (still saved/loaded via SETTINGS_SCHEMA); ThoughtSpot Password orphan field removed βœ…
160
+ - [x] **Settings test updated** β€” `tests/settings_test.py` rewritten to use panel (open accordion β†’ select Data Size) instead of Supabase writes βœ…
161
+
162
+ ### Shipped at end of Sprint 3 / mini sprint (Apr 28-29)
163
+
164
+ - [x] End-to-end Playwright quality test suite (6 test cases, LLM grading 0–100)
165
+ - [x] `is_fact_table` keyword detection β€” handles AI-generated DDL without FK constraints
166
+ - [x] `convert_value` DDL type fallback β€” fixes 22018 errors when DESCRIBE TABLE returns empty
167
+ - [x] Test/production environment separation (hf-test space + visible banner)
168
+ - [x] Critical NameError fix β€” `outliers` vs `data_outliers` in liveboard creation (every liveboard was crashing)
169
+ - [x] DATES table always spans 2 full years regardless of dataset size
170
+ - [x] LegitData API call reduction β€” seed-and-cycle (50+ calls β†’ 1 per entity type)
171
+ - [x] ThoughtSpot auth hardening β€” no fallback paths, TS Environment dropdown is required
172
+ - [x] Admin UX β€” user list sorted by most recent login (EST)
173
+ - [x] CUSTOM vertical renamed and repositioned in dropdown
174
+ - [x] Pipeline logging overhaul β€” structured logs to Supabase for every stage
175
+ - [x] Run History tab
176
+ - [x] Snowflake numeric type fix (FIXED/REAL type names now recognized)
177
 
178
  ---
179
 
tests/settings_test.py ADDED
@@ -0,0 +1,366 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Settings smoke tests for DemoPrep.
3
+
4
+ These tests verify that a user setting actually flows through the pipeline
5
+ and produces the expected output. Each test:
6
+ 1. Reads the current setting value (baseline)
7
+ 2. Writes the test value directly to Supabase
8
+ 3. Runs the pipeline with a fresh browser session (picks up new value)
9
+ 4. Verifies the output
10
+ 5. Resets the setting back (always β€” even on failure)
11
+
12
+ These are separate from e2e_quality.py which tests output quality.
13
+ These test that settings are wired correctly end-to-end.
14
+
15
+ Usage:
16
+ source demoprep/bin/activate
17
+ python tests/settings_test.py # run all
18
+ python tests/settings_test.py --test fact_10k # run one test
19
+ """
20
+
21
+ import argparse
22
+ import json
23
+ import os
24
+ import sys
25
+ import time
26
+ from datetime import datetime
27
+ from pathlib import Path
28
+ from typing import Optional
29
+
30
+ import yaml
31
+ from dotenv import load_dotenv
32
+ from playwright.sync_api import Page, sync_playwright
33
+
34
+ sys.path.insert(0, str(Path(__file__).parent.parent))
35
+ load_dotenv(Path(__file__).parent.parent / ".env")
36
+
37
+ BASE_URL = os.getenv("TEST_TARGET_URL")
38
+ if not BASE_URL:
39
+ raise ValueError("TEST_TARGET_URL not set in .env")
40
+ TEST_USER = os.getenv("TEST_USER")
41
+ TEST_PASSWORD = os.getenv("TEST_PASSWORD")
42
+
43
+ RESULTS_DIR = Path(__file__).parent / "quality_results"
44
+ RESULTS_DIR.mkdir(exist_ok=True)
45
+
46
+ # Fixed test cases (from quality_config.yaml)
47
+ FIXED_TESTS = [
48
+ {
49
+ "name": "Nike β€” Retail Sales",
50
+ "company": "Nike",
51
+ "company_url": "nike.com",
52
+ "vertical": "Retail & Consumer Goods",
53
+ "line": "Fashion/Apparel",
54
+ "function": "Sales",
55
+ "ts_environment": "secloud - primary",
56
+ },
57
+ {
58
+ "name": "Wells Fargo β€” Banking Marketing",
59
+ "company": "Wells Fargo",
60
+ "company_url": "wellsfargo.com",
61
+ "vertical": "Financial Services",
62
+ "line": "Banking",
63
+ "function": "Marketing",
64
+ "ts_environment": "secloud - primary",
65
+ },
66
+ ]
67
+
68
+
69
+ # ---------------------------------------------------------------------------
70
+ # Supabase settings helpers
71
+ # ---------------------------------------------------------------------------
72
+ def read_setting(key: str) -> str:
73
+ from supabase_client import SupabaseSettings
74
+ s = SupabaseSettings()
75
+ settings = s.load_all_settings(TEST_USER)
76
+ return settings.get(key, "")
77
+
78
+
79
+ def write_setting(key: str, value: str):
80
+ from supabase_client import SupabaseSettings
81
+ s = SupabaseSettings()
82
+ s.save_all_settings(TEST_USER, {key: value})
83
+
84
+
85
+ # ---------------------------------------------------------------------------
86
+ # Snowflake verification
87
+ # ---------------------------------------------------------------------------
88
+ def check_snowflake_rows(schema_name: str) -> dict:
89
+ """Return {table: row_count} for every table in the schema."""
90
+ try:
91
+ from snowflake_auth import get_snowflake_connection
92
+ conn = get_snowflake_connection()
93
+ cursor = conn.cursor()
94
+ cursor.execute(f'SHOW TABLES IN SCHEMA DEMOBUILD."{schema_name}"')
95
+ tables = [row[1] for row in cursor.fetchall()]
96
+ counts = {}
97
+ for t in tables:
98
+ cursor.execute(f'SELECT COUNT(*) FROM DEMOBUILD."{schema_name}"."{t}"')
99
+ counts[t] = cursor.fetchone()[0]
100
+ cursor.close()
101
+ conn.close()
102
+ return counts
103
+ except Exception as e:
104
+ return {"error": str(e)}
105
+
106
+
107
+ def get_schema_from_logs(start_time: float) -> Optional[str]:
108
+ """Pull the schema name written to session_logs during this run."""
109
+ try:
110
+ from supabase_client import SupabaseSettings
111
+ from datetime import datetime, timezone
112
+ start_iso = datetime.fromtimestamp(start_time, tz=timezone.utc).isoformat()
113
+ s = SupabaseSettings()
114
+ result = (
115
+ s.client.table("session_logs")
116
+ .select("*")
117
+ .eq("user_email", TEST_USER)
118
+ .gte("ts", start_iso)
119
+ .order("ts", desc=False)
120
+ .execute()
121
+ )
122
+ for log in (result.data or []):
123
+ meta = log.get("meta") or {}
124
+ if "schema" in meta:
125
+ return meta["schema"]
126
+ except Exception:
127
+ pass
128
+ return None
129
+
130
+
131
+ # ---------------------------------------------------------------------------
132
+ # Browser helpers (shared with e2e_quality.py pattern)
133
+ # ---------------------------------------------------------------------------
134
+ def _do_login(page: Page):
135
+ page.fill('input[type=text]', TEST_USER)
136
+ page.fill('input[type=password]', TEST_PASSWORD)
137
+ page.click('button:has-text("Login")')
138
+ page.wait_for_selector('button[role=tab]', timeout=90000)
139
+ page.wait_for_timeout(3000)
140
+
141
+
142
+ def select_gradio_dropdown(page: Page, label: str, value: str):
143
+ inp = page.locator(f'input[aria-label="{label}"]').first
144
+ inp.click(timeout=5000)
145
+ page.wait_for_timeout(300)
146
+ page.get_by_role('option', name=value, exact=True).click(timeout=5000)
147
+ page.wait_for_timeout(300)
148
+
149
+
150
+ def open_settings_accordion(page: Page):
151
+ """Open the βš™οΈ Settings accordion in the right panel if not already open."""
152
+ try:
153
+ accordion = page.locator('button:has-text("βš™οΈ Settings")').first
154
+ # Check if accordion is collapsed (aria-expanded=false)
155
+ if accordion.get_attribute('aria-expanded') == 'false':
156
+ accordion.click(timeout=5000)
157
+ page.wait_for_timeout(500)
158
+ except Exception:
159
+ pass
160
+
161
+
162
+ def submit_and_wait(page: Page, test_case: dict, timeout_min: int = 60) -> dict:
163
+ """
164
+ Submit the form and wait for pipeline completion.
165
+ Sets panel values directly β€” no Supabase write needed.
166
+ Returns {"stages": {...}, "timed_out": bool, "error": str|None}
167
+ """
168
+ # Fresh navigation β€” ensures panel loads with current Supabase defaults
169
+ page.goto(BASE_URL, timeout=90000)
170
+ page.wait_for_selector('button[role=tab], input[type=password]', timeout=90000)
171
+ if page.locator('input[type=password]').is_visible(timeout=2000):
172
+ _do_login(page)
173
+
174
+ page.wait_for_timeout(2000)
175
+ page.get_by_role('tab', name='App', exact=True).click(timeout=10000)
176
+ page.wait_for_timeout(1000)
177
+
178
+ select_gradio_dropdown(page, "TS Environment", test_case.get("ts_environment", "secloud - primary"))
179
+ select_gradio_dropdown(page, "Vertical", test_case["vertical"])
180
+ select_gradio_dropdown(page, "Line", test_case["line"])
181
+ select_gradio_dropdown(page, "Function", test_case["function"])
182
+
183
+ url_el = page.locator('input[aria-label="Company URL"], textarea[placeholder="e.g. Amazon.com"]').first
184
+ url_el.click(click_count=3, timeout=5000)
185
+ url_el.fill(test_case["company_url"])
186
+ page.wait_for_timeout(300)
187
+
188
+ # Open Settings accordion and set any overrides from test_case
189
+ open_settings_accordion(page)
190
+
191
+ if test_case.get("data_size"):
192
+ select_gradio_dropdown(page, "Data Size", test_case["data_size"])
193
+
194
+ # Liveboard name
195
+ try:
196
+ lb_el = page.locator('input[aria-label="Liveboard Name"], textarea[placeholder="Auto from company URL if blank"]').first
197
+ lb_el.scroll_into_view_if_needed(timeout=3000)
198
+ lb_el.click(click_count=3, timeout=3000)
199
+ lb_el.fill(f"Settings Test β€” {test_case['company']}")
200
+ page.wait_for_timeout(300)
201
+ except Exception:
202
+ pass
203
+
204
+ page.click('button:has-text("β†’ GO")', timeout=10000)
205
+ print(f" βœ… Submitted: {test_case['name']}")
206
+
207
+ # Poll for completion
208
+ timeout_sec = timeout_min * 60
209
+ start = time.time()
210
+ poll = 15
211
+ last_stages = {}
212
+
213
+ while time.time() - start < timeout_sec:
214
+ time.sleep(poll)
215
+ text = page.inner_text('body')
216
+ stages = {}
217
+ for key, label in [("research","Research"),("ddl","DDL"),("data","Data"),
218
+ ("thoughtspot","ThoughtSpot"),("complete","Complete")]:
219
+ if f"βœ“ {label}" in text or f"βœ… {label}" in text:
220
+ stages[key] = "complete"
221
+ elif f"β–Ά {label}" in text:
222
+ stages[key] = "running"
223
+ else:
224
+ stages[key] = "unknown"
225
+
226
+ if stages != last_stages:
227
+ done = [k for k, v in stages.items() if v == "complete"]
228
+ running = [k for k, v in stages.items() if v == "running"]
229
+ print(f" βœ“ {done} β–Ά {running}")
230
+ last_stages = stages
231
+
232
+ main = ("research", "ddl", "data", "thoughtspot")
233
+ if stages.get("complete") == "complete" or all(stages.get(s) == "complete" for s in main):
234
+ print(" βœ… Pipeline complete")
235
+ return {"stages": stages, "timed_out": False, "error": None,
236
+ "start_time": start}
237
+ else:
238
+ print(f" ⏰ Timed out after {timeout_min} min")
239
+ return {"stages": last_stages, "timed_out": True, "error": None,
240
+ "start_time": start}
241
+
242
+
243
+ # ---------------------------------------------------------------------------
244
+ # Test: fact_table_size = 10,000
245
+ # ---------------------------------------------------------------------------
246
+ def test_fact_10k():
247
+ """
248
+ Select Data Size = Large (10k rows) directly in the panel, run both fixed
249
+ test cases, and verify ~10k rows in the fact table via Snowflake.
250
+ No Supabase write needed β€” values come from the panel at run time.
251
+ """
252
+ print("\n" + "="*62)
253
+ print(" Settings Test: Data Size = Large (10,000 rows)")
254
+ print(f" Target: {BASE_URL}")
255
+ print("="*62)
256
+
257
+ # Inject data_size override into each test case
258
+ test_cases = [{**tc, "data_size": "Large"} for tc in FIXED_TESTS]
259
+
260
+ results = []
261
+
262
+ with sync_playwright() as p:
263
+ for tc in test_cases:
264
+ print(f"─── {tc['name']} ───")
265
+ browser = p.chromium.launch()
266
+ ctx = browser.new_context(viewport={"width": 1280, "height": 900})
267
+ page = ctx.new_page()
268
+
269
+ print(f" πŸ” Logging in...")
270
+ page.goto(BASE_URL, timeout=90000)
271
+ page.wait_for_selector('input[type=password]', timeout=90000)
272
+ _do_login(page)
273
+ print(" βœ… Logged in")
274
+
275
+ run = submit_and_wait(page, tc, timeout_min=90)
276
+ ctx.close()
277
+ browser.close()
278
+
279
+ # Verify row counts
280
+ schema = get_schema_from_logs(run["start_time"])
281
+ result = {
282
+ "name": tc["name"],
283
+ "timed_out": run["timed_out"],
284
+ "schema": schema,
285
+ "pass": False,
286
+ "note": "",
287
+ }
288
+
289
+ if run["timed_out"]:
290
+ result["note"] = "timed out"
291
+ elif not schema:
292
+ result["note"] = "schema not found in session_logs"
293
+ else:
294
+ counts = check_snowflake_rows(schema)
295
+ result["row_counts"] = counts
296
+ if "error" in counts:
297
+ result["note"] = f"Snowflake error: {counts['error']}"
298
+ else:
299
+ fact_rows = max(counts.values()) if counts else 0
300
+ expected = 10000
301
+ tolerance = 0.10 # within 10%
302
+ passed = fact_rows >= expected * (1 - tolerance)
303
+ result["fact_rows"] = fact_rows
304
+ result["pass"] = passed
305
+ result["note"] = (
306
+ f"fact table: {fact_rows} rows "
307
+ f"({'βœ… PASS' if passed else f'❌ FAIL β€” expected ~{expected}'})"
308
+ )
309
+ print(f" πŸ“¦ {schema}")
310
+ for t, c in sorted(counts.items()):
311
+ print(f" {t}: {c} rows")
312
+
313
+ results.append(result)
314
+ icon = "βœ…" if result["pass"] else "❌"
315
+ print(f" {icon} {result['name']}: {result['note']}\n")
316
+
317
+ # Summary
318
+ print("="*62)
319
+ passed = sum(1 for r in results if r["pass"])
320
+ print(f" RESULT: {passed}/{len(results)} passed")
321
+ for r in results:
322
+ icon = "βœ…" if r["pass"] else "❌"
323
+ print(f" {icon} {r['name']}: {r['note']}")
324
+ print("="*62 + "\n")
325
+
326
+ # Save results
327
+ ts = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
328
+ path = RESULTS_DIR / f"{ts}_settings_fact10k.json"
329
+ with open(path, "w") as f:
330
+ json.dump({"timestamp": datetime.now().isoformat(),
331
+ "test": "fact_table_size_10k",
332
+ "target": BASE_URL,
333
+ "results": results}, f, indent=2, default=str)
334
+ print(f"πŸ’Ύ Results: {path}")
335
+ return all(r["pass"] for r in results)
336
+
337
+
338
+ # ---------------------------------------------------------------------------
339
+ # Registry + CLI
340
+ # ---------------------------------------------------------------------------
341
+ TESTS = {
342
+ "fact_10k": test_fact_10k,
343
+ }
344
+
345
+
346
+ def main():
347
+ parser = argparse.ArgumentParser()
348
+ parser.add_argument("--test", choices=list(TESTS.keys()),
349
+ help="Run a specific test (default: all)")
350
+ args = parser.parse_args()
351
+
352
+ if not TEST_USER or not TEST_PASSWORD:
353
+ raise RuntimeError("TEST_USER and TEST_PASSWORD must be set in .env")
354
+
355
+ to_run = [args.test] if args.test else list(TESTS.keys())
356
+ passed = 0
357
+ for name in to_run:
358
+ ok = TESTS[name]()
359
+ if ok:
360
+ passed += 1
361
+ print(f"\nTotal: {passed}/{len(to_run)} passed")
362
+ sys.exit(0 if passed == len(to_run) else 1)
363
+
364
+
365
+ if __name__ == "__main__":
366
+ main()