@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.
- package/LICENSE +21 -0
- package/README.md +153 -0
- package/bin/commands.test.ts +185 -0
- package/bin/commands.ts +306 -0
- package/bin/emberflow.mjs +38 -0
- package/bin/init.test.ts +165 -0
- package/bin/init.ts +297 -0
- package/bin/runtime.mjs +105 -0
- package/bin/runtime.test.ts +167 -0
- package/bin/skillTargets.test.ts +108 -0
- package/bin/skillTargets.ts +36 -0
- package/bin/skillTemplates.test.ts +37 -0
- package/bin/tsconfig.json +23 -0
- package/dist/bin/commands.d.ts +24 -0
- package/dist/bin/commands.js +252 -0
- package/dist/bin/init.d.ts +17 -0
- package/dist/bin/init.js +241 -0
- package/dist/bin/runtime.d.mts +39 -0
- package/dist/bin/runtime.mjs +96 -0
- package/dist/bin/skillTargets.d.ts +13 -0
- package/dist/bin/skillTargets.js +22 -0
- package/dist/server/agents/claudeAdapter.d.ts +8 -0
- package/dist/server/agents/claudeAdapter.js +190 -0
- package/dist/server/agents/claudeParse.d.ts +2 -0
- package/dist/server/agents/claudeParse.js +162 -0
- package/dist/server/agents/codexAdapter.d.ts +13 -0
- package/dist/server/agents/codexAdapter.js +174 -0
- package/dist/server/agents/codexParse.d.ts +2 -0
- package/dist/server/agents/codexParse.js +74 -0
- package/dist/server/agents/detect.d.ts +23 -0
- package/dist/server/agents/detect.js +35 -0
- package/dist/server/agents/gitScope.d.ts +24 -0
- package/dist/server/agents/gitScope.js +95 -0
- package/dist/server/agents/modelRejectionHint.d.ts +8 -0
- package/dist/server/agents/modelRejectionHint.js +12 -0
- package/dist/server/agents/prompt.d.ts +79 -0
- package/dist/server/agents/prompt.js +364 -0
- package/dist/server/agents/runManager.d.ts +90 -0
- package/dist/server/agents/runManager.js +259 -0
- package/dist/server/agents/types.d.ts +15 -0
- package/dist/server/agents/types.js +1 -0
- package/dist/server/apiStore.d.ts +60 -0
- package/dist/server/apiStore.js +249 -0
- package/dist/server/auth/enforce.d.ts +1 -0
- package/dist/server/auth/enforce.js +6 -0
- package/dist/server/auth/types.d.ts +1 -0
- package/dist/server/auth/types.js +1 -0
- package/dist/server/auth/verifiers.d.ts +1 -0
- package/dist/server/auth/verifiers.js +4 -0
- package/dist/server/authAttach.d.ts +19 -0
- package/dist/server/authAttach.js +79 -0
- package/dist/server/cli.d.ts +7 -0
- package/dist/server/cli.js +804 -0
- package/dist/server/client.d.ts +130 -0
- package/dist/server/client.js +247 -0
- package/dist/server/environments.d.ts +103 -0
- package/dist/server/environments.js +486 -0
- package/dist/server/flowStore.d.ts +37 -0
- package/dist/server/flowStore.js +117 -0
- package/dist/server/httpOperations.d.ts +32 -0
- package/dist/server/httpOperations.js +62 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +1022 -0
- package/dist/server/infrastructure.d.ts +51 -0
- package/dist/server/infrastructure.js +116 -0
- package/dist/server/login.d.ts +21 -0
- package/dist/server/login.js +44 -0
- package/dist/server/mcp.d.ts +1 -0
- package/dist/server/mcp.js +182 -0
- package/dist/server/migrateFlows.d.ts +8 -0
- package/dist/server/migrateFlows.js +25 -0
- package/dist/server/mockHandler.d.ts +15 -0
- package/dist/server/mockHandler.js +35 -0
- package/dist/server/nodesPayload.d.ts +14 -0
- package/dist/server/nodesPayload.js +14 -0
- package/dist/server/normalizeFlow.d.ts +21 -0
- package/dist/server/normalizeFlow.js +48 -0
- package/dist/server/openBrowser.d.ts +8 -0
- package/dist/server/openBrowser.js +16 -0
- package/dist/server/operationResult.d.ts +12 -0
- package/dist/server/operationResult.js +26 -0
- package/dist/server/pathGuard.d.ts +1 -0
- package/dist/server/pathGuard.js +16 -0
- package/dist/server/pathSafety.d.ts +20 -0
- package/dist/server/pathSafety.js +35 -0
- package/dist/server/projectConfig.d.ts +47 -0
- package/dist/server/projectConfig.js +61 -0
- package/dist/server/projectMode.d.ts +26 -0
- package/dist/server/projectMode.js +44 -0
- package/dist/server/redact.d.ts +6 -0
- package/dist/server/redact.js +37 -0
- package/dist/server/runRegistry.d.ts +84 -0
- package/dist/server/runRegistry.js +200 -0
- package/dist/server/runtime/RuntimeAdapter.d.ts +22 -0
- package/dist/server/runtime/RuntimeAdapter.js +1 -0
- package/dist/server/runtime/expressAdapter.d.ts +19 -0
- package/dist/server/runtime/expressAdapter.js +56 -0
- package/dist/server/scenarioTest.d.ts +14 -0
- package/dist/server/scenarioTest.js +66 -0
- package/dist/server/subflowRunner.d.ts +32 -0
- package/dist/server/subflowRunner.js +72 -0
- package/dist/server/testRunner.d.ts +70 -0
- package/dist/server/testRunner.js +151 -0
- package/dist/src/config.d.ts +3 -0
- package/dist/src/config.js +2 -0
- package/dist/src/engine/authVerify.d.ts +33 -0
- package/dist/src/engine/authVerify.js +63 -0
- package/dist/src/engine/diagnostics.d.ts +62 -0
- package/dist/src/engine/diagnostics.js +148 -0
- package/dist/src/engine/executor.d.ts +151 -0
- package/dist/src/engine/executor.js +684 -0
- package/dist/src/engine/httpError.d.ts +7 -0
- package/dist/src/engine/httpError.js +14 -0
- package/dist/src/engine/index.d.ts +11 -0
- package/dist/src/engine/index.js +11 -0
- package/dist/src/engine/output.d.ts +11 -0
- package/dist/src/engine/output.js +20 -0
- package/dist/src/engine/publish.d.ts +30 -0
- package/dist/src/engine/publish.js +95 -0
- package/dist/src/engine/registry.d.ts +34 -0
- package/dist/src/engine/registry.js +67 -0
- package/dist/src/engine/scenarios.d.ts +12 -0
- package/dist/src/engine/scenarios.js +19 -0
- package/dist/src/engine/schemaCheck.d.ts +4 -0
- package/dist/src/engine/schemaCheck.js +34 -0
- package/dist/src/engine/trace.d.ts +12 -0
- package/dist/src/engine/trace.js +12 -0
- package/dist/src/engine/types.d.ts +308 -0
- package/dist/src/engine/types.js +1 -0
- package/dist/src/engine/validation.d.ts +26 -0
- package/dist/src/engine/validation.js +318 -0
- package/dist/src/flows/anomaly-flows.d.ts +23 -0
- package/dist/src/flows/anomaly-flows.js +151 -0
- package/dist/src/flows/city-sweep-flow.d.ts +10 -0
- package/dist/src/flows/city-sweep-flow.js +127 -0
- package/dist/src/flows/login-flow.d.ts +13 -0
- package/dist/src/flows/login-flow.js +145 -0
- package/dist/src/flows/pradar-flows.d.ts +5 -0
- package/dist/src/flows/pradar-flows.js +375 -0
- package/dist/src/flows/weather-flow.d.ts +12 -0
- package/dist/src/flows/weather-flow.js +150 -0
- package/dist/src/lib/pathParams.d.ts +3 -0
- package/dist/src/lib/pathParams.js +6 -0
- package/dist/src/nodes/anomaly.d.ts +2 -0
- package/dist/src/nodes/anomaly.js +556 -0
- package/dist/src/nodes/flow-control.d.ts +7 -0
- package/dist/src/nodes/flow-control.js +270 -0
- package/dist/src/nodes/index.d.ts +3 -0
- package/dist/src/nodes/index.js +20 -0
- package/dist/src/nodes/login.d.ts +7 -0
- package/dist/src/nodes/login.js +158 -0
- package/dist/src/nodes/loops.d.ts +11 -0
- package/dist/src/nodes/loops.js +60 -0
- package/dist/src/nodes/pradar/index.d.ts +7 -0
- package/dist/src/nodes/pradar/index.js +9 -0
- package/dist/src/nodes/pradar/logic.d.ts +189 -0
- package/dist/src/nodes/pradar/logic.js +370 -0
- package/dist/src/nodes/pradar/nodes.d.ts +42 -0
- package/dist/src/nodes/pradar/nodes.js +801 -0
- package/dist/src/nodes/requireAuth.d.ts +20 -0
- package/dist/src/nodes/requireAuth.js +48 -0
- package/dist/src/nodes/response.d.ts +8 -0
- package/dist/src/nodes/response.js +33 -0
- package/dist/src/nodes/weather.d.ts +33 -0
- package/dist/src/nodes/weather.js +260 -0
- package/package.json +92 -0
- package/server/agentRoute.test.ts +157 -0
- package/server/agents/__fixtures__/fake-claude-writer.mjs +23 -0
- package/server/agents/__fixtures__/fake-claude.mjs +55 -0
- package/server/agents/__fixtures__/fake-codex-writer.mjs +27 -0
- package/server/agents/__fixtures__/fake-codex.mjs +52 -0
- package/server/agents/__fixtures__/fake-version-empty.mjs +3 -0
- package/server/agents/__fixtures__/fake-version-fail.mjs +4 -0
- package/server/agents/__fixtures__/fake-version-garbage.mjs +4 -0
- package/server/agents/__fixtures__/fake-version-good.mjs +4 -0
- package/server/agents/__fixtures__/fake-version-hang.mjs +3 -0
- package/server/agents/claudeAdapter.test.ts +127 -0
- package/server/agents/claudeAdapter.ts +212 -0
- package/server/agents/claudeParse.ts +193 -0
- package/server/agents/codexAdapter.test.ts +125 -0
- package/server/agents/codexAdapter.ts +198 -0
- package/server/agents/codexParse.test.ts +58 -0
- package/server/agents/codexParse.ts +126 -0
- package/server/agents/detect.test.ts +65 -0
- package/server/agents/detect.ts +43 -0
- package/server/agents/gitScope.test.ts +129 -0
- package/server/agents/gitScope.ts +110 -0
- package/server/agents/modelRejectionHint.test.ts +30 -0
- package/server/agents/modelRejectionHint.ts +14 -0
- package/server/agents/prompt.test.ts +613 -0
- package/server/agents/prompt.ts +527 -0
- package/server/agents/runManager.test.ts +251 -0
- package/server/agents/runManager.ts +286 -0
- package/server/agents/types.ts +17 -0
- package/server/apiMount.test.ts +591 -0
- package/server/apiStore.test.ts +324 -0
- package/server/apiStore.ts +241 -0
- package/server/auth/enforce.test.ts +61 -0
- package/server/auth/enforce.ts +6 -0
- package/server/auth/types.ts +5 -0
- package/server/auth/verifiers.test.ts +37 -0
- package/server/auth/verifiers.ts +9 -0
- package/server/authAttach.test.ts +175 -0
- package/server/authAttach.ts +99 -0
- package/server/cli.ts +847 -0
- package/server/cliAuth.test.ts +229 -0
- package/server/cliCreate.test.ts +131 -0
- package/server/cliDoctor.test.ts +326 -0
- package/server/cliTest.test.ts +735 -0
- package/server/client.ts +314 -0
- package/server/demoProject.test.ts +29 -0
- package/server/diagnosticsRoute.test.ts +169 -0
- package/server/environments.test.ts +620 -0
- package/server/environments.ts +603 -0
- package/server/environmentsRoute.test.ts +385 -0
- package/server/errorWorkflow.test.ts +205 -0
- package/server/flowStore.test.ts +84 -0
- package/server/flowStore.ts +125 -0
- package/server/httpOperations.test.ts +377 -0
- package/server/httpOperations.ts +96 -0
- package/server/index.ts +1111 -0
- package/server/infrastructure.test.ts +154 -0
- package/server/infrastructure.ts +161 -0
- package/server/infrastructureRoute.test.ts +105 -0
- package/server/login.test.ts +153 -0
- package/server/login.ts +67 -0
- package/server/mcp.ts +278 -0
- package/server/migrateFlows.test.ts +55 -0
- package/server/migrateFlows.ts +23 -0
- package/server/mockHandler.test.ts +138 -0
- package/server/mockHandler.ts +52 -0
- package/server/mockRun.test.ts +367 -0
- package/server/mockServe.test.ts +467 -0
- package/server/nodesPayload.test.ts +29 -0
- package/server/nodesPayload.ts +26 -0
- package/server/normalizeFlow.test.ts +80 -0
- package/server/normalizeFlow.ts +59 -0
- package/server/openBrowser.test.ts +20 -0
- package/server/openBrowser.ts +22 -0
- package/server/operationResult.test.ts +67 -0
- package/server/operationResult.ts +27 -0
- package/server/pathGuard.test.ts +19 -0
- package/server/pathGuard.ts +12 -0
- package/server/pathSafety.test.ts +38 -0
- package/server/pathSafety.ts +33 -0
- package/server/projectConfig.test.ts +67 -0
- package/server/projectConfig.ts +101 -0
- package/server/projectMode.test.ts +106 -0
- package/server/projectMode.ts +59 -0
- package/server/redact.test.ts +88 -0
- package/server/redact.ts +35 -0
- package/server/redactEgress.test.ts +281 -0
- package/server/runRegistry.ts +270 -0
- package/server/runsAuth.test.ts +119 -0
- package/server/runtime/RuntimeAdapter.ts +24 -0
- package/server/runtime/expressAdapter.test.ts +38 -0
- package/server/runtime/expressAdapter.ts +70 -0
- package/server/scenarioTest.test.ts +148 -0
- package/server/scenarioTest.ts +88 -0
- package/server/setupStatus.test.ts +127 -0
- package/server/subflowRunner.ts +104 -0
- package/server/testRunner.ts +216 -0
- package/server/tsconfig.json +20 -0
- package/server/workflowTestRoute.test.ts +297 -0
- package/src/App.tsx +233 -0
- package/src/components/AgentConsole.tsx +461 -0
- package/src/components/BranchRulesEditor.tsx +156 -0
- package/src/components/CreateModal.tsx +275 -0
- package/src/components/Dock.tsx +487 -0
- package/src/components/EnvironmentDialog.tsx +569 -0
- package/src/components/EnvironmentPicker.tsx +357 -0
- package/src/components/EnvironmentsDialog.tsx +144 -0
- package/src/components/ExecutionPager.tsx +67 -0
- package/src/components/InfraPanel.test.tsx +103 -0
- package/src/components/InfraPanel.tsx +171 -0
- package/src/components/Inspector.tsx +1182 -0
- package/src/components/Json.tsx +50 -0
- package/src/components/JsonModal.tsx +52 -0
- package/src/components/LogMessage.tsx +61 -0
- package/src/components/NodeRunModal.tsx +213 -0
- package/src/components/RunLogPanel.tsx +397 -0
- package/src/components/RunbookView.tsx +1085 -0
- package/src/components/Scenarios.tsx +476 -0
- package/src/components/SettingsDialog.tsx +229 -0
- package/src/components/Sidebar.tsx +436 -0
- package/src/components/StatusBar.tsx +302 -0
- package/src/components/Toolbar.tsx +663 -0
- package/src/components/WelcomeDialog.test.tsx +107 -0
- package/src/components/WelcomeDialog.tsx +412 -0
- package/src/components/agentMarkdown.test.tsx +91 -0
- package/src/components/agentMarkdown.tsx +158 -0
- package/src/components/ui/badge.tsx +31 -0
- package/src/components/ui/button.tsx +46 -0
- package/src/components/ui/combobox.tsx +114 -0
- package/src/components/ui/command.tsx +82 -0
- package/src/components/ui/dialog.tsx +73 -0
- package/src/components/ui/input.tsx +20 -0
- package/src/components/ui/popover.tsx +32 -0
- package/src/components/ui/resizable.tsx +44 -0
- package/src/components/ui/tabs.tsx +36 -0
- package/src/config.ts +4 -0
- package/src/engine/authVerify.ts +81 -0
- package/src/engine/diagnostics.test.ts +437 -0
- package/src/engine/diagnostics.ts +211 -0
- package/src/engine/executor.test.ts +1339 -0
- package/src/engine/executor.ts +840 -0
- package/src/engine/httpError.test.ts +12 -0
- package/src/engine/httpError.ts +16 -0
- package/src/engine/index.ts +11 -0
- package/src/engine/output.ts +21 -0
- package/src/engine/publish.test.ts +176 -0
- package/src/engine/publish.ts +127 -0
- package/src/engine/registry.test.ts +64 -0
- package/src/engine/registry.ts +80 -0
- package/src/engine/scenarios.test.ts +55 -0
- package/src/engine/scenarios.ts +29 -0
- package/src/engine/schemaCheck.test.ts +21 -0
- package/src/engine/schemaCheck.ts +35 -0
- package/src/engine/trace.test.ts +19 -0
- package/src/engine/trace.ts +23 -0
- package/src/engine/types.test.ts +22 -0
- package/src/engine/types.ts +316 -0
- package/src/engine/validation.test.ts +314 -0
- package/src/engine/validation.ts +354 -0
- package/src/flows/anomaly-flows.test.ts +81 -0
- package/src/flows/anomaly-flows.ts +202 -0
- package/src/flows/city-sweep-flow.ts +131 -0
- package/src/flows/flows.test.ts +30 -0
- package/src/flows/login-flow.ts +150 -0
- package/src/flows/pradar-flows.test.ts +204 -0
- package/src/flows/pradar-flows.ts +419 -0
- package/src/flows/weather-flow.ts +154 -0
- package/src/globals.css +147 -0
- package/src/lib/pathParams.test.ts +24 -0
- package/src/lib/pathParams.ts +6 -0
- package/src/lib/registerLens.test.ts +79 -0
- package/src/lib/registerLens.ts +59 -0
- package/src/lib/runbookModel.test.ts +61 -0
- package/src/lib/runbookModel.ts +290 -0
- package/src/lib/runbookProjection.test.ts +417 -0
- package/src/lib/runbookProjection.ts +254 -0
- package/src/lib/useTailAnchor.ts +53 -0
- package/src/lib/utils.ts +7 -0
- package/src/main.tsx +13 -0
- package/src/nodes/anomaly.test.ts +98 -0
- package/src/nodes/anomaly.ts +637 -0
- package/src/nodes/effects-scan.test.ts +44 -0
- package/src/nodes/flow-control.test.ts +280 -0
- package/src/nodes/flow-control.ts +305 -0
- package/src/nodes/index.ts +22 -0
- package/src/nodes/login.test.ts +81 -0
- package/src/nodes/login.ts +186 -0
- package/src/nodes/loops.ts +69 -0
- package/src/nodes/pradar/index.ts +11 -0
- package/src/nodes/pradar/logic.test.ts +288 -0
- package/src/nodes/pradar/logic.ts +582 -0
- package/src/nodes/pradar/nodes.test.ts +146 -0
- package/src/nodes/pradar/nodes.ts +955 -0
- package/src/nodes/requireAuth.test.ts +75 -0
- package/src/nodes/requireAuth.ts +55 -0
- package/src/nodes/response.test.ts +33 -0
- package/src/nodes/response.ts +39 -0
- package/src/nodes/traceDetail.test.ts +39 -0
- package/src/nodes/weather.test.ts +65 -0
- package/src/nodes/weather.ts +306 -0
- package/src/store/agentClient.test.ts +103 -0
- package/src/store/agentClient.ts +131 -0
- package/src/store/apiTree.test.ts +17 -0
- package/src/store/apiTree.ts +73 -0
- package/src/store/builderStore.test.ts +1234 -0
- package/src/store/builderStore.ts +1879 -0
- package/src/store/infraClient.ts +63 -0
- package/src/store/nodeMeta.test.ts +24 -0
- package/src/store/nodeMeta.ts +21 -0
- package/src/store/persistence.test.ts +15 -0
- package/src/store/persistence.ts +54 -0
- package/src/store/serverRunner.ts +381 -0
- package/src/store/setupClient.ts +57 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
- package/studio-dist/assets/index-DNJwW-hM.css +1 -0
- package/studio-dist/assets/index-O26dKRjW.js +65 -0
- package/studio-dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/studio-dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/studio-dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/studio-dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/studio-dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/studio-dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/studio-dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/studio-dist/favicon.svg +1 -0
- package/studio-dist/icons.svg +24 -0
- package/studio-dist/index.html +14 -0
- package/templates/skills/emberflow-basics/SKILL.md +316 -0
- package/templates/skills/emberflow-model-process/SKILL.md +209 -0
- package/templates/skills/emberflow-new-workflow/SKILL.md +211 -0
- 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
|
+
});
|