@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,56 @@
|
|
|
1
|
+
import express, {} from 'express';
|
|
2
|
+
const METHODS = ['get', 'post', 'put', 'delete', 'patch'];
|
|
3
|
+
function isMethod(m) {
|
|
4
|
+
return METHODS.includes(m);
|
|
5
|
+
}
|
|
6
|
+
/** Express implementation of the `RuntimeAdapter` seam. Keep it thin — this is
|
|
7
|
+
* the seam other runtimes (Deno/Bun) implement later. */
|
|
8
|
+
export class ExpressAdapter {
|
|
9
|
+
_app;
|
|
10
|
+
server;
|
|
11
|
+
seen = new Set();
|
|
12
|
+
constructor(app) {
|
|
13
|
+
this._app = app ?? express();
|
|
14
|
+
}
|
|
15
|
+
get app() {
|
|
16
|
+
return this._app;
|
|
17
|
+
}
|
|
18
|
+
/** The bound address once `listen()` resolves (useful in tests to find an
|
|
19
|
+
* ephemeral port). `null` before listening / after close. */
|
|
20
|
+
get address() {
|
|
21
|
+
const addr = this.server?.address();
|
|
22
|
+
return addr && typeof addr === 'object' ? addr : null;
|
|
23
|
+
}
|
|
24
|
+
registerOperation(op, handler) {
|
|
25
|
+
const method = op.method.toUpperCase();
|
|
26
|
+
const key = `${method} ${op.path}`;
|
|
27
|
+
if (this.seen.has(key)) {
|
|
28
|
+
throw new Error(`Duplicate operation route: ${key}`);
|
|
29
|
+
}
|
|
30
|
+
const lower = method.toLowerCase();
|
|
31
|
+
if (!isMethod(lower)) {
|
|
32
|
+
throw new Error(`Unsupported HTTP method for operation route: ${method} ${op.path}`);
|
|
33
|
+
}
|
|
34
|
+
this.seen.add(key);
|
|
35
|
+
this._app[lower](op.path, handler);
|
|
36
|
+
}
|
|
37
|
+
mountRouter(basePath, router) {
|
|
38
|
+
this._app.use(basePath, router);
|
|
39
|
+
}
|
|
40
|
+
listen(port, host) {
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
this.server = this._app.listen(port, host, () => resolve());
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
close() {
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
const server = this.server;
|
|
48
|
+
if (!server) {
|
|
49
|
+
resolve();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
this.server = undefined;
|
|
53
|
+
server.close((err) => (err ? reject(err) : resolve()));
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ScenarioExpectation, WorkflowDefinition, WorkflowRun } from '../src/engine/index.js';
|
|
2
|
+
/** Deep-subset match: every key/index present in `expected` must match the
|
|
3
|
+
* corresponding key/index in `actual`, recursively. Extra keys/indices in
|
|
4
|
+
* `actual` are ignored. `null` matches only `null`. Returns human-readable
|
|
5
|
+
* failure strings with dotted (and `[i]`) paths; empty array = match. */
|
|
6
|
+
export declare function deepSubsetMatch(expected: unknown, actual: unknown, path?: string): string[];
|
|
7
|
+
/** Check a scenario's finished run against its `expect` assertion. `status`/
|
|
8
|
+
* `body` come from `extractResponse` (the flow's Response-node mapping,
|
|
9
|
+
* defaulting to status 200); `executedNodes` checks each id's node state is
|
|
10
|
+
* `succeeded`. */
|
|
11
|
+
export declare function evaluateExpectation(run: WorkflowRun, flow: WorkflowDefinition, expect: ScenarioExpectation): {
|
|
12
|
+
ok: boolean;
|
|
13
|
+
failures: string[];
|
|
14
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { extractResponse } from './operationResult.js';
|
|
2
|
+
function fmt(value) {
|
|
3
|
+
if (value === undefined)
|
|
4
|
+
return 'undefined';
|
|
5
|
+
return JSON.stringify(value);
|
|
6
|
+
}
|
|
7
|
+
/** Deep-subset match: every key/index present in `expected` must match the
|
|
8
|
+
* corresponding key/index in `actual`, recursively. Extra keys/indices in
|
|
9
|
+
* `actual` are ignored. `null` matches only `null`. Returns human-readable
|
|
10
|
+
* failure strings with dotted (and `[i]`) paths; empty array = match. */
|
|
11
|
+
export function deepSubsetMatch(expected, actual, path = '') {
|
|
12
|
+
const label = path === '' ? '' : `${path}: `;
|
|
13
|
+
if (expected === null) {
|
|
14
|
+
return actual === null ? [] : [`${label}expected null, got ${fmt(actual)}`];
|
|
15
|
+
}
|
|
16
|
+
if (Array.isArray(expected)) {
|
|
17
|
+
if (!Array.isArray(actual)) {
|
|
18
|
+
return [`${label}expected array, got ${fmt(actual)}`];
|
|
19
|
+
}
|
|
20
|
+
const failures = [];
|
|
21
|
+
for (let i = 0; i < expected.length; i++) {
|
|
22
|
+
failures.push(...deepSubsetMatch(expected[i], actual[i], `${path}[${i}]`));
|
|
23
|
+
}
|
|
24
|
+
return failures;
|
|
25
|
+
}
|
|
26
|
+
if (typeof expected === 'object') {
|
|
27
|
+
if (typeof actual !== 'object' || actual === null || Array.isArray(actual)) {
|
|
28
|
+
return [`${label}expected object, got ${fmt(actual)}`];
|
|
29
|
+
}
|
|
30
|
+
const failures = [];
|
|
31
|
+
for (const key of Object.keys(expected)) {
|
|
32
|
+
const childPath = path === '' ? key : `${path}.${key}`;
|
|
33
|
+
failures.push(...deepSubsetMatch(expected[key], actual[key], childPath));
|
|
34
|
+
}
|
|
35
|
+
return failures;
|
|
36
|
+
}
|
|
37
|
+
// Primitive.
|
|
38
|
+
return expected === actual ? [] : [`${label}expected ${fmt(expected)}, got ${fmt(actual)}`];
|
|
39
|
+
}
|
|
40
|
+
/** Check a scenario's finished run against its `expect` assertion. `status`/
|
|
41
|
+
* `body` come from `extractResponse` (the flow's Response-node mapping,
|
|
42
|
+
* defaulting to status 200); `executedNodes` checks each id's node state is
|
|
43
|
+
* `succeeded`. */
|
|
44
|
+
export function evaluateExpectation(run, flow, expect) {
|
|
45
|
+
const failures = [];
|
|
46
|
+
if (expect.status !== undefined || expect.body !== undefined) {
|
|
47
|
+
const { status, body } = extractResponse(run, flow);
|
|
48
|
+
if (expect.status !== undefined && expect.status !== status) {
|
|
49
|
+
failures.push(`status: expected ${expect.status}, got ${status}`);
|
|
50
|
+
}
|
|
51
|
+
if (expect.body !== undefined) {
|
|
52
|
+
for (const failure of deepSubsetMatch(expect.body, body)) {
|
|
53
|
+
failures.push(`body.${failure}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (expect.executedNodes) {
|
|
58
|
+
for (const id of expect.executedNodes) {
|
|
59
|
+
const state = run.nodeStates[id];
|
|
60
|
+
if (state?.status !== 'succeeded') {
|
|
61
|
+
failures.push(`executedNodes: ${id} did not execute (status: ${state?.status ?? 'not run'})`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return { ok: failures.length === 0, failures };
|
|
66
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { LogLine, NodeRegistry, StartRunOptions, TraceSink, WorkflowDefinition } from '../src/engine/index.js';
|
|
2
|
+
/** Max depth of nested Subflow calls (ancestry chain length) before a run fails. */
|
|
3
|
+
export declare const SUBFLOW_DEPTH_CAP = 8;
|
|
4
|
+
export interface SubflowRunnerOptions {
|
|
5
|
+
/** Resolves a child workflow by id (server hosts this via apiStore.load). */
|
|
6
|
+
loadFlow: (id: string) => WorkflowDefinition | undefined;
|
|
7
|
+
registry: NodeRegistry;
|
|
8
|
+
secrets: Record<string, string>;
|
|
9
|
+
vars: Record<string, string>;
|
|
10
|
+
environment: string;
|
|
11
|
+
safeMode: boolean;
|
|
12
|
+
/** When true, child runs execute in Mock mode too — nothing real is ever
|
|
13
|
+
* touched during a mock run, including inside Subflow children. See
|
|
14
|
+
* `StartRunOptions.mockRun`. */
|
|
15
|
+
mockRun?: boolean;
|
|
16
|
+
/** Optional trace sink shared with the parent run (SSE/samples plumbing). */
|
|
17
|
+
trace?: TraceSink;
|
|
18
|
+
/** Optional child-log forwarder — receives the child's log lines already
|
|
19
|
+
* prefixed with the child flow's name and the caller node's id, exactly as
|
|
20
|
+
* the live runner streams them over SSE. Absent for headless runs. */
|
|
21
|
+
onLog?: (line: LogLine) => void;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Shared Subflow-runner factory used by BOTH the live runner
|
|
25
|
+
* (server/runRegistry.ts) and the in-process test runner
|
|
26
|
+
* (server/testRunner.ts): resolve the child via `loadFlow`, run it to
|
|
27
|
+
* completion on the same registry sharing the parent's environment
|
|
28
|
+
* (secrets/vars/safeMode), and return its collected Result output. The
|
|
29
|
+
* ancestry chain enforces the depth cap and catches A→B→A cycles; nested
|
|
30
|
+
* children keep their own execution sequence.
|
|
31
|
+
*/
|
|
32
|
+
export declare function makeSubflowRunner(opts: SubflowRunnerOptions, ancestry: string[]): NonNullable<StartRunOptions['subflowRunner']>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { runOutput, startRun } from '../src/engine/index.js';
|
|
2
|
+
/** Max depth of nested Subflow calls (ancestry chain length) before a run fails. */
|
|
3
|
+
export const SUBFLOW_DEPTH_CAP = 8;
|
|
4
|
+
/**
|
|
5
|
+
* Shared Subflow-runner factory used by BOTH the live runner
|
|
6
|
+
* (server/runRegistry.ts) and the in-process test runner
|
|
7
|
+
* (server/testRunner.ts): resolve the child via `loadFlow`, run it to
|
|
8
|
+
* completion on the same registry sharing the parent's environment
|
|
9
|
+
* (secrets/vars/safeMode), and return its collected Result output. The
|
|
10
|
+
* ancestry chain enforces the depth cap and catches A→B→A cycles; nested
|
|
11
|
+
* children keep their own execution sequence.
|
|
12
|
+
*/
|
|
13
|
+
export function makeSubflowRunner(opts, ancestry) {
|
|
14
|
+
return async (workflowId, input, callerNodeId) => {
|
|
15
|
+
if (ancestry.length >= SUBFLOW_DEPTH_CAP) {
|
|
16
|
+
return { status: 'failed', error: `subflow depth cap (${SUBFLOW_DEPTH_CAP}) exceeded` };
|
|
17
|
+
}
|
|
18
|
+
if (ancestry.includes(workflowId)) {
|
|
19
|
+
return { status: 'failed', error: `subflow cycle: ${[...ancestry, workflowId].join(' → ')}` };
|
|
20
|
+
}
|
|
21
|
+
const childFlow = opts.loadFlow(workflowId);
|
|
22
|
+
if (!childFlow)
|
|
23
|
+
return { status: 'failed', error: `Unknown workflow: ${workflowId}` };
|
|
24
|
+
try {
|
|
25
|
+
const childHandle = startRun({
|
|
26
|
+
flow: childFlow,
|
|
27
|
+
registry: opts.registry,
|
|
28
|
+
secrets: opts.secrets,
|
|
29
|
+
vars: opts.vars,
|
|
30
|
+
environment: opts.environment,
|
|
31
|
+
safeMode: opts.safeMode,
|
|
32
|
+
input,
|
|
33
|
+
// Mock mode propagates into the child: nothing real is ever touched
|
|
34
|
+
// in a mock run, including inside Subflow children. Scenario context
|
|
35
|
+
// never propagates (the parent didn't pick one for the child) — only
|
|
36
|
+
// the CHILD op's own op-level mocks apply, since nodeIds are scoped
|
|
37
|
+
// per-flow and reusing the parent's mock map would be meaningless
|
|
38
|
+
// (or worse, silently wrong) for the child's node ids.
|
|
39
|
+
mockRun: opts.mockRun,
|
|
40
|
+
mocks: childFlow.mocks ?? {},
|
|
41
|
+
trace: opts.trace,
|
|
42
|
+
subflowRunner: makeSubflowRunner(opts, [...ancestry, workflowId]),
|
|
43
|
+
events: opts.onLog
|
|
44
|
+
? {
|
|
45
|
+
onLog: (line) => opts.onLog({
|
|
46
|
+
...line,
|
|
47
|
+
nodeLabel: `${childFlow.name} › ${line.nodeLabel ?? ''}`,
|
|
48
|
+
nodeId: line.nodeId ? `${callerNodeId}/${line.nodeId}` : callerNodeId,
|
|
49
|
+
}),
|
|
50
|
+
}
|
|
51
|
+
: undefined,
|
|
52
|
+
});
|
|
53
|
+
const childRun = await childHandle.runToEnd();
|
|
54
|
+
if (childRun.status !== 'succeeded') {
|
|
55
|
+
// Surface the child's actual failure (e.g. the Mock-mode "would touch
|
|
56
|
+
// real infrastructure" boundary message) through the Subflow node's
|
|
57
|
+
// own error, rather than an opaque "failed" — the first failed
|
|
58
|
+
// node's `error`, if any, wins.
|
|
59
|
+
const failedNode = Object.values(childRun.nodeStates).find((s) => s.status === 'failed' && s.error);
|
|
60
|
+
const detail = failedNode?.error;
|
|
61
|
+
return {
|
|
62
|
+
status: 'failed',
|
|
63
|
+
error: `subflow "${childFlow.name}" ${childRun.status}${detail ? `: ${detail}` : ''}`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return { status: 'succeeded', output: runOutput(childRun, childFlow) };
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
return { status: 'failed', error: err instanceof Error ? err.message : String(err) };
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { type NodeRegistry } from '../src/engine/index.js';
|
|
2
|
+
import { type EnvironmentsFile } from './environments.js';
|
|
3
|
+
import type { ApiStore } from './apiStore.js';
|
|
4
|
+
/** Thrown for CLI usage errors (unknown opId / unknown environment) — the CLI
|
|
5
|
+
* layer turns this into exit code 2, distinct from an assertion failure
|
|
6
|
+
* (exit 1) or an unexpected error (also exit 1, via runCli's generic catch). */
|
|
7
|
+
export declare class ScenarioTestUsageError extends Error {
|
|
8
|
+
}
|
|
9
|
+
export type SkipReason = 'no expect' | 'empty expect';
|
|
10
|
+
export interface ScenarioResult {
|
|
11
|
+
opId: string;
|
|
12
|
+
scenario: string;
|
|
13
|
+
status: 'passed' | 'failed' | 'skipped';
|
|
14
|
+
/** Present only when status === 'failed' — evaluateExpectation's failure strings. */
|
|
15
|
+
failures?: string[];
|
|
16
|
+
/** Present only when status === 'skipped'. */
|
|
17
|
+
reason?: SkipReason;
|
|
18
|
+
}
|
|
19
|
+
export interface TestReport {
|
|
20
|
+
results: ScenarioResult[];
|
|
21
|
+
passed: number;
|
|
22
|
+
failed: number;
|
|
23
|
+
skipped: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Runs every scenario with `expect` across the given ops (or a single op when
|
|
27
|
+
* `opId` is given), entirely in-process: `startRun` is called directly per
|
|
28
|
+
* scenario, mirroring the options shape `RunRegistry.create` passes
|
|
29
|
+
* (server/runRegistry.ts) — but bypassing RunRegistry entirely, so these runs
|
|
30
|
+
* never enter it (no SSE, no run history).
|
|
31
|
+
*
|
|
32
|
+
* This is the shared seam both the CLI (`runScenarioSuite`, which bootstraps
|
|
33
|
+
* its own project/registries/apiStore from disk) and the studio route
|
|
34
|
+
* (`POST /workflows/:id/test` in server/index.ts, which reuses the
|
|
35
|
+
* already-booted server's apiStore/registry/environmentsFile — no re-read
|
|
36
|
+
* from disk, no drift from in-memory writes) call into. NO second expectation
|
|
37
|
+
* engine — both paths land here.
|
|
38
|
+
*
|
|
39
|
+
* Throws `ScenarioTestUsageError` for usage problems (unknown opId, unknown
|
|
40
|
+
* environment) — the CLI layer maps that to exit code 2; the route maps it to
|
|
41
|
+
* 400/404.
|
|
42
|
+
*/
|
|
43
|
+
export declare function runScenarioSuiteFor(ctx: {
|
|
44
|
+
apiStore: ApiStore;
|
|
45
|
+
registry: NodeRegistry;
|
|
46
|
+
environmentsFile: EnvironmentsFile;
|
|
47
|
+
}, opts: {
|
|
48
|
+
opId?: string;
|
|
49
|
+
environmentName?: string;
|
|
50
|
+
mock?: boolean;
|
|
51
|
+
}): Promise<TestReport>;
|
|
52
|
+
/**
|
|
53
|
+
* CLI entry point: bootstraps a project from disk (loadProjectConfig/
|
|
54
|
+
* buildRegistries/buildApiStore/loadEnvironments — the same bootstrap
|
|
55
|
+
* `server/index.ts` uses) and delegates to `runScenarioSuiteFor`. NO runner
|
|
56
|
+
* process, no HTTP.
|
|
57
|
+
*/
|
|
58
|
+
export declare function runScenarioSuite(opts: {
|
|
59
|
+
projectDir: string;
|
|
60
|
+
opId?: string;
|
|
61
|
+
environmentName?: string;
|
|
62
|
+
mock?: boolean;
|
|
63
|
+
}): Promise<TestReport>;
|
|
64
|
+
/** `✓ opId · scenario` / `✗ opId · scenario (failure; failure)` — undefined for
|
|
65
|
+
* skipped scenarios, which aren't printed as individual lines (only counted
|
|
66
|
+
* in the summary), keeping default output focused on assertable scenarios. */
|
|
67
|
+
export declare function formatScenarioLine(r: ScenarioResult): string | undefined;
|
|
68
|
+
/** `12 passed, 1 failed, 3 skipped (no expect)` — the parenthetical lists the
|
|
69
|
+
* distinct skip reasons present (omitted entirely when nothing was skipped). */
|
|
70
|
+
export declare function formatSummary(report: TestReport): string;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { mergeMocks, startRun, } from '../src/engine/index.js';
|
|
2
|
+
import { loadProjectConfig } from './projectConfig.js';
|
|
3
|
+
import { buildApiStore, buildRegistries, requireProjectWhenExplicit } from './projectMode.js';
|
|
4
|
+
import { loadEnvironments, resolveRunSafety } from './environments.js';
|
|
5
|
+
import { evaluateExpectation } from './scenarioTest.js';
|
|
6
|
+
import { makeSubflowRunner } from './subflowRunner.js';
|
|
7
|
+
import { redactSecrets } from './redact.js';
|
|
8
|
+
/** Thrown for CLI usage errors (unknown opId / unknown environment) — the CLI
|
|
9
|
+
* layer turns this into exit code 2, distinct from an assertion failure
|
|
10
|
+
* (exit 1) or an unexpected error (also exit 1, via runCli's generic catch). */
|
|
11
|
+
export class ScenarioTestUsageError extends Error {
|
|
12
|
+
}
|
|
13
|
+
async function runScenario(op, scenario, apiStore, registry, env, mock) {
|
|
14
|
+
if (!scenario.expect) {
|
|
15
|
+
return { opId: op.id, scenario: scenario.name, status: 'skipped', reason: 'no expect' };
|
|
16
|
+
}
|
|
17
|
+
if (Object.keys(scenario.expect).length === 0) {
|
|
18
|
+
return { opId: op.id, scenario: scenario.name, status: 'skipped', reason: 'empty expect' };
|
|
19
|
+
}
|
|
20
|
+
// INTENTIONAL (controller decision): NO environment auth auto-attach here,
|
|
21
|
+
// unlike POST /runs (server/index.ts attachCredential). Tests are hermetic —
|
|
22
|
+
// auto-attaching the environment's credential would silently satisfy the very
|
|
23
|
+
// auth checks a deliberate 401-expectation scenario exists to exercise. If a
|
|
24
|
+
// scenario needs auth, embed the headers directly in `scenario.input`.
|
|
25
|
+
//
|
|
26
|
+
// --mock: same merge as the live serving mock-run path (mergeMocks,
|
|
27
|
+
// src/engine/scenarios.ts) — op-level `flow.mocks` overlaid by this
|
|
28
|
+
// scenario's `mocks`, scenario wins per nodeId. Nothing real is ever
|
|
29
|
+
// touched: the executor's mockRun short-circuit fails loudly on any
|
|
30
|
+
// infrastructure node (traceKind db/http/llm) that has no mock.
|
|
31
|
+
const handle = startRun({
|
|
32
|
+
flow: op,
|
|
33
|
+
registry,
|
|
34
|
+
secrets: env.secrets,
|
|
35
|
+
vars: env.vars,
|
|
36
|
+
environment: env.environment,
|
|
37
|
+
safeMode: env.safeMode,
|
|
38
|
+
input: scenario.input,
|
|
39
|
+
mockRun: mock,
|
|
40
|
+
mocks: mock ? mergeMocks(op, scenario) : undefined,
|
|
41
|
+
// Shared factory (server/subflowRunner.ts) — same depth cap/cycle guard as
|
|
42
|
+
// the live runner, minus the SSE trace/log plumbing a headless run skips.
|
|
43
|
+
subflowRunner: makeSubflowRunner({
|
|
44
|
+
loadFlow: (id) => apiStore.load(id),
|
|
45
|
+
registry,
|
|
46
|
+
secrets: env.secrets,
|
|
47
|
+
vars: env.vars,
|
|
48
|
+
environment: env.environment,
|
|
49
|
+
safeMode: env.safeMode,
|
|
50
|
+
mockRun: mock,
|
|
51
|
+
}, [op.id]),
|
|
52
|
+
});
|
|
53
|
+
const run = await handle.runToEnd();
|
|
54
|
+
const { ok, failures } = evaluateExpectation(run, op, scenario.expect);
|
|
55
|
+
return {
|
|
56
|
+
opId: op.id,
|
|
57
|
+
scenario: scenario.name,
|
|
58
|
+
status: ok ? 'passed' : 'failed',
|
|
59
|
+
...(ok ? {} : { failures }),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Runs every scenario with `expect` across the given ops (or a single op when
|
|
64
|
+
* `opId` is given), entirely in-process: `startRun` is called directly per
|
|
65
|
+
* scenario, mirroring the options shape `RunRegistry.create` passes
|
|
66
|
+
* (server/runRegistry.ts) — but bypassing RunRegistry entirely, so these runs
|
|
67
|
+
* never enter it (no SSE, no run history).
|
|
68
|
+
*
|
|
69
|
+
* This is the shared seam both the CLI (`runScenarioSuite`, which bootstraps
|
|
70
|
+
* its own project/registries/apiStore from disk) and the studio route
|
|
71
|
+
* (`POST /workflows/:id/test` in server/index.ts, which reuses the
|
|
72
|
+
* already-booted server's apiStore/registry/environmentsFile — no re-read
|
|
73
|
+
* from disk, no drift from in-memory writes) call into. NO second expectation
|
|
74
|
+
* engine — both paths land here.
|
|
75
|
+
*
|
|
76
|
+
* Throws `ScenarioTestUsageError` for usage problems (unknown opId, unknown
|
|
77
|
+
* environment) — the CLI layer maps that to exit code 2; the route maps it to
|
|
78
|
+
* 400/404.
|
|
79
|
+
*/
|
|
80
|
+
export async function runScenarioSuiteFor(ctx, opts) {
|
|
81
|
+
const { apiStore, registry, environmentsFile } = ctx;
|
|
82
|
+
const { opId, environmentName, mock = false } = opts;
|
|
83
|
+
const envName = environmentName ?? environmentsFile.defaultEnvironment;
|
|
84
|
+
const envDef = environmentsFile.environments[envName];
|
|
85
|
+
if (!envDef) {
|
|
86
|
+
throw new ScenarioTestUsageError(`Unknown environment: '${envName}'`);
|
|
87
|
+
}
|
|
88
|
+
const safety = resolveRunSafety(envName, envDef, {});
|
|
89
|
+
const runEnv = {
|
|
90
|
+
secrets: envDef.secrets,
|
|
91
|
+
vars: envDef.vars,
|
|
92
|
+
environment: envName,
|
|
93
|
+
safeMode: safety.ok ? safety.safeMode : false,
|
|
94
|
+
};
|
|
95
|
+
const allOps = apiStore.list();
|
|
96
|
+
let targetOps = allOps;
|
|
97
|
+
if (opId !== undefined) {
|
|
98
|
+
const op = allOps.find((o) => o.id === opId);
|
|
99
|
+
if (!op)
|
|
100
|
+
throw new ScenarioTestUsageError(`Unknown operation: '${opId}'`);
|
|
101
|
+
targetOps = [op];
|
|
102
|
+
}
|
|
103
|
+
const rawResults = [];
|
|
104
|
+
for (const op of targetOps) {
|
|
105
|
+
for (const scenario of op.scenarios ?? []) {
|
|
106
|
+
rawResults.push(await runScenario(op, scenario, apiStore, registry, runEnv, mock));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// evaluateExpectation diffs RAW run state, so a failure string can quote a
|
|
110
|
+
// secret value the flow echoed into its response body. Redact the whole
|
|
111
|
+
// reporter payload (value-based; raw/url-encoded/base64 forms) before it can
|
|
112
|
+
// reach stdout/CI logs — same redactSecrets the SSE event path uses.
|
|
113
|
+
const results = redactSecrets(rawResults, runEnv.secrets);
|
|
114
|
+
return {
|
|
115
|
+
results,
|
|
116
|
+
passed: results.filter((r) => r.status === 'passed').length,
|
|
117
|
+
failed: results.filter((r) => r.status === 'failed').length,
|
|
118
|
+
skipped: results.filter((r) => r.status === 'skipped').length,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* CLI entry point: bootstraps a project from disk (loadProjectConfig/
|
|
123
|
+
* buildRegistries/buildApiStore/loadEnvironments — the same bootstrap
|
|
124
|
+
* `server/index.ts` uses) and delegates to `runScenarioSuiteFor`. NO runner
|
|
125
|
+
* process, no HTTP.
|
|
126
|
+
*/
|
|
127
|
+
export async function runScenarioSuite(opts) {
|
|
128
|
+
const { projectDir, opId, environmentName, mock } = opts;
|
|
129
|
+
const project = requireProjectWhenExplicit(await loadProjectConfig(projectDir), process.env.EMBERFLOW_PROJECT, projectDir);
|
|
130
|
+
const environmentsFile = loadEnvironments(project ? project.root : projectDir);
|
|
131
|
+
const { execution } = buildRegistries(project);
|
|
132
|
+
const apiStore = buildApiStore(project);
|
|
133
|
+
return runScenarioSuiteFor({ apiStore, registry: execution, environmentsFile }, { opId, environmentName, mock });
|
|
134
|
+
}
|
|
135
|
+
/** `✓ opId · scenario` / `✗ opId · scenario (failure; failure)` — undefined for
|
|
136
|
+
* skipped scenarios, which aren't printed as individual lines (only counted
|
|
137
|
+
* in the summary), keeping default output focused on assertable scenarios. */
|
|
138
|
+
export function formatScenarioLine(r) {
|
|
139
|
+
if (r.status === 'skipped')
|
|
140
|
+
return undefined;
|
|
141
|
+
if (r.status === 'passed')
|
|
142
|
+
return `✓ ${r.opId} · ${r.scenario}`;
|
|
143
|
+
return `✗ ${r.opId} · ${r.scenario} (${(r.failures ?? []).join('; ')})`;
|
|
144
|
+
}
|
|
145
|
+
/** `12 passed, 1 failed, 3 skipped (no expect)` — the parenthetical lists the
|
|
146
|
+
* distinct skip reasons present (omitted entirely when nothing was skipped). */
|
|
147
|
+
export function formatSummary(report) {
|
|
148
|
+
const reasons = [...new Set(report.results.filter((r) => r.status === 'skipped').map((r) => r.reason))];
|
|
149
|
+
const suffix = report.skipped > 0 ? ` (${reasons.join(', ')})` : '';
|
|
150
|
+
return `${report.passed} passed, ${report.failed} failed, ${report.skipped} skipped${suffix}`;
|
|
151
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { AuthPolicy } from './types.js';
|
|
2
|
+
export interface VerifyResult {
|
|
3
|
+
user: unknown;
|
|
4
|
+
}
|
|
5
|
+
/** Given the request + the resolved secret value, return a user or throw HttpError(401). */
|
|
6
|
+
export type Verifier = (args: {
|
|
7
|
+
request: {
|
|
8
|
+
headers: Record<string, unknown>;
|
|
9
|
+
};
|
|
10
|
+
policy: AuthPolicy;
|
|
11
|
+
secret: string | undefined;
|
|
12
|
+
}) => VerifyResult;
|
|
13
|
+
export declare const bearerVerifier: Verifier;
|
|
14
|
+
export declare const apiKeyVerifier: Verifier;
|
|
15
|
+
export declare class VerifierRegistry {
|
|
16
|
+
private map;
|
|
17
|
+
register(name: string, v: Verifier): void;
|
|
18
|
+
get(name: string): Verifier | undefined;
|
|
19
|
+
}
|
|
20
|
+
export declare function createDefaultVerifierRegistry(): VerifierRegistry;
|
|
21
|
+
/**
|
|
22
|
+
* Picks the verifier (policy.verify ? verifiers.get(policy.verify) : verifiers.get(policy.scheme)),
|
|
23
|
+
* fails closed with HttpError(500) if it's missing, resolves the secret from
|
|
24
|
+
* `secrets[policy.secretRef]`, and runs the verifier (its HttpError, if any, propagates).
|
|
25
|
+
*/
|
|
26
|
+
export declare function enforceAuth(args: {
|
|
27
|
+
policy: AuthPolicy;
|
|
28
|
+
request: {
|
|
29
|
+
headers: Record<string, unknown>;
|
|
30
|
+
};
|
|
31
|
+
secrets: Record<string, unknown>;
|
|
32
|
+
verifiers: VerifierRegistry;
|
|
33
|
+
}): VerifyResult;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Browser-safe auth verification core. Lives here (not under server/auth/)
|
|
2
|
+
// because src/nodes/requireAuth.ts (bundled into the studio browser build via
|
|
3
|
+
// src/store/builderStore.ts -> src/nodes) must not import from server/. The
|
|
4
|
+
// server-side enforce.ts + verifiers.ts re-export/wrap this module so there's
|
|
5
|
+
// a single implementation instead of two diverging copies.
|
|
6
|
+
import { HttpError } from './httpError.js';
|
|
7
|
+
const bearerToken = (headers) => {
|
|
8
|
+
const raw = headers['authorization'] ?? headers['Authorization'];
|
|
9
|
+
if (typeof raw !== 'string')
|
|
10
|
+
return undefined;
|
|
11
|
+
const m = /^Bearer\s+(.+)$/i.exec(raw);
|
|
12
|
+
return m ? m[1] : undefined;
|
|
13
|
+
};
|
|
14
|
+
// Note: a plain string comparison is used here rather than a timing-safe
|
|
15
|
+
// comparison; timing-safe comparison is a later hardening item.
|
|
16
|
+
export const bearerVerifier = ({ request, secret }) => {
|
|
17
|
+
if (secret === undefined)
|
|
18
|
+
throw new HttpError(500, { error: 'auth misconfigured: secret not set' });
|
|
19
|
+
const token = bearerToken(request.headers);
|
|
20
|
+
if (!token || token !== secret)
|
|
21
|
+
throw new HttpError(401, { error: 'unauthorized' });
|
|
22
|
+
return { user: { scheme: 'bearer' } };
|
|
23
|
+
};
|
|
24
|
+
export const apiKeyVerifier = ({ request, policy, secret }) => {
|
|
25
|
+
if (secret === undefined)
|
|
26
|
+
throw new HttpError(500, { error: 'auth misconfigured: secret not set' });
|
|
27
|
+
const headerName = (policy.header ?? 'x-api-key').toLowerCase();
|
|
28
|
+
const provided = request.headers[headerName];
|
|
29
|
+
if (typeof provided !== 'string' || provided !== secret)
|
|
30
|
+
throw new HttpError(401, { error: 'unauthorized' });
|
|
31
|
+
return { user: { scheme: 'apiKey' } };
|
|
32
|
+
};
|
|
33
|
+
export class VerifierRegistry {
|
|
34
|
+
map = new Map();
|
|
35
|
+
register(name, v) {
|
|
36
|
+
this.map.set(name, v);
|
|
37
|
+
}
|
|
38
|
+
get(name) {
|
|
39
|
+
return this.map.get(name);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export function createDefaultVerifierRegistry() {
|
|
43
|
+
const reg = new VerifierRegistry();
|
|
44
|
+
reg.register('bearer', bearerVerifier);
|
|
45
|
+
reg.register('apiKey', apiKeyVerifier);
|
|
46
|
+
return reg;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Picks the verifier (policy.verify ? verifiers.get(policy.verify) : verifiers.get(policy.scheme)),
|
|
50
|
+
* fails closed with HttpError(500) if it's missing, resolves the secret from
|
|
51
|
+
* `secrets[policy.secretRef]`, and runs the verifier (its HttpError, if any, propagates).
|
|
52
|
+
*/
|
|
53
|
+
export function enforceAuth(args) {
|
|
54
|
+
const { policy, request, secrets, verifiers } = args;
|
|
55
|
+
const verifierName = policy.verify ?? policy.scheme;
|
|
56
|
+
const verifier = policy.verify ? verifiers.get(policy.verify) : verifiers.get(policy.scheme);
|
|
57
|
+
if (!verifier) {
|
|
58
|
+
throw new HttpError(500, { error: `auth misconfigured: no verifier ${verifierName}` });
|
|
59
|
+
}
|
|
60
|
+
const rawSecret = secrets[policy.secretRef];
|
|
61
|
+
const secret = typeof rawSecret === 'string' ? rawSecret : undefined;
|
|
62
|
+
return verifier({ request, policy, secret });
|
|
63
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { ScenarioDefinition, WorkflowDefinition } from './types.js';
|
|
2
|
+
export interface OperationDiagnostic {
|
|
3
|
+
severity: 'error' | 'warning' | 'info';
|
|
4
|
+
code: string;
|
|
5
|
+
message: string;
|
|
6
|
+
nodeId?: string;
|
|
7
|
+
param?: string;
|
|
8
|
+
}
|
|
9
|
+
/** Registry-derived facts diagnoseOperation cannot compute on its own — it is
|
|
10
|
+
* a pure function over (flow, scenarios) and has no node-definition lookup.
|
|
11
|
+
* Callers that hold a registry (server/index.ts's diagnostics route, the
|
|
12
|
+
* `doctor` CLI, the studio's client-side registry) derive `infraNodes` by
|
|
13
|
+
* looking up each flow node's type and keeping the ones whose `traceKind` is
|
|
14
|
+
* 'db' | 'http' | 'llm'. Callers without a registry (or without meta yet)
|
|
15
|
+
* omit `extras` entirely — `missing-node-mock` simply doesn't fire. */
|
|
16
|
+
export interface DiagnoseOperationExtras {
|
|
17
|
+
infraNodes?: Array<{
|
|
18
|
+
id: string;
|
|
19
|
+
traceKind: string;
|
|
20
|
+
}>;
|
|
21
|
+
/** Op-level mocks to check coverage against, when the caller has a more
|
|
22
|
+
* authoritative source than `flow.mocks` (e.g. an in-progress edit not yet
|
|
23
|
+
* saved). Falls back to `flow.mocks` when omitted. */
|
|
24
|
+
opMocks?: Record<string, unknown>;
|
|
25
|
+
/** Declared output field names per flow-node id, for `inputmap-schema-mismatch`.
|
|
26
|
+
* Callers that hold a registry populate this ONLY for nodes whose type is
|
|
27
|
+
* registered AND whose outputSchema declares fields (an entry's presence is
|
|
28
|
+
* the "checkable" signal). Nodes absent from the map — unregistered types, or
|
|
29
|
+
* registered ones with an empty/absent outputSchema (Input, Subflow, …) — are
|
|
30
|
+
* never flagged, so dynamic-output nodes cannot false-positive. Omitted
|
|
31
|
+
* entirely → the diagnostic simply doesn't fire. */
|
|
32
|
+
outputFieldsByNode?: Record<string, string[]>;
|
|
33
|
+
/** Implementation source per flow-node id, for `simulated-commit`. Callers
|
|
34
|
+
* that hold a registry populate this ONLY for nodes whose type is
|
|
35
|
+
* registered AND whose registered `definition.effects === 'mutation'` AND
|
|
36
|
+
* whose implementation is a function (`String(implementation)`). A node
|
|
37
|
+
* absent from the map — unregistered type, non-mutation node, or a native/
|
|
38
|
+
* bound implementation whose source isn't meaningfully inspectable — is
|
|
39
|
+
* never flagged. Omitted entirely → the diagnostic simply doesn't fire. */
|
|
40
|
+
mutationSourcesByNode?: Record<string, string>;
|
|
41
|
+
/** Project-level signal for `language-drift`, NOT per-node. Pass this ONLY
|
|
42
|
+
* when the project config's `language:` field was EXPLICIT and disagrees
|
|
43
|
+
* with the config file's own extension (e.g. `language: 'javascript'` in
|
|
44
|
+
* an `emberflow.config.ts`) — inference makes language and extension agree
|
|
45
|
+
* by construction, so that's the only case worth flagging. Callers that
|
|
46
|
+
* hold a loaded ProjectConfig (server/index.ts's diagnostics route, the
|
|
47
|
+
* `doctor` CLI) plumb this from `ProjectConfig.languageDrift`. The studio
|
|
48
|
+
* doesn't currently have project-language access client-side, so it omits
|
|
49
|
+
* this and the diagnostic simply doesn't fire there. */
|
|
50
|
+
languageDrift?: {
|
|
51
|
+
projectLanguage: 'javascript' | 'typescript';
|
|
52
|
+
configPathExtension: string;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/** Diagnose an operation (a flow with an `http` trigger, plus its scenarios)
|
|
56
|
+
* for issues a studio user or the `emberflow doctor` CLI should surface
|
|
57
|
+
* before relying on the operation. Deterministic order: all
|
|
58
|
+
* `missing-param-default` (path order), then all `param-no-real-scenario`
|
|
59
|
+
* (path order), then `no-expects`, then `missing-node-mock` (extras order),
|
|
60
|
+
* then `inputmap-schema-mismatch` (node order), then `simulated-commit`
|
|
61
|
+
* (node order), then `language-drift` (at most one, project-level). */
|
|
62
|
+
export declare function diagnoseOperation(flow: WorkflowDefinition, scenarios: ScenarioDefinition[] | undefined, extras?: DiagnoseOperationExtras): OperationDiagnostic[];
|