@kiberon-labs/behave-graph-flow 2.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 (251) hide show
  1. package/.storybook/manager.ts +6 -0
  2. package/.storybook/preview.ts +49 -1
  3. package/.storybook/styles.css +9 -3
  4. package/.turbo/turbo-build.log +1 -1
  5. package/CHANGELOG.md +368 -0
  6. package/dist/AnyControlImpl-Ds-CShIB.js +20 -0
  7. package/dist/AnyControlImpl-Ds-CShIB.js.map +1 -0
  8. package/dist/DocumentationBrowserPanelImpl-deZNzFX8.js +166 -0
  9. package/dist/DocumentationBrowserPanelImpl-deZNzFX8.js.map +1 -0
  10. package/dist/index.css +36 -33
  11. package/dist/index.css.map +1 -1
  12. package/dist/index.d.ts +1865 -550
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +14357 -11221
  15. package/dist/index.js.map +1 -1
  16. package/dist/noteImpl-KkrrWgJd.js +242 -0
  17. package/dist/noteImpl-KkrrWgJd.js.map +1 -0
  18. package/dist/styles.module-CvmpDkZj.css +3 -0
  19. package/dist/styles.module-CvmpDkZj.css.map +1 -0
  20. package/dist/styles.module-DZxg8aW9.js +271 -0
  21. package/dist/styles.module-DZxg8aW9.js.map +1 -0
  22. package/dist/useChangeNodeData-ChQGK7AI.js +23 -0
  23. package/dist/useChangeNodeData-ChQGK7AI.js.map +1 -0
  24. package/docs/protocol.md +43 -20
  25. package/package.json +5 -9
  26. package/src/components/FloatingToolbar/index.module.css +5 -13
  27. package/src/components/FloatingToolbar/index.tsx +9 -9
  28. package/src/components/Flow.tsx +34 -23
  29. package/src/components/contextMenus/DynamicContextMenu.tsx +85 -0
  30. package/src/components/contextMenus/NodePicker.module.css +13 -13
  31. package/src/components/contextMenus/edge.tsx +9 -95
  32. package/src/components/contextMenus/node.tsx +9 -149
  33. package/src/components/contextMenus/selection.tsx +5 -71
  34. package/src/components/controls/any/AnyControlImpl.tsx +14 -0
  35. package/src/components/controls/any/index.tsx +13 -2
  36. package/src/components/edges/index.tsx +75 -69
  37. package/src/components/layoutController/index.module.css +3 -0
  38. package/src/components/layoutController/index.tsx +24 -1
  39. package/src/components/layoutController/utils.ts +46 -3
  40. package/src/components/menubar/defaults.tsx +55 -19
  41. package/src/components/menubar/menuItem.module.css +18 -3
  42. package/src/components/menubar/menuItem.tsx +34 -1
  43. package/src/components/nodes/behave/NodeContainer.module.css +26 -25
  44. package/src/components/nodes/group/index.tsx +3 -3
  45. package/src/components/nodes/wrapper/styles.module.css +6 -32
  46. package/src/components/panels/alignment/index.module.css +0 -10
  47. package/src/components/panels/alignment/index.tsx +4 -4
  48. package/src/components/panels/base/styles.module.css +2 -2
  49. package/src/components/panels/common/PanelHeader.module.css +24 -0
  50. package/src/components/panels/common/PanelHeader.tsx +22 -0
  51. package/src/components/panels/common/SectionTitle.module.css +13 -0
  52. package/src/components/panels/common/SectionTitle.tsx +10 -0
  53. package/src/components/panels/events/EditEventPanel.tsx +14 -5
  54. package/src/components/panels/events/ManageEventsPanel.tsx +11 -8
  55. package/src/components/panels/events/styles.module.css +6 -64
  56. package/src/components/panels/graphProperties/index.tsx +125 -0
  57. package/src/components/panels/history/index.tsx +2 -2
  58. package/src/components/panels/history/styles.module.css +0 -9
  59. package/src/components/panels/keymaps/index.module.css +3 -13
  60. package/src/components/panels/keymaps/index.tsx +1 -2
  61. package/src/components/panels/layers/index.tsx +20 -15
  62. package/src/components/panels/layers/styles.module.css +9 -12
  63. package/src/components/panels/legend/index.tsx +1 -1
  64. package/src/components/panels/logs/index.module.css +25 -19
  65. package/src/components/panels/logs/index.tsx +7 -7
  66. package/src/components/panels/nodeInputs/InputsGroup.tsx +1 -0
  67. package/src/components/panels/nodeInputs/NodeSettings.tsx +2 -2
  68. package/src/components/panels/nodeInputs/NodeTitleEditor.tsx +1 -1
  69. package/src/components/panels/nodeInputs/OutputsGroup.tsx +2 -12
  70. package/src/components/panels/nodeInputs/index.module.css +99 -75
  71. package/src/components/panels/nodeInputs/index.tsx +21 -11
  72. package/src/components/panels/nodeInputs/useNodeHandlers.ts +2 -2
  73. package/src/components/panels/nodeInputs/useNodeInputsData.ts +23 -43
  74. package/src/components/panels/nodePicker/index.tsx +8 -8
  75. package/src/components/panels/panel/index.module.css +7 -7
  76. package/src/components/panels/search/index.module.css +0 -50
  77. package/src/components/panels/search/index.tsx +2 -2
  78. package/src/components/panels/systemSettings/ConversionsSettings.tsx +203 -0
  79. package/src/components/panels/systemSettings/index.tsx +221 -176
  80. package/src/components/panels/systemSettings/styles.module.css +135 -8
  81. package/src/components/panels/traces/GridLines.tsx +1 -1
  82. package/src/components/panels/traces/TimeGrid.tsx +3 -3
  83. package/src/components/panels/traces/TraceLane.tsx +1 -1
  84. package/src/components/panels/traces/index.module.css +1 -8
  85. package/src/components/panels/traces/index.tsx +8 -4
  86. package/src/components/panels/traces/useDerivedSpans.ts +241 -146
  87. package/src/components/panels/traces/utils.ts +8 -0
  88. package/src/components/panels/variables/CreateVariableScreen.tsx +3 -3
  89. package/src/components/panels/variables/ManageVariablesScreen.tsx +12 -9
  90. package/src/components/panels/variables/index.tsx +2 -2
  91. package/src/components/panels/variables/styles.module.css +4 -91
  92. package/src/components/primitives/icon.module.css +4 -4
  93. package/src/components/sockets/input/index.tsx +9 -2
  94. package/src/components/sockets/input/styles.module.css +2 -3
  95. package/src/components/sockets/output/index.tsx +10 -3
  96. package/src/components/sockets/output/styles.module.css +1 -6
  97. package/src/css/notes.css +135 -0
  98. package/src/css/prosemirror.css +3 -3
  99. package/src/css/rc-dock.css +143 -43
  100. package/src/css/rc-menu.css +56 -55
  101. package/src/css/themes/kiberon.css +127 -0
  102. package/src/css/vars.css +197 -13
  103. package/src/css/vscode-elements.css +124 -0
  104. package/src/generators/CallSubgraphGenerator.tsx +136 -0
  105. package/src/generators/CustomEventOnTriggeredGenerator.tsx +2 -2
  106. package/src/generators/GraphBoundaryGenerator.module.css +32 -0
  107. package/src/generators/GraphBoundaryGenerator.tsx +193 -0
  108. package/src/generators/SequenceGenerator.tsx +2 -2
  109. package/src/generators/SwitchOnIntegerGenerator.tsx +2 -2
  110. package/src/generators/SwitchOnStringGenerator.tsx +2 -2
  111. package/src/generators/callSubgraphSync.ts +126 -0
  112. package/src/generators/registerDefaultGenerators.ts +21 -0
  113. package/src/generators/registerDefaults.ts +26 -0
  114. package/src/hooks/useBehaveGraphFlow.ts +2 -2
  115. package/src/hooks/useFlowHandlers.ts +47 -9
  116. package/src/hooks/useWasdPan.ts +26 -4
  117. package/src/index.css +4 -16
  118. package/src/index.ts +17 -0
  119. package/src/manifest/contributionRegistry.ts +93 -0
  120. package/src/manifest/index.ts +4 -0
  121. package/src/manifest/loadManifest.ts +82 -0
  122. package/src/manifest/manifestPlugin.ts +29 -0
  123. package/src/manifest/passthroughValueType.ts +40 -0
  124. package/src/plugin/alignment/index.ts +22 -12
  125. package/src/plugin/autosave/controller.ts +366 -0
  126. package/src/plugin/autosave/index.tsx +114 -0
  127. package/src/plugin/autosave/panel/BackupPanel.tsx +141 -0
  128. package/src/plugin/autosave/panel/index.tsx +1 -0
  129. package/src/plugin/autosave/panel/styles.module.css +56 -0
  130. package/src/plugin/autosave/settings.ts +65 -0
  131. package/src/plugin/autosave/storage.ts +147 -0
  132. package/src/plugin/docs/index.tsx +2 -4
  133. package/src/plugin/docs/panel/DocumentationBrowserPanelImpl.tsx +200 -0
  134. package/src/plugin/docs/panel/index.tsx +15 -194
  135. package/src/plugin/docs/panel/styles.module.css +8 -8
  136. package/src/plugin/graphrunner/actions.ts +258 -185
  137. package/src/plugin/graphrunner/buttons.tsx +34 -26
  138. package/src/plugin/graphrunner/client.ts +4 -1
  139. package/src/plugin/graphrunner/index.tsx +29 -100
  140. package/src/plugin/graphrunner/panel.tsx +2 -2
  141. package/src/plugin/graphrunner/runController.ts +283 -0
  142. package/src/plugin/graphrunner/runner.ts +21 -192
  143. package/src/plugin/graphrunner/store.ts +14 -24
  144. package/src/plugin/graphrunner/styles.module.css +17 -57
  145. package/src/plugin/graphrunner/transport.ts +26 -0
  146. package/src/plugin/graphrunner/types.ts +21 -0
  147. package/src/plugin/graphrunner-local/execution-utils.ts +260 -80
  148. package/src/plugin/graphrunner-local/index.tsx +8 -2
  149. package/src/plugin/graphrunner-local/panel.tsx +131 -175
  150. package/src/plugin/graphrunner-local/styles.module.css +57 -76
  151. package/src/plugin/graphrunner-local/transport.ts +151 -184
  152. package/src/plugin/graphrunner-webworker/graph-executor.worker.ts +2 -0
  153. package/src/plugin/graphrunner-webworker/index.tsx +4 -10
  154. package/src/plugin/graphrunner-webworker/store.ts +9 -0
  155. package/src/plugin/kitchen-sink/index.ts +38 -0
  156. package/src/{layout/dagre.tsx → plugin/layout/dagre.ts} +17 -5
  157. package/src/{layout → plugin/layout}/elk.ts +22 -6
  158. package/src/plugin/layout/index.ts +80 -0
  159. package/src/plugin/notes/FormatToolbar.tsx +200 -0
  160. package/src/plugin/notes/index.tsx +191 -0
  161. package/src/plugin/notes/nodeActions.ts +100 -0
  162. package/src/plugin/notes/note.tsx +20 -0
  163. package/src/plugin/notes/noteImpl.tsx +89 -0
  164. package/src/plugin/realtime/realtimeRunner.ts +58 -4
  165. package/src/specifics/CustomEventOnTriggeredSpecific.tsx +2 -2
  166. package/src/specifics/CustomEventTriggerSpecific.tsx +2 -2
  167. package/src/specifics/VariableGetSpecific.tsx +2 -2
  168. package/src/specifics/VariableSetSpecific.tsx +2 -2
  169. package/src/store/actions.tsx +5 -5
  170. package/src/store/commands.ts +278 -0
  171. package/src/store/contextMenu.ts +192 -0
  172. package/src/store/conversions.ts +47 -0
  173. package/src/store/flow.tsx +23 -38
  174. package/src/store/graphMeta.ts +39 -0
  175. package/src/store/hotKeys.tsx +301 -260
  176. package/src/store/layers.ts +3 -3
  177. package/src/store/registry.ts +12 -4
  178. package/src/store/selection.ts +3 -3
  179. package/src/store/settings.ts +82 -82
  180. package/src/store/settingsSchema.ts +210 -0
  181. package/src/store/tabs.ts +5 -1
  182. package/src/store/traces.ts +3 -3
  183. package/src/system/graph.ts +11 -14
  184. package/src/system/graphSession.ts +172 -0
  185. package/src/system/index.ts +3 -0
  186. package/src/system/notifications.ts +13 -0
  187. package/src/system/persistence.ts +82 -0
  188. package/src/system/plugin.ts +28 -0
  189. package/src/system/provider.tsx +64 -0
  190. package/src/system/system.ts +518 -88
  191. package/src/system/tabLoader.tsx +70 -32
  192. package/src/system/undoRedo.ts +1 -1
  193. package/src/transformers/Uigraph.ts +5 -4
  194. package/src/transformers/contract.ts +87 -0
  195. package/src/transformers/flowToBehave.ts +13 -5
  196. package/src/types/nodes.ts +8 -3
  197. package/src/types.ts +2 -0
  198. package/src/util/autoConvert.ts +200 -0
  199. package/src/util/isValidConnection.ts +23 -2
  200. package/stories/defaults/defaultStoryProvider.tsx +17 -14
  201. package/stories/defaults/systemGenerator.ts +6 -1
  202. package/stories/{components/nodes/comment.stories.tsx → plugins/notes.stories.tsx} +24 -30
  203. package/tests/autoConvert.test.ts +329 -0
  204. package/tests/autosavePlugin.test.ts +204 -0
  205. package/tests/callSubgraphSync.test.ts +148 -0
  206. package/tests/commandRegistry.test.ts +137 -0
  207. package/tests/contract.test.ts +51 -0
  208. package/tests/contractSerialize.test.ts +62 -0
  209. package/tests/deriveSpans.test.ts +71 -0
  210. package/tests/flowToBehave.test.ts +2 -1
  211. package/tests/hotkeys.test.ts +79 -0
  212. package/tests/keepAliveLifecycle.test.ts +167 -0
  213. package/tests/loadManifest.test.ts +113 -0
  214. package/tests/noteMarkdown.test.ts +65 -0
  215. package/tests/notesPlugin.test.ts +162 -0
  216. package/tests/persistence.test.ts +51 -0
  217. package/tests/saveLoad.test.ts +7 -6
  218. package/tests/settings.test.ts +178 -0
  219. package/tests/traceStore.test.ts +46 -0
  220. package/tests/visual/README.md +2 -2
  221. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-conversation-chromium-win32.png +0 -0
  222. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-events-chromium-win32.png +0 -0
  223. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-history-chromium-win32.png +0 -0
  224. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-keymaps-chromium-win32.png +0 -0
  225. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-layers-chromium-win32.png +0 -0
  226. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-legend-chromium-win32.png +0 -0
  227. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-localGraphRunner-chromium-win32.png +0 -0
  228. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-logs-chromium-win32.png +0 -0
  229. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-nodeInputs-chromium-win32.png +0 -0
  230. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-nodePicker-chromium-win32.png +0 -0
  231. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-panel-chromium-win32.png +0 -0
  232. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-search-chromium-win32.png +0 -0
  233. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-systemSettings-chromium-win32.png +0 -0
  234. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-variables-chromium-win32.png +0 -0
  235. package/tests/visual/panels.visual.test.tsx +3 -3
  236. package/tests/wasdPan.test.ts +71 -0
  237. package/vitest.config.ts +1 -1
  238. package/vitest.visual.config.ts +7 -0
  239. package/.storybook/vscode.css +0 -814
  240. package/src/components/nodes/comment/FormatToolbar.tsx +0 -118
  241. package/src/components/nodes/comment/comment.tsx +0 -103
  242. package/src/components/nodes/comment/styles.module.css +0 -150
  243. package/src/components/panels/conversation/index.module.css +0 -151
  244. package/src/components/panels/conversation/index.tsx +0 -162
  245. package/src/components/panels/events/CustomEventsEditor.tsx +0 -384
  246. package/src/css/vscode.css +0 -13
  247. package/src/hooks/useDetachNodes.ts +0 -39
  248. package/src/plugin/graphrunner-webworker/types.ts +0 -17
  249. package/src/specifics/registerDefaultSpecifics.ts +0 -5
  250. package/src/store/chat.ts +0 -73
  251. package/src/store/graphRunnerClient.ts +0 -110
@@ -0,0 +1,148 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import type { Node } from 'reactflow';
3
+ import {
4
+ registerCoreProfile,
5
+ writeNodeSpecsToJSON
6
+ } from '@kiberon-labs/behave-graph';
7
+ import { System } from '../src/system/system.js';
8
+ import { setupCallSubgraphSync } from '../src/generators/callSubgraphSync.js';
9
+
10
+ const buildSystem = () => {
11
+ const registry = registerCoreProfile({
12
+ nodes: {},
13
+ values: {},
14
+ dependencies: {} as any
15
+ });
16
+ const specs = writeNodeSpecsToJSON(registry);
17
+ return new System({ values: registry.values, specs });
18
+ };
19
+
20
+ const outputBoundary = (
21
+ id: string,
22
+ params: Array<{ id?: string; name: string; valueTypeName: string }>
23
+ ): Node => ({
24
+ id,
25
+ type: 'behaveNode',
26
+ position: { x: 0, y: 0 },
27
+ data: {
28
+ type: 'graph/output',
29
+ configuration: { parameters: params },
30
+ ports: {}
31
+ }
32
+ });
33
+
34
+ const callNode = (id: string, subgraphId: string): Node => ({
35
+ id,
36
+ type: 'behaveNode',
37
+ position: { x: 0, y: 0 },
38
+ data: { type: 'flow/callSubgraph', configuration: { subgraphId }, ports: {} }
39
+ });
40
+
41
+ const getNode = (session: { nodeStore: any }, id: string): Node =>
42
+ session.nodeStore.getState().nodes.find((n: Node) => n.id === id);
43
+
44
+ describe('call subgraph contract sync', () => {
45
+ it('updates a call node when the referenced subgraph contract changes after creation', () => {
46
+ const system = buildSystem();
47
+ const caller = system.createSession('caller');
48
+ const dispose = setupCallSubgraphSync(system);
49
+
50
+ // `sub` is created AFTER the sync is registered , exercises the session
51
+ // extension applying to future graphs, not just existing ones.
52
+ const sub = system.createSession('sub');
53
+
54
+ // A call node referencing `sub`, created while `sub` has no outputs yet.
55
+ caller.nodeStore.getState().setNodes(() => [callNode('call1', sub.id)]);
56
+ expect(getNode(caller, 'call1').data.configuration.outputs ?? []).toEqual(
57
+ []
58
+ );
59
+
60
+ // Now author the subgraph's output contract.
61
+ sub.nodeStore
62
+ .getState()
63
+ .setNodes(() => [
64
+ outputBoundary('out', [
65
+ { id: 'o1', name: 'result', valueTypeName: 'float' }
66
+ ])
67
+ ]);
68
+
69
+ // The call node should have picked up the new contract reactively , no
70
+ // re-selection required.
71
+ const synced = getNode(caller, 'call1');
72
+ expect(synced.data.configuration.outputs).toEqual([
73
+ { id: 'o1', name: 'result', valueTypeName: 'float' }
74
+ ]);
75
+ expect(synced.data.dynamicPorts.outputs).toEqual([
76
+ { name: 'o1', key: 'o1', label: 'result', valueType: 'float' }
77
+ ]);
78
+
79
+ // Rename the param: the call node tracks the change (stable id, new label).
80
+ sub.nodeStore
81
+ .getState()
82
+ .setNodes(() => [
83
+ outputBoundary('out', [
84
+ { id: 'o1', name: 'renamed', valueTypeName: 'float' }
85
+ ])
86
+ ]);
87
+ expect(getNode(caller, 'call1').data.configuration.outputs).toEqual([
88
+ { id: 'o1', name: 'renamed', valueTypeName: 'float' }
89
+ ]);
90
+
91
+ dispose();
92
+ });
93
+
94
+ it('does not rewrite call nodes on non-contract edits (e.g. dragging)', () => {
95
+ const system = buildSystem();
96
+ const caller = system.createSession('caller');
97
+ const sub = system.createSession('sub');
98
+ const dispose = setupCallSubgraphSync(system);
99
+
100
+ sub.nodeStore
101
+ .getState()
102
+ .setNodes(() => [
103
+ outputBoundary('out', [
104
+ { id: 'o1', name: 'result', valueTypeName: 'float' }
105
+ ])
106
+ ]);
107
+ caller.nodeStore.getState().setNodes(() => [callNode('call1', sub.id)]);
108
+
109
+ const before = getNode(caller, 'call1');
110
+ // A position-only change to the subgraph must not churn the call node.
111
+ sub.nodeStore
112
+ .getState()
113
+ .setNodes((prev: Node[]) =>
114
+ prev.map((n) =>
115
+ n.id === 'out' ? { ...n, position: { x: 50, y: 50 } } : n
116
+ )
117
+ );
118
+ const after = getNode(caller, 'call1');
119
+
120
+ // Same object identity ⇒ the call node was not rewritten.
121
+ expect(after).toBe(before);
122
+
123
+ dispose();
124
+ });
125
+
126
+ it('stops syncing after dispose', () => {
127
+ const system = buildSystem();
128
+ const caller = system.createSession('caller');
129
+ const sub = system.createSession('sub');
130
+ const dispose = setupCallSubgraphSync(system);
131
+
132
+ caller.nodeStore.getState().setNodes(() => [callNode('call1', sub.id)]);
133
+ dispose();
134
+
135
+ sub.nodeStore
136
+ .getState()
137
+ .setNodes(() => [
138
+ outputBoundary('out', [
139
+ { id: 'o1', name: 'result', valueTypeName: 'float' }
140
+ ])
141
+ ]);
142
+
143
+ // No sync after dispose ⇒ contract not propagated.
144
+ expect(getNode(caller, 'call1').data.configuration.outputs ?? []).toEqual(
145
+ []
146
+ );
147
+ });
148
+ });
@@ -0,0 +1,137 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import type { Node } from 'reactflow';
3
+ import {
4
+ registerCoreProfile,
5
+ writeNodeSpecsToJSON
6
+ } from '@kiberon-labs/behave-graph';
7
+ import { System } from '../src/system/system.js';
8
+ import {
9
+ commandStoreFactory,
10
+ type CommandContext
11
+ } from '../src/store/commands.js';
12
+ import {
13
+ contextMenuStoreFactory,
14
+ type ContextMenuItem
15
+ } from '../src/store/contextMenu.js';
16
+
17
+ const buildSystem = () => {
18
+ const reg = registerCoreProfile({
19
+ nodes: {},
20
+ values: {},
21
+ dependencies: {} as any
22
+ });
23
+ return new System({ values: reg.values, specs: writeNodeSpecsToJSON(reg) });
24
+ };
25
+
26
+ const node = (id: string): Node => ({
27
+ id,
28
+ type: 'behaveNode',
29
+ position: { x: 0, y: 0 },
30
+ data: { type: 'debug/log', configuration: {}, ports: {} } as any
31
+ });
32
+
33
+ // The command/context-menu registries are framework-free; a stub context is fine.
34
+ const ctx = {} as CommandContext;
35
+
36
+ describe('command registry', () => {
37
+ it('registers, runs, and unregisters by id', () => {
38
+ const store = commandStoreFactory().getState();
39
+ let ran = 0;
40
+ const off = store.register({ id: 'x.do', run: () => void ran++ });
41
+
42
+ expect(store.get('x.do')?.id).toBe('x.do');
43
+ store.run('x.do', ctx);
44
+ expect(ran).toBe(1);
45
+
46
+ off();
47
+ expect(store.get('x.do')).toBeUndefined();
48
+ store.run('x.do', ctx); // no-op, no throw
49
+ expect(ran).toBe(1);
50
+ });
51
+
52
+ it('register replaces an existing id (idempotent override)', () => {
53
+ const store = commandStoreFactory().getState();
54
+ store.register({ id: 'x', run: () => {} });
55
+ store.register({ id: 'x', title: 'Second', run: () => {} });
56
+ expect(store.list()).toHaveLength(1);
57
+ expect(store.get('x')?.title).toBe('Second');
58
+ });
59
+
60
+ it('skips a disabled command', () => {
61
+ const store = commandStoreFactory().getState();
62
+ let ran = false;
63
+ store.register({
64
+ id: 'x',
65
+ isEnabled: () => false,
66
+ run: () => {
67
+ ran = true;
68
+ }
69
+ });
70
+ store.run('x', ctx);
71
+ expect(ran).toBe(false);
72
+ });
73
+ });
74
+
75
+ describe('context-menu registry', () => {
76
+ const item = (over: Partial<ContextMenuItem>): ContextMenuItem => ({
77
+ id: 'i',
78
+ target: 'node',
79
+ label: 'L',
80
+ ...over
81
+ });
82
+
83
+ it('returns items for a target sorted by order', () => {
84
+ const store = contextMenuStoreFactory().getState();
85
+ store.register(item({ id: 'b', order: 20 }));
86
+ store.register(item({ id: 'a', order: 10 }));
87
+ store.register(item({ id: 'e', target: 'edge', order: 5 }));
88
+
89
+ const nodeIds = store.getItems('node').map((i) => i.id);
90
+ expect(nodeIds).toEqual(['a', 'b']); // edge item excluded, sorted
91
+ });
92
+
93
+ it('register replaces by id; unregister removes', () => {
94
+ const store = contextMenuStoreFactory().getState();
95
+ store.register(item({ id: 'a', label: 'One' }));
96
+ store.register(item({ id: 'a', label: 'Two' }));
97
+ expect(store.getItems('node')).toHaveLength(1);
98
+ expect(store.getItems('node')[0]!.label).toBe('Two');
99
+
100
+ store.unregister('a');
101
+ expect(store.getItems('node')).toHaveLength(0);
102
+ });
103
+ });
104
+
105
+ describe('System.runCommand (hotkeys/menubar dispatch path)', () => {
106
+ it('dispatches a default command against the focused session', () => {
107
+ const system = buildSystem();
108
+ const session = system.createSession('g'); // activates by default
109
+ session.nodeStore.getState().setNodes(() => [node('n1'), node('n2')]);
110
+
111
+ system.runCommand('selection.selectAll');
112
+
113
+ expect(session.nodeStore.getState().nodes.every((n) => n.selected)).toBe(
114
+ true
115
+ );
116
+ });
117
+
118
+ it('no-ops when there is no focused graph', () => {
119
+ const system = buildSystem();
120
+ expect(() => system.runCommand('selection.selectAll')).not.toThrow();
121
+ });
122
+
123
+ it('threads target context (e.g. nodeId) into the command', () => {
124
+ const system = buildSystem();
125
+ system.createSession('g');
126
+ let captured: string | undefined;
127
+ system.commandStore.getState().register({
128
+ id: 'probe',
129
+ run: (ctx) => {
130
+ captured = ctx.nodeId;
131
+ }
132
+ });
133
+
134
+ system.runCommand('probe', { nodeId: 'abc' });
135
+ expect(captured).toBe('abc');
136
+ });
137
+ });
@@ -0,0 +1,51 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import type { Node } from 'reactflow';
3
+ import { deriveContract } from '../src/transformers/contract.js';
4
+
5
+ const boundaryNode = (
6
+ id: string,
7
+ type: 'graph/input' | 'graph/output',
8
+ parameters: Array<{ name: string; valueTypeName: string; defaultValue?: any }>
9
+ ): Node => ({
10
+ id,
11
+ type: 'behaveNode',
12
+ position: { x: 0, y: 0 },
13
+ data: { type, configuration: { parameters }, ports: {} }
14
+ });
15
+
16
+ describe('graph contract derivation', () => {
17
+ it('derives graphInputs/graphOutputs from boundary nodes', () => {
18
+ const nodes: Node[] = [
19
+ boundaryNode('in', 'graph/input', [
20
+ { name: 'x', valueTypeName: 'float', defaultValue: 1 },
21
+ { name: 'label', valueTypeName: 'string' }
22
+ ]),
23
+ boundaryNode('out', 'graph/output', [
24
+ { name: 'y', valueTypeName: 'float' }
25
+ ]),
26
+ // a non-boundary node should be ignored
27
+ {
28
+ id: 'n',
29
+ type: 'behaveNode',
30
+ position: { x: 0, y: 0 },
31
+ data: { type: 'debug/log', configuration: {}, ports: {} }
32
+ }
33
+ ];
34
+
35
+ const { graphInputs, graphOutputs } = deriveContract(nodes);
36
+
37
+ expect(graphInputs).toEqual([
38
+ { key: 'x', valueType: 'float', defaultValue: 1, label: 'x' },
39
+ { key: 'label', valueType: 'string', label: 'label' }
40
+ ]);
41
+ expect(graphOutputs).toEqual([
42
+ { key: 'y', valueType: 'float', label: 'y' }
43
+ ]);
44
+ });
45
+
46
+ it('returns empty contract when there are no boundary nodes', () => {
47
+ const { graphInputs, graphOutputs } = deriveContract([]);
48
+ expect(graphInputs).toEqual([]);
49
+ expect(graphOutputs).toEqual([]);
50
+ });
51
+ });
@@ -0,0 +1,62 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import type { Node } from 'reactflow';
3
+ import {
4
+ registerCoreProfile,
5
+ writeNodeSpecsToJSON
6
+ } from '@kiberon-labs/behave-graph';
7
+ import { System } from '../src/system/system.js';
8
+ import { flowToBehave } from '../src/transformers/flowToBehave.js';
9
+
10
+ const boundary = (
11
+ id: string,
12
+ type: 'graph/input' | 'graph/output',
13
+ params: Array<{ name: string; valueTypeName: string }>
14
+ ): Node => ({
15
+ id,
16
+ type: 'behaveNode',
17
+ position: { x: 0, y: 0 },
18
+ data: { type, configuration: { parameters: params }, ports: {} }
19
+ });
20
+
21
+ describe('contract serialization through flowToBehave', () => {
22
+ it('emits graphInputs/graphOutputs derived from boundary nodes', () => {
23
+ const coreRegistry = registerCoreProfile({
24
+ nodes: {},
25
+ values: {},
26
+ dependencies: {} as any
27
+ });
28
+ const specs = writeNodeSpecsToJSON(coreRegistry);
29
+ const system = new System({ values: coreRegistry.values, specs });
30
+ const session = system.createSession('graph');
31
+
32
+ const nodes: Node[] = [
33
+ boundary('in', 'graph/input', [{ name: 'x', valueTypeName: 'float' }]),
34
+ boundary('out', 'graph/output', [{ name: 'y', valueTypeName: 'float' }])
35
+ ];
36
+
37
+ const graph = flowToBehave(session, nodes, [], specs);
38
+
39
+ expect(graph.graphInputs).toEqual([
40
+ { key: 'x', valueType: 'float', label: 'x' }
41
+ ]);
42
+ expect(graph.graphOutputs).toEqual([
43
+ { key: 'y', valueType: 'float', label: 'y' }
44
+ ]);
45
+ });
46
+
47
+ it('omits the contract when there are no boundary nodes', () => {
48
+ const coreRegistry = registerCoreProfile({
49
+ nodes: {},
50
+ values: {},
51
+ dependencies: {} as any
52
+ });
53
+ const specs = writeNodeSpecsToJSON(coreRegistry);
54
+ const system = new System({ values: coreRegistry.values, specs });
55
+ const session = system.createSession('graph');
56
+
57
+ const graph = flowToBehave(session, [], [], specs);
58
+
59
+ expect(graph.graphInputs).toBeUndefined();
60
+ expect(graph.graphOutputs).toBeUndefined();
61
+ });
62
+ });
@@ -0,0 +1,71 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import type { SpanCollector, TraceSpan } from '../src/store/traces.js';
3
+ import { computeDerivedSpans } from '../src/components/panels/traces/useDerivedSpans.js';
4
+ import type { ViewState } from '../src/components/panels/traces/types.js';
5
+
6
+ const FOLLOW: ViewState = { start: 0, range: 5000, follow: true };
7
+
8
+ /** Minimal collector holding the given spans (lane defaults to 0). */
9
+ const collectorOf = (spans: TraceSpan[]): SpanCollector => ({
10
+ capacity: 16,
11
+ spans,
12
+ writeIndex: spans.length,
13
+ size: spans.length,
14
+ nextId: spans.length + 1,
15
+ openByNodeId: new Map(),
16
+ laneOpen: []
17
+ });
18
+
19
+ const span = (over: Partial<TraceSpan>): TraceSpan => ({
20
+ id: 1,
21
+ nodeId: 'n',
22
+ name: 'n',
23
+ start: 0,
24
+ end: 0,
25
+ lane: 0,
26
+ ...over
27
+ });
28
+
29
+ describe('computeDerivedSpans , instant span visibility', () => {
30
+ it('renders an instant (start === end) span instead of collapsing to nothing', () => {
31
+ // A node that executes in 0ms: start === end. Previously maxEnd <= minStart
32
+ // made the derivation bail and render no lanes.
33
+ const derived = computeDerivedSpans(
34
+ collectorOf([span({ start: 0, end: 0 })]),
35
+ 5000,
36
+ FOLLOW
37
+ );
38
+
39
+ expect(derived.size).toBe(1);
40
+ expect(derived.laneData).toHaveLength(1);
41
+ const visuals = derived.laneData[0]!.visualSpans;
42
+ expect(visuals).toHaveLength(1);
43
+ // It has real, clickable width…
44
+ expect(visuals[0]!.widthPct).toBeGreaterThan(0);
45
+ // …but its reported duration is still the true 0ms.
46
+ expect(visuals[0]!.durationMs).toBe(0);
47
+ });
48
+
49
+ it('reports the true duration for a normal span and keeps it visible', () => {
50
+ const derived = computeDerivedSpans(
51
+ collectorOf([span({ start: 2, end: 7 })]),
52
+ 5000,
53
+ FOLLOW
54
+ );
55
+ const v = derived.laneData[0]!.visualSpans[0]!;
56
+ expect(v.durationMs).toBe(5);
57
+ expect(v.widthPct).toBeGreaterThan(0);
58
+ });
59
+
60
+ it('labels ticks relative to the first span, not the raw clock', () => {
61
+ // Start at a large absolute time (as performance.now() produces); the first
62
+ // tick label should be relative (~0), not the absolute value.
63
+ const derived = computeDerivedSpans(
64
+ collectorOf([span({ start: 1_000_000, end: 1_000_010 })]),
65
+ 0, // window 0 ⇒ fit all
66
+ FOLLOW
67
+ );
68
+ expect(derived.ticks.length).toBeGreaterThan(0);
69
+ expect(derived.ticks[0]!.time).toBeLessThan(1000);
70
+ });
71
+ });
@@ -26,7 +26,8 @@ it('transforms from flow to behave', () => {
26
26
  specs: specJSON
27
27
  };
28
28
  const system = new System(registry);
29
- const output = flowToBehave(system, nodes, edges, specJSON);
29
+ const session = system.createSession('graph');
30
+ const output = flowToBehave(session, nodes, edges, specJSON);
30
31
 
31
32
  // Remove position metadata from expected graph since we no longer include it
32
33
  const expectedGraph = {
@@ -0,0 +1,79 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ registerCoreProfile,
4
+ writeNodeSpecsToJSON
5
+ } from '@kiberon-labs/behave-graph';
6
+ import { System } from '../src/system/system.js';
7
+ import { formatTrigger } from '../src/store/hotKeys.js';
8
+
9
+ const buildSystem = () => {
10
+ const reg = registerCoreProfile({
11
+ nodes: {},
12
+ values: {},
13
+ dependencies: {} as any
14
+ });
15
+ return new System({ values: reg.values, specs: writeNodeSpecsToJSON(reg) });
16
+ };
17
+
18
+ describe('formatTrigger', () => {
19
+ it('capitalizes modifiers and single keys', () => {
20
+ expect(formatTrigger('ctrl+s')).toBe('Ctrl+S');
21
+ expect(formatTrigger('shift+alt+f')).toBe('Shift+Alt+F');
22
+ });
23
+
24
+ it('maps arrow keys to glyphs', () => {
25
+ expect(formatTrigger('ctrl+shift+left')).toBe('Ctrl+Shift+←');
26
+ expect(formatTrigger('ctrl+shift+right')).toBe('Ctrl+Shift+→');
27
+ });
28
+
29
+ it('prefers the ctrl variant when several triggers are bound', () => {
30
+ expect(formatTrigger(['command+c', 'ctrl+c'])).toBe('Ctrl+C');
31
+ });
32
+
33
+ it('returns undefined for empty/unset triggers', () => {
34
+ expect(formatTrigger(undefined)).toBeUndefined();
35
+ expect(formatTrigger('')).toBeUndefined();
36
+ expect(formatTrigger([])).toBeUndefined();
37
+ });
38
+ });
39
+
40
+ describe('getCommandKeybinding', () => {
41
+ it('auto-detects the shortcut for a command-backed binding', () => {
42
+ const sys = buildSystem();
43
+ const store = sys.hotKeyStore.getState();
44
+ expect(store.getCommandKeybinding('selection.copy')).toBe('Ctrl+C');
45
+ expect(store.getCommandKeybinding('editor.save')).toBe('Ctrl+S');
46
+ });
47
+
48
+ it('resolves handler-only bindings via hintCommand', () => {
49
+ const sys = buildSystem();
50
+ const store = sys.hotKeyStore.getState();
51
+ expect(store.getCommandKeybinding('node.traceUpstream')).toBe(
52
+ 'Ctrl+Shift+←'
53
+ );
54
+ });
55
+
56
+ it('returns undefined for a command with no bound key', () => {
57
+ const sys = buildSystem();
58
+ const store = sys.hotKeyStore.getState();
59
+ expect(store.getCommandKeybinding('node.focus')).toBeUndefined();
60
+ expect(store.getCommandKeybinding('does.not.exist')).toBeUndefined();
61
+ });
62
+
63
+ it('reflects a rebind live, and picks up runtime-registered commands', () => {
64
+ const sys = buildSystem();
65
+ const store = sys.hotKeyStore.getState();
66
+
67
+ // Rebinding the action updates the derived hint.
68
+ store.register({ action: 'SAVE', trigger: 'ctrl+k' });
69
+ expect(sys.hotKeyStore.getState().getCommandKeybinding('editor.save')).toBe(
70
+ 'Ctrl+K'
71
+ );
72
+
73
+ // A runtime binding that names its command becomes resolvable.
74
+ store.register({ action: 'RUN', trigger: 'p', command: 'graph.run' });
75
+ expect(sys.hotKeyStore.getState().getCommandKeybinding('graph.run')).toBe(
76
+ 'P'
77
+ );
78
+ });
79
+ });