@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,104 @@
1
+ import type {
2
+ LogLine,
3
+ NodeRegistry,
4
+ StartRunOptions,
5
+ SubflowResult,
6
+ TraceSink,
7
+ WorkflowDefinition,
8
+ } from '../src/engine';
9
+ import { runOutput, startRun } from '../src/engine';
10
+
11
+ /** Max depth of nested Subflow calls (ancestry chain length) before a run fails. */
12
+ export const SUBFLOW_DEPTH_CAP = 8;
13
+
14
+ export interface SubflowRunnerOptions {
15
+ /** Resolves a child workflow by id (server hosts this via apiStore.load). */
16
+ loadFlow: (id: string) => WorkflowDefinition | undefined;
17
+ registry: NodeRegistry;
18
+ secrets: Record<string, string>;
19
+ vars: Record<string, string>;
20
+ environment: string;
21
+ safeMode: boolean;
22
+ /** When true, child runs execute in Mock mode too — nothing real is ever
23
+ * touched during a mock run, including inside Subflow children. See
24
+ * `StartRunOptions.mockRun`. */
25
+ mockRun?: boolean;
26
+ /** Optional trace sink shared with the parent run (SSE/samples plumbing). */
27
+ trace?: TraceSink;
28
+ /** Optional child-log forwarder — receives the child's log lines already
29
+ * prefixed with the child flow's name and the caller node's id, exactly as
30
+ * the live runner streams them over SSE. Absent for headless runs. */
31
+ onLog?: (line: LogLine) => void;
32
+ }
33
+
34
+ /**
35
+ * Shared Subflow-runner factory used by BOTH the live runner
36
+ * (server/runRegistry.ts) and the in-process test runner
37
+ * (server/testRunner.ts): resolve the child via `loadFlow`, run it to
38
+ * completion on the same registry sharing the parent's environment
39
+ * (secrets/vars/safeMode), and return its collected Result output. The
40
+ * ancestry chain enforces the depth cap and catches A→B→A cycles; nested
41
+ * children keep their own execution sequence.
42
+ */
43
+ export function makeSubflowRunner(
44
+ opts: SubflowRunnerOptions,
45
+ ancestry: string[],
46
+ ): NonNullable<StartRunOptions['subflowRunner']> {
47
+ return async (workflowId, input, callerNodeId): Promise<SubflowResult> => {
48
+ if (ancestry.length >= SUBFLOW_DEPTH_CAP) {
49
+ return { status: 'failed', error: `subflow depth cap (${SUBFLOW_DEPTH_CAP}) exceeded` };
50
+ }
51
+ if (ancestry.includes(workflowId)) {
52
+ return { status: 'failed', error: `subflow cycle: ${[...ancestry, workflowId].join(' → ')}` };
53
+ }
54
+ const childFlow = opts.loadFlow(workflowId);
55
+ if (!childFlow) return { status: 'failed', error: `Unknown workflow: ${workflowId}` };
56
+ try {
57
+ const childHandle = startRun({
58
+ flow: childFlow,
59
+ registry: opts.registry,
60
+ secrets: opts.secrets,
61
+ vars: opts.vars,
62
+ environment: opts.environment,
63
+ safeMode: opts.safeMode,
64
+ input,
65
+ // Mock mode propagates into the child: nothing real is ever touched
66
+ // in a mock run, including inside Subflow children. Scenario context
67
+ // never propagates (the parent didn't pick one for the child) — only
68
+ // the CHILD op's own op-level mocks apply, since nodeIds are scoped
69
+ // per-flow and reusing the parent's mock map would be meaningless
70
+ // (or worse, silently wrong) for the child's node ids.
71
+ mockRun: opts.mockRun,
72
+ mocks: (childFlow as { mocks?: Record<string, unknown> }).mocks ?? {},
73
+ trace: opts.trace,
74
+ subflowRunner: makeSubflowRunner(opts, [...ancestry, workflowId]),
75
+ events: opts.onLog
76
+ ? {
77
+ onLog: (line) =>
78
+ opts.onLog!({
79
+ ...line,
80
+ nodeLabel: `${childFlow.name} › ${line.nodeLabel ?? ''}`,
81
+ nodeId: line.nodeId ? `${callerNodeId}/${line.nodeId}` : callerNodeId,
82
+ }),
83
+ }
84
+ : undefined,
85
+ });
86
+ const childRun = await childHandle.runToEnd();
87
+ if (childRun.status !== 'succeeded') {
88
+ // Surface the child's actual failure (e.g. the Mock-mode "would touch
89
+ // real infrastructure" boundary message) through the Subflow node's
90
+ // own error, rather than an opaque "failed" — the first failed
91
+ // node's `error`, if any, wins.
92
+ const failedNode = Object.values(childRun.nodeStates).find((s) => s.status === 'failed' && s.error);
93
+ const detail = failedNode?.error;
94
+ return {
95
+ status: 'failed',
96
+ error: `subflow "${childFlow.name}" ${childRun.status}${detail ? `: ${detail}` : ''}`,
97
+ };
98
+ }
99
+ return { status: 'succeeded', output: runOutput(childRun, childFlow) };
100
+ } catch (err) {
101
+ return { status: 'failed', error: err instanceof Error ? err.message : String(err) };
102
+ }
103
+ };
104
+ }
@@ -0,0 +1,216 @@
1
+ import {
2
+ mergeMocks,
3
+ startRun,
4
+ type NodeRegistry,
5
+ type ScenarioDefinition,
6
+ type WorkflowDefinition,
7
+ } from '../src/engine';
8
+ import { loadProjectConfig } from './projectConfig';
9
+ import { buildApiStore, buildRegistries, requireProjectWhenExplicit } from './projectMode';
10
+ import { loadEnvironments, resolveRunSafety, type EnvironmentsFile } from './environments';
11
+ import { evaluateExpectation } from './scenarioTest';
12
+ import { makeSubflowRunner } from './subflowRunner';
13
+ import { redactSecrets } from './redact';
14
+ import type { ApiStore } from './apiStore';
15
+
16
+ /** Thrown for CLI usage errors (unknown opId / unknown environment) — the CLI
17
+ * layer turns this into exit code 2, distinct from an assertion failure
18
+ * (exit 1) or an unexpected error (also exit 1, via runCli's generic catch). */
19
+ export class ScenarioTestUsageError extends Error {}
20
+
21
+ export type SkipReason = 'no expect' | 'empty expect';
22
+
23
+ export interface ScenarioResult {
24
+ opId: string;
25
+ scenario: string;
26
+ status: 'passed' | 'failed' | 'skipped';
27
+ /** Present only when status === 'failed' — evaluateExpectation's failure strings. */
28
+ failures?: string[];
29
+ /** Present only when status === 'skipped'. */
30
+ reason?: SkipReason;
31
+ }
32
+
33
+ export interface TestReport {
34
+ results: ScenarioResult[];
35
+ passed: number;
36
+ failed: number;
37
+ skipped: number;
38
+ }
39
+
40
+ interface RunEnv {
41
+ secrets: Record<string, string>;
42
+ vars: Record<string, string>;
43
+ environment: string;
44
+ safeMode: boolean;
45
+ }
46
+
47
+ async function runScenario(
48
+ op: WorkflowDefinition,
49
+ scenario: ScenarioDefinition,
50
+ apiStore: ApiStore,
51
+ registry: NodeRegistry,
52
+ env: RunEnv,
53
+ mock: boolean,
54
+ ): Promise<ScenarioResult> {
55
+ if (!scenario.expect) {
56
+ return { opId: op.id, scenario: scenario.name, status: 'skipped', reason: 'no expect' };
57
+ }
58
+ if (Object.keys(scenario.expect).length === 0) {
59
+ return { opId: op.id, scenario: scenario.name, status: 'skipped', reason: 'empty expect' };
60
+ }
61
+
62
+ // INTENTIONAL (controller decision): NO environment auth auto-attach here,
63
+ // unlike POST /runs (server/index.ts attachCredential). Tests are hermetic —
64
+ // auto-attaching the environment's credential would silently satisfy the very
65
+ // auth checks a deliberate 401-expectation scenario exists to exercise. If a
66
+ // scenario needs auth, embed the headers directly in `scenario.input`.
67
+ //
68
+ // --mock: same merge as the live serving mock-run path (mergeMocks,
69
+ // src/engine/scenarios.ts) — op-level `flow.mocks` overlaid by this
70
+ // scenario's `mocks`, scenario wins per nodeId. Nothing real is ever
71
+ // touched: the executor's mockRun short-circuit fails loudly on any
72
+ // infrastructure node (traceKind db/http/llm) that has no mock.
73
+ const handle = startRun({
74
+ flow: op,
75
+ registry,
76
+ secrets: env.secrets,
77
+ vars: env.vars,
78
+ environment: env.environment,
79
+ safeMode: env.safeMode,
80
+ input: scenario.input,
81
+ mockRun: mock,
82
+ mocks: mock ? mergeMocks(op, scenario) : undefined,
83
+ // Shared factory (server/subflowRunner.ts) — same depth cap/cycle guard as
84
+ // the live runner, minus the SSE trace/log plumbing a headless run skips.
85
+ subflowRunner: makeSubflowRunner(
86
+ {
87
+ loadFlow: (id) => apiStore.load(id),
88
+ registry,
89
+ secrets: env.secrets,
90
+ vars: env.vars,
91
+ environment: env.environment,
92
+ safeMode: env.safeMode,
93
+ mockRun: mock,
94
+ },
95
+ [op.id],
96
+ ),
97
+ });
98
+ const run = await handle.runToEnd();
99
+ const { ok, failures } = evaluateExpectation(run, op, scenario.expect);
100
+ return {
101
+ opId: op.id,
102
+ scenario: scenario.name,
103
+ status: ok ? 'passed' : 'failed',
104
+ ...(ok ? {} : { failures }),
105
+ };
106
+ }
107
+
108
+ /**
109
+ * Runs every scenario with `expect` across the given ops (or a single op when
110
+ * `opId` is given), entirely in-process: `startRun` is called directly per
111
+ * scenario, mirroring the options shape `RunRegistry.create` passes
112
+ * (server/runRegistry.ts) — but bypassing RunRegistry entirely, so these runs
113
+ * never enter it (no SSE, no run history).
114
+ *
115
+ * This is the shared seam both the CLI (`runScenarioSuite`, which bootstraps
116
+ * its own project/registries/apiStore from disk) and the studio route
117
+ * (`POST /workflows/:id/test` in server/index.ts, which reuses the
118
+ * already-booted server's apiStore/registry/environmentsFile — no re-read
119
+ * from disk, no drift from in-memory writes) call into. NO second expectation
120
+ * engine — both paths land here.
121
+ *
122
+ * Throws `ScenarioTestUsageError` for usage problems (unknown opId, unknown
123
+ * environment) — the CLI layer maps that to exit code 2; the route maps it to
124
+ * 400/404.
125
+ */
126
+ export async function runScenarioSuiteFor(
127
+ ctx: { apiStore: ApiStore; registry: NodeRegistry; environmentsFile: EnvironmentsFile },
128
+ opts: { opId?: string; environmentName?: string; mock?: boolean },
129
+ ): Promise<TestReport> {
130
+ const { apiStore, registry, environmentsFile } = ctx;
131
+ const { opId, environmentName, mock = false } = opts;
132
+
133
+ const envName = environmentName ?? environmentsFile.defaultEnvironment;
134
+ const envDef = environmentsFile.environments[envName];
135
+ if (!envDef) {
136
+ throw new ScenarioTestUsageError(`Unknown environment: '${envName}'`);
137
+ }
138
+ const safety = resolveRunSafety(envName, envDef, {});
139
+ const runEnv: RunEnv = {
140
+ secrets: envDef.secrets,
141
+ vars: envDef.vars,
142
+ environment: envName,
143
+ safeMode: safety.ok ? safety.safeMode : false,
144
+ };
145
+
146
+ const allOps = apiStore.list();
147
+ let targetOps = allOps;
148
+ if (opId !== undefined) {
149
+ const op = allOps.find((o) => o.id === opId);
150
+ if (!op) throw new ScenarioTestUsageError(`Unknown operation: '${opId}'`);
151
+ targetOps = [op];
152
+ }
153
+
154
+ const rawResults: ScenarioResult[] = [];
155
+ for (const op of targetOps) {
156
+ for (const scenario of op.scenarios ?? []) {
157
+ rawResults.push(await runScenario(op, scenario, apiStore, registry, runEnv, mock));
158
+ }
159
+ }
160
+
161
+ // evaluateExpectation diffs RAW run state, so a failure string can quote a
162
+ // secret value the flow echoed into its response body. Redact the whole
163
+ // reporter payload (value-based; raw/url-encoded/base64 forms) before it can
164
+ // reach stdout/CI logs — same redactSecrets the SSE event path uses.
165
+ const results = redactSecrets(rawResults, runEnv.secrets);
166
+
167
+ return {
168
+ results,
169
+ passed: results.filter((r) => r.status === 'passed').length,
170
+ failed: results.filter((r) => r.status === 'failed').length,
171
+ skipped: results.filter((r) => r.status === 'skipped').length,
172
+ };
173
+ }
174
+
175
+ /**
176
+ * CLI entry point: bootstraps a project from disk (loadProjectConfig/
177
+ * buildRegistries/buildApiStore/loadEnvironments — the same bootstrap
178
+ * `server/index.ts` uses) and delegates to `runScenarioSuiteFor`. NO runner
179
+ * process, no HTTP.
180
+ */
181
+ export async function runScenarioSuite(opts: {
182
+ projectDir: string;
183
+ opId?: string;
184
+ environmentName?: string;
185
+ mock?: boolean;
186
+ }): Promise<TestReport> {
187
+ const { projectDir, opId, environmentName, mock } = opts;
188
+
189
+ const project = requireProjectWhenExplicit(
190
+ await loadProjectConfig(projectDir),
191
+ process.env.EMBERFLOW_PROJECT,
192
+ projectDir,
193
+ );
194
+ const environmentsFile = loadEnvironments(project ? project.root : projectDir);
195
+ const { execution } = buildRegistries(project);
196
+ const apiStore = buildApiStore(project);
197
+
198
+ return runScenarioSuiteFor({ apiStore, registry: execution, environmentsFile }, { opId, environmentName, mock });
199
+ }
200
+
201
+ /** `✓ opId · scenario` / `✗ opId · scenario (failure; failure)` — undefined for
202
+ * skipped scenarios, which aren't printed as individual lines (only counted
203
+ * in the summary), keeping default output focused on assertable scenarios. */
204
+ export function formatScenarioLine(r: ScenarioResult): string | undefined {
205
+ if (r.status === 'skipped') return undefined;
206
+ if (r.status === 'passed') return `✓ ${r.opId} · ${r.scenario}`;
207
+ return `✗ ${r.opId} · ${r.scenario} (${(r.failures ?? []).join('; ')})`;
208
+ }
209
+
210
+ /** `12 passed, 1 failed, 3 skipped (no expect)` — the parenthetical lists the
211
+ * distinct skip reasons present (omitted entirely when nothing was skipped). */
212
+ export function formatSummary(report: TestReport): string {
213
+ const reasons = [...new Set(report.results.filter((r) => r.status === 'skipped').map((r) => r.reason))];
214
+ const suffix = report.skipped > 0 ? ` (${reasons.join(', ')})` : '';
215
+ return `${report.passed} passed, ${report.failed} failed, ${report.skipped} skipped${suffix}`;
216
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2023",
4
+ "lib": ["ES2023", "DOM"],
5
+ "types": ["node"],
6
+ "skipLibCheck": true,
7
+ "module": "esnext",
8
+ "moduleResolution": "bundler",
9
+ "allowImportingTsExtensions": true,
10
+ "verbatimModuleSyntax": true,
11
+ "moduleDetection": "force",
12
+ "noEmit": true,
13
+ "strict": true,
14
+ "noUnusedLocals": true,
15
+ "noUnusedParameters": true,
16
+ "noFallthroughCasesInSwitch": true,
17
+ "allowJs": true
18
+ },
19
+ "include": ["."]
20
+ }
@@ -0,0 +1,297 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
2
+ import { spawn, type ChildProcess } from 'node:child_process';
3
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+
7
+ // Verifies POST /workflows/:id/test — the studio "Test" button's server
8
+ // route: runs one op's `expect`-carrying scenarios in-process, reusing the
9
+ // same suite/evaluation/redaction seam as `emberflow test` (server/testRunner.ts),
10
+ // and never touches RunRegistry (no SSE, no run history). Boots the actual
11
+ // runner subprocess (server/index.ts) against a scratch project dir, mirroring
12
+ // the harness in environmentsRoute.test.ts / runsAuth.test.ts.
13
+
14
+ let proc: ChildProcess;
15
+ const PORT = 8137;
16
+ const base = `http://127.0.0.1:${PORT}`;
17
+ let projectDir: string;
18
+
19
+ async function waitHealthy(url: string, tries = 40): Promise<void> {
20
+ for (let i = 0; i < tries; i++) {
21
+ try {
22
+ if ((await fetch(url)).ok) return;
23
+ } catch {
24
+ /* not up yet */
25
+ }
26
+ await new Promise((r) => setTimeout(r, 150));
27
+ }
28
+ throw new Error('runner did not become healthy');
29
+ }
30
+
31
+ function bootRunner(port: number, env: Record<string, string | undefined>): ChildProcess {
32
+ return spawn('npx', ['tsx', 'server/index.ts'], {
33
+ env: { ...process.env, EMBERFLOW_RUNNER_PORT: String(port), ...env },
34
+ stdio: 'ignore',
35
+ });
36
+ }
37
+
38
+ async function postTest(opId: string, body?: Record<string, unknown>): Promise<Response> {
39
+ return fetch(`${base}/api/workflows/${encodeURIComponent(opId)}/test`, {
40
+ method: 'POST',
41
+ headers: { 'Content-Type': 'application/json' },
42
+ body: JSON.stringify(body ?? {}),
43
+ });
44
+ }
45
+
46
+ beforeAll(async () => {
47
+ projectDir = mkdtempSync(join(tmpdir(), 'workflowtestroute-'));
48
+ writeFileSync(
49
+ join(projectDir, 'emberflow.config.mjs'),
50
+ `export default {
51
+ registerNodes(registry) {
52
+ registry.register(
53
+ { type: 'EchoToken', label: 'Echo Token', inputSchema: { fields: [{ name: 'token', type: 'string' }] } },
54
+ async (ctx) => ({ token: ctx.input.token }),
55
+ );
56
+ },
57
+ };\n`,
58
+ );
59
+ writeFileSync(
60
+ join(projectDir, 'emberflow.environments.json'),
61
+ JSON.stringify({
62
+ defaultEnvironment: 'local',
63
+ environments: {
64
+ local: { vars: {}, secrets: { API_TOKEN: 'supersecretvalue' } },
65
+ },
66
+ }),
67
+ );
68
+
69
+ const apisDir = join(projectDir, 'emberflow', 'apis', 'default');
70
+ mkdirSync(apisDir, { recursive: true });
71
+
72
+ // authcheck: one op with a pass, a fail, and an expect-less (skip) scenario —
73
+ // mirrors server/cliTest.test.ts's harness.
74
+ writeFileSync(
75
+ join(apisDir, 'authcheck.json'),
76
+ JSON.stringify({
77
+ id: 'authcheck',
78
+ name: 'Auth Check',
79
+ version: 1,
80
+ http: { method: 'POST', path: '/authcheck' },
81
+ nodes: [
82
+ {
83
+ id: 'input',
84
+ type: 'Input',
85
+ label: 'Input',
86
+ position: { x: 0, y: 0 },
87
+ config: { fields: [{ name: 'authed', type: 'boolean' }] },
88
+ },
89
+ {
90
+ id: 'route',
91
+ type: 'Route',
92
+ label: 'Route',
93
+ position: { x: 200, y: 0 },
94
+ config: { field: 'authed', branches: ['true', 'false'] },
95
+ inputMap: { value: { sourceNodeId: 'input', sourceField: '$' } },
96
+ },
97
+ {
98
+ id: 'respOk',
99
+ type: 'Response',
100
+ label: 'OK',
101
+ position: { x: 400, y: -80 },
102
+ config: { status: 200, body: { ok: true } },
103
+ },
104
+ {
105
+ id: 'respUnauth',
106
+ type: 'Response',
107
+ label: 'Unauthorized',
108
+ position: { x: 400, y: 80 },
109
+ config: { status: 401, body: { error: 'unauthorized' } },
110
+ },
111
+ ],
112
+ edges: [
113
+ { id: 'e1', source: 'input', target: 'route' },
114
+ { id: 'e2', source: 'route', target: 'respOk', sourceHandle: 'true' },
115
+ { id: 'e3', source: 'route', target: 'respUnauth', sourceHandle: 'false' },
116
+ ],
117
+ createdAt: '2026-01-01T00:00:00Z',
118
+ updatedAt: '2026-01-01T00:00:00Z',
119
+ }),
120
+ );
121
+ writeFileSync(
122
+ join(apisDir, 'authcheck.scenarios.json'),
123
+ JSON.stringify([
124
+ { id: 's-pass', name: 'authed-ok', input: { authed: true }, expect: { status: 200, body: { ok: true } } },
125
+ // Deliberately wrong expectation: authed:true routes to 200, but this
126
+ // scenario asserts 401 — must fail.
127
+ { id: 's-fail', name: 'expired', input: { authed: true }, expect: { status: 401 } },
128
+ { id: 's-skip', name: 'no-assertion', input: { authed: false } },
129
+ ]),
130
+ );
131
+
132
+ // leaky: a deliberate failure whose diff quotes a secret the flow echoed —
133
+ // must arrive redacted.
134
+ writeFileSync(
135
+ join(apisDir, 'leaky.json'),
136
+ JSON.stringify({
137
+ id: 'leaky',
138
+ name: 'Leaky',
139
+ version: 1,
140
+ nodes: [
141
+ {
142
+ id: 'echo',
143
+ type: 'EchoToken',
144
+ label: 'Echo',
145
+ position: { x: 0, y: 0 },
146
+ config: { token: { $secret: 'API_TOKEN' } },
147
+ },
148
+ {
149
+ id: 'result',
150
+ type: 'Result',
151
+ label: 'Result',
152
+ position: { x: 200, y: 0 },
153
+ config: {},
154
+ inputMap: { token: { sourceNodeId: 'echo', sourceField: 'token' } },
155
+ },
156
+ ],
157
+ edges: [{ id: 'e1', source: 'echo', target: 'result' }],
158
+ createdAt: '2026-01-01T00:00:00Z',
159
+ updatedAt: '2026-01-01T00:00:00Z',
160
+ }),
161
+ );
162
+ writeFileSync(
163
+ join(apisDir, 'leaky.scenarios.json'),
164
+ JSON.stringify([
165
+ // Deliberate body mismatch so the failure diff quotes the ACTUAL body
166
+ // (which contains the echoed secret) — it must arrive redacted.
167
+ { id: 's1', name: 'mismatch', input: {}, expect: { body: { token: 'not-the-secret' } } },
168
+ ]),
169
+ );
170
+
171
+ // broken: references an unregistered node type — startRun throws
172
+ // synchronously ("Invalid flow: Unknown node type: …"), the simplest
173
+ // reliable trigger for the route's unexpected-error path.
174
+ writeFileSync(
175
+ join(apisDir, 'broken.json'),
176
+ JSON.stringify({
177
+ id: 'broken',
178
+ name: 'Broken',
179
+ version: 1,
180
+ nodes: [{ id: 'x', type: 'NoSuchNodeType', label: 'X', position: { x: 0, y: 0 }, config: {} }],
181
+ edges: [],
182
+ createdAt: '2026-01-01T00:00:00Z',
183
+ updatedAt: '2026-01-01T00:00:00Z',
184
+ }),
185
+ );
186
+ writeFileSync(
187
+ join(apisDir, 'broken.scenarios.json'),
188
+ JSON.stringify([{ id: 's1', name: 'boom', input: {}, expect: { status: 200 } }]),
189
+ );
190
+
191
+ proc = bootRunner(PORT, { EMBERFLOW_PROJECT: projectDir });
192
+ await waitHealthy(`${base}/healthz`);
193
+ }, 20_000);
194
+
195
+ afterAll(async () => {
196
+ proc?.kill();
197
+ if (projectDir) rmSync(projectDir, { recursive: true, force: true });
198
+ });
199
+
200
+ describe('POST /workflows/:id/test', () => {
201
+ it('runs the op\'s scenarios and reports pass/fail/skip counts with failure strings', async () => {
202
+ const res = await postTest('authcheck');
203
+ expect(res.status).toBe(200);
204
+ const body = await res.json();
205
+ expect(body.passed).toBe(1);
206
+ expect(body.failed).toBe(1);
207
+ expect(body.skipped).toBe(1);
208
+ const failed = body.results.find((r: { scenario: string }) => r.scenario === 'expired');
209
+ expect(failed.status).toBe('failed');
210
+ expect(failed.failures.join(' ')).toContain('status: expected 401, got 200');
211
+ const passed = body.results.find((r: { scenario: string }) => r.scenario === 'authed-ok');
212
+ expect(passed.status).toBe('passed');
213
+ const skipped = body.results.find((r: { scenario: string }) => r.scenario === 'no-assertion');
214
+ expect(skipped.status).toBe('skipped');
215
+ });
216
+
217
+ it('only runs scenarios for the requested op (not every op in the project)', async () => {
218
+ const res = await postTest('authcheck');
219
+ const body = await res.json();
220
+ expect(body.results.every((r: { opId: string }) => r.opId === 'authcheck')).toBe(true);
221
+ });
222
+
223
+ it('redacts a secret echoed into a failing diff', async () => {
224
+ const res = await postTest('leaky');
225
+ expect(res.status).toBe(200);
226
+ const body = await res.json();
227
+ expect(body.failed).toBe(1);
228
+ const text = JSON.stringify(body);
229
+ expect(text).not.toContain('supersecretvalue');
230
+ expect(text).toContain('«secret:API_TOKEN»');
231
+ });
232
+
233
+ it('404s for an unknown op', async () => {
234
+ const res = await postTest('no-such-op');
235
+ expect(res.status).toBe(404);
236
+ const body = await res.json();
237
+ expect(body.error).toBeTruthy();
238
+ });
239
+
240
+ it('500s with a JSON error body on an unexpected throw (invalid flow: unregistered node type)', async () => {
241
+ const res = await postTest('broken');
242
+ expect(res.status).toBe(500);
243
+ expect(res.headers.get('content-type')).toContain('application/json');
244
+ const body = await res.json();
245
+ expect(body.error).toContain('Unknown node type');
246
+ });
247
+
248
+ it('400s for an unknown environment', async () => {
249
+ const res = await postTest('authcheck', { environment: 'no-such-env' });
250
+ expect(res.status).toBe(400);
251
+ const body = await res.json();
252
+ expect(body.error).toBeTruthy();
253
+ });
254
+
255
+ it('does not create a RunRegistry run — GET /runs/:id/events for an id returned mid-suite still 404s, and a pre-existing run stays the last one visible', async () => {
256
+ // Establish a real run via POST /runs (goes through RunRegistry) so we
257
+ // have a known-good runId to compare against.
258
+ const createRes = await fetch(`${base}/api/runs`, {
259
+ method: 'POST',
260
+ headers: { 'Content-Type': 'application/json' },
261
+ body: JSON.stringify({
262
+ flow: {
263
+ id: 'probe-flow',
264
+ name: 'Probe',
265
+ version: 1,
266
+ nodes: [{ id: 'input', type: 'Input', label: 'Input', position: { x: 0, y: 0 }, config: { fields: [] } }],
267
+ edges: [],
268
+ },
269
+ mode: 'step',
270
+ environment: 'local',
271
+ }),
272
+ });
273
+ expect(createRes.status).toBe(201);
274
+ const { runId } = await createRes.json();
275
+
276
+ // The probe run is visible via SSE replay.
277
+ const beforeEvents = await fetch(`${base}/api/runs/${runId}/events`);
278
+ expect(beforeEvents.status).toBe(200);
279
+ beforeEvents.body?.cancel();
280
+
281
+ // Run the test suite (which internally executes several scenario runs).
282
+ const testRes = await postTest('authcheck');
283
+ expect(testRes.status).toBe(200);
284
+
285
+ // The probe run's SSE stream is still exactly as reachable as before —
286
+ // no new run replaced or shadowed it, proving the suite's runs never
287
+ // registered with RunRegistry.
288
+ const afterEvents = await fetch(`${base}/api/runs/${runId}/events`);
289
+ expect(afterEvents.status).toBe(200);
290
+ afterEvents.body?.cancel();
291
+
292
+ // And a bogus/random run id (representing "any run the suite might have
293
+ // created") is not found — RunRegistry has no record of it.
294
+ const bogusEvents = await fetch(`${base}/api/runs/definitely-not-a-real-run-id/events`);
295
+ expect(bogusEvents.status).toBe(404);
296
+ });
297
+ });