apps/web/src/lib/eventFacts.test.ts

1import { expect, test } from 'bun:test'
2import {
3	eventFactReply,
4	eventFactTopic,
5	publishedFactLinks,
6	recordingPolicyReply,
7	selectEventFactAnswers,
8	startupAwardReply,
9	startupFactReply,
10	startupPackageInclusions,
11} from './eventFacts'
12
13test('exact FAQs retain aliases, conditional policy, and event isolation', () => {
14	const docs = [
15		{
16			source: 'FAQ.txt',
17			text:
18				'[UNLEASH in Miami 2027]\nQ: Can I register on-site?\nA: Miami policy.\n\n[UNLEASH in Paris 2026]\nQ: Can I register on-site?\nQ: Is onsite registration available?\nA: On-site registration may be available if capacity allows.\n\n[UNLEASH in Paris 2026]\nQ: Can I get free tickets?\nA: Apply via the HR professionals programme; group tickets and sponsor packages are other routes.',
19		},
20	]
21	const answers = selectEventFactAnswers(docs, 'paris', 'onsiteRegistration')
22	expect(answers).toHaveLength(1)
23	expect(eventFactReply('onsiteRegistration', answers)).toBe(
24		'On-site registration may be available if capacity allows.',
25	)
26	expect(
27		selectEventFactAnswers(docs, 'paris', 'freeTickets')[0].answer,
28	).toContain('group tickets')
29})
30
31test('an early-bird answer does not append an unrelated group-discount answer', () => {
32	const answers = [
33		{
34			question: 'Is there an early bird discount?',
35			answer: 'Early booking offers may be available.',
36			source: 'FAQ',
37		},
38		{
39			question: 'Is there a group discount?',
40			answer: 'Group discounts may be available.',
41			source: 'FAQ',
42		},
43	]
44	expect(
45		eventFactReply('discounts', answers, 'Is there an early bird discount?'),
46	).toContain('Early booking')
47	expect(
48		eventFactReply('discounts', answers, 'Is there an early bird discount?'),
49	).not.toContain('Group discounts')
50})
51
52test('startup eligibility excludes a sponsor-list FAQ and separates benefits from approval rules', () => {
53	const answers = selectEventFactAnswers(
54		[
55			{
56				source: 'Paris FAQ',
57				text: `[UNLEASH in Paris 2026]
58Q: Who sponsors the startup event?
59A: Visit Exhibition for sponsors.
60
61Q: Who are the sponsors and exhibitors?
62Q: Which companies, sponsors, and startups are exhibiting at the marketplace?
63A: Visit Exhibition to see key sponsors.
64
65Q: Can startups exhibit within the Startup Programme?
66A: Eligible HR technology startups can apply; the team reviews applications.`,
67			},
68		],
69		'paris',
70		'startupEligibility',
71	)
72	expect(answers).toHaveLength(1)
73	expect(answers[0].answer).toContain('reviews applications')
74	expect(eventFactTopic('I have applied for Explorer. What next?')).toBe(
75		'explorerApplication',
76	)
77	expect(
78		eventFactTopic('I work for Workday. Can I get an HR Leader pass?'),
79	).toBe('hrLeaderEligibility')
80	expect(eventFactTopic('We are pre-seed. Is there a lower-cost option?')).toBe(
81		'startupOptions',
82	)
83	expect(eventFactTopic('Can I bring my own stand builder?')).toBe(
84		'exhibitorStandPlans',
85	)
86})
87
88test('exhibitor guidance uses the manual and correct contact without a sales fallback', () => {
89	expect(eventFactReply('exhibitorStandPlans', [])).toContain('exhibitor manual')
90	expect(eventFactReply('exhibitorStandPlans', [])).toContain(
91		'customersuccess@unleash.ai',
92	)
93	expect(
94		eventFactReply('onsiteRegistration', [
95			{
96				question: 'Q',
97				source: 'FAQ',
98				answer:
99					'Please contact the Customer Success team at customersuccess@unleash.ai.',
100			},
101		]),
102	).toBe('Please contact the Support team at support@unleash.ai.')
103})
104
105test('package inclusions use the complete source block without inventing eligibility or omitting staff access', () => {
106	expect(
107		startupPackageInclusions({
108			content:
109				'What the Startup Networking Badge includes:\nFull conference access for 2x staff members\nAccess to all 7 stages\nBuilt around the decisions ahead\nOther content',
110		}),
111	).toBe(
112		'The Startup Networking Badge includes:\n\n- Full conference access for 2x staff members\n- Access to all 7 stages',
113	)
114	expect(
115		startupPackageInclusions({ content: 'Applications are reviewed.' }),
116	).toBeUndefined()
117	expect(
118		eventFactTopic('What does the Startup Networking Package include?'),
119	).toBe('startupInclusions')
120	expect(
121		eventFactTopic('What would I get with it?', [
122			{ role: 'user', text: 'I want the startup networking package' },
123		]),
124	).toBe('startupInclusions')
125	expect(
126		selectEventFactAnswers(
127			[
128				{
129					source: 'FAQ',
130					text:
131						'[Paris 2026 | audience: exhibitor_sponsor]\nQ: How do I book parking for exhibitors?\nA: Booking instructions will be in the exhibitor manual.',
132				},
133			],
134			'paris',
135			'parking',
136		),
137	).toEqual([])
138})
139
140test('volunteer follow-up stays factual but a workflow confirmation is not a facts request', () => {
141	expect(
142		eventFactTopic('What kind of roles are there?', [
143			{ role: 'user', text: 'Can I volunteer?' },
144		]),
145	).toBe('volunteering')
146	expect(eventFactTopic('Yes, please send it.')).toBeUndefined()
147	expect(
148		eventFactTopic('My role is HR Director', [
149			{ role: 'user', text: 'Can I volunteer?' },
150		]),
151	).toBeUndefined()
152})
153
154test('FAQ answers never link unpublished internal pages', () => {
155	expect(
156		publishedFactLinks(
157			'See [Old manual](/events/unleash-paris/manual) or Attend (https://www.unleash.ai/events/unleash-paris/attend). Partner: https://partner.example/apply',
158			['/events/unleash-paris/attend'],
159		),
160	).toBe(
161		'See Old manual or Attend (https://www.unleash.ai/events/unleash-paris/attend). Partner: https://partner.example/apply',
162	)
163	expect(
164		publishedFactLinks(
165			'Old https://staging-abc.unleash.ai/events/unleash-paris/manual',
166			[],
167		),
168	).toBe('Old ')
169})
170
171test('audience and recording answers need complete exact source evidence', () => {
172	const docs = [
173		{
174			source: 'Paris facts',
175			text: `[UNLEASH in Paris 2026]
176Q: How many people and speakers attend UNLEASH in Paris?
177A: 8500+ attendees and 300+ speakers.
178
179Q: What industries attend?
180A: HR, workforce technology and recruitment.`,
181		},
182	]
183	expect(eventFactTopic('How many people attend?')).toBe('attendance')
184	expect(eventFactTopic('What industries attend?')).toBe('industries')
185	expect(
186		eventFactReply(
187			'attendance',
188			selectEventFactAnswers(docs, 'paris', 'attendance'),
189		),
190	).toContain('300+ speakers')
191	expect(selectEventFactAnswers(docs, 'paris', 'recordingAccess')).toEqual([])
192	expect(eventFactReply('recordingAccess', [])).toBeUndefined()
193	const policy = recordingPolicyReply([
194		{
195			title: 'Event terms',
196			url: '/events/unleash-paris/event-terms-and-conditions',
197			content:
198				'Show recordings purchased before, at or after the show can take up to 6-8 weeks to be available. Other policy.',
199		},
200	])
201	expect(policy).toContain('can take up to 6-8 weeks')
202	expect(policy).toContain('does not establish that recordings are included')
203	expect(
204		recordingPolicyReply([
205			{
206				title: 'Media',
207				url: '/events/unleash-paris/attend/media',
208				content: 'Recordings are immediate.',
209			},
210		]),
211	).toBeUndefined()
212})
213
214test('ticket policy aliases resolve complete FAQs without confusing exhibitor registration or package upgrades', () => {
215	const documents = [
216		{
217			source: 'Paris FAQ',
218			text: `[UNLEASH in Paris 2026]
219Q: How do I book my place?
220A: Register online, complete checkout, and receive a confirmation email.
221
222Q: How do I register my exhibitor team?
223A: Use the exhibitor portal.
224
225Q: Can I upgrade my sponsorship package?
226A: Discuss sponsorship options.
227
228Q: Can I upgrade my ticket or move to a higher-tier pass?
229A: An upgrade may be possible if available. Contact the Customer Success team.
230
231Q: Can I transfer my ticket to a colleague?
232A: Transfers are usually possible subject to Terms & Conditions and deadlines.`,
233		},
234	]
235	expect(eventFactTopic('Can a colleague go instead of me?')).toBe(
236		'ticketTransfers',
237	)
238	expect(eventFactTopic('Can I move to a higher tier pass later?')).toBe(
239		'ticketUpgrades',
240	)
241	expect(
242		selectEventFactAnswers(documents, 'paris', 'registration'),
243	).toHaveLength(1)
244	const upgrades = selectEventFactAnswers(documents, 'paris', 'ticketUpgrades')
245	expect(upgrades).toHaveLength(1)
246	expect(eventFactReply('ticketUpgrades', upgrades)).toContain(
247		'support@unleash.ai',
248	)
249	expect(eventFactReply('ticketUpgrades', upgrades)).not.toContain(
250		'sponsorship options',
251	)
252	expect(
253		selectEventFactAnswers(documents, 'miami', 'ticketTransfers'),
254	).toHaveLength(0)
255})
256
257test('startup follow-ups answer pricing, audience and registration separately without repeating benefits', () => {
258	const page = {
259		title: 'Startup Networking Package',
260		url: '/events/unleash-paris/exhibition/startup-networking-package',
261		content:
262			'What the Startup Networking Badge includes:\nFull conference access for 2x staff members\nAccess to all 7 stages\n\nBuilt around your needs.\n[Get Your Badge](<https://events.unleash.ai/startup>)',
263	}
264	const history = [
265		{
266			role: 'assistant',
267			text:
268				'The Startup Networking Package is for HR technology startups and scaleups.',
269		},
270	]
271	const price = startupFactReply(
272		'startupOptions',
273		page,
274		'Does the startup package pricing apply to companies our size?',
275		history,
276	)
277	expect(price).toContain('can’t confirm')
278	expect(price).not.toContain('EUR')
279	expect(price).not.toContain('Full conference access')
280	expect(price).toContain('https://events.unleash.ai/startup')
281	const apply = startupFactReply(
282		'startupOptions',
283		page,
284		'Should I register or apply for approval first?',
285		history,
286	)
287	expect(apply).toContain('separate option')
288	expect(apply).toContain('applying to exhibit')
289	const criteria = startupFactReply(
290		'startupEligibility',
291		page,
292		'We are recruitment tech. Do we qualify?',
293		history,
294	)
295	expect(criteria).toContain('Your description is relevant')
296	expect(criteria).toContain('guarantee approval')
297	expect(criteria).not.toContain('Full conference access')
298	expect(eventFactTopic('What criteria are used?', history)).toBe(
299		'startupEligibility',
300	)
301	expect(eventFactTopic('What is the startup package pricing?', history)).toBe(
302		'startupOptions',
303	)
304	expect(eventFactTopic('How much is Diamond?', history)).toBeUndefined()
305})
306
307test('Award questions use the published competition and its application rather than badge purchase', () => {
308	const page = {
309		title: 'Startup Award',
310		url: '/events/unleash-paris/exhibition/startup-award',
311		content:
312			'[Apply Now](<https://events.unleash.ai/award>)\nThe\nUNLEASH Startup Award is the longest-running startup competition\nfor HR Tech.\nPitch in front of industry experts for a potential support package.\nUnlock the future of HR',
313	}
314	expect(
315		eventFactTopic('What does the Startup Award involve and how do I apply?'),
316	).toBe('startupAward')
317	const answer = startupAwardReply(page)
318	expect(answer).toContain('Pitch in front of industry experts')
319	expect(answer).toContain('https://events.unleash.ai/award')
320	expect(answer).toContain('does not guarantee selection or a prize')
321	expect(answer).not.toContain('registration-startup-pass')
322})
323