GitHub Actions commited on
Commit
020705e
·
1 Parent(s): 889e86b

Sync from GitHub · 2026-09-26 23:46 UTC

Browse files
routes/bff.py CHANGED
@@ -109,7 +109,7 @@ def _stage_guidance(stage: str) -> str:
109
  return f"\n\nBUSINESS STAGE: {label}\n{rule}"
110
 
111
 
112
- def _call_groq_json(system_prompt: str, user_prompt: str) -> dict:
113
  """
114
  Thin wrapper — delegates to the key-rotating Groq client utility.
115
 
@@ -118,7 +118,7 @@ def _call_groq_json(system_prompt: str, user_prompt: str) -> dict:
118
  reasoning before writing the final JSON — a tight budget here truncates
119
  the JSON output and fails to parse.
120
  """
121
- return call_groq_json(system_prompt, user_prompt, model=HEAVY_MODEL, max_tokens=3000)
122
 
123
 
124
  # ─── Route: Generate follow-up questions ─────────────────────────────────────
@@ -185,7 +185,7 @@ Question types:
185
  """
186
 
187
  try:
188
- data = _call_groq_json(system_prompt, user_prompt)
189
  return data
190
  except Exception as e:
191
  raise HTTPException(status_code=500, detail=f"Follow-up generation failed: {str(e)}")
@@ -370,7 +370,7 @@ each containing a list of 2-4 concise bullet points:
370
  if i > 0:
371
  time.sleep(15) # 15s gap: ~3,000 tokens recovered at 200 tok/s
372
  try:
373
- results[key] = _call_groq_json(base_system, prompt_fn())
374
  except Exception as e:
375
  errors.append(f"{key}: {str(e)}")
376
  print(f"[bff] Failed to generate {key}: {e}")
 
109
  return f"\n\nBUSINESS STAGE: {label}\n{rule}"
110
 
111
 
112
+ def _call_groq_json(system_prompt: str, user_prompt: str, fixture: str | None = None) -> dict:
113
  """
114
  Thin wrapper — delegates to the key-rotating Groq client utility.
115
 
 
118
  reasoning before writing the final JSON — a tight budget here truncates
119
  the JSON output and fails to parse.
120
  """
121
+ return call_groq_json(system_prompt, user_prompt, model=HEAVY_MODEL, max_tokens=3000, fixture=fixture)
122
 
123
 
124
  # ─── Route: Generate follow-up questions ─────────────────────────────────────
 
185
  """
186
 
187
  try:
188
+ data = _call_groq_json(system_prompt, user_prompt, fixture="followups")
189
  return data
190
  except Exception as e:
191
  raise HTTPException(status_code=500, detail=f"Follow-up generation failed: {str(e)}")
 
370
  if i > 0:
371
  time.sleep(15) # 15s gap: ~3,000 tokens recovered at 200 tok/s
372
  try:
373
+ results[key] = _call_groq_json(base_system, prompt_fn(), fixture=key)
374
  except Exception as e:
375
  errors.append(f"{key}: {str(e)}")
376
  print(f"[bff] Failed to generate {key}: {e}")
routes/chat.py CHANGED
@@ -113,6 +113,7 @@ RULES:
113
  user_prompt=body.message,
114
  model=model,
115
  max_tokens=512,
 
116
  )
117
  for chunk in stream_response:
118
  token = chunk.choices[0].delta.content
 
113
  user_prompt=body.message,
114
  model=model,
115
  max_tokens=512,
116
+ fixture="chat_response",
117
  )
118
  for chunk in stream_response:
119
  token = chunk.choices[0].delta.content
routes/consultation.py CHANGED
@@ -106,7 +106,7 @@ Return JSON:
106
  }}"""
107
 
108
  try:
109
- result = call_groq_json(system, prompt, model=HEAVY_MODEL, max_tokens=800)
110
  return {
111
  "openingMessage": result.get("openingMessage", ""),
112
  "focusAreas": result.get("focusAreas", []),
@@ -180,7 +180,7 @@ Write a formal consultation report. Return JSON:
180
  }}"""
181
 
182
  try:
183
- result = call_groq_json(system, prompt, model=HEAVY_MODEL, max_tokens=2000)
184
  return {"report": result}
185
  except Exception as e:
186
  raise HTTPException(status_code=500, detail=f"Report generation failed: {str(e)}")
 
106
  }}"""
107
 
108
  try:
109
+ result = call_groq_json(system, prompt, model=HEAVY_MODEL, max_tokens=800, fixture="consultation_start")
110
  return {
111
  "openingMessage": result.get("openingMessage", ""),
112
  "focusAreas": result.get("focusAreas", []),
 
180
  }}"""
181
 
182
  try:
183
+ result = call_groq_json(system, prompt, model=HEAVY_MODEL, max_tokens=2000, fixture="consultation_report")
184
  return {"report": result}
185
  except Exception as e:
186
  raise HTTPException(status_code=500, detail=f"Report generation failed: {str(e)}")
routes/documents.py CHANGED
@@ -82,7 +82,7 @@ Generate a comprehensive Business Plan. Return JSON:
82
  "financialSummary": "Startup costs, expected revenue, break-even estimate",
83
  "implementationTimeline": "Key milestones for the first 12 months"
84
  }}"""
85
- return call_groq_json(system, prompt, model=HEAVY_MODEL, max_tokens=2500)
86
 
87
 
88
  # ─── Pitch Deck ────────────────────────────────────────────────────────────────
@@ -130,7 +130,7 @@ Generate a 7-slide Pitch Deck. Return JSON where each key is a slide:
130
  "bullets": ["Seeking: ...", "Use of funds: ...", "Expected outcome: ..."]
131
  }}
132
  }}"""
133
- return call_groq_json(system, prompt, model=HEAVY_MODEL, max_tokens=2000)
134
 
135
 
136
  # ─── Route ────────────────────────────────────────────────────────────────────
 
82
  "financialSummary": "Startup costs, expected revenue, break-even estimate",
83
  "implementationTimeline": "Key milestones for the first 12 months"
84
  }}"""
85
+ return call_groq_json(system, prompt, model=HEAVY_MODEL, max_tokens=2500, fixture="business_plan")
86
 
87
 
88
  # ─── Pitch Deck ────────────────────────────────────────────────────────────────
 
130
  "bullets": ["Seeking: ...", "Use of funds: ...", "Expected outcome: ..."]
131
  }}
132
  }}"""
133
+ return call_groq_json(system, prompt, model=HEAVY_MODEL, max_tokens=2000, fixture="pitch_deck")
134
 
135
 
136
  # ─── Route ────────────────────────────────────────────────────────────────────
routes/planning.py CHANGED
@@ -130,7 +130,10 @@ Return JSON:
130
  """
131
 
132
  try:
133
- result = call_groq_json(system, prompt, model=HEAVY_MODEL, max_tokens=800)
 
 
 
134
  suggestions = result.get("suggestions", [])
135
  if not suggestions:
136
  raise ValueError("No suggestions returned")
 
130
  """
131
 
132
  try:
133
+ result = call_groq_json(
134
+ system, prompt, model=HEAVY_MODEL, max_tokens=800,
135
+ fixture=f"planning_{body.type}",
136
+ )
137
  suggestions = result.get("suggestions", [])
138
  if not suggestions:
139
  raise ValueError("No suggestions returned")
routes/transcribe.py CHANGED
@@ -51,6 +51,7 @@ async def transcribe_audio(
51
  file=(audio.filename or "recording.webm", audio_bytes, content_type),
52
  response_format="json",
53
  language="en",
 
54
  )
55
  return {"transcript": transcription.text}
56
 
 
51
  file=(audio.filename or "recording.webm", audio_bytes, content_type),
52
  response_format="json",
53
  language="en",
54
+ fixture="transcript",
55
  )
56
  return {"transcript": transcription.text}
57
 
tests/fixtures/README.md ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ # Test fixtures
2
+
3
+ - `groq/` — canned Groq responses for `E2E_FIXTURE_MODE` (see `utils/groq_client.py`, ADR-0013).
4
+ - `fake-service-account.json` — **not a real credential.** A throwaway RSA key generated locally
5
+ (`openssl genpkey`) and never registered with any Google Cloud project. It exists only so
6
+ `firebase_admin.credentials.Certificate()` has a syntactically valid key to parse when the
7
+ backend initializes against the local Firebase Emulator Suite in E2E tests
8
+ (`playwright.config.ts`) — the emulators never validate it against real Google infrastructure.
9
+ If a secret scanner flags this file: it's a known, intentional fixture, not a leaked key.
tests/fixtures/fake-service-account.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "type": "service_account",
3
+ "project_id": "venthrive-e2e-demo",
4
+ "private_key_id": "e2e-fake-key-id",
5
+ "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC47ouHuvQe0Irn\nix5oJ7DZDhfrgot0D23dYwV6cdmlpvTKyeDUWwrgHbPmTUJ5ouo3UvwG7GeiEM3t\n0cDWNL/WkeW+FS5RodY9GsfFCZJNBGUfIdjLLyWN3LMT92njkMu7pLRvuC/Cvi/i\nOKzRCwE8bY+HfDDAVCwNnacVlM6c/sHY23QJ2rKVbOcbdXa18rtSEHwHLjMfFyr/\nVokMc21dyPTyqt0QERC1qMgFoaCDj2gEHmZRq06ZqSsqJoY0hH/HvH/bqqqYzuZN\n8w8QU2Ft/4Yl3C5yPg/Hyjar/92giMf0+j9w+HxKpMK73NXLOPGEq33jkUiy0OXF\niv37vFPTAgMBAAECggEAJqpljQL3QKwcps3NFTzbOn66XJjmXGVPdL7b6d59AagX\nPYEZG31PmE1NjGTx9ams69VTcYUjI9z8mXFGLuNHiu7lSKl4DkRWnV8IjrdyGd1F\n4Uz73Xulj/Qpkw6jWpuvJTQKL+OkRMTsERIFfA2ffAlyPYMnVEclENG6GgaLboJw\nLQv56tcD+LDuqDPmP+AWKvNeLurOgS+UjcF1s7ImGJq6iA1IscuXwCFlCUHIw+yW\nTrjh1vgzra5kDM1HbFK4cc6TbQ7PFt9L9BdR4HRBKj93tG8oWT/3tggGMqmw10j+\nRCBrNp0looDXMQWk9ZPzCUzjDFeH6FWGsGkWnhNgeQKBgQDsOuMCkxec7/AasRwv\nOLJAob+frXTBkgMHASQ/smPMmvLRIOXmjPz3aosZfaE++RbfAtfgF7D9rcceAaDF\n5n5l8DJvyaMpnkTJm7KPzr96EoDau1nb0E9fxb2busbtTV74C/UmCj7dI1jdBt5S\n7SHjRBlar8lTkVI47gaOtoi4+QKBgQDIaJ5+9M5o6VXpyp6ojz8TxjYaxgJ3aIPX\nEeWo/r4g5EzudNEjtDyaIngOauDkEFg6ecjrUkrmuL9ChZpdBdvwOcXORtdKUai+\n5HdLS11zYmVFdPlV9bWJri/8bNt/P4WD7OSkF4NQ0skfupCQUKBOYEIYHn//EpPx\n7CNLFpHSKwKBgQCYPasncZx6K7fVu+aqGCPou1U/lw3z4iHJVdcCpfjjV8i9NjJz\nztEIssVKfEh6yRUQTEjLxvjtxBzbHbl7UPyMvawDlzNsEu/J7mm5fEiyZ1JbpPpY\nS3ucAVgdENY7AO3/ddVb2qaA5dLTPyXw05pPWQfuBfP9w132CpB3UPP0IQKBgQCv\nNtirwrjb/HVCrAW+9U5+qWMQUWJCp7vswxb5g48TDsc8DCH1h1UcpVo1sLfibW/V\n4DgLla0qvwdoz4pAHilGhBwu1rzPm0lfbDvoNoIDqVlFOpnrz6DiOMp3dW0Keazv\nxKO521zRwxhYhzJxGiytmXbvw2rAxatXbc8FjTv8dQKBgGf1WUk/596vqjmhGyKa\nqQuyTTGXKUCAEXduM7OedAIFoBIzHTKvFtBEDRpisDLOtwNd42RaTlgl4m5CyvJ9\nAA0aDtcPcVcVihf2QkQRy+7wb6psQMIHVL7K4MookW1Uklxy6Fdm6cQhmoWauw5n\n459M3aPoqVx/aqViD82UwLOf\n-----END PRIVATE KEY-----\n",
6
+ "client_email": "e2e-fake@venthrive-e2e-demo.iam.gserviceaccount.com",
7
+ "client_id": "000000000000000000000",
8
+ "auth_uri": "https://accounts.google.com/o/oauth2/auth",
9
+ "token_uri": "https://oauth2.googleapis.com/token",
10
+ "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
11
+ "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/e2e-fake%40venthrive-e2e-demo.iam.gserviceaccount.com",
12
+ "universe_domain": "googleapis.com"
13
+ }
tests/fixtures/groq/businessIdentity.json ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "businessName": "Ada's Fresh Kitchen",
3
+ "sector": "Food & Beverage",
4
+ "subsector": "Meal Prep & Catering",
5
+ "niche": "Healthy lunch delivery for office workers",
6
+ "summary": "Ada's Fresh Kitchen prepares and delivers healthy, affordable lunches to office workers in Lagos who don't have time to cook. Every meal is made fresh that morning using locally sourced ingredients.",
7
+ "valueProposition": "Fresh, healthy lunch delivered to your desk before 12pm, every workday.",
8
+ "brandVoice": "warm, reliable, no-nonsense",
9
+ "tagline": "Fresh food, fast desk."
10
+ }
tests/fixtures/groq/businessModel.json ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "keyPartners": ["Local vegetable and grain suppliers", "Dispatch rider network (Gokada, Max.ng)", "Office building facility managers"],
3
+ "keyActivities": ["Daily meal prep and cooking", "Order-taking and route planning", "Quality control and packaging"],
4
+ "keyResources": ["Commercial kitchen space", "Delivery riders", "Recurring customer subscription list"],
5
+ "valuePropositions": ["Fresh, healthy lunch delivered before 12pm", "Consistent quality and pricing", "Subscription convenience — no daily ordering hassle"],
6
+ "customerRelationships": ["WhatsApp broadcast list for weekly menus", "Subscription plans with loyalty discounts", "Direct feedback line for complaints"],
7
+ "channels": ["WhatsApp ordering", "Office cluster word-of-mouth", "Instagram for menu showcases"],
8
+ "customerSegments": ["Office workers in Lagos mainland business districts", "Small companies ordering team lunches"],
9
+ "costStructure": ["Ingredient sourcing", "Rider delivery fees", "Kitchen rent and utilities", "Packaging"],
10
+ "revenueStreams": ["Weekly/monthly lunch subscriptions", "One-off bulk office orders", "Corporate catering for events"]
11
+ }
tests/fixtures/groq/business_plan.json ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "executiveSummary": "Ada's Fresh Kitchen delivers fresh, healthy lunches to office workers in Lagos who lack the time to cook and are wary of street food. By preparing meals fresh each morning and delivering directly to office clusters, the business aims to become the trusted daily lunch option for busy professionals. The goal for the next 12 months is to grow from a single-cook operation to a small team serving 300+ weekly subscribers across three office clusters.",
3
+ "businessOpportunity": "Office workers in Lagos mainland business districts spend significant time and money on inconsistent lunch options — either unreliable street food or slow, expensive delivery apps. There is a clear gap for a trusted, healthy, consistently-timed lunch service.",
4
+ "companyDescription": "Ada's Fresh Kitchen is a sole-proprietor meal-prep and delivery business based in Lagos, operating out of a small commercial kitchen and delivering via contracted riders.",
5
+ "targetMarket": "Office workers aged 25-40 in Ikeja, Yaba, and Maryland business districts, middle-income, health-conscious, and time-constrained.",
6
+ "productsServices": "Daily fresh lunch plates and weekly subscription plans, with menu rotation across 5 core meals. Pricing targets ₦1,500-₦2,500 per meal, with a subscription discount for weekly commitments.",
7
+ "marketingStrategy": "WhatsApp-based ordering and weekly menu broadcasts, office-cluster word-of-mouth referrals, and Instagram content showcasing daily meals.",
8
+ "operationsPlan": "Meals are prepared each morning in a rented commercial kitchen, packaged, and dispatched via contracted riders on fixed delivery routes timed to arrive before 12pm.",
9
+ "financialSummary": "Estimated startup costs of ₦800,000 (kitchen equipment, initial ingredient stock, packaging). Break-even is projected around month 5 at approximately 150 weekly subscribers.",
10
+ "implementationTimeline": "Month 1-2: formalize subscription plan and onboard first 50 subscribers. Month 3-6: expand to a second office cluster. Month 7-12: hire a second cook and add a third delivery route."
11
+ }
tests/fixtures/groq/chat_response.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ That's a great question. For a lunch delivery business like yours, the fastest way to build repeat customers is a weekly subscription — it gives you predictable income and gives them one less decision to make each morning. Start with a simple 5-day plan at a slight discount versus ordering daily, and promote it directly to the office clusters you already deliver to. Track who renews each week so you know which offices are your strongest base.
tests/fixtures/groq/consultation_report.json ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "date": "September 26, 2026",
3
+ "sessionSummary": "We discussed customer acquisition and retention for Ada's Fresh Kitchen, focusing on how to convert one-off lunch orders into a reliable weekly subscription base.",
4
+ "keyFindings": [
5
+ "Most current orders are one-off rather than recurring, leaving revenue unpredictable week to week.",
6
+ "Word-of-mouth within office clusters is the strongest existing acquisition channel.",
7
+ "There is no simple way for customers to commit to a recurring order today."
8
+ ],
9
+ "recommendations": [
10
+ {
11
+ "action": "Launch a 5-day weekly subscription plan at a small discount versus daily ordering",
12
+ "rationale": "Gives Ada's Fresh Kitchen predictable weekly revenue and removes the daily ordering decision for customers",
13
+ "timeline": "This week"
14
+ },
15
+ {
16
+ "action": "Ask every satisfied customer to refer one colleague in their office",
17
+ "rationale": "Office clusters are already the strongest channel — formalizing referrals compounds it",
18
+ "timeline": "Within 30 days"
19
+ }
20
+ ],
21
+ "nextSteps": [
22
+ "Draft the subscription pricing and share it with 10 existing customers for feedback",
23
+ "Set up a simple WhatsApp broadcast list for weekly menu announcements",
24
+ "Track subscription renewal rate weekly for the first month"
25
+ ],
26
+ "closingNote": "Ada's Fresh Kitchen already has loyal customers — the subscription model just needs to make it easy for them to keep coming back."
27
+ }
tests/fixtures/groq/consultation_start.json ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "openingMessage": "Hi! I've reviewed the Business Footing Framework profile for Ada's Fresh Kitchen — a fresh, healthy lunch delivery service for Lagos office workers. It's a solid foundation with a clear value proposition. Today, I'd like you to pick one area to dig into together:\n\n1. Strategy & Market Position\n2. Customer Acquisition & Retention\n3. Financial Planning & Pricing\n4. Operations & Standard Processes\n5. Investor Readiness & Growth\n\nWhich one would be most useful to focus on right now?",
3
+ "focusAreas": [
4
+ "Strategy & Market Position",
5
+ "Customer Acquisition & Retention",
6
+ "Financial Planning & Pricing",
7
+ "Operations & Standard Processes",
8
+ "Investor Readiness & Growth"
9
+ ]
10
+ }
tests/fixtures/groq/customerProfile.json ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "demographics": {
3
+ "ageRange": "25-40",
4
+ "gender": "mixed, slightly more female",
5
+ "location": "Lagos mainland office districts (Ikeja, Yaba, Maryland)",
6
+ "incomeLevel": "middle",
7
+ "educationLevel": "mostly tertiary-educated professionals",
8
+ "employmentStatus": "full-time office employees"
9
+ },
10
+ "psychographics": {
11
+ "values": ["convenience", "health", "value for money", "reliability", "time"],
12
+ "lifestyle": "Busy weekday routine, limited time for meal prep, health-conscious but budget-aware.",
13
+ "painPoints": ["No time to cook during the week", "Street food feels unhealthy", "Restaurant delivery is slow and expensive"],
14
+ "motivations": ["Wants to eat healthy without the effort", "Values getting lunch on time so they don't lose their break", "Prefers a trustworthy, consistent vendor over trying something new every day"]
15
+ },
16
+ "buyerPersona": "Meet Amaka, a 29-year-old marketing executive in Ikeja. She's at her desk by 8am and rarely leaves for lunch. She used to grab suya from a roadside vendor but started worrying about hygiene and wants something that fits her fitness goals. She'd happily pay a bit more for a lunch she trusts, as long as it shows up on time."
17
+ }
tests/fixtures/groq/followups.json ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "questions": [
3
+ {
4
+ "id": "q1",
5
+ "question": "How many meals do you currently make in a typical day?",
6
+ "type": "text",
7
+ "placeholder": "e.g. around 30 meals"
8
+ },
9
+ {
10
+ "id": "q2",
11
+ "question": "Do you deliver the food yourself, or does someone else handle delivery?",
12
+ "type": "select",
13
+ "options": ["I deliver myself", "I have a rider", "Customers pick up"]
14
+ },
15
+ {
16
+ "id": "q3",
17
+ "question": "Have you had any repeat customers who order from you every week?",
18
+ "type": "boolean"
19
+ },
20
+ {
21
+ "id": "q4",
22
+ "question": "What's the biggest challenge you're facing right now?",
23
+ "type": "text",
24
+ "placeholder": "e.g. finding reliable delivery riders"
25
+ }
26
+ ]
27
+ }
tests/fixtures/groq/marketData.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "tam": "The Nigerian food delivery and meal-prep market is estimated at over ₦500 billion annually, driven by urban office workers.",
3
+ "sam": "Office-worker lunch delivery within Lagos mainland business districts — roughly ₦15 billion annually.",
4
+ "som": "A realistic first-year target is 200-400 regular subscribers across 2-3 office clusters, roughly ₦40-60 million in annual revenue.",
5
+ "swot": {
6
+ "strengths": ["Fresh, made-to-order meals", "Direct relationships with office clusters", "Low overhead (no dine-in space needed)"],
7
+ "weaknesses": ["Limited delivery radius without more riders", "Dependent on consistent ingredient sourcing", "Brand still unknown outside initial customer base"],
8
+ "opportunities": ["Growing health-consciousness among young professionals", "Rise of corporate wellness budgets that could subsidize lunch", "Partnerships with office building management"],
9
+ "threats": ["Established food delivery apps (Chowdeck, Glovo) entering the same space", "Fuel and ingredient price volatility raising delivery costs", "Traffic delays affecting on-time delivery reputation"]
10
+ },
11
+ "pestle": {
12
+ "political": "Lagos State's food safety regulations are becoming stricter — proactive NAFDAC-aligned hygiene practices are a differentiator.",
13
+ "economic": "Inflation pressures ingredient costs; subscription pricing must be revisited quarterly.",
14
+ "social": "Increasing dual-income households and long commutes are pushing more workers toward pre-made meals over home cooking.",
15
+ "technological": "WhatsApp-based ordering and mobile payments (Moniepoint, OPay) make low-tech order-taking viable without a full app.",
16
+ "legal": "Food handling permits and NAFDAC registration required for packaged meal sales.",
17
+ "environmental": "Packaging waste is a growing concern; biodegradable containers could become a selling point."
18
+ },
19
+ "competitiveLandscape": "Competes with informal roadside food vendors on price and with app-based delivery (Chowdeck, Glovo) on convenience. Differentiation comes from a curated healthy menu and direct office-cluster relationships rather than trying to out-scale the delivery apps."
20
+ }
tests/fixtures/groq/pitch_deck.json ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "slide1_problem": {
3
+ "title": "The Problem",
4
+ "bullets": ["Office workers waste time and money on unreliable lunch options", "Street food raises hygiene concerns", "Delivery apps are slow and expensive for daily use"]
5
+ },
6
+ "slide2_solution": {
7
+ "title": "Our Solution",
8
+ "bullets": ["Fresh, healthy lunch delivered before 12pm", "Direct relationships with office clusters", "Simple WhatsApp-based subscription ordering"]
9
+ },
10
+ "slide3_market": {
11
+ "title": "Market Opportunity",
12
+ "bullets": ["TAM: ₦500B+ Nigerian food delivery market", "SAM: ₦15B Lagos office-lunch segment", "SOM: 200-400 subscribers in year one, ~₦40-60M revenue"]
13
+ },
14
+ "slide4_business_model": {
15
+ "title": "How We Make Money",
16
+ "bullets": ["Weekly/monthly subscription plans", "One-off bulk office orders", "Corporate event catering"]
17
+ },
18
+ "slide5_traction": {
19
+ "title": "Traction & Milestones",
20
+ "bullets": ["Serving 3 office buildings today", "Growing repeat-customer base via word of mouth", "Planning first formal subscription launch"]
21
+ },
22
+ "slide6_strategy": {
23
+ "title": "Go-To-Market Strategy",
24
+ "bullets": ["Expand cluster by cluster, not city-wide", "Referral incentives within existing offices", "WhatsApp and Instagram over paid ads"]
25
+ },
26
+ "slide7_ask": {
27
+ "title": "The Ask",
28
+ "bullets": ["Seeking: ₦2,000,000 working capital", "Use of funds: second kitchen hire, delivery bikes, packaging", "Expected outcome: 3 office clusters served within 12 months"]
29
+ }
30
+ }
tests/fixtures/groq/planning_milestones.json ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "suggestions": [
3
+ {"title": "Launch first formal weekly subscription plan", "targetDate": "2026-10-26"},
4
+ {"title": "Reach 100 active weekly subscribers", "targetDate": "2026-11-25"},
5
+ {"title": "Expand delivery to a second office cluster", "targetDate": "2026-12-25"},
6
+ {"title": "Hire a second cook to support kitchen capacity", "targetDate": "2027-03-25"},
7
+ {"title": "Reach 300 active weekly subscribers across three clusters", "targetDate": "2027-09-26"}
8
+ ]
9
+ }
tests/fixtures/groq/planning_tasks.json ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "suggestions": [
3
+ {"title": "Draft a weekly subscription pricing sheet", "description": "Gives Ada's Fresh Kitchen a concrete offer to test with existing customers this week", "column": "todo"},
4
+ {"title": "Call 10 regular customers to pitch the subscription plan", "description": "Direct feedback validates pricing before a wider launch", "column": "todo"},
5
+ {"title": "Set up a WhatsApp broadcast list for weekly menus", "description": "Low-cost channel to keep customers engaged between orders", "column": "todo"},
6
+ {"title": "Confirm rider availability for a second delivery route", "description": "Needed before expanding to a second office cluster", "column": "todo"},
7
+ {"title": "Order packaging supplies for the next two weeks", "description": "Avoids running out mid-week during a busy order period", "column": "in_progress"}
8
+ ]
9
+ }
tests/fixtures/groq/transcript.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {
2
+ "text": "I make about thirty meals a day right now and I deliver most of them myself on my motorbike."
3
+ }
tests/test_groq_fixture_mode.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Unit tests for the E2E fixture-mode switch in utils/groq_client.py.
3
+
4
+ Scope: fixture mode is what lets the (not-yet-built) E2E/crawler suite walk
5
+ the app without spending real Groq tokens — see docs/qa/test-strategy.md and
6
+ the project-e2e-testing-design memory. These tests only verify the switch
7
+ itself behaves correctly; they don't touch real Groq (ADR-0009 keeps that
8
+ out of scope for automation).
9
+ """
10
+ import importlib
11
+ import os
12
+
13
+ import pytest
14
+
15
+
16
+ @pytest.fixture
17
+ def fixture_mode(monkeypatch):
18
+ """Reloads utils.groq_client with E2E_FIXTURE_MODE=1, then reloads it back
19
+ off afterward so other tests aren't affected by module-level state."""
20
+ monkeypatch.setenv("E2E_FIXTURE_MODE", "1")
21
+ import utils.groq_client as gc
22
+ importlib.reload(gc)
23
+ yield gc
24
+ monkeypatch.delenv("E2E_FIXTURE_MODE", raising=False)
25
+ importlib.reload(gc)
26
+
27
+
28
+ def test_call_groq_json_returns_fixture_without_network(fixture_mode):
29
+ data = fixture_mode.call_groq_json("system", "user", fixture="businessIdentity")
30
+ assert data["businessName"] == "Ada's Fresh Kitchen"
31
+
32
+
33
+ def test_call_groq_json_without_fixture_key_raises(fixture_mode):
34
+ with pytest.raises(RuntimeError, match="no fixture key"):
35
+ fixture_mode.call_groq_json("system", "user")
36
+
37
+
38
+ def test_call_groq_json_missing_fixture_file_raises(fixture_mode):
39
+ with pytest.raises(RuntimeError, match="no fixture file"):
40
+ fixture_mode.call_groq_json("system", "user", fixture="does-not-exist")
41
+
42
+
43
+ def test_call_groq_stream_yields_chunks_shaped_like_the_real_sdk(fixture_mode):
44
+ chunks = list(fixture_mode.call_groq_stream("system", "user", fixture="chat_response"))
45
+ assert len(chunks) > 1
46
+ rebuilt = "".join(c.choices[0].delta.content for c in chunks)
47
+ assert "subscription" in rebuilt
48
+
49
+
50
+ def test_call_groq_transcribe_returns_object_with_text(fixture_mode):
51
+ result = fixture_mode.call_groq_transcribe(file=("a.webm", b"", "audio/webm"), fixture="transcript")
52
+ assert "meals a day" in result.text
53
+
54
+
55
+ def test_fixture_mode_is_off_by_default():
56
+ import utils.groq_client as gc
57
+ importlib.reload(gc)
58
+ assert gc.FIXTURE_MODE is False
utils/groq_client.py CHANGED
@@ -19,8 +19,42 @@ import os
19
  import json
20
  import time
21
  import threading
 
 
22
  from groq import Groq
23
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  # ─── Load all keys ────────────────────────────────────────────────────────────
25
  # Accepts: GROQ_API_KEY, GROQ_API_KEY_1 ... GROQ_API_KEY_N (any suffix)
26
  _KEYS: list[str] = []
@@ -129,6 +163,7 @@ def call_groq_transcribe(
129
  model: str = TRANSCRIBE_MODEL,
130
  response_format: str = "json",
131
  language: str | None = None,
 
132
  ):
133
  """
134
  Calls Groq's Whisper transcription endpoint with the same key-rotation
@@ -136,6 +171,15 @@ def call_groq_transcribe(
136
 
137
  `file` is the (filename, bytes, content_type) tuple the Groq SDK expects.
138
  """
 
 
 
 
 
 
 
 
 
139
  last_error: Exception | None = None
140
 
141
  for attempt in range(len(_KEYS) + 1):
@@ -170,8 +214,17 @@ def call_groq_json(
170
  user_prompt: str,
171
  model: str = HEAVY_MODEL,
172
  max_tokens: int = 1500,
 
173
  ) -> dict:
174
  """Convenience wrapper: calls Groq in JSON mode and returns parsed dict."""
 
 
 
 
 
 
 
 
175
  raw = call_groq(system_prompt, user_prompt, model=model,
176
  json_mode=True, max_tokens=max_tokens)
177
  if not raw:
@@ -187,16 +240,33 @@ def call_groq_json(
187
  raise RuntimeError(f"Groq returned invalid JSON (likely truncated): {e}") from e
188
 
189
 
 
 
 
 
 
 
 
 
190
  def call_groq_stream(
191
  system_prompt: str,
192
  user_prompt: str,
193
  model: str = LIGHT_MODEL,
194
  max_tokens: int = 512,
 
195
  ):
196
  """
197
  Returns a Groq streaming completion object.
198
  Rotates key on 429 like all other calls.
199
  """
 
 
 
 
 
 
 
 
200
  last_error: Exception | None = None
201
  for attempt in range(len(_KEYS) + 1):
202
  try:
 
19
  import json
20
  import time
21
  import threading
22
+ from pathlib import Path
23
+ from types import SimpleNamespace
24
  from groq import Groq
25
 
26
+ # ─── E2E fixture mode ─────────────────────────────────────────────────────────
27
+ # When E2E_FIXTURE_MODE=1, every call below returns a canned fixture instead of
28
+ # hitting the real Groq API — no cost, deterministic, works offline. Used by
29
+ # the E2E/surface-discovery test suite (see docs/qa/test-strategy.md and the
30
+ # project-e2e-testing-design memory), never by production or by the unit tests
31
+ # in backend/tests/ (those stay pure-function tests with no I/O).
32
+ # Each call site passes a `fixture` key identifying which canned response it
33
+ # wants; fixture mode with no key given is a bug at the call site, not a
34
+ # silent fallback, so it raises loudly instead of guessing.
35
+ FIXTURE_MODE = os.environ.get("E2E_FIXTURE_MODE") == "1"
36
+ _FIXTURES_DIR = Path(__file__).resolve().parent.parent / "tests" / "fixtures" / "groq"
37
+
38
+
39
+ def _load_json_fixture(name: str) -> dict:
40
+ path = _FIXTURES_DIR / f"{name}.json"
41
+ if not path.exists():
42
+ raise RuntimeError(
43
+ f"[E2E_FIXTURE_MODE] no fixture file for '{name}' at {path} — "
44
+ "add one under backend/tests/fixtures/groq/"
45
+ )
46
+ return json.loads(path.read_text(encoding="utf-8"))
47
+
48
+
49
+ def _load_text_fixture(name: str) -> str:
50
+ path = _FIXTURES_DIR / f"{name}.txt"
51
+ if not path.exists():
52
+ raise RuntimeError(
53
+ f"[E2E_FIXTURE_MODE] no fixture file for '{name}' at {path} — "
54
+ "add one under backend/tests/fixtures/groq/"
55
+ )
56
+ return path.read_text(encoding="utf-8")
57
+
58
  # ─── Load all keys ────────────────────────────────────────────────────────────
59
  # Accepts: GROQ_API_KEY, GROQ_API_KEY_1 ... GROQ_API_KEY_N (any suffix)
60
  _KEYS: list[str] = []
 
163
  model: str = TRANSCRIBE_MODEL,
164
  response_format: str = "json",
165
  language: str | None = None,
166
+ fixture: str | None = None,
167
  ):
168
  """
169
  Calls Groq's Whisper transcription endpoint with the same key-rotation
 
171
 
172
  `file` is the (filename, bytes, content_type) tuple the Groq SDK expects.
173
  """
174
+ if FIXTURE_MODE:
175
+ if not fixture:
176
+ raise RuntimeError(
177
+ "[E2E_FIXTURE_MODE] call_groq_transcribe was called with no fixture key — "
178
+ "every call site must pass one when fixture mode is on"
179
+ )
180
+ data = _load_json_fixture(fixture)
181
+ return SimpleNamespace(text=data.get("text", ""))
182
+
183
  last_error: Exception | None = None
184
 
185
  for attempt in range(len(_KEYS) + 1):
 
214
  user_prompt: str,
215
  model: str = HEAVY_MODEL,
216
  max_tokens: int = 1500,
217
+ fixture: str | None = None,
218
  ) -> dict:
219
  """Convenience wrapper: calls Groq in JSON mode and returns parsed dict."""
220
+ if FIXTURE_MODE:
221
+ if not fixture:
222
+ raise RuntimeError(
223
+ "[E2E_FIXTURE_MODE] call_groq_json was called with no fixture key — "
224
+ "every call site must pass one when fixture mode is on"
225
+ )
226
+ return _load_json_fixture(fixture)
227
+
228
  raw = call_groq(system_prompt, user_prompt, model=model,
229
  json_mode=True, max_tokens=max_tokens)
230
  if not raw:
 
240
  raise RuntimeError(f"Groq returned invalid JSON (likely truncated): {e}") from e
241
 
242
 
243
+ def _fixture_stream(text: str):
244
+ """Yields fake chunks shaped like the Groq SDK's streaming response —
245
+ chunk.choices[0].delta.content — so callers don't need to know they're
246
+ in fixture mode."""
247
+ for word in text.split(" "):
248
+ yield SimpleNamespace(choices=[SimpleNamespace(delta=SimpleNamespace(content=word + " "))])
249
+
250
+
251
  def call_groq_stream(
252
  system_prompt: str,
253
  user_prompt: str,
254
  model: str = LIGHT_MODEL,
255
  max_tokens: int = 512,
256
+ fixture: str | None = None,
257
  ):
258
  """
259
  Returns a Groq streaming completion object.
260
  Rotates key on 429 like all other calls.
261
  """
262
+ if FIXTURE_MODE:
263
+ if not fixture:
264
+ raise RuntimeError(
265
+ "[E2E_FIXTURE_MODE] call_groq_stream was called with no fixture key — "
266
+ "every call site must pass one when fixture mode is on"
267
+ )
268
+ return _fixture_stream(_load_text_fixture(fixture))
269
+
270
  last_error: Exception | None = None
271
  for attempt in range(len(_KEYS) + 1):
272
  try: