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