@nyaruka/temba-components 0.129.7 → 0.129.9

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.
Files changed (269) hide show
  1. package/.devcontainer/Dockerfile +11 -4
  2. package/.devcontainer/devcontainer.json +3 -2
  3. package/.github/workflows/build.yml +4 -14
  4. package/CHANGELOG.md +29 -0
  5. package/demo/components/flow/example.html +1 -1
  6. package/demo/components/message-editor/example.html +125 -0
  7. package/demo/components/textinput/completion.html +1 -0
  8. package/demo/data/flows/food-order.json +12 -21
  9. package/demo/data/flows/sample-flow.json +210 -104
  10. package/dist/temba-components.js +715 -364
  11. package/dist/temba-components.js.map +1 -1
  12. package/out-tsc/src/display/Thumbnail.js +2 -1
  13. package/out-tsc/src/display/Thumbnail.js.map +1 -1
  14. package/out-tsc/src/events.js.map +1 -1
  15. package/out-tsc/src/excellent/helpers.js +2 -2
  16. package/out-tsc/src/excellent/helpers.js.map +1 -1
  17. package/out-tsc/src/flow/CanvasNode.js +25 -7
  18. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  19. package/out-tsc/src/flow/Editor.js +11 -1
  20. package/out-tsc/src/flow/Editor.js.map +1 -1
  21. package/out-tsc/src/flow/NodeEditor.js +342 -276
  22. package/out-tsc/src/flow/NodeEditor.js.map +1 -1
  23. package/out-tsc/src/flow/actions/add_input_labels.js +40 -0
  24. package/out-tsc/src/flow/actions/add_input_labels.js.map +1 -1
  25. package/out-tsc/src/flow/actions/call_llm.js +56 -3
  26. package/out-tsc/src/flow/actions/call_llm.js.map +1 -1
  27. package/out-tsc/src/flow/actions/call_webhook.js +26 -17
  28. package/out-tsc/src/flow/actions/call_webhook.js.map +1 -1
  29. package/out-tsc/src/flow/actions/open_ticket.js +65 -3
  30. package/out-tsc/src/flow/actions/open_ticket.js.map +1 -1
  31. package/out-tsc/src/flow/actions/send_msg.js +147 -6
  32. package/out-tsc/src/flow/actions/send_msg.js.map +1 -1
  33. package/out-tsc/src/flow/actions/set_run_result.js +75 -0
  34. package/out-tsc/src/flow/actions/set_run_result.js.map +1 -1
  35. package/out-tsc/src/flow/config.js +4 -0
  36. package/out-tsc/src/flow/config.js.map +1 -1
  37. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js +227 -0
  38. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js.map +1 -0
  39. package/out-tsc/src/flow/nodes/split_by_ticket.js +18 -0
  40. package/out-tsc/src/flow/nodes/split_by_ticket.js.map +1 -0
  41. package/out-tsc/src/flow/nodes/wait_for_response.js +27 -1
  42. package/out-tsc/src/flow/nodes/wait_for_response.js.map +1 -1
  43. package/out-tsc/src/flow/types.js +0 -65
  44. package/out-tsc/src/flow/types.js.map +1 -1
  45. package/out-tsc/src/form/ArrayEditor.js +87 -57
  46. package/out-tsc/src/form/ArrayEditor.js.map +1 -1
  47. package/out-tsc/src/form/BaseListEditor.js +19 -4
  48. package/out-tsc/src/form/BaseListEditor.js.map +1 -1
  49. package/out-tsc/src/form/FieldRenderer.js +305 -0
  50. package/out-tsc/src/form/FieldRenderer.js.map +1 -0
  51. package/out-tsc/src/form/FormField.js +4 -4
  52. package/out-tsc/src/form/FormField.js.map +1 -1
  53. package/out-tsc/src/form/KeyValueEditor.js +1 -1
  54. package/out-tsc/src/form/KeyValueEditor.js.map +1 -1
  55. package/out-tsc/src/form/MediaPicker.js +13 -1
  56. package/out-tsc/src/form/MediaPicker.js.map +1 -1
  57. package/out-tsc/src/form/MessageEditor.js +422 -0
  58. package/out-tsc/src/form/MessageEditor.js.map +1 -0
  59. package/out-tsc/src/form/TextInput.js +13 -6
  60. package/out-tsc/src/form/TextInput.js.map +1 -1
  61. package/out-tsc/src/form/select/Select.js +52 -24
  62. package/out-tsc/src/form/select/Select.js.map +1 -1
  63. package/out-tsc/src/live/ContactChat.js +66 -15
  64. package/out-tsc/src/live/ContactChat.js.map +1 -1
  65. package/out-tsc/src/markdown.js +13 -11
  66. package/out-tsc/src/markdown.js.map +1 -1
  67. package/out-tsc/temba-modules.js +2 -0
  68. package/out-tsc/temba-modules.js.map +1 -1
  69. package/out-tsc/test/ActionHelper.js +2 -0
  70. package/out-tsc/test/ActionHelper.js.map +1 -1
  71. package/out-tsc/test/NodeHelper.js +148 -0
  72. package/out-tsc/test/NodeHelper.js.map +1 -0
  73. package/out-tsc/test/actions/call_llm.test.js +103 -0
  74. package/out-tsc/test/actions/call_llm.test.js.map +1 -0
  75. package/out-tsc/test/nodes/split_by_llm_categorize.test.js +532 -0
  76. package/out-tsc/test/nodes/split_by_llm_categorize.test.js.map +1 -0
  77. package/out-tsc/test/nodes/split_by_random.test.js +150 -0
  78. package/out-tsc/test/nodes/split_by_random.test.js.map +1 -0
  79. package/out-tsc/test/nodes/wait_for_digits.test.js +150 -0
  80. package/out-tsc/test/nodes/wait_for_digits.test.js.map +1 -0
  81. package/out-tsc/test/nodes/wait_for_response.test.js +171 -0
  82. package/out-tsc/test/nodes/wait_for_response.test.js.map +1 -0
  83. package/out-tsc/test/temba-add-input-labels.test.js +70 -0
  84. package/out-tsc/test/temba-add-input-labels.test.js.map +1 -0
  85. package/out-tsc/test/temba-field-config.test.js +4 -2
  86. package/out-tsc/test/temba-field-config.test.js.map +1 -1
  87. package/out-tsc/test/temba-field-renderer.test.js +296 -0
  88. package/out-tsc/test/temba-field-renderer.test.js.map +1 -0
  89. package/out-tsc/test/temba-markdown.test.js +1 -1
  90. package/out-tsc/test/temba-markdown.test.js.map +1 -1
  91. package/out-tsc/test/temba-message-editor.test.js +194 -0
  92. package/out-tsc/test/temba-message-editor.test.js.map +1 -0
  93. package/out-tsc/test/temba-node-editor.test.js +471 -0
  94. package/out-tsc/test/temba-node-editor.test.js.map +1 -1
  95. package/out-tsc/test/temba-select.test.js +7 -4
  96. package/out-tsc/test/temba-select.test.js.map +1 -1
  97. package/out-tsc/test/temba-textinput.test.js +16 -0
  98. package/out-tsc/test/temba-textinput.test.js.map +1 -1
  99. package/out-tsc/test/temba-webchat.test.js +5 -1
  100. package/out-tsc/test/temba-webchat.test.js.map +1 -1
  101. package/out-tsc/test/utils.test.js +2 -8
  102. package/out-tsc/test/utils.test.js.map +1 -1
  103. package/package.json +7 -4
  104. package/screenshots/truth/actions/add_contact_groups/editor/descriptive-group-names.png +0 -0
  105. package/screenshots/truth/actions/add_contact_groups/editor/long-group-names.png +0 -0
  106. package/screenshots/truth/actions/add_contact_groups/editor/many-groups.png +0 -0
  107. package/screenshots/truth/actions/add_contact_groups/editor/multiple-groups.png +0 -0
  108. package/screenshots/truth/actions/add_contact_groups/editor/single-group.png +0 -0
  109. package/screenshots/truth/actions/call_llm/editor/information-extraction.png +0 -0
  110. package/screenshots/truth/actions/call_llm/editor/sentiment-analysis.png +0 -0
  111. package/screenshots/truth/actions/call_llm/editor/summarization.png +0 -0
  112. package/screenshots/truth/actions/call_llm/editor/translation-task.png +0 -0
  113. package/screenshots/truth/actions/call_llm/render/information-extraction.png +0 -0
  114. package/screenshots/truth/actions/call_llm/render/sentiment-analysis.png +0 -0
  115. package/screenshots/truth/actions/call_llm/render/summarization.png +0 -0
  116. package/screenshots/truth/actions/call_llm/render/translation-task.png +0 -0
  117. package/screenshots/truth/actions/remove_contact_groups/editor/cleanup-groups.png +0 -0
  118. package/screenshots/truth/actions/remove_contact_groups/editor/long-descriptive-group-names.png +0 -0
  119. package/screenshots/truth/actions/remove_contact_groups/editor/many-groups.png +0 -0
  120. package/screenshots/truth/actions/remove_contact_groups/editor/multiple-groups.png +0 -0
  121. package/screenshots/truth/actions/remove_contact_groups/editor/remove-from-all-groups.png +0 -0
  122. package/screenshots/truth/actions/remove_contact_groups/editor/single-group.png +0 -0
  123. package/screenshots/truth/actions/send_email/editor/complex-business-email.png +0 -0
  124. package/screenshots/truth/actions/send_email/editor/empty-body.png +0 -0
  125. package/screenshots/truth/actions/send_email/editor/empty-subject.png +0 -0
  126. package/screenshots/truth/actions/send_email/editor/long-subject.png +0 -0
  127. package/screenshots/truth/actions/send_email/editor/multiline-body.png +0 -0
  128. package/screenshots/truth/actions/send_email/editor/multiple-recipients.png +0 -0
  129. package/screenshots/truth/actions/send_email/editor/simple-email.png +0 -0
  130. package/screenshots/truth/actions/send_email/editor/with-expressions.png +0 -0
  131. package/screenshots/truth/actions/send_msg/editor/long-quick-replies.png +0 -0
  132. package/screenshots/truth/actions/send_msg/editor/multiline-text-with-replies.png +0 -0
  133. package/screenshots/truth/actions/send_msg/editor/simple-text.png +0 -0
  134. package/screenshots/truth/actions/send_msg/editor/text-with-linebreaks.png +0 -0
  135. package/screenshots/truth/actions/send_msg/editor/text-with-many-quick-replies.png +0 -0
  136. package/screenshots/truth/actions/send_msg/editor/text-with-quick-replies.png +0 -0
  137. package/screenshots/truth/actions/send_msg/editor/text-without-quick-replies.png +0 -0
  138. package/screenshots/truth/editor/router.png +0 -0
  139. package/screenshots/truth/editor/send_msg.png +0 -0
  140. package/screenshots/truth/editor/set_contact_language.png +0 -0
  141. package/screenshots/truth/editor/set_contact_name.png +0 -0
  142. package/screenshots/truth/editor/set_run_result.png +0 -0
  143. package/screenshots/truth/editor/wait.png +0 -0
  144. package/screenshots/truth/field-renderer/checkbox-checked.png +0 -0
  145. package/screenshots/truth/field-renderer/checkbox-unchecked.png +0 -0
  146. package/screenshots/truth/field-renderer/checkbox-with-errors.png +0 -0
  147. package/screenshots/truth/field-renderer/context-comparison.png +0 -0
  148. package/screenshots/truth/field-renderer/key-value-with-label.png +0 -0
  149. package/screenshots/truth/field-renderer/message-editor-with-label.png +0 -0
  150. package/screenshots/truth/field-renderer/select-multi.png +0 -0
  151. package/screenshots/truth/field-renderer/select-no-label.png +0 -0
  152. package/screenshots/truth/field-renderer/select-with-label.png +0 -0
  153. package/screenshots/truth/field-renderer/text-evaluated.png +0 -0
  154. package/screenshots/truth/field-renderer/text-no-label.png +0 -0
  155. package/screenshots/truth/field-renderer/text-with-errors.png +0 -0
  156. package/screenshots/truth/field-renderer/text-with-label.png +0 -0
  157. package/screenshots/truth/field-renderer/textarea-evaluated.png +0 -0
  158. package/screenshots/truth/field-renderer/textarea-with-label.png +0 -0
  159. package/screenshots/truth/formfield/markdown-errors.png +0 -0
  160. package/screenshots/truth/formfield/no-errors.png +0 -0
  161. package/screenshots/truth/formfield/plain-text-errors.png +0 -0
  162. package/screenshots/truth/message-editor/autogrow-initial-content.png +0 -0
  163. package/screenshots/truth/message-editor/default.png +0 -0
  164. package/screenshots/truth/message-editor/drag-highlight.png +0 -0
  165. package/screenshots/truth/message-editor/filtered-attachments.png +0 -0
  166. package/screenshots/truth/message-editor/with-completion.png +0 -0
  167. package/screenshots/truth/message-editor/with-properties.png +0 -0
  168. package/screenshots/truth/nodes/split_by_llm_categorize/editor/basic-categorization.png +0 -0
  169. package/screenshots/truth/nodes/split_by_llm_categorize/editor/custom-input-and-result-name.png +0 -0
  170. package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
  171. package/screenshots/truth/nodes/split_by_llm_categorize/editor/many-categories.png +0 -0
  172. package/screenshots/truth/nodes/split_by_llm_categorize/editor/minimal-categories.png +0 -0
  173. package/screenshots/truth/nodes/split_by_llm_categorize/render/basic-categorization.png +0 -0
  174. package/screenshots/truth/nodes/split_by_llm_categorize/render/custom-input-and-result-name.png +0 -0
  175. package/screenshots/truth/nodes/split_by_llm_categorize/render/feedback-categorization.png +0 -0
  176. package/screenshots/truth/nodes/split_by_llm_categorize/render/many-categories.png +0 -0
  177. package/screenshots/truth/nodes/split_by_llm_categorize/render/minimal-categories.png +0 -0
  178. package/screenshots/truth/nodes/split_by_random/editor/ab-test-multiple-variants.png +0 -0
  179. package/screenshots/truth/nodes/split_by_random/editor/sampling-split.png +0 -0
  180. package/screenshots/truth/nodes/split_by_random/editor/three-way-split.png +0 -0
  181. package/screenshots/truth/nodes/split_by_random/editor/two-bucket-split.png +0 -0
  182. package/screenshots/truth/nodes/split_by_random/render/ab-test-multiple-variants.png +0 -0
  183. package/screenshots/truth/nodes/split_by_random/render/sampling-split.png +0 -0
  184. package/screenshots/truth/nodes/split_by_random/render/three-way-split.png +0 -0
  185. package/screenshots/truth/nodes/split_by_random/render/two-bucket-split.png +0 -0
  186. package/screenshots/truth/nodes/wait_for_digits/editor/basic-digits-wait.png +0 -0
  187. package/screenshots/truth/nodes/wait_for_digits/editor/phone-number-collection.png +0 -0
  188. package/screenshots/truth/nodes/wait_for_digits/editor/single-digit-with-timeout.png +0 -0
  189. package/screenshots/truth/nodes/wait_for_digits/editor/verification-code.png +0 -0
  190. package/screenshots/truth/nodes/wait_for_digits/render/basic-digits-wait.png +0 -0
  191. package/screenshots/truth/nodes/wait_for_digits/render/phone-number-collection.png +0 -0
  192. package/screenshots/truth/nodes/wait_for_digits/render/single-digit-with-timeout.png +0 -0
  193. package/screenshots/truth/nodes/wait_for_digits/render/verification-code.png +0 -0
  194. package/screenshots/truth/nodes/wait_for_response/editor/basic-wait.png +0 -0
  195. package/screenshots/truth/nodes/wait_for_response/editor/custom-result-name.png +0 -0
  196. package/screenshots/truth/nodes/wait_for_response/editor/no-timeout.png +0 -0
  197. package/screenshots/truth/nodes/wait_for_response/editor/short-timeout.png +0 -0
  198. package/screenshots/truth/nodes/wait_for_response/render/basic-wait.png +0 -0
  199. package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
  200. package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
  201. package/screenshots/truth/nodes/wait_for_response/render/short-timeout.png +0 -0
  202. package/screenshots/truth/omnibox/selected.png +0 -0
  203. package/screenshots/truth/select/functions.png +0 -0
  204. package/screenshots/truth/select/multi-with-endpoint.png +0 -0
  205. package/screenshots/truth/select/search-enabled.png +0 -0
  206. package/screenshots/truth/textinput/autogrow-initial.png +0 -0
  207. package/screenshots/truth/textinput/input-form.png +0 -0
  208. package/src/display/Thumbnail.ts +2 -1
  209. package/src/events.ts +13 -1
  210. package/src/excellent/helpers.ts +2 -2
  211. package/src/flow/CanvasNode.ts +22 -1
  212. package/src/flow/Editor.ts +12 -1
  213. package/src/flow/NodeEditor.ts +412 -354
  214. package/src/flow/actions/add_input_labels.ts +45 -0
  215. package/src/flow/actions/call_llm.ts +57 -3
  216. package/src/flow/actions/call_webhook.ts +28 -18
  217. package/src/flow/actions/open_ticket.ts +74 -3
  218. package/src/flow/actions/send_msg.ts +170 -6
  219. package/src/flow/actions/set_run_result.ts +83 -0
  220. package/src/flow/config.ts +4 -0
  221. package/src/flow/nodes/split_by_llm_categorize.ts +277 -0
  222. package/src/flow/nodes/split_by_ticket.ts +19 -0
  223. package/src/flow/nodes/wait_for_response.ts +28 -1
  224. package/src/flow/types.ts +46 -128
  225. package/src/form/ArrayEditor.ts +96 -66
  226. package/src/form/BaseListEditor.ts +22 -6
  227. package/src/form/FieldRenderer.ts +465 -0
  228. package/src/form/FormField.ts +4 -4
  229. package/src/form/KeyValueEditor.ts +1 -1
  230. package/src/form/MediaPicker.ts +13 -1
  231. package/src/form/MessageEditor.ts +449 -0
  232. package/src/form/TextInput.ts +16 -8
  233. package/src/form/select/Select.ts +55 -24
  234. package/src/live/ContactChat.ts +69 -19
  235. package/src/markdown.ts +19 -11
  236. package/src/store/flow-definition.d.ts +5 -2
  237. package/static/api/labels.json +31 -0
  238. package/static/api/topics.json +24 -9
  239. package/static/api/users.json +35 -16
  240. package/static/css/temba-components.css +5 -3
  241. package/static/mr/docs/en-us/editor.json +2588 -0
  242. package/stress-test.js +143 -0
  243. package/temba-modules.ts +2 -0
  244. package/test/ActionHelper.ts +2 -0
  245. package/test/NodeHelper.ts +184 -0
  246. package/test/actions/call_llm.test.ts +137 -0
  247. package/test/nodes/README.md +78 -0
  248. package/test/nodes/split_by_llm_categorize.test.ts +698 -0
  249. package/test/nodes/split_by_random.test.ts +177 -0
  250. package/test/nodes/wait_for_digits.test.ts +176 -0
  251. package/test/nodes/wait_for_response.test.ts +206 -0
  252. package/test/temba-add-input-labels.test.ts +87 -0
  253. package/test/temba-field-config.test.ts +4 -2
  254. package/test/temba-field-renderer.test.ts +482 -0
  255. package/test/temba-markdown.test.ts +1 -1
  256. package/test/temba-message-editor.test.ts +300 -0
  257. package/test/temba-node-editor.test.ts +590 -0
  258. package/test/temba-select.test.ts +7 -7
  259. package/test/temba-textinput.test.ts +26 -0
  260. package/test/temba-webchat.test.ts +6 -1
  261. package/test/utils.test.ts +2 -13
  262. package/test-assets/contacts/history.json +19 -0
  263. package/test-assets/select/llms.json +18 -0
  264. package/test-assets/style.css +2 -0
  265. package/web-dev-mock.mjs +523 -0
  266. package/web-dev-server.config.mjs +74 -6
  267. package/web-test-runner.config.mjs +9 -4
  268. package/test/temba-flow-editor.test.ts.backup +0 -563
  269. package/test/temba-utils-index.test.ts.backup +0 -1737
@@ -0,0 +1,532 @@
1
+ import { expect } from '@open-wc/testing';
2
+ import { split_by_llm_categorize } from '../../src/flow/nodes/split_by_llm_categorize';
3
+ import { NodeTest } from '../NodeHelper';
4
+ // Helper function to create routers with proper cases and exits
5
+ function createSplitRouter(categoryNames) {
6
+ const categories = [];
7
+ const exits = [];
8
+ const cases = [];
9
+ // Add user categories
10
+ categoryNames.forEach((categoryName) => {
11
+ const categoryUuid = `category-${categoryName
12
+ .toLowerCase()
13
+ .replace(/\s+/g, '-')}`;
14
+ const exitUuid = `exit-${categoryName.toLowerCase().replace(/\s+/g, '-')}`;
15
+ const caseUuid = `case-${categoryName.toLowerCase().replace(/\s+/g, '-')}`;
16
+ categories.push({
17
+ uuid: categoryUuid,
18
+ name: categoryName,
19
+ exit_uuid: exitUuid
20
+ });
21
+ exits.push({
22
+ uuid: exitUuid,
23
+ destination_uuid: null
24
+ });
25
+ cases.push({
26
+ uuid: caseUuid,
27
+ type: 'has_only_text',
28
+ arguments: [categoryName],
29
+ category_uuid: categoryUuid
30
+ });
31
+ });
32
+ // Add "Other" category (default)
33
+ const otherCategoryUuid = 'category-other';
34
+ const otherExitUuid = 'exit-other';
35
+ categories.push({
36
+ uuid: otherCategoryUuid,
37
+ name: 'Other',
38
+ exit_uuid: otherExitUuid
39
+ });
40
+ exits.push({
41
+ uuid: otherExitUuid,
42
+ destination_uuid: null
43
+ });
44
+ // Add "Failure" category
45
+ const failureCategoryUuid = 'category-failure';
46
+ const failureExitUuid = 'exit-failure';
47
+ const failureCaseUuid = 'case-failure';
48
+ categories.push({
49
+ uuid: failureCategoryUuid,
50
+ name: 'Failure',
51
+ exit_uuid: failureExitUuid
52
+ });
53
+ exits.push({
54
+ uuid: failureExitUuid,
55
+ destination_uuid: null
56
+ });
57
+ // Add failure case for <ERROR>
58
+ cases.push({
59
+ uuid: failureCaseUuid,
60
+ type: 'has_only_text',
61
+ arguments: ['<ERROR>'],
62
+ category_uuid: failureCategoryUuid
63
+ });
64
+ return {
65
+ router: {
66
+ type: 'switch',
67
+ categories: categories,
68
+ default_category_uuid: otherCategoryUuid,
69
+ operand: '@locals._llm_output',
70
+ cases: cases
71
+ },
72
+ exits: exits
73
+ };
74
+ }
75
+ /**
76
+ * Test suite for the split_by_llm_categorize node configuration.
77
+ */
78
+ describe('split_by_llm_categorize node config', () => {
79
+ const helper = new NodeTest(split_by_llm_categorize, 'split_by_llm_categorize');
80
+ describe('basic properties', () => {
81
+ helper.testBasicProperties();
82
+ it('has correct name', () => {
83
+ expect(split_by_llm_categorize.name).to.equal('Split by AI');
84
+ });
85
+ it('has correct type', () => {
86
+ expect(split_by_llm_categorize.type).to.equal('split_by_llm_categorize');
87
+ });
88
+ });
89
+ describe('node scenarios', () => {
90
+ const basicRouter = createSplitRouter(['Greeting', 'Question']);
91
+ helper.testNode({
92
+ uuid: 'test-node-1',
93
+ actions: [
94
+ {
95
+ uuid: 'call-llm-uuid',
96
+ type: 'call_llm',
97
+ llm: { uuid: 'llm-123', name: 'Claude' },
98
+ input: '@input',
99
+ instructions: '@(prompt("categorize", slice(node.categories, 0, -2)))',
100
+ output_local: '_llm_output'
101
+ }
102
+ ],
103
+ router: basicRouter.router,
104
+ exits: basicRouter.exits
105
+ }, { type: 'split_by_llm_categorize' }, 'basic-categorization');
106
+ const premiumRouter = createSplitRouter(['Premium', 'Regular', 'VIP']);
107
+ helper.testNode({
108
+ uuid: 'test-node-2',
109
+ actions: [
110
+ {
111
+ uuid: 'call-llm-uuid-2',
112
+ type: 'call_llm',
113
+ llm: { uuid: 'llm-456', name: 'GPT-4' },
114
+ input: '@contact.name',
115
+ instructions: '@(prompt("categorize", slice(node.categories, 0, -2)))',
116
+ output_local: '_llm_output'
117
+ }
118
+ ],
119
+ router: premiumRouter.router,
120
+ exits: premiumRouter.exits
121
+ }, { type: 'split_by_llm_categorize' }, 'custom-input-and-result-name');
122
+ const priorityRouter = createSplitRouter([
123
+ 'High',
124
+ 'Medium',
125
+ 'Low',
126
+ 'Critical',
127
+ 'Urgent'
128
+ ]);
129
+ helper.testNode({
130
+ uuid: 'test-node-3',
131
+ actions: [
132
+ {
133
+ uuid: 'call-llm-uuid-3',
134
+ type: 'call_llm',
135
+ llm: { uuid: 'llm-789', name: 'Gemini' },
136
+ input: '@fields.priority',
137
+ instructions: '@(prompt("categorize", slice(node.categories, 0, -2)))',
138
+ output_local: '_llm_output'
139
+ }
140
+ ],
141
+ router: priorityRouter.router,
142
+ exits: priorityRouter.exits
143
+ }, { type: 'split_by_llm_categorize' }, 'many-categories');
144
+ const minimalRouter = createSplitRouter(['Yes']);
145
+ helper.testNode({
146
+ uuid: 'test-node-4',
147
+ actions: [
148
+ {
149
+ uuid: 'call-llm-uuid-4',
150
+ type: 'call_llm',
151
+ llm: { uuid: 'llm-minimal', name: 'Basic LLM' },
152
+ input: '@input',
153
+ instructions: '@(prompt("categorize", slice(node.categories, 0, -2)))',
154
+ output_local: '_llm_output'
155
+ }
156
+ ],
157
+ router: minimalRouter.router,
158
+ exits: minimalRouter.exits
159
+ }, { type: 'split_by_llm_categorize' }, 'minimal-categories');
160
+ const feedbackRouter = createSplitRouter([
161
+ 'Bug Report',
162
+ 'Feature Request',
163
+ 'General Feedback',
164
+ 'Support Request'
165
+ ]);
166
+ helper.testNode({
167
+ uuid: 'test-node-5',
168
+ actions: [
169
+ {
170
+ uuid: 'call-llm-uuid-5',
171
+ type: 'call_llm',
172
+ llm: { uuid: 'llm-special', name: 'Special Characters LLM' },
173
+ input: '@contact.fields.feedback',
174
+ instructions: '@(prompt("categorize", slice(node.categories, 0, -2)))',
175
+ output_local: '_llm_output'
176
+ }
177
+ ],
178
+ router: feedbackRouter.router,
179
+ exits: feedbackRouter.exits
180
+ }, { type: 'split_by_llm_categorize' }, 'feedback-categorization');
181
+ });
182
+ describe('round-trip conversion validation', () => {
183
+ it('converts to form data correctly', () => {
184
+ const testRouter = createSplitRouter(['Greeting', 'Question']);
185
+ const node = {
186
+ uuid: 'test-node',
187
+ actions: [
188
+ {
189
+ uuid: 'call-llm-uuid',
190
+ type: 'call_llm',
191
+ llm: { uuid: 'llm-123', name: 'Test LLM' },
192
+ input: '@input',
193
+ instructions: '@(prompt("categorize", slice(node.categories, 0, -2)))',
194
+ output_local: '_llm_output'
195
+ }
196
+ ],
197
+ router: testRouter.router,
198
+ exits: testRouter.exits
199
+ };
200
+ const formData = split_by_llm_categorize.toFormData(node);
201
+ expect(formData.uuid).to.equal('test-node');
202
+ expect(formData.llm).to.deep.equal([
203
+ { value: 'llm-123', name: 'Test LLM' }
204
+ ]);
205
+ expect(formData.input).to.equal('@input');
206
+ expect(formData.categories).to.deep.equal([
207
+ { name: 'Greeting' },
208
+ { name: 'Question' }
209
+ ]);
210
+ });
211
+ it('converts from form data correctly', () => {
212
+ const formData = {
213
+ uuid: 'test-node',
214
+ llm: [{ value: 'llm-456', name: 'GPT-4' }],
215
+ input: '@contact.name',
216
+ categories: [{ name: 'Premium' }, { name: 'Regular' }]
217
+ };
218
+ const originalNode = {
219
+ uuid: 'test-node',
220
+ actions: [],
221
+ exits: []
222
+ };
223
+ const result = split_by_llm_categorize.fromFormData(formData, originalNode);
224
+ expect(result.uuid).to.equal('test-node');
225
+ expect(result.actions).to.have.length(1);
226
+ expect(result.actions[0].type).to.equal('call_llm');
227
+ expect(result.actions[0].llm.uuid).to.equal('llm-456');
228
+ expect(result.actions[0].llm.name).to.equal('GPT-4');
229
+ expect(result.actions[0].input).to.equal('@contact.name');
230
+ // Should have user categories plus Other and Failure
231
+ expect(result.router.categories).to.have.length(4);
232
+ const categoryNames = result.router.categories.map((cat) => cat.name);
233
+ expect(categoryNames).to.include.members([
234
+ 'Premium',
235
+ 'Regular',
236
+ 'Other',
237
+ 'Failure'
238
+ ]);
239
+ // Should have corresponding exits
240
+ expect(result.exits).to.have.length(4);
241
+ });
242
+ });
243
+ describe('edge cases and validation', () => {
244
+ it('handles categories with empty names correctly', () => {
245
+ const formData = {
246
+ uuid: 'test-node-uuid',
247
+ llm: [{ value: 'llm-uuid-123', name: 'Claude' }],
248
+ input: '@input',
249
+ categories: [
250
+ { name: 'Valid Category' },
251
+ { name: '' }, // empty name
252
+ { name: ' ' }, // only whitespace
253
+ { name: 'Another Valid' }
254
+ ],
255
+ result_name: 'Intent'
256
+ };
257
+ const originalNode = {
258
+ uuid: 'test-node-uuid',
259
+ actions: [],
260
+ exits: []
261
+ };
262
+ const result = split_by_llm_categorize.fromFormData(formData, originalNode);
263
+ // Should only include non-empty categories
264
+ const userCategories = result.router.categories.filter((cat) => cat.name !== 'Other' && cat.name !== 'Failure');
265
+ expect(userCategories).to.have.length(2);
266
+ expect(userCategories.map((cat) => cat.name)).to.deep.equal([
267
+ 'Valid Category',
268
+ 'Another Valid'
269
+ ]);
270
+ });
271
+ it('handles categories with special characters', () => {
272
+ const formData = {
273
+ uuid: 'test-node-uuid',
274
+ llm: [{ value: 'llm-uuid-123', name: 'Claude' }],
275
+ input: '@input',
276
+ categories: [
277
+ { name: 'Category-1' },
278
+ { name: 'Category_2' },
279
+ { name: 'Category@3' },
280
+ { name: 'Category with spaces' }
281
+ ],
282
+ result_name: 'Intent'
283
+ };
284
+ const originalNode = {
285
+ uuid: 'test-node-uuid',
286
+ actions: [],
287
+ exits: []
288
+ };
289
+ const result = split_by_llm_categorize.fromFormData(formData, originalNode);
290
+ // Should preserve all special characters in category names
291
+ const userCategories = result.router.categories.filter((cat) => cat.name !== 'Other' && cat.name !== 'Failure');
292
+ expect(userCategories).to.have.length(4);
293
+ expect(userCategories.map((cat) => cat.name)).to.include.members([
294
+ 'Category-1',
295
+ 'Category_2',
296
+ 'Category@3',
297
+ 'Category with spaces'
298
+ ]);
299
+ // Verify cases also have correct names
300
+ const caseNames = result
301
+ .router.cases.filter((c) => c.arguments[0] !== '<ERROR>')
302
+ .map((c) => c.arguments[0]);
303
+ expect(caseNames).to.include.members([
304
+ 'Category-1',
305
+ 'Category_2',
306
+ 'Category@3',
307
+ 'Category with spaces'
308
+ ]);
309
+ });
310
+ it('maintains UUID consistency between categories, cases, and exits', () => {
311
+ const formData = {
312
+ uuid: 'test-node-uuid',
313
+ llm: [{ value: 'llm-uuid-123', name: 'Claude' }],
314
+ input: '@input',
315
+ categories: [{ name: 'Test Category' }],
316
+ result_name: 'Intent'
317
+ };
318
+ const originalNode = {
319
+ uuid: 'test-node-uuid',
320
+ actions: [],
321
+ exits: []
322
+ };
323
+ const result = split_by_llm_categorize.fromFormData(formData, originalNode);
324
+ // Find the test category
325
+ const testCategory = result.router.categories.find((cat) => cat.name === 'Test Category');
326
+ const testCase = result.router.cases.find((c) => c.arguments[0] === 'Test Category');
327
+ const testExit = result.exits.find((exit) => exit.uuid === testCategory.exit_uuid);
328
+ // Verify UUID consistency
329
+ expect(testCase.category_uuid).to.equal(testCategory.uuid);
330
+ expect(testExit.uuid).to.equal(testCategory.exit_uuid);
331
+ });
332
+ it('generates unique UUIDs for each run', () => {
333
+ const formData = {
334
+ uuid: 'test-node-uuid',
335
+ llm: [{ value: 'llm-uuid-123', name: 'Claude' }],
336
+ input: '@input',
337
+ categories: [{ name: 'Test' }],
338
+ result_name: 'Intent'
339
+ };
340
+ const originalNode = {
341
+ uuid: 'test-node-uuid',
342
+ actions: [],
343
+ exits: []
344
+ };
345
+ const result1 = split_by_llm_categorize.fromFormData(formData, originalNode);
346
+ const result2 = split_by_llm_categorize.fromFormData(formData, originalNode);
347
+ // UUIDs should be different for each generation
348
+ expect(result1.actions[0].uuid).to.not.equal(result2.actions[0].uuid);
349
+ expect(result1.router.categories[0].uuid).to.not.equal(result2.router.categories[0].uuid);
350
+ expect(result1.exits[0].uuid).to.not.equal(result2.exits[0].uuid);
351
+ });
352
+ it('roundtrip conversion (fromFormData -> toFormData) works correctly', () => {
353
+ const originalFormData = {
354
+ uuid: 'test-node-uuid',
355
+ llm: [{ value: 'llm-uuid-123', name: 'Claude' }],
356
+ input: '@custom.input',
357
+ categories: [{ name: 'Category1' }, { name: 'Category2' }],
358
+ result_name: 'CustomResult'
359
+ };
360
+ const originalNode = {
361
+ uuid: 'test-node-uuid',
362
+ actions: [],
363
+ exits: []
364
+ };
365
+ // Convert form data to node
366
+ const node = split_by_llm_categorize.fromFormData(originalFormData, originalNode);
367
+ // Convert back to form data
368
+ const recoveredFormData = split_by_llm_categorize.toFormData(node);
369
+ // Should match original data
370
+ expect(recoveredFormData.uuid).to.equal(originalFormData.uuid);
371
+ expect(recoveredFormData.llm).to.deep.equal(originalFormData.llm);
372
+ expect(recoveredFormData.input).to.equal(originalFormData.input);
373
+ expect(recoveredFormData.categories).to.deep.equal(originalFormData.categories);
374
+ });
375
+ it('handles max 10 categories requirement', () => {
376
+ // Create 12 categories to test the limit
377
+ const categories = Array.from({ length: 12 }, (_, i) => ({
378
+ name: `Category${i + 1}`
379
+ }));
380
+ const formData = {
381
+ uuid: 'test-node-uuid',
382
+ llm: [{ value: 'llm-uuid-123', name: 'Claude' }],
383
+ input: '@input',
384
+ categories: categories,
385
+ result_name: 'Intent'
386
+ };
387
+ const originalNode = {
388
+ uuid: 'test-node-uuid',
389
+ actions: [],
390
+ exits: []
391
+ };
392
+ const result = split_by_llm_categorize.fromFormData(formData, originalNode);
393
+ // Should process all categories provided (fromFormData doesn't enforce the limit, validation should)
394
+ const userCategories = result.router.categories.filter((cat) => cat.name !== 'Other' && cat.name !== 'Failure');
395
+ expect(userCategories).to.have.length(12);
396
+ // Note: The actual 10-category limit should be enforced by the UI validation
397
+ // which uses the maxItems: 10 property in the form configuration
398
+ });
399
+ it('preserves original node UUID', () => {
400
+ const formData = {
401
+ uuid: 'should-be-ignored',
402
+ llm: [{ value: 'llm-uuid-123', name: 'Claude' }],
403
+ input: '@input',
404
+ categories: [{ name: 'Test' }],
405
+ result_name: 'Intent'
406
+ };
407
+ const originalNode = {
408
+ uuid: 'original-node-uuid',
409
+ actions: [],
410
+ exits: []
411
+ };
412
+ const result = split_by_llm_categorize.fromFormData(formData, originalNode);
413
+ // Should use original node UUID, not the one from form data
414
+ expect(result.uuid).to.equal('original-node-uuid');
415
+ });
416
+ });
417
+ describe('validation', () => {
418
+ it('should validate duplicate category names', () => {
419
+ const formData = {
420
+ uuid: 'test-node-uuid',
421
+ llm: [{ value: 'llm-uuid-123', name: 'Claude' }],
422
+ input: '@input',
423
+ categories: [
424
+ { name: 'Category1' },
425
+ { name: 'Category2' },
426
+ { name: 'Category1' }, // duplicate
427
+ { name: 'category2' }, // case insensitive duplicate
428
+ { name: 'Category3' }
429
+ ]
430
+ };
431
+ const validationResult = split_by_llm_categorize.validate(formData);
432
+ expect(validationResult.valid).to.be.false;
433
+ expect(validationResult.errors.categories).to.include('Duplicate category names found');
434
+ expect(validationResult.errors.categories).to.include('Category1');
435
+ expect(validationResult.errors.categories).to.include('Category2');
436
+ expect(validationResult.errors.categories).to.include('category2');
437
+ });
438
+ it('should pass validation with unique category names', () => {
439
+ const formData = {
440
+ uuid: 'test-node-uuid',
441
+ llm: [{ value: 'llm-uuid-123', name: 'Claude' }],
442
+ input: '@input',
443
+ categories: [
444
+ { name: 'Category1' },
445
+ { name: 'Category2' },
446
+ { name: 'Category3' }
447
+ ]
448
+ };
449
+ const validationResult = split_by_llm_categorize.validate(formData);
450
+ expect(validationResult.valid).to.be.true;
451
+ expect(Object.keys(validationResult.errors)).to.have.length(0);
452
+ });
453
+ it('should ignore empty categories in validation', () => {
454
+ const formData = {
455
+ uuid: 'test-node-uuid',
456
+ llm: [{ value: 'llm-uuid-123', name: 'Claude' }],
457
+ input: '@input',
458
+ categories: [
459
+ { name: 'Category1' },
460
+ { name: '' }, // empty
461
+ { name: ' ' }, // whitespace only
462
+ { name: 'Category2' }
463
+ ]
464
+ };
465
+ const validationResult = split_by_llm_categorize.validate(formData);
466
+ expect(validationResult.valid).to.be.true;
467
+ expect(Object.keys(validationResult.errors)).to.have.length(0);
468
+ });
469
+ });
470
+ describe('JSON output verification', () => {
471
+ it('generates JSON matching the exact format from the issue', () => {
472
+ const formData = {
473
+ uuid: '145eb3d3-b841-4e66-abac-297ae525c7ad',
474
+ llm: [
475
+ { value: '1c06c884-39dd-4ce4-ad9f-9a01cbe6c000', name: 'Claude' }
476
+ ],
477
+ input: '@input',
478
+ categories: [{ name: 'Flights' }, { name: 'Hotels' }],
479
+ result_name: 'Intent'
480
+ };
481
+ const originalNode = {
482
+ uuid: '145eb3d3-b841-4e66-abac-297ae525c7ad',
483
+ actions: [],
484
+ exits: []
485
+ };
486
+ const result = split_by_llm_categorize.fromFormData(formData, originalNode);
487
+ // Verify the call_llm action
488
+ const callLlmAction = result.actions[0];
489
+ expect(callLlmAction.type).to.equal('call_llm');
490
+ expect(callLlmAction.llm.uuid).to.equal('1c06c884-39dd-4ce4-ad9f-9a01cbe6c000');
491
+ expect(callLlmAction.llm.name).to.equal('Claude');
492
+ expect(callLlmAction.instructions).to.equal('@(prompt("categorize", slice(node.categories, 0, -2)))');
493
+ expect(callLlmAction.input).to.equal('@input');
494
+ expect(callLlmAction.output_local).to.equal('_llm_output');
495
+ // Verify the router structure
496
+ const router = result.router;
497
+ expect(router.type).to.equal('switch');
498
+ expect(router.operand).to.equal('@locals._llm_output');
499
+ // Verify categories structure
500
+ expect(router.categories).to.have.length(4);
501
+ const categoryNames = router.categories.map((cat) => cat.name);
502
+ expect(categoryNames).to.include.members([
503
+ 'Flights',
504
+ 'Hotels',
505
+ 'Other',
506
+ 'Failure'
507
+ ]);
508
+ // Verify cases structure
509
+ expect(router.cases).to.have.length(3);
510
+ const caseArguments = router.cases.map((c) => c.arguments[0]);
511
+ expect(caseArguments).to.include.members([
512
+ 'Flights',
513
+ 'Hotels',
514
+ '<ERROR>'
515
+ ]);
516
+ // Verify all cases use has_only_text
517
+ router.cases.forEach((caseItem) => {
518
+ expect(caseItem.type).to.equal('has_only_text');
519
+ });
520
+ // Verify exits match categories
521
+ expect(result.exits).to.have.length(4);
522
+ router.categories.forEach((category) => {
523
+ const matchingExit = result.exits.find((exit) => exit.uuid === category.exit_uuid);
524
+ expect(matchingExit).to.exist;
525
+ });
526
+ // Verify default category is "Other"
527
+ const otherCategory = router.categories.find((cat) => cat.name === 'Other');
528
+ expect(router.default_category_uuid).to.equal(otherCategory.uuid);
529
+ });
530
+ });
531
+ });
532
+ //# sourceMappingURL=split_by_llm_categorize.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"split_by_llm_categorize.test.js","sourceRoot":"","sources":["../../../test/nodes/split_by_llm_categorize.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { expect } from '@open-wc/testing';\nimport { split_by_llm_categorize } from '../../src/flow/nodes/split_by_llm_categorize';\nimport { Node } from '../../src/store/flow-definition';\nimport { NodeTest } from '../NodeHelper';\n\n// Helper function to create routers with proper cases and exits\nfunction createSplitRouter(categoryNames: string[]) {\n const categories = [];\n const exits = [];\n const cases = [];\n\n // Add user categories\n categoryNames.forEach((categoryName) => {\n const categoryUuid = `category-${categoryName\n .toLowerCase()\n .replace(/\\s+/g, '-')}`;\n const exitUuid = `exit-${categoryName.toLowerCase().replace(/\\s+/g, '-')}`;\n const caseUuid = `case-${categoryName.toLowerCase().replace(/\\s+/g, '-')}`;\n\n categories.push({\n uuid: categoryUuid,\n name: categoryName,\n exit_uuid: exitUuid\n });\n\n exits.push({\n uuid: exitUuid,\n destination_uuid: null\n });\n\n cases.push({\n uuid: caseUuid,\n type: 'has_only_text',\n arguments: [categoryName],\n category_uuid: categoryUuid\n });\n });\n\n // Add \"Other\" category (default)\n const otherCategoryUuid = 'category-other';\n const otherExitUuid = 'exit-other';\n\n categories.push({\n uuid: otherCategoryUuid,\n name: 'Other',\n exit_uuid: otherExitUuid\n });\n exits.push({\n uuid: otherExitUuid,\n destination_uuid: null\n });\n\n // Add \"Failure\" category\n const failureCategoryUuid = 'category-failure';\n const failureExitUuid = 'exit-failure';\n const failureCaseUuid = 'case-failure';\n\n categories.push({\n uuid: failureCategoryUuid,\n name: 'Failure',\n exit_uuid: failureExitUuid\n });\n exits.push({\n uuid: failureExitUuid,\n destination_uuid: null\n });\n\n // Add failure case for <ERROR>\n cases.push({\n uuid: failureCaseUuid,\n type: 'has_only_text',\n arguments: ['<ERROR>'],\n category_uuid: failureCategoryUuid\n });\n\n return {\n router: {\n type: 'switch' as const,\n categories: categories,\n default_category_uuid: otherCategoryUuid,\n operand: '@locals._llm_output',\n cases: cases\n },\n exits: exits\n };\n}\n\n/**\n * Test suite for the split_by_llm_categorize node configuration.\n */\ndescribe('split_by_llm_categorize node config', () => {\n const helper = new NodeTest(\n split_by_llm_categorize,\n 'split_by_llm_categorize'\n );\n\n describe('basic properties', () => {\n helper.testBasicProperties();\n\n it('has correct name', () => {\n expect(split_by_llm_categorize.name).to.equal('Split by AI');\n });\n\n it('has correct type', () => {\n expect(split_by_llm_categorize.type).to.equal('split_by_llm_categorize');\n });\n });\n\n describe('node scenarios', () => {\n const basicRouter = createSplitRouter(['Greeting', 'Question']);\n helper.testNode(\n {\n uuid: 'test-node-1',\n actions: [\n {\n uuid: 'call-llm-uuid',\n type: 'call_llm',\n llm: { uuid: 'llm-123', name: 'Claude' },\n input: '@input',\n instructions:\n '@(prompt(\"categorize\", slice(node.categories, 0, -2)))',\n output_local: '_llm_output'\n } as any\n ],\n router: basicRouter.router,\n exits: basicRouter.exits\n } as Node,\n { type: 'split_by_llm_categorize' },\n 'basic-categorization'\n );\n\n const premiumRouter = createSplitRouter(['Premium', 'Regular', 'VIP']);\n helper.testNode(\n {\n uuid: 'test-node-2',\n actions: [\n {\n uuid: 'call-llm-uuid-2',\n type: 'call_llm',\n llm: { uuid: 'llm-456', name: 'GPT-4' },\n input: '@contact.name',\n instructions:\n '@(prompt(\"categorize\", slice(node.categories, 0, -2)))',\n output_local: '_llm_output'\n } as any\n ],\n router: premiumRouter.router,\n exits: premiumRouter.exits\n } as Node,\n { type: 'split_by_llm_categorize' },\n 'custom-input-and-result-name'\n );\n\n const priorityRouter = createSplitRouter([\n 'High',\n 'Medium',\n 'Low',\n 'Critical',\n 'Urgent'\n ]);\n helper.testNode(\n {\n uuid: 'test-node-3',\n actions: [\n {\n uuid: 'call-llm-uuid-3',\n type: 'call_llm',\n llm: { uuid: 'llm-789', name: 'Gemini' },\n input: '@fields.priority',\n instructions:\n '@(prompt(\"categorize\", slice(node.categories, 0, -2)))',\n output_local: '_llm_output'\n } as any\n ],\n router: priorityRouter.router,\n exits: priorityRouter.exits\n } as Node,\n { type: 'split_by_llm_categorize' },\n 'many-categories'\n );\n\n const minimalRouter = createSplitRouter(['Yes']);\n helper.testNode(\n {\n uuid: 'test-node-4',\n actions: [\n {\n uuid: 'call-llm-uuid-4',\n type: 'call_llm',\n llm: { uuid: 'llm-minimal', name: 'Basic LLM' },\n input: '@input',\n instructions:\n '@(prompt(\"categorize\", slice(node.categories, 0, -2)))',\n output_local: '_llm_output'\n } as any\n ],\n router: minimalRouter.router,\n exits: minimalRouter.exits\n } as Node,\n { type: 'split_by_llm_categorize' },\n 'minimal-categories'\n );\n\n const feedbackRouter = createSplitRouter([\n 'Bug Report',\n 'Feature Request',\n 'General Feedback',\n 'Support Request'\n ]);\n helper.testNode(\n {\n uuid: 'test-node-5',\n actions: [\n {\n uuid: 'call-llm-uuid-5',\n type: 'call_llm',\n llm: { uuid: 'llm-special', name: 'Special Characters LLM' },\n input: '@contact.fields.feedback',\n instructions:\n '@(prompt(\"categorize\", slice(node.categories, 0, -2)))',\n output_local: '_llm_output'\n } as any\n ],\n router: feedbackRouter.router,\n exits: feedbackRouter.exits\n } as Node,\n { type: 'split_by_llm_categorize' },\n 'feedback-categorization'\n );\n });\n\n describe('round-trip conversion validation', () => {\n it('converts to form data correctly', () => {\n const testRouter = createSplitRouter(['Greeting', 'Question']);\n const node: Node = {\n uuid: 'test-node',\n actions: [\n {\n uuid: 'call-llm-uuid',\n type: 'call_llm',\n llm: { uuid: 'llm-123', name: 'Test LLM' },\n input: '@input',\n instructions:\n '@(prompt(\"categorize\", slice(node.categories, 0, -2)))',\n output_local: '_llm_output'\n } as any\n ],\n router: testRouter.router,\n exits: testRouter.exits\n };\n\n const formData = split_by_llm_categorize.toFormData!(node);\n\n expect(formData.uuid).to.equal('test-node');\n expect(formData.llm).to.deep.equal([\n { value: 'llm-123', name: 'Test LLM' }\n ]);\n expect(formData.input).to.equal('@input');\n expect(formData.categories).to.deep.equal([\n { name: 'Greeting' },\n { name: 'Question' }\n ]);\n });\n\n it('converts from form data correctly', () => {\n const formData = {\n uuid: 'test-node',\n llm: [{ value: 'llm-456', name: 'GPT-4' }],\n input: '@contact.name',\n categories: [{ name: 'Premium' }, { name: 'Regular' }]\n };\n\n const originalNode: Node = {\n uuid: 'test-node',\n actions: [],\n exits: []\n };\n\n const result = split_by_llm_categorize.fromFormData!(\n formData,\n originalNode\n );\n\n expect(result.uuid).to.equal('test-node');\n expect(result.actions).to.have.length(1);\n expect(result.actions[0].type).to.equal('call_llm');\n expect((result.actions[0] as any).llm.uuid).to.equal('llm-456');\n expect((result.actions[0] as any).llm.name).to.equal('GPT-4');\n expect((result.actions[0] as any).input).to.equal('@contact.name');\n\n // Should have user categories plus Other and Failure\n expect(result.router!.categories).to.have.length(4);\n const categoryNames = result.router!.categories.map((cat) => cat.name);\n expect(categoryNames).to.include.members([\n 'Premium',\n 'Regular',\n 'Other',\n 'Failure'\n ]);\n\n // Should have corresponding exits\n expect(result.exits).to.have.length(4);\n });\n });\n\n describe('edge cases and validation', () => {\n it('handles categories with empty names correctly', () => {\n const formData = {\n uuid: 'test-node-uuid',\n llm: [{ value: 'llm-uuid-123', name: 'Claude' }],\n input: '@input',\n categories: [\n { name: 'Valid Category' },\n { name: '' }, // empty name\n { name: ' ' }, // only whitespace\n { name: 'Another Valid' }\n ],\n result_name: 'Intent'\n };\n\n const originalNode: Node = {\n uuid: 'test-node-uuid',\n actions: [],\n exits: []\n };\n\n const result = split_by_llm_categorize.fromFormData!(\n formData,\n originalNode\n );\n\n // Should only include non-empty categories\n const userCategories = result.router!.categories.filter(\n (cat) => cat.name !== 'Other' && cat.name !== 'Failure'\n );\n expect(userCategories).to.have.length(2);\n expect(userCategories.map((cat) => cat.name)).to.deep.equal([\n 'Valid Category',\n 'Another Valid'\n ]);\n });\n\n it('handles categories with special characters', () => {\n const formData = {\n uuid: 'test-node-uuid',\n llm: [{ value: 'llm-uuid-123', name: 'Claude' }],\n input: '@input',\n categories: [\n { name: 'Category-1' },\n { name: 'Category_2' },\n { name: 'Category@3' },\n { name: 'Category with spaces' }\n ],\n result_name: 'Intent'\n };\n\n const originalNode: Node = {\n uuid: 'test-node-uuid',\n actions: [],\n exits: []\n };\n\n const result = split_by_llm_categorize.fromFormData!(\n formData,\n originalNode\n );\n\n // Should preserve all special characters in category names\n const userCategories = result.router!.categories.filter(\n (cat) => cat.name !== 'Other' && cat.name !== 'Failure'\n );\n expect(userCategories).to.have.length(4);\n expect(userCategories.map((cat) => cat.name)).to.include.members([\n 'Category-1',\n 'Category_2',\n 'Category@3',\n 'Category with spaces'\n ]);\n\n // Verify cases also have correct names\n const caseNames = result\n .router!.cases.filter((c) => c.arguments[0] !== '<ERROR>')\n .map((c) => c.arguments[0]);\n expect(caseNames).to.include.members([\n 'Category-1',\n 'Category_2',\n 'Category@3',\n 'Category with spaces'\n ]);\n });\n\n it('maintains UUID consistency between categories, cases, and exits', () => {\n const formData = {\n uuid: 'test-node-uuid',\n llm: [{ value: 'llm-uuid-123', name: 'Claude' }],\n input: '@input',\n categories: [{ name: 'Test Category' }],\n result_name: 'Intent'\n };\n\n const originalNode: Node = {\n uuid: 'test-node-uuid',\n actions: [],\n exits: []\n };\n\n const result = split_by_llm_categorize.fromFormData!(\n formData,\n originalNode\n );\n\n // Find the test category\n const testCategory = result.router!.categories.find(\n (cat) => cat.name === 'Test Category'\n );\n const testCase = result.router!.cases.find(\n (c) => c.arguments[0] === 'Test Category'\n );\n const testExit = result.exits.find(\n (exit) => exit.uuid === testCategory!.exit_uuid\n );\n\n // Verify UUID consistency\n expect(testCase!.category_uuid).to.equal(testCategory!.uuid);\n expect(testExit!.uuid).to.equal(testCategory!.exit_uuid);\n });\n\n it('generates unique UUIDs for each run', () => {\n const formData = {\n uuid: 'test-node-uuid',\n llm: [{ value: 'llm-uuid-123', name: 'Claude' }],\n input: '@input',\n categories: [{ name: 'Test' }],\n result_name: 'Intent'\n };\n\n const originalNode: Node = {\n uuid: 'test-node-uuid',\n actions: [],\n exits: []\n };\n\n const result1 = split_by_llm_categorize.fromFormData!(\n formData,\n originalNode\n );\n const result2 = split_by_llm_categorize.fromFormData!(\n formData,\n originalNode\n );\n\n // UUIDs should be different for each generation\n expect(result1.actions[0].uuid).to.not.equal(result2.actions[0].uuid);\n expect(result1.router!.categories[0].uuid).to.not.equal(\n result2.router!.categories[0].uuid\n );\n expect(result1.exits[0].uuid).to.not.equal(result2.exits[0].uuid);\n });\n\n it('roundtrip conversion (fromFormData -> toFormData) works correctly', () => {\n const originalFormData = {\n uuid: 'test-node-uuid',\n llm: [{ value: 'llm-uuid-123', name: 'Claude' }],\n input: '@custom.input',\n categories: [{ name: 'Category1' }, { name: 'Category2' }],\n result_name: 'CustomResult'\n };\n\n const originalNode: Node = {\n uuid: 'test-node-uuid',\n actions: [],\n exits: []\n };\n\n // Convert form data to node\n const node = split_by_llm_categorize.fromFormData!(\n originalFormData,\n originalNode\n );\n\n // Convert back to form data\n const recoveredFormData = split_by_llm_categorize.toFormData!(node);\n\n // Should match original data\n expect(recoveredFormData.uuid).to.equal(originalFormData.uuid);\n expect(recoveredFormData.llm).to.deep.equal(originalFormData.llm);\n expect(recoveredFormData.input).to.equal(originalFormData.input);\n expect(recoveredFormData.categories).to.deep.equal(\n originalFormData.categories\n );\n });\n\n it('handles max 10 categories requirement', () => {\n // Create 12 categories to test the limit\n const categories = Array.from({ length: 12 }, (_, i) => ({\n name: `Category${i + 1}`\n }));\n\n const formData = {\n uuid: 'test-node-uuid',\n llm: [{ value: 'llm-uuid-123', name: 'Claude' }],\n input: '@input',\n categories: categories,\n result_name: 'Intent'\n };\n\n const originalNode: Node = {\n uuid: 'test-node-uuid',\n actions: [],\n exits: []\n };\n\n const result = split_by_llm_categorize.fromFormData!(\n formData,\n originalNode\n );\n\n // Should process all categories provided (fromFormData doesn't enforce the limit, validation should)\n const userCategories = result.router!.categories.filter(\n (cat) => cat.name !== 'Other' && cat.name !== 'Failure'\n );\n expect(userCategories).to.have.length(12);\n\n // Note: The actual 10-category limit should be enforced by the UI validation\n // which uses the maxItems: 10 property in the form configuration\n });\n\n it('preserves original node UUID', () => {\n const formData = {\n uuid: 'should-be-ignored',\n llm: [{ value: 'llm-uuid-123', name: 'Claude' }],\n input: '@input',\n categories: [{ name: 'Test' }],\n result_name: 'Intent'\n };\n\n const originalNode: Node = {\n uuid: 'original-node-uuid',\n actions: [],\n exits: []\n };\n\n const result = split_by_llm_categorize.fromFormData!(\n formData,\n originalNode\n );\n\n // Should use original node UUID, not the one from form data\n expect(result.uuid).to.equal('original-node-uuid');\n });\n });\n\n describe('validation', () => {\n it('should validate duplicate category names', () => {\n const formData = {\n uuid: 'test-node-uuid',\n llm: [{ value: 'llm-uuid-123', name: 'Claude' }],\n input: '@input',\n categories: [\n { name: 'Category1' },\n { name: 'Category2' },\n { name: 'Category1' }, // duplicate\n { name: 'category2' }, // case insensitive duplicate\n { name: 'Category3' }\n ]\n };\n\n const validationResult = split_by_llm_categorize.validate!(formData);\n\n expect(validationResult.valid).to.be.false;\n expect(validationResult.errors.categories).to.include(\n 'Duplicate category names found'\n );\n expect(validationResult.errors.categories).to.include('Category1');\n expect(validationResult.errors.categories).to.include('Category2');\n expect(validationResult.errors.categories).to.include('category2');\n });\n\n it('should pass validation with unique category names', () => {\n const formData = {\n uuid: 'test-node-uuid',\n llm: [{ value: 'llm-uuid-123', name: 'Claude' }],\n input: '@input',\n categories: [\n { name: 'Category1' },\n { name: 'Category2' },\n { name: 'Category3' }\n ]\n };\n\n const validationResult = split_by_llm_categorize.validate!(formData);\n\n expect(validationResult.valid).to.be.true;\n expect(Object.keys(validationResult.errors)).to.have.length(0);\n });\n\n it('should ignore empty categories in validation', () => {\n const formData = {\n uuid: 'test-node-uuid',\n llm: [{ value: 'llm-uuid-123', name: 'Claude' }],\n input: '@input',\n categories: [\n { name: 'Category1' },\n { name: '' }, // empty\n { name: ' ' }, // whitespace only\n { name: 'Category2' }\n ]\n };\n\n const validationResult = split_by_llm_categorize.validate!(formData);\n\n expect(validationResult.valid).to.be.true;\n expect(Object.keys(validationResult.errors)).to.have.length(0);\n });\n });\n\n describe('JSON output verification', () => {\n it('generates JSON matching the exact format from the issue', () => {\n const formData = {\n uuid: '145eb3d3-b841-4e66-abac-297ae525c7ad',\n llm: [\n { value: '1c06c884-39dd-4ce4-ad9f-9a01cbe6c000', name: 'Claude' }\n ],\n input: '@input',\n categories: [{ name: 'Flights' }, { name: 'Hotels' }],\n result_name: 'Intent'\n };\n\n const originalNode: Node = {\n uuid: '145eb3d3-b841-4e66-abac-297ae525c7ad',\n actions: [],\n exits: []\n };\n\n const result = split_by_llm_categorize.fromFormData!(\n formData,\n originalNode\n );\n\n // Verify the call_llm action\n const callLlmAction = result.actions[0] as any;\n expect(callLlmAction.type).to.equal('call_llm');\n expect(callLlmAction.llm.uuid).to.equal(\n '1c06c884-39dd-4ce4-ad9f-9a01cbe6c000'\n );\n expect(callLlmAction.llm.name).to.equal('Claude');\n expect(callLlmAction.instructions).to.equal(\n '@(prompt(\"categorize\", slice(node.categories, 0, -2)))'\n );\n expect(callLlmAction.input).to.equal('@input');\n expect(callLlmAction.output_local).to.equal('_llm_output');\n\n // Verify the router structure\n const router = result.router!;\n expect(router.type).to.equal('switch');\n expect(router.operand).to.equal('@locals._llm_output');\n\n // Verify categories structure\n expect(router.categories).to.have.length(4);\n const categoryNames = router.categories.map((cat) => cat.name);\n expect(categoryNames).to.include.members([\n 'Flights',\n 'Hotels',\n 'Other',\n 'Failure'\n ]);\n\n // Verify cases structure\n expect(router.cases).to.have.length(3);\n const caseArguments = router.cases.map((c) => c.arguments[0]);\n expect(caseArguments).to.include.members([\n 'Flights',\n 'Hotels',\n '<ERROR>'\n ]);\n\n // Verify all cases use has_only_text\n router.cases.forEach((caseItem) => {\n expect(caseItem.type).to.equal('has_only_text');\n });\n\n // Verify exits match categories\n expect(result.exits).to.have.length(4);\n router.categories.forEach((category) => {\n const matchingExit = result.exits.find(\n (exit) => exit.uuid === category.exit_uuid\n );\n expect(matchingExit).to.exist;\n });\n\n // Verify default category is \"Other\"\n const otherCategory = router.categories.find(\n (cat) => cat.name === 'Other'\n );\n expect(router.default_category_uuid).to.equal(otherCategory!.uuid);\n });\n });\n});\n"]}