@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,467 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
2
|
+
import { spawn, type ChildProcess } from 'node:child_process';
|
|
3
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { runCli } from './cli';
|
|
7
|
+
|
|
8
|
+
// Task 6: `--mock` serving. Boots the real runner subprocess twice — once
|
|
9
|
+
// with EMBERFLOW_MOCK=1 (mounted HTTP operations answer from scenario
|
|
10
|
+
// expectations, no nodes execute) and once without (unchanged, real
|
|
11
|
+
// execution) — against a scratch project with two routed operations:
|
|
12
|
+
// `ping` (has scenarios with/without `expect`) and `echo` (no scenarios at
|
|
13
|
+
// all, for the 501 case).
|
|
14
|
+
|
|
15
|
+
const MOCK_PORT = 8142;
|
|
16
|
+
const LIVE_PORT = 8143;
|
|
17
|
+
const RUNTIME_PORT = 8145;
|
|
18
|
+
const NO_ENV_PORT = 8148;
|
|
19
|
+
const WITH_ENV_PORT = 8149;
|
|
20
|
+
const WITH_ENV_MOCK_PORT = 8150;
|
|
21
|
+
const mockBase = `http://127.0.0.1:${MOCK_PORT}`;
|
|
22
|
+
const liveBase = `http://127.0.0.1:${LIVE_PORT}`;
|
|
23
|
+
const runtimeBase = `http://127.0.0.1:${RUNTIME_PORT}`;
|
|
24
|
+
const noEnvBase = `http://127.0.0.1:${NO_ENV_PORT}`;
|
|
25
|
+
const withEnvBase = `http://127.0.0.1:${WITH_ENV_PORT}`;
|
|
26
|
+
const withEnvMockBase = `http://127.0.0.1:${WITH_ENV_MOCK_PORT}`;
|
|
27
|
+
|
|
28
|
+
async function waitHealthy(url: string, tries = 40): Promise<void> {
|
|
29
|
+
for (let i = 0; i < tries; i++) {
|
|
30
|
+
try {
|
|
31
|
+
if ((await fetch(url)).ok) return;
|
|
32
|
+
} catch {
|
|
33
|
+
/* not up yet */
|
|
34
|
+
}
|
|
35
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
36
|
+
}
|
|
37
|
+
throw new Error('runner did not become healthy');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function bootRunner(port: number, env: Record<string, string | undefined>): ChildProcess {
|
|
41
|
+
return spawn('npx', ['tsx', 'server/index.ts'], {
|
|
42
|
+
env: { ...process.env, EMBERFLOW_RUNNER_PORT: String(port), ...env },
|
|
43
|
+
stdio: 'ignore',
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function postServing(base: string, mode: unknown): Promise<Response> {
|
|
48
|
+
return fetch(`${base}/serving`, {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: { 'Content-Type': 'application/json' },
|
|
51
|
+
body: JSON.stringify({ mode }),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let mockProc: ChildProcess;
|
|
56
|
+
let liveProc: ChildProcess;
|
|
57
|
+
let runtimeProc: ChildProcess;
|
|
58
|
+
let noEnvProc: ChildProcess;
|
|
59
|
+
let withEnvProc: ChildProcess;
|
|
60
|
+
let withEnvMockProc: ChildProcess;
|
|
61
|
+
let projectDir: string;
|
|
62
|
+
let runtimeProjectDir: string;
|
|
63
|
+
let noEnvProjectDir: string;
|
|
64
|
+
let withEnvProjectDir: string;
|
|
65
|
+
|
|
66
|
+
/** Writes a minimal routed `ping` op (+ scenario) under `dir`, the smallest
|
|
67
|
+
* fixture needed to prove which serving mode answered a mounted route. */
|
|
68
|
+
function writePingOp(dir: string): void {
|
|
69
|
+
mkdirSync(join(dir, 'emberflow', 'apis', 'default'), { recursive: true });
|
|
70
|
+
writeFileSync(join(dir, 'emberflow.config.mjs'), 'export default {};\n');
|
|
71
|
+
writeFileSync(
|
|
72
|
+
join(dir, 'emberflow', 'apis', 'default', 'ping.json'),
|
|
73
|
+
JSON.stringify({
|
|
74
|
+
id: 'ping',
|
|
75
|
+
name: 'Ping',
|
|
76
|
+
version: 1,
|
|
77
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
78
|
+
updatedAt: '2026-01-01T00:00:00Z',
|
|
79
|
+
http: { method: 'GET', path: '/ping' },
|
|
80
|
+
nodes: [
|
|
81
|
+
{
|
|
82
|
+
id: 'response',
|
|
83
|
+
type: 'Response',
|
|
84
|
+
label: 'Response',
|
|
85
|
+
position: { x: 0, y: 0 },
|
|
86
|
+
config: { status: 200, body: { real: true } },
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
edges: [],
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
92
|
+
writeFileSync(
|
|
93
|
+
join(dir, 'emberflow', 'apis', 'default', 'ping.scenarios.json'),
|
|
94
|
+
JSON.stringify([{ name: 'ok', input: {}, expect: { status: 200, body: { pong: true } } }]),
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
beforeAll(async () => {
|
|
99
|
+
projectDir = mkdtempSync(join(tmpdir(), 'mockserve-'));
|
|
100
|
+
mkdirSync(join(projectDir, 'emberflow', 'apis', 'default'), { recursive: true });
|
|
101
|
+
writeFileSync(join(projectDir, 'emberflow.config.mjs'), 'export default {};\n');
|
|
102
|
+
// This suite is specifically about the EMBERFLOW_MOCK flag, not the
|
|
103
|
+
// default-mock-when-unconfigured behavior (covered separately below), so
|
|
104
|
+
// give the project a configured environment to keep liveBase real.
|
|
105
|
+
writeFileSync(
|
|
106
|
+
join(projectDir, 'emberflow.environments.json'),
|
|
107
|
+
JSON.stringify({
|
|
108
|
+
defaultEnvironment: 'local',
|
|
109
|
+
environments: { local: { vars: { BASE_URL: 'http://example.test' }, secrets: {} } },
|
|
110
|
+
}),
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
// `ping`: a real Response node the mock must NOT execute — its live body
|
|
114
|
+
// differs from the scenario's mocked body, so a passing assertion proves
|
|
115
|
+
// which path answered.
|
|
116
|
+
writeFileSync(
|
|
117
|
+
join(projectDir, 'emberflow', 'apis', 'default', 'ping.json'),
|
|
118
|
+
JSON.stringify({
|
|
119
|
+
id: 'ping',
|
|
120
|
+
name: 'Ping',
|
|
121
|
+
version: 1,
|
|
122
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
123
|
+
updatedAt: '2026-01-01T00:00:00Z',
|
|
124
|
+
http: { method: 'GET', path: '/ping' },
|
|
125
|
+
nodes: [
|
|
126
|
+
{
|
|
127
|
+
id: 'response',
|
|
128
|
+
type: 'Response',
|
|
129
|
+
label: 'Response',
|
|
130
|
+
position: { x: 0, y: 0 },
|
|
131
|
+
config: { status: 200, body: { real: true } },
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
edges: [],
|
|
135
|
+
}),
|
|
136
|
+
);
|
|
137
|
+
writeFileSync(
|
|
138
|
+
join(projectDir, 'emberflow', 'apis', 'default', 'ping.scenarios.json'),
|
|
139
|
+
JSON.stringify([
|
|
140
|
+
{ name: 'ok', input: {}, expect: { status: 200, body: { pong: true } } },
|
|
141
|
+
{ name: 'teapot', input: {}, expect: { status: 418, body: { pong: 'teapot' } } },
|
|
142
|
+
]),
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// `echo`: routed op with a scenario that has NO `expect` — nothing
|
|
146
|
+
// mockable, so mock mode must answer 501.
|
|
147
|
+
writeFileSync(
|
|
148
|
+
join(projectDir, 'emberflow', 'apis', 'default', 'echo.json'),
|
|
149
|
+
JSON.stringify({
|
|
150
|
+
id: 'echo',
|
|
151
|
+
name: 'Echo',
|
|
152
|
+
version: 1,
|
|
153
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
154
|
+
updatedAt: '2026-01-01T00:00:00Z',
|
|
155
|
+
http: { method: 'GET', path: '/echo' },
|
|
156
|
+
nodes: [
|
|
157
|
+
{
|
|
158
|
+
id: 'response',
|
|
159
|
+
type: 'Response',
|
|
160
|
+
label: 'Response',
|
|
161
|
+
position: { x: 0, y: 0 },
|
|
162
|
+
config: { status: 200, body: { real: true } },
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
edges: [],
|
|
166
|
+
}),
|
|
167
|
+
);
|
|
168
|
+
writeFileSync(
|
|
169
|
+
join(projectDir, 'emberflow', 'apis', 'default', 'echo.scenarios.json'),
|
|
170
|
+
JSON.stringify([{ name: 'no-expect', input: {} }]),
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
mockProc = bootRunner(MOCK_PORT, { EMBERFLOW_PROJECT: projectDir, EMBERFLOW_MOCK: '1' });
|
|
174
|
+
liveProc = bootRunner(LIVE_PORT, { EMBERFLOW_PROJECT: projectDir });
|
|
175
|
+
|
|
176
|
+
// A second scratch project for the runtime-flip suite below, boot WITHOUT
|
|
177
|
+
// EMBERFLOW_MOCK (real by default) so POST /serving is what flips it. Also
|
|
178
|
+
// carries a bearer-protected op so the no-auth-in-mock property stays
|
|
179
|
+
// test-asserted: real 401s it, mock answers it straight from the scenario.
|
|
180
|
+
runtimeProjectDir = mkdtempSync(join(tmpdir(), 'mockserve-runtime-'));
|
|
181
|
+
mkdirSync(join(runtimeProjectDir, 'emberflow', 'apis', 'default'), { recursive: true });
|
|
182
|
+
mkdirSync(join(runtimeProjectDir, 'emberflow', 'apis', 'secure'), { recursive: true });
|
|
183
|
+
writeFileSync(join(runtimeProjectDir, 'emberflow.config.mjs'), 'export default {};\n');
|
|
184
|
+
writeFileSync(
|
|
185
|
+
join(runtimeProjectDir, 'emberflow.environments.json'),
|
|
186
|
+
JSON.stringify({
|
|
187
|
+
defaultEnvironment: 'local',
|
|
188
|
+
environments: { local: { vars: {}, secrets: { API_TOKEN: 'topsecret' } } },
|
|
189
|
+
}),
|
|
190
|
+
);
|
|
191
|
+
writeFileSync(
|
|
192
|
+
join(runtimeProjectDir, 'emberflow', 'apis', 'default', 'ping.json'),
|
|
193
|
+
JSON.stringify({
|
|
194
|
+
id: 'ping',
|
|
195
|
+
name: 'Ping',
|
|
196
|
+
version: 1,
|
|
197
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
198
|
+
updatedAt: '2026-01-01T00:00:00Z',
|
|
199
|
+
http: { method: 'GET', path: '/ping' },
|
|
200
|
+
nodes: [
|
|
201
|
+
{
|
|
202
|
+
id: 'response',
|
|
203
|
+
type: 'Response',
|
|
204
|
+
label: 'Response',
|
|
205
|
+
position: { x: 0, y: 0 },
|
|
206
|
+
config: { status: 200, body: { real: true } },
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
edges: [],
|
|
210
|
+
}),
|
|
211
|
+
);
|
|
212
|
+
writeFileSync(
|
|
213
|
+
join(runtimeProjectDir, 'emberflow', 'apis', 'default', 'ping.scenarios.json'),
|
|
214
|
+
JSON.stringify([{ name: 'ok', input: {}, expect: { status: 200, body: { pong: true } } }]),
|
|
215
|
+
);
|
|
216
|
+
// apis/secure/: every op under here inherits a bearer auth policy — the
|
|
217
|
+
// real path must enforce it, the mock path must not.
|
|
218
|
+
writeFileSync(
|
|
219
|
+
join(runtimeProjectDir, 'emberflow', 'apis', 'secure', '_meta.json'),
|
|
220
|
+
JSON.stringify({ auth: { scheme: 'bearer', secretRef: 'API_TOKEN' } }),
|
|
221
|
+
);
|
|
222
|
+
writeFileSync(
|
|
223
|
+
join(runtimeProjectDir, 'emberflow', 'apis', 'secure', 'protected.json'),
|
|
224
|
+
JSON.stringify({
|
|
225
|
+
id: 'protected',
|
|
226
|
+
name: 'Protected',
|
|
227
|
+
version: 1,
|
|
228
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
229
|
+
updatedAt: '2026-01-01T00:00:00Z',
|
|
230
|
+
http: { method: 'GET', path: '/protected' },
|
|
231
|
+
nodes: [
|
|
232
|
+
{
|
|
233
|
+
id: 'response',
|
|
234
|
+
type: 'Response',
|
|
235
|
+
label: 'Response',
|
|
236
|
+
position: { x: 0, y: 0 },
|
|
237
|
+
config: { status: 200, body: { real: true } },
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
edges: [],
|
|
241
|
+
}),
|
|
242
|
+
);
|
|
243
|
+
writeFileSync(
|
|
244
|
+
join(runtimeProjectDir, 'emberflow', 'apis', 'secure', 'protected.scenarios.json'),
|
|
245
|
+
JSON.stringify([{ name: 'ok', input: {}, expect: { status: 200, body: { mocked: true } } }]),
|
|
246
|
+
);
|
|
247
|
+
runtimeProc = bootRunner(RUNTIME_PORT, { EMBERFLOW_PROJECT: runtimeProjectDir });
|
|
248
|
+
|
|
249
|
+
// Task 1 (default-mock) fixtures.
|
|
250
|
+
// No environments file at all, no EMBERFLOW_MOCK: should default to mock.
|
|
251
|
+
noEnvProjectDir = mkdtempSync(join(tmpdir(), 'mockserve-noenv-'));
|
|
252
|
+
writePingOp(noEnvProjectDir);
|
|
253
|
+
noEnvProc = bootRunner(NO_ENV_PORT, { EMBERFLOW_PROJECT: noEnvProjectDir });
|
|
254
|
+
|
|
255
|
+
// Environments file WITH a configured environment (non-empty vars),
|
|
256
|
+
// shared by both the plain-real and the EMBERFLOW_MOCK=1-forces-mock cases.
|
|
257
|
+
withEnvProjectDir = mkdtempSync(join(tmpdir(), 'mockserve-withenv-'));
|
|
258
|
+
writePingOp(withEnvProjectDir);
|
|
259
|
+
writeFileSync(
|
|
260
|
+
join(withEnvProjectDir, 'emberflow.environments.json'),
|
|
261
|
+
JSON.stringify({
|
|
262
|
+
defaultEnvironment: 'local',
|
|
263
|
+
environments: { local: { vars: { BASE_URL: 'http://example.test' }, secrets: {} } },
|
|
264
|
+
}),
|
|
265
|
+
);
|
|
266
|
+
withEnvProc = bootRunner(WITH_ENV_PORT, { EMBERFLOW_PROJECT: withEnvProjectDir });
|
|
267
|
+
withEnvMockProc = bootRunner(WITH_ENV_MOCK_PORT, {
|
|
268
|
+
EMBERFLOW_PROJECT: withEnvProjectDir,
|
|
269
|
+
EMBERFLOW_MOCK: '1',
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
await Promise.all([
|
|
273
|
+
waitHealthy(`${mockBase}/healthz`),
|
|
274
|
+
waitHealthy(`${liveBase}/healthz`),
|
|
275
|
+
waitHealthy(`${runtimeBase}/healthz`),
|
|
276
|
+
waitHealthy(`${noEnvBase}/healthz`),
|
|
277
|
+
waitHealthy(`${withEnvBase}/healthz`),
|
|
278
|
+
waitHealthy(`${withEnvMockBase}/healthz`),
|
|
279
|
+
]);
|
|
280
|
+
}, 20_000);
|
|
281
|
+
|
|
282
|
+
afterAll(() => {
|
|
283
|
+
mockProc?.kill();
|
|
284
|
+
liveProc?.kill();
|
|
285
|
+
runtimeProc?.kill();
|
|
286
|
+
noEnvProc?.kill();
|
|
287
|
+
withEnvProc?.kill();
|
|
288
|
+
withEnvMockProc?.kill();
|
|
289
|
+
if (projectDir) rmSync(projectDir, { recursive: true, force: true });
|
|
290
|
+
if (runtimeProjectDir) rmSync(runtimeProjectDir, { recursive: true, force: true });
|
|
291
|
+
if (noEnvProjectDir) rmSync(noEnvProjectDir, { recursive: true, force: true });
|
|
292
|
+
if (withEnvProjectDir) rmSync(withEnvProjectDir, { recursive: true, force: true });
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
describe('--mock serving', () => {
|
|
296
|
+
it('healthz reports mock:true under EMBERFLOW_MOCK, mock:false without it', async () => {
|
|
297
|
+
expect(await (await fetch(`${mockBase}/healthz`)).json()).toMatchObject({ mock: true });
|
|
298
|
+
expect(await (await fetch(`${liveBase}/healthz`)).json()).toMatchObject({ mock: false });
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('answers the default (first-with-expect) scenario, with the mock header', async () => {
|
|
302
|
+
const res = await fetch(`${mockBase}/ping`);
|
|
303
|
+
expect(res.status).toBe(200);
|
|
304
|
+
expect(res.headers.get('x-emberflow-mock')).toBe('true');
|
|
305
|
+
expect(await res.json()).toEqual({ pong: true });
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('selects a named scenario via x-emberflow-scenario', async () => {
|
|
309
|
+
const res = await fetch(`${mockBase}/ping`, { headers: { 'x-emberflow-scenario': 'teapot' } });
|
|
310
|
+
expect(res.status).toBe(418);
|
|
311
|
+
expect(res.headers.get('x-emberflow-mock')).toBe('true');
|
|
312
|
+
expect(await res.json()).toEqual({ pong: 'teapot' });
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('returns 501 for an op whose scenarios lack an expect', async () => {
|
|
316
|
+
const res = await fetch(`${mockBase}/echo`);
|
|
317
|
+
expect(res.status).toBe(501);
|
|
318
|
+
expect(res.headers.get('x-emberflow-mock')).toBe('true');
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('without EMBERFLOW_MOCK, the same route executes real nodes (unchanged)', async () => {
|
|
322
|
+
const res = await fetch(`${liveBase}/ping`);
|
|
323
|
+
expect(res.status).toBe(200);
|
|
324
|
+
expect(res.headers.get('x-emberflow-mock')).toBeNull();
|
|
325
|
+
expect(await res.json()).toEqual({ real: true });
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
describe('runtime serving mode (POST /serving)', () => {
|
|
330
|
+
it('boots real (no EMBERFLOW_MOCK), serves real nodes and enforces auth', async () => {
|
|
331
|
+
expect(await (await fetch(`${runtimeBase}/healthz`)).json()).toMatchObject({ mock: false });
|
|
332
|
+
|
|
333
|
+
const ping = await fetch(`${runtimeBase}/ping`);
|
|
334
|
+
expect(ping.status).toBe(200);
|
|
335
|
+
expect(ping.headers.get('x-emberflow-mock')).toBeNull();
|
|
336
|
+
expect(await ping.json()).toEqual({ real: true });
|
|
337
|
+
|
|
338
|
+
// No-auth-in-mock is only meaningful if auth is actually enforced on the
|
|
339
|
+
// real path first — this op 401s without a token.
|
|
340
|
+
const unauthed = await fetch(`${runtimeBase}/protected`);
|
|
341
|
+
expect(unauthed.status).toBe(401);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('POST /serving {mode:"mock"} flips the SAME already-mounted routes to mock, live, no restart', async () => {
|
|
345
|
+
const flip = await postServing(runtimeBase, 'mock');
|
|
346
|
+
expect(flip.status).toBe(204);
|
|
347
|
+
|
|
348
|
+
expect(await (await fetch(`${runtimeBase}/healthz`)).json()).toMatchObject({ mock: true });
|
|
349
|
+
|
|
350
|
+
const ping = await fetch(`${runtimeBase}/ping`);
|
|
351
|
+
expect(ping.status).toBe(200);
|
|
352
|
+
expect(ping.headers.get('x-emberflow-mock')).toBe('true');
|
|
353
|
+
expect(await ping.json()).toEqual({ pong: true });
|
|
354
|
+
|
|
355
|
+
// The auth policy wired at mount time is skipped entirely in mock mode:
|
|
356
|
+
// the same protected route that just 401s in real now answers straight
|
|
357
|
+
// from the scenario expectation with NO token.
|
|
358
|
+
const protectedRes = await fetch(`${runtimeBase}/protected`);
|
|
359
|
+
expect(protectedRes.status).toBe(200);
|
|
360
|
+
expect(protectedRes.headers.get('x-emberflow-mock')).toBe('true');
|
|
361
|
+
expect(await protectedRes.json()).toEqual({ mocked: true });
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('POST /serving {mode:"real"} flips back — auth and real execution resume on the same routes', async () => {
|
|
365
|
+
const flip = await postServing(runtimeBase, 'real');
|
|
366
|
+
expect(flip.status).toBe(204);
|
|
367
|
+
|
|
368
|
+
expect(await (await fetch(`${runtimeBase}/healthz`)).json()).toMatchObject({ mock: false });
|
|
369
|
+
|
|
370
|
+
const ping = await fetch(`${runtimeBase}/ping`);
|
|
371
|
+
expect(ping.status).toBe(200);
|
|
372
|
+
expect(ping.headers.get('x-emberflow-mock')).toBeNull();
|
|
373
|
+
expect(await ping.json()).toEqual({ real: true });
|
|
374
|
+
|
|
375
|
+
const unauthed = await fetch(`${runtimeBase}/protected`);
|
|
376
|
+
expect(unauthed.status).toBe(401);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it('POST /serving with a bogus mode 400s and does not change the live mode', async () => {
|
|
380
|
+
const res = await postServing(runtimeBase, 'bogus');
|
|
381
|
+
expect(res.status).toBe(400);
|
|
382
|
+
const body = await res.json();
|
|
383
|
+
expect(typeof body.error).toBe('string');
|
|
384
|
+
|
|
385
|
+
// Still real from the previous test's flip-back.
|
|
386
|
+
expect(await (await fetch(`${runtimeBase}/healthz`)).json()).toMatchObject({ mock: false });
|
|
387
|
+
const ping = await fetch(`${runtimeBase}/ping`);
|
|
388
|
+
expect(await ping.json()).toEqual({ real: true });
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
describe('default serving mode (no environments configured)', () => {
|
|
393
|
+
it('no environments file, no EMBERFLOW_MOCK: healthz reports mock:true and the mounted route answers mock', async () => {
|
|
394
|
+
expect(await (await fetch(`${noEnvBase}/healthz`)).json()).toMatchObject({ mock: true });
|
|
395
|
+
|
|
396
|
+
const ping = await fetch(`${noEnvBase}/ping`);
|
|
397
|
+
expect(ping.status).toBe(200);
|
|
398
|
+
expect(ping.headers.get('x-emberflow-mock')).toBe('true');
|
|
399
|
+
expect(await ping.json()).toEqual({ pong: true });
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('POST /serving {mode:"real"} is still honored with zero envs — permissive override', async () => {
|
|
403
|
+
const flip = await postServing(noEnvBase, 'real');
|
|
404
|
+
expect(flip.status).toBe(204);
|
|
405
|
+
|
|
406
|
+
expect(await (await fetch(`${noEnvBase}/healthz`)).json()).toMatchObject({ mock: false });
|
|
407
|
+
const ping = await fetch(`${noEnvBase}/ping`);
|
|
408
|
+
expect(ping.status).toBe(200);
|
|
409
|
+
expect(ping.headers.get('x-emberflow-mock')).toBeNull();
|
|
410
|
+
expect(await ping.json()).toEqual({ real: true });
|
|
411
|
+
|
|
412
|
+
// Flip back so this suite's fixture state doesn't leak to later tests.
|
|
413
|
+
expect((await postServing(noEnvBase, 'mock')).status).toBe(204);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it('GET /environments reports configured:false so the studio shows its zero-env onboarding state', async () => {
|
|
417
|
+
const body = await (await fetch(`${noEnvBase}/environments`)).json();
|
|
418
|
+
expect(body.configured).toBe(false);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it('a project WITH a configured environment (>=1 env) boots real — today\'s behavior, unchanged', async () => {
|
|
422
|
+
expect(await (await fetch(`${withEnvBase}/healthz`)).json()).toMatchObject({ mock: false });
|
|
423
|
+
const ping = await fetch(`${withEnvBase}/ping`);
|
|
424
|
+
expect(ping.status).toBe(200);
|
|
425
|
+
expect(ping.headers.get('x-emberflow-mock')).toBeNull();
|
|
426
|
+
expect(await ping.json()).toEqual({ real: true });
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it('EMBERFLOW_MOCK=1 with environments present still forces mock — unchanged', async () => {
|
|
430
|
+
expect(await (await fetch(`${withEnvMockBase}/healthz`)).json()).toMatchObject({ mock: true });
|
|
431
|
+
const ping = await fetch(`${withEnvMockBase}/ping`);
|
|
432
|
+
expect(ping.status).toBe(200);
|
|
433
|
+
expect(ping.headers.get('x-emberflow-mock')).toBe('true');
|
|
434
|
+
expect(await ping.json()).toEqual({ pong: true });
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
describe('serving CLI command', () => {
|
|
439
|
+
const origRunnerUrl = process.env.EMBERFLOW_RUNNER_URL;
|
|
440
|
+
|
|
441
|
+
beforeAll(() => {
|
|
442
|
+
// Drive the CLI (client.ts) against the already-booted runtime runner
|
|
443
|
+
// above, the same way the register-API bin does.
|
|
444
|
+
process.env.EMBERFLOW_RUNNER_URL = runtimeBase;
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
afterAll(() => {
|
|
448
|
+
if (origRunnerUrl === undefined) delete process.env.EMBERFLOW_RUNNER_URL;
|
|
449
|
+
else process.env.EMBERFLOW_RUNNER_URL = origRunnerUrl;
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it('serving mock: exit 0, flips the runner to mock (healthz mock:true)', async () => {
|
|
453
|
+
const code = await runCli(['serving', 'mock']);
|
|
454
|
+
expect(code).toBe(0);
|
|
455
|
+
expect(await (await fetch(`${runtimeBase}/healthz`)).json()).toMatchObject({ mock: true });
|
|
456
|
+
|
|
457
|
+
// Flip back so this test doesn't leak mock mode to any test that follows.
|
|
458
|
+
expect(await runCli(['serving', 'real'])).toBe(0);
|
|
459
|
+
expect(await (await fetch(`${runtimeBase}/healthz`)).json()).toMatchObject({ mock: false });
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it('serving bogus: exit 2, usage failure, does not change the live mode', async () => {
|
|
463
|
+
const code = await runCli(['serving', 'bogus']);
|
|
464
|
+
expect(code).toBe(2);
|
|
465
|
+
expect(await (await fetch(`${runtimeBase}/healthz`)).json()).toMatchObject({ mock: false });
|
|
466
|
+
});
|
|
467
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { NodeRegistry } from '../src/engine';
|
|
3
|
+
import { nodesPayload } from './nodesPayload';
|
|
4
|
+
|
|
5
|
+
describe('nodesPayload', () => {
|
|
6
|
+
it('returns each node definition with its implementation source', () => {
|
|
7
|
+
const r = new NodeRegistry();
|
|
8
|
+
r.register(
|
|
9
|
+
{ type: 'Shout', label: 'Shout', category: 'demo', tags: ['x'] },
|
|
10
|
+
async (ctx) => ({ loud: String(ctx.input.text).toUpperCase() }),
|
|
11
|
+
);
|
|
12
|
+
const payload = nodesPayload(r);
|
|
13
|
+
expect(payload.nodes).toHaveLength(1);
|
|
14
|
+
const node = payload.nodes[0];
|
|
15
|
+
expect(node.type).toBe('Shout');
|
|
16
|
+
expect(node.label).toBe('Shout');
|
|
17
|
+
expect(node.category).toBe('demo');
|
|
18
|
+
expect(node.tags).toEqual(['x']);
|
|
19
|
+
expect(node.source).toContain('toUpperCase');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('is JSON-serializable (no functions leak through)', () => {
|
|
23
|
+
const r = new NodeRegistry();
|
|
24
|
+
r.register({ type: 'A', label: 'A' }, async () => ({}));
|
|
25
|
+
const round = JSON.parse(JSON.stringify(nodesPayload(r)));
|
|
26
|
+
expect(round.nodes[0].type).toBe('A');
|
|
27
|
+
expect(typeof round.nodes[0].source).toBe('string');
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { NodeDefinition, NodeRegistry } from '../src/engine';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Node metadata served to the studio over HTTP. Definitions carry everything
|
|
5
|
+
* the UI needs (label, schemas, category, tags); `source` is the
|
|
6
|
+
* implementation's toString() so the Inspector can show a node's code without
|
|
7
|
+
* the browser bundling the implementation — the key to consumer nodes
|
|
8
|
+
* appearing in the palette without a per-project browser build.
|
|
9
|
+
*/
|
|
10
|
+
export interface NodeMetaPayload {
|
|
11
|
+
nodes: Array<NodeDefinition & { source?: string }>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function nodesPayload(registry: NodeRegistry): NodeMetaPayload {
|
|
15
|
+
return {
|
|
16
|
+
nodes: registry.list().map((definition) => {
|
|
17
|
+
let source: string | undefined;
|
|
18
|
+
try {
|
|
19
|
+
source = registry.get(definition.type).implementation.toString();
|
|
20
|
+
} catch {
|
|
21
|
+
source = undefined;
|
|
22
|
+
}
|
|
23
|
+
return { ...definition, source };
|
|
24
|
+
}),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { seedParamDefaults } from './normalizeFlow';
|
|
3
|
+
import type { WorkflowDefinition, WorkflowNode } from '../src/engine';
|
|
4
|
+
|
|
5
|
+
function inputNode(config: Record<string, unknown> = {}): WorkflowNode {
|
|
6
|
+
return { id: 'input', type: 'Input', label: 'Input', position: { x: 0, y: 0 }, config };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function flow(over: Partial<WorkflowDefinition> = {}): WorkflowDefinition {
|
|
10
|
+
return {
|
|
11
|
+
id: 'f1',
|
|
12
|
+
name: 'Flow',
|
|
13
|
+
version: 1,
|
|
14
|
+
nodes: [],
|
|
15
|
+
edges: [],
|
|
16
|
+
createdAt: '2024-01-01T00:00:00.000Z',
|
|
17
|
+
updatedAt: '2024-01-01T00:00:00.000Z',
|
|
18
|
+
...over,
|
|
19
|
+
} as WorkflowDefinition;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('seedParamDefaults', () => {
|
|
23
|
+
it('seeds only missing keys as "" and returns which were seeded', () => {
|
|
24
|
+
const f = flow({
|
|
25
|
+
http: { method: 'GET', path: '/api/channels/:id/approvals/:approvalId' },
|
|
26
|
+
nodes: [inputNode({ defaults: { params: { id: 'c1' } } })],
|
|
27
|
+
});
|
|
28
|
+
const { flow: result, seeded } = seedParamDefaults(f);
|
|
29
|
+
expect(seeded).toEqual(['approvalId']);
|
|
30
|
+
const resultInput = result.nodes.find((n) => n.type === 'Input')!;
|
|
31
|
+
expect((resultInput.config?.defaults as { params: Record<string, unknown> }).params).toEqual({
|
|
32
|
+
id: 'c1',
|
|
33
|
+
approvalId: '',
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('never overwrites an existing value', () => {
|
|
38
|
+
const f = flow({
|
|
39
|
+
http: { method: 'GET', path: '/api/channels/:id' },
|
|
40
|
+
nodes: [inputNode({ defaults: { params: { id: 'real-value' } } })],
|
|
41
|
+
});
|
|
42
|
+
const { flow: result, seeded } = seedParamDefaults(f);
|
|
43
|
+
expect(seeded).toEqual([]);
|
|
44
|
+
const resultInput = result.nodes.find((n) => n.type === 'Input')!;
|
|
45
|
+
expect((resultInput.config?.defaults as { params: Record<string, unknown> }).params).toEqual({
|
|
46
|
+
id: 'real-value',
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('is a no-op without an Input node — returns seeded: [] and the same object', () => {
|
|
51
|
+
const f = flow({ http: { method: 'GET', path: '/api/channels/:id' }, nodes: [] });
|
|
52
|
+
const { flow: result, seeded } = seedParamDefaults(f);
|
|
53
|
+
expect(seeded).toEqual([]);
|
|
54
|
+
expect(result).toBe(f);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('is a no-op without :params in the path — returns seeded: [] and the same object', () => {
|
|
58
|
+
const f = flow({ http: { method: 'GET', path: '/api/channels' }, nodes: [inputNode()] });
|
|
59
|
+
const { flow: result, seeded } = seedParamDefaults(f);
|
|
60
|
+
expect(seeded).toEqual([]);
|
|
61
|
+
expect(result).toBe(f);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('is a no-op when every param already has a default — returns seeded: [] and the same object', () => {
|
|
65
|
+
const f = flow({
|
|
66
|
+
http: { method: 'GET', path: '/api/channels/:id' },
|
|
67
|
+
nodes: [inputNode({ defaults: { params: { id: '' } } })],
|
|
68
|
+
});
|
|
69
|
+
const { flow: result, seeded } = seedParamDefaults(f);
|
|
70
|
+
expect(seeded).toEqual([]);
|
|
71
|
+
expect(result).toBe(f);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('is a no-op when there is no http trigger at all', () => {
|
|
75
|
+
const f = flow({ nodes: [inputNode()] });
|
|
76
|
+
const { flow: result, seeded } = seedParamDefaults(f);
|
|
77
|
+
expect(seeded).toEqual([]);
|
|
78
|
+
expect(result).toBe(f);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { parsePathParams } from '../src/lib/pathParams';
|
|
2
|
+
import type { WorkflowDefinition, WorkflowNode } from '../src/engine';
|
|
3
|
+
|
|
4
|
+
export interface SeedParamDefaultsResult {
|
|
5
|
+
flow: WorkflowDefinition;
|
|
6
|
+
/** Path-param names that were newly seeded with `''`, in path order. */
|
|
7
|
+
seeded: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Pure normalization step run before validation/persist on every write path
|
|
12
|
+
* (PUT /workflows/:id, POST /operations): seeds an empty-string placeholder
|
|
13
|
+
* under the first Input node's `config.defaults.params` for every `:param`
|
|
14
|
+
* in `flow.http.path` that has no default yet, so a plain Run reaches nodes
|
|
15
|
+
* with `params.<name> === ''` instead of crashing on `undefined` (same
|
|
16
|
+
* scaffold shape `emberflow create` already produces).
|
|
17
|
+
*
|
|
18
|
+
* Mirrors the `missing-param-default` guard semantics (src/engine/diagnostics.ts):
|
|
19
|
+
* presence, not meaningfulness, is what's checked/seeded. No-ops (returning
|
|
20
|
+
* the SAME `flow` object, unmodified) when there's no `:param` in the path,
|
|
21
|
+
* no Input node, or every param already has a default — never overwrites an
|
|
22
|
+
* existing value, whatever it is (including `''`).
|
|
23
|
+
*/
|
|
24
|
+
export function seedParamDefaults(flow: WorkflowDefinition): SeedParamDefaultsResult {
|
|
25
|
+
const path = flow.http?.path;
|
|
26
|
+
const params = path ? parsePathParams(path) : [];
|
|
27
|
+
if (params.length === 0) return { flow, seeded: [] };
|
|
28
|
+
|
|
29
|
+
const inputNode = flow.nodes.find((n) => n.type === 'Input');
|
|
30
|
+
if (!inputNode) return { flow, seeded: [] };
|
|
31
|
+
|
|
32
|
+
const existingDefaults = inputNode.config?.defaults as Record<string, unknown> | undefined;
|
|
33
|
+
const existingParams =
|
|
34
|
+
existingDefaults && typeof existingDefaults === 'object' && !Array.isArray(existingDefaults)
|
|
35
|
+
? (existingDefaults.params as Record<string, unknown> | undefined)
|
|
36
|
+
: undefined;
|
|
37
|
+
|
|
38
|
+
const seeded = params.filter((name) => !existingParams || existingParams[name] === undefined);
|
|
39
|
+
if (seeded.length === 0) return { flow, seeded: [] };
|
|
40
|
+
|
|
41
|
+
const newParams = { ...(existingParams ?? {}) };
|
|
42
|
+
for (const name of seeded) newParams[name] = '';
|
|
43
|
+
|
|
44
|
+
const newInputNode: WorkflowNode = {
|
|
45
|
+
...inputNode,
|
|
46
|
+
config: {
|
|
47
|
+
...(inputNode.config ?? {}),
|
|
48
|
+
defaults: { ...(existingDefaults ?? {}), params: newParams },
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
flow: {
|
|
54
|
+
...flow,
|
|
55
|
+
nodes: flow.nodes.map((n) => (n === inputNode ? newInputNode : n)),
|
|
56
|
+
},
|
|
57
|
+
seeded,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { openBrowser } from './openBrowser';
|
|
3
|
+
|
|
4
|
+
describe('openBrowser', () => {
|
|
5
|
+
it('invokes the platform opener with the url', () => {
|
|
6
|
+
const calls: Array<[string, string[]]> = [];
|
|
7
|
+
const fakeSpawn = ((cmd: string, args: string[]) => { calls.push([cmd, args]); return { unref() {} }; }) as never;
|
|
8
|
+
openBrowser('http://localhost:8092', { platform: 'darwin', spawn: fakeSpawn });
|
|
9
|
+
expect(calls[0][0]).toBe('open');
|
|
10
|
+
expect(calls[0][1]).toContain('http://localhost:8092');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('uses xdg-open on linux and start on win32', () => {
|
|
14
|
+
const seen: string[] = [];
|
|
15
|
+
const fakeSpawn = ((cmd: string) => { seen.push(cmd); return { unref() {} }; }) as never;
|
|
16
|
+
openBrowser('http://x', { platform: 'linux', spawn: fakeSpawn });
|
|
17
|
+
openBrowser('http://x', { platform: 'win32', spawn: fakeSpawn });
|
|
18
|
+
expect(seen).toEqual(['xdg-open', 'cmd']);
|
|
19
|
+
});
|
|
20
|
+
});
|