@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,280 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createDefaultRegistry } from './index';
|
|
3
|
+
import { startRun } from '../engine';
|
|
4
|
+
import type { LogLine, WorkflowDefinition, WorkflowRun } from '../engine';
|
|
5
|
+
|
|
6
|
+
const inputOnlyFlow = (): WorkflowDefinition => ({
|
|
7
|
+
id: 'input-only',
|
|
8
|
+
name: 'Input Only',
|
|
9
|
+
version: 1,
|
|
10
|
+
nodes: [
|
|
11
|
+
{
|
|
12
|
+
id: 'input',
|
|
13
|
+
type: 'Input',
|
|
14
|
+
label: 'Input',
|
|
15
|
+
position: { x: 0, y: 0 },
|
|
16
|
+
config: {
|
|
17
|
+
fields: [
|
|
18
|
+
{ name: 'username', type: 'string', required: true },
|
|
19
|
+
{ name: 'password', type: 'string', required: true },
|
|
20
|
+
],
|
|
21
|
+
defaults: { username: 'ada' },
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
edges: [],
|
|
26
|
+
createdAt: '2026-07-02T00:00:00Z',
|
|
27
|
+
updatedAt: '2026-07-02T00:00:00Z',
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('Input node', () => {
|
|
31
|
+
it('fails listing the missing required field(s)', async () => {
|
|
32
|
+
const run = await startRun({ flow: inputOnlyFlow(), registry: createDefaultRegistry(0) }).runToEnd();
|
|
33
|
+
expect(run.status).toBe('failed');
|
|
34
|
+
expect(run.nodeStates.input.error).toBe('Missing required input field(s): password');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('merges the invocation payload over configured defaults', async () => {
|
|
38
|
+
const run = await startRun({
|
|
39
|
+
flow: inputOnlyFlow(),
|
|
40
|
+
registry: createDefaultRegistry(0),
|
|
41
|
+
input: { password: 'lovelace' },
|
|
42
|
+
}).runToEnd();
|
|
43
|
+
expect(run.status).toBe('succeeded');
|
|
44
|
+
expect(run.nodeStates.input.output).toEqual({ username: 'ada', password: 'lovelace' });
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const environmentFlow = (): WorkflowDefinition => ({
|
|
49
|
+
id: 'environment-only',
|
|
50
|
+
name: 'Environment Only',
|
|
51
|
+
version: 1,
|
|
52
|
+
nodes: [
|
|
53
|
+
{ id: 'env', type: 'Environment', label: 'Environment', position: { x: 0, y: 0 }, config: {} },
|
|
54
|
+
],
|
|
55
|
+
edges: [],
|
|
56
|
+
createdAt: '2026-07-05T00:00:00Z',
|
|
57
|
+
updatedAt: '2026-07-05T00:00:00Z',
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('Environment node', () => {
|
|
61
|
+
it('passes the run environment through to its output', async () => {
|
|
62
|
+
const run = await startRun({
|
|
63
|
+
flow: environmentFlow(),
|
|
64
|
+
registry: createDefaultRegistry(0),
|
|
65
|
+
environment: 'prod',
|
|
66
|
+
}).runToEnd();
|
|
67
|
+
expect(run.status).toBe('succeeded');
|
|
68
|
+
expect(run.nodeStates.env.output).toEqual({ environment: 'prod', isProd: true });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('defaults to "local" (isProd false) when the run carries no environment', async () => {
|
|
72
|
+
const run = await startRun({
|
|
73
|
+
flow: environmentFlow(),
|
|
74
|
+
registry: createDefaultRegistry(0),
|
|
75
|
+
}).runToEnd();
|
|
76
|
+
expect(run.status).toBe('succeeded');
|
|
77
|
+
expect(run.nodeStates.env.output).toEqual({ environment: 'local', isProd: false });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('isProd is false for any non-prod environment name', async () => {
|
|
81
|
+
const run = await startRun({
|
|
82
|
+
flow: environmentFlow(),
|
|
83
|
+
registry: createDefaultRegistry(0),
|
|
84
|
+
environment: 'dev',
|
|
85
|
+
}).runToEnd();
|
|
86
|
+
expect(run.status).toBe('succeeded');
|
|
87
|
+
expect(run.nodeStates.env.output).toEqual({ environment: 'dev', isProd: false });
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const conditionalFlow = (
|
|
92
|
+
value: unknown,
|
|
93
|
+
branches: unknown[],
|
|
94
|
+
fallback?: string,
|
|
95
|
+
): WorkflowDefinition => ({
|
|
96
|
+
id: 'conditional-only',
|
|
97
|
+
name: 'Conditional Only',
|
|
98
|
+
version: 1,
|
|
99
|
+
nodes: [
|
|
100
|
+
{
|
|
101
|
+
id: 'cond',
|
|
102
|
+
type: 'Conditional',
|
|
103
|
+
label: 'Conditional',
|
|
104
|
+
position: { x: 0, y: 0 },
|
|
105
|
+
config: { value, branches, ...(fallback !== undefined ? { fallback } : {}) },
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
edges: [],
|
|
109
|
+
createdAt: '2026-07-03T00:00:00Z',
|
|
110
|
+
updatedAt: '2026-07-03T00:00:00Z',
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
async function runConditional(
|
|
114
|
+
value: unknown,
|
|
115
|
+
branches: unknown[],
|
|
116
|
+
fallback?: string,
|
|
117
|
+
): Promise<{ run: WorkflowRun; logs: LogLine[] }> {
|
|
118
|
+
const logs: LogLine[] = [];
|
|
119
|
+
const run = await startRun({
|
|
120
|
+
flow: conditionalFlow(value, branches, fallback),
|
|
121
|
+
registry: createDefaultRegistry(0),
|
|
122
|
+
events: { onLog: (line) => logs.push(line) },
|
|
123
|
+
}).runToEnd();
|
|
124
|
+
return { run, logs };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
describe('Conditional node', () => {
|
|
128
|
+
it('matches eq on strict equality', async () => {
|
|
129
|
+
const { run } = await runConditional('active', [{ name: 'A', op: 'eq', value: 'active' }]);
|
|
130
|
+
expect(run.nodeStates.cond.output).toEqual({ value: 'active', $branch: 'A' });
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('matches eq on numeric-looking values of different types', async () => {
|
|
134
|
+
const { run } = await runConditional('3', [{ name: 'A', op: 'eq', value: 3 }]);
|
|
135
|
+
expect(run.nodeStates.cond.output).toEqual({ value: '3', $branch: 'A' });
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('neq matches when values differ', async () => {
|
|
139
|
+
const { run } = await runConditional('active', [{ name: 'A', op: 'neq', value: 'inactive' }]);
|
|
140
|
+
expect(run.nodeStates.cond.output).toEqual({ value: 'active', $branch: 'A' });
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('neq does not match equal (numeric-looking) values', async () => {
|
|
144
|
+
const { run } = await runConditional('3', [
|
|
145
|
+
{ name: 'A', op: 'neq', value: 3 },
|
|
146
|
+
{ name: 'B', op: 'eq', value: 3 },
|
|
147
|
+
]);
|
|
148
|
+
expect(run.nodeStates.cond.output).toEqual({ value: '3', $branch: 'B' });
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('gt matches with Number() coercion', async () => {
|
|
152
|
+
const { run } = await runConditional('5', [{ name: 'A', op: 'gt', value: 3 }]);
|
|
153
|
+
expect(run.nodeStates.cond.output).toEqual({ value: '5', $branch: 'A' });
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('gte matches at the boundary', async () => {
|
|
157
|
+
const { run } = await runConditional(3, [{ name: 'A', op: 'gte', value: 3 }]);
|
|
158
|
+
expect(run.nodeStates.cond.output).toEqual({ value: 3, $branch: 'A' });
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('lt matches with Number() coercion', async () => {
|
|
162
|
+
const { run } = await runConditional(1, [{ name: 'A', op: 'lt', value: 3 }]);
|
|
163
|
+
expect(run.nodeStates.cond.output).toEqual({ value: 1, $branch: 'A' });
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('lte matches at the boundary', async () => {
|
|
167
|
+
const { run } = await runConditional(3, [{ name: 'A', op: 'lte', value: 3 }]);
|
|
168
|
+
expect(run.nodeStates.cond.output).toEqual({ value: 3, $branch: 'A' });
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('contains matches substrings on strings', async () => {
|
|
172
|
+
const { run } = await runConditional('hello world', [{ name: 'A', op: 'contains', value: 'world' }]);
|
|
173
|
+
expect(run.nodeStates.cond.output).toEqual({ value: 'hello world', $branch: 'A' });
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('contains matches membership on arrays', async () => {
|
|
177
|
+
const { run } = await runConditional(['a', 'b'], [{ name: 'A', op: 'contains', value: 'b' }]);
|
|
178
|
+
expect(run.nodeStates.cond.output).toEqual({ value: ['a', 'b'], $branch: 'A' });
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('contains does not match non-string, non-array input', async () => {
|
|
182
|
+
const { run } = await runConditional(42, [
|
|
183
|
+
{ name: 'A', op: 'contains', value: 4 },
|
|
184
|
+
{ name: 'fallback', op: 'exists', value: undefined },
|
|
185
|
+
]);
|
|
186
|
+
expect(run.nodeStates.cond.output).toEqual({ value: 42, $branch: 'fallback' });
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('exists matches defined, non-null input', async () => {
|
|
190
|
+
const { run } = await runConditional(0, [{ name: 'A', op: 'exists' }]);
|
|
191
|
+
expect(run.nodeStates.cond.output).toEqual({ value: 0, $branch: 'A' });
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('exists does not match null or undefined input', async () => {
|
|
195
|
+
const { run } = await runConditional(null, [
|
|
196
|
+
{ name: 'A', op: 'exists' },
|
|
197
|
+
{ name: 'B', op: 'truthy' },
|
|
198
|
+
], 'clean');
|
|
199
|
+
expect(run.nodeStates.cond.output).toEqual({ value: null, $branch: 'clean' });
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('truthy matches non-falsy input', async () => {
|
|
203
|
+
const { run } = await runConditional('x', [{ name: 'A', op: 'truthy' }]);
|
|
204
|
+
expect(run.nodeStates.cond.output).toEqual({ value: 'x', $branch: 'A' });
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('truthy does not match falsy input', async () => {
|
|
208
|
+
const { run } = await runConditional(0, [{ name: 'A', op: 'truthy' }], 'clean');
|
|
209
|
+
expect(run.nodeStates.cond.output).toEqual({ value: 0, $branch: 'clean' });
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('first matching rule wins, in order', async () => {
|
|
213
|
+
const { run } = await runConditional(5, [
|
|
214
|
+
{ name: 'P1', op: 'gt', value: 3 },
|
|
215
|
+
{ name: 'P2', op: 'gt', value: 0 },
|
|
216
|
+
]);
|
|
217
|
+
expect(run.nodeStates.cond.output).toEqual({ value: 5, $branch: 'P1' });
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('falls back to the configured fallback branch when no rule matches', async () => {
|
|
221
|
+
const { run } = await runConditional(-1, [
|
|
222
|
+
{ name: 'P1', op: 'gt', value: 3 },
|
|
223
|
+
{ name: 'P2', op: 'gt', value: 0 },
|
|
224
|
+
], 'clean');
|
|
225
|
+
expect(run.nodeStates.cond.output).toEqual({ value: -1, $branch: 'clean' });
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('fails when no rule matches and no fallback is configured', async () => {
|
|
229
|
+
const { run } = await runConditional(-1, [{ name: 'P1', op: 'gt', value: 3 }]);
|
|
230
|
+
expect(run.status).toBe('failed');
|
|
231
|
+
expect(run.nodeStates.cond.error).toBe('Conditional: no rule matched and no fallback is set');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('non-numeric input on a numeric op falls through to the fallback', async () => {
|
|
235
|
+
const { run } = await runConditional('not-a-number', [
|
|
236
|
+
{ name: 'P1', op: 'gt', value: 3 },
|
|
237
|
+
], 'clean');
|
|
238
|
+
expect(run.nodeStates.cond.output).toEqual({ value: 'not-a-number', $branch: 'clean' });
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('skips a malformed rule (missing name) with a warn log and keeps evaluating', async () => {
|
|
242
|
+
const { run, logs } = await runConditional(5, [
|
|
243
|
+
{ op: 'gt', value: 3 },
|
|
244
|
+
{ name: 'P2', op: 'gt', value: 0 },
|
|
245
|
+
]);
|
|
246
|
+
expect(run.nodeStates.cond.output).toEqual({ value: 5, $branch: 'P2' });
|
|
247
|
+
expect(logs.some((l) => l.level === 'warn' && l.message.includes('malformed rule'))).toBe(true);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('skips a malformed rule (unknown op) with a warn log and keeps evaluating', async () => {
|
|
251
|
+
const { run, logs } = await runConditional(5, [
|
|
252
|
+
{ name: 'P1', op: 'between', value: [0, 10] },
|
|
253
|
+
{ name: 'P2', op: 'gt', value: 0 },
|
|
254
|
+
]);
|
|
255
|
+
expect(run.nodeStates.cond.output).toEqual({ value: 5, $branch: 'P2' });
|
|
256
|
+
expect(logs.some((l) => l.level === 'warn' && l.message.includes('malformed rule'))).toBe(true);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
describe('Conditional numeric-coercion guards', () => {
|
|
261
|
+
it('eq 0 does not match null, empty string, or empty array', async () => {
|
|
262
|
+
for (const input of [null, '', []]) {
|
|
263
|
+
const { run } = await runConditional(
|
|
264
|
+
input,
|
|
265
|
+
[{ name: 'zero', op: 'eq', value: 0 }],
|
|
266
|
+
'other',
|
|
267
|
+
);
|
|
268
|
+
expect(run.nodeStates.cond.output).toEqual({ value: input, $branch: 'other' });
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('gt -1 does not match null', async () => {
|
|
273
|
+
const { run } = await runConditional(
|
|
274
|
+
null,
|
|
275
|
+
[{ name: 'pos', op: 'gt', value: -1 }],
|
|
276
|
+
'other',
|
|
277
|
+
);
|
|
278
|
+
expect(run.nodeStates.cond.output).toEqual({ value: null, $branch: 'other' });
|
|
279
|
+
});
|
|
280
|
+
});
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { getByPath } from '../engine';
|
|
2
|
+
import type { NodeRegistry } from '../engine';
|
|
3
|
+
|
|
4
|
+
/** A single Conditional rule row: `{ name, op, value? }`. */
|
|
5
|
+
interface ConditionalRule {
|
|
6
|
+
name?: unknown;
|
|
7
|
+
op?: unknown;
|
|
8
|
+
value?: unknown;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const CONDITIONAL_OPS = new Set([
|
|
12
|
+
'eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'contains', 'exists', 'truthy',
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Only numbers and non-empty numeric strings take part in numeric coercion —
|
|
17
|
+
* Number(null), Number(''), and Number([]) are all 0, which would make
|
|
18
|
+
* `eq 0` match null/''/[] and `gt -1` match null.
|
|
19
|
+
*/
|
|
20
|
+
function asComparableNumber(v: unknown): number {
|
|
21
|
+
if (typeof v === 'number') return v;
|
|
22
|
+
if (typeof v === 'string' && v.trim() !== '') return Number(v);
|
|
23
|
+
return NaN;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** eq/neq base: strict equality, plus numeric-looking equality (`Number()` on both sides). */
|
|
27
|
+
function looksEqual(input: unknown, comparand: unknown): boolean {
|
|
28
|
+
if (input === comparand) return true;
|
|
29
|
+
const a = asComparableNumber(input);
|
|
30
|
+
const b = asComparableNumber(comparand);
|
|
31
|
+
return !Number.isNaN(a) && !Number.isNaN(b) && a === b;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function numericMatch(op: 'gt' | 'gte' | 'lt' | 'lte', input: unknown, comparand: unknown): boolean {
|
|
35
|
+
const a = asComparableNumber(input);
|
|
36
|
+
const b = asComparableNumber(comparand);
|
|
37
|
+
if (Number.isNaN(a) || Number.isNaN(b)) return false;
|
|
38
|
+
switch (op) {
|
|
39
|
+
case 'gt': return a > b;
|
|
40
|
+
case 'gte': return a >= b;
|
|
41
|
+
case 'lt': return a < b;
|
|
42
|
+
case 'lte': return a <= b;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function ruleMatches(op: string, input: unknown, comparand: unknown): boolean {
|
|
47
|
+
switch (op) {
|
|
48
|
+
case 'eq': return looksEqual(input, comparand);
|
|
49
|
+
case 'neq': return !looksEqual(input, comparand);
|
|
50
|
+
case 'gt': case 'gte': case 'lt': case 'lte':
|
|
51
|
+
return numericMatch(op, input, comparand);
|
|
52
|
+
case 'contains':
|
|
53
|
+
if (typeof input === 'string') return input.includes(String(comparand));
|
|
54
|
+
if (Array.isArray(input)) return input.includes(comparand);
|
|
55
|
+
return false;
|
|
56
|
+
case 'exists':
|
|
57
|
+
return input !== undefined && input !== null;
|
|
58
|
+
case 'truthy':
|
|
59
|
+
return Boolean(input);
|
|
60
|
+
default:
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Flow-control nodes. Route reads a field from its input object and emits
|
|
67
|
+
* `$branch`; the executor then skips downstream nodes hanging off untaken
|
|
68
|
+
* branch handles (edge.sourceHandle = branch name).
|
|
69
|
+
*/
|
|
70
|
+
export function registerFlowControlNodes(registry: NodeRegistry): void {
|
|
71
|
+
registry.register(
|
|
72
|
+
{
|
|
73
|
+
type: 'Input',
|
|
74
|
+
label: 'Input',
|
|
75
|
+
description:
|
|
76
|
+
'Entry point — emits the payload the run was invoked with (falling back to configured defaults).',
|
|
77
|
+
simpleDescription: 'Starts the run with its input data',
|
|
78
|
+
category: 'input',
|
|
79
|
+
traceKind: 'compute',
|
|
80
|
+
tags: ['entry'],
|
|
81
|
+
configSchema: {
|
|
82
|
+
fields: [
|
|
83
|
+
{ name: 'fields', type: 'array' },
|
|
84
|
+
{ name: 'defaults', type: 'object' },
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
// Dynamic — the emitted shape depends on the configured fields.
|
|
88
|
+
outputSchema: { fields: [] },
|
|
89
|
+
},
|
|
90
|
+
async (ctx) => {
|
|
91
|
+
const merged = {
|
|
92
|
+
...((ctx.config.defaults as Record<string, unknown>) ?? {}),
|
|
93
|
+
...ctx.runInput,
|
|
94
|
+
};
|
|
95
|
+
const declared = Array.isArray(ctx.config.fields)
|
|
96
|
+
? (ctx.config.fields as { name: string; required?: boolean }[])
|
|
97
|
+
: [];
|
|
98
|
+
const missing = declared
|
|
99
|
+
.filter((f) => f.required && merged[f.name] === undefined)
|
|
100
|
+
.map((f) => f.name);
|
|
101
|
+
if (missing.length) {
|
|
102
|
+
throw new Error(`Missing required input field(s): ${missing.join(', ')}`);
|
|
103
|
+
}
|
|
104
|
+
const summary = JSON.stringify(merged);
|
|
105
|
+
// Full JSON, untruncated: the log UI renders parseable payloads as
|
|
106
|
+
// collapsible highlighted blocks — a sliced tail breaks the parse and
|
|
107
|
+
// regresses to a wall of escaped text.
|
|
108
|
+
ctx.log('info', `Run input: ${summary}`);
|
|
109
|
+
return merged;
|
|
110
|
+
},
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
registry.register(
|
|
114
|
+
{
|
|
115
|
+
type: 'Environment',
|
|
116
|
+
label: 'Environment',
|
|
117
|
+
description:
|
|
118
|
+
'Reports the environment this run targets. Emits `environment` (the run\'s target environment name, e.g. "prod"; "local" when the run carries none) and `isProd` (a convenience boolean, true only when environment === "prod"). PORT NOTE: the name comes from the run\'s target environment threaded through node ctx (ctx.environment) — it is undefined for browser/in-tab runs, which report "local". `isProd` exists so the common case (branch on "is this production?") is a one-field Conditional; branch on `environment` directly for finer-grained routing.',
|
|
119
|
+
simpleDescription: 'Reports which environment this run targets, so steps can branch on it.',
|
|
120
|
+
category: 'logic',
|
|
121
|
+
traceKind: 'compute',
|
|
122
|
+
tags: ['branching'],
|
|
123
|
+
// No inputs — the environment is read from the run context.
|
|
124
|
+
inputSchema: { fields: [] },
|
|
125
|
+
outputSchema: {
|
|
126
|
+
fields: [
|
|
127
|
+
{ name: 'environment', type: 'string' },
|
|
128
|
+
{ name: 'isProd', type: 'boolean' },
|
|
129
|
+
],
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
async (ctx) => {
|
|
133
|
+
const environment = ctx.environment ?? 'local';
|
|
134
|
+
const isProd = environment === 'prod';
|
|
135
|
+
ctx.log('info', `Environment: ${environment} (isProd=${isProd})`);
|
|
136
|
+
return { environment, isProd };
|
|
137
|
+
},
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
registry.register(
|
|
141
|
+
{
|
|
142
|
+
type: 'Route',
|
|
143
|
+
label: 'Route',
|
|
144
|
+
description:
|
|
145
|
+
'Branches the flow on a field of its input. Edges leaving a named branch handle only run when that branch matches.',
|
|
146
|
+
simpleDescription: 'Sends the data down a different path based on one value',
|
|
147
|
+
category: 'logic',
|
|
148
|
+
traceKind: 'compute',
|
|
149
|
+
tags: ['branching'],
|
|
150
|
+
inputSchema: {
|
|
151
|
+
fields: [{ name: 'value', type: 'object', required: true }],
|
|
152
|
+
},
|
|
153
|
+
configSchema: {
|
|
154
|
+
fields: [
|
|
155
|
+
{ name: 'field', type: 'string', required: true },
|
|
156
|
+
{ name: 'branches', type: 'array', required: true },
|
|
157
|
+
{ name: 'fallback', type: 'string' },
|
|
158
|
+
],
|
|
159
|
+
},
|
|
160
|
+
outputSchema: {
|
|
161
|
+
fields: [{ name: '$branch', type: 'string' }],
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
async (ctx) => {
|
|
165
|
+
const value = (ctx.input.value ?? {}) as Record<string, unknown>;
|
|
166
|
+
const field = String(ctx.config.field ?? '');
|
|
167
|
+
const branches = Array.isArray(ctx.config.branches)
|
|
168
|
+
? (ctx.config.branches as unknown[]).map(String)
|
|
169
|
+
: [];
|
|
170
|
+
const raw = getByPath(value, field);
|
|
171
|
+
const candidate = String(raw);
|
|
172
|
+
let branch: string | undefined = branches.includes(candidate) ? candidate : undefined;
|
|
173
|
+
if (branch === undefined && typeof ctx.config.fallback === 'string' && ctx.config.fallback) {
|
|
174
|
+
branch = ctx.config.fallback;
|
|
175
|
+
}
|
|
176
|
+
if (branch === undefined) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`Route: "${field}" resolved to "${candidate}", which matches no branch and no fallback is set`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
ctx.log('info', `Routing ${field}=${candidate} → branch "${branch}"`);
|
|
182
|
+
return { ...value, $branch: branch };
|
|
183
|
+
},
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
registry.register(
|
|
187
|
+
{
|
|
188
|
+
type: 'Conditional',
|
|
189
|
+
label: 'Conditional',
|
|
190
|
+
description:
|
|
191
|
+
'Branches the flow on an ordered set of rules evaluated against its input. The first matching rule wins; edges leaving a named branch handle only run when that branch matches.',
|
|
192
|
+
simpleDescription: 'Chooses a path based on the data',
|
|
193
|
+
category: 'logic',
|
|
194
|
+
traceKind: 'compute',
|
|
195
|
+
tags: ['branching'],
|
|
196
|
+
inputSchema: {
|
|
197
|
+
fields: [{ name: 'value', type: 'object', required: true }],
|
|
198
|
+
},
|
|
199
|
+
configSchema: {
|
|
200
|
+
fields: [
|
|
201
|
+
{ name: 'branches', type: 'array', required: true },
|
|
202
|
+
{ name: 'fallback', type: 'string' },
|
|
203
|
+
],
|
|
204
|
+
},
|
|
205
|
+
outputSchema: {
|
|
206
|
+
fields: [{ name: '$branch', type: 'string' }],
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
async (ctx) => {
|
|
210
|
+
const value = ctx.input.value;
|
|
211
|
+
const rules = Array.isArray(ctx.config.branches) ? (ctx.config.branches as unknown[]) : [];
|
|
212
|
+
let branch: string | undefined;
|
|
213
|
+
for (const raw of rules) {
|
|
214
|
+
const rule = (raw !== null && typeof raw === 'object' ? raw : {}) as ConditionalRule;
|
|
215
|
+
const name = typeof rule.name === 'string' && rule.name ? rule.name : undefined;
|
|
216
|
+
const op = typeof rule.op === 'string' ? rule.op : undefined;
|
|
217
|
+
if (!name || !op || !CONDITIONAL_OPS.has(op)) {
|
|
218
|
+
ctx.log('warn', `Conditional: skipping malformed rule ${JSON.stringify(raw)}`);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (ruleMatches(op, value, rule.value)) {
|
|
222
|
+
branch = name;
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (branch === undefined && typeof ctx.config.fallback === 'string' && ctx.config.fallback) {
|
|
227
|
+
branch = ctx.config.fallback;
|
|
228
|
+
}
|
|
229
|
+
if (branch === undefined) {
|
|
230
|
+
throw new Error('Conditional: no rule matched and no fallback is set');
|
|
231
|
+
}
|
|
232
|
+
ctx.log('info', `Conditional matched → branch "${branch}"`);
|
|
233
|
+
return { value, $branch: branch };
|
|
234
|
+
},
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
registry.register(
|
|
238
|
+
{
|
|
239
|
+
type: 'Merge',
|
|
240
|
+
label: 'Merge',
|
|
241
|
+
description:
|
|
242
|
+
'Coalesces converging branch arms into one value: emits `value` = the first of its mapped inputs that is neither undefined nor null, in inputMap declaration order. PORT NOTE: the engine leaves an inputMap field undefined when its source node was skipped (an untaken branch arm), so wiring one input per arm makes `value` resolve to whichever arm actually ran. Use it as the join point after a Conditional whose arms each produce the same downstream value from a DIFFERENT node.',
|
|
243
|
+
simpleDescription: 'Joins branch paths — passes on whichever arm produced a value.',
|
|
244
|
+
category: 'logic',
|
|
245
|
+
traceKind: 'compute',
|
|
246
|
+
tags: ['branching'],
|
|
247
|
+
// Free-form: the converging arm values are supplied via inputMap, one
|
|
248
|
+
// field per arm; declaration order sets coalesce priority.
|
|
249
|
+
inputSchema: { fields: [] },
|
|
250
|
+
outputSchema: {
|
|
251
|
+
fields: [{ name: 'value', type: 'object' }],
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
async (ctx) => {
|
|
255
|
+
const entries = Object.entries(ctx.input);
|
|
256
|
+
const chosen = entries.find(([, v]) => v !== undefined && v !== null);
|
|
257
|
+
if (!chosen) {
|
|
258
|
+
ctx.log('warn', 'Merge: no incoming arm produced a value');
|
|
259
|
+
return { value: undefined };
|
|
260
|
+
}
|
|
261
|
+
ctx.log('info', `Merge: passing on value from "${chosen[0]}"`);
|
|
262
|
+
return { value: chosen[1] };
|
|
263
|
+
},
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
registry.register(
|
|
267
|
+
{
|
|
268
|
+
type: 'Subflow',
|
|
269
|
+
label: 'Subflow',
|
|
270
|
+
description:
|
|
271
|
+
'Runs another workflow to completion with the mapped input, then emits that child run\'s collected output.',
|
|
272
|
+
simpleDescription: 'Runs another workflow and uses its result',
|
|
273
|
+
category: 'logic',
|
|
274
|
+
traceKind: 'compute',
|
|
275
|
+
tags: ['subflow'],
|
|
276
|
+
// Free-form input: whatever is mapped onto this node becomes the child
|
|
277
|
+
// run's invocation payload (like an Input node's run input).
|
|
278
|
+
inputSchema: { fields: [] },
|
|
279
|
+
configSchema: {
|
|
280
|
+
fields: [{ name: 'workflowId', type: 'string', required: true }],
|
|
281
|
+
},
|
|
282
|
+
// Dynamic — the emitted shape is the child flow's Result output.
|
|
283
|
+
outputSchema: { fields: [] },
|
|
284
|
+
},
|
|
285
|
+
async (ctx) => {
|
|
286
|
+
const workflowId = String(ctx.config.workflowId ?? '');
|
|
287
|
+
if (!workflowId) throw new Error('Subflow: no workflowId configured');
|
|
288
|
+
if (!ctx.runSubflow) {
|
|
289
|
+
throw new Error('subflows need a host that can look up workflows');
|
|
290
|
+
}
|
|
291
|
+
ctx.log('info', `Running subflow "${workflowId}"`);
|
|
292
|
+
const result = await ctx.runSubflow(workflowId, ctx.input);
|
|
293
|
+
if (result.status === 'failed') {
|
|
294
|
+
throw new Error(result.error ?? 'subflow failed');
|
|
295
|
+
}
|
|
296
|
+
const output = result.output;
|
|
297
|
+
// Spread a plain object so downstream nodes can map its fields directly;
|
|
298
|
+
// wrap anything else under `output`.
|
|
299
|
+
if (output !== null && typeof output === 'object' && !Array.isArray(output)) {
|
|
300
|
+
return { ...(output as Record<string, unknown>) };
|
|
301
|
+
}
|
|
302
|
+
return { output };
|
|
303
|
+
},
|
|
304
|
+
);
|
|
305
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { NodeRegistry } from '../engine';
|
|
2
|
+
import { createLoginRegistry } from './login';
|
|
3
|
+
import { registerWeatherNodes } from './weather';
|
|
4
|
+
import { registerAnomalyNodes } from './anomaly';
|
|
5
|
+
import { registerFlowControlNodes } from './flow-control';
|
|
6
|
+
import { registerLoopNodes } from './loops';
|
|
7
|
+
import { registerPradarNodesModule } from './pradar';
|
|
8
|
+
import { registerResponseNodes } from './response';
|
|
9
|
+
import { registerRequireAuthNode } from './requireAuth';
|
|
10
|
+
|
|
11
|
+
/** All built-in nodes: login examples, Open-Meteo weather, anomaly-detection API, EV charging demo, HTTP Response, requireAuth. */
|
|
12
|
+
export function createDefaultRegistry(delayMs?: number): NodeRegistry {
|
|
13
|
+
const registry = createLoginRegistry(delayMs);
|
|
14
|
+
registerWeatherNodes(registry);
|
|
15
|
+
registerAnomalyNodes(registry);
|
|
16
|
+
registerFlowControlNodes(registry);
|
|
17
|
+
registerLoopNodes(registry);
|
|
18
|
+
registerPradarNodesModule(registry);
|
|
19
|
+
registerResponseNodes(registry);
|
|
20
|
+
registerRequireAuthNode(registry);
|
|
21
|
+
return registry;
|
|
22
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createLoginRegistry } from './login';
|
|
3
|
+
import { createDefaultRegistry } from './index';
|
|
4
|
+
import { createLoginFlow } from '../flows/login-flow';
|
|
5
|
+
import { startRun } from '../engine';
|
|
6
|
+
|
|
7
|
+
describe('login example', () => {
|
|
8
|
+
it('registers the five login nodes', () => {
|
|
9
|
+
const types = createLoginRegistry(0).list().map((d) => d.type);
|
|
10
|
+
expect(types).toEqual(
|
|
11
|
+
expect.arrayContaining(['ValidateCredentials', 'FetchUser', 'CheckPlan', 'IssueToken', 'Result']),
|
|
12
|
+
);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('runs the prebuilt flow end to end', async () => {
|
|
16
|
+
const run = await startRun({ flow: createLoginFlow(), registry: createDefaultRegistry(0) }).runToEnd();
|
|
17
|
+
expect(run.status).toBe('succeeded');
|
|
18
|
+
const result = Object.values(run.nodeStates).find((s) => (s.output as any)?.token !== undefined);
|
|
19
|
+
expect(result).toBeDefined();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('result node output contains the token', async () => {
|
|
23
|
+
const flow = createLoginFlow();
|
|
24
|
+
const run = await startRun({ flow, registry: createDefaultRegistry(0) }).runToEnd();
|
|
25
|
+
const resultNode = flow.nodes.find((n) => n.type === 'Result')!;
|
|
26
|
+
expect((run.nodeStates[resultNode.id].output as any).data.token).toBe('tok_user-ada');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('fails on short password', async () => {
|
|
30
|
+
const flow = createLoginFlow();
|
|
31
|
+
(flow.nodes.find((n) => n.id === 'input')!.config.defaults as Record<string, unknown>).password = 'x';
|
|
32
|
+
const run = await startRun({ flow, registry: createDefaultRegistry(0) }).runToEnd();
|
|
33
|
+
expect(run.status).toBe('failed');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('takes the welcome branch for a run invoked with a new-user payload', async () => {
|
|
37
|
+
const run = await startRun({
|
|
38
|
+
flow: createLoginFlow(),
|
|
39
|
+
registry: createDefaultRegistry(0),
|
|
40
|
+
input: { username: 'new-zoe', password: 'longenough' },
|
|
41
|
+
}).runToEnd();
|
|
42
|
+
expect(run.status).toBe('succeeded');
|
|
43
|
+
expect((run.nodeStates.route.output as any).$branch).toBe('true');
|
|
44
|
+
expect(run.nodeStates.welcome.status).toBe('succeeded');
|
|
45
|
+
expect((run.nodeStates.issueToken.output as any).token).toBe('tok_user-new-zoe');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('falls back to configured defaults when invoked with no input', async () => {
|
|
49
|
+
const run = await startRun({
|
|
50
|
+
flow: createLoginFlow(),
|
|
51
|
+
registry: createDefaultRegistry(0),
|
|
52
|
+
}).runToEnd();
|
|
53
|
+
expect(run.status).toBe('succeeded');
|
|
54
|
+
expect((run.nodeStates.issueToken.output as any).token).toBe('tok_user-ada');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('existing user (ada) routes through checkPlan and skips welcome', async () => {
|
|
58
|
+
const flow = createLoginFlow();
|
|
59
|
+
const run = await startRun({ flow, registry: createDefaultRegistry(0) }).runToEnd();
|
|
60
|
+
expect(run.status).toBe('succeeded');
|
|
61
|
+
expect(run.nodeStates.route.status).toBe('succeeded');
|
|
62
|
+
expect((run.nodeStates.route.output as any).$branch).toBe('existing');
|
|
63
|
+
expect(run.nodeStates.checkPlan.status).toBe('succeeded');
|
|
64
|
+
expect(run.nodeStates.welcome.status).toBe('skipped');
|
|
65
|
+
expect(run.nodeStates.issueToken.status).toBe('succeeded');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('new user (new-ada) routes through welcome and skips checkPlan', async () => {
|
|
69
|
+
const flow = createLoginFlow();
|
|
70
|
+
(flow.nodes.find((n) => n.id === 'input')!.config.defaults as Record<string, unknown>).username = 'new-ada';
|
|
71
|
+
const run = await startRun({ flow, registry: createDefaultRegistry(0) }).runToEnd();
|
|
72
|
+
expect(run.status).toBe('succeeded');
|
|
73
|
+
expect((run.nodeStates.route.output as any).$branch).toBe('true');
|
|
74
|
+
expect(run.nodeStates.welcome.status).toBe('succeeded');
|
|
75
|
+
expect((run.nodeStates.welcome.output as any).message).toBe('Welcome aboard, new-ada!');
|
|
76
|
+
expect(run.nodeStates.checkPlan.status).toBe('skipped');
|
|
77
|
+
expect(run.nodeStates.issueToken.status).toBe('succeeded');
|
|
78
|
+
// checkPlan skipped → plan input undefined → IssueToken defaults to 'free'.
|
|
79
|
+
expect((run.nodeStates.issueToken.output as any).plan).toBe('free');
|
|
80
|
+
});
|
|
81
|
+
});
|