@foresthubai/workflow-core 0.4.3 → 0.4.5

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 (298) hide show
  1. package/LICENSE +202 -202
  2. package/NOTICE +14 -14
  3. package/README.md +63 -63
  4. package/dist/api/deployment.d.ts +74 -0
  5. package/dist/api/deployment.d.ts.map +1 -0
  6. package/dist/api/deployment.js +6 -0
  7. package/dist/api/deployment.js.map +1 -0
  8. package/dist/api/engine.d.ts +744 -0
  9. package/dist/api/engine.d.ts.map +1 -0
  10. package/dist/api/engine.js +6 -0
  11. package/dist/api/engine.js.map +1 -0
  12. package/dist/api/index.d.ts +4 -0
  13. package/dist/api/index.d.ts.map +1 -1
  14. package/dist/api/index.js +2 -0
  15. package/dist/api/index.js.map +1 -1
  16. package/dist/api/workflow.d.ts +25 -9
  17. package/dist/api/workflow.d.ts.map +1 -1
  18. package/dist/channel/Channel.d.ts +1 -1
  19. package/dist/channel/Channel.d.ts.map +1 -1
  20. package/dist/channel/Channel.js +3 -1
  21. package/dist/channel/Channel.js.map +1 -1
  22. package/dist/channel/ChannelDefinition.d.ts.map +1 -1
  23. package/dist/channel/ChannelDefinition.js +24 -0
  24. package/dist/channel/ChannelDefinition.js.map +1 -1
  25. package/dist/channel/index.d.ts.map +1 -1
  26. package/dist/channel/index.js +2 -0
  27. package/dist/channel/index.js.map +1 -1
  28. package/dist/channel/serialization.d.ts.map +1 -1
  29. package/dist/channel/serialization.js +8 -0
  30. package/dist/channel/serialization.js.map +1 -1
  31. package/dist/deploy/index.d.ts +5 -1
  32. package/dist/deploy/index.d.ts.map +1 -1
  33. package/dist/deploy/index.js +4 -1
  34. package/dist/deploy/index.js.map +1 -1
  35. package/dist/deploy/inputs.d.ts +26 -0
  36. package/dist/deploy/inputs.d.ts.map +1 -0
  37. package/dist/deploy/inputs.js +4 -0
  38. package/dist/deploy/inputs.js.map +1 -0
  39. package/dist/deploy/requirements.d.ts +24 -0
  40. package/dist/deploy/requirements.d.ts.map +1 -1
  41. package/dist/deploy/requirements.js +63 -0
  42. package/dist/deploy/requirements.js.map +1 -1
  43. package/dist/deploy/spec.d.ts +27 -0
  44. package/dist/deploy/spec.d.ts.map +1 -0
  45. package/dist/deploy/spec.js +352 -0
  46. package/dist/deploy/spec.js.map +1 -0
  47. package/dist/diagnostics/__fixtures__/diagnosticFixtures.d.ts.map +1 -1
  48. package/dist/diagnostics/__fixtures__/diagnosticFixtures.js +2 -0
  49. package/dist/diagnostics/__fixtures__/diagnosticFixtures.js.map +1 -1
  50. package/dist/diagnostics/diagnostics.d.ts.map +1 -1
  51. package/dist/diagnostics/diagnostics.js +2 -0
  52. package/dist/diagnostics/diagnostics.js.map +1 -1
  53. package/dist/diagnostics/index.d.ts.map +1 -1
  54. package/dist/diagnostics/index.js +2 -0
  55. package/dist/diagnostics/index.js.map +1 -1
  56. package/dist/edge/Edge.d.ts.map +1 -1
  57. package/dist/edge/Edge.js +2 -0
  58. package/dist/edge/Edge.js.map +1 -1
  59. package/dist/edge/EdgeDefinition.d.ts.map +1 -1
  60. package/dist/edge/EdgeDefinition.js +2 -0
  61. package/dist/edge/EdgeDefinition.js.map +1 -1
  62. package/dist/edge/EdgeType.d.ts.map +1 -1
  63. package/dist/edge/EdgeType.js +2 -0
  64. package/dist/edge/EdgeType.js.map +1 -1
  65. package/dist/edge/index.d.ts.map +1 -1
  66. package/dist/edge/index.js +2 -2
  67. package/dist/edge/index.js.map +1 -1
  68. package/dist/edge/serialization.d.ts.map +1 -1
  69. package/dist/edge/serialization.js +2 -0
  70. package/dist/edge/serialization.js.map +1 -1
  71. package/dist/expression/index.d.ts.map +1 -1
  72. package/dist/expression/index.js +2 -0
  73. package/dist/expression/index.js.map +1 -1
  74. package/dist/expression/parser.d.ts.map +1 -1
  75. package/dist/expression/parser.js +2 -0
  76. package/dist/expression/parser.js.map +1 -1
  77. package/dist/expression/types.d.ts.map +1 -1
  78. package/dist/expression/types.js +2 -0
  79. package/dist/expression/types.js.map +1 -1
  80. package/dist/function/FunctionDeclaration.d.ts.map +1 -1
  81. package/dist/function/FunctionDeclaration.js +2 -0
  82. package/dist/function/FunctionDeclaration.js.map +1 -1
  83. package/dist/function/index.d.ts.map +1 -1
  84. package/dist/function/index.js +2 -0
  85. package/dist/function/index.js.map +1 -1
  86. package/dist/function/serialization.d.ts.map +1 -1
  87. package/dist/function/serialization.js +2 -0
  88. package/dist/function/serialization.js.map +1 -1
  89. package/dist/id/index.d.ts.map +1 -1
  90. package/dist/id/index.js +2 -0
  91. package/dist/id/index.js.map +1 -1
  92. package/dist/index.d.ts.map +1 -1
  93. package/dist/index.js +2 -0
  94. package/dist/index.js.map +1 -1
  95. package/dist/memory/Memory.d.ts.map +1 -1
  96. package/dist/memory/Memory.js +2 -0
  97. package/dist/memory/Memory.js.map +1 -1
  98. package/dist/memory/MemoryDefinition.d.ts.map +1 -1
  99. package/dist/memory/MemoryDefinition.js +2 -0
  100. package/dist/memory/MemoryDefinition.js.map +1 -1
  101. package/dist/memory/MemoryFileDefinition.d.ts.map +1 -1
  102. package/dist/memory/MemoryFileDefinition.js +2 -0
  103. package/dist/memory/MemoryFileDefinition.js.map +1 -1
  104. package/dist/memory/MemoryRegistry.d.ts.map +1 -1
  105. package/dist/memory/MemoryRegistry.js +2 -0
  106. package/dist/memory/MemoryRegistry.js.map +1 -1
  107. package/dist/memory/VectorDatabaseDefinition.d.ts.map +1 -1
  108. package/dist/memory/VectorDatabaseDefinition.js +2 -0
  109. package/dist/memory/VectorDatabaseDefinition.js.map +1 -1
  110. package/dist/memory/index.d.ts.map +1 -1
  111. package/dist/memory/index.js +2 -0
  112. package/dist/memory/index.js.map +1 -1
  113. package/dist/memory/serialization.d.ts.map +1 -1
  114. package/dist/memory/serialization.js +2 -0
  115. package/dist/memory/serialization.js.map +1 -1
  116. package/dist/migration/index.d.ts.map +1 -1
  117. package/dist/migration/index.js +2 -0
  118. package/dist/migration/index.js.map +1 -1
  119. package/dist/migration/migrate.d.ts.map +1 -1
  120. package/dist/migration/migrate.js +2 -0
  121. package/dist/migration/migrate.js.map +1 -1
  122. package/dist/migration/migrations.d.ts.map +1 -1
  123. package/dist/migration/migrations.js +2 -0
  124. package/dist/migration/migrations.js.map +1 -1
  125. package/dist/migration/version.d.ts.map +1 -1
  126. package/dist/migration/version.js +2 -0
  127. package/dist/migration/version.js.map +1 -1
  128. package/dist/model/LLMModelDefinition.d.ts.map +1 -1
  129. package/dist/model/LLMModelDefinition.js +2 -0
  130. package/dist/model/LLMModelDefinition.js.map +1 -1
  131. package/dist/model/Model.d.ts.map +1 -1
  132. package/dist/model/Model.js +2 -13
  133. package/dist/model/Model.js.map +1 -1
  134. package/dist/model/ModelDefinition.d.ts.map +1 -1
  135. package/dist/model/ModelDefinition.js +2 -0
  136. package/dist/model/ModelDefinition.js.map +1 -1
  137. package/dist/model/ModelRegistry.d.ts.map +1 -1
  138. package/dist/model/ModelRegistry.js +2 -0
  139. package/dist/model/ModelRegistry.js.map +1 -1
  140. package/dist/model/index.d.ts.map +1 -1
  141. package/dist/model/index.js +2 -0
  142. package/dist/model/index.js.map +1 -1
  143. package/dist/model/serialization.d.ts.map +1 -1
  144. package/dist/model/serialization.js +2 -0
  145. package/dist/model/serialization.js.map +1 -1
  146. package/dist/node/AgentNode.d.ts.map +1 -1
  147. package/dist/node/AgentNode.js +2 -0
  148. package/dist/node/AgentNode.js.map +1 -1
  149. package/dist/node/DataNode.d.ts.map +1 -1
  150. package/dist/node/DataNode.js +2 -0
  151. package/dist/node/DataNode.js.map +1 -1
  152. package/dist/node/FunctionNode.d.ts.map +1 -1
  153. package/dist/node/FunctionNode.js +2 -0
  154. package/dist/node/FunctionNode.js.map +1 -1
  155. package/dist/node/InputNode.d.ts.map +1 -1
  156. package/dist/node/InputNode.js +2 -0
  157. package/dist/node/InputNode.js.map +1 -1
  158. package/dist/node/LogicNode.d.ts.map +1 -1
  159. package/dist/node/LogicNode.js +2 -0
  160. package/dist/node/LogicNode.js.map +1 -1
  161. package/dist/node/MqttNode.d.ts.map +1 -1
  162. package/dist/node/MqttNode.js +2 -0
  163. package/dist/node/MqttNode.js.map +1 -1
  164. package/dist/node/Node.d.ts.map +1 -1
  165. package/dist/node/Node.js +2 -7
  166. package/dist/node/Node.js.map +1 -1
  167. package/dist/node/NodeDefinition.d.ts.map +1 -1
  168. package/dist/node/NodeDefinition.js +2 -0
  169. package/dist/node/NodeDefinition.js.map +1 -1
  170. package/dist/node/NodeRegistry.d.ts.map +1 -1
  171. package/dist/node/NodeRegistry.js +2 -0
  172. package/dist/node/NodeRegistry.js.map +1 -1
  173. package/dist/node/OutputNode.d.ts.map +1 -1
  174. package/dist/node/OutputNode.js +4 -2
  175. package/dist/node/OutputNode.js.map +1 -1
  176. package/dist/node/ToolNode.d.ts.map +1 -1
  177. package/dist/node/ToolNode.js +2 -0
  178. package/dist/node/ToolNode.js.map +1 -1
  179. package/dist/node/TriggerNode.d.ts.map +1 -1
  180. package/dist/node/TriggerNode.js +2 -0
  181. package/dist/node/TriggerNode.js.map +1 -1
  182. package/dist/node/constants.d.ts.map +1 -1
  183. package/dist/node/constants.js +2 -0
  184. package/dist/node/constants.js.map +1 -1
  185. package/dist/node/index.d.ts.map +1 -1
  186. package/dist/node/index.js +2 -0
  187. package/dist/node/index.js.map +1 -1
  188. package/dist/node/methods.d.ts.map +1 -1
  189. package/dist/node/methods.js +2 -0
  190. package/dist/node/methods.js.map +1 -1
  191. package/dist/node/serialization.d.ts.map +1 -1
  192. package/dist/node/serialization.js +33 -13
  193. package/dist/node/serialization.js.map +1 -1
  194. package/dist/parameter/OutputParameter.d.ts.map +1 -1
  195. package/dist/parameter/OutputParameter.js +2 -0
  196. package/dist/parameter/OutputParameter.js.map +1 -1
  197. package/dist/parameter/Parameter.d.ts.map +1 -1
  198. package/dist/parameter/Parameter.js +2 -0
  199. package/dist/parameter/Parameter.js.map +1 -1
  200. package/dist/parameter/index.d.ts.map +1 -1
  201. package/dist/parameter/index.js +2 -0
  202. package/dist/parameter/index.js.map +1 -1
  203. package/dist/variable/Variable.d.ts.map +1 -1
  204. package/dist/variable/Variable.js +2 -0
  205. package/dist/variable/Variable.js.map +1 -1
  206. package/dist/variable/index.d.ts.map +1 -1
  207. package/dist/variable/index.js +2 -3
  208. package/dist/variable/index.js.map +1 -1
  209. package/dist/variable/operations.d.ts.map +1 -1
  210. package/dist/variable/operations.js +2 -0
  211. package/dist/variable/operations.js.map +1 -1
  212. package/dist/workflow/Workflow.d.ts.map +1 -1
  213. package/dist/workflow/Workflow.js +2 -0
  214. package/dist/workflow/Workflow.js.map +1 -1
  215. package/dist/workflow/index.d.ts.map +1 -1
  216. package/dist/workflow/index.js +2 -0
  217. package/dist/workflow/index.js.map +1 -1
  218. package/dist/workflow/serialization.d.ts.map +1 -1
  219. package/dist/workflow/serialization.js +2 -4
  220. package/dist/workflow/serialization.js.map +1 -1
  221. package/package.json +2 -2
  222. package/src/api/deployment.ts +74 -0
  223. package/src/api/engine.ts +744 -0
  224. package/src/api/index.ts +29 -11
  225. package/src/api/workflow.ts +25 -9
  226. package/src/channel/Channel.ts +14 -11
  227. package/src/channel/ChannelDefinition.ts +101 -76
  228. package/src/channel/index.ts +9 -6
  229. package/src/channel/serialization.ts +77 -68
  230. package/src/deploy/index.ts +17 -1
  231. package/src/deploy/inputs.ts +44 -0
  232. package/src/deploy/requirements.test.ts +64 -61
  233. package/src/deploy/requirements.ts +159 -41
  234. package/src/deploy/spec.test.ts +227 -0
  235. package/src/deploy/spec.ts +397 -0
  236. package/src/diagnostics/__fixtures__/diagnosticFixtures.ts +161 -158
  237. package/src/diagnostics/diagnostics.test.ts +881 -878
  238. package/src/diagnostics/diagnostics.ts +939 -936
  239. package/src/diagnostics/index.ts +14 -11
  240. package/src/edge/Edge.ts +26 -23
  241. package/src/edge/EdgeDefinition.ts +48 -45
  242. package/src/edge/EdgeType.ts +22 -19
  243. package/src/edge/index.ts +11 -8
  244. package/src/edge/serialization.ts +86 -83
  245. package/src/expression/index.ts +7 -4
  246. package/src/expression/parser.ts +365 -362
  247. package/src/expression/types.ts +33 -30
  248. package/src/function/FunctionDeclaration.ts +57 -54
  249. package/src/function/index.ts +6 -3
  250. package/src/function/serialization.ts +43 -40
  251. package/src/globals.d.ts +12 -9
  252. package/src/id/index.ts +11 -8
  253. package/src/index.ts +25 -22
  254. package/src/memory/Memory.ts +18 -15
  255. package/src/memory/MemoryDefinition.ts +19 -16
  256. package/src/memory/MemoryFileDefinition.ts +40 -37
  257. package/src/memory/MemoryRegistry.ts +38 -35
  258. package/src/memory/VectorDatabaseDefinition.ts +24 -21
  259. package/src/memory/index.ts +11 -8
  260. package/src/memory/serialization.ts +50 -47
  261. package/src/migration/index.ts +7 -4
  262. package/src/migration/migrate.test.ts +47 -44
  263. package/src/migration/migrate.ts +61 -58
  264. package/src/migration/migrations.ts +27 -24
  265. package/src/migration/version.ts +12 -9
  266. package/src/model/LLMModelDefinition.ts +15 -12
  267. package/src/model/Model.ts +42 -39
  268. package/src/model/ModelDefinition.ts +18 -15
  269. package/src/model/ModelRegistry.ts +36 -33
  270. package/src/model/index.ts +10 -7
  271. package/src/model/serialization.ts +33 -30
  272. package/src/node/AgentNode.ts +85 -82
  273. package/src/node/DataNode.ts +44 -41
  274. package/src/node/FunctionNode.ts +79 -76
  275. package/src/node/InputNode.ts +188 -185
  276. package/src/node/LogicNode.ts +36 -33
  277. package/src/node/MqttNode.ts +130 -127
  278. package/src/node/Node.ts +64 -61
  279. package/src/node/NodeDefinition.ts +40 -37
  280. package/src/node/NodeRegistry.ts +88 -85
  281. package/src/node/OutputNode.ts +90 -87
  282. package/src/node/ToolNode.ts +35 -32
  283. package/src/node/TriggerNode.ts +275 -272
  284. package/src/node/constants.ts +19 -16
  285. package/src/node/index.ts +29 -26
  286. package/src/node/methods.ts +281 -278
  287. package/src/node/serialization.test.ts +51 -0
  288. package/src/node/serialization.ts +565 -544
  289. package/src/parameter/OutputParameter.ts +71 -68
  290. package/src/parameter/Parameter.ts +246 -243
  291. package/src/parameter/index.ts +36 -33
  292. package/src/variable/Variable.ts +13 -10
  293. package/src/variable/index.ts +19 -16
  294. package/src/variable/operations.ts +109 -106
  295. package/src/workflow/Workflow.ts +44 -41
  296. package/src/workflow/index.ts +6 -3
  297. package/src/workflow/serialization.test.ts +243 -240
  298. package/src/workflow/serialization.ts +245 -242
@@ -1,240 +1,243 @@
1
- import { describe, it, expect } from "vitest";
2
- import { serialize, deserialize, computeVariablesFromNodes, buildCanvasVariables } from "./serialization";
3
- import { MAIN_CANVAS_ID, type Workflow, type Canvas } from "./Workflow";
4
- import type { Schemas } from "../api";
5
- import type { NodeData } from "../node";
6
-
7
- // ============================================================================
8
- // Reverse roundtrip: api JSON deserialize → serialize → deep-equal JSON
9
- //
10
- // This direction is the strongest invariant — deserialize is the function
11
- // that reconstructs derivable state (variable records); if it gets the rest
12
- // right, re-serialize must produce the same JSON back. Tests every code path
13
- // in deserialize + serialize without separately constructing WorkflowState.
14
- // ============================================================================
15
-
16
- const empty: Schemas["Workflow"] = {
17
- schemaVersion: 1,
18
- nodes: [],
19
- edges: [],
20
- functions: [],
21
- declaredVariables: [],
22
- channels: [],
23
- memory: [],
24
- models: [],
25
- };
26
-
27
- const mainOnly: Schemas["Workflow"] = {
28
- schemaVersion: 1,
29
- nodes: [
30
- {
31
- id: "n1",
32
- type: "Agent",
33
- position: { x: 100, y: 200 },
34
- label: "My Agent",
35
- arguments: {
36
- name: "agent-1",
37
- model: "claude-opus-4-7",
38
- instructions: "be helpful",
39
- outputDeclarations: [],
40
- memoryRefs: [],
41
- answer: { active: true, mode: "emit", name: "answer" },
42
- },
43
- },
44
- ],
45
- edges: [],
46
- functions: [],
47
- declaredVariables: [
48
- { uid: "d1", name: "counter", dataType: "int", initialValue: 0 },
49
- ],
50
- channels: [],
51
- memory: [],
52
- models: [],
53
- };
54
-
55
- const allEdgeTypes: Schemas["Workflow"] = {
56
- schemaVersion: 1,
57
- nodes: [],
58
- edges: [
59
- { id: "edge-control", type: "control", from: { nodeId: "a", port: "out" }, to: { nodeId: "b", port: "in" } },
60
- { id: "edge-tool", type: "tool", from: { nodeId: "a", port: "tool-out" }, to: { nodeId: "b", port: "tool-in" } },
61
- {
62
- id: "edge-task",
63
- type: "agentTask",
64
- from: { nodeId: "agent", port: "tool" },
65
- to: { nodeId: "task", port: "trigger" },
66
- prompt: { expression: '"summarize"', references: [], dataType: "string" },
67
- },
68
- {
69
- id: "edge-choice",
70
- type: "agentChoice",
71
- from: { nodeId: "agent", port: "choice" },
72
- to: { nodeId: "branchA", port: "in" },
73
- description: "when the user asks about weather",
74
- },
75
- {
76
- id: "edge-delegate",
77
- type: "agentDelegate",
78
- from: { nodeId: "agent", port: "delegate" },
79
- to: { nodeId: "sub", port: "in" },
80
- prompt: { expression: '"continue"', references: [], dataType: "string" },
81
- description: "delegate everything else",
82
- },
83
- ],
84
- functions: [],
85
- declaredVariables: [],
86
- channels: [],
87
- memory: [],
88
- models: [],
89
- };
90
-
91
- const withFunctionCanvas: Schemas["Workflow"] = {
92
- schemaVersion: 1,
93
- nodes: [
94
- {
95
- id: "fcall",
96
- type: "FunctionCall",
97
- position: { x: 0, y: 0 },
98
- // The wire stores only the reference; the signature is resolved from
99
- // `functions[]` and the snapshot rebuilt on deserialize.
100
- functionId: "fn-uuid",
101
- arguments: {
102
- inputBindings: {
103
- "arg-x": { expression: "1", references: [], dataType: "int" },
104
- "arg-y": { expression: "2", references: [], dataType: "int" },
105
- },
106
- outputBindings: {
107
- "ret-sum": { active: true, mode: "emit", name: "result" },
108
- },
109
- },
110
- },
111
- ],
112
- edges: [],
113
- functions: [
114
- {
115
- functionInfo: {
116
- id: "fn-uuid",
117
- version: 1,
118
- name: "add",
119
- arguments: [
120
- { uid: "arg-x", name: "x", dataType: "int" },
121
- { uid: "arg-y", name: "y", dataType: "int" },
122
- ],
123
- returns: [{ uid: "ret-sum", name: "sum", dataType: "int" }],
124
- },
125
- outputAssignments: {
126
- "ret-sum": { expression: "x + y", references: [
127
- { srcId: "fnarg", varId: "arg-x" },
128
- { srcId: "fnarg", varId: "arg-y" },
129
- ], dataType: "int" },
130
- },
131
- nodes: [],
132
- edges: [],
133
- declaredVariables: [],
134
- },
135
- ],
136
- declaredVariables: [],
137
- channels: [],
138
- memory: [],
139
- models: [],
140
- };
141
-
142
- describe("workflowSerialization — reverse roundtrip (JSON → deserialize → serialize)", () => {
143
- it("empty workflow", () => {
144
- expect(serialize(deserialize(empty))).toEqual(empty);
145
- });
146
-
147
- it("main canvas with declared variables and one node", () => {
148
- expect(serialize(deserialize(mainOnly))).toEqual(mainOnly);
149
- });
150
-
151
- it("preserves all edge type variants verbatim, including ids", () => {
152
- const out = serialize(deserialize(allEdgeTypes));
153
- expect(out).toEqual(allEdgeTypes);
154
- // Specific guardrail: each edge id is preserved (the bug we fixed).
155
- const ids = out.edges.map((e) => e.id).sort();
156
- expect(ids).toEqual(["edge-choice", "edge-control", "edge-delegate", "edge-task", "edge-tool"]);
157
- });
158
-
159
- it("function canvas with fnargs, output assignments, and a FunctionCall on main", () => {
160
- const out = serialize(deserialize(withFunctionCanvas));
161
- expect(out).toEqual(withFunctionCanvas);
162
- });
163
- });
164
-
165
- // ============================================================================
166
- // Forward roundtrip: WorkflowState → serialize → deserialize → equality
167
- //
168
- // Variables are reconstructed from nodes + functionInfo + declared on every
169
- // deserialize. To get exact equality, build fixtures whose `variables` field
170
- // matches what reconstruction would produce (i.e., let buildCanvasVariables
171
- // produce the expected value for us).
172
- // ============================================================================
173
-
174
- function makeMainCanvas(nodes: Canvas["nodes"] = [], edges: Canvas["edges"] = [], declared: Schemas["Variable"][] = []): Canvas {
175
- return {
176
- nodes,
177
- edges,
178
- variables: buildCanvasVariables(nodes, [], declared),
179
- };
180
- }
181
-
182
- describe("workflowSerialization — forward roundtrip (state → serialize → deserialize)", () => {
183
- it("empty state roundtrips to a state with an empty main canvas", () => {
184
- const state: Workflow = { canvases: { [MAIN_CANVAS_ID]: makeMainCanvas() }, functions: {}, channels: {}, memory: {}, models: {} };
185
- expect(deserialize(serialize(state))).toEqual(state);
186
- });
187
-
188
- it("preserves edge ids across serialize/deserialize", () => {
189
- const edges: Canvas["edges"] = [
190
- { id: "stable-id-A", type: "control", source: "n1", sourceHandle: "out", target: "n2", targetHandle: "in" },
191
- { id: "stable-id-B", type: "tool", source: "n1", sourceHandle: "tool", target: "n3", targetHandle: "in" },
192
- ];
193
- const state: Workflow = { canvases: { [MAIN_CANVAS_ID]: makeMainCanvas([], edges) }, functions: {}, channels: {}, memory: {}, models: {} };
194
- const roundTripped = deserialize(serialize(state));
195
- expect(roundTripped.canvases[MAIN_CANVAS_ID]!.edges.map((e) => e.id)).toEqual(["stable-id-A", "stable-id-B"]);
196
- });
197
- });
198
-
199
- // ============================================================================
200
- // Variable reconstruction guardrails
201
- // ============================================================================
202
-
203
- describe("buildCanvasVariables", () => {
204
- it("reconstructs node-output variables from nodes via getNodeOutput", () => {
205
- const nodes: NodeData[] = [
206
- {
207
- id: "agent1",
208
- type: "Agent",
209
- arguments: {
210
- name: "a",
211
- model: "claude-opus-4-7",
212
- instructions: "",
213
- maxTurns: undefined,
214
- outputDeclarations: [],
215
- memoryRefs: [],
216
- answer: { active: true, mode: "emit", name: "answer" },
217
- },
218
- } as NodeData,
219
- ];
220
- const vars = computeVariablesFromNodes(nodes);
221
- // Agent emits at least one output (the answer); key is "<nodeId>:<outputId>".
222
- const keys = Object.keys(vars);
223
- expect(keys.length).toBeGreaterThan(0);
224
- expect(keys.every((k) => k.startsWith("agent1:"))).toBe(true);
225
- });
226
-
227
- it("merges declared + fnarg + node-output into disjoint key namespaces", () => {
228
- const declared: Schemas["Variable"][] = [{ uid: "d1", name: "x", dataType: "int" }];
229
- const fnInfo = {
230
- id: "fn",
231
- version: 1,
232
- name: "f",
233
- arguments: [{ uid: "a1", name: "arg1", dataType: "int" as const }],
234
- returns: [],
235
- };
236
- const merged = buildCanvasVariables([], fnInfo.arguments, declared);
237
- expect(merged["declared:d1"]).toMatchObject({ kind: "declared", uid: "d1" });
238
- expect(merged["fnarg:a1"]).toMatchObject({ kind: "fnarg", uid: "a1" });
239
- });
240
- });
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) 2026 ForestHub.
3
+
4
+ import { describe, it, expect } from "vitest";
5
+ import { serialize, deserialize, computeVariablesFromNodes, buildCanvasVariables } from "./serialization";
6
+ import { MAIN_CANVAS_ID, type Workflow, type Canvas } from "./Workflow";
7
+ import type { Schemas } from "../api";
8
+ import type { NodeData } from "../node";
9
+
10
+ // ============================================================================
11
+ // Reverse roundtrip: api JSON deserialize serialize deep-equal JSON
12
+ //
13
+ // This direction is the strongest invariant deserialize is the function
14
+ // that reconstructs derivable state (variable records); if it gets the rest
15
+ // right, re-serialize must produce the same JSON back. Tests every code path
16
+ // in deserialize + serialize without separately constructing WorkflowState.
17
+ // ============================================================================
18
+
19
+ const empty: Schemas["Workflow"] = {
20
+ schemaVersion: 1,
21
+ nodes: [],
22
+ edges: [],
23
+ functions: [],
24
+ declaredVariables: [],
25
+ channels: [],
26
+ memory: [],
27
+ models: [],
28
+ };
29
+
30
+ const mainOnly: Schemas["Workflow"] = {
31
+ schemaVersion: 1,
32
+ nodes: [
33
+ {
34
+ id: "n1",
35
+ type: "Agent",
36
+ position: { x: 100, y: 200 },
37
+ label: "My Agent",
38
+ arguments: {
39
+ name: "agent-1",
40
+ model: "claude-opus-4-7",
41
+ instructions: "be helpful",
42
+ outputDeclarations: [],
43
+ memoryRefs: [],
44
+ answer: { active: true, mode: "emit", name: "answer" },
45
+ },
46
+ },
47
+ ],
48
+ edges: [],
49
+ functions: [],
50
+ declaredVariables: [
51
+ { uid: "d1", name: "counter", dataType: "int", initialValue: 0 },
52
+ ],
53
+ channels: [],
54
+ memory: [],
55
+ models: [],
56
+ };
57
+
58
+ const allEdgeTypes: Schemas["Workflow"] = {
59
+ schemaVersion: 1,
60
+ nodes: [],
61
+ edges: [
62
+ { id: "edge-control", type: "control", from: { nodeId: "a", port: "out" }, to: { nodeId: "b", port: "in" } },
63
+ { id: "edge-tool", type: "tool", from: { nodeId: "a", port: "tool-out" }, to: { nodeId: "b", port: "tool-in" } },
64
+ {
65
+ id: "edge-task",
66
+ type: "agentTask",
67
+ from: { nodeId: "agent", port: "tool" },
68
+ to: { nodeId: "task", port: "trigger" },
69
+ prompt: { expression: '"summarize"', references: [], dataType: "string" },
70
+ },
71
+ {
72
+ id: "edge-choice",
73
+ type: "agentChoice",
74
+ from: { nodeId: "agent", port: "choice" },
75
+ to: { nodeId: "branchA", port: "in" },
76
+ description: "when the user asks about weather",
77
+ },
78
+ {
79
+ id: "edge-delegate",
80
+ type: "agentDelegate",
81
+ from: { nodeId: "agent", port: "delegate" },
82
+ to: { nodeId: "sub", port: "in" },
83
+ prompt: { expression: '"continue"', references: [], dataType: "string" },
84
+ description: "delegate everything else",
85
+ },
86
+ ],
87
+ functions: [],
88
+ declaredVariables: [],
89
+ channels: [],
90
+ memory: [],
91
+ models: [],
92
+ };
93
+
94
+ const withFunctionCanvas: Schemas["Workflow"] = {
95
+ schemaVersion: 1,
96
+ nodes: [
97
+ {
98
+ id: "fcall",
99
+ type: "FunctionCall",
100
+ position: { x: 0, y: 0 },
101
+ // The wire stores only the reference; the signature is resolved from
102
+ // `functions[]` and the snapshot rebuilt on deserialize.
103
+ functionId: "fn-uuid",
104
+ arguments: {
105
+ inputBindings: {
106
+ "arg-x": { expression: "1", references: [], dataType: "int" },
107
+ "arg-y": { expression: "2", references: [], dataType: "int" },
108
+ },
109
+ outputBindings: {
110
+ "ret-sum": { active: true, mode: "emit", name: "result" },
111
+ },
112
+ },
113
+ },
114
+ ],
115
+ edges: [],
116
+ functions: [
117
+ {
118
+ functionInfo: {
119
+ id: "fn-uuid",
120
+ version: 1,
121
+ name: "add",
122
+ arguments: [
123
+ { uid: "arg-x", name: "x", dataType: "int" },
124
+ { uid: "arg-y", name: "y", dataType: "int" },
125
+ ],
126
+ returns: [{ uid: "ret-sum", name: "sum", dataType: "int" }],
127
+ },
128
+ outputAssignments: {
129
+ "ret-sum": { expression: "x + y", references: [
130
+ { srcId: "fnarg", varId: "arg-x" },
131
+ { srcId: "fnarg", varId: "arg-y" },
132
+ ], dataType: "int" },
133
+ },
134
+ nodes: [],
135
+ edges: [],
136
+ declaredVariables: [],
137
+ },
138
+ ],
139
+ declaredVariables: [],
140
+ channels: [],
141
+ memory: [],
142
+ models: [],
143
+ };
144
+
145
+ describe("workflowSerialization — reverse roundtrip (JSON → deserialize → serialize)", () => {
146
+ it("empty workflow", () => {
147
+ expect(serialize(deserialize(empty))).toEqual(empty);
148
+ });
149
+
150
+ it("main canvas with declared variables and one node", () => {
151
+ expect(serialize(deserialize(mainOnly))).toEqual(mainOnly);
152
+ });
153
+
154
+ it("preserves all edge type variants verbatim, including ids", () => {
155
+ const out = serialize(deserialize(allEdgeTypes));
156
+ expect(out).toEqual(allEdgeTypes);
157
+ // Specific guardrail: each edge id is preserved (the bug we fixed).
158
+ const ids = out.edges.map((e) => e.id).sort();
159
+ expect(ids).toEqual(["edge-choice", "edge-control", "edge-delegate", "edge-task", "edge-tool"]);
160
+ });
161
+
162
+ it("function canvas with fnargs, output assignments, and a FunctionCall on main", () => {
163
+ const out = serialize(deserialize(withFunctionCanvas));
164
+ expect(out).toEqual(withFunctionCanvas);
165
+ });
166
+ });
167
+
168
+ // ============================================================================
169
+ // Forward roundtrip: WorkflowState serialize deserialize equality
170
+ //
171
+ // Variables are reconstructed from nodes + functionInfo + declared on every
172
+ // deserialize. To get exact equality, build fixtures whose `variables` field
173
+ // matches what reconstruction would produce (i.e., let buildCanvasVariables
174
+ // produce the expected value for us).
175
+ // ============================================================================
176
+
177
+ function makeMainCanvas(nodes: Canvas["nodes"] = [], edges: Canvas["edges"] = [], declared: Schemas["Variable"][] = []): Canvas {
178
+ return {
179
+ nodes,
180
+ edges,
181
+ variables: buildCanvasVariables(nodes, [], declared),
182
+ };
183
+ }
184
+
185
+ describe("workflowSerialization — forward roundtrip (state → serialize → deserialize)", () => {
186
+ it("empty state roundtrips to a state with an empty main canvas", () => {
187
+ const state: Workflow = { canvases: { [MAIN_CANVAS_ID]: makeMainCanvas() }, functions: {}, channels: {}, memory: {}, models: {} };
188
+ expect(deserialize(serialize(state))).toEqual(state);
189
+ });
190
+
191
+ it("preserves edge ids across serialize/deserialize", () => {
192
+ const edges: Canvas["edges"] = [
193
+ { id: "stable-id-A", type: "control", source: "n1", sourceHandle: "out", target: "n2", targetHandle: "in" },
194
+ { id: "stable-id-B", type: "tool", source: "n1", sourceHandle: "tool", target: "n3", targetHandle: "in" },
195
+ ];
196
+ const state: Workflow = { canvases: { [MAIN_CANVAS_ID]: makeMainCanvas([], edges) }, functions: {}, channels: {}, memory: {}, models: {} };
197
+ const roundTripped = deserialize(serialize(state));
198
+ expect(roundTripped.canvases[MAIN_CANVAS_ID]!.edges.map((e) => e.id)).toEqual(["stable-id-A", "stable-id-B"]);
199
+ });
200
+ });
201
+
202
+ // ============================================================================
203
+ // Variable reconstruction guardrails
204
+ // ============================================================================
205
+
206
+ describe("buildCanvasVariables", () => {
207
+ it("reconstructs node-output variables from nodes via getNodeOutput", () => {
208
+ const nodes: NodeData[] = [
209
+ {
210
+ id: "agent1",
211
+ type: "Agent",
212
+ arguments: {
213
+ name: "a",
214
+ model: "claude-opus-4-7",
215
+ instructions: "",
216
+ maxTurns: undefined,
217
+ outputDeclarations: [],
218
+ memoryRefs: [],
219
+ answer: { active: true, mode: "emit", name: "answer" },
220
+ },
221
+ } as NodeData,
222
+ ];
223
+ const vars = computeVariablesFromNodes(nodes);
224
+ // Agent emits at least one output (the answer); key is "<nodeId>:<outputId>".
225
+ const keys = Object.keys(vars);
226
+ expect(keys.length).toBeGreaterThan(0);
227
+ expect(keys.every((k) => k.startsWith("agent1:"))).toBe(true);
228
+ });
229
+
230
+ it("merges declared + fnarg + node-output into disjoint key namespaces", () => {
231
+ const declared: Schemas["Variable"][] = [{ uid: "d1", name: "x", dataType: "int" }];
232
+ const fnInfo = {
233
+ id: "fn",
234
+ version: 1,
235
+ name: "f",
236
+ arguments: [{ uid: "a1", name: "arg1", dataType: "int" as const }],
237
+ returns: [],
238
+ };
239
+ const merged = buildCanvasVariables([], fnInfo.arguments, declared);
240
+ expect(merged["declared:d1"]).toMatchObject({ kind: "declared", uid: "d1" });
241
+ expect(merged["fnarg:a1"]).toMatchObject({ kind: "fnarg", uid: "a1" });
242
+ });
243
+ });