@xdelivered/emberflow 0.2.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 (414) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +153 -0
  3. package/bin/commands.test.ts +185 -0
  4. package/bin/commands.ts +306 -0
  5. package/bin/emberflow.mjs +38 -0
  6. package/bin/init.test.ts +165 -0
  7. package/bin/init.ts +297 -0
  8. package/bin/runtime.mjs +105 -0
  9. package/bin/runtime.test.ts +167 -0
  10. package/bin/skillTargets.test.ts +108 -0
  11. package/bin/skillTargets.ts +36 -0
  12. package/bin/skillTemplates.test.ts +37 -0
  13. package/bin/tsconfig.json +23 -0
  14. package/dist/bin/commands.d.ts +24 -0
  15. package/dist/bin/commands.js +252 -0
  16. package/dist/bin/init.d.ts +17 -0
  17. package/dist/bin/init.js +241 -0
  18. package/dist/bin/runtime.d.mts +39 -0
  19. package/dist/bin/runtime.mjs +96 -0
  20. package/dist/bin/skillTargets.d.ts +13 -0
  21. package/dist/bin/skillTargets.js +22 -0
  22. package/dist/server/agents/claudeAdapter.d.ts +8 -0
  23. package/dist/server/agents/claudeAdapter.js +190 -0
  24. package/dist/server/agents/claudeParse.d.ts +2 -0
  25. package/dist/server/agents/claudeParse.js +162 -0
  26. package/dist/server/agents/codexAdapter.d.ts +13 -0
  27. package/dist/server/agents/codexAdapter.js +174 -0
  28. package/dist/server/agents/codexParse.d.ts +2 -0
  29. package/dist/server/agents/codexParse.js +74 -0
  30. package/dist/server/agents/detect.d.ts +23 -0
  31. package/dist/server/agents/detect.js +35 -0
  32. package/dist/server/agents/gitScope.d.ts +24 -0
  33. package/dist/server/agents/gitScope.js +95 -0
  34. package/dist/server/agents/modelRejectionHint.d.ts +8 -0
  35. package/dist/server/agents/modelRejectionHint.js +12 -0
  36. package/dist/server/agents/prompt.d.ts +79 -0
  37. package/dist/server/agents/prompt.js +364 -0
  38. package/dist/server/agents/runManager.d.ts +90 -0
  39. package/dist/server/agents/runManager.js +259 -0
  40. package/dist/server/agents/types.d.ts +15 -0
  41. package/dist/server/agents/types.js +1 -0
  42. package/dist/server/apiStore.d.ts +60 -0
  43. package/dist/server/apiStore.js +249 -0
  44. package/dist/server/auth/enforce.d.ts +1 -0
  45. package/dist/server/auth/enforce.js +6 -0
  46. package/dist/server/auth/types.d.ts +1 -0
  47. package/dist/server/auth/types.js +1 -0
  48. package/dist/server/auth/verifiers.d.ts +1 -0
  49. package/dist/server/auth/verifiers.js +4 -0
  50. package/dist/server/authAttach.d.ts +19 -0
  51. package/dist/server/authAttach.js +79 -0
  52. package/dist/server/cli.d.ts +7 -0
  53. package/dist/server/cli.js +804 -0
  54. package/dist/server/client.d.ts +130 -0
  55. package/dist/server/client.js +247 -0
  56. package/dist/server/environments.d.ts +103 -0
  57. package/dist/server/environments.js +486 -0
  58. package/dist/server/flowStore.d.ts +37 -0
  59. package/dist/server/flowStore.js +117 -0
  60. package/dist/server/httpOperations.d.ts +32 -0
  61. package/dist/server/httpOperations.js +62 -0
  62. package/dist/server/index.d.ts +1 -0
  63. package/dist/server/index.js +1022 -0
  64. package/dist/server/infrastructure.d.ts +51 -0
  65. package/dist/server/infrastructure.js +116 -0
  66. package/dist/server/login.d.ts +21 -0
  67. package/dist/server/login.js +44 -0
  68. package/dist/server/mcp.d.ts +1 -0
  69. package/dist/server/mcp.js +182 -0
  70. package/dist/server/migrateFlows.d.ts +8 -0
  71. package/dist/server/migrateFlows.js +25 -0
  72. package/dist/server/mockHandler.d.ts +15 -0
  73. package/dist/server/mockHandler.js +35 -0
  74. package/dist/server/nodesPayload.d.ts +14 -0
  75. package/dist/server/nodesPayload.js +14 -0
  76. package/dist/server/normalizeFlow.d.ts +21 -0
  77. package/dist/server/normalizeFlow.js +48 -0
  78. package/dist/server/openBrowser.d.ts +8 -0
  79. package/dist/server/openBrowser.js +16 -0
  80. package/dist/server/operationResult.d.ts +12 -0
  81. package/dist/server/operationResult.js +26 -0
  82. package/dist/server/pathGuard.d.ts +1 -0
  83. package/dist/server/pathGuard.js +16 -0
  84. package/dist/server/pathSafety.d.ts +20 -0
  85. package/dist/server/pathSafety.js +35 -0
  86. package/dist/server/projectConfig.d.ts +47 -0
  87. package/dist/server/projectConfig.js +61 -0
  88. package/dist/server/projectMode.d.ts +26 -0
  89. package/dist/server/projectMode.js +44 -0
  90. package/dist/server/redact.d.ts +6 -0
  91. package/dist/server/redact.js +37 -0
  92. package/dist/server/runRegistry.d.ts +84 -0
  93. package/dist/server/runRegistry.js +200 -0
  94. package/dist/server/runtime/RuntimeAdapter.d.ts +22 -0
  95. package/dist/server/runtime/RuntimeAdapter.js +1 -0
  96. package/dist/server/runtime/expressAdapter.d.ts +19 -0
  97. package/dist/server/runtime/expressAdapter.js +56 -0
  98. package/dist/server/scenarioTest.d.ts +14 -0
  99. package/dist/server/scenarioTest.js +66 -0
  100. package/dist/server/subflowRunner.d.ts +32 -0
  101. package/dist/server/subflowRunner.js +72 -0
  102. package/dist/server/testRunner.d.ts +70 -0
  103. package/dist/server/testRunner.js +151 -0
  104. package/dist/src/config.d.ts +3 -0
  105. package/dist/src/config.js +2 -0
  106. package/dist/src/engine/authVerify.d.ts +33 -0
  107. package/dist/src/engine/authVerify.js +63 -0
  108. package/dist/src/engine/diagnostics.d.ts +62 -0
  109. package/dist/src/engine/diagnostics.js +148 -0
  110. package/dist/src/engine/executor.d.ts +151 -0
  111. package/dist/src/engine/executor.js +684 -0
  112. package/dist/src/engine/httpError.d.ts +7 -0
  113. package/dist/src/engine/httpError.js +14 -0
  114. package/dist/src/engine/index.d.ts +11 -0
  115. package/dist/src/engine/index.js +11 -0
  116. package/dist/src/engine/output.d.ts +11 -0
  117. package/dist/src/engine/output.js +20 -0
  118. package/dist/src/engine/publish.d.ts +30 -0
  119. package/dist/src/engine/publish.js +95 -0
  120. package/dist/src/engine/registry.d.ts +34 -0
  121. package/dist/src/engine/registry.js +67 -0
  122. package/dist/src/engine/scenarios.d.ts +12 -0
  123. package/dist/src/engine/scenarios.js +19 -0
  124. package/dist/src/engine/schemaCheck.d.ts +4 -0
  125. package/dist/src/engine/schemaCheck.js +34 -0
  126. package/dist/src/engine/trace.d.ts +12 -0
  127. package/dist/src/engine/trace.js +12 -0
  128. package/dist/src/engine/types.d.ts +308 -0
  129. package/dist/src/engine/types.js +1 -0
  130. package/dist/src/engine/validation.d.ts +26 -0
  131. package/dist/src/engine/validation.js +318 -0
  132. package/dist/src/flows/anomaly-flows.d.ts +23 -0
  133. package/dist/src/flows/anomaly-flows.js +151 -0
  134. package/dist/src/flows/city-sweep-flow.d.ts +10 -0
  135. package/dist/src/flows/city-sweep-flow.js +127 -0
  136. package/dist/src/flows/login-flow.d.ts +13 -0
  137. package/dist/src/flows/login-flow.js +145 -0
  138. package/dist/src/flows/pradar-flows.d.ts +5 -0
  139. package/dist/src/flows/pradar-flows.js +375 -0
  140. package/dist/src/flows/weather-flow.d.ts +12 -0
  141. package/dist/src/flows/weather-flow.js +150 -0
  142. package/dist/src/lib/pathParams.d.ts +3 -0
  143. package/dist/src/lib/pathParams.js +6 -0
  144. package/dist/src/nodes/anomaly.d.ts +2 -0
  145. package/dist/src/nodes/anomaly.js +556 -0
  146. package/dist/src/nodes/flow-control.d.ts +7 -0
  147. package/dist/src/nodes/flow-control.js +270 -0
  148. package/dist/src/nodes/index.d.ts +3 -0
  149. package/dist/src/nodes/index.js +20 -0
  150. package/dist/src/nodes/login.d.ts +7 -0
  151. package/dist/src/nodes/login.js +158 -0
  152. package/dist/src/nodes/loops.d.ts +11 -0
  153. package/dist/src/nodes/loops.js +60 -0
  154. package/dist/src/nodes/pradar/index.d.ts +7 -0
  155. package/dist/src/nodes/pradar/index.js +9 -0
  156. package/dist/src/nodes/pradar/logic.d.ts +189 -0
  157. package/dist/src/nodes/pradar/logic.js +370 -0
  158. package/dist/src/nodes/pradar/nodes.d.ts +42 -0
  159. package/dist/src/nodes/pradar/nodes.js +801 -0
  160. package/dist/src/nodes/requireAuth.d.ts +20 -0
  161. package/dist/src/nodes/requireAuth.js +48 -0
  162. package/dist/src/nodes/response.d.ts +8 -0
  163. package/dist/src/nodes/response.js +33 -0
  164. package/dist/src/nodes/weather.d.ts +33 -0
  165. package/dist/src/nodes/weather.js +260 -0
  166. package/package.json +92 -0
  167. package/server/agentRoute.test.ts +157 -0
  168. package/server/agents/__fixtures__/fake-claude-writer.mjs +23 -0
  169. package/server/agents/__fixtures__/fake-claude.mjs +55 -0
  170. package/server/agents/__fixtures__/fake-codex-writer.mjs +27 -0
  171. package/server/agents/__fixtures__/fake-codex.mjs +52 -0
  172. package/server/agents/__fixtures__/fake-version-empty.mjs +3 -0
  173. package/server/agents/__fixtures__/fake-version-fail.mjs +4 -0
  174. package/server/agents/__fixtures__/fake-version-garbage.mjs +4 -0
  175. package/server/agents/__fixtures__/fake-version-good.mjs +4 -0
  176. package/server/agents/__fixtures__/fake-version-hang.mjs +3 -0
  177. package/server/agents/claudeAdapter.test.ts +127 -0
  178. package/server/agents/claudeAdapter.ts +212 -0
  179. package/server/agents/claudeParse.ts +193 -0
  180. package/server/agents/codexAdapter.test.ts +125 -0
  181. package/server/agents/codexAdapter.ts +198 -0
  182. package/server/agents/codexParse.test.ts +58 -0
  183. package/server/agents/codexParse.ts +126 -0
  184. package/server/agents/detect.test.ts +65 -0
  185. package/server/agents/detect.ts +43 -0
  186. package/server/agents/gitScope.test.ts +129 -0
  187. package/server/agents/gitScope.ts +110 -0
  188. package/server/agents/modelRejectionHint.test.ts +30 -0
  189. package/server/agents/modelRejectionHint.ts +14 -0
  190. package/server/agents/prompt.test.ts +613 -0
  191. package/server/agents/prompt.ts +527 -0
  192. package/server/agents/runManager.test.ts +251 -0
  193. package/server/agents/runManager.ts +286 -0
  194. package/server/agents/types.ts +17 -0
  195. package/server/apiMount.test.ts +591 -0
  196. package/server/apiStore.test.ts +324 -0
  197. package/server/apiStore.ts +241 -0
  198. package/server/auth/enforce.test.ts +61 -0
  199. package/server/auth/enforce.ts +6 -0
  200. package/server/auth/types.ts +5 -0
  201. package/server/auth/verifiers.test.ts +37 -0
  202. package/server/auth/verifiers.ts +9 -0
  203. package/server/authAttach.test.ts +175 -0
  204. package/server/authAttach.ts +99 -0
  205. package/server/cli.ts +847 -0
  206. package/server/cliAuth.test.ts +229 -0
  207. package/server/cliCreate.test.ts +131 -0
  208. package/server/cliDoctor.test.ts +326 -0
  209. package/server/cliTest.test.ts +735 -0
  210. package/server/client.ts +314 -0
  211. package/server/demoProject.test.ts +29 -0
  212. package/server/diagnosticsRoute.test.ts +169 -0
  213. package/server/environments.test.ts +620 -0
  214. package/server/environments.ts +603 -0
  215. package/server/environmentsRoute.test.ts +385 -0
  216. package/server/errorWorkflow.test.ts +205 -0
  217. package/server/flowStore.test.ts +84 -0
  218. package/server/flowStore.ts +125 -0
  219. package/server/httpOperations.test.ts +377 -0
  220. package/server/httpOperations.ts +96 -0
  221. package/server/index.ts +1111 -0
  222. package/server/infrastructure.test.ts +154 -0
  223. package/server/infrastructure.ts +161 -0
  224. package/server/infrastructureRoute.test.ts +105 -0
  225. package/server/login.test.ts +153 -0
  226. package/server/login.ts +67 -0
  227. package/server/mcp.ts +278 -0
  228. package/server/migrateFlows.test.ts +55 -0
  229. package/server/migrateFlows.ts +23 -0
  230. package/server/mockHandler.test.ts +138 -0
  231. package/server/mockHandler.ts +52 -0
  232. package/server/mockRun.test.ts +367 -0
  233. package/server/mockServe.test.ts +467 -0
  234. package/server/nodesPayload.test.ts +29 -0
  235. package/server/nodesPayload.ts +26 -0
  236. package/server/normalizeFlow.test.ts +80 -0
  237. package/server/normalizeFlow.ts +59 -0
  238. package/server/openBrowser.test.ts +20 -0
  239. package/server/openBrowser.ts +22 -0
  240. package/server/operationResult.test.ts +67 -0
  241. package/server/operationResult.ts +27 -0
  242. package/server/pathGuard.test.ts +19 -0
  243. package/server/pathGuard.ts +12 -0
  244. package/server/pathSafety.test.ts +38 -0
  245. package/server/pathSafety.ts +33 -0
  246. package/server/projectConfig.test.ts +67 -0
  247. package/server/projectConfig.ts +101 -0
  248. package/server/projectMode.test.ts +106 -0
  249. package/server/projectMode.ts +59 -0
  250. package/server/redact.test.ts +88 -0
  251. package/server/redact.ts +35 -0
  252. package/server/redactEgress.test.ts +281 -0
  253. package/server/runRegistry.ts +270 -0
  254. package/server/runsAuth.test.ts +119 -0
  255. package/server/runtime/RuntimeAdapter.ts +24 -0
  256. package/server/runtime/expressAdapter.test.ts +38 -0
  257. package/server/runtime/expressAdapter.ts +70 -0
  258. package/server/scenarioTest.test.ts +148 -0
  259. package/server/scenarioTest.ts +88 -0
  260. package/server/setupStatus.test.ts +127 -0
  261. package/server/subflowRunner.ts +104 -0
  262. package/server/testRunner.ts +216 -0
  263. package/server/tsconfig.json +20 -0
  264. package/server/workflowTestRoute.test.ts +297 -0
  265. package/src/App.tsx +233 -0
  266. package/src/components/AgentConsole.tsx +461 -0
  267. package/src/components/BranchRulesEditor.tsx +156 -0
  268. package/src/components/CreateModal.tsx +275 -0
  269. package/src/components/Dock.tsx +487 -0
  270. package/src/components/EnvironmentDialog.tsx +569 -0
  271. package/src/components/EnvironmentPicker.tsx +357 -0
  272. package/src/components/EnvironmentsDialog.tsx +144 -0
  273. package/src/components/ExecutionPager.tsx +67 -0
  274. package/src/components/InfraPanel.test.tsx +103 -0
  275. package/src/components/InfraPanel.tsx +171 -0
  276. package/src/components/Inspector.tsx +1182 -0
  277. package/src/components/Json.tsx +50 -0
  278. package/src/components/JsonModal.tsx +52 -0
  279. package/src/components/LogMessage.tsx +61 -0
  280. package/src/components/NodeRunModal.tsx +213 -0
  281. package/src/components/RunLogPanel.tsx +397 -0
  282. package/src/components/RunbookView.tsx +1085 -0
  283. package/src/components/Scenarios.tsx +476 -0
  284. package/src/components/SettingsDialog.tsx +229 -0
  285. package/src/components/Sidebar.tsx +436 -0
  286. package/src/components/StatusBar.tsx +302 -0
  287. package/src/components/Toolbar.tsx +663 -0
  288. package/src/components/WelcomeDialog.test.tsx +107 -0
  289. package/src/components/WelcomeDialog.tsx +412 -0
  290. package/src/components/agentMarkdown.test.tsx +91 -0
  291. package/src/components/agentMarkdown.tsx +158 -0
  292. package/src/components/ui/badge.tsx +31 -0
  293. package/src/components/ui/button.tsx +46 -0
  294. package/src/components/ui/combobox.tsx +114 -0
  295. package/src/components/ui/command.tsx +82 -0
  296. package/src/components/ui/dialog.tsx +73 -0
  297. package/src/components/ui/input.tsx +20 -0
  298. package/src/components/ui/popover.tsx +32 -0
  299. package/src/components/ui/resizable.tsx +44 -0
  300. package/src/components/ui/tabs.tsx +36 -0
  301. package/src/config.ts +4 -0
  302. package/src/engine/authVerify.ts +81 -0
  303. package/src/engine/diagnostics.test.ts +437 -0
  304. package/src/engine/diagnostics.ts +211 -0
  305. package/src/engine/executor.test.ts +1339 -0
  306. package/src/engine/executor.ts +840 -0
  307. package/src/engine/httpError.test.ts +12 -0
  308. package/src/engine/httpError.ts +16 -0
  309. package/src/engine/index.ts +11 -0
  310. package/src/engine/output.ts +21 -0
  311. package/src/engine/publish.test.ts +176 -0
  312. package/src/engine/publish.ts +127 -0
  313. package/src/engine/registry.test.ts +64 -0
  314. package/src/engine/registry.ts +80 -0
  315. package/src/engine/scenarios.test.ts +55 -0
  316. package/src/engine/scenarios.ts +29 -0
  317. package/src/engine/schemaCheck.test.ts +21 -0
  318. package/src/engine/schemaCheck.ts +35 -0
  319. package/src/engine/trace.test.ts +19 -0
  320. package/src/engine/trace.ts +23 -0
  321. package/src/engine/types.test.ts +22 -0
  322. package/src/engine/types.ts +316 -0
  323. package/src/engine/validation.test.ts +314 -0
  324. package/src/engine/validation.ts +354 -0
  325. package/src/flows/anomaly-flows.test.ts +81 -0
  326. package/src/flows/anomaly-flows.ts +202 -0
  327. package/src/flows/city-sweep-flow.ts +131 -0
  328. package/src/flows/flows.test.ts +30 -0
  329. package/src/flows/login-flow.ts +150 -0
  330. package/src/flows/pradar-flows.test.ts +204 -0
  331. package/src/flows/pradar-flows.ts +419 -0
  332. package/src/flows/weather-flow.ts +154 -0
  333. package/src/globals.css +147 -0
  334. package/src/lib/pathParams.test.ts +24 -0
  335. package/src/lib/pathParams.ts +6 -0
  336. package/src/lib/registerLens.test.ts +79 -0
  337. package/src/lib/registerLens.ts +59 -0
  338. package/src/lib/runbookModel.test.ts +61 -0
  339. package/src/lib/runbookModel.ts +290 -0
  340. package/src/lib/runbookProjection.test.ts +417 -0
  341. package/src/lib/runbookProjection.ts +254 -0
  342. package/src/lib/useTailAnchor.ts +53 -0
  343. package/src/lib/utils.ts +7 -0
  344. package/src/main.tsx +13 -0
  345. package/src/nodes/anomaly.test.ts +98 -0
  346. package/src/nodes/anomaly.ts +637 -0
  347. package/src/nodes/effects-scan.test.ts +44 -0
  348. package/src/nodes/flow-control.test.ts +280 -0
  349. package/src/nodes/flow-control.ts +305 -0
  350. package/src/nodes/index.ts +22 -0
  351. package/src/nodes/login.test.ts +81 -0
  352. package/src/nodes/login.ts +186 -0
  353. package/src/nodes/loops.ts +69 -0
  354. package/src/nodes/pradar/index.ts +11 -0
  355. package/src/nodes/pradar/logic.test.ts +288 -0
  356. package/src/nodes/pradar/logic.ts +582 -0
  357. package/src/nodes/pradar/nodes.test.ts +146 -0
  358. package/src/nodes/pradar/nodes.ts +955 -0
  359. package/src/nodes/requireAuth.test.ts +75 -0
  360. package/src/nodes/requireAuth.ts +55 -0
  361. package/src/nodes/response.test.ts +33 -0
  362. package/src/nodes/response.ts +39 -0
  363. package/src/nodes/traceDetail.test.ts +39 -0
  364. package/src/nodes/weather.test.ts +65 -0
  365. package/src/nodes/weather.ts +306 -0
  366. package/src/store/agentClient.test.ts +103 -0
  367. package/src/store/agentClient.ts +131 -0
  368. package/src/store/apiTree.test.ts +17 -0
  369. package/src/store/apiTree.ts +73 -0
  370. package/src/store/builderStore.test.ts +1234 -0
  371. package/src/store/builderStore.ts +1879 -0
  372. package/src/store/infraClient.ts +63 -0
  373. package/src/store/nodeMeta.test.ts +24 -0
  374. package/src/store/nodeMeta.ts +21 -0
  375. package/src/store/persistence.test.ts +15 -0
  376. package/src/store/persistence.ts +54 -0
  377. package/src/store/serverRunner.ts +381 -0
  378. package/src/store/setupClient.ts +57 -0
  379. package/studio-dist/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
  380. package/studio-dist/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
  381. package/studio-dist/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
  382. package/studio-dist/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
  383. package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
  384. package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
  385. package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
  386. package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
  387. package/studio-dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  388. package/studio-dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  389. package/studio-dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  390. package/studio-dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  391. package/studio-dist/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
  392. package/studio-dist/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
  393. package/studio-dist/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
  394. package/studio-dist/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
  395. package/studio-dist/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
  396. package/studio-dist/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
  397. package/studio-dist/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
  398. package/studio-dist/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
  399. package/studio-dist/assets/index-DNJwW-hM.css +1 -0
  400. package/studio-dist/assets/index-O26dKRjW.js +65 -0
  401. package/studio-dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  402. package/studio-dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  403. package/studio-dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  404. package/studio-dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  405. package/studio-dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  406. package/studio-dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  407. package/studio-dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  408. package/studio-dist/favicon.svg +1 -0
  409. package/studio-dist/icons.svg +24 -0
  410. package/studio-dist/index.html +14 -0
  411. package/templates/skills/emberflow-basics/SKILL.md +316 -0
  412. package/templates/skills/emberflow-model-process/SKILL.md +209 -0
  413. package/templates/skills/emberflow-new-workflow/SKILL.md +211 -0
  414. package/templates/skills/emberflow-review-workflow/SKILL.md +148 -0
@@ -0,0 +1,385 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
2
+ import { spawn, type ChildProcess } from 'node:child_process';
3
+ import { createServer, type Server } from 'node:http';
4
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
5
+ import { tmpdir } from 'node:os';
6
+ import { join } from 'node:path';
7
+
8
+ // Verifies GET /environments reports auth status and POST /environments/:name/login
9
+ // performs the login flow, persisting the captured credential (Task 5).
10
+ // Boots the actual runner subprocess (server/index.ts) against a scratch
11
+ // project dir, mirroring the harness in apiMount.test.ts / runsAuth.test.ts.
12
+
13
+ let proc: ChildProcess;
14
+ const PORT = 8131;
15
+ const base = `http://127.0.0.1:${PORT}`;
16
+ let projectDir: string;
17
+
18
+ let loginServer: Server;
19
+ const LOGIN_PORT = 8132;
20
+ let concurrentLoginHits = 0;
21
+
22
+ // Single-node flow: an Input node that just echoes whatever input the run
23
+ // was invoked with (including `headers`) — reused from runsAuth.test.ts's
24
+ // harness pattern to prove the run-auto-attach-after-PUT-secret case.
25
+ const echoFlow = {
26
+ id: 'echo-input',
27
+ name: 'Echo Input',
28
+ version: 1,
29
+ createdAt: '2026-01-01T00:00:00Z',
30
+ updatedAt: '2026-01-01T00:00:00Z',
31
+ nodes: [
32
+ {
33
+ id: 'input',
34
+ type: 'Input',
35
+ label: 'Input',
36
+ position: { x: 0, y: 0 },
37
+ config: { fields: [], defaults: {} },
38
+ },
39
+ ],
40
+ edges: [],
41
+ };
42
+
43
+ /** POSTs a step-mode run, steps it once, then reads back the Input node's recorded sample output. */
44
+ async function runAndGetInputOutput(input: Record<string, unknown>): Promise<any> {
45
+ const createRes = await fetch(`${base}/api/runs`, {
46
+ method: 'POST',
47
+ headers: { 'Content-Type': 'application/json' },
48
+ body: JSON.stringify({ flow: echoFlow, mode: 'step', input, environment: 'runattach' }),
49
+ });
50
+ expect(createRes.status).toBe(201);
51
+ const { runId } = await createRes.json();
52
+
53
+ const stepRes = await fetch(`${base}/api/runs/${runId}/step`, { method: 'POST' });
54
+ expect(stepRes.status).toBe(200);
55
+
56
+ const samplesRes = await fetch(`${base}/api/samples?nodeId=input`);
57
+ expect(samplesRes.status).toBe(200);
58
+ const { samples } = await samplesRes.json();
59
+ const sample = samples.find((s: { runId: string }) => s.runId === runId);
60
+ expect(sample).toBeDefined();
61
+ return sample.output;
62
+ }
63
+
64
+ async function waitHealthy(url: string, tries = 40): Promise<void> {
65
+ for (let i = 0; i < tries; i++) {
66
+ try {
67
+ if ((await fetch(url)).ok) return;
68
+ } catch {
69
+ /* not up yet */
70
+ }
71
+ await new Promise((r) => setTimeout(r, 150));
72
+ }
73
+ throw new Error('runner did not become healthy');
74
+ }
75
+
76
+ function bootRunner(port: number, env: Record<string, string | undefined>): ChildProcess {
77
+ return spawn('npx', ['tsx', 'server/index.ts'], {
78
+ env: { ...process.env, EMBERFLOW_RUNNER_PORT: String(port), ...env },
79
+ stdio: 'ignore',
80
+ });
81
+ }
82
+
83
+ beforeAll(async () => {
84
+ // Tiny stub login target: responds 200 with a Set-Cookie header.
85
+ loginServer = createServer((req, res) => {
86
+ if (req.url === '/login-concurrent') concurrentLoginHits += 1;
87
+ res.setHeader('Set-Cookie', 'session=captured-token; Path=/');
88
+ res.writeHead(200, { 'Content-Type': 'application/json' });
89
+ res.end(JSON.stringify({ ok: true }));
90
+ });
91
+ await new Promise<void>((resolve) => loginServer.listen(LOGIN_PORT, resolve));
92
+
93
+ projectDir = mkdtempSync(join(tmpdir(), 'envroute-'));
94
+ mkdirSync(join(projectDir, 'emberflow', 'apis', 'default'), { recursive: true });
95
+ writeFileSync(join(projectDir, 'emberflow.config.mjs'), 'export default {};\n');
96
+ writeFileSync(
97
+ join(projectDir, 'emberflow.environments.json'),
98
+ JSON.stringify({
99
+ defaultEnvironment: 'dev',
100
+ environments: {
101
+ dev: {
102
+ vars: {},
103
+ secrets: {},
104
+ auth: {
105
+ attach: { as: 'cookie', name: 'session', secretRef: 'sessionCookie' },
106
+ login: {
107
+ request: { method: 'POST', url: `http://127.0.0.1:${LOGIN_PORT}/login` },
108
+ capture: { from: 'set-cookie', cookieName: 'session' },
109
+ },
110
+ },
111
+ },
112
+ noauth: {
113
+ vars: {},
114
+ secrets: {},
115
+ },
116
+ concurrent: {
117
+ vars: {},
118
+ secrets: {},
119
+ auth: {
120
+ attach: { as: 'cookie', name: 'session', secretRef: 'sessionCookie' },
121
+ login: {
122
+ request: { method: 'POST', url: `http://127.0.0.1:${LOGIN_PORT}/login-concurrent` },
123
+ capture: { from: 'set-cookie', cookieName: 'session' },
124
+ },
125
+ },
126
+ },
127
+ secrettest: {
128
+ vars: {},
129
+ secrets: {},
130
+ },
131
+ authtest: {
132
+ vars: {},
133
+ secrets: {},
134
+ },
135
+ runattach: {
136
+ vars: {},
137
+ secrets: {},
138
+ auth: { attach: { as: 'cookie', name: 'session', secretRef: 'sessionCookie' } },
139
+ },
140
+ },
141
+ }),
142
+ );
143
+
144
+ proc = bootRunner(PORT, { EMBERFLOW_PROJECT: projectDir });
145
+ await waitHealthy(`${base}/healthz`);
146
+ }, 20_000);
147
+
148
+ afterAll(async () => {
149
+ proc?.kill();
150
+ await new Promise<void>((resolve) => loginServer.close(() => resolve()));
151
+ if (projectDir) rmSync(projectDir, { recursive: true, force: true });
152
+ });
153
+
154
+ describe('GET /environments auth status', () => {
155
+ it('reports configured true, authenticated false when no secret is stored yet', async () => {
156
+ const res = await fetch(`${base}/api/environments`);
157
+ expect(res.status).toBe(200);
158
+ const body = await res.json();
159
+ const dev = body.environments.find((e: { name: string }) => e.name === 'dev');
160
+ expect(dev.auth).toEqual({
161
+ configured: true,
162
+ authenticated: false,
163
+ secretRef: 'sessionCookie',
164
+ config: {
165
+ attach: { as: 'cookie', name: 'session', secretRef: 'sessionCookie' },
166
+ login: {
167
+ request: { method: 'POST', url: `http://127.0.0.1:${LOGIN_PORT}/login` },
168
+ capture: { from: 'set-cookie', cookieName: 'session' },
169
+ },
170
+ },
171
+ });
172
+ });
173
+
174
+ it('omits auth.configured=false env cleanly (no auth block details leak)', async () => {
175
+ const res = await fetch(`${base}/api/environments`);
176
+ const body = await res.json();
177
+ const noauth = body.environments.find((e: { name: string }) => e.name === 'noauth');
178
+ expect(noauth.auth).toEqual({ configured: false, authenticated: false });
179
+ });
180
+
181
+ it('reports configured:true for a project with an environments file', async () => {
182
+ const res = await fetch(`${base}/api/environments`);
183
+ const body = await res.json();
184
+ expect(body.configured).toBe(true);
185
+ });
186
+
187
+ it('re-reads the environments file on each GET — agent edits appear without a restart', async () => {
188
+ const envPath = join(projectDir, 'emberflow.environments.json');
189
+ const file = JSON.parse(readFileSync(envPath, 'utf8'));
190
+ file.environments.staging = { vars: { BASE_URL: 'https://staging.example.com' } };
191
+ writeFileSync(envPath, JSON.stringify(file, null, 2));
192
+
193
+ const res = await fetch(`${base}/api/environments`);
194
+ const body = await res.json();
195
+ const staging = body.environments.find((e: { name: string }) => e.name === 'staging');
196
+ expect(staging).toBeDefined();
197
+ expect(staging.varKeys).toEqual(['BASE_URL']);
198
+ });
199
+ });
200
+
201
+ describe('POST /environments/:name/login', () => {
202
+ it('400s when the environment has no auth.login configured', async () => {
203
+ const res = await fetch(`${base}/api/environments/noauth/login`, { method: 'POST' });
204
+ expect(res.status).toBe(400);
205
+ const body = await res.json();
206
+ expect(body.error).toBeTruthy();
207
+ });
208
+
209
+ it('performs the login, returns authenticated:true, and updates GET /environments in-process', async () => {
210
+ const res = await fetch(`${base}/api/environments/dev/login`, { method: 'POST' });
211
+ expect(res.status).toBe(200);
212
+ const body = await res.json();
213
+ expect(body).toEqual({ authenticated: true, secretRef: 'sessionCookie' });
214
+ // no secret value ever returned
215
+ expect(JSON.stringify(body)).not.toContain('captured-token');
216
+
217
+ const getRes = await fetch(`${base}/api/environments`);
218
+ const getBody = await getRes.json();
219
+ const dev = getBody.environments.find((e: { name: string }) => e.name === 'dev');
220
+ expect(dev.auth).toEqual({
221
+ configured: true,
222
+ authenticated: true,
223
+ secretRef: 'sessionCookie',
224
+ config: {
225
+ attach: { as: 'cookie', name: 'session', secretRef: 'sessionCookie' },
226
+ login: {
227
+ request: { method: 'POST', url: `http://127.0.0.1:${LOGIN_PORT}/login` },
228
+ capture: { from: 'set-cookie', cookieName: 'session' },
229
+ },
230
+ },
231
+ });
232
+ });
233
+
234
+ it('single-flights two concurrent POST logins to the same environment: upstream hit exactly once, both responses 200', async () => {
235
+ expect(concurrentLoginHits).toBe(0);
236
+ const [res1, res2] = await Promise.all([
237
+ fetch(`${base}/api/environments/concurrent/login`, { method: 'POST' }),
238
+ fetch(`${base}/api/environments/concurrent/login`, { method: 'POST' }),
239
+ ]);
240
+ expect(res1.status).toBe(200);
241
+ expect(res2.status).toBe(200);
242
+ const [body1, body2] = await Promise.all([res1.json(), res2.json()]);
243
+ expect(body1).toEqual({ authenticated: true, secretRef: 'sessionCookie' });
244
+ expect(body2).toEqual({ authenticated: true, secretRef: 'sessionCookie' });
245
+ expect(concurrentLoginHits).toBe(1);
246
+ });
247
+ });
248
+
249
+ describe('PUT /environments/:name/secrets/:key', () => {
250
+ it('404s for an unknown environment', async () => {
251
+ const res = await fetch(`${base}/api/environments/nope/secrets/foo`, {
252
+ method: 'PUT',
253
+ headers: { 'Content-Type': 'application/json' },
254
+ body: JSON.stringify({ value: 'x' }),
255
+ });
256
+ expect(res.status).toBe(404);
257
+ const body = await res.json();
258
+ expect(body.error).toBeTruthy();
259
+ });
260
+
261
+ it('400s when value is missing or not a string', async () => {
262
+ const missing = await fetch(`${base}/api/environments/secrettest/secrets/apiKey`, {
263
+ method: 'PUT',
264
+ headers: { 'Content-Type': 'application/json' },
265
+ body: JSON.stringify({}),
266
+ });
267
+ expect(missing.status).toBe(400);
268
+ expect((await missing.json()).error).toBeTruthy();
269
+
270
+ const nonString = await fetch(`${base}/api/environments/secrettest/secrets/apiKey`, {
271
+ method: 'PUT',
272
+ headers: { 'Content-Type': 'application/json' },
273
+ body: JSON.stringify({ value: 42 }),
274
+ });
275
+ expect(nonString.status).toBe(400);
276
+ expect((await nonString.json()).error).toBeTruthy();
277
+ });
278
+
279
+ it('204s with an empty body, never echoing the value, and GET /environments shows the key (not the value)', async () => {
280
+ const putRes = await fetch(`${base}/api/environments/secrettest/secrets/apiKey`, {
281
+ method: 'PUT',
282
+ headers: { 'Content-Type': 'application/json' },
283
+ body: JSON.stringify({ value: 'super-secret-value' }),
284
+ });
285
+ expect(putRes.status).toBe(204);
286
+ const text = await putRes.text();
287
+ expect(text).toBe('');
288
+ expect(text).not.toContain('super-secret-value');
289
+
290
+ const getRes = await fetch(`${base}/api/environments`);
291
+ const getBody = await getRes.json();
292
+ expect(JSON.stringify(getBody)).not.toContain('super-secret-value');
293
+ const env = getBody.environments.find((e: { name: string }) => e.name === 'secrettest');
294
+ expect(env.secretKeys).toContain('apiKey');
295
+ });
296
+ });
297
+
298
+ describe('DELETE /environments/:name/secrets/:key', () => {
299
+ it('404s for an unknown environment', async () => {
300
+ const res = await fetch(`${base}/api/environments/nope/secrets/apiKey`, { method: 'DELETE' });
301
+ expect(res.status).toBe(404);
302
+ const body = await res.json();
303
+ expect(body.error).toBeTruthy();
304
+ });
305
+
306
+ it('204s and removes the key from GET /environments secretKeys', async () => {
307
+ // Depends on the PUT test above having set secrettest.apiKey.
308
+ const delRes = await fetch(`${base}/api/environments/secrettest/secrets/apiKey`, { method: 'DELETE' });
309
+ expect(delRes.status).toBe(204);
310
+
311
+ const getRes = await fetch(`${base}/api/environments`);
312
+ const getBody = await getRes.json();
313
+ const env = getBody.environments.find((e: { name: string }) => e.name === 'secrettest');
314
+ expect(env.secretKeys).not.toContain('apiKey');
315
+ });
316
+ });
317
+
318
+ describe('PUT /environments/:name/auth', () => {
319
+ it('404s for an unknown environment', async () => {
320
+ const res = await fetch(`${base}/api/environments/nope/auth`, {
321
+ method: 'PUT',
322
+ headers: { 'Content-Type': 'application/json' },
323
+ body: JSON.stringify({ attach: { as: 'cookie', name: 'session', secretRef: 'sessionCookie' } }),
324
+ });
325
+ expect(res.status).toBe(404);
326
+ const body = await res.json();
327
+ expect(body.error).toBeTruthy();
328
+ });
329
+
330
+ it('400s on an invalid auth shape', async () => {
331
+ const res = await fetch(`${base}/api/environments/authtest/auth`, {
332
+ method: 'PUT',
333
+ headers: { 'Content-Type': 'application/json' },
334
+ body: JSON.stringify({ attach: { as: 'nope', name: 'session', secretRef: 'sessionCookie' } }),
335
+ });
336
+ expect(res.status).toBe(400);
337
+ const body = await res.json();
338
+ expect(body.error).toBeTruthy();
339
+ });
340
+
341
+ it('204s on success and GET /environments reflects auth.config', async () => {
342
+ const putRes = await fetch(`${base}/api/environments/authtest/auth`, {
343
+ method: 'PUT',
344
+ headers: { 'Content-Type': 'application/json' },
345
+ body: JSON.stringify({ attach: { as: 'header', name: 'X-Api-Key', secretRef: 'apiKeySecret' } }),
346
+ });
347
+ expect(putRes.status).toBe(204);
348
+ expect(await putRes.text()).toBe('');
349
+
350
+ const getRes = await fetch(`${base}/api/environments`);
351
+ const getBody = await getRes.json();
352
+ const env = getBody.environments.find((e: { name: string }) => e.name === 'authtest');
353
+ expect(env.auth.config).toEqual({
354
+ attach: { as: 'header', name: 'X-Api-Key', secretRef: 'apiKeySecret' },
355
+ });
356
+ });
357
+
358
+ it('204s clearing auth with a null body, and GET /environments omits config', async () => {
359
+ const putRes = await fetch(`${base}/api/environments/authtest/auth`, {
360
+ method: 'PUT',
361
+ headers: { 'Content-Type': 'application/json' },
362
+ body: JSON.stringify(null),
363
+ });
364
+ expect(putRes.status).toBe(204);
365
+
366
+ const getRes = await fetch(`${base}/api/environments`);
367
+ const getBody = await getRes.json();
368
+ const env = getBody.environments.find((e: { name: string }) => e.name === 'authtest');
369
+ expect(env.auth).toEqual({ configured: false, authenticated: false });
370
+ });
371
+ });
372
+
373
+ describe('a run auto-attaches a PUT-set secret (proves in-memory refresh)', () => {
374
+ it('attaches the session cookie from a secret set via PUT, without a restart', async () => {
375
+ const putRes = await fetch(`${base}/api/environments/runattach/secrets/sessionCookie`, {
376
+ method: 'PUT',
377
+ headers: { 'Content-Type': 'application/json' },
378
+ body: JSON.stringify({ value: 'freshly-put-cookie' }),
379
+ });
380
+ expect(putRes.status).toBe(204);
381
+
382
+ const output = await runAndGetInputOutput({});
383
+ expect(output.headers.cookie).toContain('session=«secret:sessionCookie»');
384
+ });
385
+ });
@@ -0,0 +1,205 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { createDefaultRegistry } from '../src/nodes';
3
+ import type { WorkflowDefinition } from '../src/engine';
4
+ import { RunRegistry, type RunEvent } from './runRegistry';
5
+
6
+ const base = { version: 1, createdAt: '2026-01-01T00:00:00Z', updatedAt: '2026-01-01T00:00:00Z' };
7
+
8
+ /** A registry with a node type that always throws — used to force run failures. */
9
+ function registryWithBoom() {
10
+ const registry = createDefaultRegistry();
11
+ registry.register({ type: 'boom', label: 'Boom' }, async () => {
12
+ throw new Error('kaboom');
13
+ });
14
+ return registry;
15
+ }
16
+
17
+ const failingFlow: WorkflowDefinition = {
18
+ ...base,
19
+ id: 'failing-flow',
20
+ name: 'Failing Flow',
21
+ nodes: [{ id: 'a', type: 'boom', label: 'Boom', position: { x: 0, y: 0 }, config: {} }],
22
+ edges: [],
23
+ };
24
+
25
+ /** Error op: an Input node so the run's input is visible via nodeStates.input.input. */
26
+ const errorOp: WorkflowDefinition = {
27
+ ...base,
28
+ id: 'error-op',
29
+ name: 'Error Op',
30
+ nodes: [
31
+ { id: 'input', type: 'Input', label: 'Input', position: { x: 0, y: 0 }, config: { fields: [], defaults: {} } },
32
+ ],
33
+ edges: [],
34
+ };
35
+
36
+ const errorOpThatFails: WorkflowDefinition = {
37
+ ...base,
38
+ id: 'error-op-boom',
39
+ name: 'Error Op Boom',
40
+ nodes: [{ id: 'a', type: 'boom', label: 'Boom', position: { x: 0, y: 0 }, config: {} }],
41
+ edges: [],
42
+ };
43
+
44
+ const runOpts = {
45
+ secrets: {},
46
+ vars: {},
47
+ environment: 'test',
48
+ safeMode: false,
49
+ };
50
+
51
+ function flowStore(flows: WorkflowDefinition[]) {
52
+ const byId = new Map(flows.map((f) => [f.id, f]));
53
+ return (id: string) => byId.get(id);
54
+ }
55
+
56
+ describe('error workflow', () => {
57
+ it('fires the configured error op with failure details when a run fails', async () => {
58
+ const runs = new RunRegistry(flowStore([failingFlow, errorOp]), registryWithBoom(), 'error-op');
59
+ const { handle } = runs.create(failingFlow, runOpts);
60
+ const before = new Set([...(runs as unknown as { runs: Map<string, unknown> }).runs.keys()]);
61
+ await handle.runToEnd();
62
+
63
+ // Poll briefly: the error op run is created synchronously inside onRunFinished.
64
+ const allRunsMap = (runs as unknown as { runs: Map<string, { handle: { run: { workflowId: string; nodeStates: Record<string, { status: string; input?: unknown }> } } }> }).runs;
65
+ const newIds = [...allRunsMap.keys()].filter((id) => !before.has(id));
66
+ expect(newIds.length).toBe(1);
67
+ const errorRunEntry = allRunsMap.get(newIds[0])!;
68
+ expect(errorRunEntry.handle.run.workflowId).toBe('error-op');
69
+ const inputState = errorRunEntry.handle.run.nodeStates.input;
70
+ expect(inputState.status).toBe('succeeded');
71
+ const capturedInput = (inputState as unknown as { output: { failedWorkflowId: string; failedRunId: string; error: string } }).output;
72
+ expect(capturedInput.failedWorkflowId).toBe('failing-flow');
73
+ expect(capturedInput.failedRunId).toBe(handle.run.id);
74
+ expect(capturedInput.error).toBe('kaboom');
75
+ });
76
+
77
+ it("tags the error-handler run's finished event with errorHandler.firedBy; the failed run carries none", async () => {
78
+ const runs = new RunRegistry(flowStore([failingFlow, errorOp]), registryWithBoom(), 'error-op');
79
+ const { handle, runId: failedRunId } = runs.create(failingFlow, runOpts);
80
+ const before = new Set([...(runs as unknown as { runs: Map<string, unknown> }).runs.keys()]);
81
+ await handle.runToEnd();
82
+
83
+ const allRunsMap = (runs as unknown as { runs: Map<string, unknown> }).runs;
84
+ const newIds = [...allRunsMap.keys()].filter((id) => !before.has(id));
85
+ expect(newIds.length).toBe(1);
86
+ const errorRunId = newIds[0];
87
+
88
+ const errorRunEvents: RunEvent[] = [];
89
+ runs.subscribe(errorRunId, (e) => errorRunEvents.push(e));
90
+ const errorFinished = errorRunEvents.find((e) => e.type === 'finished');
91
+ expect(errorFinished?.errorHandler).toEqual({ firedBy: 'failing-flow' });
92
+
93
+ const failedRunEvents: RunEvent[] = [];
94
+ runs.subscribe(failedRunId, (e) => failedRunEvents.push(e));
95
+ const failedFinished = failedRunEvents.find((e) => e.type === 'finished');
96
+ expect(failedFinished?.errorHandler).toBeUndefined();
97
+ });
98
+
99
+ it('reports the halting failure, not an earlier fail-soft optional node', async () => {
100
+ // 'soft' fails first (declaration order) but is optional — fail-soft, the
101
+ // run continues. 'fatal' then fails and halts the run. The error op must
102
+ // receive fatal's id/error, not soft's.
103
+ const mixedFlow: WorkflowDefinition = {
104
+ ...base,
105
+ id: 'mixed-flow',
106
+ name: 'Mixed Flow',
107
+ nodes: [
108
+ { id: 'soft', type: 'boom', label: 'Soft Boom', position: { x: 0, y: 0 }, config: {}, optional: true },
109
+ { id: 'fatal', type: 'boom2', label: 'Fatal Boom', position: { x: 200, y: 0 }, config: {} },
110
+ ],
111
+ edges: [],
112
+ };
113
+ const registry = registryWithBoom();
114
+ registry.register({ type: 'boom2', label: 'Boom2' }, async () => {
115
+ throw new Error('fatal kaboom');
116
+ });
117
+ const runs = new RunRegistry(flowStore([mixedFlow, errorOp]), registry, 'error-op');
118
+ const { handle } = runs.create(mixedFlow, runOpts);
119
+ const before = new Set([...(runs as unknown as { runs: Map<string, unknown> }).runs.keys()]);
120
+ await handle.runToEnd();
121
+
122
+ const allRunsMap = (runs as unknown as { runs: Map<string, { handle: { run: { workflowId: string; nodeStates: Record<string, { status: string; output?: unknown }> } } }> }).runs;
123
+ const newIds = [...allRunsMap.keys()].filter((id) => !before.has(id));
124
+ expect(newIds.length).toBe(1);
125
+ const inputState = allRunsMap.get(newIds[0])!.handle.run.nodeStates.input;
126
+ const capturedInput = (inputState as unknown as { output: { failedNodeId: string; error: string } }).output;
127
+ expect(capturedInput.failedNodeId).toBe('fatal');
128
+ expect(capturedInput.error).toBe('fatal kaboom');
129
+ });
130
+
131
+ it('does not spawn a third run when the error op itself fails', async () => {
132
+ const runs = new RunRegistry(flowStore([failingFlow, errorOpThatFails]), registryWithBoom(), 'error-op-boom');
133
+ const { handle } = runs.create(failingFlow, runOpts);
134
+ const before = new Set([...(runs as unknown as { runs: Map<string, unknown> }).runs.keys()]);
135
+ await handle.runToEnd();
136
+
137
+ const allRunsMap = (runs as unknown as { runs: Map<string, unknown> }).runs;
138
+ const newIds = [...allRunsMap.keys()].filter((id) => !before.has(id));
139
+ // Only the error-op run itself; it must not recurse into a third run.
140
+ expect(newIds.length).toBe(1);
141
+ });
142
+
143
+ it('does nothing when no errorOperation is configured', async () => {
144
+ const runs = new RunRegistry(flowStore([failingFlow]), registryWithBoom());
145
+ const { handle } = runs.create(failingFlow, runOpts);
146
+ const before = new Set([...(runs as unknown as { runs: Map<string, unknown> }).runs.keys()]);
147
+ await handle.runToEnd();
148
+
149
+ const allRunsMap = (runs as unknown as { runs: Map<string, unknown> }).runs;
150
+ const newIds = [...allRunsMap.keys()].filter((id) => !before.has(id));
151
+ expect(newIds.length).toBe(0);
152
+ });
153
+
154
+ it('a failed MOCK run fires its error op as a MOCK run — infra in the error op fails loud instead of executing real', async () => {
155
+ // The error op carries a db-flavoured node with NO mock: if mock mode did
156
+ // not propagate, its implementation would run (here: throw a sentinel we
157
+ // can detect); with propagation it must fail with the infra boundary.
158
+ const registry = registryWithBoom();
159
+ let realCalls = 0;
160
+ registry.register({ type: 'dbWrite', label: 'DB Write', traceKind: 'db' }, async () => {
161
+ realCalls += 1;
162
+ return { wrote: true };
163
+ });
164
+ const errorOpWithInfra: WorkflowDefinition = {
165
+ ...base,
166
+ id: 'error-op-infra',
167
+ name: 'Error Op Infra',
168
+ nodes: [{ id: 'log', type: 'dbWrite', label: 'DB Write', position: { x: 0, y: 0 }, config: {} }],
169
+ edges: [],
170
+ };
171
+ const runs = new RunRegistry(flowStore([failingFlow, errorOpWithInfra]), registry, 'error-op-infra');
172
+ const before = new Set([...(runs as unknown as { runs: Map<string, unknown> }).runs.keys()]);
173
+ const { handle } = runs.create(failingFlow, { ...runOpts, mockRun: true, mocks: {} });
174
+ await handle.runToEnd();
175
+
176
+ const allRunsMap = (runs as unknown as { runs: Map<string, { handle: { run: { workflowId: string; status: string; nodeStates: Record<string, { status: string; error?: string }> }; runToEnd(): Promise<unknown> } }> }).runs;
177
+ const newIds = [...allRunsMap.keys()].filter((id) => !before.has(id) && id !== handle.run.id);
178
+ expect(newIds.length).toBe(1);
179
+ const errorRun = allRunsMap.get(newIds[0])!.handle;
180
+ await errorRun.runToEnd().catch(() => {});
181
+ expect(realCalls).toBe(0);
182
+ expect(errorRun.run.nodeStates.log.status).toBe('failed');
183
+ expect(errorRun.run.nodeStates.log.error).toContain('would touch real infrastructure');
184
+ });
185
+
186
+ it('does nothing when the run succeeds', async () => {
187
+ const okFlow: WorkflowDefinition = {
188
+ ...base,
189
+ id: 'ok-flow',
190
+ name: 'Ok Flow',
191
+ nodes: [
192
+ { id: 'input', type: 'Input', label: 'Input', position: { x: 0, y: 0 }, config: { fields: [], defaults: {} } },
193
+ ],
194
+ edges: [],
195
+ };
196
+ const runs = new RunRegistry(flowStore([okFlow, errorOp]), createDefaultRegistry(), 'error-op');
197
+ const { handle } = runs.create(okFlow, runOpts);
198
+ const before = new Set([...(runs as unknown as { runs: Map<string, unknown> }).runs.keys()]);
199
+ await handle.runToEnd();
200
+
201
+ const allRunsMap = (runs as unknown as { runs: Map<string, unknown> }).runs;
202
+ const newIds = [...allRunsMap.keys()].filter((id) => !before.has(id));
203
+ expect(newIds.length).toBe(0);
204
+ });
205
+ });
@@ -0,0 +1,84 @@
1
+ import { mkdtempSync, readFileSync, existsSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { afterEach, describe, expect, it } from 'vitest';
5
+ import { FlowStore } from './flowStore';
6
+ import type { WorkflowDefinition } from '../src/engine';
7
+
8
+ const dirs: string[] = [];
9
+ function scratch(): string {
10
+ const d = mkdtempSync(join(tmpdir(), 'ef-store-'));
11
+ dirs.push(d);
12
+ return d;
13
+ }
14
+ afterEach(() => { for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true }); });
15
+
16
+ const flow = (id: string, scenarios?: WorkflowDefinition['scenarios']): WorkflowDefinition => ({
17
+ id, name: id, version: 1, nodes: [], edges: [],
18
+ createdAt: '2026-01-01T00:00:00Z', updatedAt: '2026-01-01T00:00:00Z',
19
+ ...(scenarios ? { scenarios } : {}),
20
+ });
21
+ const scenario = { id: 's1', name: 'happy', input: { a: 1 } };
22
+
23
+ describe('FlowStore scenario sidecars', () => {
24
+ it('sidecar mode: save splits scenarios into <id>.scenarios.json', () => {
25
+ const d = scratch();
26
+ const store = new FlowStore(d, { scenarioSidecars: true });
27
+ store.save(flow('f1', [scenario]));
28
+ const onDisk = JSON.parse(readFileSync(join(d, 'f1.json'), 'utf8'));
29
+ expect(onDisk.scenarios).toBeUndefined();
30
+ const sidecar = JSON.parse(readFileSync(join(d, 'f1.scenarios.json'), 'utf8'));
31
+ expect(sidecar).toEqual([scenario]);
32
+ });
33
+
34
+ it('sidecar mode: load and list merge the sidecar back', () => {
35
+ const d = scratch();
36
+ const store = new FlowStore(d, { scenarioSidecars: true });
37
+ store.save(flow('f1', [scenario]));
38
+ expect(store.load('f1')!.scenarios).toEqual([scenario]);
39
+ expect(store.list()[0].scenarios).toEqual([scenario]);
40
+ });
41
+
42
+ it('sidecar mode: saving with no scenarios removes a stale sidecar', () => {
43
+ const d = scratch();
44
+ const store = new FlowStore(d, { scenarioSidecars: true });
45
+ store.save(flow('f1', [scenario]));
46
+ store.save(flow('f1'));
47
+ expect(existsSync(join(d, 'f1.scenarios.json'))).toBe(false);
48
+ expect(store.load('f1')!.scenarios).toBeUndefined();
49
+ });
50
+
51
+ it('sidecar mode: hand-authored sidecar is merged even if flow file has none', () => {
52
+ const d = scratch();
53
+ writeFileSync(join(d, 'f2.json'), JSON.stringify(flow('f2')));
54
+ writeFileSync(join(d, 'f2.scenarios.json'), JSON.stringify([scenario]));
55
+ const store = new FlowStore(d, { scenarioSidecars: true });
56
+ expect(store.load('f2')!.scenarios).toEqual([scenario]);
57
+ });
58
+
59
+ it('default mode: scenarios stay embedded, no sidecar written', () => {
60
+ const d = scratch();
61
+ const store = new FlowStore(d);
62
+ store.save(flow('f1', [scenario]));
63
+ const onDisk = JSON.parse(readFileSync(join(d, 'f1.json'), 'utf8'));
64
+ expect(onDisk.scenarios).toEqual([scenario]);
65
+ expect(existsSync(join(d, 'f1.scenarios.json'))).toBe(false);
66
+ });
67
+
68
+ it('remove cleans sidecar and seedIfEmpty ignores orphaned sidecars', () => {
69
+ const d = scratch();
70
+ const store = new FlowStore(d, { scenarioSidecars: true });
71
+ store.save(flow('f1', [scenario]));
72
+ store.remove('f1');
73
+ expect(existsSync(join(d, 'f1.scenarios.json'))).toBe(false);
74
+ store.seedIfEmpty([flow('seeded')]);
75
+ expect(existsSync(join(d, 'seeded.json'))).toBe(true);
76
+ });
77
+
78
+ it('list skips .scenarios.json files as flow candidates in both modes', () => {
79
+ const d = scratch();
80
+ const store = new FlowStore(d, { scenarioSidecars: true });
81
+ store.save(flow('f1', [scenario]));
82
+ expect(store.list().map((f) => f.id)).toEqual(['f1']);
83
+ });
84
+ });