apps/web/src/lib/aiFlows.test.ts
1import { describe, expect, test } from 'bun:test'
2import {
3 AGENDA_INTEREST_QUESTION,
4 acceptedAgendaEmail,
5 acceptedSalesOffer,
6 asksRestrictedSessionAccess,
7 isCommercialInformationRequest,
8 isCommercialLogisticsQuestion,
9 isContentInformationRequest,
10 isConversationClosing,
11 isSelfServiceApplication,
12 isTicketInformationRequest,
13 isTicketSalesConversation,
14 needsAgendaInterests,
15 requestsTicketBooking,
16} from './aiFlows'
17
18test('eligibility and self-service applications do not enter contact collection', () => {
19 expect(isCommercialInformationRequest('Can I get an HR Leader pass?')).toBe(
20 true,
21 )
22 expect(
23 isCommercialInformationRequest('What sponsorship packages are available?'),
24 ).toBe(true)
25 expect(
26 isCommercialInformationRequest(
27 'Can you contact me about sponsorship packages?',
28 ),
29 ).toBe(false)
30 expect(isCommercialInformationRequest('I want to buy two tickets')).toBe(false)
31 expect(
32 isCommercialInformationRequest(
33 'We are thinking 3 tickets. What are the pass types and pricing?',
34 ),
35 ).toBe(true)
36 expect(
37 isSelfServiceApplication(
38 "Thanks, I'll submit an application through that page then.",
39 ),
40 ).toBe(true)
41 expect(isSelfServiceApplication('Could you help me apply?')).toBe(false)
42})
43
44test('restricted session access questions cannot be satisfied by badge recommendations', () => {
45 expect(
46 asksRestrictedSessionAccess(
47 'Am I eligible for the invite-only CHRO Summit?',
48 [],
49 ),
50 ).toBe(true)
51 expect(
52 asksRestrictedSessionAccess('Would upgrading to Diamond give me access?', [
53 { role: 'user', text: 'Can I attend the invitation-only summit?' },
54 ]),
55 ).toBe(true)
56 expect(
57 asksRestrictedSessionAccess(
58 'Build an agenda without invite-only sessions',
59 [],
60 ),
61 ).toBe(false)
62})
63
64test('polite closing does not become another flow offer or consent', () => {
65 expect(isConversationClosing("Thanks, that's all.")).toBe(true)
66 expect(isConversationClosing('Thank you for your help!')).toBe(true)
67 expect(isConversationClosing('Thanks, please send my enquiry.')).toBe(false)
68 expect(isConversationClosing('Yes please')).toBe(false)
69})
70
71test('content discovery cannot become ticket qualification', () => {
72 expect(
73 isContentInformationRequest(
74 "I'm wanting to find out more about the future of work.",
75 ),
76 ).toBe(true)
77 expect(isContentInformationRequest('Show me AI in HR articles')).toBe(true)
78 expect(isContentInformationRequest('I want to buy two tickets')).toBe(false)
79 expect(isContentInformationRequest('Build my AI agenda')).toBe(false)
80})
81
82test('agenda flow does not invent interests or reopen ticket qualification', () => {
83 expect(needsAgendaInterests([], 'can you build me an agenda?', [])).toBe(true)
84 expect(
85 needsAgendaInterests(
86 [{ role: 'assistant', text: AGENDA_INTEREST_QUESTION }],
87 'yes pleas',
88 [],
89 ),
90 ).toBe(true)
91 expect(needsAgendaInterests([], 'build me an agenda about AI', [])).toBe(false)
92 expect(
93 isTicketSalesConversation([
94 { role: 'user', text: 'I want tickets' },
95 { role: 'user', text: 'can you build me an agenda?' },
96 ]),
97 ).toBe(false)
98})
99
100test('short acceptance after a long plan advances to email', () => {
101 const history = [
102 {
103 role: 'assistant' as const,
104 text:
105 'Session details '.repeat(600) +
106 'Would you like me to email your selected agenda?',
107 },
108 ]
109 for (const reply of ['yep', 'yes', 'yes please'])
110 expect(acceptedAgendaEmail(history, reply)).toBe(true)
111 expect(acceptedAgendaEmail(history, 'change the agenda')).toBe(false)
112})
113
114const offer = {
115 role: 'assistant' as const,
116 text:
117 'Options are on [Sponsor UNLEASH Paris 2026](/events/unleash-paris/exhibition/sponsorship-opportunities). Would you like our sales team to get in touch?',
118}
119
120describe('ticket sales routing', () => {
121 test('explicit buying intent starts capture, informational questions do not', () => {
122 for (const message of [
123 'I want to buy 8 tickets',
124 "I'd like to book a pass",
125 'We would like to buy tickets',
126 'Can you help me book tickets?',
127 ]) {
128 expect(requestsTicketBooking(message)).toBe(true)
129 }
130 for (const message of [
131 'How much are tickets?',
132 'How do I buy tickets?',
133 'What passes are available?',
134 'I do not want to buy tickets',
135 'How do I cancel tickets?',
136 ]) {
137 expect(requestsTicketBooking(message)).toBe(false)
138 }
139 })
140 test('recognises a ticket sales connection offer', () => {
141 expect(
142 acceptedSalesOffer(
143 [
144 {
145 role: 'assistant',
146 text: 'Would you like me to connect you with our ticket sales team?',
147 },
148 ],
149 'yes please',
150 ),
151 ).toBe(true)
152 })
153 test('accepting ticket help does not enter sponsorship qualification', () => {
154 const history = [
155 { role: 'user' as const, text: 'I want 8 tickets for my team' },
156 offer,
157 ]
158 expect(acceptedSalesOffer(history, 'yes please')).toBe(true)
159 expect(isTicketSalesConversation(history)).toBe(true)
160 })
161 test('the latest user topic wins; assistant badge copy cannot change it', () => {
162 expect(
163 isTicketSalesConversation([
164 { role: 'user', text: 'I need tickets' },
165 { role: 'user', text: 'Actually I want to exhibit' },
166 { role: 'assistant', text: 'Packages include tickets' },
167 ]),
168 ).toBe(false)
169 expect(
170 isTicketSalesConversation([
171 { role: 'user', text: 'What sponsorship is available?' },
172 { role: 'user', text: 'I just want to buy a pass' },
173 ]),
174 ).toBe(true)
175 })
176})
177
178describe('acceptedSalesOffer', () => {
179 test('a bare yes right after the offer', () => {
180 expect(
181 acceptedSalesOffer(
182 [{ role: 'user', text: 'can i sponsor?' }, offer],
183 'yes please',
184 ),
185 ).toBe(true)
186 expect(acceptedSalesOffer([offer], 'Sure!')).toBe(true)
187 expect(acceptedSalesOffer([offer], 'that would be great.')).toBe(true)
188 })
189
190 test('not when the offer was not the last assistant turn, or the answer carries content', () => {
191 expect(
192 acceptedSalesOffer(
193 [
194 offer,
195 { role: 'user', text: 'yes' },
196 { role: 'assistant', text: 'What is your goal?' },
197 ],
198 'yes',
199 ),
200 ).toBe(false)
201 expect(
202 acceptedSalesOffer([offer], 'yes, we are Acme and want lead generation'),
203 ).toBe(false)
204 expect(acceptedSalesOffer([offer], 'no thanks')).toBe(false)
205 expect(
206 acceptedSalesOffer(
207 [
208 {
209 role: 'assistant',
210 text: 'Would you like me to send this agenda to your email?',
211 },
212 ],
213 'yes',
214 ),
215 ).toBe(false)
216 })
217})
218
219test('natural ticket requests from the failed conversations start qualification', () => {
220 for (const text of [
221 'can you get me a diamond ticket?',
222 'can you get me a diamond badge',
223 'Book me two tickets',
224 'I need two tickets',
225 'I want to buy tickets, not sponsor',
226 ]) {
227 expect(requestsTicketBooking(text)).toBe(true)
228 }
229 const history = [
230 {
231 role: 'assistant' as const,
232 text: 'General Admission is our core ticket. Would you like help?',
233 },
234 ]
235 expect(requestsTicketBooking('can you get me one', history)).toBe(true)
236 expect(requestsTicketBooking('can you get me one')).toBe(false)
237 expect(
238 requestsTicketBooking('can you get me one', [
239 { role: 'assistant', text: 'Here is an article.' },
240 ]),
241 ).toBe(false)
242 for (const text of [
243 'What ticket should I buy?',
244 'How do I buy tickets?',
245 'Can I get a Diamond badge?',
246 "I don't want a ticket",
247 'I want to sponsor, with two tickets included',
248 ]) {
249 expect(requestsTicketBooking(text)).toBe(false)
250 }
251})
252
253test('accepts a conversational booking enquiry offer without accepting a comparison offer', () => {
254 expect(
255 acceptedSalesOffer(
256 [
257 {
258 role: 'assistant',
259 text:
260 'If you want to go ahead, I can help you start the booking enquiry for the Explorer Badge.',
261 },
262 ],
263 'yes',
264 ),
265 ).toBe(true)
266 expect(
267 acceptedSalesOffer(
268 [{ role: 'assistant', text: 'I can help you compare badges.' }],
269 'yes',
270 ),
271 ).toBe(false)
272})
273
274test('factual detours and choosing help do not depend on punctuation', () => {
275 for (const text of [
276 'how much does it cost',
277 'how much does it cost?',
278 'Is Diamond refundable?',
279 'help me choose',
280 'compare the passes',
281 ])
282 expect(isTicketInformationRequest(text)).toBe(true)
283 for (const text of [
284 'CTO at Acme',
285 'two tickets',
286 'Diamond',
287 'can you get me a badge',
288 ])
289 expect(isTicketInformationRequest(text)).toBe(false)
290})
291
292test('contextual buying stays active through missing-field answers', () => {
293 const history = [
294 { role: 'assistant' as const, text: 'General Admission is our core ticket.' },
295 { role: 'user' as const, text: 'can you get me one' },
296 {
297 role: 'assistant' as const,
298 text: 'What company are you with, and what is your role?',
299 },
300 { role: 'user' as const, text: 'CTO at Acme' },
301 ]
302 expect(isTicketSalesConversation(history)).toBe(true)
303 expect(
304 isTicketSalesConversation([
305 ...history,
306 { role: 'user', text: 'Actually I want to exhibit' },
307 ]),
308 ).toBe(false)
309 expect(
310 isTicketSalesConversation([
311 { role: 'user', text: 'I want to buy tickets, not sponsor' },
312 ]),
313 ).toBe(true)
314 expect(
315 isTicketSalesConversation([
316 { role: 'assistant', text: 'I can help you start a booking enquiry.' },
317 { role: 'user', text: 'yes' },
318 ]),
319 ).toBe(true)
320})
321
322test('short replies accept pass-selection help instead of repeating qualification', () => {
323 const history = [
324 {
325 role: 'assistant' as const,
326 text: 'Which pass would you like, or would you like help choosing one?',
327 },
328 ]
329 for (const text of [
330 'help',
331 'help please',
332 'yes',
333 'yes please',
334 'not sure',
335 "I don't know",
336 'you choose',
337 ]) {
338 expect(isTicketInformationRequest(text, history)).toBe(true)
339 }
340 expect(isTicketInformationRequest('Diamond', history)).toBe(false)
341 expect(isTicketInformationRequest('two', history)).toBe(false)
342 expect(
343 isTicketInformationRequest('yes', [
344 { role: 'assistant', text: 'Shall I send this request with your consent?' },
345 ]),
346 ).toBe(false)
347})
348
349test('exact Langfuse can I get one follow-up enters booking, not information', () => {
350 const history = [
351 {
352 role: 'assistant' as const,
353 text:
354 'General Admission Badge is our core pass. I can help you compare passes.',
355 },
356 ]
357 expect(requestsTicketBooking('can I get one', history)).toBe(true)
358 expect(isTicketInformationRequest('can I get one', history)).toBe(false)
359 expect(
360 requestsTicketBooking('can I get one', [
361 { role: 'assistant', text: 'Here is a hotel.' },
362 ]),
363 ).toBe(false)
364})
365
366test('repeated choosing help stays informational after a recommendation', () => {
367 expect(
368 isTicketInformationRequest('help please', [
369 {
370 role: 'assistant',
371 text:
372 'I recommend General Admission. Which would you like to choose: General Admission, Explorer, or Diamond?',
373 },
374 ]),
375 ).toBe(true)
376})
377
378test('help remains a request after any pass comparison wording', () => {
379 expect(
380 isTicketInformationRequest('help please', [
381 {
382 role: 'assistant',
383 text:
384 'Would you like General Admission, or do you want me to help you compare it with Explorer and Diamond?',
385 },
386 ]),
387 ).toBe(true)
388})
389
390test('buying logistics and upgrading an existing registration do not start a new sale', () => {
391 for (const text of [
392 'Can I buy a ticket at the venue?',
393 'Can I upgrade my already registered ticket?',
394 'Where can I buy tickets?',
395 ]) {
396 expect(requestsTicketBooking(text)).toBe(false)
397 expect(isTicketInformationRequest(text)).toBe(true)
398 }
399 expect(requestsTicketBooking('I want to buy two tickets')).toBe(true)
400})
401
402test('commercial logistics questions stay information without suppressing buying requests', () => {
403 for (const text of [
404 'Can I buy a ticket at the venue?',
405 'Can I upgrade my pass after registering?',
406 'Where can I buy tickets?',
407 'Where do I upload stand plans?',
408 'Can I get a free ticket?',
409 "I'm looking for free tickets",
410 ])
411 expect(isCommercialLogisticsQuestion(text)).toBe(true)
412 for (const text of [
413 'I want to buy two tickets',
414 'Please connect me with your sponsorship team',
415 'I want to sponsor the event',
416 ])
417 expect(isCommercialLogisticsQuestion(text)).toBe(false)
418})
419
420test('natural agenda email acceptance survives subsequent contact collection', () => {
421 const history = [
422 {
423 role: 'assistant' as const,
424 text: 'Would you like me to email your selected agenda?',
425 },
426 ]
427 for (const text of [
428 'Yes, please email it to me.',
429 'Please send my selected agenda to my email.',
430 ])
431 expect(acceptedAgendaEmail(history, text)).toBe(true)
432 for (const text of [
433 'No, do not email it',
434 'Please send it tomorrow',
435 'Please send my personal information',
436 ])
437 expect(acceptedAgendaEmail(history, text)).toBe(false)
438})
439
440test('registration help and an explicit pass choice switch out of agenda', () => {
441 expect(
442 requestsTicketBooking(
443 "Could you help me with the registration? I'd like to make sure I'm all set with a ticket first.",
444 ),
445 ).toBe(true)
446 expect(
447 requestsTicketBooking(
448 "I'd like to go ahead with the General Admission Badge.",
449 ),
450 ).toBe(true)
451 expect(requestsTicketBooking('Where is the registration desk?')).toBe(false)
452})
453
454test('recipient-bearing agenda acceptance stays pending through name collection', () => {
455 const history = [
456 {
457 role: 'assistant' as const,
458 text:
459 'Your UNLEASH in Paris agenda\nWould you like me to email your selected agenda?',
460 },
461 ]
462 expect(
463 acceptedAgendaEmail(
464 history,
465 'Perfect, yes please send that to jordan.blake@example.com',
466 ),
467 ).toBe(true)
468 expect(
469 acceptedAgendaEmail(history, 'Do not send that to jordan.blake@example.com'),
470 ).toBe(false)
471 expect(isCommercialLogisticsQuestion('How do I book my place?')).toBe(true)
472 expect(isCommercialLogisticsQuestion('and the group tickets page?')).toBe(true)
473})
474
475test('team quote handoffs can proceed without selecting a pass; closing on an external registration does not re-open qualification', () => {
476 const quote = 'Can your team follow up with a custom quote for a group of 4?'
477 expect(requestsTicketBooking(quote)).toBe(true)
478 expect(isTicketInformationRequest(quote)).toBe(false)
479 expect(requestsTicketBooking('What is group pricing?')).toBe(false)
480 expect(
481 requestsTicketBooking('Please do not contact me about a custom quote'),
482 ).toBe(false)
483 expect(
484 isConversationClosing(
485 "Thanks, I'll go ahead and register through the startup pass link.",
486 ),
487 ).toBe(true)
488 expect(isConversationClosing('Thanks, can you register me?')).toBe(false)
489})
490
491test('thanking us before contacting the team is a closing, not a new startup facts request', () => {
492 expect(
493 isConversationClosing(
494 "That's helpful, thanks. I'll reach out to customersuccess@unleash.ai to learn more about the Startup Award details before we register.",
495 ),
496 ).toBe(true)
497 expect(
498 isConversationClosing('Thanks, can you tell me more about the Award?'),
499 ).toBe(false)
500})
501
502test('external Support sign-off does not repeat a pending enquiry summary', () => {
503 expect(isConversationClosing('Thanks, I will contact support directly.')).toBe(
504 true,
505 )
506 expect(isConversationClosing('Thanks, will you contact support for me?')).toBe(
507 false,
508 )
509})
510
511test('a polite pricing-information request does not collect company/role', () => {
512 const message =
513 "I'd definitely like to hear about the pass options and pricing. How many team members could we bring, and what would that look like cost-wise for our organization?"
514 expect(isTicketInformationRequest(message)).toBe(true)
515 expect(isCommercialInformationRequest(message)).toBe(true)
516 expect(requestsTicketBooking(message)).toBe(false)
517 expect(
518 isCommercialInformationRequest(
519 'Can your team follow up with a custom quote for a group of 4?',
520 ),
521 ).toBe(false)
522})
523