@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,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,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
+ });
@@ -24,6 +24,7 @@ describe('Save and Load Graph', () => {
24
24
  };
25
25
 
26
26
  system = new System(registry);
27
+ system.createSession('graph');
27
28
  });
28
29
 
29
30
  it('should save and load a graph with node positions', () => {
@@ -67,7 +68,7 @@ describe('Save and Load Graph', () => {
67
68
  system.edgeStore.getState().setEdges(initialEdges);
68
69
 
69
70
  // Save the graph
70
- const savedGraph = buildUIGraphJSON(system);
71
+ const savedGraph = buildUIGraphJSON(system.session!);
71
72
 
72
73
  // Verify saved graph has nodes with positions
73
74
  expect(savedGraph.nodes).toHaveLength(2);
@@ -115,7 +116,7 @@ describe('Save and Load Graph', () => {
115
116
  system.nodeStore.getState().setNodes(initialNodes);
116
117
 
117
118
  // Save the graph
118
- const savedGraph = buildUIGraphJSON(system);
119
+ const savedGraph = buildUIGraphJSON(system.session!);
119
120
 
120
121
  // Verify viewport is saved
121
122
  expect(savedGraph.user?.viewport).toEqual({ x: 50, y: 100, zoom: 1.5 });
@@ -151,7 +152,7 @@ describe('Save and Load Graph', () => {
151
152
  system.nodeStore.getState().setNodes(nodes);
152
153
 
153
154
  // Save the graph
154
- const savedGraph = buildUIGraphJSON(system);
155
+ const savedGraph = buildUIGraphJSON(system.session!);
155
156
 
156
157
  // Verify variables are in the flow
157
158
  expect(savedGraph.flow.variables).toBeDefined();
@@ -200,7 +201,7 @@ describe('Save and Load Graph', () => {
200
201
  system.nodeStore.getState().setNodes(nodes);
201
202
 
202
203
  // Save the graph
203
- const savedGraph = buildUIGraphJSON(system);
204
+ const savedGraph = buildUIGraphJSON(system.session!);
204
205
 
205
206
  // Verify custom events are in the flow
206
207
  expect(savedGraph.flow.customEvents).toEqual(customEvents);
@@ -236,7 +237,7 @@ describe('Save and Load Graph', () => {
236
237
  system.nodeStore.getState().setNodes(nodesWithPositions);
237
238
 
238
239
  // Save the graph
239
- const savedGraph = buildUIGraphJSON(system);
240
+ const savedGraph = buildUIGraphJSON(system.session!);
240
241
 
241
242
  // Modify the node position
242
243
  system.nodeStore.getState().setNodes([
@@ -340,7 +341,7 @@ describe('Save and Load Graph', () => {
340
341
  system.variableStore.getState().setVariables(variables);
341
342
 
342
343
  // Save
343
- const saved = buildUIGraphJSON(system);
344
+ const saved = buildUIGraphJSON(system.session!);
344
345
 
345
346
  // Clear
346
347
  system.nodeStore.getState().setNodes([]);