@servicetitan/titan-chatbot-api 4.4.0 → 4.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,388 @@
1
+ import { expect } from '@jest/globals';
2
+ import { Models, ModelsMocks } from '../..';
3
+ import { createSelectionsModel } from '../model-utils';
4
+
5
+ describe('[model-utils] createSelectionsModel', () => {
6
+ afterEach(() => {
7
+ jest.clearAllMocks();
8
+ });
9
+
10
+ describe('with no selections', () => {
11
+ test('should return undefined when no selections are made', () => {
12
+ const frontendModel = ModelsMocks.mockFrontendModel();
13
+ const filters = [frontendModel.options];
14
+ const selected = new Map<string, string[]>();
15
+
16
+ const result = createSelectionsModel(filters, selected);
17
+
18
+ expect(result).toBeUndefined();
19
+ });
20
+
21
+ test('should return undefined when selected map is empty', () => {
22
+ const frontendModel = ModelsMocks.mockFrontendModel();
23
+ const filters = [frontendModel.options];
24
+ const selected = new Map<string, string[]>();
25
+
26
+ const result = createSelectionsModel(filters, selected);
27
+
28
+ expect(result).toBeUndefined();
29
+ });
30
+
31
+ test('should handle empty selection arrays', () => {
32
+ const frontendModel = ModelsMocks.mockFrontendModel();
33
+ const filters = [frontendModel.options];
34
+ const selected = new Map<string, string[]>([
35
+ ['Sources', []],
36
+ ['ContentTypes', []],
37
+ ]);
38
+
39
+ const result = createSelectionsModel(filters, selected);
40
+
41
+ expect(result).toBeUndefined();
42
+ });
43
+ });
44
+
45
+ describe('with flat filter structure (leaf filters only)', () => {
46
+ test('should handle single leaf filter selection', () => {
47
+ const flatModel = ModelsMocks.mockFrontendModelFlat();
48
+ const flatFilters = flatModel.options.subOptions ?? [];
49
+ const selected = new Map<string, string[]>([['ContentTypes', ['kbReleaseNotes']]]);
50
+
51
+ const result = createSelectionsModel(flatFilters, selected);
52
+
53
+ expect(result).toBeDefined();
54
+ expect(result?.subOptions).toBeDefined();
55
+ expect(result?.subOptions!.ContentTypes).toBeDefined();
56
+ expect(result?.subOptions!.ContentTypes.values).toEqual(['kbReleaseNotes']);
57
+ });
58
+
59
+ test('should handle multiple values in single leaf filter', () => {
60
+ const flatModel = ModelsMocks.mockFrontendModelFlat();
61
+ const flatFilters = flatModel.options.subOptions ?? [];
62
+ const selected = new Map<string, string[]>([
63
+ ['ContentTypes', ['kbReleaseNotes', 'kbFaq', 'kbHowTo']],
64
+ ]);
65
+
66
+ const result = createSelectionsModel(flatFilters, selected);
67
+
68
+ expect(result).toBeDefined();
69
+ expect(result?.subOptions!.ContentTypes.values).toEqual([
70
+ 'kbReleaseNotes',
71
+ 'kbFaq',
72
+ 'kbHowTo',
73
+ ]);
74
+ });
75
+
76
+ test('should handle multiple different leaf filters', () => {
77
+ const flatModel = ModelsMocks.mockFrontendModelFlat();
78
+ const flatFilters = flatModel.options.subOptions ?? [];
79
+ const selected = new Map<string, string[]>([
80
+ ['ContentTypes', ['kbReleaseNotes', 'kbFaq']],
81
+ ['ProductAreas', ['Marketing']],
82
+ ]);
83
+
84
+ const result = createSelectionsModel(flatFilters, selected);
85
+
86
+ expect(result).toBeDefined();
87
+ expect(result?.subOptions!.ContentTypes.values).toEqual(['kbReleaseNotes', 'kbFaq']);
88
+ expect(result?.subOptions!.ProductAreas.values).toEqual(['Marketing']);
89
+ });
90
+
91
+ test('should filter out non-matching selections', () => {
92
+ const flatModel = ModelsMocks.mockFrontendModelFlat();
93
+ const flatFilters = flatModel.options.subOptions ?? [];
94
+ const selected = new Map<string, string[]>([
95
+ ['ContentTypes', ['kbReleaseNotes', 'nonExistentOption']],
96
+ ]);
97
+
98
+ const result = createSelectionsModel(flatFilters, selected);
99
+
100
+ expect(result).toBeDefined();
101
+ // Only kbReleaseNotes should be included, nonExistentOption should be filtered out
102
+ expect(result?.subOptions!.ContentTypes.values).toEqual(['kbReleaseNotes']);
103
+ });
104
+
105
+ test('should return undefined when all selected options are invalid', () => {
106
+ const flatModel = ModelsMocks.mockFrontendModelFlat();
107
+ const flatFilters = flatModel.options.subOptions ?? [];
108
+ const selected = new Map<string, string[]>([
109
+ ['ContentTypes', ['nonExistent1', 'nonExistent2']],
110
+ ]);
111
+
112
+ const result = createSelectionsModel(flatFilters, selected);
113
+
114
+ expect(result).toBeUndefined();
115
+ });
116
+ });
117
+
118
+ describe('with mixed filter structure', () => {
119
+ test('should handle selections in mixed structure (1st level option without nested filters)', () => {
120
+ const frontendModel = ModelsMocks.mockFrontendModelMixed();
121
+ const filters = frontendModel.options.subOptions ?? [];
122
+ const selected = new Map<string, string[]>([['Sources', ['Jarvis']]]);
123
+
124
+ const result = createSelectionsModel(filters, selected);
125
+
126
+ expect(result).toBeDefined();
127
+ expect(result?.subOptions).toBeDefined();
128
+ expect(result?.subOptions!.Sources).toBeDefined();
129
+ expect(result?.subOptions!.Sources.values).toEqual(['Jarvis']);
130
+ });
131
+
132
+ test('should handle selections in mixed structure (1st level option with nested filters)', () => {
133
+ const frontendModel = ModelsMocks.mockFrontendModelMixed();
134
+ const filters = frontendModel.options.subOptions ?? [];
135
+ const selected = new Map<string, string[]>([['Sources', ['KnowledgeBase', 'Jarvis']]]);
136
+
137
+ const result = createSelectionsModel(filters, selected);
138
+
139
+ expect(result).toBeDefined();
140
+ expect(result?.subOptions).toBeDefined();
141
+ expect(result?.subOptions!.Sources).toBeDefined();
142
+ expect(result?.subOptions!.Sources.values).toEqual(['Jarvis']);
143
+ expect(result?.subOptions!.Sources.subOptions).toBeDefined();
144
+ expect(result?.subOptions!.Sources.subOptions!.KnowledgeBase).toBeDefined();
145
+ expect(result?.subOptions!.Sources.subOptions!.KnowledgeBase.subOptions).toEqual({});
146
+ });
147
+ });
148
+
149
+ describe('with nested filter structure', () => {
150
+ test('should handle single source selection without sub-filters', () => {
151
+ const frontendModel = ModelsMocks.mockFrontendModel();
152
+ const filters = frontendModel.options.subOptions ?? [];
153
+ const selected = new Map<string, string[]>([['Sources', ['KnowledgeBase']]]);
154
+
155
+ const result = createSelectionsModel(filters, selected);
156
+
157
+ expect(result).toBeDefined();
158
+ expect(result?.subOptions).toBeDefined();
159
+ expect(result?.subOptions!.Sources).toBeDefined();
160
+ expect(result?.subOptions!.Sources.subOptions).toBeDefined();
161
+ expect(result?.subOptions!.Sources.subOptions!.KnowledgeBase).toBeDefined();
162
+ expect(result?.subOptions!.Sources.subOptions!.KnowledgeBase.subOptions).toEqual({});
163
+ });
164
+
165
+ test('should handle source with content type selection', () => {
166
+ const frontendModel = ModelsMocks.mockFrontendModel();
167
+ const filters = frontendModel.options.subOptions ?? [];
168
+ const selected = new Map<string, string[]>([
169
+ ['Sources', ['KnowledgeBase']],
170
+ ['ContentTypes', ['kbReleaseNotes']],
171
+ ]);
172
+
173
+ const result = createSelectionsModel(filters, selected);
174
+
175
+ expect(result).toBeDefined();
176
+ const kbSubOptions = result?.subOptions!.Sources.subOptions!.KnowledgeBase.subOptions;
177
+ expect(kbSubOptions).toBeDefined();
178
+ expect(kbSubOptions!.ContentTypes).toBeDefined();
179
+ expect(kbSubOptions!.ContentTypes.values).toEqual(['kbReleaseNotes']);
180
+ });
181
+
182
+ test('should handle multiple sources with same content type selection', () => {
183
+ const frontendModel = ModelsMocks.mockFrontendModel();
184
+ const filters = frontendModel.options.subOptions ?? [];
185
+ const selected = new Map<string, string[]>([
186
+ ['Sources', ['KnowledgeBase', 'Jarvis']],
187
+ ['ContentTypes', ['kbReleaseNotes']],
188
+ ]);
189
+
190
+ const result = createSelectionsModel(filters, selected);
191
+
192
+ expect(result).toBeDefined();
193
+ expect(result?.subOptions!.Sources.subOptions).toBeDefined();
194
+ expect(result?.subOptions!.Sources.subOptions!.KnowledgeBase).toBeDefined();
195
+ expect(result?.subOptions!.Sources.subOptions!.Jarvis).toBeDefined();
196
+
197
+ // Both sources should have the ContentTypes selection (filtered by available options)
198
+ expect(
199
+ result?.subOptions!.Sources.subOptions!.KnowledgeBase.subOptions!.ContentTypes
200
+ .values
201
+ ).toEqual(['kbReleaseNotes']);
202
+ expect(
203
+ result?.subOptions!.Sources.subOptions!.Jarvis.subOptions!.ContentTypes.values
204
+ ).toEqual(['kbReleaseNotes']);
205
+ });
206
+
207
+ test('should filter content types based on each sources available options', () => {
208
+ const frontendModel = ModelsMocks.mockFrontendModel();
209
+ const filters = frontendModel.options.subOptions ?? [];
210
+ const selected = new Map<string, string[]>([
211
+ ['Sources', ['KnowledgeBase', 'Jarvis']],
212
+ ['ContentTypes', ['kbReleaseNotes', 'kbFaq', 'xxx']],
213
+ ]);
214
+
215
+ const result = createSelectionsModel(filters, selected);
216
+
217
+ expect(result).toBeDefined();
218
+
219
+ // KnowledgeBase has [kbReleaseNotes, kbFaq, kbHowTo], so it should have [kbReleaseNotes, kbFaq]
220
+ const kbContentTypes =
221
+ result?.subOptions!.Sources.subOptions!.KnowledgeBase.subOptions!.ContentTypes
222
+ .values;
223
+ expect(kbContentTypes).toEqual(['kbReleaseNotes', 'kbFaq']);
224
+
225
+ // Jarvis has [kbReleaseNotes, xxx, yyy], so it should have [kbReleaseNotes, xxx]
226
+ const jarvisContentTypes =
227
+ result?.subOptions!.Sources.subOptions!.Jarvis.subOptions!.ContentTypes.values;
228
+ expect(jarvisContentTypes).toEqual(['kbReleaseNotes', 'xxx']);
229
+ });
230
+
231
+ test('should handle product areas with nested structure', () => {
232
+ const frontendModel = ModelsMocks.mockFrontendModel();
233
+ const filters = frontendModel.options.subOptions ?? [];
234
+ const selected = new Map<string, string[]>([
235
+ ['Sources', ['KnowledgeBase']],
236
+ ['ProductAreas', ['Marketing', 'Marketing Pro']],
237
+ ]);
238
+
239
+ const result = createSelectionsModel(filters, selected);
240
+
241
+ expect(result).toBeDefined();
242
+ const kbSubOptions = result?.subOptions!.Sources.subOptions!.KnowledgeBase.subOptions;
243
+ expect(kbSubOptions!.ProductAreas).toBeDefined();
244
+ expect(kbSubOptions!.ProductAreas.values).toEqual(['Marketing', 'Marketing Pro']);
245
+ });
246
+
247
+ test('should handle both content types and product areas', () => {
248
+ const frontendModel = ModelsMocks.mockFrontendModel();
249
+ const filters = frontendModel.options.subOptions ?? [];
250
+ const selected = new Map<string, string[]>([
251
+ ['Sources', ['KnowledgeBase']],
252
+ ['ContentTypes', ['kbReleaseNotes', 'kbHowTo']],
253
+ ['ProductAreas', ['Marketing']],
254
+ ]);
255
+
256
+ const result = createSelectionsModel(filters, selected);
257
+
258
+ expect(result).toBeDefined();
259
+ const kbSubOptions = result?.subOptions!.Sources.subOptions!.KnowledgeBase.subOptions;
260
+
261
+ expect(kbSubOptions!.ContentTypes.values).toEqual(['kbReleaseNotes', 'kbHowTo']);
262
+ expect(kbSubOptions!.ProductAreas.values).toEqual(['Marketing']);
263
+ });
264
+
265
+ test('should handle complex scenario with multiple sources and filters', () => {
266
+ const frontendModel = ModelsMocks.mockFrontendModel();
267
+ const filters = frontendModel.options.subOptions ?? [];
268
+ const selected = new Map<string, string[]>([
269
+ ['Sources', ['KnowledgeBase', 'Jarvis']],
270
+ ['ContentTypes', ['kbReleaseNotes', 'yyy', 'kbHowTo']],
271
+ ['ProductAreas', ['Marketing', 'Marketing Pro']],
272
+ ]);
273
+
274
+ const result = createSelectionsModel(filters, selected);
275
+
276
+ expect(result).toBeDefined();
277
+ expect(result?.subOptions!.Sources.subOptions).toBeDefined();
278
+
279
+ // Verify KnowledgeBase selections
280
+ const kbSubOptions = result?.subOptions!.Sources.subOptions!.KnowledgeBase.subOptions;
281
+ // KB has kbReleaseNotes, kbFaq, kbHowTo - matches kbReleaseNotes and kbHowTo
282
+ expect(kbSubOptions!.ContentTypes.values).toEqual(['kbReleaseNotes', 'kbHowTo']);
283
+ expect(kbSubOptions!.ProductAreas.values).toEqual(['Marketing', 'Marketing Pro']);
284
+
285
+ // Verify Jarvis selections
286
+ const jarvisSubOptions = result?.subOptions!.Sources.subOptions!.Jarvis.subOptions;
287
+ // Jarvis has kbReleaseNotes, xxx, yyy - matches kbReleaseNotes and yyy
288
+ expect(jarvisSubOptions!.ContentTypes.values).toEqual(['kbReleaseNotes', 'yyy']);
289
+ // Jarvis doesn't have ProductAreas, so it shouldn't be present
290
+ expect(jarvisSubOptions!.ProductAreas).toBeUndefined();
291
+ });
292
+
293
+ test('should match the FilterStore export test case', () => {
294
+ // This test matches the scenario from filter.store.test.ts line 266
295
+ const frontendModel = ModelsMocks.mockFrontendModel();
296
+ const filters = frontendModel.options.subOptions ?? [];
297
+ const selected = new Map<string, string[]>([
298
+ ['Sources', ['KnowledgeBase', 'Jarvis']],
299
+ ['ContentTypes', ['kbReleaseNotes', 'yyy', 'kbHowTo']],
300
+ ['ProductAreas', ['Marketing', 'Marketing Pro']],
301
+ ]);
302
+
303
+ const result = createSelectionsModel(filters, selected);
304
+
305
+ // Should match the expected output from filter.store.test.ts
306
+ expect(result).toEqual({
307
+ subOptions: {
308
+ Sources: {
309
+ subOptions: {
310
+ KnowledgeBase: {
311
+ subOptions: {
312
+ ContentTypes: {
313
+ values: ['kbReleaseNotes', 'kbHowTo'],
314
+ },
315
+ ProductAreas: {
316
+ values: ['Marketing', 'Marketing Pro'],
317
+ },
318
+ },
319
+ },
320
+ Jarvis: {
321
+ subOptions: {
322
+ ContentTypes: {
323
+ values: ['kbReleaseNotes', 'yyy'],
324
+ },
325
+ },
326
+ },
327
+ },
328
+ },
329
+ },
330
+ });
331
+ });
332
+ });
333
+
334
+ describe('edge cases', () => {
335
+ test('should ignore non-Group type filters', () => {
336
+ const customFilters: Models.IOption[] = [
337
+ new Models.Option({
338
+ key: 'NotAGroup',
339
+ displayName: 'Not A Group',
340
+ type: Models.OptionType.Selectable,
341
+ subOptions: [
342
+ new Models.Option({
343
+ key: 'option1',
344
+ displayName: 'Option 1',
345
+ type: Models.OptionType.Selectable,
346
+ }),
347
+ ],
348
+ }),
349
+ ];
350
+ const selected = new Map<string, string[]>([['NotAGroup', ['option1']]]);
351
+
352
+ const result = createSelectionsModel(customFilters, selected);
353
+
354
+ expect(result).toBeUndefined();
355
+ });
356
+
357
+ test('should handle filters with no subOptions', () => {
358
+ const customFilters: Models.IOption[] = [
359
+ new Models.Option({
360
+ key: 'EmptyFilter',
361
+ displayName: 'Empty Filter',
362
+ type: Models.OptionType.Group,
363
+ subOptions: [],
364
+ }),
365
+ ];
366
+ const selected = new Map<string, string[]>([['EmptyFilter', ['anything']]]);
367
+
368
+ const result = createSelectionsModel(customFilters, selected);
369
+
370
+ expect(result).toBeUndefined();
371
+ });
372
+
373
+ test('should create proper Selections model structure', () => {
374
+ const frontendModel = ModelsMocks.mockFrontendModel();
375
+ const filters = frontendModel.options.subOptions ?? [];
376
+ const selected = new Map<string, string[]>([
377
+ ['Sources', ['KnowledgeBase']],
378
+ ['ContentTypes', ['kbReleaseNotes']],
379
+ ]);
380
+
381
+ const result = createSelectionsModel(filters, selected);
382
+
383
+ expect(result).toBeInstanceOf(Models.Selections);
384
+ expect(result?.subOptions).toBeDefined();
385
+ expect(typeof result?.subOptions).toBe('object');
386
+ });
387
+ });
388
+ });
@@ -22,44 +22,48 @@ export function createSelectionsModel(
22
22
  x => x.type === Models.OptionType.Group && Boolean(x.subOptions?.length)
23
23
  );
24
24
  for (const filter of filterList) {
25
- const hasSubFilters = Boolean(
26
- filter.subOptions?.some(x => Boolean(x.subOptions?.length))
25
+ const selectedLeafOptions = filter.subOptions!.filter(
26
+ x =>
27
+ x.type === Models.OptionType.Selectable &&
28
+ selected.get(filter.key)?.includes(x.key!) &&
29
+ !x.subOptions?.length
27
30
  );
28
- if (!hasSubFilters) {
31
+ let values: string[] | undefined;
32
+ if (selectedLeafOptions.length) {
29
33
  // Leaf filter: just collect selected values
30
- const values = filter.subOptions
34
+ values = selectedLeafOptions
31
35
  ?.map(o => o.key)
32
36
  .filter(o => selected.get(filter.key)?.includes(o));
33
37
  if (values?.length) {
34
38
  ensureResult();
35
39
  result!.subOptions![filter.key] = new Models.Selections({ values });
36
40
  }
37
- } else {
38
- // Non-leaf filter: add selected options as selectables and process sub-filters
39
- const filterSelectables = filter.subOptions!.filter(
40
- x =>
41
- x.type === Models.OptionType.Selectable &&
42
- Boolean(x.subOptions?.length) &&
43
- selected.get(filter.key)?.includes(x.key!)
44
- );
45
- if (!filterSelectables.length) {
46
- continue;
47
- }
48
- ensureResult();
49
- const filterResult = new Models.Selections({
50
- subOptions: {},
51
- });
52
- result!.subOptions![filter.key] = filterResult;
53
- for (const filterSelectable of filterSelectables) {
54
- // Process sub-filters: if any sub-filters selected, add them to the result
55
- const subFilters = filterSelectable.subOptions!;
56
- const resultSubFilters = process(subFilters);
57
- filterResult.subOptions![filterSelectable.key] =
58
- resultSubFilters ??
59
- new Models.Selections({
60
- subOptions: {},
61
- });
62
- }
41
+ }
42
+ // Non-leaf filter: add selected options as selectables and process sub-filters
43
+ const selectedNonLeafOptions = filter.subOptions!.filter(
44
+ x =>
45
+ x.type === Models.OptionType.Selectable &&
46
+ selected.get(filter.key)?.includes(x.key!) &&
47
+ Boolean(x.subOptions?.length)
48
+ );
49
+ if (!selectedNonLeafOptions.length) {
50
+ continue;
51
+ }
52
+ ensureResult();
53
+ const filterResult = new Models.Selections({
54
+ values,
55
+ subOptions: {},
56
+ });
57
+ result!.subOptions![filter.key] = filterResult;
58
+ for (const noneLeafOption of selectedNonLeafOptions) {
59
+ // Process sub-filters: if any sub-filters selected, add them to the result
60
+ const subFilters = noneLeafOption.subOptions!;
61
+ const resultSubFilters = process(subFilters);
62
+ filterResult.subOptions![noneLeafOption.key] =
63
+ resultSubFilters ??
64
+ new Models.Selections({
65
+ subOptions: {},
66
+ });
63
67
  }
64
68
  }
65
69
  return result;