mikeboone Claude Sonnet 4.6 commited on
Commit
0d7febe
·
1 Parent(s): e2d85b8

fix: stabilize e2e test suite — session resilience + LegitData API call reduction

Browse files

Tests 2-6 (page load timeout): add _navigate_and_ensure_logged_in() to
submit_job — waits for tabs OR login form, re-logs in on session expiry,
retries up to 5min if space is rebuilding after a push.

Test 1 (45-min hang): generate_entity_batch was making N/20 API calls per
dimension table (10 calls × 5 tables = 50+ calls for Banking/Marketing standard
preset). Changed to one seed call (≤50 entities) then cycle — cuts API calls
from 50+ down to ~5, well within the 15-min POP_TIMEOUT.

Monitoring loop: now detects pipeline error text in page body and stage stuck
>20min to bail early instead of waiting the full 45min timeout.

Also includes ★ CUSTOM ★ vertical rename (demo_personas.py + chat_interface.py).

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

chat_interface.py CHANGED
@@ -5411,15 +5411,15 @@ def create_chat_tab(chat_controller_state, settings, current_stage, current_mode
5411
  # App tab: vertical → line + function cascade
5412
  def update_line_on_vertical(vertical):
5413
  lines = VERTICAL_LINES.get(vertical, [])
5414
- if vertical == "Custom" or not lines:
5415
  return (
5416
- gr.Dropdown(choices=[], value=None, interactive=False),
5417
- gr.Dropdown(choices=[], value=None, interactive=False),
5418
  gr.Textbox(label="Context *", placeholder="Describe your use case, industry, and key metrics...", interactive=True),
5419
  )
5420
  return (
5421
- gr.Dropdown(choices=lines, value=lines[0], interactive=True),
5422
- gr.Dropdown(choices=DEMO_FUNCTIONS, value=DEMO_FUNCTIONS[0], interactive=True),
5423
  gr.Textbox(label="Context", placeholder="Any extra context for the demo...", interactive=True),
5424
  )
5425
 
@@ -5457,7 +5457,7 @@ def create_chat_tab(chat_controller_state, settings, current_stage, current_mode
5457
  else:
5458
  raw_company = f"{line or vertical} Demo"
5459
 
5460
- is_custom = (vertical == "Custom")
5461
  hide_welcome = gr.update(visible=False)
5462
 
5463
  if is_custom:
@@ -5762,7 +5762,7 @@ def create_settings_tab():
5762
  # Build use case choices from VERTICAL_LINES × DEMO_FUNCTIONS
5763
  use_case_choices = []
5764
  for v_name, lines in VERTICAL_LINES.items():
5765
- if v_name == "Custom":
5766
  continue
5767
  for line in lines:
5768
  for f_name in DEMO_FUNCTIONS:
 
5411
  # App tab: vertical → line + function cascade
5412
  def update_line_on_vertical(vertical):
5413
  lines = VERTICAL_LINES.get(vertical, [])
5414
+ if vertical == "★ CUSTOM ★" or not lines:
5415
  return (
5416
+ gr.Dropdown(choices=["— not used —"], value="— not used —", interactive=False, label="Line (n/a for custom)"),
5417
+ gr.Dropdown(choices=["— not used —"], value="— not used —", interactive=False, label="Function (n/a for custom)"),
5418
  gr.Textbox(label="Context *", placeholder="Describe your use case, industry, and key metrics...", interactive=True),
5419
  )
5420
  return (
5421
+ gr.Dropdown(choices=lines, value=lines[0], interactive=True, label="Line"),
5422
+ gr.Dropdown(choices=DEMO_FUNCTIONS, value=DEMO_FUNCTIONS[0], interactive=True, label="Function"),
5423
  gr.Textbox(label="Context", placeholder="Any extra context for the demo...", interactive=True),
5424
  )
5425
 
 
5457
  else:
5458
  raw_company = f"{line or vertical} Demo"
5459
 
5460
+ is_custom = (vertical == "★ CUSTOM ★")
5461
  hide_welcome = gr.update(visible=False)
5462
 
5463
  if is_custom:
 
5762
  # Build use case choices from VERTICAL_LINES × DEMO_FUNCTIONS
5763
  use_case_choices = []
5764
  for v_name, lines in VERTICAL_LINES.items():
5765
+ if v_name == "★ CUSTOM ★":
5766
  continue
5767
  for line in lines:
5768
  for f_name in DEMO_FUNCTIONS:
demo_personas.py CHANGED
@@ -115,7 +115,7 @@ VERTICAL_LINES = {
115
  "Agriculture": [
116
  "Forestry", "Fishing", "Hunting", "Cultivation", "Raw Materials",
117
  ],
118
- "Custom": [],
119
  }
120
 
121
  DEMO_FUNCTIONS = ["Sales", "Marketing", "Finance", "HR", "IT", "Legal"]
 
115
  "Agriculture": [
116
  "Forestry", "Fishing", "Hunting", "Cultivation", "Raw Materials",
117
  ],
118
+ "★ CUSTOM ★": [],
119
  }
120
 
121
  DEMO_FUNCTIONS = ["Sales", "Marketing", "Finance", "HR", "IT", "Legal"]
legitdata_project/legitdata/sourcer/ai_generator.py CHANGED
@@ -221,15 +221,12 @@ Return ONLY a JSON array of strings. Example: ["value1", "value2", "value3"]"""
221
  columns_desc = "\n".join(column_specs)
222
  column_names = [col['name'] for col in columns]
223
 
224
- # Generate in batches to avoid token limits
 
 
225
  all_entities = []
226
- batch_size = min(20, num_entities)
227
-
228
- while len(all_entities) < num_entities:
229
- remaining = num_entities - len(all_entities)
230
- current_batch = min(batch_size, remaining)
231
-
232
- prompt = f"""Generate {current_batch} realistic, coherent {table_name} records for a database.
233
 
234
  Company: {context.company_name}
235
  Industry: {context.industry}
@@ -243,7 +240,7 @@ CRITICAL: The values in each row must be COHERENT with each other. For example:
243
  - If product_category is "Books", product_description should describe a book
244
  - If brand_name is "Nike", the product should be sports/athletic related
245
 
246
- Generate {current_batch} diverse, realistic records. Each record should tell a coherent story.
247
 
248
  Return ONLY a JSON array of objects. Example format:
249
  [
@@ -251,54 +248,55 @@ Return ONLY a JSON array of objects. Example format:
251
  ...
252
  ]"""
253
 
254
- try:
255
- response = self.anthropic_client.messages.create(
256
- model="claude-sonnet-4-20250514",
257
- max_tokens=4000,
258
- messages=[{"role": "user", "content": prompt}]
259
- )
260
-
261
- json_str = response.content[0].text.strip()
262
- # Handle markdown
263
- if json_str.startswith('```'):
264
- json_str = re.sub(r'^```\w*\n?', '', json_str)
265
- json_str = re.sub(r'\n?```$', '', json_str)
266
-
267
- entities = json.loads(json_str)
268
-
269
- if isinstance(entities, list):
270
- for entity in entities:
271
- if isinstance(entity, dict):
272
- # Normalize keys to match column names
273
- normalized = {}
274
- for col in columns:
275
- col_name = col['name']
276
- # Try exact match first, then case-insensitive
277
- if col_name in entity:
278
- normalized[col_name] = str(entity[col_name])
279
- else:
280
- for key in entity:
281
- if key.lower() == col_name.lower():
282
- normalized[col_name] = str(entity[key])
283
- break
284
- if normalized:
285
- all_entities.append(normalized)
286
-
287
- except Exception as e:
288
- print(f"Warning: Entity batch generation failed for {table_name}: {e}")
289
- # Fall back to single-column generation
290
- break
291
-
292
- if len(all_entities) >= num_entities:
293
- break
294
-
295
- # If we didn't get enough, fill with fallback
 
296
  if len(all_entities) < num_entities:
297
  fallback = self._generate_entity_fallback(
298
  table_name, columns, num_entities - len(all_entities)
299
  )
300
  all_entities.extend(fallback)
301
-
302
  return all_entities[:num_entities]
303
 
304
  def _generate_entity_fallback(
 
221
  columns_desc = "\n".join(column_specs)
222
  column_names = [col['name'] for col in columns]
223
 
224
+ # Generate a "seed" batch via a single API call, then cycle for remaining rows.
225
+ # Avoids making N/20 API calls when num_entities is large (e.g. 200 dim rows).
226
+ seed_target = min(50, num_entities)
227
  all_entities = []
228
+
229
+ prompt = f"""Generate {seed_target} realistic, coherent {table_name} records for a database.
 
 
 
 
 
230
 
231
  Company: {context.company_name}
232
  Industry: {context.industry}
 
240
  - If product_category is "Books", product_description should describe a book
241
  - If brand_name is "Nike", the product should be sports/athletic related
242
 
243
+ Generate {seed_target} diverse, realistic records. Each record should tell a coherent story.
244
 
245
  Return ONLY a JSON array of objects. Example format:
246
  [
 
248
  ...
249
  ]"""
250
 
251
+ try:
252
+ response = self.anthropic_client.messages.create(
253
+ model="claude-sonnet-4-20250514",
254
+ max_tokens=4000,
255
+ messages=[{"role": "user", "content": prompt}]
256
+ )
257
+
258
+ json_str = response.content[0].text.strip()
259
+ # Handle markdown
260
+ if json_str.startswith('```'):
261
+ json_str = re.sub(r'^```\w*\n?', '', json_str)
262
+ json_str = re.sub(r'\n?```$', '', json_str)
263
+
264
+ entities = json.loads(json_str)
265
+
266
+ if isinstance(entities, list):
267
+ for entity in entities:
268
+ if isinstance(entity, dict):
269
+ # Normalize keys to match column names
270
+ normalized = {}
271
+ for col in columns:
272
+ col_name = col['name']
273
+ # Try exact match first, then case-insensitive
274
+ if col_name in entity:
275
+ normalized[col_name] = str(entity[col_name])
276
+ else:
277
+ for key in entity:
278
+ if key.lower() == col_name.lower():
279
+ normalized[col_name] = str(entity[key])
280
+ break
281
+ if normalized:
282
+ all_entities.append(normalized)
283
+
284
+ except Exception as e:
285
+ print(f"Warning: Entity batch generation failed for {table_name}: {e}")
286
+
287
+ # If seed generation produced results, cycle them to fill remaining rows
288
+ if all_entities and len(all_entities) < num_entities:
289
+ seed = list(all_entities)
290
+ while len(all_entities) < num_entities:
291
+ all_entities.append(seed[len(all_entities) % len(seed)].copy())
292
+
293
+ # If we still don't have enough (seed generation failed), fall back
294
  if len(all_entities) < num_entities:
295
  fallback = self._generate_entity_fallback(
296
  table_name, columns, num_entities - len(all_entities)
297
  )
298
  all_entities.extend(fallback)
299
+
300
  return all_entities[:num_entities]
301
 
302
  def _generate_entity_fallback(
tests/e2e_quality.py CHANGED
@@ -185,11 +185,55 @@ def select_gradio_dropdown(page: Page, label: str, value: str):
185
  page.wait_for_timeout(300)
186
 
187
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  def submit_job(page: Page, test_case: dict):
189
  """Fill the form and click GO."""
190
- # Navigate to App tab, then App sub-tab
191
- page.goto(BASE_URL, timeout=90000)
192
- page.wait_for_selector('button[role=tab]', timeout=60000)
193
  page.wait_for_timeout(2000)
194
 
195
  page.click('button[role=tab]:has-text("📱 App")', timeout=10000)
@@ -914,8 +958,17 @@ def run_single_test(page: Page, test_case: dict, config: dict) -> dict:
914
  submit_job(page, test_case)
915
  print(f" ⏳ Monitoring pipeline (timeout: {config['grading']['timeout_minutes']}min)...")
916
 
917
- poll_interval = 15
918
- last_stages = {}
 
 
 
 
 
 
 
 
 
919
  while time.time() - start < timeout_sec:
920
  time.sleep(poll_interval)
921
  stages = read_progress(page)
@@ -923,10 +976,26 @@ def run_single_test(page: Page, test_case: dict, config: dict) -> dict:
923
  done = [k for k, v in stages.items() if v == "complete"]
924
  running = [k for k, v in stages.items() if v == "running"]
925
  print(f" ✓ {done} ▶ {running}")
926
- last_stages = stages
 
927
  if pipeline_finished(stages):
928
  print(" ✅ Pipeline complete")
929
  break
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
930
  else:
931
  result["timed_out"] = True
932
  print(f" ⏰ Timed out after {config['grading']['timeout_minutes']} min")
@@ -1039,11 +1108,7 @@ def run_quality_suite(max_tests: int = None):
1039
  page = ctx.new_page()
1040
  page.goto(BASE_URL, timeout=90000)
1041
  page.wait_for_selector('input[placeholder="Type here..."]', timeout=90000)
1042
- page.fill('input[type=text]', TEST_USER)
1043
- page.fill('input[type=password]', TEST_PASSWORD)
1044
- page.click('button:has-text("Login")')
1045
- page.wait_for_selector('button[role=tab]', timeout=90000)
1046
- page.wait_for_timeout(3000)
1047
  print("✅ Logged in\n")
1048
 
1049
  for i, test_case in enumerate(suite, 1):
 
185
  page.wait_for_timeout(300)
186
 
187
 
188
+ def _do_login(page: Page):
189
+ """Fill and submit the login form, then wait for tabs."""
190
+ page.fill('input[type=text]', TEST_USER)
191
+ page.fill('input[type=password]', TEST_PASSWORD)
192
+ page.click('button:has-text("Login")')
193
+ page.wait_for_selector('button[role=tab]', timeout=90000)
194
+ page.wait_for_timeout(3000)
195
+
196
+
197
+ def _navigate_and_ensure_logged_in(page: Page, max_wait_secs: int = 300):
198
+ """
199
+ Navigate to BASE_URL and ensure we're on the logged-in app.
200
+ Handles: HF space sleeping/rebuilding after a long test, session expiry.
201
+ Retries for up to max_wait_secs before raising.
202
+ """
203
+ deadline = time.time() + max_wait_secs
204
+ attempt = 0
205
+ while True:
206
+ attempt += 1
207
+ try:
208
+ page.goto(BASE_URL, timeout=90000)
209
+ # Wait for either the logged-in app (tabs) or the login form
210
+ page.wait_for_selector(
211
+ 'button[role=tab], input[type=password]',
212
+ timeout=60000,
213
+ )
214
+ break
215
+ except Exception as nav_err:
216
+ remaining = int(deadline - time.time())
217
+ if remaining <= 0:
218
+ raise RuntimeError(
219
+ f"Space not reachable after {max_wait_secs}s: {nav_err}"
220
+ ) from nav_err
221
+ print(f" ⚠️ Space not ready (attempt {attempt}) — retrying in 30s ({remaining}s left)...")
222
+ time.sleep(30)
223
+
224
+ # If we landed on the login page (session expired or space rebuilt), re-login
225
+ try:
226
+ if page.locator('input[type=password]').is_visible(timeout=2000):
227
+ print(" 🔑 Session expired — re-logging in...")
228
+ _do_login(page)
229
+ except Exception:
230
+ pass # Already on the app — no login needed
231
+
232
+
233
  def submit_job(page: Page, test_case: dict):
234
  """Fill the form and click GO."""
235
+ # Navigate, re-logging in if the session expired (e.g. after a long prior test)
236
+ _navigate_and_ensure_logged_in(page)
 
237
  page.wait_for_timeout(2000)
238
 
239
  page.click('button[role=tab]:has-text("📱 App")', timeout=10000)
 
958
  submit_job(page, test_case)
959
  print(f" ⏳ Monitoring pipeline (timeout: {config['grading']['timeout_minutes']}min)...")
960
 
961
+ poll_interval = 15
962
+ last_stages = {}
963
+ last_change_time = time.time()
964
+ STUCK_THRESHOLD = 20 * 60 # 20 min with no stage change → bail early
965
+ PIPELINE_ERROR_INDICATORS = [
966
+ "pipeline has been interrupted",
967
+ "An unexpected error occurred",
968
+ "Population failed",
969
+ "Pipeline failed",
970
+ "Something went wrong during the pipeline",
971
+ ]
972
  while time.time() - start < timeout_sec:
973
  time.sleep(poll_interval)
974
  stages = read_progress(page)
 
976
  done = [k for k, v in stages.items() if v == "complete"]
977
  running = [k for k, v in stages.items() if v == "running"]
978
  print(f" ✓ {done} ▶ {running}")
979
+ last_stages = stages
980
+ last_change_time = time.time()
981
  if pipeline_finished(stages):
982
  print(" ✅ Pipeline complete")
983
  break
984
+ # Detect hard pipeline failure in the chat output
985
+ try:
986
+ page_text = page.inner_text('body')
987
+ if any(ind in page_text for ind in PIPELINE_ERROR_INDICATORS):
988
+ result["timed_out"] = True
989
+ print(" ❌ Pipeline error detected in page — stopping early")
990
+ break
991
+ except Exception:
992
+ pass
993
+ # Bail if stages have been stuck for too long
994
+ stuck_secs = time.time() - last_change_time
995
+ if stuck_secs > STUCK_THRESHOLD and any(v == "running" for v in stages.values()):
996
+ result["timed_out"] = True
997
+ print(f" ⏰ Stage stuck for {int(stuck_secs/60)}min — treating as failure")
998
+ break
999
  else:
1000
  result["timed_out"] = True
1001
  print(f" ⏰ Timed out after {config['grading']['timeout_minutes']} min")
 
1108
  page = ctx.new_page()
1109
  page.goto(BASE_URL, timeout=90000)
1110
  page.wait_for_selector('input[placeholder="Type here..."]', timeout=90000)
1111
+ _do_login(page)
 
 
 
 
1112
  print("✅ Logged in\n")
1113
 
1114
  for i, test_case in enumerate(suite, 1):