apps/web/src/lib/agendaPlan.test.ts
1import { describe, expect, test } from 'bun:test'
2import type { AgendaSession } from '@local/ai/components/api/pineconeUpload'
3import {
4 createAgendaPlan,
5 requestedAgendaDay,
6 sessionsOverlap,
7 validateAgendaSelection,
8} from './agendaPlan'
9
10function session(
11 id: string,
12 start: string,
13 end: string,
14 changes: Partial<AgendaSession> = {},
15): AgendaSession {
16 return {
17 title: `AI skills ${id}`,
18 url: `/agenda?session=${id}`,
19 start: `2026-10-21T${start}:00`,
20 end: `2026-10-21T${end}:00`,
21 day: '2026-10-21',
22 dayLabel: 'Wednesday 21 October',
23 room: 'Stage 1',
24 access: '',
25 speakers: 'Speaker',
26 format: 'panel',
27 keynote: false,
28 topics: 'AI, skills',
29 track: '',
30 ...changes,
31 }
32}
33
34describe('validated agenda plans', () => {
35 test('one-day requests and dated refinements override model omissions across contact turns', () => {
36 const all = [
37 session('a', '10:00', '10:30'),
38 session('b', '10:00', '10:30', {
39 day: '2026-10-22',
40 start: '2026-10-22T10:00:00',
41 end: '2026-10-22T10:30:00',
42 }),
43 ]
44 expect(
45 requestedAgendaDay(all, [
46 { role: 'user', text: 'Build a one-day AI agenda' },
47 { role: 'user', text: 'My company is Example' },
48 ]),
49 ).toBe('2026-10-21')
50 expect(
51 requestedAgendaDay(
52 all,
53 [{ role: 'user', text: 'Build an agenda for 22 October' }],
54 '2026-10-21',
55 ),
56 ).toBe('2026-10-22')
57 expect(
58 requestedAgendaDay(all, [{ role: 'user', text: 'Wednesday please' }]),
59 ).toBe('2026-10-21')
60 expect(
61 requestedAgendaDay(all, [
62 { role: 'user', text: 'one-day agenda' },
63 { role: 'user', text: 'Actually both days' },
64 ]),
65 ).toBeUndefined()
66 expect(requestedAgendaDay(all, [{ role: 'user', text: '23 October' }])).toBe(
67 '2026-10-23',
68 )
69 })
70 test('personal plans include grounded match explanations and unscheduled break guidance', () => {
71 const plan = createAgendaPlan(
72 'paris',
73 [session('a', '10:00', '10:30')],
74 '/agenda',
75 'AI, skills',
76 )
77 expect(plan.text).toContain(
78 'Why it matches: matches your interest in ai, skills',
79 )
80 expect(plan.text).toContain('lunch, networking and the exhibition')
81 expect(plan.text).toContain('not scheduled event sessions')
82 })
83 test('pillar recommendations use stored classifications rather than keynote topic matches', () => {
84 const all = [
85 session('matching', '10:00', '10:30', {
86 title: 'Making better decisions',
87 topics: '',
88 decisionPillarId: 'datatrustpeopleinfrastructure',
89 decisionPillar: 'Data, Trust, People & Infrastructure',
90 }),
91 session('wrong-pillar', '11:00', '11:30', {
92 title: 'Data, Trust, People & Infrastructure',
93 keynote: true,
94 decisionPillarId: 'rewardpayfairnesssystems',
95 }),
96 ]
97 for (const interest of [
98 'datatrustpeopleinfrastructure',
99 'Data, Trust, People & Infrastructure',
100 ])
101 expect(
102 createAgendaPlan('paris', all, null, interest).sessions.map((s) => s.url),
103 ).toEqual(['/agenda?session=matching'])
104 })
105 test('separates conflicting sessions and keeps exact source details', () => {
106 const all = [
107 session('a', '10:00', '10:30'),
108 session('b', '10:25', '10:55'),
109 session('c', '11:45', '12:15'),
110 session('d', '11:45', '12:15'),
111 ]
112 const plan = createAgendaPlan('paris', all, '/agenda', 'AI, skills')
113 expect(plan.sessions.map((s) => s.url)).toEqual([
114 '/agenda?session=a',
115 '/agenda?session=c',
116 ])
117 expect(plan.alternatives.map((s) => s.url)).toEqual([
118 '/agenda?session=b',
119 '/agenda?session=d',
120 ])
121 expect(plan.text).not.toContain('session=b')
122 for (const [i, a] of plan.sessions.entries())
123 for (const b of plan.sessions.slice(i + 1))
124 expect(sessionsOverlap(a, b)).toBe(false)
125 })
126 test('excludes restricted sessions, invalid times and missing links', () => {
127 const plan = createAgendaPlan(
128 'miami',
129 [
130 session('private', '10:00', '10:30', { access: 'INVITE ONLY' }),
131 session('backward', '11:00', '10:00'),
132 session('no-link', '12:00', '13:00', { url: '' }),
133 ],
134 null,
135 'AI',
136 )
137 expect(plan.sessions).toEqual([])
138 })
139 test('allows consecutive sessions in one room but reserves transfer time between rooms', () => {
140 const all = [
141 session('a', '10:00', '10:30'),
142 session('b', '10:30', '11:00'),
143 session('c', '11:00', '11:30', { room: 'Stage 2' }),
144 ]
145 expect(
146 createAgendaPlan('paris', all, null, 'AI').sessions.map((s) => s.url),
147 ).toEqual(['/agenda?session=a', '/agenda?session=b'])
148 })
149 test('filters by day and does not invent a fallback for unmatched interests', () => {
150 const all = [session('a', '10:00', '10:30')]
151 expect(
152 createAgendaPlan('paris', all, null, 'AI', '2026-10-22').sessions,
153 ).toEqual([])
154 expect(
155 createAgendaPlan('paris', all, null, 'underwater archaeology').sessions,
156 ).toEqual([])
157 })
158 test('email selection rejects forged, duplicated, overlapping and newly restricted sessions', () => {
159 const all = [
160 session('a', '10:00', '10:30'),
161 session('b', '10:25', '10:55'),
162 session('c', '12:00', '13:00', { access: 'INVITE ONLY' }),
163 ]
164 for (const ids of [['fake'], ['a', 'a'], ['a', 'b'], ['c']])
165 expect(
166 validateAgendaSelection(
167 all,
168 ids.map((id) => `/agenda?session=${id}`),
169 ),
170 ).toBeNull()
171 expect(validateAgendaSelection(all, ['/agenda?session=a'])?.[0].title).toBe(
172 'AI skills a',
173 )
174 })
175})
176