CognxSafeTrack Claude Sonnet 4.6 commited on
Commit
1c602ea
·
1 Parent(s): 641e72b

feat: gold standards tests (15 days + STT + vision) & botName editable in AI setup

Browse files

- ai-gold-standards: 40 test cases covering all 15 lesson days, STT smoke, vision
(day 11 imageUrl), CRM broadcast/conversation, curriculum (FR+EN), pitch deck EN,
business profile extraction days 1-6 with quality assertions
- AIAgentSetup: botName field now editable (was hardcoded 'Agent IA'); loaded from
personality API on mount, saved with PATCH; falls back to 'Agent IA' if blank
- i18n: add bot_name_label/placeholder/hint keys to FR, EN, ES, PT locales

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

apps/admin/src/locales/en.json CHANGED
@@ -213,7 +213,10 @@
213
  "no_kb_hint": "Add your FAQs, pricing, or product sheets to activate the agent.",
214
  "stats_status_label": "Status",
215
  "stats_active_label": "Active",
216
- "words_covered": "Words covered"
 
 
 
217
  },
218
  "livefeed": {
219
  "title": "Live Feed",
 
213
  "no_kb_hint": "Add your FAQs, pricing, or product sheets to activate the agent.",
214
  "stats_status_label": "Status",
215
  "stats_active_label": "Active",
216
+ "words_covered": "Words covered",
217
+ "bot_name_label": "Agent name",
218
+ "bot_name_placeholder": "E.g. Kora, Awa, SupportBot...",
219
+ "bot_name_hint": "The name your agent will use to introduce itself on WhatsApp."
220
  },
221
  "livefeed": {
222
  "title": "Live Feed",
apps/admin/src/locales/es.json CHANGED
@@ -335,7 +335,10 @@
335
  "no_kb_hint": "Agrega tus FAQ, tarifas o fichas de producto para activar el agente.",
336
  "stats_status_label": "Estado",
337
  "stats_active_label": "Activo",
338
- "words_covered": "Palabras cubiertas"
 
 
 
339
  },
340
  "tracks": {
341
  "title": "Cursos",
 
335
  "no_kb_hint": "Agrega tus FAQ, tarifas o fichas de producto para activar el agente.",
336
  "stats_status_label": "Estado",
337
  "stats_active_label": "Activo",
338
+ "words_covered": "Palabras cubiertas",
339
+ "bot_name_label": "Nombre del agente",
340
+ "bot_name_placeholder": "Ej. Kora, Awa, SupportBot...",
341
+ "bot_name_hint": "El nombre que su agente usará para presentarse en WhatsApp."
342
  },
343
  "tracks": {
344
  "title": "Cursos",
apps/admin/src/locales/fr.json CHANGED
@@ -335,7 +335,10 @@
335
  "no_kb_hint": "Ajoutez vos FAQ, tarifs ou fiches produit pour activer l'agent.",
336
  "stats_status_label": "Statut",
337
  "stats_active_label": "Actif",
338
- "words_covered": "Mots couverts"
 
 
 
339
  },
340
  "tracks": {
341
  "title": "Parcours",
 
335
  "no_kb_hint": "Ajoutez vos FAQ, tarifs ou fiches produit pour activer l'agent.",
336
  "stats_status_label": "Statut",
337
  "stats_active_label": "Actif",
338
+ "words_covered": "Mots couverts",
339
+ "bot_name_label": "Nom de l'agent",
340
+ "bot_name_placeholder": "Ex: Kora, Awa, SupportBot...",
341
+ "bot_name_hint": "Le prénom que votre agent utilisera pour se présenter sur WhatsApp."
342
  },
343
  "tracks": {
344
  "title": "Parcours",
apps/admin/src/locales/pt.json CHANGED
@@ -335,7 +335,10 @@
335
  "no_kb_hint": "Adicione as suas FAQs, preços ou fichas de produto para ativar o agente.",
336
  "stats_status_label": "Estado",
337
  "stats_active_label": "Ativo",
338
- "words_covered": "Palavras cobertas"
 
 
 
339
  },
340
  "tracks": {
341
  "title": "Cursos",
 
335
  "no_kb_hint": "Adicione as suas FAQs, preços ou fichas de produto para ativar o agente.",
336
  "stats_status_label": "Estado",
337
  "stats_active_label": "Ativo",
338
+ "words_covered": "Palavras cobertas",
339
+ "bot_name_label": "Nome do agente",
340
+ "bot_name_placeholder": "Ex: Kora, Awa, SupportBot...",
341
+ "bot_name_hint": "O nome que seu agente usará para se apresentar no WhatsApp."
342
  },
343
  "tracks": {
344
  "title": "Cursos",
apps/admin/src/pages/AIAgentSetup.tsx CHANGED
@@ -44,6 +44,7 @@ export default function AIAgentSetup() {
44
  const [uploadStatus, setUploadStatus] = useState<'IDLE' | 'UPLOADING' | 'SUCCESS' | 'ERROR'>('IDLE');
45
  const [uploadError, setUploadError] = useState('');
46
 
 
47
  const [role, setRole] = useState('');
48
  const [selectedTone, setSelectedTone] = useState<string>('Professionnel');
49
  const [recommendedTone, setRecommendedTone] = useState<string | null>(null);
@@ -81,6 +82,7 @@ export default function AIAgentSetup() {
81
  if (!personality?.toneDescription) setSelectedTone(rec);
82
  }
83
  if (personality) {
 
84
  setRole(personality.coreMission || '');
85
  if (personality.toneDescription) setSelectedTone(personality.toneDescription);
86
  setSelectedTemplateName(personality.notificationTemplateName || '');
@@ -129,7 +131,7 @@ export default function AIAgentSetup() {
129
  setSaveStatus('SAVING');
130
  try {
131
  await api.patch(`/v1/organizations/${selectedOrgId}/personality`, {
132
- botName: 'Agent IA',
133
  coreMission: role,
134
  toneDescription: selectedTone,
135
  notificationTemplateName: selectedTemplateName || null,
@@ -235,6 +237,18 @@ export default function AIAgentSetup() {
235
  <span>🧠</span> {t('ai_setup.personality_title')}
236
  </h2>
237
  <div className="space-y-4">
 
 
 
 
 
 
 
 
 
 
 
 
238
  <div>
239
  <label className="block text-sm font-medium text-slate-600 mb-1">{t('ai_setup.role_label')}</label>
240
  <input
 
44
  const [uploadStatus, setUploadStatus] = useState<'IDLE' | 'UPLOADING' | 'SUCCESS' | 'ERROR'>('IDLE');
45
  const [uploadError, setUploadError] = useState('');
46
 
47
+ const [botName, setBotName] = useState('');
48
  const [role, setRole] = useState('');
49
  const [selectedTone, setSelectedTone] = useState<string>('Professionnel');
50
  const [recommendedTone, setRecommendedTone] = useState<string | null>(null);
 
82
  if (!personality?.toneDescription) setSelectedTone(rec);
83
  }
84
  if (personality) {
85
+ setBotName(personality.botName || '');
86
  setRole(personality.coreMission || '');
87
  if (personality.toneDescription) setSelectedTone(personality.toneDescription);
88
  setSelectedTemplateName(personality.notificationTemplateName || '');
 
131
  setSaveStatus('SAVING');
132
  try {
133
  await api.patch(`/v1/organizations/${selectedOrgId}/personality`, {
134
+ botName: botName.trim() || 'Agent IA',
135
  coreMission: role,
136
  toneDescription: selectedTone,
137
  notificationTemplateName: selectedTemplateName || null,
 
237
  <span>🧠</span> {t('ai_setup.personality_title')}
238
  </h2>
239
  <div className="space-y-4">
240
+ <div>
241
+ <label className="block text-sm font-medium text-slate-600 mb-1">{t('ai_setup.bot_name_label')}</label>
242
+ <input
243
+ type="text"
244
+ value={botName}
245
+ onChange={e => setBotName(e.target.value)}
246
+ placeholder={t('ai_setup.bot_name_placeholder')}
247
+ maxLength={50}
248
+ className="w-full px-4 py-3 border border-slate-200 rounded-xl focus:ring-2 focus:ring-emerald-500 outline-none"
249
+ />
250
+ <p className="text-[11px] text-slate-400 mt-1.5">{t('ai_setup.bot_name_hint')}</p>
251
+ </div>
252
  <div>
253
  <label className="block text-sm font-medium text-slate-600 mb-1">{t('ai_setup.role_label')}</label>
254
  <input
apps/api/test/regression/ai-gold-standards.test.ts CHANGED
@@ -1,6 +1,7 @@
1
  /**
2
  * Gold Standards — AI Regression Suite
3
- * Covers: feedback quality, lesson generation, CRM, multi-language, business profiling
 
4
  * Run: pnpm --filter api exec vitest run test/regression/ai-gold-standards.test.ts
5
  */
6
  import { describe, it, expect, beforeAll } from 'vitest';
@@ -10,8 +11,8 @@ import path from 'path';
10
  dotenv.config({ path: path.resolve(__dirname, '../../../.env') });
11
 
12
  const TIMEOUT = 45_000;
 
13
 
14
- // Shared service instance (dynamic import after dotenv)
15
  let ai: Awaited<ReturnType<typeof import('../../src/services/ai')>>['aiService'];
16
 
17
  beforeAll(async () => {
@@ -19,6 +20,27 @@ beforeAll(async () => {
19
  ai = mod.aiService;
20
  });
21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  // ---------------------------------------------------------------------------
23
  // Feedback quality
24
  // ---------------------------------------------------------------------------
@@ -28,7 +50,7 @@ describe('Feedback — Language & Quality Guards', () => {
28
  const result = await ai.generateFeedback(
29
  'Damay jaay jus guir am xaliss ci Dakar',
30
  'Décris ton projet de vente.',
31
- 'Leçon sur le Business Model de vente directe.',
32
  'WOLOF',
33
  { activityLabel: 'Vente de jus', region: 'Dakar' }
34
  );
@@ -45,7 +67,7 @@ describe('Feedback — Language & Quality Guards', () => {
45
  const result = await ai.generateFeedback(
46
  'Je vends des pagnes au marché',
47
  'Décris ton modèle économique.',
48
- 'Leçon Jour 3 — Modèle de revenus.',
49
  'FR',
50
  { activityLabel: 'Commerce de pagnes', region: 'Thiès' }
51
  );
@@ -58,19 +80,37 @@ describe('Feedback — Language & Quality Guards', () => {
58
  const result = await ai.generateFeedback(
59
  'I sell handmade jewelry online',
60
  'Describe your revenue model.',
61
- 'Day 3 lesson — Revenue streams.',
62
  'EN',
63
  { activityLabel: 'Jewelry', region: 'Abidjan' }
64
  );
65
  if (result.aiSource === 'MOCK') return;
66
  const text = result.validation.toLowerCase();
67
- // Simple heuristic: should not start with common French phrases
68
  expect(text).not.toMatch(/^(bonjour|salut|bien)/);
69
  }, TIMEOUT);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  });
71
 
72
  // ---------------------------------------------------------------------------
73
- // Business profile extraction
74
  // ---------------------------------------------------------------------------
75
 
76
  describe('Business Profile Extraction', () => {
@@ -87,41 +127,121 @@ describe('Business Profile Extraction', () => {
87
  expect(result.activityLabel.toLowerCase()).toMatch(/épicerie|commerce|alimentation|vente/);
88
  }, TIMEOUT);
89
 
90
- it('Day 2 — extracts region from context', async () => {
91
- const result = await ai.extractBusinessProfile('Je travaille à Saint-Louis du Sénégal', 2, 'FR');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  if (result.aiSource === 'MOCK') return;
93
  expect(result).toBeDefined();
94
  }, TIMEOUT);
95
  });
96
 
97
  // ---------------------------------------------------------------------------
98
- // Personalized lesson generation
99
  // ---------------------------------------------------------------------------
100
 
101
- describe('Lesson Generation — Personalization', () => {
102
- it('generates lesson for Jour 3 (modèle de revenus) with correct structure', async () => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  const result = await ai.generatePersonalizedLesson(
104
- 3,
105
- { activityLabel: 'Vente de beignets', region: 'Dakar', language: 'FR' },
 
 
 
 
 
 
 
 
 
106
  'FR'
107
  );
108
- expect(result).toBeDefined();
109
  if (result.aiSource === 'MOCK') return;
110
- // Lesson should reference the user's activity
111
- const text = JSON.stringify(result).toLowerCase();
112
- expect(text).toMatch(/beignet|vente|revenu/);
113
  }, TIMEOUT);
114
 
115
- it('generates lesson for Jour 7 (stratégie marketing) in Wolof', async () => {
116
  const result = await ai.generatePersonalizedLesson(
117
- 7,
118
- { activityLabel: 'Couture traditionnelle', region: 'Thiès', language: 'WOLOF' },
119
- 'WOLOF'
 
 
 
 
120
  );
121
- expect(result).toBeDefined();
122
  }, TIMEOUT);
123
  });
124
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  // ---------------------------------------------------------------------------
126
  // CRM — campaign & broadcast
127
  // ---------------------------------------------------------------------------
@@ -136,7 +256,7 @@ describe('CRM — Campaign Generation', () => {
136
  }, TIMEOUT);
137
 
138
  it('generates broadcast message in campaign format', async () => {
139
- const result = await ai.generateBroadcastMessage('Invite tous les contacts à notre webinaire gratuit sur l\'entrepreneuriat féminin');
140
  expect(result.text).toBeDefined();
141
  expect(['campaign', 'support']).toContain(result.type);
142
  }, TIMEOUT);
@@ -149,10 +269,18 @@ describe('CRM — Campaign Generation', () => {
149
  );
150
  expect(result.message).toBeDefined();
151
  if (result.aiSource === 'MOCK') return;
152
- // Should guide toward importing
153
  const text = result.message.toLowerCase();
154
  expect(text).toMatch(/import|fichier|liste|contact/);
155
  }, TIMEOUT);
 
 
 
 
 
 
 
 
 
156
  });
157
 
158
  // ---------------------------------------------------------------------------
@@ -166,7 +294,7 @@ describe('Pitch Deck & One-Pager', () => {
166
  'FR'
167
  );
168
  expect(result.slides).toHaveLength(13);
169
- }, 60_000);
170
 
171
  it('one-pager: returns expected top-level keys', async () => {
172
  const result = await ai.generateOnePagerData(
@@ -176,10 +304,46 @@ describe('Pitch Deck & One-Pager', () => {
176
  expect(result).toHaveProperty('summary');
177
  expect(result).toHaveProperty('sections');
178
  }, TIMEOUT);
 
 
 
 
 
 
 
 
 
 
179
  });
180
 
181
  // ---------------------------------------------------------------------------
182
- // Free text generation (generic guard)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
  // ---------------------------------------------------------------------------
184
 
185
  describe('generateText — safety & format', () => {
@@ -190,4 +354,14 @@ describe('generateText — safety & format', () => {
190
  );
191
  expect(result.text.trim().length).toBeGreaterThan(5);
192
  }, TIMEOUT);
 
 
 
 
 
 
 
 
 
 
193
  });
 
1
  /**
2
  * Gold Standards — AI Regression Suite
3
+ * Covers: feedback quality, lesson generation (days 1-15), CRM, multi-language,
4
+ * business profiling, curriculum, STT smoke test, vision feedback
5
  * Run: pnpm --filter api exec vitest run test/regression/ai-gold-standards.test.ts
6
  */
7
  import { describe, it, expect, beforeAll } from 'vitest';
 
11
  dotenv.config({ path: path.resolve(__dirname, '../../../.env') });
12
 
13
  const TIMEOUT = 45_000;
14
+ const LONG_TIMEOUT = 75_000;
15
 
 
16
  let ai: Awaited<ReturnType<typeof import('../../src/services/ai')>>['aiService'];
17
 
18
  beforeAll(async () => {
 
20
  ai = mod.aiService;
21
  });
22
 
23
+ // ---------------------------------------------------------------------------
24
+ // Sample lesson content used across day coverage tests
25
+ // ---------------------------------------------------------------------------
26
+ const LESSONS: Record<number, string> = {
27
+ 1: "Bienvenue dans la formation. Aujourd'hui tu vas explorer ton idée de business et définir ton activité principale.",
28
+ 2: "Jour 2 : Identifie ton client idéal. Qui achète ton produit ? Quels sont ses problèmes quotidiens ?",
29
+ 3: "Jour 3 : Modèle de revenus. Comment gagnes-tu de l'argent ? Vente directe, abonnement, commission ?",
30
+ 4: "Jour 4 : Analyse de tes coûts. Quelles sont les charges fixes et variables de ton activité ?",
31
+ 5: "Jour 5 : Stratégie marketing. Comment attires-tu tes clients ? WhatsApp, bouche-à-oreille, réseaux ?",
32
+ 6: "Jour 6 : Concurrence. Qui sont tes concurrents directs ? Quelle est ta différence ?",
33
+ 7: "Jour 7 : Plan marketing complet. Définis ton budget communication et tes canaux prioritaires.",
34
+ 8: "Jour 8 : Communication client. Comment fidélises-tu tes acheteurs après la vente ?",
35
+ 9: "Jour 9 : Prévisions financières. Projette tes revenus sur les 3 prochains mois.",
36
+ 10: "Jour 10 : Business plan simplifié. Consolide tous les éléments de ta stratégie.",
37
+ 11: "Jour 11 : Ton équipe. Qui travaille avec toi ? Quelles compétences manquent dans ton équipe ?",
38
+ 12: "Jour 12 : Le pitch. Présente ton projet en 2 minutes de manière convaincante.",
39
+ 13: "Jour 13 : Revue complète. Analyse tes forces et tes axes d'amélioration.",
40
+ 14: "Jour 14 : Plan d'action. Définis 3 objectifs SMART pour les 30 prochains jours.",
41
+ 15: "Jour 15 : Certification. Tu as complété la formation. Fais le bilan de ton parcours.",
42
+ };
43
+
44
  // ---------------------------------------------------------------------------
45
  // Feedback quality
46
  // ---------------------------------------------------------------------------
 
50
  const result = await ai.generateFeedback(
51
  'Damay jaay jus guir am xaliss ci Dakar',
52
  'Décris ton projet de vente.',
53
+ LESSONS[3],
54
  'WOLOF',
55
  { activityLabel: 'Vente de jus', region: 'Dakar' }
56
  );
 
67
  const result = await ai.generateFeedback(
68
  'Je vends des pagnes au marché',
69
  'Décris ton modèle économique.',
70
+ LESSONS[3],
71
  'FR',
72
  { activityLabel: 'Commerce de pagnes', region: 'Thiès' }
73
  );
 
80
  const result = await ai.generateFeedback(
81
  'I sell handmade jewelry online',
82
  'Describe your revenue model.',
83
+ LESSONS[3],
84
  'EN',
85
  { activityLabel: 'Jewelry', region: 'Abidjan' }
86
  );
87
  if (result.aiSource === 'MOCK') return;
88
  const text = result.validation.toLowerCase();
 
89
  expect(text).not.toMatch(/^(bonjour|salut|bien)/);
90
  }, TIMEOUT);
91
+
92
+ it('Day 11 feedback with imageUrl: vision path does not crash', async () => {
93
+ const result = await ai.generateFeedback(
94
+ 'Voici mon équipe : Fatou est chargée des ventes et Moussa gère la logistique.',
95
+ 'Décris les membres de ton équipe.',
96
+ LESSONS[11],
97
+ 'FR',
98
+ { activityLabel: 'Vente de vêtements', region: 'Dakar' }, // businessProfile
99
+ undefined, // exerciseCriteria
100
+ 'Vente de vêtements', // userActivity
101
+ 'Dakar', // userRegion
102
+ 11, // dayNumber
103
+ undefined, // previousResponses
104
+ false, // isDeepDive
105
+ 0, // iterationCount
106
+ 'https://upload.wikimedia.org/wikipedia/commons/thumb/3/3f/Fronalpstock_big.jpg/800px-Fronalpstock_big.jpg' // imageUrl
107
+ );
108
+ expect(result.validation).toBeDefined();
109
+ }, TIMEOUT);
110
  });
111
 
112
  // ---------------------------------------------------------------------------
113
+ // Business profile extraction — days 1-6
114
  // ---------------------------------------------------------------------------
115
 
116
  describe('Business Profile Extraction', () => {
 
127
  expect(result.activityLabel.toLowerCase()).toMatch(/épicerie|commerce|alimentation|vente/);
128
  }, TIMEOUT);
129
 
130
+ it('Day 2 — extracts customer profile from French input', async () => {
131
+ const result = await ai.extractBusinessProfile('Mes clients sont des femmes qui cherchent des tontines en ligne', 2, 'FR');
132
+ if (result.aiSource === 'MOCK') return;
133
+ expect(result).toBeDefined();
134
+ expect(result.mainCustomer).toBeTruthy();
135
+ }, TIMEOUT);
136
+
137
+ it('Day 3 — revenue model extraction', async () => {
138
+ const result = await ai.extractBusinessProfile('Je vends mes services en forfait mensuel à 15 000 FCFA', 3, 'FR');
139
+ if (result.aiSource === 'MOCK') return;
140
+ expect(result).toBeDefined();
141
+ }, TIMEOUT);
142
+
143
+ it('Day 4 — cost analysis extraction', async () => {
144
+ const result = await ai.extractBusinessProfile('Mon loyer est 50 000 FCFA et matières premières 20 000 FCFA par mois', 4, 'FR');
145
+ if (result.aiSource === 'MOCK') return;
146
+ expect(result).toBeDefined();
147
+ }, TIMEOUT);
148
+
149
+ it('Day 5 — marketing channel extraction', async () => {
150
+ const result = await ai.extractBusinessProfile("J'utilise WhatsApp et les marchés hebdomadaires pour attirer des clients", 5, 'FR');
151
+ if (result.aiSource === 'MOCK') return;
152
+ expect(result).toBeDefined();
153
+ }, TIMEOUT);
154
+
155
+ it('Day 6 — competitive analysis extraction', async () => {
156
+ const result = await ai.extractBusinessProfile("Il y a 3 boutiques similaires dans mon quartier mais elles ne livrent pas", 6, 'FR');
157
  if (result.aiSource === 'MOCK') return;
158
  expect(result).toBeDefined();
159
  }, TIMEOUT);
160
  });
161
 
162
  // ---------------------------------------------------------------------------
163
+ // Personalized lesson generation — days 1-15
164
  // ---------------------------------------------------------------------------
165
 
166
+ describe('Lesson Generation — Full Day Coverage', () => {
167
+ const userActivity = 'Vente de beignets';
168
+
169
+ for (const [dayStr, lessonText] of Object.entries(LESSONS)) {
170
+ const day = Number(dayStr);
171
+ it(`Day ${day} — personalizes lesson in FR`, async () => {
172
+ const result = await ai.generatePersonalizedLesson(
173
+ lessonText,
174
+ userActivity,
175
+ 'FR'
176
+ );
177
+ expect(result.lessonText).toBeDefined();
178
+ if (result.aiSource === 'MOCK') return;
179
+ expect(result.lessonText.length).toBeGreaterThan(20);
180
+ }, TIMEOUT);
181
+ }
182
+
183
+ it('Day 7 — personalizes lesson in WOLOF', async () => {
184
  const result = await ai.generatePersonalizedLesson(
185
+ LESSONS[7],
186
+ 'Couture traditionnelle',
187
+ 'WOLOF'
188
+ );
189
+ expect(result.lessonText).toBeDefined();
190
+ }, TIMEOUT);
191
+
192
+ it('Day 3 — lesson references user activity', async () => {
193
+ const result = await ai.generatePersonalizedLesson(
194
+ LESSONS[3],
195
+ 'Atelier de couture',
196
  'FR'
197
  );
 
198
  if (result.aiSource === 'MOCK') return;
199
+ // The personalized lesson should acknowledge the activity domain
200
+ expect(result.lessonText.toLowerCase()).toMatch(/couture|vêtement|tissu|atelier|revenu/);
 
201
  }, TIMEOUT);
202
 
203
+ it('Day 15 lesson with previous responses context', async () => {
204
  const result = await ai.generatePersonalizedLesson(
205
+ LESSONS[15],
206
+ 'Livraison de repas',
207
+ 'FR',
208
+ [
209
+ { day: 1, response: "Je livre des plats cuisinés à domicile" },
210
+ { day: 3, response: "Je facture 3 000 FCFA par livraison" },
211
+ ]
212
  );
213
+ expect(result.lessonText).toBeDefined();
214
  }, TIMEOUT);
215
  });
216
 
217
+ // ---------------------------------------------------------------------------
218
+ // Curriculum generation
219
+ // ---------------------------------------------------------------------------
220
+
221
+ describe('Curriculum Generation', () => {
222
+ it('generates 5-day curriculum for a Dakar market', async () => {
223
+ const result = await ai.generateCurriculum(
224
+ 'Formation à la vente et au digital pour commerçantes de Dakar',
225
+ 5,
226
+ 'FR',
227
+ 'Femmes entrepreneures, marché informel'
228
+ );
229
+ expect(result.days).toHaveLength(5);
230
+ expect(result.days[0]).toHaveProperty('title');
231
+ expect(result.days[0]).toHaveProperty('content');
232
+ }, LONG_TIMEOUT);
233
+
234
+ it('generates 10-day curriculum in English', async () => {
235
+ const result = await ai.generateCurriculum(
236
+ 'Digital marketing fundamentals for SME owners in Abidjan',
237
+ 10,
238
+ 'EN',
239
+ 'Small business owners, 25-45 years'
240
+ );
241
+ expect(result.days).toHaveLength(10);
242
+ }, LONG_TIMEOUT);
243
+ });
244
+
245
  // ---------------------------------------------------------------------------
246
  // CRM — campaign & broadcast
247
  // ---------------------------------------------------------------------------
 
256
  }, TIMEOUT);
257
 
258
  it('generates broadcast message in campaign format', async () => {
259
+ const result = await ai.generateBroadcastMessage("Invite tous les contacts à notre webinaire gratuit sur l'entrepreneuriat féminin");
260
  expect(result.text).toBeDefined();
261
  expect(['campaign', 'support']).toContain(result.type);
262
  }, TIMEOUT);
 
269
  );
270
  expect(result.message).toBeDefined();
271
  if (result.aiSource === 'MOCK') return;
 
272
  const text = result.message.toLowerCase();
273
  expect(text).toMatch(/import|fichier|liste|contact/);
274
  }, TIMEOUT);
275
+
276
+ it('CRM conversation: responds to broadcast request', async () => {
277
+ const result = await ai.handleCrmConversation(
278
+ "Je veux envoyer un message promotionnel à tous mes clients",
279
+ [],
280
+ { hasContactList: true, contactCount: 150 }
281
+ );
282
+ expect(result.message).toBeDefined();
283
+ }, TIMEOUT);
284
  });
285
 
286
  // ---------------------------------------------------------------------------
 
294
  'FR'
295
  );
296
  expect(result.slides).toHaveLength(13);
297
+ }, LONG_TIMEOUT);
298
 
299
  it('one-pager: returns expected top-level keys', async () => {
300
  const result = await ai.generateOnePagerData(
 
304
  expect(result).toHaveProperty('summary');
305
  expect(result).toHaveProperty('sections');
306
  }, TIMEOUT);
307
+
308
+ it('pitch deck: generated in English', async () => {
309
+ const result = await ai.generatePitchDeckData(
310
+ 'Mobile payment solution for informal traders in West Africa.',
311
+ 'EN'
312
+ );
313
+ expect(result.slides).toHaveLength(13);
314
+ if (result.aiSource === 'MOCK') return;
315
+ expect(JSON.stringify(result.slides).toLowerCase()).toMatch(/market|revenue|product|team/);
316
+ }, LONG_TIMEOUT);
317
  });
318
 
319
  // ---------------------------------------------------------------------------
320
+ // STT smoke test (skipped if no real audio — requires OPENAI_API_KEY)
321
+ // ---------------------------------------------------------------------------
322
+
323
+ describe('Audio Transcription — STT Smoke', () => {
324
+ it('transcribeAudio: returns defined result or throws provider error (not crash)', async () => {
325
+ if (!process.env.OPENAI_API_KEY) {
326
+ console.log('[SKIP] OPENAI_API_KEY not set — skipping STT smoke test');
327
+ return;
328
+ }
329
+ // Minimal silent OGG/Opus frame (24 bytes header) — Whisper will return empty string, not crash
330
+ const silentOgg = Buffer.from(
331
+ '4f67675300020000000000000000a2a2a21e00000000000000000000000000',
332
+ 'hex'
333
+ );
334
+ try {
335
+ const result = await ai.transcribeAudio(silentOgg, 'test.ogg', 'fr');
336
+ // Either returns empty transcription or throws a provider error — both are acceptable
337
+ expect(result).toBeDefined();
338
+ } catch (err: any) {
339
+ // Provider-level error (invalid audio) is acceptable; a crash/unhandled rejection is not
340
+ expect(err.message).toBeDefined();
341
+ }
342
+ }, TIMEOUT);
343
+ });
344
+
345
+ // ---------------------------------------------------------------------------
346
+ // Free text generation
347
  // ---------------------------------------------------------------------------
348
 
349
  describe('generateText — safety & format', () => {
 
354
  );
355
  expect(result.text.trim().length).toBeGreaterThan(5);
356
  }, TIMEOUT);
357
+
358
+ it('responds to English prompt in English', async () => {
359
+ const result = await ai.generateText(
360
+ 'You are a business coach.',
361
+ 'Give me one sales tip in one sentence.'
362
+ );
363
+ if (result.aiSource === 'MOCK') return;
364
+ expect(result.text.trim().length).toBeGreaterThan(5);
365
+ expect(result.text.toLowerCase()).not.toMatch(/^(bonjour|salut)/);
366
+ }, TIMEOUT);
367
  });