@kiberon-labs/behave-graph-flow 1.0.0 → 2.0.0

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 (314) hide show
  1. package/.fallowrc.json +16 -0
  2. package/.storybook/main.ts +32 -0
  3. package/.storybook/preview.ts +16 -0
  4. package/.storybook/styles.css +10 -0
  5. package/.storybook/vscode.css +814 -0
  6. package/.turbo/turbo-build.log +7 -0
  7. package/LICENSE +6 -0
  8. package/README.md +2 -2
  9. package/data/Polynomial.json +510 -0
  10. package/data/sequence.json +337 -0
  11. package/data/trigger-event.json +241 -0
  12. package/data/variable-change.json +210 -0
  13. package/dist/entry.css +4 -0
  14. package/dist/index.css +39 -0
  15. package/dist/index.css.map +1 -0
  16. package/dist/index.d.ts +2282 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +14873 -0
  19. package/dist/index.js.map +1 -0
  20. package/docs/notifications.md +246 -0
  21. package/docs/protocol.md +679 -0
  22. package/docs/specifics.md +191 -0
  23. package/package.json +85 -21
  24. package/postcss.config.ts +3 -4
  25. package/src/annotations/index.ts +32 -0
  26. package/src/components/FloatingToolbar/index.module.css +45 -0
  27. package/src/components/FloatingToolbar/index.tsx +256 -0
  28. package/src/components/Flow.tsx +276 -75
  29. package/src/components/contextMenus/NodePicker.module.css +274 -0
  30. package/src/components/contextMenus/NodePicker.tsx +481 -0
  31. package/src/components/contextMenus/edge.tsx +108 -0
  32. package/src/components/contextMenus/node.tsx +155 -0
  33. package/src/components/contextMenus/selection.tsx +77 -0
  34. package/src/components/controls/any/index.tsx +8 -0
  35. package/src/components/controls/boolean/index.tsx +13 -0
  36. package/src/components/controls/colorPicker/InputPopover.module.css +100 -0
  37. package/src/components/controls/colorPicker/InputPopover.tsx +31 -0
  38. package/src/components/controls/colorPicker/index.module.css +18 -0
  39. package/src/components/controls/colorPicker/index.tsx +61 -0
  40. package/src/components/controls/number/index.tsx +35 -0
  41. package/src/components/controls/string/index.tsx +16 -0
  42. package/src/components/edges/index.tsx +469 -0
  43. package/src/components/edges/offsetBezier.ts +134 -0
  44. package/src/components/hotKeys.tsx +20 -0
  45. package/src/components/layoutController/index.module.css +10 -0
  46. package/src/components/layoutController/index.tsx +117 -0
  47. package/src/components/layoutController/utils.ts +205 -0
  48. package/src/components/menubar/defaults.tsx +480 -0
  49. package/src/components/menubar/index.tsx +49 -0
  50. package/src/components/menubar/menuItem.module.css +16 -0
  51. package/src/components/menubar/menuItem.tsx +32 -0
  52. package/src/components/nodes/behave/Node.module.css +23 -0
  53. package/src/components/nodes/behave/Node.tsx +176 -0
  54. package/src/components/nodes/behave/NodeContainer.module.css +87 -0
  55. package/src/components/nodes/behave/NodeContainer.tsx +46 -0
  56. package/src/components/nodes/behave/index.tsx +14 -0
  57. package/src/components/nodes/comment/FormatToolbar.tsx +118 -0
  58. package/src/components/nodes/comment/comment.tsx +103 -0
  59. package/src/components/nodes/comment/styles.module.css +150 -0
  60. package/src/components/nodes/group/index.tsx +109 -0
  61. package/src/components/nodes/wrapper/index.tsx +73 -0
  62. package/src/components/nodes/wrapper/styles.module.css +113 -0
  63. package/src/components/notifications/NotificationProvider.tsx +81 -0
  64. package/src/components/notifications/index.ts +2 -0
  65. package/src/components/notifications/utils.ts +71 -0
  66. package/src/components/panels/alignment/index.module.css +20 -0
  67. package/src/components/panels/alignment/index.tsx +244 -0
  68. package/src/components/panels/base/index.tsx +5 -0
  69. package/src/components/panels/base/styles.module.css +12 -0
  70. package/src/components/panels/conversation/index.module.css +151 -0
  71. package/src/components/panels/conversation/index.tsx +162 -0
  72. package/src/components/panels/events/CustomEventsEditor.tsx +384 -0
  73. package/src/components/panels/events/EditEventPanel.tsx +315 -0
  74. package/src/components/panels/events/ManageEventsPanel.tsx +98 -0
  75. package/src/components/panels/events/index.tsx +23 -0
  76. package/src/components/panels/events/styles.module.css +236 -0
  77. package/src/components/panels/history/index.tsx +92 -0
  78. package/src/components/panels/history/styles.module.css +106 -0
  79. package/src/components/panels/keymaps/index.module.css +78 -0
  80. package/src/components/panels/keymaps/index.tsx +167 -0
  81. package/src/components/panels/layers/index.tsx +240 -0
  82. package/src/components/panels/layers/styles.module.css +110 -0
  83. package/src/components/panels/legend/index.module.css +6 -0
  84. package/src/components/panels/legend/index.tsx +76 -0
  85. package/src/components/panels/logs/index.module.css +212 -0
  86. package/src/components/panels/logs/index.tsx +288 -0
  87. package/src/components/panels/nodeInputs/InputControl.tsx +63 -0
  88. package/src/components/panels/nodeInputs/InputsGroup.tsx +64 -0
  89. package/src/components/panels/nodeInputs/MultipleNodesView.tsx +37 -0
  90. package/src/components/panels/nodeInputs/NodeSettings.tsx +92 -0
  91. package/src/components/panels/nodeInputs/NodeTitleEditor.tsx +125 -0
  92. package/src/components/panels/nodeInputs/OutputsGroup.tsx +65 -0
  93. package/src/components/panels/nodeInputs/SocketGenerators.tsx +32 -0
  94. package/src/components/panels/nodeInputs/index.module.css +284 -0
  95. package/src/components/panels/nodeInputs/index.tsx +339 -0
  96. package/src/components/panels/nodeInputs/useNodeHandlers.ts +76 -0
  97. package/src/components/panels/nodeInputs/useNodeInputsData.ts +173 -0
  98. package/src/components/panels/nodePicker/index.tsx +115 -0
  99. package/src/components/panels/panel/index.module.css +66 -0
  100. package/src/components/panels/panel/index.tsx +88 -0
  101. package/src/components/panels/search/index.module.css +66 -0
  102. package/src/components/panels/search/index.tsx +215 -0
  103. package/src/components/panels/systemSettings/index.tsx +206 -0
  104. package/src/components/panels/systemSettings/styles.module.css +11 -0
  105. package/src/components/panels/traces/GridLines.tsx +38 -0
  106. package/src/components/panels/traces/TimeGrid.tsx +48 -0
  107. package/src/components/panels/traces/TraceLane.tsx +62 -0
  108. package/src/components/panels/traces/TraceTooltip.tsx +22 -0
  109. package/src/components/panels/traces/TracesHeader.tsx +56 -0
  110. package/src/components/panels/traces/index.module.css +166 -0
  111. package/src/components/panels/traces/index.tsx +294 -0
  112. package/src/components/panels/traces/types.ts +48 -0
  113. package/src/components/panels/traces/useDerivedSpans.ts +212 -0
  114. package/src/components/panels/traces/utils.ts +25 -0
  115. package/src/components/panels/variables/CreateVariableScreen.tsx +162 -0
  116. package/src/components/panels/variables/ManageVariablesScreen.tsx +144 -0
  117. package/src/components/panels/variables/index.tsx +125 -0
  118. package/src/components/panels/variables/styles.module.css +236 -0
  119. package/src/components/primitives/icon.module.css +45 -0
  120. package/src/components/primitives/icon.tsx +38 -0
  121. package/src/components/sockets/input/index.tsx +76 -0
  122. package/src/components/sockets/input/styles.module.css +27 -0
  123. package/src/components/sockets/output/index.tsx +61 -0
  124. package/src/components/sockets/output/styles.module.css +27 -0
  125. package/src/css/prosemirror.css +57 -0
  126. package/src/css/rc-dock.css +112 -0
  127. package/src/css/rc-menu.css +100 -0
  128. package/src/css/vars.css +14 -0
  129. package/src/css/vscode.css +13 -0
  130. package/src/entry.css +4 -0
  131. package/src/generators/CustomEventOnTriggeredGenerator.tsx +85 -0
  132. package/src/generators/SequenceGenerator.tsx +104 -0
  133. package/src/generators/SwitchOnIntegerGenerator.tsx +256 -0
  134. package/src/generators/SwitchOnStringGenerator.tsx +263 -0
  135. package/src/generators/registerDefaultGenerators.ts +34 -0
  136. package/src/hooks/useBehaveGraphFlow.ts +17 -16
  137. package/src/hooks/useDetachNodes.ts +39 -0
  138. package/src/hooks/useFlowHandlers.ts +115 -29
  139. package/src/hooks/useWasdPan.ts +188 -0
  140. package/src/index.css +146 -0
  141. package/src/index.ts +36 -18
  142. package/src/layout/dagre.tsx +119 -0
  143. package/src/layout/elk.ts +200 -0
  144. package/src/plugin/alignment/index.ts +81 -0
  145. package/src/plugin/docs/index.tsx +299 -0
  146. package/src/plugin/docs/panel/index.tsx +200 -0
  147. package/src/plugin/docs/panel/styles.module.css +174 -0
  148. package/src/plugin/graphrunner/actions.ts +253 -0
  149. package/src/plugin/graphrunner/buttons.tsx +87 -0
  150. package/src/plugin/graphrunner/client.ts +704 -0
  151. package/src/plugin/graphrunner/index.tsx +255 -0
  152. package/src/plugin/graphrunner/panel.tsx +386 -0
  153. package/src/plugin/graphrunner/runner.ts +358 -0
  154. package/src/plugin/graphrunner/session.ts +243 -0
  155. package/src/plugin/graphrunner/store.ts +206 -0
  156. package/src/plugin/graphrunner/styles.module.css +211 -0
  157. package/src/plugin/graphrunner/transport.ts +224 -0
  158. package/src/plugin/graphrunner/types.ts +672 -0
  159. package/src/plugin/graphrunner-local/execution-utils.ts +457 -0
  160. package/src/plugin/graphrunner-local/index.tsx +166 -0
  161. package/src/plugin/graphrunner-local/panel.tsx +231 -0
  162. package/src/plugin/graphrunner-local/store.ts +41 -0
  163. package/src/plugin/graphrunner-local/styles.module.css +101 -0
  164. package/src/plugin/graphrunner-local/transport.ts +1372 -0
  165. package/src/plugin/graphrunner-local/types.ts +10 -0
  166. package/src/plugin/graphrunner-webworker/graph-executor.worker.ts +633 -0
  167. package/src/plugin/graphrunner-webworker/index.tsx +146 -0
  168. package/src/plugin/graphrunner-webworker/panel.tsx +173 -0
  169. package/src/plugin/graphrunner-webworker/store.ts +89 -0
  170. package/src/plugin/graphrunner-webworker/types.ts +17 -0
  171. package/src/plugin/graphrunner-webworker/worker-transport.ts +123 -0
  172. package/src/plugin/realtime/realtimeRunner.ts +570 -0
  173. package/src/specifics/CustomEventOnTriggeredSpecific.tsx +92 -0
  174. package/src/specifics/CustomEventTriggerSpecific.tsx +141 -0
  175. package/src/specifics/VariableGetSpecific.tsx +110 -0
  176. package/src/specifics/VariableSetSpecific.tsx +110 -0
  177. package/src/specifics/registerDefaultSpecifics.ts +5 -0
  178. package/src/store/actions.tsx +698 -0
  179. package/src/store/chat.ts +73 -0
  180. package/src/store/controls.tsx +62 -0
  181. package/src/store/documentation.tsx +69 -0
  182. package/src/store/events.tsx +116 -0
  183. package/src/store/flow.tsx +245 -0
  184. package/src/store/graphRunnerClient.ts +110 -0
  185. package/src/store/hotKeys.tsx +323 -0
  186. package/src/store/layers.ts +259 -0
  187. package/src/store/legend.tsx +76 -0
  188. package/src/store/logs.ts +28 -0
  189. package/src/store/menubar.ts +41 -0
  190. package/src/store/refs.ts +84 -0
  191. package/src/store/registry.ts +43 -0
  192. package/src/store/selection.ts +22 -0
  193. package/src/store/settings.ts +99 -0
  194. package/src/store/socketGenerator.tsx +54 -0
  195. package/src/store/specific.tsx +75 -0
  196. package/src/store/specs.tsx +35 -0
  197. package/src/store/tabs.ts +278 -0
  198. package/src/store/toolbar.tsx +45 -0
  199. package/src/store/traces.ts +240 -0
  200. package/src/store/variables.ts +37 -0
  201. package/src/system/graph.ts +134 -0
  202. package/src/system/index.ts +3 -0
  203. package/src/system/notifications.ts +98 -0
  204. package/src/system/plugin.ts +27 -0
  205. package/src/system/provider.tsx +22 -0
  206. package/src/system/pubsub.ts +323 -0
  207. package/src/system/system.ts +223 -0
  208. package/src/system/tabLoader.tsx +265 -0
  209. package/src/system/undoRedo.ts +103 -0
  210. package/src/transformers/Uigraph.ts +60 -0
  211. package/src/transformers/behaveToFlow.ts +16 -4
  212. package/src/transformers/flowToBehave.ts +32 -12
  213. package/src/types/NodeMetadata.ts +27 -0
  214. package/src/types/graph.ts +49 -0
  215. package/src/types/nodes.ts +45 -0
  216. package/src/types.ts +16 -0
  217. package/src/util/colors.ts +1 -29
  218. package/src/util/downloadJson.ts +18 -0
  219. package/src/util/extractNodeMetadata.ts +16 -0
  220. package/src/util/getPickerFilters.ts +1 -1
  221. package/src/util/isBehaveNode.ts +6 -0
  222. package/src/util/isValidConnection.ts +28 -15
  223. package/src/util/mergeSockets.ts +29 -0
  224. package/src/util/serializeVariables.ts +66 -0
  225. package/src/util/sockets.ts +43 -0
  226. package/stories/apex/layoutController/example-graph.worker.ts +39 -0
  227. package/stories/apex/layoutController/index.stories.tsx +48 -0
  228. package/stories/apex/layoutController/webworker.stories.tsx +103 -0
  229. package/stories/apex/menubar/menubar.stories.tsx +19 -0
  230. package/stories/components/colorpicker/index.stories.tsx +20 -0
  231. package/stories/components/contextMenus/edge.stories.tsx +32 -0
  232. package/stories/components/contextMenus/node.stories.tsx +26 -0
  233. package/stories/components/contextMenus/nodePicker.stories.tsx +115 -0
  234. package/stories/components/controls/any/index.stories.tsx +19 -0
  235. package/stories/components/controls/boolean/index.stories.tsx +19 -0
  236. package/stories/components/controls/colorPicker/index.stories.tsx +49 -0
  237. package/stories/components/controls/number/index.stories.tsx +19 -0
  238. package/stories/components/controls/string/index.stories.tsx +19 -0
  239. package/stories/components/nodes/behaveNode.stories.tsx +108 -0
  240. package/stories/components/nodes/comment.stories.tsx +106 -0
  241. package/stories/components/panels/alignment.stories.tsx +24 -0
  242. package/stories/components/panels/events.stories.tsx +38 -0
  243. package/stories/components/panels/graphRunner.stories.tsx +317 -0
  244. package/stories/components/panels/history.stories.tsx +37 -0
  245. package/stories/components/panels/keymaps.stories.tsx +21 -0
  246. package/stories/components/panels/legend.stories.tsx +37 -0
  247. package/stories/components/panels/logs.stories.tsx +24 -0
  248. package/stories/components/panels/nodeInputs.stories.tsx +21 -0
  249. package/stories/components/panels/nodePicker.stories.tsx +37 -0
  250. package/stories/components/panels/panel.stories.tsx +39 -0
  251. package/stories/components/panels/search.stories.tsx +24 -0
  252. package/stories/components/panels/systemSettings.stories.tsx +26 -0
  253. package/stories/components/panels/traces.stories.tsx +225 -0
  254. package/stories/components/panels/variables.stories.tsx +24 -0
  255. package/stories/defaults/defaultStoryProvider.tsx +167 -0
  256. package/stories/defaults/systemGenerator.ts +38 -0
  257. package/tests/components/edges/offsetBezier.test.ts +51 -0
  258. package/tests/components/layoutController/utils.test.ts +68 -0
  259. package/tests/components/panels/traces/utils.test.ts +52 -0
  260. package/tests/flowToBehave.test.ts +26 -4
  261. package/tests/notifications.test.ts +87 -0
  262. package/tests/saveLoad.test.ts +372 -0
  263. package/tests/util/calculateNewEdge.test.ts +98 -0
  264. package/tests/util/getSocketsByNodeTypeAndHandleType.test.ts +31 -0
  265. package/tests/util/hasPositionMetaData.test.ts +33 -0
  266. package/tests/util/isBehaveNode.test.ts +22 -0
  267. package/tests/util/isHandleConnected.test.ts +37 -0
  268. package/tests/util/mergeSockets.test.ts +43 -0
  269. package/tests/visual/README.md +64 -0
  270. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-alignment-chromium-win32.png +0 -0
  271. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-conversation-chromium-win32.png +0 -0
  272. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-events-chromium-win32.png +0 -0
  273. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-history-chromium-win32.png +0 -0
  274. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-keymaps-chromium-win32.png +0 -0
  275. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-layers-chromium-win32.png +0 -0
  276. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-legend-chromium-win32.png +0 -0
  277. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-logs-chromium-win32.png +0 -0
  278. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-nodeInputs-chromium-win32.png +0 -0
  279. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-nodePicker-chromium-win32.png +0 -0
  280. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-panel-chromium-win32.png +0 -0
  281. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-search-chromium-win32.png +0 -0
  282. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-systemSettings-chromium-win32.png +0 -0
  283. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-traces-chromium-win32.png +0 -0
  284. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-variables-chromium-win32.png +0 -0
  285. package/tests/visual/panels.visual.test.tsx +76 -0
  286. package/tsconfig.base.json +39 -0
  287. package/tsconfig.json +18 -59
  288. package/tsconfig.prod.json +23 -0
  289. package/tsdown.config.ts +15 -3
  290. package/typedoc.json +7 -7
  291. package/vite.config.js +7 -0
  292. package/vitest.config.ts +5 -2
  293. package/vitest.visual.config.ts +48 -0
  294. package/src/components/AutoSizeInput.tsx +0 -65
  295. package/src/components/Controls.tsx +0 -87
  296. package/src/components/InputSocket.tsx +0 -142
  297. package/src/components/Node.tsx +0 -68
  298. package/src/components/NodeContainer.tsx +0 -46
  299. package/src/components/NodePicker.tsx +0 -77
  300. package/src/components/OutputSocket.tsx +0 -58
  301. package/src/components/modals/ClearModal.tsx +0 -40
  302. package/src/components/modals/HelpModal.tsx +0 -36
  303. package/src/components/modals/LoadModal.tsx +0 -96
  304. package/src/components/modals/Modal.tsx +0 -64
  305. package/src/components/modals/SaveModal.tsx +0 -60
  306. package/src/hooks/useCustomNodeTypes.tsx +0 -31
  307. package/src/hooks/useGraphRunner.ts +0 -104
  308. package/src/hooks/useMergeMap.ts +0 -14
  309. package/src/hooks/useNodeSpecJson.ts +0 -20
  310. package/src/hooks/useQueriableDefinitions.ts +0 -22
  311. package/src/styles.css +0 -8
  312. package/tailwind.config.ts +0 -19
  313. package/tests/tsconfig.json +0 -10
  314. /package/src/{types.d.ts → types-declarations.d.ts} +0 -0
@@ -0,0 +1,87 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { System } from '@/system/system';
3
+ import {
4
+ notifySuccess,
5
+ notifyError,
6
+ notifyInfo,
7
+ notifyLoading
8
+ } from '@/components/notifications/utils';
9
+
10
+ describe('Notification System', () => {
11
+ let system: System;
12
+ let publishSpy: ReturnType<typeof vi.spyOn>;
13
+
14
+ beforeEach(() => {
15
+ system = new System();
16
+ publishSpy = vi.spyOn(system.pubsub, 'publish');
17
+ });
18
+
19
+ describe('notifySuccess', () => {
20
+ it('should publish success notification', () => {
21
+ const message = 'Success!';
22
+
23
+ notifySuccess(system.pubsub, message);
24
+
25
+ expect(publishSpy).toHaveBeenCalledWith('notification', {
26
+ type: 'success',
27
+ message,
28
+ options: undefined
29
+ });
30
+ });
31
+
32
+ it('should include options when provided', () => {
33
+ const message = 'Success with options!';
34
+ const options = { duration: 5000, id: 'test-id' };
35
+
36
+ notifySuccess(system.pubsub, message, options);
37
+
38
+ expect(publishSpy).toHaveBeenCalledWith('notification', {
39
+ type: 'success',
40
+ message,
41
+ options
42
+ });
43
+ });
44
+ });
45
+
46
+ describe('notifyError', () => {
47
+ it('should publish error notification', () => {
48
+ const message = 'Error!';
49
+
50
+ notifyError(system.pubsub, message);
51
+
52
+ expect(publishSpy).toHaveBeenCalledWith('notification', {
53
+ type: 'error',
54
+ message,
55
+ options: undefined
56
+ });
57
+ });
58
+ });
59
+
60
+ describe('notifyInfo', () => {
61
+ it('should publish info notification', () => {
62
+ const message = 'Info!';
63
+
64
+ notifyInfo(system.pubsub, message);
65
+
66
+ expect(publishSpy).toHaveBeenCalledWith('notification', {
67
+ type: 'info',
68
+ message,
69
+ options: undefined
70
+ });
71
+ });
72
+ });
73
+
74
+ describe('notifyLoading', () => {
75
+ it('should publish loading notification', () => {
76
+ const message = 'Loading...';
77
+
78
+ notifyLoading(system.pubsub, message);
79
+
80
+ expect(publishSpy).toHaveBeenCalledWith('notification', {
81
+ type: 'loading',
82
+ message,
83
+ options: undefined
84
+ });
85
+ });
86
+ });
87
+ });
@@ -0,0 +1,372 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import {
3
+ registerCoreProfile,
4
+ writeNodeSpecsToJSON
5
+ } from '@kiberon-labs/behave-graph';
6
+ import { System } from '../src/system/system.js';
7
+ import { buildUIGraphJSON } from '../src/transformers/Uigraph.js';
8
+
9
+ describe('Save and Load Graph', () => {
10
+ let system: System;
11
+
12
+ beforeEach(() => {
13
+ // Create a system with a basic registry
14
+ const coreRegistry = registerCoreProfile({
15
+ nodes: {},
16
+ values: {},
17
+ dependencies: {}
18
+ });
19
+
20
+ const nodeSpecs = writeNodeSpecsToJSON(coreRegistry);
21
+ const registry = {
22
+ values: coreRegistry.values,
23
+ specs: nodeSpecs
24
+ };
25
+
26
+ system = new System(registry);
27
+ });
28
+
29
+ it('should save and load a graph with node positions', () => {
30
+ // Setup: Create a graph with positioned nodes
31
+ const initialNodes = [
32
+ {
33
+ id: 'node1',
34
+ type: 'behaveNode',
35
+ position: { x: 100, y: 200 },
36
+ data: {
37
+ type: 'lifecycle/onStart',
38
+ configuration: {},
39
+ ports: {},
40
+ annotations: {}
41
+ }
42
+ },
43
+ {
44
+ id: 'node2',
45
+ type: 'behaveNode',
46
+ position: { x: 300, y: 400 },
47
+ data: {
48
+ type: 'debug/log',
49
+ configuration: {},
50
+ ports: {},
51
+ annotations: {}
52
+ }
53
+ }
54
+ ];
55
+
56
+ const initialEdges = [
57
+ {
58
+ id: 'edge1',
59
+ source: 'node1',
60
+ target: 'node2',
61
+ sourceHandle: 'flow',
62
+ targetHandle: 'flow'
63
+ }
64
+ ];
65
+
66
+ system.nodeStore.getState().setNodes(initialNodes);
67
+ system.edgeStore.getState().setEdges(initialEdges);
68
+
69
+ // Save the graph
70
+ const savedGraph = buildUIGraphJSON(system);
71
+
72
+ // Verify saved graph has nodes with positions
73
+ expect(savedGraph.nodes).toHaveLength(2);
74
+ expect(savedGraph.nodes[0].position).toEqual({ x: 100, y: 200 });
75
+ expect(savedGraph.nodes[1].position).toEqual({ x: 300, y: 400 });
76
+
77
+ // Clear the system
78
+ system.nodeStore.getState().setNodes([]);
79
+ system.edgeStore.getState().setEdges([]);
80
+
81
+ // Load the graph
82
+ system.graph.deseralize(savedGraph);
83
+ system.flowStore.getState().setGraph(savedGraph.flow, { skipLayout: true });
84
+
85
+ // Verify loaded nodes have correct positions
86
+ const loadedNodes = system.nodeStore.getState().nodes;
87
+ expect(loadedNodes).toHaveLength(2);
88
+ expect(loadedNodes[0].position).toEqual({ x: 100, y: 200 });
89
+ expect(loadedNodes[1].position).toEqual({ x: 300, y: 400 });
90
+ });
91
+
92
+ it('should preserve viewport when saving and loading', () => {
93
+ // Setup: Create nodes and set viewport
94
+ const mockReactFlow = {
95
+ getViewport: () => ({ x: 50, y: 100, zoom: 1.5 }),
96
+ setViewport: () => {}
97
+ };
98
+
99
+ system.refStore.getState().setRef('reactflow', mockReactFlow);
100
+
101
+ const initialNodes = [
102
+ {
103
+ id: 'node1',
104
+ type: 'behaveNode',
105
+ position: { x: 100, y: 200 },
106
+ data: {
107
+ type: 'lifecycle/onStart',
108
+ configuration: {},
109
+ ports: {},
110
+ annotations: {}
111
+ }
112
+ }
113
+ ];
114
+
115
+ system.nodeStore.getState().setNodes(initialNodes);
116
+
117
+ // Save the graph
118
+ const savedGraph = buildUIGraphJSON(system);
119
+
120
+ // Verify viewport is saved
121
+ expect(savedGraph.user?.viewport).toEqual({ x: 50, y: 100, zoom: 1.5 });
122
+ });
123
+
124
+ it('should preserve variables when saving and loading', () => {
125
+ // Setup: Create variables
126
+ const variables = {
127
+ var1: {
128
+ id: 'var1',
129
+ name: 'myVariable',
130
+ valueTypeName: 'string',
131
+ initialValue: 'test value'
132
+ }
133
+ };
134
+
135
+ system.variableStore.getState().setVariables(variables);
136
+
137
+ // Add a node so we have something to save
138
+ const nodes = [
139
+ {
140
+ id: 'node1',
141
+ type: 'behaveNode',
142
+ position: { x: 0, y: 0 },
143
+ data: {
144
+ type: 'lifecycle/onStart',
145
+ configuration: {},
146
+ ports: {},
147
+ annotations: {}
148
+ }
149
+ }
150
+ ];
151
+ system.nodeStore.getState().setNodes(nodes);
152
+
153
+ // Save the graph
154
+ const savedGraph = buildUIGraphJSON(system);
155
+
156
+ // Verify variables are in the flow
157
+ expect(savedGraph.flow.variables).toBeDefined();
158
+ expect(savedGraph.flow.variables?.length).toBe(1);
159
+ expect(savedGraph.flow.variables?.[0].name).toBe('myVariable');
160
+
161
+ // Clear variables
162
+ system.variableStore.getState().setVariables({});
163
+
164
+ // Load the graph
165
+ system.graph.deseralize(savedGraph);
166
+ system.flowStore.getState().setGraph(savedGraph.flow, { skipLayout: true });
167
+
168
+ // Verify variables are restored
169
+ const loadedVariables = system.variableStore.getState().variables;
170
+ expect(Object.keys(loadedVariables)).toHaveLength(1);
171
+ expect(loadedVariables['var1'].name).toBe('myVariable');
172
+ });
173
+
174
+ it('should preserve custom events when saving and loading', () => {
175
+ // Setup: Create custom events
176
+ const customEvents = [
177
+ {
178
+ id: 'event1',
179
+ name: 'customEvent',
180
+ parameters: []
181
+ }
182
+ ];
183
+
184
+ system.eventsStore.getState().setCustomEvents(customEvents);
185
+
186
+ // Add a node so we have something to save
187
+ const nodes = [
188
+ {
189
+ id: 'node1',
190
+ type: 'behaveNode',
191
+ position: { x: 0, y: 0 },
192
+ data: {
193
+ type: 'lifecycle/onStart',
194
+ configuration: {},
195
+ ports: {},
196
+ annotations: {}
197
+ }
198
+ }
199
+ ];
200
+ system.nodeStore.getState().setNodes(nodes);
201
+
202
+ // Save the graph
203
+ const savedGraph = buildUIGraphJSON(system);
204
+
205
+ // Verify custom events are in the flow
206
+ expect(savedGraph.flow.customEvents).toEqual(customEvents);
207
+
208
+ // Clear events
209
+ system.eventsStore.getState().setCustomEvents([]);
210
+
211
+ // Load the graph
212
+ system.graph.deseralize(savedGraph);
213
+ system.flowStore.getState().setGraph(savedGraph.flow, { skipLayout: true });
214
+
215
+ // Verify custom events are restored
216
+ const loadedEvents = system.eventsStore.getState().getCustomEvents();
217
+ expect(loadedEvents).toEqual(customEvents);
218
+ });
219
+
220
+ it('should not overwrite node positions when loading with skipLayout', () => {
221
+ // Setup: Create a graph with specific positions
222
+ const nodesWithPositions = [
223
+ {
224
+ id: 'node1',
225
+ type: 'behaveNode',
226
+ position: { x: 100, y: 200 },
227
+ data: {
228
+ type: 'lifecycle/onStart',
229
+ configuration: {},
230
+ ports: {},
231
+ annotations: {}
232
+ }
233
+ }
234
+ ];
235
+
236
+ system.nodeStore.getState().setNodes(nodesWithPositions);
237
+
238
+ // Save the graph
239
+ const savedGraph = buildUIGraphJSON(system);
240
+
241
+ // Modify the node position
242
+ system.nodeStore.getState().setNodes([
243
+ {
244
+ ...nodesWithPositions[0],
245
+ position: { x: 999, y: 999 }
246
+ }
247
+ ]);
248
+
249
+ // Load the graph with skipLayout
250
+ system.graph.deseralize(savedGraph);
251
+ system.flowStore.getState().setGraph(savedGraph.flow, { skipLayout: true });
252
+
253
+ // Verify positions from deseralize are preserved (not overwritten)
254
+ const loadedNodes = system.nodeStore.getState().nodes;
255
+ expect(loadedNodes[0].position).toEqual({ x: 100, y: 200 });
256
+ });
257
+
258
+ it('should apply auto-layout when loading graph without position metadata', () => {
259
+ // Create a plain GraphJSON without position metadata
260
+ const graphWithoutPositions = {
261
+ nodes: [
262
+ {
263
+ id: 'node1',
264
+ type: 'lifecycle/onStart',
265
+ metadata: {}
266
+ },
267
+ {
268
+ id: 'node2',
269
+ type: 'debug/log',
270
+ metadata: {}
271
+ }
272
+ ],
273
+ variables: [],
274
+ customEvents: []
275
+ };
276
+
277
+ // Load the graph (should apply auto-layout)
278
+ system.flowStore.getState().setGraph(graphWithoutPositions);
279
+
280
+ // Verify nodes were created with positions (auto-layout applied)
281
+ const loadedNodes = system.nodeStore.getState().nodes;
282
+ expect(loadedNodes).toHaveLength(2);
283
+ expect(loadedNodes[0].position).toBeDefined();
284
+ expect(loadedNodes[1].position).toBeDefined();
285
+ // Auto-layout should set non-zero positions
286
+ const hasLayout = loadedNodes.some(
287
+ (n) => n.position.x !== 0 || n.position.y !== 0
288
+ );
289
+ expect(hasLayout).toBe(true);
290
+ });
291
+
292
+ it('should handle round-trip save/load without data loss', () => {
293
+ // Setup a complete graph
294
+ const nodes = [
295
+ {
296
+ id: 'node1',
297
+ type: 'behaveNode',
298
+ position: { x: 100, y: 200 },
299
+ data: {
300
+ type: 'lifecycle/onStart',
301
+ configuration: {},
302
+ ports: {},
303
+ annotations: {}
304
+ }
305
+ },
306
+ {
307
+ id: 'node2',
308
+ type: 'behaveNode',
309
+ position: { x: 300, y: 400 },
310
+ data: {
311
+ type: 'debug/log',
312
+ configuration: { text: { value: 'test' } },
313
+ ports: {},
314
+ annotations: {}
315
+ }
316
+ }
317
+ ];
318
+
319
+ const edges = [
320
+ {
321
+ id: 'edge1',
322
+ source: 'node1',
323
+ target: 'node2',
324
+ sourceHandle: 'flow',
325
+ targetHandle: 'flow'
326
+ }
327
+ ];
328
+
329
+ const variables = {
330
+ var1: {
331
+ id: 'var1',
332
+ name: 'testVar',
333
+ valueTypeName: 'float',
334
+ initialValue: 42
335
+ }
336
+ };
337
+
338
+ system.nodeStore.getState().setNodes(nodes);
339
+ system.edgeStore.getState().setEdges(edges);
340
+ system.variableStore.getState().setVariables(variables);
341
+
342
+ // Save
343
+ const saved = buildUIGraphJSON(system);
344
+
345
+ // Clear
346
+ system.nodeStore.getState().setNodes([]);
347
+ system.edgeStore.getState().setEdges([]);
348
+ system.variableStore.getState().setVariables({});
349
+
350
+ // Load
351
+ system.graph.deseralize(saved);
352
+ system.flowStore.getState().setGraph(saved.flow, { skipLayout: true });
353
+
354
+ // Verify everything is restored
355
+ const loadedNodes = system.nodeStore.getState().nodes;
356
+ const loadedEdges = system.edgeStore.getState().edges;
357
+ const loadedVars = system.variableStore.getState().variables;
358
+
359
+ expect(loadedNodes).toHaveLength(2);
360
+ expect(loadedNodes[0].position).toEqual({ x: 100, y: 200 });
361
+ expect(loadedNodes[1].data.configuration).toEqual({
362
+ text: { value: 'test' }
363
+ });
364
+
365
+ expect(loadedEdges).toHaveLength(1);
366
+ expect(loadedEdges[0].source).toBe('node1');
367
+ expect(loadedEdges[0].target).toBe('node2');
368
+
369
+ expect(Object.keys(loadedVars)).toHaveLength(1);
370
+ expect(loadedVars['var1'].initialValue).toBe(42);
371
+ });
372
+ });
@@ -0,0 +1,98 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { calculateNewEdge } from '@/util/calculateNewEdge';
3
+ import type { NodeSpecJSON } from '@kiberon-labs/behave-graph';
4
+ import type { Node, OnConnectStartParams } from 'reactflow';
5
+
6
+ const specJSON = [
7
+ {
8
+ type: 'source/node',
9
+ inputs: [],
10
+ outputs: [{ name: 'out', valueType: 'flow' }]
11
+ },
12
+ {
13
+ type: 'target/node',
14
+ inputs: [{ name: 'in', valueType: 'flow' }],
15
+ outputs: []
16
+ }
17
+ ] as unknown as NodeSpecJSON[];
18
+
19
+ const originNode = { id: 'origin', type: 'source/node' } as Node;
20
+
21
+ describe('calculateNewEdge', () => {
22
+ it('wires source -> target when dragging from a source handle', () => {
23
+ const connection: OnConnectStartParams = {
24
+ nodeId: 'origin',
25
+ handleId: 'out',
26
+ handleType: 'source'
27
+ };
28
+
29
+ const edge = calculateNewEdge(
30
+ originNode,
31
+ 'target/node',
32
+ 'dest',
33
+ connection,
34
+ specJSON
35
+ );
36
+
37
+ expect(edge).toMatchObject({
38
+ source: 'origin',
39
+ sourceHandle: 'out',
40
+ target: 'dest',
41
+ targetHandle: 'in'
42
+ });
43
+ expect(typeof edge.id).toBe('string');
44
+ expect(edge.id.length).toBeGreaterThan(0);
45
+ });
46
+
47
+ it('wires target -> source when dragging from a target handle', () => {
48
+ const connection: OnConnectStartParams = {
49
+ nodeId: 'origin',
50
+ handleId: 'in',
51
+ handleType: 'target'
52
+ };
53
+ const targetOrigin = { id: 'origin', type: 'target/node' } as Node;
54
+
55
+ const edge = calculateNewEdge(
56
+ targetOrigin,
57
+ 'source/node',
58
+ 'dest',
59
+ connection,
60
+ specJSON
61
+ );
62
+
63
+ expect(edge).toMatchObject({
64
+ target: 'origin',
65
+ targetHandle: 'in',
66
+ source: 'dest',
67
+ sourceHandle: 'out'
68
+ });
69
+ });
70
+
71
+ it('matches the destination socket by value type', () => {
72
+ const multiSpec = [
73
+ {
74
+ type: 'source/node',
75
+ inputs: [],
76
+ outputs: [{ name: 'out', valueType: 'string' }]
77
+ },
78
+ {
79
+ type: 'target/node',
80
+ inputs: [
81
+ { name: 'flowIn', valueType: 'flow' },
82
+ { name: 'strIn', valueType: 'string' }
83
+ ],
84
+ outputs: []
85
+ }
86
+ ] as unknown as NodeSpecJSON[];
87
+
88
+ const edge = calculateNewEdge(
89
+ originNode,
90
+ 'target/node',
91
+ 'dest',
92
+ { nodeId: 'origin', handleId: 'out', handleType: 'source' },
93
+ multiSpec
94
+ );
95
+
96
+ expect(edge.targetHandle).toBe('strIn');
97
+ });
98
+ });
@@ -0,0 +1,31 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { getSocketsByNodeTypeAndHandleType } from '@/util/getSocketsByNodeTypeAndHandleType';
3
+ import type { NodeSpecJSON } from '@kiberon-labs/behave-graph';
4
+
5
+ const specs = [
6
+ {
7
+ type: 'math/add',
8
+ inputs: [{ name: 'a', valueType: 'float' }],
9
+ outputs: [{ name: 'result', valueType: 'float' }]
10
+ }
11
+ ] as unknown as NodeSpecJSON[];
12
+
13
+ describe('getSocketsByNodeTypeAndHandleType', () => {
14
+ it('returns the output sockets for a source handle', () => {
15
+ expect(
16
+ getSocketsByNodeTypeAndHandleType(specs, 'math/add', 'source')
17
+ ).toEqual([{ name: 'result', valueType: 'float' }]);
18
+ });
19
+
20
+ it('returns the input sockets for a target handle', () => {
21
+ expect(
22
+ getSocketsByNodeTypeAndHandleType(specs, 'math/add', 'target')
23
+ ).toEqual([{ name: 'a', valueType: 'float' }]);
24
+ });
25
+
26
+ it('returns undefined for an unknown node type', () => {
27
+ expect(
28
+ getSocketsByNodeTypeAndHandleType(specs, 'does/not/exist', 'source')
29
+ ).toBeUndefined();
30
+ });
31
+ });
@@ -0,0 +1,33 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { hasPositionMetaData } from '@/util/hasPositionMetaData';
3
+ import type { GraphJSON } from '@kiberon-labs/behave-graph';
4
+
5
+ const graph = (nodes: unknown): GraphJSON => ({ nodes }) as GraphJSON;
6
+
7
+ describe('hasPositionMetaData', () => {
8
+ it('returns true when any node carries position metadata', () => {
9
+ expect(
10
+ hasPositionMetaData(
11
+ graph([
12
+ { type: 'a', id: '1' },
13
+ { type: 'b', id: '2', metadata: { positionX: '10', positionY: '20' } }
14
+ ])
15
+ )
16
+ ).toBe(true);
17
+ });
18
+
19
+ it('returns false when no node carries position metadata', () => {
20
+ expect(
21
+ hasPositionMetaData(
22
+ graph([
23
+ { type: 'a', id: '1' },
24
+ { type: 'b', id: '2', metadata: { label: 'hi' } }
25
+ ])
26
+ )
27
+ ).toBe(false);
28
+ });
29
+
30
+ it('returns false when there are no nodes', () => {
31
+ expect(hasPositionMetaData(graph(undefined))).toBe(false);
32
+ });
33
+ });
@@ -0,0 +1,22 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { isBehaveNode } from '@/util/isBehaveNode';
3
+ import type { Node } from 'reactflow';
4
+
5
+ const node = (type: unknown): Node =>
6
+ ({ id: 'n', position: { x: 0, y: 0 }, data: {}, type }) as Node;
7
+
8
+ describe('isBehaveNode', () => {
9
+ it('returns true for node types starting with "behaveNode"', () => {
10
+ expect(isBehaveNode(node('behaveNode'))).toBe(true);
11
+ expect(isBehaveNode(node('behaveNode-flow'))).toBe(true);
12
+ });
13
+
14
+ it('returns false for other node types', () => {
15
+ expect(isBehaveNode(node('comment'))).toBe(false);
16
+ expect(isBehaveNode(node('group'))).toBe(false);
17
+ });
18
+
19
+ it('returns false when the type is missing', () => {
20
+ expect(isBehaveNode(node(undefined))).toBe(false);
21
+ });
22
+ });
@@ -0,0 +1,37 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { isHandleConnected } from '@/util/isHandleConnected';
3
+ import type { Edge } from 'reactflow';
4
+
5
+ const edge = (over: Partial<Edge>): Edge =>
6
+ ({ id: 'e', source: 's', target: 't', ...over }) as Edge;
7
+
8
+ describe('isHandleConnected', () => {
9
+ const edges: Edge[] = [
10
+ edge({
11
+ source: 'n1',
12
+ sourceHandle: 'out',
13
+ target: 'n2',
14
+ targetHandle: 'in'
15
+ })
16
+ ];
17
+
18
+ it('detects a connected source handle', () => {
19
+ expect(isHandleConnected(edges, 'n1', 'out', 'source')).toBe(true);
20
+ });
21
+
22
+ it('detects a connected target handle', () => {
23
+ expect(isHandleConnected(edges, 'n2', 'in', 'target')).toBe(true);
24
+ });
25
+
26
+ it('returns false when the node id does not match', () => {
27
+ expect(isHandleConnected(edges, 'other', 'out', 'source')).toBe(false);
28
+ });
29
+
30
+ it('returns false when the handle id does not match', () => {
31
+ expect(isHandleConnected(edges, 'n1', 'wrong', 'source')).toBe(false);
32
+ });
33
+
34
+ it('returns false for an empty edge list', () => {
35
+ expect(isHandleConnected([], 'n1', 'out', 'source')).toBe(false);
36
+ });
37
+ });