@kiberon-labs/behave-graph-flow 1.0.0 → 3.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 (378) hide show
  1. package/.fallowrc.json +16 -0
  2. package/.storybook/main.ts +32 -0
  3. package/.storybook/manager.ts +6 -0
  4. package/.storybook/preview.ts +64 -0
  5. package/.storybook/styles.css +16 -0
  6. package/.turbo/turbo-build.log +7 -0
  7. package/CHANGELOG.md +368 -0
  8. package/LICENSE +6 -0
  9. package/README.md +2 -2
  10. package/data/Polynomial.json +510 -0
  11. package/data/sequence.json +337 -0
  12. package/data/trigger-event.json +241 -0
  13. package/data/variable-change.json +210 -0
  14. package/dist/AnyControlImpl-Ds-CShIB.js +20 -0
  15. package/dist/AnyControlImpl-Ds-CShIB.js.map +1 -0
  16. package/dist/DocumentationBrowserPanelImpl-deZNzFX8.js +166 -0
  17. package/dist/DocumentationBrowserPanelImpl-deZNzFX8.js.map +1 -0
  18. package/dist/entry.css +4 -0
  19. package/dist/index.css +42 -0
  20. package/dist/index.css.map +1 -0
  21. package/dist/index.d.ts +3597 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +18009 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/noteImpl-KkrrWgJd.js +242 -0
  26. package/dist/noteImpl-KkrrWgJd.js.map +1 -0
  27. package/dist/styles.module-CvmpDkZj.css +3 -0
  28. package/dist/styles.module-CvmpDkZj.css.map +1 -0
  29. package/dist/styles.module-DZxg8aW9.js +271 -0
  30. package/dist/styles.module-DZxg8aW9.js.map +1 -0
  31. package/dist/useChangeNodeData-ChQGK7AI.js +23 -0
  32. package/dist/useChangeNodeData-ChQGK7AI.js.map +1 -0
  33. package/docs/notifications.md +246 -0
  34. package/docs/protocol.md +702 -0
  35. package/docs/specifics.md +191 -0
  36. package/package.json +82 -22
  37. package/postcss.config.ts +3 -4
  38. package/src/annotations/index.ts +32 -0
  39. package/src/components/FloatingToolbar/index.module.css +37 -0
  40. package/src/components/FloatingToolbar/index.tsx +256 -0
  41. package/src/components/Flow.tsx +287 -75
  42. package/src/components/contextMenus/DynamicContextMenu.tsx +85 -0
  43. package/src/components/contextMenus/NodePicker.module.css +274 -0
  44. package/src/components/contextMenus/NodePicker.tsx +481 -0
  45. package/src/components/contextMenus/edge.tsx +22 -0
  46. package/src/components/contextMenus/node.tsx +15 -0
  47. package/src/components/contextMenus/selection.tsx +11 -0
  48. package/src/components/controls/any/AnyControlImpl.tsx +14 -0
  49. package/src/components/controls/any/index.tsx +19 -0
  50. package/src/components/controls/boolean/index.tsx +13 -0
  51. package/src/components/controls/colorPicker/InputPopover.module.css +100 -0
  52. package/src/components/controls/colorPicker/InputPopover.tsx +31 -0
  53. package/src/components/controls/colorPicker/index.module.css +18 -0
  54. package/src/components/controls/colorPicker/index.tsx +61 -0
  55. package/src/components/controls/number/index.tsx +35 -0
  56. package/src/components/controls/string/index.tsx +16 -0
  57. package/src/components/edges/index.tsx +475 -0
  58. package/src/components/edges/offsetBezier.ts +134 -0
  59. package/src/components/hotKeys.tsx +20 -0
  60. package/src/components/layoutController/index.module.css +13 -0
  61. package/src/components/layoutController/index.tsx +140 -0
  62. package/src/components/layoutController/utils.ts +248 -0
  63. package/src/components/menubar/defaults.tsx +516 -0
  64. package/src/components/menubar/index.tsx +49 -0
  65. package/src/components/menubar/menuItem.module.css +31 -0
  66. package/src/components/menubar/menuItem.tsx +65 -0
  67. package/src/components/nodes/behave/Node.module.css +23 -0
  68. package/src/components/nodes/behave/Node.tsx +176 -0
  69. package/src/components/nodes/behave/NodeContainer.module.css +88 -0
  70. package/src/components/nodes/behave/NodeContainer.tsx +46 -0
  71. package/src/components/nodes/behave/index.tsx +14 -0
  72. package/src/components/nodes/group/index.tsx +109 -0
  73. package/src/components/nodes/wrapper/index.tsx +73 -0
  74. package/src/components/nodes/wrapper/styles.module.css +87 -0
  75. package/src/components/notifications/NotificationProvider.tsx +81 -0
  76. package/src/components/notifications/index.ts +2 -0
  77. package/src/components/notifications/utils.ts +71 -0
  78. package/src/components/panels/alignment/index.module.css +10 -0
  79. package/src/components/panels/alignment/index.tsx +244 -0
  80. package/src/components/panels/base/index.tsx +5 -0
  81. package/src/components/panels/base/styles.module.css +12 -0
  82. package/src/components/panels/common/PanelHeader.module.css +24 -0
  83. package/src/components/panels/common/PanelHeader.tsx +22 -0
  84. package/src/components/panels/common/SectionTitle.module.css +13 -0
  85. package/src/components/panels/common/SectionTitle.tsx +10 -0
  86. package/src/components/panels/events/EditEventPanel.tsx +324 -0
  87. package/src/components/panels/events/ManageEventsPanel.tsx +101 -0
  88. package/src/components/panels/events/index.tsx +23 -0
  89. package/src/components/panels/events/styles.module.css +178 -0
  90. package/src/components/panels/graphProperties/index.tsx +125 -0
  91. package/src/components/panels/history/index.tsx +92 -0
  92. package/src/components/panels/history/styles.module.css +97 -0
  93. package/src/components/panels/keymaps/index.module.css +68 -0
  94. package/src/components/panels/keymaps/index.tsx +166 -0
  95. package/src/components/panels/layers/index.tsx +245 -0
  96. package/src/components/panels/layers/styles.module.css +107 -0
  97. package/src/components/panels/legend/index.module.css +6 -0
  98. package/src/components/panels/legend/index.tsx +76 -0
  99. package/src/components/panels/logs/index.module.css +218 -0
  100. package/src/components/panels/logs/index.tsx +288 -0
  101. package/src/components/panels/nodeInputs/InputControl.tsx +63 -0
  102. package/src/components/panels/nodeInputs/InputsGroup.tsx +65 -0
  103. package/src/components/panels/nodeInputs/MultipleNodesView.tsx +37 -0
  104. package/src/components/panels/nodeInputs/NodeSettings.tsx +92 -0
  105. package/src/components/panels/nodeInputs/NodeTitleEditor.tsx +125 -0
  106. package/src/components/panels/nodeInputs/OutputsGroup.tsx +55 -0
  107. package/src/components/panels/nodeInputs/SocketGenerators.tsx +32 -0
  108. package/src/components/panels/nodeInputs/index.module.css +308 -0
  109. package/src/components/panels/nodeInputs/index.tsx +349 -0
  110. package/src/components/panels/nodeInputs/useNodeHandlers.ts +76 -0
  111. package/src/components/panels/nodeInputs/useNodeInputsData.ts +153 -0
  112. package/src/components/panels/nodePicker/index.tsx +115 -0
  113. package/src/components/panels/panel/index.module.css +66 -0
  114. package/src/components/panels/panel/index.tsx +88 -0
  115. package/src/components/panels/search/index.module.css +16 -0
  116. package/src/components/panels/search/index.tsx +215 -0
  117. package/src/components/panels/systemSettings/ConversionsSettings.tsx +203 -0
  118. package/src/components/panels/systemSettings/index.tsx +251 -0
  119. package/src/components/panels/systemSettings/styles.module.css +138 -0
  120. package/src/components/panels/traces/GridLines.tsx +38 -0
  121. package/src/components/panels/traces/TimeGrid.tsx +48 -0
  122. package/src/components/panels/traces/TraceLane.tsx +62 -0
  123. package/src/components/panels/traces/TraceTooltip.tsx +22 -0
  124. package/src/components/panels/traces/TracesHeader.tsx +56 -0
  125. package/src/components/panels/traces/index.module.css +159 -0
  126. package/src/components/panels/traces/index.tsx +298 -0
  127. package/src/components/panels/traces/types.ts +48 -0
  128. package/src/components/panels/traces/useDerivedSpans.ts +307 -0
  129. package/src/components/panels/traces/utils.ts +33 -0
  130. package/src/components/panels/variables/CreateVariableScreen.tsx +162 -0
  131. package/src/components/panels/variables/ManageVariablesScreen.tsx +147 -0
  132. package/src/components/panels/variables/index.tsx +125 -0
  133. package/src/components/panels/variables/styles.module.css +149 -0
  134. package/src/components/primitives/icon.module.css +45 -0
  135. package/src/components/primitives/icon.tsx +38 -0
  136. package/src/components/sockets/input/index.tsx +83 -0
  137. package/src/components/sockets/input/styles.module.css +26 -0
  138. package/src/components/sockets/output/index.tsx +68 -0
  139. package/src/components/sockets/output/styles.module.css +22 -0
  140. package/src/css/notes.css +135 -0
  141. package/src/css/prosemirror.css +57 -0
  142. package/src/css/rc-dock.css +212 -0
  143. package/src/css/rc-menu.css +101 -0
  144. package/src/css/themes/kiberon.css +127 -0
  145. package/src/css/vars.css +198 -0
  146. package/src/css/vscode-elements.css +124 -0
  147. package/src/entry.css +4 -0
  148. package/src/generators/CallSubgraphGenerator.tsx +136 -0
  149. package/src/generators/CustomEventOnTriggeredGenerator.tsx +85 -0
  150. package/src/generators/GraphBoundaryGenerator.module.css +32 -0
  151. package/src/generators/GraphBoundaryGenerator.tsx +193 -0
  152. package/src/generators/SequenceGenerator.tsx +104 -0
  153. package/src/generators/SwitchOnIntegerGenerator.tsx +256 -0
  154. package/src/generators/SwitchOnStringGenerator.tsx +263 -0
  155. package/src/generators/callSubgraphSync.ts +126 -0
  156. package/src/generators/registerDefaultGenerators.ts +55 -0
  157. package/src/generators/registerDefaults.ts +26 -0
  158. package/src/hooks/useBehaveGraphFlow.ts +17 -16
  159. package/src/hooks/useFlowHandlers.ts +154 -30
  160. package/src/hooks/useWasdPan.ts +210 -0
  161. package/src/index.css +134 -0
  162. package/src/index.ts +53 -18
  163. package/src/manifest/contributionRegistry.ts +93 -0
  164. package/src/manifest/index.ts +4 -0
  165. package/src/manifest/loadManifest.ts +82 -0
  166. package/src/manifest/manifestPlugin.ts +29 -0
  167. package/src/manifest/passthroughValueType.ts +40 -0
  168. package/src/plugin/alignment/index.ts +91 -0
  169. package/src/plugin/autosave/controller.ts +366 -0
  170. package/src/plugin/autosave/index.tsx +114 -0
  171. package/src/plugin/autosave/panel/BackupPanel.tsx +141 -0
  172. package/src/plugin/autosave/panel/index.tsx +1 -0
  173. package/src/plugin/autosave/panel/styles.module.css +56 -0
  174. package/src/plugin/autosave/settings.ts +65 -0
  175. package/src/plugin/autosave/storage.ts +147 -0
  176. package/src/plugin/docs/index.tsx +297 -0
  177. package/src/plugin/docs/panel/DocumentationBrowserPanelImpl.tsx +200 -0
  178. package/src/plugin/docs/panel/index.tsx +21 -0
  179. package/src/plugin/docs/panel/styles.module.css +174 -0
  180. package/src/plugin/graphrunner/actions.ts +326 -0
  181. package/src/plugin/graphrunner/buttons.tsx +95 -0
  182. package/src/plugin/graphrunner/client.ts +707 -0
  183. package/src/plugin/graphrunner/index.tsx +184 -0
  184. package/src/plugin/graphrunner/panel.tsx +386 -0
  185. package/src/plugin/graphrunner/runController.ts +283 -0
  186. package/src/plugin/graphrunner/runner.ts +187 -0
  187. package/src/plugin/graphrunner/session.ts +243 -0
  188. package/src/plugin/graphrunner/store.ts +196 -0
  189. package/src/plugin/graphrunner/styles.module.css +171 -0
  190. package/src/plugin/graphrunner/transport.ts +250 -0
  191. package/src/plugin/graphrunner/types.ts +693 -0
  192. package/src/plugin/graphrunner-local/execution-utils.ts +637 -0
  193. package/src/plugin/graphrunner-local/index.tsx +172 -0
  194. package/src/plugin/graphrunner-local/panel.tsx +187 -0
  195. package/src/plugin/graphrunner-local/store.ts +41 -0
  196. package/src/plugin/graphrunner-local/styles.module.css +82 -0
  197. package/src/plugin/graphrunner-local/transport.ts +1339 -0
  198. package/src/plugin/graphrunner-local/types.ts +10 -0
  199. package/src/plugin/graphrunner-webworker/graph-executor.worker.ts +635 -0
  200. package/src/plugin/graphrunner-webworker/index.tsx +140 -0
  201. package/src/plugin/graphrunner-webworker/panel.tsx +173 -0
  202. package/src/plugin/graphrunner-webworker/store.ts +98 -0
  203. package/src/plugin/graphrunner-webworker/worker-transport.ts +123 -0
  204. package/src/plugin/kitchen-sink/index.ts +38 -0
  205. package/src/plugin/layout/dagre.ts +131 -0
  206. package/src/plugin/layout/elk.ts +216 -0
  207. package/src/plugin/layout/index.ts +80 -0
  208. package/src/plugin/notes/FormatToolbar.tsx +200 -0
  209. package/src/plugin/notes/index.tsx +191 -0
  210. package/src/plugin/notes/nodeActions.ts +100 -0
  211. package/src/plugin/notes/note.tsx +20 -0
  212. package/src/plugin/notes/noteImpl.tsx +89 -0
  213. package/src/plugin/realtime/realtimeRunner.ts +624 -0
  214. package/src/specifics/CustomEventOnTriggeredSpecific.tsx +92 -0
  215. package/src/specifics/CustomEventTriggerSpecific.tsx +141 -0
  216. package/src/specifics/VariableGetSpecific.tsx +110 -0
  217. package/src/specifics/VariableSetSpecific.tsx +110 -0
  218. package/src/store/actions.tsx +698 -0
  219. package/src/store/commands.ts +278 -0
  220. package/src/store/contextMenu.ts +192 -0
  221. package/src/store/controls.tsx +62 -0
  222. package/src/store/conversions.ts +47 -0
  223. package/src/store/documentation.tsx +69 -0
  224. package/src/store/events.tsx +116 -0
  225. package/src/store/flow.tsx +230 -0
  226. package/src/store/graphMeta.ts +39 -0
  227. package/src/store/hotKeys.tsx +364 -0
  228. package/src/store/layers.ts +259 -0
  229. package/src/store/legend.tsx +76 -0
  230. package/src/store/logs.ts +28 -0
  231. package/src/store/menubar.ts +41 -0
  232. package/src/store/refs.ts +84 -0
  233. package/src/store/registry.ts +51 -0
  234. package/src/store/selection.ts +22 -0
  235. package/src/store/settings.ts +99 -0
  236. package/src/store/settingsSchema.ts +210 -0
  237. package/src/store/socketGenerator.tsx +54 -0
  238. package/src/store/specific.tsx +75 -0
  239. package/src/store/specs.tsx +35 -0
  240. package/src/store/tabs.ts +282 -0
  241. package/src/store/toolbar.tsx +45 -0
  242. package/src/store/traces.ts +240 -0
  243. package/src/store/variables.ts +37 -0
  244. package/src/system/graph.ts +131 -0
  245. package/src/system/graphSession.ts +172 -0
  246. package/src/system/index.ts +6 -0
  247. package/src/system/notifications.ts +111 -0
  248. package/src/system/persistence.ts +82 -0
  249. package/src/system/plugin.ts +55 -0
  250. package/src/system/provider.tsx +86 -0
  251. package/src/system/pubsub.ts +323 -0
  252. package/src/system/system.ts +653 -0
  253. package/src/system/tabLoader.tsx +303 -0
  254. package/src/system/undoRedo.ts +103 -0
  255. package/src/transformers/Uigraph.ts +61 -0
  256. package/src/transformers/behaveToFlow.ts +16 -4
  257. package/src/transformers/contract.ts +87 -0
  258. package/src/transformers/flowToBehave.ts +40 -12
  259. package/src/types/NodeMetadata.ts +27 -0
  260. package/src/types/graph.ts +49 -0
  261. package/src/types/nodes.ts +50 -0
  262. package/src/types.ts +18 -0
  263. package/src/util/autoConvert.ts +200 -0
  264. package/src/util/colors.ts +1 -29
  265. package/src/util/downloadJson.ts +18 -0
  266. package/src/util/extractNodeMetadata.ts +16 -0
  267. package/src/util/getPickerFilters.ts +1 -1
  268. package/src/util/isBehaveNode.ts +6 -0
  269. package/src/util/isValidConnection.ts +51 -17
  270. package/src/util/mergeSockets.ts +29 -0
  271. package/src/util/serializeVariables.ts +66 -0
  272. package/src/util/sockets.ts +43 -0
  273. package/stories/apex/layoutController/example-graph.worker.ts +39 -0
  274. package/stories/apex/layoutController/index.stories.tsx +48 -0
  275. package/stories/apex/layoutController/webworker.stories.tsx +103 -0
  276. package/stories/apex/menubar/menubar.stories.tsx +19 -0
  277. package/stories/components/colorpicker/index.stories.tsx +20 -0
  278. package/stories/components/contextMenus/edge.stories.tsx +32 -0
  279. package/stories/components/contextMenus/node.stories.tsx +26 -0
  280. package/stories/components/contextMenus/nodePicker.stories.tsx +115 -0
  281. package/stories/components/controls/any/index.stories.tsx +19 -0
  282. package/stories/components/controls/boolean/index.stories.tsx +19 -0
  283. package/stories/components/controls/colorPicker/index.stories.tsx +49 -0
  284. package/stories/components/controls/number/index.stories.tsx +19 -0
  285. package/stories/components/controls/string/index.stories.tsx +19 -0
  286. package/stories/components/nodes/behaveNode.stories.tsx +108 -0
  287. package/stories/components/panels/alignment.stories.tsx +24 -0
  288. package/stories/components/panels/events.stories.tsx +38 -0
  289. package/stories/components/panels/graphRunner.stories.tsx +317 -0
  290. package/stories/components/panels/history.stories.tsx +37 -0
  291. package/stories/components/panels/keymaps.stories.tsx +21 -0
  292. package/stories/components/panels/legend.stories.tsx +37 -0
  293. package/stories/components/panels/logs.stories.tsx +24 -0
  294. package/stories/components/panels/nodeInputs.stories.tsx +21 -0
  295. package/stories/components/panels/nodePicker.stories.tsx +37 -0
  296. package/stories/components/panels/panel.stories.tsx +39 -0
  297. package/stories/components/panels/search.stories.tsx +24 -0
  298. package/stories/components/panels/systemSettings.stories.tsx +26 -0
  299. package/stories/components/panels/traces.stories.tsx +225 -0
  300. package/stories/components/panels/variables.stories.tsx +24 -0
  301. package/stories/defaults/defaultStoryProvider.tsx +170 -0
  302. package/stories/defaults/systemGenerator.ts +43 -0
  303. package/stories/plugins/notes.stories.tsx +100 -0
  304. package/tests/autoConvert.test.ts +329 -0
  305. package/tests/autosavePlugin.test.ts +204 -0
  306. package/tests/callSubgraphSync.test.ts +148 -0
  307. package/tests/commandRegistry.test.ts +137 -0
  308. package/tests/components/edges/offsetBezier.test.ts +51 -0
  309. package/tests/components/layoutController/utils.test.ts +68 -0
  310. package/tests/components/panels/traces/utils.test.ts +52 -0
  311. package/tests/contract.test.ts +51 -0
  312. package/tests/contractSerialize.test.ts +62 -0
  313. package/tests/deriveSpans.test.ts +71 -0
  314. package/tests/flowToBehave.test.ts +27 -4
  315. package/tests/hotkeys.test.ts +79 -0
  316. package/tests/keepAliveLifecycle.test.ts +167 -0
  317. package/tests/loadManifest.test.ts +113 -0
  318. package/tests/noteMarkdown.test.ts +65 -0
  319. package/tests/notesPlugin.test.ts +162 -0
  320. package/tests/notifications.test.ts +87 -0
  321. package/tests/persistence.test.ts +51 -0
  322. package/tests/saveLoad.test.ts +373 -0
  323. package/tests/settings.test.ts +178 -0
  324. package/tests/traceStore.test.ts +46 -0
  325. package/tests/util/calculateNewEdge.test.ts +98 -0
  326. package/tests/util/getSocketsByNodeTypeAndHandleType.test.ts +31 -0
  327. package/tests/util/hasPositionMetaData.test.ts +33 -0
  328. package/tests/util/isBehaveNode.test.ts +22 -0
  329. package/tests/util/isHandleConnected.test.ts +37 -0
  330. package/tests/util/mergeSockets.test.ts +43 -0
  331. package/tests/visual/README.md +64 -0
  332. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-alignment-chromium-win32.png +0 -0
  333. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-conversation-chromium-win32.png +0 -0
  334. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-events-chromium-win32.png +0 -0
  335. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-history-chromium-win32.png +0 -0
  336. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-keymaps-chromium-win32.png +0 -0
  337. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-layers-chromium-win32.png +0 -0
  338. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-legend-chromium-win32.png +0 -0
  339. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-localGraphRunner-chromium-win32.png +0 -0
  340. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-logs-chromium-win32.png +0 -0
  341. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-nodeInputs-chromium-win32.png +0 -0
  342. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-nodePicker-chromium-win32.png +0 -0
  343. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-panel-chromium-win32.png +0 -0
  344. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-search-chromium-win32.png +0 -0
  345. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-systemSettings-chromium-win32.png +0 -0
  346. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-traces-chromium-win32.png +0 -0
  347. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-variables-chromium-win32.png +0 -0
  348. package/tests/visual/panels.visual.test.tsx +76 -0
  349. package/tests/wasdPan.test.ts +71 -0
  350. package/tsconfig.base.json +39 -0
  351. package/tsconfig.json +18 -59
  352. package/tsconfig.prod.json +23 -0
  353. package/tsdown.config.ts +15 -3
  354. package/typedoc.json +7 -7
  355. package/vite.config.js +7 -0
  356. package/vitest.config.ts +5 -2
  357. package/vitest.visual.config.ts +55 -0
  358. package/src/components/AutoSizeInput.tsx +0 -65
  359. package/src/components/Controls.tsx +0 -87
  360. package/src/components/InputSocket.tsx +0 -142
  361. package/src/components/Node.tsx +0 -68
  362. package/src/components/NodeContainer.tsx +0 -46
  363. package/src/components/NodePicker.tsx +0 -77
  364. package/src/components/OutputSocket.tsx +0 -58
  365. package/src/components/modals/ClearModal.tsx +0 -40
  366. package/src/components/modals/HelpModal.tsx +0 -36
  367. package/src/components/modals/LoadModal.tsx +0 -96
  368. package/src/components/modals/Modal.tsx +0 -64
  369. package/src/components/modals/SaveModal.tsx +0 -60
  370. package/src/hooks/useCustomNodeTypes.tsx +0 -31
  371. package/src/hooks/useGraphRunner.ts +0 -104
  372. package/src/hooks/useMergeMap.ts +0 -14
  373. package/src/hooks/useNodeSpecJson.ts +0 -20
  374. package/src/hooks/useQueriableDefinitions.ts +0 -22
  375. package/src/styles.css +0 -8
  376. package/tailwind.config.ts +0 -19
  377. package/tests/tsconfig.json +0 -10
  378. /package/src/{types.d.ts → types-declarations.d.ts} +0 -0
@@ -0,0 +1,167 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ Engine,
4
+ ManualLifecycleEventEmitter,
5
+ makeEventNodeDefinition,
6
+ makeFlowNodeDefinition,
7
+ readGraphFromJSON,
8
+ registerCoreProfile,
9
+ NodeCategory,
10
+ type GraphJSON
11
+ } from '@kiberon-labs/behave-graph';
12
+ import {
13
+ executeGraphLifecycle,
14
+ type ActiveRun,
15
+ type MessageContext
16
+ } from '../src/plugin/graphrunner-local/execution-utils.js';
17
+
18
+ /**
19
+ * A minimal external event source standing in for the AI conversation runtime:
20
+ * the event node subscribes on init, and `fire()` commits a flow the way
21
+ * ai/onToolCall does when the model requests a tool.
22
+ */
23
+ const makeExternalSource = () => {
24
+ const listeners = new Set<() => void>();
25
+ return {
26
+ subscribe(listener: () => void) {
27
+ listeners.add(listener);
28
+ return () => listeners.delete(listener);
29
+ },
30
+ fire() {
31
+ listeners.forEach((listener) => listener());
32
+ },
33
+ get listenerCount() {
34
+ return listeners.size;
35
+ }
36
+ };
37
+ };
38
+
39
+ const buildRun = (handled: string[]) => {
40
+ const source = makeExternalSource();
41
+
42
+ const onExternal = makeEventNodeDefinition({
43
+ typeName: 'test/onExternal',
44
+ category: NodeCategory.Event,
45
+ in: {},
46
+ out: { flow: 'flow' },
47
+ initialState: { unsubscribe: undefined as undefined | (() => void) },
48
+ init: ({ commit }) => ({
49
+ unsubscribe: source.subscribe(() => commit('flow'))
50
+ }),
51
+ dispose: ({ state }) => {
52
+ state.unsubscribe?.();
53
+ return { unsubscribe: undefined };
54
+ }
55
+ });
56
+
57
+ const handle = makeFlowNodeDefinition({
58
+ typeName: 'test/handle',
59
+ category: NodeCategory.Action,
60
+ in: { flow: 'flow' },
61
+ out: {},
62
+ initialState: undefined,
63
+ triggered: () => {
64
+ handled.push('handled');
65
+ }
66
+ });
67
+
68
+ const registry = registerCoreProfile({
69
+ nodes: {},
70
+ values: {},
71
+ dependencies: {
72
+ ILifecycleEventEmitter: new ManualLifecycleEventEmitter()
73
+ }
74
+ });
75
+ registry.nodes['test/onExternal'] = onExternal;
76
+ registry.nodes['test/handle'] = handle;
77
+
78
+ const graphJson: GraphJSON = {
79
+ name: 'keep-alive test',
80
+ nodes: [
81
+ {
82
+ id: 'listener',
83
+ type: 'test/onExternal',
84
+ flows: { flow: { nodeId: 'handler', socket: 'flow' } }
85
+ },
86
+ { id: 'handler', type: 'test/handle' }
87
+ ],
88
+ variables: [],
89
+ customEvents: []
90
+ };
91
+
92
+ const graphInstance = readGraphFromJSON({ graphJson, registry });
93
+ const engine = new Engine(graphInstance, registry);
94
+
95
+ const run: ActiveRun = {
96
+ runId: 'run-1',
97
+ graphId: 'graph-1',
98
+ engine,
99
+ graphInstance,
100
+ registry,
101
+ status: 'running',
102
+ startedAt: Date.now(),
103
+ performance: { nodesExecuted: 0, eventsEmitted: 0, variableChanges: 0 },
104
+ isPaused: false,
105
+ executionPhase: 'start',
106
+ currentTick: 0
107
+ };
108
+
109
+ return { run, source };
110
+ };
111
+
112
+ const collectMessages = () => {
113
+ const messages: Array<{ type: string }> = [];
114
+ const ctx: MessageContext = {
115
+ sendMessage: (message) => messages.push(message),
116
+ sendError: () => {}
117
+ };
118
+ return { messages, ctx };
119
+ };
120
+
121
+ describe('graph run lifecycle', () => {
122
+ it('autoEnd: true finalizes when flows drain and unsubscribes event nodes', async () => {
123
+ const handled: string[] = [];
124
+ const { run, source } = buildRun(handled);
125
+ const { messages, ctx } = collectMessages();
126
+
127
+ // The engine constructor fires event-node init without awaiting it; let
128
+ // its state (the unsubscribe handle) settle before the lifecycle disposes.
129
+ await new Promise((resolve) => setTimeout(resolve, 0));
130
+
131
+ await executeGraphLifecycle(run, 'graph-1', ctx, { autoEnd: true });
132
+
133
+ expect(run.status).toBe('completed');
134
+ expect(messages.some((m) => m.type === 'completed')).toBe(true);
135
+ // Dispose tore the subscription down; a late event finds no listener.
136
+ expect(source.listenerCount).toBe(0);
137
+ });
138
+
139
+ it('stays alive by default and services events fired after the flows drain', async () => {
140
+ const handled: string[] = [];
141
+ const { run, source } = buildRun(handled);
142
+ const { messages, ctx } = collectMessages();
143
+
144
+ const lifecycle = executeGraphLifecycle(run, 'graph-1', ctx, {
145
+ tickInterval: 1
146
+ });
147
+
148
+ // Give the lifecycle time to drain the (empty) start flow. With autoEnd
149
+ // it would have completed here.
150
+ await new Promise((resolve) => setTimeout(resolve, 20));
151
+ expect(run.status).toBe('running');
152
+ expect(source.listenerCount).toBe(1);
153
+
154
+ // The out-of-band event (the "tool call") arrives after the start flow
155
+ // ended; the idle loop must still drain the fiber it commits.
156
+ source.fire();
157
+ await new Promise((resolve) => setTimeout(resolve, 20));
158
+ expect(handled).toEqual(['handled']);
159
+
160
+ // Stopping ends the idle loop; no `completed` message is emitted.
161
+ run.status = 'stopped';
162
+ await lifecycle;
163
+ expect(messages.some((m) => m.type === 'completed')).toBe(false);
164
+
165
+ run.engine.dispose();
166
+ });
167
+ });
@@ -0,0 +1,113 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import type {
3
+ ManifestJSON,
4
+ PackageRequirement
5
+ } from '@kiberon-labs/behave-graph';
6
+ import { System } from '../src/system/system.js';
7
+ import { loadManifest } from '../src/manifest/loadManifest.js';
8
+
9
+ const Vec2Control = () => null;
10
+ const conversionRule = {
11
+ from: 'vec2',
12
+ to: 'object',
13
+ nodeType: 'convert/vec2ToObject'
14
+ };
15
+
16
+ const manifest: ManifestJSON = {
17
+ manifestVersion: 1,
18
+ package: { name: '@test/pkg', version: '1.0.0' },
19
+ values: [{ name: 'vec2', defaultJSON: { x: 0, y: 0 } }],
20
+ nodes: [
21
+ {
22
+ type: 'test/node',
23
+ category: 'Logic',
24
+ label: 'Test Node',
25
+ inputs: [],
26
+ outputs: [],
27
+ configuration: []
28
+ }
29
+ ],
30
+ contributions: [
31
+ {
32
+ id: 'vec2-ctrl',
33
+ kind: 'control',
34
+ export: './ui.js#Vec2Control',
35
+ bind: { controlName: 'vec2' }
36
+ },
37
+ { id: 'vec2-conv', kind: 'conversion', export: './ui.js#rule' }
38
+ ],
39
+ requirements: [
40
+ { kind: 'backendService', entry: './server.js', persistent: true }
41
+ ]
42
+ };
43
+
44
+ const resolve = (c: { id: string }) => {
45
+ if (c.id === 'vec2-ctrl') return Vec2Control;
46
+ if (c.id === 'vec2-conv') return conversionRule;
47
+ return undefined;
48
+ };
49
+
50
+ describe('loadManifest', () => {
51
+ it('loads nodes + pass-through value types without trust (no code exec)', async () => {
52
+ const system = new System();
53
+ await loadManifest(system, manifest);
54
+
55
+ const reg = system.registry.getState();
56
+ expect(reg.specs.some((s) => s.type === 'test/node')).toBe(true);
57
+
58
+ const vec2 = reg.values['vec2'];
59
+ expect(vec2).toBeDefined();
60
+ // Pass-through creator returns a *clone* of the declared default.
61
+ const created = vec2.creator();
62
+ expect(created).toEqual({ x: 0, y: 0 });
63
+ expect(created).not.toBe(manifest.values[0].defaultJSON);
64
+ // Identity (de)serialize keeps existing UI call sites working.
65
+ expect(vec2.serialize?.(created)).toEqual({ x: 0, y: 0 });
66
+
67
+ // No contributions applied without trust.
68
+ expect(system.controlStore.getState().controls['vec2']).toBeUndefined();
69
+ });
70
+
71
+ it('surfaces host requirements via onRequirement', async () => {
72
+ const system = new System();
73
+ const seen: PackageRequirement[] = [];
74
+ await loadManifest(system, manifest, {
75
+ onRequirement: (req) => seen.push(req)
76
+ });
77
+ expect(seen).toHaveLength(1);
78
+ expect(seen[0].kind).toBe('backendService');
79
+ });
80
+
81
+ it('applies code contributions only under trust + resolve', async () => {
82
+ const system = new System();
83
+ await loadManifest(system, manifest, { trust: true, resolve });
84
+
85
+ expect(system.controlStore.getState().controls['vec2']).toBe(Vec2Control);
86
+ expect(
87
+ system.conversionStore.getState().findConversion('vec2', 'object')
88
+ ).toEqual(conversionRule);
89
+ });
90
+
91
+ it('skips contributions when trusted but no resolver is given', async () => {
92
+ const system = new System();
93
+ await loadManifest(system, manifest, { trust: true });
94
+ expect(system.controlStore.getState().controls['vec2']).toBeUndefined();
95
+ });
96
+
97
+ it('does not let one failing contribution abort the rest', async () => {
98
+ const system = new System();
99
+ const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
100
+ await loadManifest(system, manifest, {
101
+ trust: true,
102
+ resolve: (c) => {
103
+ if (c.id === 'vec2-ctrl') throw new Error('boom');
104
+ return resolve(c);
105
+ }
106
+ });
107
+ // The conversion still registered despite the control throwing.
108
+ expect(
109
+ system.conversionStore.getState().findConversion('vec2', 'object')
110
+ ).toEqual(conversionRule);
111
+ spy.mockRestore();
112
+ });
113
+ });
@@ -0,0 +1,65 @@
1
+ // @vitest-environment happy-dom
2
+ // @vitest-environment-options { "settings": { "disableIframePageLoading": true } }
3
+ // (embedded youtube iframes must not trigger real network requests in tests)
4
+
5
+ import { describe, it, expect } from 'vitest';
6
+ import { Editor } from '@tiptap/react';
7
+ import StarterKit from '@tiptap/starter-kit';
8
+ import Youtube from '@tiptap/extension-youtube';
9
+ import { Markdown } from 'tiptap-markdown';
10
+
11
+ const makeEditor = (content: string) =>
12
+ new Editor({
13
+ element: document.createElement('div'),
14
+ extensions: [StarterKit, Markdown, Youtube.configure({ nocookie: true })],
15
+ content
16
+ });
17
+
18
+ const getMarkdown = (editor: Editor): string =>
19
+ (
20
+ editor.storage as unknown as { markdown: { getMarkdown(): string } }
21
+ ).markdown.getMarkdown();
22
+
23
+ describe('note markdown serialization', () => {
24
+ it('round-trips formatted text', () => {
25
+ const editor = makeEditor('# Title\n\nSome **bold** and `code`.');
26
+ const md = getMarkdown(editor);
27
+ expect(md).toContain('# Title');
28
+ expect(md).toContain('**bold**');
29
+ expect(md).toContain('`code`');
30
+ editor.destroy();
31
+ });
32
+
33
+ it('embeds a youtube video and round-trips it through markdown', () => {
34
+ const editor = makeEditor('');
35
+ const inserted = editor
36
+ .chain()
37
+ .setYoutubeVideo({ src: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ' })
38
+ .run();
39
+ expect(inserted).toBe(true);
40
+
41
+ // The video serializes as an HTML block inside the markdown text.
42
+ const md = getMarkdown(editor);
43
+ expect(md).toContain('data-youtube-video');
44
+ expect(md).toContain('dQw4w9WgXcQ');
45
+ editor.destroy();
46
+
47
+ // Loading that markdown back (what NoteNodeImpl does with data.text)
48
+ // restores the embed.
49
+ const reloaded = makeEditor(md);
50
+ expect(reloaded.getHTML()).toContain('data-youtube-video');
51
+ expect(reloaded.getHTML()).toContain('dQw4w9WgXcQ');
52
+ reloaded.destroy();
53
+ });
54
+
55
+ it('rejects non-youtube urls', () => {
56
+ const editor = makeEditor('');
57
+ const inserted = editor
58
+ .chain()
59
+ .setYoutubeVideo({ src: 'https://example.com/video.mp4' })
60
+ .run();
61
+ expect(inserted).toBe(false);
62
+ expect(getMarkdown(editor)).not.toContain('example.com');
63
+ editor.destroy();
64
+ });
65
+ });
@@ -0,0 +1,162 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { System } from '../src/system/system.js';
3
+ import type { GraphSession } from '../src/system/graphSession.js';
4
+ import {
5
+ notesPlugin,
6
+ NOTE_NODE_TYPE,
7
+ LEGACY_COMMENT_NODE_TYPE
8
+ } from '../src/plugin/notes/index.js';
9
+
10
+ describe('notes plugin', () => {
11
+ let system: System;
12
+ let session: GraphSession;
13
+
14
+ const addNote = (position = { x: 10, y: 20 }) =>
15
+ system.commandStore.getState().run('notes.addNote', {
16
+ editor: system,
17
+ session,
18
+ position
19
+ });
20
+
21
+ const nodes = () => session.nodeStore.getState().nodes;
22
+
23
+ beforeEach(async () => {
24
+ system = new System();
25
+ await system.registerPlugin(notesPlugin);
26
+ session = system.createSession('graph');
27
+ });
28
+
29
+ it('does not register note node types without the plugin', () => {
30
+ const bare = new System();
31
+ const bareSession = bare.createSession('graph');
32
+ expect(bareSession.flowStore.getState().nodeTypes[NOTE_NODE_TYPE]).toBe(
33
+ undefined
34
+ );
35
+ });
36
+
37
+ it('registers the note component (and legacy alias) on new sessions', () => {
38
+ const { nodeTypes } = session.flowStore.getState();
39
+ expect(nodeTypes[NOTE_NODE_TYPE]).toBeDefined();
40
+ expect(nodeTypes[LEGACY_COMMENT_NODE_TYPE]).toBe(nodeTypes[NOTE_NODE_TYPE]);
41
+ });
42
+
43
+ it('adds a toolbar group with the Add Note button', () => {
44
+ const group = system.toolbarStore
45
+ .getState()
46
+ .groups.find((g) => g.id === 'notes');
47
+ expect(group).toBeDefined();
48
+ expect(group?.buttons).toHaveLength(1);
49
+ });
50
+
51
+ it('notes.addNote creates a selected, header-draggable note (undoable)', () => {
52
+ void addNote();
53
+
54
+ expect(nodes()).toHaveLength(1);
55
+ const note = nodes()[0];
56
+ expect(note?.type).toBe(NOTE_NODE_TYPE);
57
+ expect(note?.position).toEqual({ x: 10, y: 20 });
58
+ expect(note?.selected).toBe(true);
59
+ expect(note?.dragHandle).toBe('.notes-node__header');
60
+ expect(note?.data).toEqual({ text: '' });
61
+
62
+ session.undoManager.undo();
63
+ expect(nodes()).toHaveLength(0);
64
+ session.undoManager.redo();
65
+ expect(nodes()).toHaveLength(1);
66
+ });
67
+
68
+ it('note nodes are ignored by the behave graph transform', () => {
69
+ void addNote();
70
+ const graph = session.flowStore.getState().getGraph();
71
+ expect(graph.nodes ?? []).toHaveLength(0);
72
+ });
73
+
74
+ it('note.duplicate clones the note with an offset', () => {
75
+ void addNote();
76
+ const original = nodes()[0]!;
77
+
78
+ void system.commandStore.getState().run('note.duplicate', {
79
+ editor: system,
80
+ session,
81
+ nodeId: original.id
82
+ });
83
+
84
+ expect(nodes()).toHaveLength(2);
85
+ const copy = nodes()[1]!;
86
+ expect(copy.id).not.toBe(original.id);
87
+ expect(copy.position).toEqual({
88
+ x: original.position.x + 24,
89
+ y: original.position.y + 24
90
+ });
91
+
92
+ session.undoManager.undo();
93
+ expect(nodes()).toHaveLength(1);
94
+ });
95
+
96
+ it('note.delete removes the note and undo restores it', () => {
97
+ void addNote();
98
+ const note = nodes()[0]!;
99
+
100
+ void system.commandStore.getState().run('note.delete', {
101
+ editor: system,
102
+ session,
103
+ nodeId: note.id
104
+ });
105
+ expect(nodes()).toHaveLength(0);
106
+
107
+ session.undoManager.undo();
108
+ expect(nodes()).toHaveLength(1);
109
+ expect(nodes()[0]?.id).toBe(note.id);
110
+ });
111
+
112
+ it('bring to front / send to back reorder within the nodes array', () => {
113
+ void addNote({ x: 0, y: 0 });
114
+ void addNote({ x: 5, y: 5 });
115
+ const [first, second] = nodes();
116
+
117
+ void system.commandStore.getState().run('note.bringToFront', {
118
+ editor: system,
119
+ session,
120
+ nodeId: first!.id
121
+ });
122
+ expect(nodes().map((n) => n.id)).toEqual([second!.id, first!.id]);
123
+
124
+ void system.commandStore.getState().run('note.sendToBack', {
125
+ editor: system,
126
+ session,
127
+ nodeId: first!.id
128
+ });
129
+ expect(nodes().map((n) => n.id)).toEqual([first!.id, second!.id]);
130
+ });
131
+
132
+ it('behave-only context menu items are hidden on notes, note items shown', () => {
133
+ void addNote();
134
+ const note = nodes()[0]!;
135
+ const ctx = { editor: system, session, nodeId: note.id };
136
+
137
+ const visible = system.contextMenuStore
138
+ .getState()
139
+ .getItems('node')
140
+ .filter((i) => !i.when || i.when(ctx))
141
+ .map((i) => i.id);
142
+
143
+ expect(visible).toContain('note.duplicate');
144
+ expect(visible).toContain('note.delete');
145
+ expect(visible).not.toContain('node.traceUpstream');
146
+ expect(visible).not.toContain('node.togglePinned');
147
+ });
148
+
149
+ it('note commands no-op for behave nodes', () => {
150
+ session.actionStore
151
+ .getState()
152
+ .actions.addBehaveNode('debug/log', { x: 0, y: 0 });
153
+ const behave = nodes()[0]!;
154
+
155
+ void system.commandStore.getState().run('note.delete', {
156
+ editor: system,
157
+ session,
158
+ nodeId: behave.id
159
+ });
160
+ expect(nodes()).toHaveLength(1);
161
+ });
162
+ });
@@ -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,51 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { System } from '../src/system/system.js';
3
+ import type { UIGraphJSON } from '../src/types/graph.js';
4
+
5
+ const fakeUIGraph = (): UIGraphJSON =>
6
+ ({ flow: { nodes: [] } }) as unknown as UIGraphJSON;
7
+
8
+ describe('default graph/layout persistence', () => {
9
+ let system: System;
10
+
11
+ beforeEach(() => {
12
+ system = new System();
13
+ });
14
+
15
+ it('subscribes save handlers by default (no per-host wiring)', () => {
16
+ // The three save topics each have exactly one default subscriber.
17
+ expect(system.pubsub.countSubscriptions('graph:saved')).toBe(1);
18
+ expect(system.pubsub.countSubscriptions('graph:inner:saved')).toBe(1);
19
+ expect(system.pubsub.countSubscriptions('layout:saved')).toBe(1);
20
+ });
21
+
22
+ it('routes a custom adapter and keeps defaults for omitted topics', () => {
23
+ const saveGraph = vi.fn();
24
+ system.enablePersistence({ saveGraph });
25
+
26
+ // Replacing does not stack subscriptions.
27
+ expect(system.pubsub.countSubscriptions('graph:saved')).toBe(1);
28
+
29
+ const graph = fakeUIGraph();
30
+ system.pubsub.publishSync('graph:saved', graph);
31
+ expect(saveGraph).toHaveBeenCalledWith(graph);
32
+ });
33
+
34
+ it('disablePersistence removes all save handlers', () => {
35
+ system.disablePersistence();
36
+ expect(system.pubsub.countSubscriptions('graph:saved')).toBe(0);
37
+ expect(system.pubsub.countSubscriptions('graph:inner:saved')).toBe(0);
38
+ expect(system.pubsub.countSubscriptions('layout:saved')).toBe(0);
39
+
40
+ // Publishing after disabling is a no-op (no subscriber throws).
41
+ expect(() =>
42
+ system.pubsub.publishSync('graph:saved', fakeUIGraph())
43
+ ).not.toThrow();
44
+ });
45
+
46
+ it('does not throw when the default (download) sink runs without a DOM', () => {
47
+ const graph = fakeUIGraph();
48
+ // jsdom provides document/URL, but the guard makes this safe either way.
49
+ expect(() => system.pubsub.publishSync('graph:saved', graph)).not.toThrow();
50
+ });
51
+ });