packages/ai/components/workflows.test.ts
1import { describe, expect, test } from 'bun:test'
2import {
3 confirmedHandoff,
4 emailFromMessage,
5 HANDOFF_FAILURE_REPLY,
6 historyAfterHandoffCancellation,
7 isHandoffCancellation,
8 normalizeEmailEscapes,
9 resolveConversationEvent,
10 sessionConsent,
11} from './workflows'
12
13describe('conversational workflows', () => {
14 test('confirmation can name the destination team and include a polite closing', () => {
15 const history = [
16 {
17 role: 'assistant' as const,
18 text: 'Shall I send your enquiry to the UNLEASH team?',
19 },
20 ]
21 expect(
22 confirmedHandoff(
23 history,
24 "Yes, please go ahead and send that enquiry to the UNLEASH team. I'm looking forward to hearing from them about next steps.",
25 ),
26 ).toBe(true)
27 expect(
28 confirmedHandoff(
29 history,
30 'Yes, please send it to our sponsorship team. Thank you for your help.',
31 ),
32 ).toBe(true)
33 })
34 test('trace consent replies progress only after the consent question', () => {
35 const history = [
36 {
37 role: 'assistant' as const,
38 text: 'Shall I send this request with your consent?',
39 },
40 ]
41 for (const message of [
42 'yes pls',
43 'send it',
44 'Send the request.',
45 'Yes, please send that through for us.',
46 'Yes, I consent. Please send the enquiry.',
47 'I agree. Send my agenda.',
48 'I consent.',
49 'Yes, that all looks correct. Please send my enquiry to the UNLEASH team.',
50 'Yes, please send that enquiry through. That all looks correct to me.',
51 'Yes, please go ahead and send it.',
52 "Yes, that's correct. Please go ahead and send it.",
53 'Yes please send that enquiry through',
54 'Please email my agenda.',
55 'Go ahead and submit the enquiry, thanks.',
56 ]) {
57 expect(confirmedHandoff(history, message)).toBe(true)
58 expect(sessionConsent(history, message)).toBe(true)
59 expect(
60 confirmedHandoff(
61 [
62 {
63 role: 'assistant',
64 text: 'Would you like our sales team to get in touch?',
65 },
66 ],
67 message,
68 ),
69 ).toBe(false)
70 }
71 for (const message of [
72 'yes pls but later',
73 'send it if it is free',
74 'do not send it',
75 'Yes, please send that through for us if it is free',
76 'Yes, change the enquiry first',
77 'Yes, I consent. Please send the enquiry if it is free.',
78 'I agree. Actually change the email first.',
79 'Yes, please send it. I am looking forward to the event, but only if it is free.',
80 ])
81 expect(confirmedHandoff(history, message)).toBe(false)
82 })
83 test('event comes from the page, with explicit conversational override', () => {
84 expect(
85 resolveConversationEvent(
86 '/events/unleash-miami',
87 'What about the unlesh pariss one?',
88 [],
89 'miami',
90 ),
91 ).toBe('paris')
92 expect(
93 resolveConversationEvent(
94 '/events/unleash-paris/agenda',
95 'Build my agenda',
96 [],
97 ),
98 ).toBe('paris')
99 expect(
100 resolveConversationEvent('/events/unleash-miami-27/attend', 'tickets?', []),
101 ).toBe('miami')
102 expect(
103 resolveConversationEvent('/events/unleash-paris', 'Actually Miami', []),
104 ).toBe('miami')
105 expect(resolveConversationEvent('/', 'tickets?', [])).toBeUndefined()
106 expect(
107 resolveConversationEvent('/', 'tickets?', [{ role: 'user', text: 'Paris' }]),
108 ).toBe('paris')
109 })
110 test('sales acceptance is not submission consent', () => {
111 expect(
112 confirmedHandoff(
113 [
114 {
115 role: 'assistant',
116 text: 'Would you like our sales team to get in touch?',
117 },
118 ],
119 'yes',
120 ),
121 ).toBe(false)
122 })
123 test('consent applies only to an affirmative reply to the confirmation', () => {
124 const history = [
125 {
126 role: 'assistant' as const,
127 text: 'Shall I send this request with your consent?',
128 },
129 ]
130 expect(confirmedHandoff(history, 'yes please')).toBe(true)
131 expect(confirmedHandoff(history, 'not yet')).toBe(false)
132 expect(confirmedHandoff(history, 'change my email')).toBe(false)
133 expect(
134 confirmedHandoff(
135 [...history, { role: 'assistant', text: 'Anything else?' }],
136 'yes',
137 ),
138 ).toBe(false)
139 })
140})
141
142describe('session consent and email corrections', () => {
143 test('retains consent after an invalid address and a corrected address', () => {
144 const history = [
145 {
146 role: 'assistant' as const,
147 text: 'Shall I send this request with your consent?',
148 },
149 { role: 'user' as const, text: 'yes please' },
150 {
151 role: 'assistant' as const,
152 text: 'The email address needs to be in a valid format.',
153 },
154 ]
155 expect(sessionConsent(history, 'federico\\@itsoffbrand.com')).toBe(true)
156 expect(emailFromMessage('federico\\@itsoffbrand.com')).toBe(
157 'federico@itsoffbrand.com',
158 )
159 expect(emailFromMessage('yeah to federicoitsoffbrand.com')).toBeUndefined()
160 })
161 test('keeps the stored session decision after history is trimmed', () => {
162 expect(sessionConsent([], 'another question', true)).toBe(true)
163 expect(sessionConsent([], 'another question', false)).toBe(false)
164 })
165 test('allows consent to be withdrawn without confusing sales acceptance with consent', () => {
166 expect(sessionConsent([], 'I withdraw my consent', true)).toBe(false)
167 expect(
168 sessionConsent(
169 [
170 {
171 role: 'assistant',
172 text: 'Would you like our sales team to get in touch?',
173 },
174 ],
175 'yes',
176 ),
177 ).toBe(false)
178 })
179 test('normalizes markdown escaping without guessing a missing at sign', () => {
180 expect(normalizeEmailEscapes('federico\\@itsoffbrand.com')).toBe(
181 'federico@itsoffbrand.com',
182 )
183 expect(normalizeEmailEscapes('federicoitsoffbrand.com')).toBe(
184 'federicoitsoffbrand.com',
185 )
186 })
187})
188
189describe('cancelling contact requests', () => {
190 const history = [
191 {
192 role: 'assistant' as const,
193 text:
194 'What company are you with, and what would you like to achieve through sponsoring or exhibiting?',
195 },
196 ]
197 test('the exact staging cancellation stops the flow and withdraws stored consent', () => {
198 const message = 'Actually, cancel that. I do not want anyone to contact me.'
199 expect(isHandoffCancellation(history, message)).toBe(true)
200 expect(sessionConsent(history, message, true)).toBe(false)
201 })
202 test('short cancellation works at qualification and contact-detail steps', () => {
203 expect(
204 isHandoffCancellation(
205 [
206 {
207 role: 'assistant',
208 text: 'What company are you with, and what is your role?',
209 },
210 ],
211 'No thanks',
212 ),
213 ).toBe(true)
214 expect(
215 isHandoffCancellation(
216 [{ role: 'assistant', text: 'How many tickets would you like to book?' }],
217 'Cancel that',
218 ),
219 ).toBe(true)
220 expect(isHandoffCancellation(history, 'Cancel that.')).toBe(true)
221 expect(
222 isHandoffCancellation(
223 [
224 {
225 role: 'assistant',
226 text:
227 'Who should our team contact? Please share your name and work email.',
228 },
229 ],
230 'No thanks',
231 ),
232 ).toBe(true)
233 })
234 test('ordinary company details and ticket cancellation questions are not contact withdrawal', () => {
235 expect(isHandoffCancellation(history, 'Example Ltd, lead generation')).toBe(
236 false,
237 )
238 expect(isHandoffCancellation(history, 'How can I cancel my ticket?')).toBe(
239 false,
240 )
241 expect(isHandoffCancellation([], 'cancel that')).toBe(false)
242 })
243 test('old sales acceptance is cleared, while a later new request remains', () => {
244 const turns = [
245 { role: 'user' as const, text: 'yes please' },
246 ...history,
247 { role: 'user' as const, text: 'Cancel that.' },
248 { role: 'assistant' as const, text: 'Cancelled.' },
249 { role: 'user' as const, text: 'A different question' },
250 ]
251 expect(historyAfterHandoffCancellation(turns)).toEqual(turns.slice(3))
252 })
253})
254
255test('natural confirmation accepts send wording but rejects conditions and withdrawal', () => {
256 const history = [
257 {
258 role: 'assistant' as const,
259 text: 'Shall I send this request with your consent?',
260 },
261 ]
262 for (const text of [
263 'yes please, send!',
264 'Yes, please go ahead and send it.',
265 'Yes, please send it. Thanks for your help!',
266 "Yes, please go ahead and send it. I'm looking forward to the event!",
267 ])
268 expect(confirmedHandoff(history, text)).toBe(true)
269 for (const text of [
270 'yes but do not send it',
271 'yes if it is free',
272 'please send it tomorrow',
273 'no thanks',
274 'yes please cancel it',
275 'yes please. Thanks for not sending it',
276 ])
277 expect(confirmedHandoff(history, text)).toBe(false)
278})
279
280test('failed handoffs do not reactivate earlier accepted sales offers', () => {
281 const history = [
282 {
283 role: 'assistant' as const,
284 text: 'Would you like our sales team to get in touch?',
285 },
286 { role: 'user' as const, text: 'yes' },
287 { role: 'assistant' as const, text: HANDOFF_FAILURE_REPLY },
288 { role: 'user' as const, text: 'thanks' },
289 ]
290 expect(historyAfterHandoffCancellation(history)).toEqual([history[3]])
291})
292
293test('natural send confirmations pass but conditions and unrelated requests do not', () => {
294 const history = [
295 {
296 role: 'assistant' as const,
297 text: 'Shall I send this request with your consent?',
298 },
299 ]
300 for (const text of [
301 'Yes, please send it through.',
302 "Yes, that's all correct. Please send it.",
303 'Yes, absolutely — please send it.',
304 'Yes, absolutely - please send it to my email',
305 ]) {
306 expect(confirmedHandoff(history, text.replace(' — ', ', '))).toBe(true)
307 }
308 for (const text of [
309 'thanks',
310 'Yes, what are the prices?',
311 'Yes please send it tomorrow',
312 'yes but only if free',
313 'Yes. Change my email first.',
314 ])
315 expect(confirmedHandoff(history, text)).toBe(false)
316})
317
318test('approval of the reviewed summary with send instruction is confirmation, not another sales offer', () => {
319 const history = [
320 {
321 role: 'assistant' as const,
322 text: 'Shall I send your enquiry to our sponsorship team?',
323 },
324 ]
325 for (const message of [
326 'That looks perfect, yes please send it through.',
327 "Yes, please go ahead and send that to the UNLEASH team. I'm looking forward to hearing from them about the Diamond Badge.",
328 'Yes, that all looks correct. Please go ahead and send my enquiry to your sponsorship team.',
329 'Yes, please send it through.',
330 ])
331 expect(confirmedHandoff(history, message)).toBe(true)
332 for (const message of [
333 'That looks perfect, yes please send it if it is free.',
334 'That looks perfect, please change my email.',
335 'Yes, but do not send it.',
336 ])
337 expect(confirmedHandoff(history, message)).toBe(false)
338})
339
340test('group quote confirmation accepts explicit consent but never conditions or field changes', () => {
341 const history = [
342 {
343 role: 'assistant' as const,
344 text:
345 'Your ticket enquiry for UNLEASH in Miami. Shall I send your enquiry to the UNLEASH team?',
346 },
347 ]
348 expect(
349 confirmedHandoff(
350 history,
351 'Yes please, send this group quote enquiry with my consent.',
352 ),
353 ).toBe(true)
354 expect(
355 confirmedHandoff(
356 history,
357 'Yes please, send this group quote enquiry with my consent, if it is free.',
358 ),
359 ).toBe(false)
360 expect(
361 confirmedHandoff(
362 history,
363 'Send this group quote enquiry with my consent but change it to 500 tickets.',
364 ),
365 ).toBe(false)
366 expect(
367 confirmedHandoff(
368 [],
369 'Yes please, send this group quote enquiry with my consent.',
370 ),
371 ).toBe(false)
372})
373