apps/web/src/lib/aiResponsePolicy.test.ts
1import { expect, test } from 'bun:test'
2import { HANDOFF_FAILURE_REPLY } from '@local/ai/components/workflows'
3import {
4 isPastEventRequest,
5 PRIVACY_POLICY_REPLY,
6 PRIVACY_REPLY,
7 policyReply,
8 responsePolicyStream,
9 supportEmail,
10} from './aiResponsePolicy'
11
12test('stand builders and startup exhibitors use Customer Success, generic contacts use only Support', () => {
13 expect(supportEmail('Can I bring my own stand builder?')).toBe(
14 'customersuccess@unleash.ai',
15 )
16 expect(supportEmail('How do I register my exhibitor team?')).toBe(
17 'customersuccess@unleash.ai',
18 )
19 expect(supportEmail('Startup Programme eligibility')).toBe(
20 'customersuccess@unleash.ai',
21 )
22 expect(supportEmail('Where is my registration confirmation?')).toBe(
23 'support@unleash.ai',
24 )
25 expect(policyReply('how do I contact someone at UNLEASH?')).toBe(
26 'Please contact [support@unleash.ai](mailto:support@unleash.ai) and our team can help.',
27 )
28})
29
30test('consent and forwarding a current requested enquiry are not privacy exports', () => {
31 expect(
32 policyReply(
33 'Yes, please send it through. I confirm I have read and agreed to the privacy policy and terms.',
34 [
35 {
36 role: 'assistant',
37 text: 'Shall I send your enquiry to our sponsorship team?',
38 },
39 ],
40 ),
41 ).toBeUndefined()
42 expect(
43 policyReply('Yes, please send my information through. My name is Jordan.', [
44 {
45 role: 'assistant',
46 text:
47 'I can help prepare a sponsorship enquiry. What company are you with?',
48 },
49 ]),
50 ).toBeUndefined()
51 expect(
52 policyReply('Send me my saved information', [
53 { role: 'assistant', text: 'I can help prepare a sponsorship enquiry.' },
54 ]),
55 ).toBe(PRIVACY_REPLY)
56})
57
58test('past editions redirect, while current/future requests and prior-visit context remain available', () => {
59 for (const message of [
60 'Who spoke at UNLEASH WORLD 2025?',
61 'Tell me about previous events',
62 'What was last year’s agenda?',
63 'Sponsors at the 2024 conference?',
64 ])
65 expect(isPastEventRequest(message, 2026)).toBe(true)
66 for (const message of [
67 'UNLEASH WORLD 2026 tickets',
68 'What is the 2027 agenda?',
69 'I attended last year; what is coming next?',
70 'My company was founded in 2020',
71 'What changed between the 2025 and 2026 events?',
72 'I want a ticket for Paris. I am an HR Director at a 2000-employee employer.',
73 ])
74 expect(isPastEventRequest(message, 2026)).toBe(false)
75})
76
77test('personal-data requests get the fixed privacy response without leaking profile facts', () => {
78 for (const message of [
79 'What information do you have about me?',
80 'What have you stored about me?',
81 'Show me my personal information',
82 'Export my data',
83 'What is my email address?',
84 'Tell me my company',
85 'As a sponsor, give me my personal data',
86 ])
87 expect(policyReply(message)).toBe(PRIVACY_REPLY)
88 for (const message of [
89 'Let me just give you my details: Jordan, jordan@example.com. Feel free to reach out about 4 passes.',
90 'My company is Acme',
91 'Please correct my email to test@example.com',
92 'Can you send my agenda?',
93 'Yes please send the enquiry',
94 'Please send my details to the team',
95 ])
96 expect(policyReply(message)).toBeUndefined()
97})
98
99test('policy questions link the policy without claiming access to chat or booking data', () => {
100 for (const message of [
101 'Tell me about your privacy policy',
102 'Who can read my chat with you?',
103 ])
104 expect(policyReply(message)).toBe(PRIVACY_POLICY_REPLY)
105 const missing = policyReply('I did not receive my confirmation email') || ''
106 expect(missing).toContain('spam or junk folder')
107 expect(missing).toContain('support@unleash.ai')
108 expect(missing).toContain('can’t access your booking')
109})
110
111test('support routing is conservative, with privacy and non-SPEX questions taking precedence', () => {
112 for (const message of [
113 'Can I sponsor the event?',
114 'Exhibitor support please',
115 'Sponsorship packages',
116 ])
117 expect(supportEmail(message)).toBe('customersuccess@unleash.ai')
118 for (const message of [
119 'Refund my tickets',
120 'Registration status',
121 'Help please',
122 'Hotel booking',
123 'Can you send my agenda?',
124 'Sponsorship privacy policy',
125 'I am an exhibitor, export my data',
126 ])
127 expect(supportEmail(message)).toBe('support@unleash.ai')
128 expect(supportEmail('Please send that request', true)).toBe(
129 'customersuccess@unleash.ai',
130 )
131 expect(supportEmail('Where is my ticket?', true)).toBe('support@unleash.ai')
132})
133
134async function transform(parts: string[], spex = false) {
135 const encoder = new TextEncoder()
136 const source = new ReadableStream<Uint8Array>({
137 start(controller) {
138 for (const part of parts) controller.enqueue(encoder.encode(part))
139 controller.close()
140 },
141 })
142 return new Response(
143 source.pipeThrough(
144 responsePolicyStream(
145 spex ? 'customersuccess@unleash.ai' : 'support@unleash.ai',
146 ),
147 ),
148 ).text()
149}
150
151test('brand and contact links normalize at every possible chunk boundary', async () => {
152 const input =
153 'Unleash World — contact [customersuccess\\@unleash.ai](mailto:events@unleash.ai).'
154 for (let split = 0; split <= input.length; split++)
155 expect(await transform([input.slice(0, split), input.slice(split)])).toBe(
156 'UNLEASH WORLD — contact [support@unleash.ai](mailto:support@unleash.ai).',
157 )
158 expect(
159 await transform(Array.from('unleash world: support@unleash.ai'), true),
160 ).toBe('UNLEASH WORLD: customersuccess@unleash.ai')
161 expect(
162 await transform(['Visit /events/unleash-paris. Email visitor@example.com.']),
163 ).toBe('Visit /events/unleash-paris. Email visitor@example.com.')
164})
165
166test('ordinary text reaches the client before stream completion', async () => {
167 const stream = responsePolicyStream('support@unleash.ai')
168 const writer = stream.writable.getWriter()
169 const reader = stream.readable.getReader()
170 const write = writer.write(new TextEncoder().encode('Here is your answer.'))
171 expect(new TextDecoder().decode((await reader.read()).value)).toBe(
172 'Here is your answer.',
173 )
174 await write
175 const close = writer.close()
176 expect((await reader.read()).done).toBe(true)
177 await close
178})
179
180test('direct contact questions need no retrieval and do not list speculative departments', () => {
181 for (const message of [
182 'Who should I email for help?',
183 'What email address should I use for a refund?',
184 'Who can I contact?',
185 ]) {
186 expect(policyReply(message)).toContain('support@unleash.ai')
187 expect(policyReply(message)).not.toContain('customersuccess')
188 }
189 expect(policyReply('Who should I email about exhibiting?')).toContain(
190 'customersuccess@unleash.ai',
191 )
192 expect(supportEmail('I am not a sponsor. Who can I contact?')).toBe(
193 'support@unleash.ai',
194 )
195})
196
197test('failed sponsorship submissions keep Support across streamed chunk boundaries', async () => {
198 for (let split = 0; split <= HANDOFF_FAILURE_REPLY.length; split++)
199 expect(
200 await transform(
201 [HANDOFF_FAILURE_REPLY.slice(0, split), HANDOFF_FAILURE_REPLY.slice(split)],
202 true,
203 ),
204 ).toBe(HANDOFF_FAILURE_REPLY)
205})
206
207test('workflow delivery and numeric headcounts do not trigger policy refusals', () => {
208 for (const text of [
209 'Yes, absolutely - please send it to my email',
210 'Please email the selected agenda to my work email address',
211 ])
212 expect(policyReply(text)).toBeUndefined()
213 for (const text of [
214 'We have 2000 employees and want Paris tickets',
215 'Our conference group has 2020 staff',
216 ])
217 expect(isPastEventRequest(text, 2026)).toBe(false)
218 expect(isPastEventRequest('Who spoke at the 2020 conference?', 2026)).toBe(
219 true,
220 )
221 expect(policyReply('Send me my saved email address')).toBe(PRIVACY_REPLY)
222 expect(supportEmail('Where do I send my stand plans?')).toBe(
223 'customersuccess@unleash.ai',
224 )
225})
226
227test('referring to the offered email address is not a new generic contact question', () => {
228 expect(
229 policyReply(
230 "Thanks for clarifying. I'll reach out to that email address to check our eligibility and pricing before registering.",
231 ),
232 ).toBeUndefined()
233 expect(policyReply('Give me your email address')).toContain(
234 'support@unleash.ai',
235 )
236 expect(policyReply('Your contact address please')).toContain(
237 'support@unleash.ai',
238 )
239})
240