@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
package/server/mcp.ts
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import {
|
|
5
|
+
findScenario,
|
|
6
|
+
publishFlow,
|
|
7
|
+
scenarioNames,
|
|
8
|
+
validateFlow,
|
|
9
|
+
type WorkflowDefinition,
|
|
10
|
+
} from '../src/engine';
|
|
11
|
+
import { createDefaultRegistry } from '../src/nodes';
|
|
12
|
+
import * as client from './client';
|
|
13
|
+
|
|
14
|
+
/** Wraps any value as an MCP text-content result. */
|
|
15
|
+
function json(value: unknown, isError = false): { content: { type: 'text'; text: string }[]; isError?: boolean } {
|
|
16
|
+
return {
|
|
17
|
+
content: [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
|
18
|
+
...(isError ? { isError: true } : {}),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Runs a handler, turning any thrown error into an isError text result. */
|
|
23
|
+
async function guard(
|
|
24
|
+
fn: () => Promise<ReturnType<typeof json>>,
|
|
25
|
+
): Promise<ReturnType<typeof json>> {
|
|
26
|
+
try {
|
|
27
|
+
return await fn();
|
|
28
|
+
} catch (err) {
|
|
29
|
+
return json({ error: err instanceof Error ? err.message : String(err) }, true);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const server = new McpServer({ name: 'emberflow', version: '0.0.0' });
|
|
34
|
+
|
|
35
|
+
server.tool('list_nodes', 'List all registered node types', {}, () =>
|
|
36
|
+
guard(async () => {
|
|
37
|
+
const registry = createDefaultRegistry();
|
|
38
|
+
return json(
|
|
39
|
+
registry.list().map((d) => ({
|
|
40
|
+
type: d.type,
|
|
41
|
+
label: d.label,
|
|
42
|
+
category: d.category,
|
|
43
|
+
description: d.description,
|
|
44
|
+
})),
|
|
45
|
+
);
|
|
46
|
+
}),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
server.tool(
|
|
50
|
+
'get_node_schema',
|
|
51
|
+
'Get the full NodeDefinition for a node type',
|
|
52
|
+
{ type: z.string() },
|
|
53
|
+
({ type }) =>
|
|
54
|
+
guard(async () => {
|
|
55
|
+
const registry = createDefaultRegistry();
|
|
56
|
+
if (!registry.has(type)) return json({ error: `Unknown node type: ${type}` }, true);
|
|
57
|
+
return json(registry.get(type).definition);
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
server.tool(
|
|
62
|
+
'list_operations',
|
|
63
|
+
'List every operation the runner knows, annotated with its API location. Each entry: ' +
|
|
64
|
+
'{ id, name, path, folder, http }. "path" is the apis/ tree location (api/…folders/op) ' +
|
|
65
|
+
'and equals the id. "http" is the HTTP trigger { method, path } for operations exposed as ' +
|
|
66
|
+
'endpoints — an HTTP operation runs as Input({ params, query, body, headers }) → ' +
|
|
67
|
+
'Response({ status, body }); "http" is null for internal sub-flow operations invoked by ' +
|
|
68
|
+
'other operations rather than over HTTP.',
|
|
69
|
+
{},
|
|
70
|
+
() =>
|
|
71
|
+
guard(async () => {
|
|
72
|
+
const ops = await client.listOperations();
|
|
73
|
+
return json(
|
|
74
|
+
ops.map((op) => ({
|
|
75
|
+
id: op.id,
|
|
76
|
+
name: op.name,
|
|
77
|
+
path: op.path,
|
|
78
|
+
folder: op.path.split('/')[0],
|
|
79
|
+
http: op.http ? { method: op.http.method, path: op.http.path } : null,
|
|
80
|
+
})),
|
|
81
|
+
);
|
|
82
|
+
}),
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
server.tool(
|
|
86
|
+
'list_apis',
|
|
87
|
+
'Return the API tree: top-level APIs → folders → operations (each op carries its ' +
|
|
88
|
+
'id, name, path, and http trigger, if any). Built from the operations\' apis/ paths so an ' +
|
|
89
|
+
'agent can see how operations are organised across APIs and which are HTTP endpoints ' +
|
|
90
|
+
'vs internal sub-flows.',
|
|
91
|
+
{},
|
|
92
|
+
() => guard(async () => json(await client.apiTree())),
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
server.tool(
|
|
96
|
+
'get_operation',
|
|
97
|
+
"Get an operation's full JSON by id (its nodes, edges, http trigger + auth, scenarios). " +
|
|
98
|
+
'The id equals the operation\'s apis/ path.',
|
|
99
|
+
{ id: z.string() },
|
|
100
|
+
({ id }) =>
|
|
101
|
+
guard(async () => {
|
|
102
|
+
const flow = await client.getWorkflow(id);
|
|
103
|
+
if (!flow) return json({ error: `Unknown operation: ${id}` }, true);
|
|
104
|
+
return json(flow);
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
server.tool(
|
|
109
|
+
'create_operation',
|
|
110
|
+
'Create a brand-new operation at an explicit apis/ path via POST /operations. "path" is the ' +
|
|
111
|
+
'apis/ tree location (api/…folders/op) and MUST equal flow.id — the runner rejects a ' +
|
|
112
|
+
'mismatch and 409s if an operation already exists at that path (use save_operation to ' +
|
|
113
|
+
'update an existing one). Set flow.http = { method, path[, auth] } to expose it as an HTTP ' +
|
|
114
|
+
'endpoint (Input({ params, query, body, headers }) → Response({ status, body })); omit http ' +
|
|
115
|
+
'for an internal sub-flow.',
|
|
116
|
+
{
|
|
117
|
+
flow: z.record(z.string(), z.unknown()),
|
|
118
|
+
path: z.string(),
|
|
119
|
+
},
|
|
120
|
+
({ flow, path }) =>
|
|
121
|
+
guard(async () => {
|
|
122
|
+
const definition = flow as unknown as WorkflowDefinition;
|
|
123
|
+
const registry = createDefaultRegistry();
|
|
124
|
+
const issues = validateFlow(definition, registry);
|
|
125
|
+
if (issues.some((i) => i.severity === 'error')) return json({ issues }, true);
|
|
126
|
+
const result = await client.createOperation(definition, path);
|
|
127
|
+
if (!result.ok) return json({ error: result.error }, true);
|
|
128
|
+
return json({ ok: true, id: definition.id, path });
|
|
129
|
+
}),
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
server.tool(
|
|
133
|
+
'delete_operation',
|
|
134
|
+
'Delete an operation by id (its apis/ tree path, e.g. "billing/charge"). Removes the ' +
|
|
135
|
+
'operation file and its scenarios sidecar from the runner. Prefer this over editing files ' +
|
|
136
|
+
'for removals — a deleted op is unmounted immediately. Returns { ok } or an error if the id ' +
|
|
137
|
+
'is unknown.',
|
|
138
|
+
{ id: z.string() },
|
|
139
|
+
({ id }) =>
|
|
140
|
+
guard(async () => {
|
|
141
|
+
const result = await client.deleteOperation(id);
|
|
142
|
+
if (!result.ok) return json({ error: result.error }, true);
|
|
143
|
+
return json({ ok: true, id });
|
|
144
|
+
}),
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
server.tool(
|
|
148
|
+
'save_operation',
|
|
149
|
+
'Validate then save an existing operation to the runner (rejects operations with ' +
|
|
150
|
+
'error-severity issues). Persists the whole flow including its http trigger ' +
|
|
151
|
+
'({ method, path, auth }) and auth fields. Use create_operation for a brand-new op.',
|
|
152
|
+
{ flow: z.record(z.string(), z.unknown()) },
|
|
153
|
+
({ flow }) =>
|
|
154
|
+
guard(async () => {
|
|
155
|
+
const definition = flow as unknown as WorkflowDefinition;
|
|
156
|
+
const registry = createDefaultRegistry();
|
|
157
|
+
const issues = validateFlow(definition, registry);
|
|
158
|
+
if (issues.some((i) => i.severity === 'error')) return json({ issues }, true);
|
|
159
|
+
await client.saveWorkflow(definition);
|
|
160
|
+
return json({ ok: true, id: definition.id });
|
|
161
|
+
}),
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
server.tool(
|
|
165
|
+
'validate_operation',
|
|
166
|
+
'Validate an operation flow and return its issues (does not save).',
|
|
167
|
+
{ flow: z.record(z.string(), z.unknown()) },
|
|
168
|
+
({ flow }) =>
|
|
169
|
+
guard(async () => {
|
|
170
|
+
const registry = createDefaultRegistry();
|
|
171
|
+
const issues = validateFlow(flow as unknown as WorkflowDefinition, registry);
|
|
172
|
+
const valid = !issues.some((i) => i.severity === 'error');
|
|
173
|
+
return json({ valid, issues });
|
|
174
|
+
}),
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
server.tool(
|
|
178
|
+
'run_operation',
|
|
179
|
+
'Run an operation to completion by id or inline flow; returns status, nodeStates, logs. ' +
|
|
180
|
+
'Accepts either an explicit "input" payload or a "scenario" name/id naming a test input ' +
|
|
181
|
+
'embedded in the operation. For an HTTP operation the input is the request shape ' +
|
|
182
|
+
'{ params, query, body, headers } and the run resolves a Response({ status, body }). ' +
|
|
183
|
+
'"environment" selects a named environment from emberflow.environments.json (default: the ' +
|
|
184
|
+
"runner's default environment). \"safeMode\" overrides safe mode (default: the environment's " +
|
|
185
|
+
'default — protected environments default to safe). Disabling safe mode on a protected ' +
|
|
186
|
+
'environment additionally requires "confirm" to equal "environment" exactly, or the run is ' +
|
|
187
|
+
'rejected.',
|
|
188
|
+
{
|
|
189
|
+
id: z.string().optional(),
|
|
190
|
+
flow: z.record(z.string(), z.unknown()).optional(),
|
|
191
|
+
input: z.record(z.string(), z.unknown()).optional(),
|
|
192
|
+
scenario: z.string().optional(),
|
|
193
|
+
environment: z.string().optional(),
|
|
194
|
+
safeMode: z.boolean().optional(),
|
|
195
|
+
confirm: z.string().optional(),
|
|
196
|
+
},
|
|
197
|
+
({ id, flow, input, scenario, environment, safeMode, confirm }) =>
|
|
198
|
+
guard(async () => {
|
|
199
|
+
let definition: WorkflowDefinition | undefined;
|
|
200
|
+
if (flow) {
|
|
201
|
+
definition = flow as unknown as WorkflowDefinition;
|
|
202
|
+
} else if (id) {
|
|
203
|
+
definition = await client.getWorkflow(id);
|
|
204
|
+
if (!definition) return json({ error: `Unknown operation: ${id}` }, true);
|
|
205
|
+
} else {
|
|
206
|
+
return json({ error: 'run_operation requires either "id" or "flow"' }, true);
|
|
207
|
+
}
|
|
208
|
+
if (scenario && input) {
|
|
209
|
+
return json({ error: 'run_operation accepts either "scenario" or "input", not both' }, true);
|
|
210
|
+
}
|
|
211
|
+
let resolvedInput = input;
|
|
212
|
+
if (scenario) {
|
|
213
|
+
const found = findScenario(definition, scenario);
|
|
214
|
+
if (!found) {
|
|
215
|
+
const names = scenarioNames(definition);
|
|
216
|
+
return json(
|
|
217
|
+
{ error: `Unknown scenario: "${scenario}". Available: ${names.length ? names.join(', ') : 'none'}` },
|
|
218
|
+
true,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
resolvedInput = found.input;
|
|
222
|
+
}
|
|
223
|
+
const runId = await client.startRun({
|
|
224
|
+
flow: definition,
|
|
225
|
+
input: resolvedInput,
|
|
226
|
+
environment,
|
|
227
|
+
safeMode,
|
|
228
|
+
confirm,
|
|
229
|
+
});
|
|
230
|
+
const { run, logs } = await client.waitForRun(runId);
|
|
231
|
+
return json(
|
|
232
|
+
{ status: run.status, environment: run.environment, safeMode: run.safeMode, nodeStates: run.nodeStates, logs },
|
|
233
|
+
run.status !== 'succeeded',
|
|
234
|
+
);
|
|
235
|
+
}),
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
server.tool(
|
|
239
|
+
'list_environments',
|
|
240
|
+
'List the named environments the runner offers (from emberflow.environments.json): each ' +
|
|
241
|
+
'entry is { name, protected, varKeys, secretKeys } — only key NAMES are returned, never ' +
|
|
242
|
+
'values/secrets. Also returns defaultEnvironment. Use a name as run_operation\'s ' +
|
|
243
|
+
'"environment". Protected environments default to safe mode.',
|
|
244
|
+
{},
|
|
245
|
+
() => guard(async () => json(await client.listEnvironments())),
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
server.tool(
|
|
249
|
+
'publish_operation',
|
|
250
|
+
'Publish an operation by id into a sealed emberflow/v1 artifact (validates, strips pins, ' +
|
|
251
|
+
'hashes node implementations); does not write any files.',
|
|
252
|
+
{ id: z.string() },
|
|
253
|
+
({ id }) =>
|
|
254
|
+
guard(async () => {
|
|
255
|
+
const flow = await client.getWorkflow(id);
|
|
256
|
+
if (!flow) return json({ error: `Unknown operation: ${id}` }, true);
|
|
257
|
+
const registry = createDefaultRegistry();
|
|
258
|
+
const artifact = await publishFlow(flow, registry);
|
|
259
|
+
return json(artifact);
|
|
260
|
+
}),
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
server.tool(
|
|
264
|
+
'get_node_samples',
|
|
265
|
+
'Get recorded execution samples for a node',
|
|
266
|
+
{ nodeId: z.string() },
|
|
267
|
+
({ nodeId }) => guard(async () => json(await client.samples(nodeId))),
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
async function main(): Promise<void> {
|
|
271
|
+
const transport = new StdioServerTransport();
|
|
272
|
+
await server.connect(transport);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
main().catch((err: unknown) => {
|
|
276
|
+
process.stderr.write(`${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
|
|
277
|
+
process.exit(1);
|
|
278
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync, existsSync, readFileSync, rmSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { migrateFlowsToApis } from './migrateFlows';
|
|
6
|
+
|
|
7
|
+
let ef: string;
|
|
8
|
+
let flowsDir: string;
|
|
9
|
+
let apisDir: string;
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
ef = join(tmpdir(), `mig-${Math.random().toString(36).slice(2)}`);
|
|
12
|
+
flowsDir = join(ef, 'flows');
|
|
13
|
+
apisDir = join(ef, 'apis');
|
|
14
|
+
mkdirSync(flowsDir, { recursive: true });
|
|
15
|
+
writeFileSync(join(flowsDir, 'triage.json'), JSON.stringify({ id: 'triage', name: 'Triage', nodes: [], edges: [] }));
|
|
16
|
+
writeFileSync(join(flowsDir, 'triage.scenarios.json'), JSON.stringify([{ name: 'x', input: {} }]));
|
|
17
|
+
});
|
|
18
|
+
afterEach(() => rmSync(ef, { recursive: true, force: true }));
|
|
19
|
+
|
|
20
|
+
describe('migrateFlowsToApis', () => {
|
|
21
|
+
it('moves flat flows into apis/default/, keeping ids and sidecars', () => {
|
|
22
|
+
const { moved } = migrateFlowsToApis(flowsDir, apisDir);
|
|
23
|
+
expect(moved).toContain('triage');
|
|
24
|
+
expect(existsSync(join(apisDir, 'default', 'triage.json'))).toBe(true);
|
|
25
|
+
expect(existsSync(join(apisDir, 'default', 'triage.scenarios.json'))).toBe(true);
|
|
26
|
+
expect(JSON.parse(readFileSync(join(apisDir, 'default', 'triage.json'), 'utf8')).id).toBe('triage');
|
|
27
|
+
expect(existsSync(flowsDir)).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('is a no-op when apis/ already exists', () => {
|
|
31
|
+
mkdirSync(apisDir, { recursive: true });
|
|
32
|
+
const { moved } = migrateFlowsToApis(flowsDir, apisDir);
|
|
33
|
+
expect(moved).toEqual([]);
|
|
34
|
+
expect(existsSync(flowsDir)).toBe(true); // untouched
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('is a no-op when there is no flows/ dir', () => {
|
|
38
|
+
rmSync(flowsDir, { recursive: true });
|
|
39
|
+
expect(migrateFlowsToApis(flowsDir, apisDir).moved).toEqual([]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('migrates a custom-named flows directory (e.g. my-flows) the same way', () => {
|
|
43
|
+
const customFlowsDir = join(ef, 'my-flows');
|
|
44
|
+
mkdirSync(customFlowsDir, { recursive: true });
|
|
45
|
+
writeFileSync(
|
|
46
|
+
join(customFlowsDir, 'onboarding.json'),
|
|
47
|
+
JSON.stringify({ id: 'onboarding', name: 'Onboarding', nodes: [], edges: [] }),
|
|
48
|
+
);
|
|
49
|
+
const customApisDir = join(ef, 'apis');
|
|
50
|
+
const { moved } = migrateFlowsToApis(customFlowsDir, customApisDir);
|
|
51
|
+
expect(moved).toContain('onboarding');
|
|
52
|
+
expect(existsSync(join(customApisDir, 'default', 'onboarding.json'))).toBe(true);
|
|
53
|
+
expect(existsSync(customFlowsDir)).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, renameSync, rmdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
/** One-time relocation of the legacy flat flows layout into `<apisDir>/default/`.
|
|
5
|
+
* Ids and sidecars are preserved; subflow references (by id) stay valid.
|
|
6
|
+
* Idempotent: a no-op once `apisDir` exists or `flowsDir` is absent.
|
|
7
|
+
* `flowsDir` is the actual configured flows directory — callers must not
|
|
8
|
+
* assume it is literally named `flows` (project configs can rename it). */
|
|
9
|
+
export function migrateFlowsToApis(flowsDir: string, apisDir: string): { moved: string[] } {
|
|
10
|
+
if (existsSync(apisDir) || !existsSync(flowsDir)) return { moved: [] };
|
|
11
|
+
|
|
12
|
+
const target = join(apisDir, 'default');
|
|
13
|
+
mkdirSync(target, { recursive: true });
|
|
14
|
+
const moved: string[] = [];
|
|
15
|
+
for (const file of readdirSync(flowsDir)) {
|
|
16
|
+
renameSync(join(flowsDir, file), join(target, file));
|
|
17
|
+
if (file.endsWith('.json') && !file.endsWith('.scenarios.json')) {
|
|
18
|
+
moved.push(file.slice(0, -'.json'.length));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
try { rmdirSync(flowsDir); } catch { /* non-empty or already gone — leave it */ }
|
|
22
|
+
return { moved };
|
|
23
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { mockResponseFor } from './mockHandler';
|
|
3
|
+
import type { ScenarioDefinition, WorkflowDefinition } from '../src/engine';
|
|
4
|
+
|
|
5
|
+
function flow(overrides: Partial<WorkflowDefinition> = {}): WorkflowDefinition {
|
|
6
|
+
return {
|
|
7
|
+
id: 'flow-1',
|
|
8
|
+
name: 'Flow One',
|
|
9
|
+
version: 1,
|
|
10
|
+
nodes: [],
|
|
11
|
+
edges: [],
|
|
12
|
+
createdAt: '2020-01-01T00:00:00.000Z',
|
|
13
|
+
updatedAt: '2020-01-01T00:00:00.000Z',
|
|
14
|
+
...overrides,
|
|
15
|
+
} as WorkflowDefinition;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function scenario(overrides: Partial<ScenarioDefinition> = {}): ScenarioDefinition {
|
|
19
|
+
return {
|
|
20
|
+
id: 'scn-1',
|
|
21
|
+
name: 'Scenario One',
|
|
22
|
+
input: {},
|
|
23
|
+
...overrides,
|
|
24
|
+
} as ScenarioDefinition;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('mockResponseFor', () => {
|
|
28
|
+
it('defaults to the first scenario that has an expect, skipping expect-less ones', () => {
|
|
29
|
+
const scenarios = [
|
|
30
|
+
scenario({ id: 'a', name: 'A' }),
|
|
31
|
+
scenario({ id: 'b', name: 'B', expect: { status: 201, body: { ok: true } } }),
|
|
32
|
+
scenario({ id: 'c', name: 'C', expect: { status: 999, body: {} } }),
|
|
33
|
+
];
|
|
34
|
+
const result = mockResponseFor(flow(), scenarios, { headers: {}, query: {} });
|
|
35
|
+
expect(result).toEqual({ status: 201, body: { ok: true }, scenario: 'B' });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('selects by x-emberflow-scenario header (exact name match)', () => {
|
|
39
|
+
const scenarios = [
|
|
40
|
+
scenario({ id: 'a', name: 'A', expect: { status: 200, body: { which: 'a' } } }),
|
|
41
|
+
scenario({ id: 'b', name: 'B', expect: { status: 202, body: { which: 'b' } } }),
|
|
42
|
+
];
|
|
43
|
+
const result = mockResponseFor(flow(), scenarios, {
|
|
44
|
+
headers: { 'x-emberflow-scenario': 'B' },
|
|
45
|
+
query: {},
|
|
46
|
+
});
|
|
47
|
+
expect(result).toEqual({ status: 202, body: { which: 'b' }, scenario: 'B' });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('selects by __scenario query param when no header is present', () => {
|
|
51
|
+
const scenarios = [
|
|
52
|
+
scenario({ id: 'a', name: 'A', expect: { status: 200, body: { which: 'a' } } }),
|
|
53
|
+
scenario({ id: 'b', name: 'B', expect: { status: 202, body: { which: 'b' } } }),
|
|
54
|
+
];
|
|
55
|
+
const result = mockResponseFor(flow(), scenarios, {
|
|
56
|
+
headers: {},
|
|
57
|
+
query: { __scenario: 'A' },
|
|
58
|
+
});
|
|
59
|
+
expect(result).toEqual({ status: 200, body: { which: 'a' }, scenario: 'A' });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('prefers the header over the query param when both are present', () => {
|
|
63
|
+
const scenarios = [
|
|
64
|
+
scenario({ id: 'a', name: 'A', expect: { status: 200, body: { which: 'a' } } }),
|
|
65
|
+
scenario({ id: 'b', name: 'B', expect: { status: 202, body: { which: 'b' } } }),
|
|
66
|
+
];
|
|
67
|
+
const result = mockResponseFor(flow(), scenarios, {
|
|
68
|
+
headers: { 'x-emberflow-scenario': 'B' },
|
|
69
|
+
query: { __scenario: 'A' },
|
|
70
|
+
});
|
|
71
|
+
expect(result).toEqual({ status: 202, body: { which: 'b' }, scenario: 'B' });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('returns 404 for a named-but-unknown scenario (header)', () => {
|
|
75
|
+
const scenarios = [scenario({ id: 'a', name: 'A', expect: { status: 200, body: {} } })];
|
|
76
|
+
const result = mockResponseFor(flow(), scenarios, {
|
|
77
|
+
headers: { 'x-emberflow-scenario': 'Nope' },
|
|
78
|
+
query: {},
|
|
79
|
+
});
|
|
80
|
+
expect(result).toEqual({ status: 404, body: { error: 'unknown scenario "Nope"' } });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('returns 404 for a named-but-unknown scenario (query)', () => {
|
|
84
|
+
const scenarios = [scenario({ id: 'a', name: 'A', expect: { status: 200, body: {} } })];
|
|
85
|
+
const result = mockResponseFor(flow(), scenarios, {
|
|
86
|
+
headers: {},
|
|
87
|
+
query: { __scenario: 'Nope' },
|
|
88
|
+
});
|
|
89
|
+
expect(result).toEqual({ status: 404, body: { error: 'unknown scenario "Nope"' } });
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('returns 501 when a named scenario exists but has no expect', () => {
|
|
93
|
+
const scenarios = [scenario({ id: 'a', name: 'A' })];
|
|
94
|
+
const result = mockResponseFor(flow(), scenarios, {
|
|
95
|
+
headers: { 'x-emberflow-scenario': 'A' },
|
|
96
|
+
query: {},
|
|
97
|
+
});
|
|
98
|
+
expect(result).toEqual({
|
|
99
|
+
status: 501,
|
|
100
|
+
body: { error: 'scenario "A" has no expect to mock from' },
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('returns 501 when no scenario has an expect at all', () => {
|
|
105
|
+
const scenarios = [scenario({ id: 'a', name: 'A' }), scenario({ id: 'b', name: 'B' })];
|
|
106
|
+
const result = mockResponseFor(flow(), scenarios, { headers: {}, query: {} });
|
|
107
|
+
expect(result).toEqual({
|
|
108
|
+
status: 501,
|
|
109
|
+
body: { error: 'no mockable scenario (add an expect to a scenario)' },
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('returns 501 with no scenarios at all', () => {
|
|
114
|
+
const result = mockResponseFor(flow(), [], { headers: {}, query: {} });
|
|
115
|
+
expect(result).toEqual({
|
|
116
|
+
status: 501,
|
|
117
|
+
body: { error: 'no mockable scenario (add an expect to a scenario)' },
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('defaults status to 200 when expect only has a body', () => {
|
|
122
|
+
const scenarios = [scenario({ id: 'a', name: 'A', expect: { body: { hello: 'world' } } })];
|
|
123
|
+
const result = mockResponseFor(flow(), scenarios, { headers: {}, query: {} });
|
|
124
|
+
expect(result).toEqual({ status: 200, body: { hello: 'world' }, scenario: 'A' });
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('defaults body to {} when expect only has a status', () => {
|
|
128
|
+
const scenarios = [scenario({ id: 'a', name: 'A', expect: { status: 204 } })];
|
|
129
|
+
const result = mockResponseFor(flow(), scenarios, { headers: {}, query: {} });
|
|
130
|
+
expect(result).toEqual({ status: 204, body: {}, scenario: 'A' });
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('treats an empty expect object as mockable with defaults', () => {
|
|
134
|
+
const scenarios = [scenario({ id: 'a', name: 'A', expect: {} })];
|
|
135
|
+
const result = mockResponseFor(flow(), scenarios, { headers: {}, query: {} });
|
|
136
|
+
expect(result).toEqual({ status: 200, body: {}, scenario: 'A' });
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ScenarioDefinition, WorkflowDefinition } from '../src/engine';
|
|
2
|
+
|
|
3
|
+
const SCENARIO_HEADER = 'x-emberflow-scenario';
|
|
4
|
+
const SCENARIO_QUERY = '__scenario';
|
|
5
|
+
|
|
6
|
+
export interface MockResponse {
|
|
7
|
+
status: number;
|
|
8
|
+
body: unknown;
|
|
9
|
+
scenario?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function fromExpect(scenario: ScenarioDefinition): MockResponse {
|
|
13
|
+
const expect = scenario.expect;
|
|
14
|
+
if (!expect) {
|
|
15
|
+
return {
|
|
16
|
+
status: 501,
|
|
17
|
+
body: { error: `scenario "${scenario.name}" has no expect to mock from` },
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
return { status: expect.status ?? 200, body: expect.body ?? {}, scenario: scenario.name };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function asScenarioName(value: unknown): string | undefined {
|
|
24
|
+
return typeof value === 'string' ? value : undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Pure scenario→response selection for mock mode. No I/O, no auth, no node
|
|
28
|
+
* execution. Selection: `x-emberflow-scenario` header (exact scenario name)
|
|
29
|
+
* → `__scenario` query param → first scenario with an `expect`. A named but
|
|
30
|
+
* unknown scenario is a 404; a named scenario without an `expect` is a 501;
|
|
31
|
+
* no scenario with an `expect` at all is a 501. */
|
|
32
|
+
export function mockResponseFor(
|
|
33
|
+
_flow: WorkflowDefinition,
|
|
34
|
+
scenarios: ScenarioDefinition[],
|
|
35
|
+
req: { headers: Record<string, unknown>; query: Record<string, unknown> },
|
|
36
|
+
): MockResponse {
|
|
37
|
+
const requestedName = asScenarioName(req.headers[SCENARIO_HEADER]) ?? asScenarioName(req.query[SCENARIO_QUERY]);
|
|
38
|
+
|
|
39
|
+
if (requestedName !== undefined) {
|
|
40
|
+
const named = scenarios.find((s) => s.name === requestedName);
|
|
41
|
+
if (!named) {
|
|
42
|
+
return { status: 404, body: { error: `unknown scenario "${requestedName}"` } };
|
|
43
|
+
}
|
|
44
|
+
return fromExpect(named);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const defaultScenario = scenarios.find((s) => s.expect !== undefined);
|
|
48
|
+
if (!defaultScenario) {
|
|
49
|
+
return { status: 501, body: { error: 'no mockable scenario (add an expect to a scenario)' } };
|
|
50
|
+
}
|
|
51
|
+
return fromExpect(defaultScenario);
|
|
52
|
+
}
|