@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,417 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { iterationSummary, projectRunbook } from './runbookProjection';
|
|
3
|
+
import { buildRunbook, type RunbookDoc, type RunbookItem } from './runbookModel';
|
|
4
|
+
import { NodeRegistry, type ExecutionRecord, type LogLine, type WorkflowDefinition, type WorkflowRun } from '../engine';
|
|
5
|
+
import type { RunHistoryEntry } from '../store/builderStore';
|
|
6
|
+
import evEvaluateCycle from '../../workflows/apis/default/ev-evaluate-cycle.json';
|
|
7
|
+
|
|
8
|
+
const FLOW_ID = 'test-flow';
|
|
9
|
+
|
|
10
|
+
function step(nodeId: string, typeName = 'Http'): RunbookItem {
|
|
11
|
+
return {
|
|
12
|
+
kind: 'step',
|
|
13
|
+
nodeId,
|
|
14
|
+
number: '1',
|
|
15
|
+
depth: 0,
|
|
16
|
+
label: nodeId,
|
|
17
|
+
typeName,
|
|
18
|
+
description: '',
|
|
19
|
+
simpleDescription: '',
|
|
20
|
+
mutation: false,
|
|
21
|
+
subflow: false,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Synthetic doc: cond branches into onArm (member onStep) / offArm (member
|
|
27
|
+
* offStep), plus a 3-iteration loop (fe1/co1) with body step body1.
|
|
28
|
+
*/
|
|
29
|
+
function makeDoc(): RunbookDoc {
|
|
30
|
+
const items: RunbookItem[] = [
|
|
31
|
+
step('cond', 'If'),
|
|
32
|
+
{
|
|
33
|
+
kind: 'branch',
|
|
34
|
+
ownerId: 'cond',
|
|
35
|
+
arm: 'onArm',
|
|
36
|
+
number: '1.1',
|
|
37
|
+
depth: 1,
|
|
38
|
+
items: [step('onStep')],
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
kind: 'branch',
|
|
42
|
+
ownerId: 'cond',
|
|
43
|
+
arm: 'offArm',
|
|
44
|
+
number: '1.2',
|
|
45
|
+
depth: 1,
|
|
46
|
+
items: [step('offStep')],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
kind: 'loop',
|
|
50
|
+
forEachId: 'fe1',
|
|
51
|
+
collectId: 'co1',
|
|
52
|
+
number: '2',
|
|
53
|
+
depth: 0,
|
|
54
|
+
label: 'Loop',
|
|
55
|
+
items: [step('body1', 'Transform')],
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
const guards = new Map<string, Array<{ ownerId: string; arm: string }>>([
|
|
60
|
+
['cond', []],
|
|
61
|
+
['onStep', [{ ownerId: 'cond', arm: 'onArm' }]],
|
|
62
|
+
['offStep', [{ ownerId: 'cond', arm: 'offArm' }]],
|
|
63
|
+
['body1', []],
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
const arms = [
|
|
67
|
+
{ ownerId: 'cond', arm: 'onArm' },
|
|
68
|
+
{ ownerId: 'cond', arm: 'offArm' },
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
return { items, guards, arms };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function makeRun(overrides: Partial<WorkflowRun> = {}): WorkflowRun {
|
|
75
|
+
return {
|
|
76
|
+
id: 'run-1',
|
|
77
|
+
workflowId: FLOW_ID,
|
|
78
|
+
status: 'running',
|
|
79
|
+
startedAt: '2026-01-01T00:00:00.000Z',
|
|
80
|
+
nodeStates: {},
|
|
81
|
+
...overrides,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
describe('projectRunbook', () => {
|
|
86
|
+
it('status: comes straight from run.nodeStates, mapped per status', () => {
|
|
87
|
+
const doc = makeDoc();
|
|
88
|
+
const run = makeRun({
|
|
89
|
+
nodeStates: {
|
|
90
|
+
onStep: { status: 'running' },
|
|
91
|
+
offStep: { status: 'skipped' },
|
|
92
|
+
body1: { status: 'succeeded' },
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
const proj = projectRunbook(doc, run, [], [], FLOW_ID);
|
|
96
|
+
expect(proj.steps.get('onStep')!.status).toBe('active');
|
|
97
|
+
expect(proj.steps.get('offStep')!.status).toBe('skipped');
|
|
98
|
+
expect(proj.steps.get('body1')!.status).toBe('ran');
|
|
99
|
+
expect(proj.steps.get('cond')!.status).toBe('idle'); // absent from nodeStates
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('status is idle for every step when run is null', () => {
|
|
103
|
+
const doc = makeDoc();
|
|
104
|
+
const proj = projectRunbook(doc, null, [], [], FLOW_ID);
|
|
105
|
+
for (const id of ['cond', 'onStep', 'offStep', 'body1']) {
|
|
106
|
+
expect(proj.steps.get(id)!.status).toBe('idle');
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('outcome: the LAST info-level log line for the node wins', () => {
|
|
111
|
+
const doc = makeDoc();
|
|
112
|
+
const run = makeRun({ nodeStates: { onStep: { status: 'succeeded' } } });
|
|
113
|
+
const logs: LogLine[] = [
|
|
114
|
+
{ timestamp: 't1', level: 'info', runId: 'run-1', nodeId: 'onStep', message: 'first message' },
|
|
115
|
+
{ timestamp: 't2', level: 'debug', runId: 'run-1', nodeId: 'onStep', message: 'ignored debug' },
|
|
116
|
+
{ timestamp: 't3', level: 'info', runId: 'run-1', nodeId: 'onStep', message: 'second message' },
|
|
117
|
+
];
|
|
118
|
+
const proj = projectRunbook(doc, run, logs, [], FLOW_ID);
|
|
119
|
+
expect(proj.steps.get('onStep')!.outcome).toBe('second message');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('outcome is empty string when there is no info log for the node', () => {
|
|
123
|
+
const doc = makeDoc();
|
|
124
|
+
const proj = projectRunbook(doc, makeRun(), [], [], FLOW_ID);
|
|
125
|
+
expect(proj.steps.get('onStep')!.outcome).toBe('');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('tech line: typeName · durationMs · in[...] -> out[...] when ran; bare typeName otherwise', () => {
|
|
129
|
+
const doc = makeDoc();
|
|
130
|
+
const run = makeRun({
|
|
131
|
+
nodeStates: {
|
|
132
|
+
onStep: {
|
|
133
|
+
status: 'succeeded',
|
|
134
|
+
startedAt: '2026-01-01T00:00:00.000Z',
|
|
135
|
+
completedAt: '2026-01-01T00:00:01.500Z',
|
|
136
|
+
input: { a: 1, b: 2, c: 3, d: 4, e: 5 },
|
|
137
|
+
output: { x: 1, y: 2 },
|
|
138
|
+
},
|
|
139
|
+
offStep: { status: 'idle' },
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
const proj = projectRunbook(doc, run, [], [], FLOW_ID);
|
|
143
|
+
expect(proj.steps.get('onStep')!.tech).toBe('Http · 1500ms · in[a,b,c,d,…] → out[x,y]');
|
|
144
|
+
expect(proj.steps.get('onStep')!.durationMs).toBe(1500);
|
|
145
|
+
expect(proj.steps.get('offStep')!.tech).toBe('Http');
|
|
146
|
+
expect(proj.steps.get('offStep')!.durationMs).toBeNull();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('tech line: prefixes #N from the ordered execution receipt (latest wins)', () => {
|
|
150
|
+
const doc = makeDoc();
|
|
151
|
+
const run = makeRun({
|
|
152
|
+
nodeStates: {
|
|
153
|
+
onStep: {
|
|
154
|
+
status: 'succeeded',
|
|
155
|
+
startedAt: '2026-01-01T00:00:00.000Z',
|
|
156
|
+
completedAt: '2026-01-01T00:00:00.200Z',
|
|
157
|
+
input: { a: 1 },
|
|
158
|
+
output: { x: 1 },
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
const logs: LogLine[] = [
|
|
163
|
+
{ timestamp: 't1', level: 'debug', runId: 'run-1', nodeId: 'onStep', message: '#3 ▶ execute' },
|
|
164
|
+
// a later, unrelated debug line must not clobber the receipt seq
|
|
165
|
+
{ timestamp: 't2', level: 'debug', runId: 'run-1', nodeId: 'onStep', message: 'validating input' },
|
|
166
|
+
];
|
|
167
|
+
const proj = projectRunbook(doc, run, logs, [], FLOW_ID);
|
|
168
|
+
expect(proj.steps.get('onStep')!.tech).toBe('#3 Http · 200ms · in[a] → out[x]');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('tech line: a loop-body node uses its LATEST receipt seq', () => {
|
|
172
|
+
const doc = makeDoc();
|
|
173
|
+
const run = makeRun({
|
|
174
|
+
nodeStates: {
|
|
175
|
+
body1: {
|
|
176
|
+
status: 'succeeded',
|
|
177
|
+
startedAt: '2026-01-01T00:00:00.000Z',
|
|
178
|
+
completedAt: '2026-01-01T00:00:00.100Z',
|
|
179
|
+
input: { i: 1 },
|
|
180
|
+
output: { o: 1 },
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
const logs: LogLine[] = [
|
|
185
|
+
{ timestamp: 't1', level: 'debug', runId: 'run-1', nodeId: 'body1', message: '#5 ▶ execute (iteration 1/3)' },
|
|
186
|
+
{ timestamp: 't2', level: 'debug', runId: 'run-1', nodeId: 'body1', message: '#7 ▶ execute (iteration 2/3)' },
|
|
187
|
+
];
|
|
188
|
+
const proj = projectRunbook(doc, run, logs, [], FLOW_ID);
|
|
189
|
+
expect(proj.steps.get('body1')!.tech).toBe('#7 Transform · 100ms · in[i] → out[o]');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('tech line: failed form is `[#N ]typeName · ms · ERROR: <first 80 chars>`', () => {
|
|
193
|
+
const doc = makeDoc();
|
|
194
|
+
const longError = 'x'.repeat(120);
|
|
195
|
+
const run = makeRun({
|
|
196
|
+
nodeStates: {
|
|
197
|
+
onStep: {
|
|
198
|
+
status: 'failed',
|
|
199
|
+
startedAt: '2026-01-01T00:00:00.000Z',
|
|
200
|
+
completedAt: '2026-01-01T00:00:00.050Z',
|
|
201
|
+
error: longError,
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
const logs: LogLine[] = [
|
|
206
|
+
{ timestamp: 't1', level: 'debug', runId: 'run-1', nodeId: 'onStep', message: '#2 ▶ execute' },
|
|
207
|
+
];
|
|
208
|
+
const proj = projectRunbook(doc, run, logs, [], FLOW_ID);
|
|
209
|
+
expect(proj.steps.get('onStep')!.tech).toBe(`#2 Http · 50ms · ERROR: ${'x'.repeat(80)}`);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('tech line: no receipt yet means no #N prefix', () => {
|
|
213
|
+
const doc = makeDoc();
|
|
214
|
+
const run = makeRun({ nodeStates: { onStep: { status: 'idle' } } });
|
|
215
|
+
const proj = projectRunbook(doc, run, [], [], FLOW_ID);
|
|
216
|
+
expect(proj.steps.get('onStep')!.tech).toBe('Http');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('reviewer note: a step whose guard says arm A still projects status "ran" from its own nodeState, independent of arm bookkeeping', () => {
|
|
220
|
+
// onStep is a gated member of cond::onArm per doc.guards (rendered under
|
|
221
|
+
// that arm for readability), but here it succeeds in a run driven by the
|
|
222
|
+
// OTHER arm (offArm) — e.g. reached via a mapping edge, per Task 1's
|
|
223
|
+
// reviewer note that guard inference and executor reachability diverge.
|
|
224
|
+
// status must reflect the node's own run state, never be suppressed
|
|
225
|
+
// because the "wrong" arm looks taken.
|
|
226
|
+
const doc = makeDoc();
|
|
227
|
+
const run = makeRun({
|
|
228
|
+
nodeStates: {
|
|
229
|
+
offStep: { status: 'succeeded' },
|
|
230
|
+
onStep: { status: 'succeeded' },
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
const proj = projectRunbook(doc, run, [], [], FLOW_ID);
|
|
234
|
+
expect(proj.steps.get('onStep')!.status).toBe('ran');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('arm takenNow: true when any gated member ran/failed/succeeded in the current run', () => {
|
|
238
|
+
const doc = makeDoc();
|
|
239
|
+
const run = makeRun({ nodeStates: { onStep: { status: 'succeeded' } } });
|
|
240
|
+
const proj = projectRunbook(doc, run, [], [], FLOW_ID);
|
|
241
|
+
expect(proj.arms.get('cond::onArm')!.takenNow).toBe(true);
|
|
242
|
+
expect(proj.arms.get('cond::offArm')!.takenNow).toBe(false);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('arm takenNow: true from the OWNER decision alone when the arm has no gated members (join downstream)', () => {
|
|
246
|
+
// cond::joinArm has no entry in doc.guards pointing at it — its only
|
|
247
|
+
// downstream node is a join whose guard set got intersected away
|
|
248
|
+
// (mirrors target::belowTarget / reconcile::hold in ev-evaluate-cycle).
|
|
249
|
+
const doc = makeDoc();
|
|
250
|
+
doc.arms.push({ ownerId: 'cond', arm: 'joinArm' });
|
|
251
|
+
const run = makeRun({ nodeStates: { cond: { status: 'succeeded', output: { $branch: 'joinArm' } } } });
|
|
252
|
+
const proj = projectRunbook(doc, run, [], [], FLOW_ID);
|
|
253
|
+
expect(proj.arms.get('cond::joinArm')!.takenNow).toBe(true);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('arm coveredBy: covered via history when the owner output.$branch matches, even with no gated members', () => {
|
|
257
|
+
const doc = makeDoc();
|
|
258
|
+
doc.arms.push({ ownerId: 'cond', arm: 'joinArm' });
|
|
259
|
+
const history: RunHistoryEntry[] = [
|
|
260
|
+
{
|
|
261
|
+
id: 'hist-1',
|
|
262
|
+
workflowId: FLOW_ID,
|
|
263
|
+
status: 'succeeded',
|
|
264
|
+
startedAt: 't0',
|
|
265
|
+
nodeStates: { cond: { status: 'succeeded', output: { $branch: 'joinArm' } } },
|
|
266
|
+
scenarioName: 'join-scenario',
|
|
267
|
+
},
|
|
268
|
+
];
|
|
269
|
+
const proj = projectRunbook(doc, makeRun(), [], history, FLOW_ID);
|
|
270
|
+
expect(proj.arms.get('cond::joinArm')!.coveredBy).toEqual(['join-scenario']);
|
|
271
|
+
expect(proj.coverage).toEqual({ covered: 1, total: 3 });
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('coverage + coveredBy: history entries cover an arm whose member succeeded/failed there, dedup + name fallback', () => {
|
|
275
|
+
const doc = makeDoc();
|
|
276
|
+
const history: RunHistoryEntry[] = [
|
|
277
|
+
{
|
|
278
|
+
id: 'hist-1',
|
|
279
|
+
workflowId: FLOW_ID,
|
|
280
|
+
status: 'succeeded',
|
|
281
|
+
startedAt: 't0',
|
|
282
|
+
nodeStates: { offStep: { status: 'succeeded' } },
|
|
283
|
+
scenarioName: 'sunset-stop',
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
id: 'hist-2',
|
|
287
|
+
workflowId: FLOW_ID,
|
|
288
|
+
status: 'succeeded',
|
|
289
|
+
startedAt: 't0',
|
|
290
|
+
nodeStates: { offStep: { status: 'failed' } },
|
|
291
|
+
scenarioName: 'sunset-stop', // dedupe
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
id: 'hist-3',
|
|
295
|
+
workflowId: 'other-flow',
|
|
296
|
+
status: 'succeeded',
|
|
297
|
+
startedAt: 't0',
|
|
298
|
+
nodeStates: { onStep: { status: 'succeeded' } },
|
|
299
|
+
},
|
|
300
|
+
];
|
|
301
|
+
// current run takes neither arm
|
|
302
|
+
const proj = projectRunbook(doc, makeRun(), [], history, FLOW_ID);
|
|
303
|
+
expect(proj.arms.get('cond::offArm')!.coveredBy).toEqual(['sunset-stop']);
|
|
304
|
+
expect(proj.arms.get('cond::onArm')!.coveredBy).toEqual([]);
|
|
305
|
+
expect(proj.coverage).toEqual({ covered: 1, total: 2 });
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('coveredBy caps at 3 distinct scenario names and falls back to "manual run" when unnamed', () => {
|
|
309
|
+
const doc = makeDoc();
|
|
310
|
+
const history: RunHistoryEntry[] = [
|
|
311
|
+
{ id: 'h1', workflowId: FLOW_ID, status: 'succeeded', startedAt: 't', nodeStates: { onStep: { status: 'succeeded' } }, scenarioName: 'a' },
|
|
312
|
+
{ id: 'h2', workflowId: FLOW_ID, status: 'succeeded', startedAt: 't', nodeStates: { onStep: { status: 'succeeded' } }, scenarioName: 'b' },
|
|
313
|
+
{ id: 'h3', workflowId: FLOW_ID, status: 'succeeded', startedAt: 't', nodeStates: { onStep: { status: 'succeeded' } } },
|
|
314
|
+
{ id: 'h4', workflowId: FLOW_ID, status: 'succeeded', startedAt: 't', nodeStates: { onStep: { status: 'succeeded' } }, scenarioName: 'd' },
|
|
315
|
+
];
|
|
316
|
+
const proj = projectRunbook(doc, makeRun(), [], history, FLOW_ID);
|
|
317
|
+
expect(proj.arms.get('cond::onArm')!.coveredBy).toEqual(['a', 'b', 'manual run']);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('loops: per-iteration status from ForEach executions, plus a running body node marks its index running', () => {
|
|
321
|
+
const doc = makeDoc();
|
|
322
|
+
const run = makeRun({
|
|
323
|
+
nodeStates: {
|
|
324
|
+
fe1: {
|
|
325
|
+
status: 'running',
|
|
326
|
+
executions: [
|
|
327
|
+
{ iteration: { index: 0, total: 3 }, status: 'succeeded' },
|
|
328
|
+
{ iteration: { index: 1, total: 3 }, status: 'failed' },
|
|
329
|
+
],
|
|
330
|
+
},
|
|
331
|
+
body1: { status: 'running', iteration: { index: 2, total: 3 } },
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
const proj = projectRunbook(doc, run, [], [], FLOW_ID);
|
|
335
|
+
expect(proj.loops.get('fe1')).toEqual({ statuses: ['done', 'failed', 'running'], count: 3 });
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('loops: no run yields an empty loop projection', () => {
|
|
339
|
+
const doc = makeDoc();
|
|
340
|
+
const proj = projectRunbook(doc, null, [], [], FLOW_ID);
|
|
341
|
+
expect(proj.loops.get('fe1')).toEqual({ statuses: [], count: 0 });
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('iteration field is set on a loop-body step projection from its NodeRunState.iteration', () => {
|
|
345
|
+
const doc = makeDoc();
|
|
346
|
+
const run = makeRun({ nodeStates: { body1: { status: 'running', iteration: { index: 1, total: 3 } } } });
|
|
347
|
+
const proj = projectRunbook(doc, run, [], [], FLOW_ID);
|
|
348
|
+
expect(proj.steps.get('body1')!.iteration).toEqual({ index: 1, total: 3 });
|
|
349
|
+
expect(proj.steps.get('onStep')!.iteration).toBeUndefined();
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('outcome on a failed node is the node error, not a stale info log', () => {
|
|
353
|
+
const doc = makeDoc();
|
|
354
|
+
const run = makeRun({ nodeStates: { onStep: { status: 'failed', error: 'connection refused' } } });
|
|
355
|
+
const logs: LogLine[] = [
|
|
356
|
+
{ timestamp: 't1', level: 'info', runId: 'run-1', nodeId: 'onStep', message: 'stale success message' },
|
|
357
|
+
];
|
|
358
|
+
const proj = projectRunbook(doc, run, logs, [], FLOW_ID);
|
|
359
|
+
expect(proj.steps.get('onStep')!.outcome).toBe('connection refused');
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it('outcome on a failed node falls back to the last info log when there is no error text', () => {
|
|
363
|
+
const doc = makeDoc();
|
|
364
|
+
const run = makeRun({ nodeStates: { onStep: { status: 'failed' } } });
|
|
365
|
+
const logs: LogLine[] = [
|
|
366
|
+
{ timestamp: 't1', level: 'info', runId: 'run-1', nodeId: 'onStep', message: 'partial progress' },
|
|
367
|
+
];
|
|
368
|
+
const proj = projectRunbook(doc, run, logs, [], FLOW_ID);
|
|
369
|
+
expect(proj.steps.get('onStep')!.outcome).toBe('partial progress');
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
describe('projectRunbook against a real flow (ev-evaluate-cycle)', () => {
|
|
374
|
+
it('coverage reaches total when every branching owner has a history entry for each of its arms (incl. join-only arms like target::belowTarget / reconcile::hold)', () => {
|
|
375
|
+
const doc = buildRunbook(evEvaluateCycle as unknown as WorkflowDefinition, new NodeRegistry());
|
|
376
|
+
|
|
377
|
+
// One history entry per (ownerId, arm), recording only the owner's own
|
|
378
|
+
// decision — no gated-member nodeStates at all. Before the fix,
|
|
379
|
+
// target::belowTarget and reconcile::hold have zero gated members (their
|
|
380
|
+
// only downstream node, `decision`, is a join whose guard set was
|
|
381
|
+
// intersected away in buildGuardMap), so they could never be covered.
|
|
382
|
+
const history: RunHistoryEntry[] = doc.arms.map(({ ownerId, arm }, i) => ({
|
|
383
|
+
id: `hist-${i}`,
|
|
384
|
+
workflowId: 'ev-evaluate-cycle',
|
|
385
|
+
status: 'succeeded',
|
|
386
|
+
startedAt: 't0',
|
|
387
|
+
nodeStates: { [ownerId]: { status: 'succeeded', output: { $branch: arm } } },
|
|
388
|
+
scenarioName: `${ownerId}::${arm}`,
|
|
389
|
+
}));
|
|
390
|
+
|
|
391
|
+
const proj = projectRunbook(doc, null, [], history, 'ev-evaluate-cycle');
|
|
392
|
+
expect(proj.coverage.covered).toBe(proj.coverage.total);
|
|
393
|
+
expect(proj.arms.get('target::belowTarget')!.coveredBy).toEqual(['target::belowTarget']);
|
|
394
|
+
expect(proj.arms.get('reconcile::hold')!.coveredBy).toEqual(['reconcile::hold']);
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
describe('iterationSummary', () => {
|
|
399
|
+
it('returns the error text when the iteration failed', () => {
|
|
400
|
+
const exec: ExecutionRecord = { iteration: { index: 0, total: 2 }, status: 'failed', error: 'timeout' };
|
|
401
|
+
expect(iterationSummary(exec)).toBe('timeout');
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it('summarizes output field names (capped at 4) when the iteration succeeded', () => {
|
|
405
|
+
const exec: ExecutionRecord = {
|
|
406
|
+
iteration: { index: 0, total: 2 },
|
|
407
|
+
status: 'succeeded',
|
|
408
|
+
output: { a: 1, b: 2, c: 3, d: 4, e: 5 },
|
|
409
|
+
};
|
|
410
|
+
expect(iterationSummary(exec)).toBe('→ out[a, b, c, d]');
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it('summarizes an empty/missing output as an empty field list', () => {
|
|
414
|
+
const exec: ExecutionRecord = { iteration: { index: 0, total: 2 }, status: 'succeeded' };
|
|
415
|
+
expect(iterationSummary(exec)).toBe('→ out[]');
|
|
416
|
+
});
|
|
417
|
+
});
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import type { ExecutionRecord, LogLine, NodeRunState, WorkflowRun } from '../engine';
|
|
2
|
+
import type { RunHistoryEntry } from '../store/builderStore';
|
|
3
|
+
import type { RunbookDoc, RunbookItem } from './runbookModel';
|
|
4
|
+
|
|
5
|
+
export type StepVisualStatus = 'idle' | 'active' | 'ran' | 'failed' | 'skipped';
|
|
6
|
+
|
|
7
|
+
export interface StepProjection {
|
|
8
|
+
status: StepVisualStatus;
|
|
9
|
+
/** simple-register outcome: last info-level log line for this node in the current run, else '' */
|
|
10
|
+
outcome: string;
|
|
11
|
+
/**
|
|
12
|
+
* technical line, prefixed `#N` once the node's receipt exists:
|
|
13
|
+
* ran → `[#N ]${typeName} · ${durationMs}ms · in[a,b] → out[c,d]`,
|
|
14
|
+
* failed → `[#N ]${typeName} · ${durationMs}ms · ERROR: ${error.slice(0,80)}`,
|
|
15
|
+
* else → `[#N ]${typeName}`
|
|
16
|
+
*/
|
|
17
|
+
tech: string;
|
|
18
|
+
durationMs: number | null;
|
|
19
|
+
/** loop-body nodes: latest execution index and total, from NodeRunState.iteration */
|
|
20
|
+
iteration?: { index: number; total: number };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ArmProjection {
|
|
24
|
+
/** taken in the CURRENT run */
|
|
25
|
+
takenNow: boolean;
|
|
26
|
+
/** scenario names from run history whose runs took this arm (nodeStates of any gated member succeeded) */
|
|
27
|
+
coveredBy: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface RunbookProjection {
|
|
31
|
+
steps: Map<string, StepProjection>;
|
|
32
|
+
arms: Map<string, ArmProjection>; // key `${ownerId}::${arm}`
|
|
33
|
+
coverage: { covered: number; total: number };
|
|
34
|
+
loops: Map<string, { statuses: ('done' | 'failed' | 'pending' | 'running')[]; count: number }>; // key forEachId
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function statusOf(state: NodeRunState | undefined): StepVisualStatus {
|
|
38
|
+
switch (state?.status) {
|
|
39
|
+
case 'running':
|
|
40
|
+
return 'active';
|
|
41
|
+
case 'succeeded':
|
|
42
|
+
return 'ran';
|
|
43
|
+
case 'failed':
|
|
44
|
+
return 'failed';
|
|
45
|
+
case 'skipped':
|
|
46
|
+
return 'skipped';
|
|
47
|
+
default:
|
|
48
|
+
return 'idle';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function fieldList(value: unknown, label: string): string {
|
|
53
|
+
const keys = value && typeof value === 'object' ? Object.keys(value as Record<string, unknown>) : [];
|
|
54
|
+
const capped = keys.slice(0, 4);
|
|
55
|
+
const shown = keys.length > 4 ? [...capped, '…'] : capped;
|
|
56
|
+
return `${label}[${shown.join(',')}]`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function lastInfoOutcome(logs: LogLine[], nodeId: string): string {
|
|
60
|
+
for (let i = logs.length - 1; i >= 0; i--) {
|
|
61
|
+
const line = logs[i];
|
|
62
|
+
if (line.nodeId === nodeId && line.level === 'info') return line.message;
|
|
63
|
+
}
|
|
64
|
+
return '';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The execution sequence number from the node's ordered receipt — the
|
|
69
|
+
* `#N ▶ execute` debug line the executor emits before each run (executor.ts).
|
|
70
|
+
* The technical line prefixes `#N` so a step reads as proof of exactly what
|
|
71
|
+
* ran, in what order. Returns the LATEST receipt (loop bodies emit one per
|
|
72
|
+
* iteration); null when the node has no receipt yet (never executed).
|
|
73
|
+
*/
|
|
74
|
+
const RECEIPT_RE = /^#(\d+) ▶ execute/;
|
|
75
|
+
function receiptSeq(logs: LogLine[], nodeId: string): string | null {
|
|
76
|
+
for (let i = logs.length - 1; i >= 0; i--) {
|
|
77
|
+
const line = logs[i];
|
|
78
|
+
if (line.nodeId === nodeId && line.level === 'debug') {
|
|
79
|
+
const match = RECEIPT_RE.exec(line.message);
|
|
80
|
+
if (match) return match[1];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function durationOf(state: NodeRunState | undefined): number | null {
|
|
87
|
+
if (!state?.startedAt || !state?.completedAt) return null;
|
|
88
|
+
return new Date(state.completedAt).getTime() - new Date(state.startedAt).getTime();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Per-iteration outcome text for a loop-body row when a loop chip is
|
|
93
|
+
* selected. Logs aren't addressable per iteration, so this is the fallback
|
|
94
|
+
* summary built straight from the execution record instead: the error when
|
|
95
|
+
* the iteration failed (destructive text upstream), else the output field
|
|
96
|
+
* names, matching the live outcome line's `out[a,b,c,d]` shorthand.
|
|
97
|
+
*/
|
|
98
|
+
export function iterationSummary(exec: ExecutionRecord): string {
|
|
99
|
+
if (exec.error) return exec.error;
|
|
100
|
+
const keys =
|
|
101
|
+
exec.output && typeof exec.output === 'object' ? Object.keys(exec.output as Record<string, unknown>) : [];
|
|
102
|
+
return `→ out[${keys.slice(0, 4).join(', ')}]`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
interface CollectedStep {
|
|
106
|
+
nodeId: string;
|
|
107
|
+
typeName: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Walks the item tree, collecting every 'step' leaf (branch/loop bodies included), recursively. */
|
|
111
|
+
function collectSteps(items: RunbookItem[], out: CollectedStep[] = []): CollectedStep[] {
|
|
112
|
+
for (const item of items) {
|
|
113
|
+
if (item.kind === 'step') {
|
|
114
|
+
out.push({ nodeId: item.nodeId, typeName: item.typeName });
|
|
115
|
+
} else {
|
|
116
|
+
collectSteps(item.items, out);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
interface CollectedLoop {
|
|
123
|
+
forEachId: string;
|
|
124
|
+
bodyStepIds: string[];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Walks the item tree for loop groups (including nested ones inside branches/other loops). */
|
|
128
|
+
function collectLoops(items: RunbookItem[], out: CollectedLoop[] = []): CollectedLoop[] {
|
|
129
|
+
for (const item of items) {
|
|
130
|
+
if (item.kind === 'loop') {
|
|
131
|
+
out.push({ forEachId: item.forEachId, bodyStepIds: collectSteps(item.items).map((s) => s.nodeId) });
|
|
132
|
+
collectLoops(item.items, out);
|
|
133
|
+
} else if (item.kind === 'branch') {
|
|
134
|
+
collectLoops(item.items, out);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Every node id that is a gated member of `(ownerId, arm)`, per doc.guards. */
|
|
141
|
+
function membersOf(doc: RunbookDoc, ownerId: string, arm: string): string[] {
|
|
142
|
+
const members: string[] = [];
|
|
143
|
+
for (const [nodeId, guardList] of doc.guards) {
|
|
144
|
+
if (guardList.some((g) => g.ownerId === ownerId && g.arm === arm)) members.push(nodeId);
|
|
145
|
+
}
|
|
146
|
+
return members;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function projectRunbook(
|
|
150
|
+
doc: RunbookDoc,
|
|
151
|
+
run: WorkflowRun | null,
|
|
152
|
+
logs: LogLine[],
|
|
153
|
+
history: RunHistoryEntry[],
|
|
154
|
+
flowId: string,
|
|
155
|
+
): RunbookProjection {
|
|
156
|
+
// ── steps ──
|
|
157
|
+
const steps = new Map<string, StepProjection>();
|
|
158
|
+
for (const { nodeId, typeName } of collectSteps(doc.items)) {
|
|
159
|
+
const state = run?.nodeStates[nodeId];
|
|
160
|
+
const status = statusOf(state);
|
|
161
|
+
// A failed node's outcome line is its error, not the last info log (which
|
|
162
|
+
// may be stale from a prior successful attempt or simply absent).
|
|
163
|
+
const outcome = status === 'failed' ? state?.error || lastInfoOutcome(logs, nodeId) : lastInfoOutcome(logs, nodeId);
|
|
164
|
+
const durationMs = durationOf(state);
|
|
165
|
+
// `#N` receipt prefix once the node has executed; bare type name before that.
|
|
166
|
+
const seq = receiptSeq(logs, nodeId);
|
|
167
|
+
const prefix = seq ? `#${seq} ` : '';
|
|
168
|
+
const ms = `${durationMs ?? 0}ms`;
|
|
169
|
+
let tech: string;
|
|
170
|
+
if (status === 'ran') {
|
|
171
|
+
tech = `${prefix}${typeName} · ${ms} · ${fieldList(state?.input, 'in')} → ${fieldList(state?.output, 'out')}`;
|
|
172
|
+
} else if (status === 'failed') {
|
|
173
|
+
tech = `${prefix}${typeName} · ${ms} · ERROR: ${(state?.error ?? '').slice(0, 80)}`;
|
|
174
|
+
} else {
|
|
175
|
+
tech = `${prefix}${typeName}`;
|
|
176
|
+
}
|
|
177
|
+
const projection: StepProjection = { status, outcome, tech, durationMs };
|
|
178
|
+
if (state?.iteration) projection.iteration = state.iteration;
|
|
179
|
+
steps.set(nodeId, projection);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ── arms ──
|
|
183
|
+
const arms = new Map<string, ArmProjection>();
|
|
184
|
+
for (const { ownerId, arm } of doc.arms) {
|
|
185
|
+
const members = membersOf(doc, ownerId, arm);
|
|
186
|
+
|
|
187
|
+
// Primary signal: the OWNER's own recorded decision. This is what makes
|
|
188
|
+
// every arm coverable — an arm whose only downstream node is a join (per
|
|
189
|
+
// buildGuardMap's intersection, see runbookModel.ts) has no gated members
|
|
190
|
+
// at all, so member-based detection can never fire for it. The gated
|
|
191
|
+
// member check remains as an OR-fallback for robustness: some Route-like
|
|
192
|
+
// nodes spread their branch output onto downstream nodes rather than
|
|
193
|
+
// keeping `$branch` on their own nodeState.
|
|
194
|
+
const ownerTookArm = (state: NodeRunState | undefined): boolean =>
|
|
195
|
+
(state?.output as Record<string, unknown> | undefined)?.$branch === arm;
|
|
196
|
+
|
|
197
|
+
const takenNow =
|
|
198
|
+
(!!run && ownerTookArm(run.nodeStates[ownerId])) ||
|
|
199
|
+
(!!run &&
|
|
200
|
+
members.some((id) => {
|
|
201
|
+
const s = run.nodeStates[id]?.status;
|
|
202
|
+
return s === 'succeeded' || s === 'failed' || s === 'running';
|
|
203
|
+
}));
|
|
204
|
+
|
|
205
|
+
const coveredBy: string[] = [];
|
|
206
|
+
for (const entry of history) {
|
|
207
|
+
if (coveredBy.length >= 3) break;
|
|
208
|
+
if (entry.workflowId !== flowId) continue;
|
|
209
|
+
const covers =
|
|
210
|
+
ownerTookArm(entry.nodeStates[ownerId]) ||
|
|
211
|
+
members.some((id) => {
|
|
212
|
+
const s = entry.nodeStates[id]?.status;
|
|
213
|
+
return s === 'succeeded' || s === 'failed';
|
|
214
|
+
});
|
|
215
|
+
if (!covers) continue;
|
|
216
|
+
const name = entry.scenarioName ?? 'manual run';
|
|
217
|
+
if (!coveredBy.includes(name)) coveredBy.push(name);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
arms.set(`${ownerId}::${arm}`, { takenNow, coveredBy });
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const covered = [...arms.values()].filter((a) => a.takenNow || a.coveredBy.length > 0).length;
|
|
224
|
+
const coverage = { covered, total: doc.arms.length };
|
|
225
|
+
|
|
226
|
+
// ── loops ──
|
|
227
|
+
const loops = new Map<string, { statuses: ('done' | 'failed' | 'pending' | 'running')[]; count: number }>();
|
|
228
|
+
for (const { forEachId, bodyStepIds } of collectLoops(doc.items)) {
|
|
229
|
+
const feState = run?.nodeStates[forEachId];
|
|
230
|
+
const executions = feState?.executions ?? [];
|
|
231
|
+
|
|
232
|
+
let count = feState?.iteration?.total ?? 0;
|
|
233
|
+
for (const ex of executions) count = Math.max(count, ex.iteration.total);
|
|
234
|
+
for (const id of bodyStepIds) {
|
|
235
|
+
const it = run?.nodeStates[id]?.iteration;
|
|
236
|
+
if (it) count = Math.max(count, it.total);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const statuses: ('done' | 'failed' | 'pending' | 'running')[] = Array.from({ length: count }, () => 'pending');
|
|
240
|
+
for (const ex of executions) {
|
|
241
|
+
statuses[ex.iteration.index] = ex.status === 'succeeded' ? 'done' : 'failed';
|
|
242
|
+
}
|
|
243
|
+
for (const id of bodyStepIds) {
|
|
244
|
+
const st = run?.nodeStates[id];
|
|
245
|
+
if (st?.status === 'running' && st.iteration && statuses[st.iteration.index] === 'pending') {
|
|
246
|
+
statuses[st.iteration.index] = 'running';
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
loops.set(forEachId, { statuses, count });
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return { steps, arms, coverage, loops };
|
|
254
|
+
}
|