@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,23 @@
|
|
|
1
|
+
import type { NodeExecutionSample } from './types';
|
|
2
|
+
|
|
3
|
+
export interface TraceSink {
|
|
4
|
+
record(sample: NodeExecutionSample): void;
|
|
5
|
+
samplesFor(nodeId: string): NodeExecutionSample[];
|
|
6
|
+
all(): NodeExecutionSample[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class InMemoryTraceSink implements TraceSink {
|
|
10
|
+
private samples: NodeExecutionSample[] = [];
|
|
11
|
+
|
|
12
|
+
record(sample: NodeExecutionSample): void {
|
|
13
|
+
this.samples.push(sample);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
samplesFor(nodeId: string): NodeExecutionSample[] {
|
|
17
|
+
return this.all().filter((s) => s.nodeId === nodeId);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
all(): NodeExecutionSample[] {
|
|
21
|
+
return [...this.samples].reverse();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { WorkflowDefinition, HttpTrigger } from './types';
|
|
3
|
+
|
|
4
|
+
describe('WorkflowDefinition.http', () => {
|
|
5
|
+
it('round-trips optional http metadata through JSON', () => {
|
|
6
|
+
const http: HttpTrigger = { method: 'POST', path: '/claims', inputSchema: { type: 'object' } };
|
|
7
|
+
const flow: WorkflowDefinition = {
|
|
8
|
+
id: 'x', name: 'X', version: 1, nodes: [], edges: [], http,
|
|
9
|
+
createdAt: '2026-01-01T00:00:00Z', updatedAt: '2026-01-01T00:00:00Z',
|
|
10
|
+
};
|
|
11
|
+
const back = JSON.parse(JSON.stringify(flow)) as WorkflowDefinition;
|
|
12
|
+
expect(back.http).toEqual(http);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('is optional — a flow without http is still valid', () => {
|
|
16
|
+
const flow: WorkflowDefinition = {
|
|
17
|
+
id: 'y', name: 'Y', version: 1, nodes: [], edges: [],
|
|
18
|
+
createdAt: '2026-01-01T00:00:00Z', updatedAt: '2026-01-01T00:00:00Z',
|
|
19
|
+
};
|
|
20
|
+
expect(flow.http).toBeUndefined();
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
export type FieldType =
|
|
2
|
+
| 'string' | 'number' | 'boolean' | 'enum'
|
|
3
|
+
| 'datetime' | 'object' | 'array';
|
|
4
|
+
|
|
5
|
+
export interface FieldDefinition {
|
|
6
|
+
name: string;
|
|
7
|
+
type: FieldType;
|
|
8
|
+
required?: boolean;
|
|
9
|
+
enumValues?: string[];
|
|
10
|
+
description?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface SchemaDefinition {
|
|
14
|
+
fields: FieldDefinition[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface NodeDefinition {
|
|
18
|
+
type: string;
|
|
19
|
+
label: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Plain-language one-liner for non-developers, shown in the simple register
|
|
23
|
+
* (runbook idle text, simple Inspector description) instead of `description`,
|
|
24
|
+
* which may lead with mechanism (PORT NOTE, file paths, implementation
|
|
25
|
+
* detail). Falls back to the first sentence of `description` when absent.
|
|
26
|
+
*/
|
|
27
|
+
simpleDescription?: string;
|
|
28
|
+
category?: string;
|
|
29
|
+
inputSchema?: SchemaDefinition;
|
|
30
|
+
outputSchema?: SchemaDefinition;
|
|
31
|
+
configSchema?: SchemaDefinition;
|
|
32
|
+
icon?: string;
|
|
33
|
+
tags?: string[];
|
|
34
|
+
/**
|
|
35
|
+
* Side-effect class. 'mutation' nodes write state someone else can observe
|
|
36
|
+
* (DB writes, emails, external POSTs that create/change resources) and are
|
|
37
|
+
* dry-run under safe mode. Absent = 'read' (default).
|
|
38
|
+
*/
|
|
39
|
+
effects?: 'read' | 'mutation';
|
|
40
|
+
/**
|
|
41
|
+
* What kind of work this node does, for the runbook's technical register. Pure
|
|
42
|
+
* documentation — the engine ignores it. 'db' = queries Postgres, 'http' =
|
|
43
|
+
* calls an external service over HTTP, 'llm' = calls a language model,
|
|
44
|
+
* 'compute' = pure in-process logic. Absent when unknown (an absent badge is
|
|
45
|
+
* honest; a wrong one is not).
|
|
46
|
+
*/
|
|
47
|
+
traceKind?: 'db' | 'http' | 'llm' | 'compute';
|
|
48
|
+
/**
|
|
49
|
+
* A short, STATIC technical descriptor of the concrete call this node makes,
|
|
50
|
+
* for the runbook's technical register (the trace badge hover card). Pure
|
|
51
|
+
* documentation — the engine ignores it. For 'db' the query shape (e.g.
|
|
52
|
+
* 'SELECT … FROM projects JOIN users … WHERE p.id = $1'); for 'http' the
|
|
53
|
+
* method + endpoint (e.g. 'POST https://oauth2.googleapis.com/token'); for
|
|
54
|
+
* 'llm' the provider + model (e.g. 'openrouter.ai · x-ai/grok-4.3:online').
|
|
55
|
+
* Describes the mechanism, NOT runtime values. Absent on 'compute' nodes and
|
|
56
|
+
* whenever the target is dynamic enough that no honest static shape exists.
|
|
57
|
+
*/
|
|
58
|
+
traceDetail?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
62
|
+
|
|
63
|
+
/** Outcome of running a child workflow synchronously via a Subflow node. */
|
|
64
|
+
export interface SubflowResult {
|
|
65
|
+
status: 'succeeded' | 'failed';
|
|
66
|
+
output?: unknown;
|
|
67
|
+
error?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface NodeExecutionContext {
|
|
71
|
+
input: Record<string, unknown>;
|
|
72
|
+
config: Record<string, unknown>;
|
|
73
|
+
/** Runner-supplied secrets; empty in browser execution. */
|
|
74
|
+
secrets: Record<string, string>;
|
|
75
|
+
/** Non-secret environment values (API base URLs, seed ids, flags). Empty in browser execution. */
|
|
76
|
+
vars: Record<string, string>;
|
|
77
|
+
/** The environment name this run targets (e.g. 'prod'); undefined in browser/in-tab runs. */
|
|
78
|
+
environment?: string;
|
|
79
|
+
/** When true, mutation nodes must dry-run their side effects instead of executing them. */
|
|
80
|
+
safeMode: boolean;
|
|
81
|
+
/** The payload this run was invoked with (emberflow.run(name, input)). */
|
|
82
|
+
runInput: Record<string, unknown>;
|
|
83
|
+
log: (level: LogLevel, message: string) => void;
|
|
84
|
+
/**
|
|
85
|
+
* Runs a child workflow to completion and returns its collected output.
|
|
86
|
+
* Provided by the host (browser store / server run registry); absent when the
|
|
87
|
+
* run has no way to look up other workflows, in which case a Subflow node
|
|
88
|
+
* fails with a clear message.
|
|
89
|
+
*/
|
|
90
|
+
runSubflow?: (workflowId: string, input: Record<string, unknown>) => Promise<SubflowResult>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export type NodeImplementation = (ctx: NodeExecutionContext) => Promise<unknown>;
|
|
94
|
+
|
|
95
|
+
export interface FieldMapping {
|
|
96
|
+
sourceNodeId: string;
|
|
97
|
+
/** Dot path into the source node's output. '$' means the entire output. */
|
|
98
|
+
sourceField: string;
|
|
99
|
+
transform?: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface WorkflowNode {
|
|
103
|
+
id: string;
|
|
104
|
+
type: string;
|
|
105
|
+
label: string;
|
|
106
|
+
position: { x: number; y: number };
|
|
107
|
+
config: Record<string, unknown>;
|
|
108
|
+
inputMap?: Record<string, FieldMapping>;
|
|
109
|
+
/**
|
|
110
|
+
* Fail-soft flag. When true, this node throwing marks it `failed` but does
|
|
111
|
+
* NOT abort the run: the cursor continues, so independent branches still run
|
|
112
|
+
* and the run can finish `succeeded`. The failed node produces no output, so
|
|
113
|
+
* its downstream consumers become unreachable (dead edges) and skip — exactly
|
|
114
|
+
* like a live "mirror" step whose failure must not break the deterministic
|
|
115
|
+
* path beside it. A non-optional node's failure still aborts the run.
|
|
116
|
+
*
|
|
117
|
+
* Applies to standard node execution. A ForEach region's own setup failure is
|
|
118
|
+
* NOT fail-soft (the flag is ignored there); mark ordinary/Subflow nodes.
|
|
119
|
+
*/
|
|
120
|
+
optional?: boolean;
|
|
121
|
+
/**
|
|
122
|
+
* Retry the implementation call on throw. `maxTries` is the total number of
|
|
123
|
+
* attempts including the first (so `maxTries: 3` means up to 2 retries).
|
|
124
|
+
* `waitMs` (default 0) is a fixed delay between attempts. Only the
|
|
125
|
+
* `implementation({...})` call is retried — config/input resolution runs
|
|
126
|
+
* once, and only the final attempt's outcome is recorded (state, trace,
|
|
127
|
+
* fail-soft). A non-object value or `maxTries < 1` is treated as absent.
|
|
128
|
+
*/
|
|
129
|
+
retry?: { maxTries: number; waitMs?: number };
|
|
130
|
+
/** Builder-owned extras (e.g. pinnedOutput). The engine never reads this. */
|
|
131
|
+
metadata?: Record<string, unknown>;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface WorkflowEdge {
|
|
135
|
+
id: string;
|
|
136
|
+
source: string;
|
|
137
|
+
target: string;
|
|
138
|
+
sourceHandle?: string;
|
|
139
|
+
targetHandle?: string;
|
|
140
|
+
label?: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface ScenarioDefinition {
|
|
144
|
+
id: string;
|
|
145
|
+
name: string;
|
|
146
|
+
description?: string;
|
|
147
|
+
/** Run payload, same shape as StartRunOptions.input. For an operation (a flow
|
|
148
|
+
* with `http`), this is the request shape `{ params?, query?, body?, headers? }`
|
|
149
|
+
* — an in-studio "Run" of a scenario passes this object as `input` through the
|
|
150
|
+
* same `RunRegistry.create` path a live HTTP request builds and passes, so the
|
|
151
|
+
* two are parity-equivalent (see server/httpOperations.test.ts). For a non-HTTP
|
|
152
|
+
* (internal) flow, `input` is used as-is — unchanged behavior. */
|
|
153
|
+
input: Record<string, unknown>;
|
|
154
|
+
/** Optional assertion the scenario's run must satisfy. Consumed by `emberflow
|
|
155
|
+
* test` (evaluateExpectation in server/scenarioTest.ts) and mock mode; a
|
|
156
|
+
* scenario without `expect` is a fixture only (not assertable). */
|
|
157
|
+
expect?: ScenarioExpectation;
|
|
158
|
+
/** nodeId -> canned output (verbatim, same shape the implementation would return).
|
|
159
|
+
* Consulted only when the run has `mockRun: true`; merges over the operation-level
|
|
160
|
+
* mocks map (scenario wins per nodeId). Plumbing (reading/merging these into a run)
|
|
161
|
+
* is a later increment — this is type-only for now. */
|
|
162
|
+
mocks?: Record<string, unknown>;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Assertion checked against a scenario's run + its flow's response mapping.
|
|
166
|
+
* `body` is a deep-subset match (see evaluateExpectation): every expected key
|
|
167
|
+
* must match recursively, arrays compare index-wise on the expected prefix,
|
|
168
|
+
* extra actual keys are fine, and `null` matches only `null`. */
|
|
169
|
+
export interface ScenarioExpectation {
|
|
170
|
+
/** Expected HTTP status of the extracted response (default response is 200). */
|
|
171
|
+
status?: number;
|
|
172
|
+
/** Expected response body, matched as a deep subset of the actual body. */
|
|
173
|
+
body?: unknown;
|
|
174
|
+
/** Node ids expected to have run to `succeeded`. */
|
|
175
|
+
executedNodes?: string[];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** An inheritable auth policy: scheme + secret ref + optional named verifier.
|
|
179
|
+
* Set at the API/folder level via `_meta.json` (inherited by descendants) or
|
|
180
|
+
* overridden per-operation via `HttpTrigger.auth`. Lives here (not in
|
|
181
|
+
* server/auth/types.ts) because src/ must not import from server/; server/auth/types.ts
|
|
182
|
+
* re-exports this type. */
|
|
183
|
+
export interface AuthPolicy {
|
|
184
|
+
scheme: 'bearer' | 'apiKey';
|
|
185
|
+
/** Name of the secret (in the run environment's secrets) holding the expected token/key. */
|
|
186
|
+
secretRef: string;
|
|
187
|
+
/** Optional named custom verifier; when set, overrides the built-in scheme verifier. */
|
|
188
|
+
verify?: string;
|
|
189
|
+
/** Header to read for apiKey (default 'x-api-key'); bearer always reads Authorization. */
|
|
190
|
+
header?: string;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** HTTP trigger metadata for an operation. Routing is a later increment; this
|
|
194
|
+
* increment only stores/round-trips it. */
|
|
195
|
+
export interface HttpTrigger {
|
|
196
|
+
method: string;
|
|
197
|
+
path: string;
|
|
198
|
+
inputSchema?: unknown;
|
|
199
|
+
/** Effective auth policy for this operation. 'none' makes it explicitly
|
|
200
|
+
* public (overriding any inherited _meta.json policy); 'inherit' (or
|
|
201
|
+
* absent) defers to the nearest ancestor _meta.json's policy, if any. */
|
|
202
|
+
auth?: AuthPolicy | 'none' | 'inherit';
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export interface WorkflowDefinition {
|
|
206
|
+
id: string;
|
|
207
|
+
name: string;
|
|
208
|
+
version: number;
|
|
209
|
+
/** Optional sidebar folder path (single level, e.g. "Anomaly Detection"). */
|
|
210
|
+
folder?: string;
|
|
211
|
+
/** Present → this flow is (or will be) an HTTP endpoint. Absent → internal sub-flow. */
|
|
212
|
+
http?: HttpTrigger;
|
|
213
|
+
/**
|
|
214
|
+
* Preferred environment name. When set and the runner offers a matching
|
|
215
|
+
* environment, opening this flow auto-selects it — so a flow that targets
|
|
216
|
+
* prod infrastructure doesn't silently run against local.
|
|
217
|
+
*/
|
|
218
|
+
environment?: string;
|
|
219
|
+
nodes: WorkflowNode[];
|
|
220
|
+
edges: WorkflowEdge[];
|
|
221
|
+
/** Named test inputs ("storybook" scenarios). Dev fixtures — stripped on publish. */
|
|
222
|
+
scenarios?: ScenarioDefinition[];
|
|
223
|
+
/** Op-level nodeId -> canned output (verbatim), consulted only when a run
|
|
224
|
+
* has `mockRun: true`. Merges under any per-scenario `mocks` (scenario
|
|
225
|
+
* wins per nodeId — see `ScenarioDefinition.mocks`). Lives in the
|
|
226
|
+
* `<op>.scenarios.json` sidecar alongside `scenarios`, exposed here by
|
|
227
|
+
* the store that loads it (e.g. `ApiStore`). Dev fixture — stripped on
|
|
228
|
+
* publish, same as `scenarios`. */
|
|
229
|
+
mocks?: Record<string, unknown>;
|
|
230
|
+
viewport?: { x: number; y: number; zoom: number };
|
|
231
|
+
createdAt: string;
|
|
232
|
+
updatedAt: string;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export type NodeStatus =
|
|
236
|
+
| 'idle' | 'queued' | 'running' | 'succeeded' | 'failed' | 'skipped';
|
|
237
|
+
export type RunStatus = 'running' | 'succeeded' | 'failed' | 'cancelled';
|
|
238
|
+
|
|
239
|
+
/** One execution of a node within a loop region — one entry per iteration. */
|
|
240
|
+
export interface ExecutionRecord {
|
|
241
|
+
iteration: { index: number; total: number };
|
|
242
|
+
input?: unknown;
|
|
243
|
+
output?: unknown;
|
|
244
|
+
error?: string;
|
|
245
|
+
status: 'succeeded' | 'failed';
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export interface NodeRunState {
|
|
249
|
+
status: NodeStatus;
|
|
250
|
+
input?: unknown;
|
|
251
|
+
output?: unknown;
|
|
252
|
+
error?: string;
|
|
253
|
+
startedAt?: string;
|
|
254
|
+
completedAt?: string;
|
|
255
|
+
/** True when this node's output came from a pin instead of execution. */
|
|
256
|
+
pinned?: boolean;
|
|
257
|
+
/** True when a mutation-declared node completed under safe mode (its writes were dry-run). */
|
|
258
|
+
mutationBlocked?: boolean;
|
|
259
|
+
/**
|
|
260
|
+
* Present on a ForEach node and its loop-body nodes while a loop region is
|
|
261
|
+
* iterating (and left at its last value once the loop completes), giving
|
|
262
|
+
* the 0-based position and total iteration count.
|
|
263
|
+
*/
|
|
264
|
+
iteration?: { index: number; total: number };
|
|
265
|
+
/**
|
|
266
|
+
* Full per-iteration execution history, present on a ForEach node and its
|
|
267
|
+
* loop-body nodes, ordered by iteration. Absent on non-loop executions.
|
|
268
|
+
*/
|
|
269
|
+
executions?: ExecutionRecord[];
|
|
270
|
+
/**
|
|
271
|
+
* Total number of implementation-call attempts (including the first),
|
|
272
|
+
* present only when the node has a `retry` and used more than one attempt
|
|
273
|
+
* to reach its final outcome (success or exhausted failure). Absent
|
|
274
|
+
* whenever a single attempt sufficed — including nodes without `retry` at
|
|
275
|
+
* all — so existing single-try consumers see no shape change.
|
|
276
|
+
*/
|
|
277
|
+
attempts?: number;
|
|
278
|
+
/** True when this node's output is a canned mock value from a mock run, not the result of executing its implementation. */
|
|
279
|
+
mocked?: boolean;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export interface WorkflowRun {
|
|
283
|
+
id: string;
|
|
284
|
+
workflowId: string;
|
|
285
|
+
status: RunStatus;
|
|
286
|
+
startedAt: string;
|
|
287
|
+
completedAt?: string;
|
|
288
|
+
/** The environment this run pointed at (from StartRunOptions.environment). */
|
|
289
|
+
environment?: string;
|
|
290
|
+
/** Whether safe mode was active for this run. */
|
|
291
|
+
safeMode?: boolean;
|
|
292
|
+
nodeStates: Record<string, NodeRunState>;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export interface LogLine {
|
|
296
|
+
timestamp: string;
|
|
297
|
+
level: LogLevel;
|
|
298
|
+
runId: string;
|
|
299
|
+
nodeId?: string;
|
|
300
|
+
nodeLabel?: string;
|
|
301
|
+
message: string;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export interface NodeExecutionSample {
|
|
305
|
+
id: string;
|
|
306
|
+
workflowId: string;
|
|
307
|
+
runId: string;
|
|
308
|
+
nodeId: string;
|
|
309
|
+
nodeType: string;
|
|
310
|
+
nodeLabel: string;
|
|
311
|
+
input: unknown;
|
|
312
|
+
output?: unknown;
|
|
313
|
+
status: 'succeeded' | 'failed';
|
|
314
|
+
startedAt: string;
|
|
315
|
+
completedAt?: string;
|
|
316
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { NodeRegistry } from './registry';
|
|
3
|
+
import { computeLoopRegions, topoSort, validateFlow } from './validation';
|
|
4
|
+
import type { WorkflowDefinition, WorkflowNode } from './types';
|
|
5
|
+
|
|
6
|
+
const registry = new NodeRegistry();
|
|
7
|
+
registry.register(
|
|
8
|
+
{
|
|
9
|
+
type: 'needy',
|
|
10
|
+
label: 'Needy',
|
|
11
|
+
inputSchema: { fields: [{ name: 'userId', type: 'string', required: true }] },
|
|
12
|
+
},
|
|
13
|
+
async () => ({}),
|
|
14
|
+
);
|
|
15
|
+
registry.register({ type: 'plain', label: 'Plain' }, async () => ({}));
|
|
16
|
+
registry.register({ type: 'writer', label: 'Writer', effects: 'mutation' }, async () => ({}));
|
|
17
|
+
registry.register({ type: 'Input', label: 'Input' }, async () => ({}));
|
|
18
|
+
registry.register({ type: 'ForEach', label: 'For Each' }, async () => ({}));
|
|
19
|
+
registry.register({ type: 'Collect', label: 'Collect' }, async () => ({}));
|
|
20
|
+
registry.register(
|
|
21
|
+
{ type: 'Subflow', label: 'Subflow', configSchema: { fields: [{ name: 'workflowId', type: 'string', required: true }] } },
|
|
22
|
+
async () => ({}),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const node = (id: string, type = 'plain', extra: Partial<WorkflowNode> = {}): WorkflowNode => ({
|
|
26
|
+
id, type, label: id, position: { x: 0, y: 0 }, config: {}, ...extra,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const flow = (
|
|
30
|
+
nodes: WorkflowNode[],
|
|
31
|
+
edges: WorkflowDefinition['edges'],
|
|
32
|
+
scenarios?: WorkflowDefinition['scenarios'],
|
|
33
|
+
): WorkflowDefinition => ({
|
|
34
|
+
id: 'f', name: 'f', version: 1, nodes, edges,
|
|
35
|
+
createdAt: '2026-01-01T00:00:00Z', updatedAt: '2026-01-01T00:00:00Z',
|
|
36
|
+
...(scenarios ? { scenarios } : {}),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('validateFlow', () => {
|
|
40
|
+
it('accepts a valid linear flow', () => {
|
|
41
|
+
const f = flow([node('a'), node('b')], [{ id: 'e1', source: 'a', target: 'b' }]);
|
|
42
|
+
expect(validateFlow(f, registry)).toEqual([]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('errors when a Subflow node names its own containing flow', () => {
|
|
46
|
+
const f = flow([node('s', 'Subflow', { config: { workflowId: 'f' } })], []);
|
|
47
|
+
const errors = validateFlow(f, registry).filter((i) => i.severity === 'error');
|
|
48
|
+
expect(errors).toContainEqual({
|
|
49
|
+
severity: 'error', nodeId: 's', message: 'subflow cannot call its own flow',
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('accepts a Subflow node that names a different flow', () => {
|
|
54
|
+
const f = flow([node('s', 'Subflow', { config: { workflowId: 'other' } })], []);
|
|
55
|
+
const errors = validateFlow(f, registry).filter((i) => i.severity === 'error');
|
|
56
|
+
expect(errors).toEqual([]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('flags unknown node types', () => {
|
|
60
|
+
const f = flow([node('a', 'ghost')], []);
|
|
61
|
+
expect(validateFlow(f, registry)).toContainEqual(
|
|
62
|
+
expect.objectContaining({ severity: 'error', nodeId: 'a', message: expect.stringContaining('ghost') }),
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('flags edges to missing nodes', () => {
|
|
67
|
+
const f = flow([node('a')], [{ id: 'e1', source: 'a', target: 'zz' }]);
|
|
68
|
+
expect(validateFlow(f, registry).some((i) => i.severity === 'error' && i.message.includes('zz'))).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('flags cycles', () => {
|
|
72
|
+
const f = flow(
|
|
73
|
+
[node('a'), node('b')],
|
|
74
|
+
[{ id: 'e1', source: 'a', target: 'b' }, { id: 'e2', source: 'b', target: 'a' }],
|
|
75
|
+
);
|
|
76
|
+
expect(validateFlow(f, registry).some((i) => i.message.includes('cycle'))).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('flags inputMap pointing at a missing node', () => {
|
|
80
|
+
const f = flow(
|
|
81
|
+
[node('a', 'needy', { inputMap: { userId: { sourceNodeId: 'ghost', sourceField: 'x' } } })],
|
|
82
|
+
[],
|
|
83
|
+
);
|
|
84
|
+
expect(validateFlow(f, registry).some((i) => i.severity === 'error' && i.message.includes('ghost'))).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('accepts inputMap from an indirect upstream ancestor', () => {
|
|
88
|
+
const f = flow(
|
|
89
|
+
[node('a'), node('b'), node('c', 'needy', { inputMap: { userId: { sourceNodeId: 'a', sourceField: 'x' } } })],
|
|
90
|
+
[{ id: 'e1', source: 'a', target: 'b' }, { id: 'e2', source: 'b', target: 'c' }],
|
|
91
|
+
);
|
|
92
|
+
expect(validateFlow(f, registry)).toEqual([]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('warns when inputMap source is not upstream of the node', () => {
|
|
96
|
+
const f = flow(
|
|
97
|
+
[node('a'), node('b', 'needy', { inputMap: { userId: { sourceNodeId: 'a', sourceField: 'x' } } })],
|
|
98
|
+
[],
|
|
99
|
+
);
|
|
100
|
+
expect(validateFlow(f, registry)).toContainEqual(
|
|
101
|
+
expect.objectContaining({ severity: 'warning', nodeId: 'b', message: expect.stringContaining('not upstream') }),
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('warns when a mapping targets a field the node does not declare', () => {
|
|
106
|
+
const f = flow(
|
|
107
|
+
[node('a'), node('b', 'plain', { inputMap: { ghost: { sourceNodeId: 'a', sourceField: 'x' } } })],
|
|
108
|
+
[{ id: 'e1', source: 'a', target: 'b' }],
|
|
109
|
+
);
|
|
110
|
+
expect(validateFlow(f, registry)).toContainEqual(
|
|
111
|
+
expect.objectContaining({
|
|
112
|
+
severity: 'warning',
|
|
113
|
+
nodeId: 'b',
|
|
114
|
+
message: expect.stringContaining('not declared'),
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('warns on unmapped required input', () => {
|
|
120
|
+
const f = flow([node('a', 'needy')], []);
|
|
121
|
+
expect(validateFlow(f, registry)).toContainEqual(
|
|
122
|
+
expect.objectContaining({ severity: 'warning', nodeId: 'a', message: expect.stringContaining('userId') }),
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('does not warn when required input satisfied by config', () => {
|
|
127
|
+
const f = flow([node('a', 'needy', { config: { userId: 'u1' } })], []);
|
|
128
|
+
expect(validateFlow(f, registry)).toEqual([]);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('warns for each mutation-declared node with the dry-run message', () => {
|
|
132
|
+
const f = flow(
|
|
133
|
+
[node('w1', 'writer'), node('w2', 'writer'), node('r', 'plain')],
|
|
134
|
+
[{ id: 'e1', source: 'w1', target: 'w2' }, { id: 'e2', source: 'w2', target: 'r' }],
|
|
135
|
+
);
|
|
136
|
+
const issues = validateFlow(f, registry);
|
|
137
|
+
const mutationWarnings = issues.filter((i) => i.message.includes('performs writes'));
|
|
138
|
+
expect(mutationWarnings).toHaveLength(2);
|
|
139
|
+
expect(mutationWarnings).toContainEqual({
|
|
140
|
+
severity: 'warning',
|
|
141
|
+
nodeId: 'w1',
|
|
142
|
+
message: 'Mutation node "w1" performs writes — dry-run under safe mode',
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('does not warn for read nodes', () => {
|
|
147
|
+
const f = flow([node('a', 'plain')], []);
|
|
148
|
+
expect(validateFlow(f, registry).some((i) => i.message.includes('performs writes'))).toBe(false);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe('validateFlow scenarios', () => {
|
|
153
|
+
const inputNode = (extra: Partial<WorkflowNode> = {}) =>
|
|
154
|
+
node('input', 'Input', {
|
|
155
|
+
config: {
|
|
156
|
+
fields: [
|
|
157
|
+
{ name: 'userId', type: 'string', required: true },
|
|
158
|
+
{ name: 'note', type: 'string' },
|
|
159
|
+
],
|
|
160
|
+
defaults: { userId: 'u1' },
|
|
161
|
+
},
|
|
162
|
+
...extra,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('accepts a valid scenario against an Input node with no warnings', () => {
|
|
166
|
+
const f = flow([inputNode()], [], [
|
|
167
|
+
{ id: 's1', name: 'Happy path', input: { userId: 'u2', note: 'hi' } },
|
|
168
|
+
]);
|
|
169
|
+
expect(validateFlow(f, registry)).toEqual([]);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('warns when a scenario omits a required field with no default', () => {
|
|
173
|
+
const withoutDefault = node('input', 'Input', {
|
|
174
|
+
config: {
|
|
175
|
+
fields: [{ name: 'userId', type: 'string', required: true }],
|
|
176
|
+
defaults: {},
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
const f = flow([withoutDefault], [], [{ id: 's1', name: 'Missing', input: {} }]);
|
|
180
|
+
expect(validateFlow(f, registry)).toContainEqual(
|
|
181
|
+
expect.objectContaining({
|
|
182
|
+
severity: 'warning',
|
|
183
|
+
nodeId: 'input',
|
|
184
|
+
message: expect.stringMatching(/Missing.*userId/),
|
|
185
|
+
}),
|
|
186
|
+
);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('does not warn when a required field is satisfied by defaults', () => {
|
|
190
|
+
const f = flow([inputNode()], [], [{ id: 's1', name: 'Uses default', input: {} }]);
|
|
191
|
+
expect(validateFlow(f, registry)).toEqual([]);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('warns when a scenario provides an undeclared input field', () => {
|
|
195
|
+
const f = flow([inputNode()], [], [
|
|
196
|
+
{ id: 's1', name: 'Extra', input: { userId: 'u2', bogus: true } },
|
|
197
|
+
]);
|
|
198
|
+
expect(validateFlow(f, registry)).toContainEqual(
|
|
199
|
+
expect.objectContaining({
|
|
200
|
+
severity: 'warning',
|
|
201
|
+
nodeId: 'input',
|
|
202
|
+
message: expect.stringMatching(/Extra.*bogus/),
|
|
203
|
+
}),
|
|
204
|
+
);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('warns exactly once when scenarios exist but there is no Input node', () => {
|
|
208
|
+
const f = flow([node('a')], [], [{ id: 's1', name: 'Orphan', input: { x: 1 } }]);
|
|
209
|
+
const warnings = validateFlow(f, registry).filter((i) => i.message.includes('no Input node'));
|
|
210
|
+
expect(warnings).toHaveLength(1);
|
|
211
|
+
expect(warnings[0]).toEqual(
|
|
212
|
+
expect.objectContaining({ severity: 'warning' }),
|
|
213
|
+
);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe('topoSort', () => {
|
|
218
|
+
it('orders dependencies first', () => {
|
|
219
|
+
const f = flow(
|
|
220
|
+
[node('c'), node('a'), node('b')],
|
|
221
|
+
[{ id: 'e1', source: 'a', target: 'b' }, { id: 'e2', source: 'b', target: 'c' }],
|
|
222
|
+
);
|
|
223
|
+
expect(topoSort(f)).toEqual(['a', 'b', 'c']);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('throws on cycles', () => {
|
|
227
|
+
const f = flow(
|
|
228
|
+
[node('a'), node('b')],
|
|
229
|
+
[{ id: 'e1', source: 'a', target: 'b' }, { id: 'e2', source: 'b', target: 'a' }],
|
|
230
|
+
);
|
|
231
|
+
expect(() => topoSort(f)).toThrow('Flow contains a cycle');
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe('ForEach/Collect loop regions', () => {
|
|
236
|
+
const region = () =>
|
|
237
|
+
flow(
|
|
238
|
+
[node('fe', 'ForEach'), node('body', 'plain'), node('col', 'Collect')],
|
|
239
|
+
[
|
|
240
|
+
{ id: 'e1', source: 'fe', target: 'body' },
|
|
241
|
+
{ id: 'e2', source: 'body', target: 'col' },
|
|
242
|
+
],
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
it('accepts a valid ForEach → body → Collect region with no errors', () => {
|
|
246
|
+
expect(validateFlow(region(), registry)).toEqual([]);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('computeLoopRegions returns the correct bodyIds for a valid region', () => {
|
|
250
|
+
expect(computeLoopRegions(region())).toEqual([
|
|
251
|
+
{ forEachId: 'fe', collectId: 'col', bodyIds: ['body'] },
|
|
252
|
+
]);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('flags a ForEach with no Collect among its descendants', () => {
|
|
256
|
+
const f = flow([node('fe', 'ForEach'), node('body', 'plain')], [{ id: 'e1', source: 'fe', target: 'body' }]);
|
|
257
|
+
expect(validateFlow(f, registry)).toContainEqual(
|
|
258
|
+
expect.objectContaining({ severity: 'error', nodeId: 'fe', message: expect.stringContaining('no Collect') }),
|
|
259
|
+
);
|
|
260
|
+
expect(computeLoopRegions(f)).toEqual([]);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('flags a ForEach with two Collect descendants', () => {
|
|
264
|
+
const f = flow(
|
|
265
|
+
[node('fe', 'ForEach'), node('c1', 'Collect'), node('c2', 'Collect')],
|
|
266
|
+
[{ id: 'e1', source: 'fe', target: 'c1' }, { id: 'e2', source: 'fe', target: 'c2' }],
|
|
267
|
+
);
|
|
268
|
+
expect(validateFlow(f, registry)).toContainEqual(
|
|
269
|
+
expect.objectContaining({ severity: 'error', nodeId: 'fe', message: expect.stringContaining('multiple Collect') }),
|
|
270
|
+
);
|
|
271
|
+
expect(computeLoopRegions(f)).toEqual([]);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('flags a Collect with no ForEach among its ancestors', () => {
|
|
275
|
+
const f = flow([node('body', 'plain'), node('col', 'Collect')], [{ id: 'e1', source: 'body', target: 'col' }]);
|
|
276
|
+
expect(validateFlow(f, registry)).toContainEqual(
|
|
277
|
+
expect.objectContaining({ severity: 'error', nodeId: 'col', message: expect.stringContaining('no ForEach') }),
|
|
278
|
+
);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('flags a leaking edge out of the loop region', () => {
|
|
282
|
+
const f = flow(
|
|
283
|
+
[node('fe', 'ForEach'), node('body', 'plain'), node('col', 'Collect'), node('outside', 'plain')],
|
|
284
|
+
[
|
|
285
|
+
{ id: 'e1', source: 'fe', target: 'body' },
|
|
286
|
+
{ id: 'e2', source: 'body', target: 'col' },
|
|
287
|
+
{ id: 'e3', source: 'body', target: 'outside' },
|
|
288
|
+
],
|
|
289
|
+
);
|
|
290
|
+
expect(validateFlow(f, registry)).toContainEqual(
|
|
291
|
+
expect.objectContaining({ severity: 'error', nodeId: 'body', message: expect.stringContaining('e3') }),
|
|
292
|
+
);
|
|
293
|
+
expect(computeLoopRegions(f)).toEqual([]);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('flags nested ForEach regions', () => {
|
|
297
|
+
const f = flow(
|
|
298
|
+
[
|
|
299
|
+
node('fe1', 'ForEach'),
|
|
300
|
+
node('fe2', 'ForEach'),
|
|
301
|
+
node('col2', 'Collect'),
|
|
302
|
+
node('col1', 'Collect'),
|
|
303
|
+
],
|
|
304
|
+
[
|
|
305
|
+
{ id: 'e1', source: 'fe1', target: 'fe2' },
|
|
306
|
+
{ id: 'e2', source: 'fe2', target: 'col2' },
|
|
307
|
+
{ id: 'e3', source: 'col2', target: 'col1' },
|
|
308
|
+
],
|
|
309
|
+
);
|
|
310
|
+
const issues = validateFlow(f, registry);
|
|
311
|
+
expect(issues.some((i) => i.severity === 'error' && i.message.includes('Nested ForEach'))).toBe(true);
|
|
312
|
+
expect(computeLoopRegions(f)).toEqual([]);
|
|
313
|
+
});
|
|
314
|
+
});
|