@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,119 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
2
|
+
import { spawn, type ChildProcess } from 'node:child_process';
|
|
3
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
// Verifies POST /runs auto-attaches the environment's credential (Task 4).
|
|
8
|
+
// Boots the actual runner subprocess (server/index.ts) against a scratch
|
|
9
|
+
// project dir, mirroring the harness in apiMount.test.ts.
|
|
10
|
+
|
|
11
|
+
let proc: ChildProcess;
|
|
12
|
+
const PORT = 8129;
|
|
13
|
+
const base = `http://127.0.0.1:${PORT}`;
|
|
14
|
+
let projectDir: string;
|
|
15
|
+
|
|
16
|
+
async function waitHealthy(url: string, tries = 40): Promise<void> {
|
|
17
|
+
for (let i = 0; i < tries; i++) {
|
|
18
|
+
try {
|
|
19
|
+
if ((await fetch(url)).ok) return;
|
|
20
|
+
} catch {
|
|
21
|
+
/* not up yet */
|
|
22
|
+
}
|
|
23
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
24
|
+
}
|
|
25
|
+
throw new Error('runner did not become healthy');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function bootRunner(port: number, env: Record<string, string | undefined>): ChildProcess {
|
|
29
|
+
return spawn('npx', ['tsx', 'server/index.ts'], {
|
|
30
|
+
env: { ...process.env, EMBERFLOW_RUNNER_PORT: String(port), ...env },
|
|
31
|
+
stdio: 'ignore',
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Single-node flow: an Input node that just echoes whatever input the run
|
|
36
|
+
// was invoked with (including `headers`) — a "trivial flow whose Input
|
|
37
|
+
// echoes headers" per the brief.
|
|
38
|
+
const echoFlow = {
|
|
39
|
+
id: 'echo-input',
|
|
40
|
+
name: 'Echo Input',
|
|
41
|
+
version: 1,
|
|
42
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
43
|
+
updatedAt: '2026-01-01T00:00:00Z',
|
|
44
|
+
nodes: [
|
|
45
|
+
{
|
|
46
|
+
id: 'input',
|
|
47
|
+
type: 'Input',
|
|
48
|
+
label: 'Input',
|
|
49
|
+
position: { x: 0, y: 0 },
|
|
50
|
+
config: { fields: [], defaults: {} },
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
edges: [],
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** POSTs a step-mode run, steps it once, then reads back the Input node's recorded sample output. */
|
|
57
|
+
async function runAndGetInputOutput(input: Record<string, unknown>): Promise<any> {
|
|
58
|
+
const createRes = await fetch(`${base}/api/runs`, {
|
|
59
|
+
method: 'POST',
|
|
60
|
+
headers: { 'Content-Type': 'application/json' },
|
|
61
|
+
body: JSON.stringify({ flow: echoFlow, mode: 'step', input }),
|
|
62
|
+
});
|
|
63
|
+
expect(createRes.status).toBe(201);
|
|
64
|
+
const { runId } = await createRes.json();
|
|
65
|
+
|
|
66
|
+
const stepRes = await fetch(`${base}/api/runs/${runId}/step`, { method: 'POST' });
|
|
67
|
+
expect(stepRes.status).toBe(200);
|
|
68
|
+
|
|
69
|
+
const samplesRes = await fetch(`${base}/api/samples?nodeId=input`);
|
|
70
|
+
expect(samplesRes.status).toBe(200);
|
|
71
|
+
const { samples } = await samplesRes.json();
|
|
72
|
+
const sample = samples.find((s: { runId: string }) => s.runId === runId);
|
|
73
|
+
expect(sample).toBeDefined();
|
|
74
|
+
return sample.output;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
beforeAll(async () => {
|
|
78
|
+
projectDir = mkdtempSync(join(tmpdir(), 'runsauth-'));
|
|
79
|
+
mkdirSync(join(projectDir, 'emberflow', 'apis', 'default'), { recursive: true });
|
|
80
|
+
writeFileSync(join(projectDir, 'emberflow.config.mjs'), 'export default {};\n');
|
|
81
|
+
writeFileSync(
|
|
82
|
+
join(projectDir, 'emberflow.environments.json'),
|
|
83
|
+
JSON.stringify({
|
|
84
|
+
defaultEnvironment: 'local',
|
|
85
|
+
environments: {
|
|
86
|
+
local: {
|
|
87
|
+
vars: {},
|
|
88
|
+
secrets: { sessionCookie: 'supersecretcookievalue' },
|
|
89
|
+
auth: { attach: { as: 'cookie', name: 'session', secretRef: 'sessionCookie' } },
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
proc = bootRunner(PORT, { EMBERFLOW_PROJECT: projectDir });
|
|
96
|
+
await waitHealthy(`${base}/healthz`);
|
|
97
|
+
}, 20_000);
|
|
98
|
+
|
|
99
|
+
afterAll(() => {
|
|
100
|
+
proc?.kill();
|
|
101
|
+
if (projectDir) rmSync(projectDir, { recursive: true, force: true });
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('POST /runs auto-attaches the environment credential', () => {
|
|
105
|
+
it('attaches the session cookie from the environment secret when the run omits headers', async () => {
|
|
106
|
+
// The flow receives the raw secret internally (real attachment happened);
|
|
107
|
+
// only the studio-facing sample output (via GET /samples) is redacted.
|
|
108
|
+
// The name= prefix on the redaction token proves attachment occurred.
|
|
109
|
+
const output = await runAndGetInputOutput({});
|
|
110
|
+
expect(output.headers.cookie).toContain('session=«secret:sessionCookie»');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('does not overwrite an explicit value for the same cookie name (non-destructive)', async () => {
|
|
114
|
+
// The run already sets the target cookie name ("session") explicitly —
|
|
115
|
+
// attachCredential must leave it exactly as the caller set it.
|
|
116
|
+
const output = await runAndGetInputOutput({ headers: { cookie: 'session=mine' } });
|
|
117
|
+
expect(output.headers.cookie).toBe('session=mine');
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { RequestHandler, Router } from 'express';
|
|
2
|
+
|
|
3
|
+
/** A single routed operation's method + path. */
|
|
4
|
+
export interface RegisteredOp {
|
|
5
|
+
method: string;
|
|
6
|
+
path: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The seam between the runner and whatever HTTP runtime serves it. Express is
|
|
11
|
+
* the only implementation today; Deno/Bun adapters can implement this same
|
|
12
|
+
* interface later without touching the runner.
|
|
13
|
+
*/
|
|
14
|
+
export interface RuntimeAdapter {
|
|
15
|
+
/** Register a routed operation's handler at `{method, path}`. Throws on a
|
|
16
|
+
* duplicate method+path registration. */
|
|
17
|
+
registerOperation(op: RegisteredOp, handler: RequestHandler): void;
|
|
18
|
+
/** Mount a sub-router at a base path (used for `/api` + the studio). */
|
|
19
|
+
mountRouter(basePath: string, router: Router): void;
|
|
20
|
+
/** Start listening; resolves once the server is up. */
|
|
21
|
+
listen(port: number, host: string): Promise<void>;
|
|
22
|
+
/** Stop the server; resolves once fully closed. */
|
|
23
|
+
close(): Promise<void>;
|
|
24
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { ExpressAdapter } from './expressAdapter';
|
|
3
|
+
|
|
4
|
+
describe('ExpressAdapter', () => {
|
|
5
|
+
let adapter: ExpressAdapter | undefined;
|
|
6
|
+
|
|
7
|
+
afterEach(async () => {
|
|
8
|
+
await adapter?.close();
|
|
9
|
+
adapter = undefined;
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('registers an operation route and serves it', async () => {
|
|
13
|
+
adapter = new ExpressAdapter();
|
|
14
|
+
adapter.registerOperation({ method: 'GET', path: '/ping' }, (_req, res) => res.json({ pong: true }));
|
|
15
|
+
await adapter.listen(0, '127.0.0.1');
|
|
16
|
+
const port = adapter.address?.port;
|
|
17
|
+
expect(port).toBeTypeOf('number');
|
|
18
|
+
|
|
19
|
+
const res = await fetch(`http://127.0.0.1:${port}/ping`);
|
|
20
|
+
expect(res.status).toBe(200);
|
|
21
|
+
expect(await res.json()).toEqual({ pong: true });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('throws on a duplicate method+path', () => {
|
|
25
|
+
adapter = new ExpressAdapter();
|
|
26
|
+
const h = (_req: never, res: { end: () => void }) => res.end();
|
|
27
|
+
adapter.registerOperation({ method: 'POST', path: '/x' }, h as never);
|
|
28
|
+
expect(() => adapter?.registerOperation({ method: 'POST', path: '/x' }, h as never)).toThrow(/POST \/x/);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('resolves listen/close cleanly', async () => {
|
|
32
|
+
adapter = new ExpressAdapter();
|
|
33
|
+
await adapter.listen(0, '127.0.0.1');
|
|
34
|
+
expect(adapter.address).not.toBeNull();
|
|
35
|
+
await adapter.close();
|
|
36
|
+
expect(adapter.address).toBeNull();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import express, { type Express, type RequestHandler, type Router } from 'express';
|
|
2
|
+
import type { AddressInfo } from 'node:net';
|
|
3
|
+
import type { Server } from 'node:http';
|
|
4
|
+
import type { RegisteredOp, RuntimeAdapter } from './RuntimeAdapter';
|
|
5
|
+
|
|
6
|
+
const METHODS = ['get', 'post', 'put', 'delete', 'patch'] as const;
|
|
7
|
+
type Method = (typeof METHODS)[number];
|
|
8
|
+
|
|
9
|
+
function isMethod(m: string): m is Method {
|
|
10
|
+
return (METHODS as readonly string[]).includes(m);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Express implementation of the `RuntimeAdapter` seam. Keep it thin — this is
|
|
14
|
+
* the seam other runtimes (Deno/Bun) implement later. */
|
|
15
|
+
export class ExpressAdapter implements RuntimeAdapter {
|
|
16
|
+
private readonly _app: Express;
|
|
17
|
+
private server?: Server;
|
|
18
|
+
private readonly seen = new Set<string>();
|
|
19
|
+
|
|
20
|
+
constructor(app?: Express) {
|
|
21
|
+
this._app = app ?? express();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
get app(): Express {
|
|
25
|
+
return this._app;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The bound address once `listen()` resolves (useful in tests to find an
|
|
29
|
+
* ephemeral port). `null` before listening / after close. */
|
|
30
|
+
get address(): AddressInfo | null {
|
|
31
|
+
const addr = this.server?.address();
|
|
32
|
+
return addr && typeof addr === 'object' ? addr : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
registerOperation(op: RegisteredOp, handler: RequestHandler): void {
|
|
36
|
+
const method = op.method.toUpperCase();
|
|
37
|
+
const key = `${method} ${op.path}`;
|
|
38
|
+
if (this.seen.has(key)) {
|
|
39
|
+
throw new Error(`Duplicate operation route: ${key}`);
|
|
40
|
+
}
|
|
41
|
+
const lower = method.toLowerCase();
|
|
42
|
+
if (!isMethod(lower)) {
|
|
43
|
+
throw new Error(`Unsupported HTTP method for operation route: ${method} ${op.path}`);
|
|
44
|
+
}
|
|
45
|
+
this.seen.add(key);
|
|
46
|
+
this._app[lower](op.path, handler);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
mountRouter(basePath: string, router: Router): void {
|
|
50
|
+
this._app.use(basePath, router);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
listen(port: number, host: string): Promise<void> {
|
|
54
|
+
return new Promise((resolve) => {
|
|
55
|
+
this.server = this._app.listen(port, host, () => resolve());
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
close(): Promise<void> {
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
const server = this.server;
|
|
62
|
+
if (!server) {
|
|
63
|
+
resolve();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
this.server = undefined;
|
|
67
|
+
server.close((err) => (err ? reject(err) : resolve()));
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { deepSubsetMatch, evaluateExpectation } from './scenarioTest';
|
|
3
|
+
import type { WorkflowDefinition, WorkflowRun } from '../src/engine';
|
|
4
|
+
|
|
5
|
+
describe('deepSubsetMatch', () => {
|
|
6
|
+
it('passes on equal primitives', () => {
|
|
7
|
+
expect(deepSubsetMatch(1, 1)).toEqual([]);
|
|
8
|
+
expect(deepSubsetMatch('a', 'a')).toEqual([]);
|
|
9
|
+
expect(deepSubsetMatch(true, true)).toEqual([]);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('fails on unequal primitives', () => {
|
|
13
|
+
expect(deepSubsetMatch(1, 2)).toEqual(['expected 1, got 2']);
|
|
14
|
+
expect(deepSubsetMatch('a', 'b')).toEqual(['expected "a", got "b"']);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('passes when actual is a superset of a nested object', () => {
|
|
18
|
+
const failures = deepSubsetMatch(
|
|
19
|
+
{ user: { name: 'Ada' } },
|
|
20
|
+
{ user: { name: 'Ada', age: 30 }, extra: true },
|
|
21
|
+
);
|
|
22
|
+
expect(failures).toEqual([]);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('reports a dotted path for a missing/mismatched nested key', () => {
|
|
26
|
+
const failures = deepSubsetMatch({ user: { name: 'Ada' } }, { user: { name: 'Bob' } });
|
|
27
|
+
expect(failures).toEqual(['user.name: expected "Ada", got "Bob"']);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('reports missing actual key', () => {
|
|
31
|
+
const failures = deepSubsetMatch({ user: { name: 'Ada' } }, { user: {} });
|
|
32
|
+
expect(failures).toEqual(['user.name: expected "Ada", got undefined']);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('matches arrays index-wise on the expected prefix', () => {
|
|
36
|
+
expect(deepSubsetMatch([1, 2], [1, 2, 3])).toEqual([]);
|
|
37
|
+
expect(deepSubsetMatch([1, 2], [1])).toEqual(['[1]: expected 2, got undefined']);
|
|
38
|
+
expect(deepSubsetMatch([1, 2], [1, 9])).toEqual(['[1]: expected 2, got 9']);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('allows extra actual keys anywhere', () => {
|
|
42
|
+
expect(deepSubsetMatch({ a: 1 }, { a: 1, b: 2, c: 3 })).toEqual([]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('null expected matches only null', () => {
|
|
46
|
+
expect(deepSubsetMatch(null, null)).toEqual([]);
|
|
47
|
+
expect(deepSubsetMatch(null, undefined)).toEqual(['expected null, got undefined']);
|
|
48
|
+
expect(deepSubsetMatch(null, 0)).toEqual(['expected null, got 0']);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
function makeFlow(overrides: Partial<WorkflowDefinition> = {}): WorkflowDefinition {
|
|
53
|
+
return {
|
|
54
|
+
id: 'flow-1',
|
|
55
|
+
name: 'Flow 1',
|
|
56
|
+
version: 1,
|
|
57
|
+
nodes: [
|
|
58
|
+
{ id: 'response-200', type: 'Response', label: 'OK', position: { x: 0, y: 0 }, config: {} },
|
|
59
|
+
{ id: 'response-401', type: 'Response', label: 'Unauthorized', position: { x: 0, y: 0 }, config: {} },
|
|
60
|
+
],
|
|
61
|
+
edges: [],
|
|
62
|
+
createdAt: new Date().toISOString(),
|
|
63
|
+
updatedAt: new Date().toISOString(),
|
|
64
|
+
...overrides,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function makeRun(overrides: Partial<WorkflowRun> = {}): WorkflowRun {
|
|
69
|
+
return {
|
|
70
|
+
id: 'run-1',
|
|
71
|
+
workflowId: 'flow-1',
|
|
72
|
+
status: 'succeeded',
|
|
73
|
+
startedAt: new Date().toISOString(),
|
|
74
|
+
nodeStates: {},
|
|
75
|
+
...overrides,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
describe('evaluateExpectation', () => {
|
|
80
|
+
it('passes when status matches', () => {
|
|
81
|
+
const flow = makeFlow();
|
|
82
|
+
const run = makeRun({
|
|
83
|
+
nodeStates: {
|
|
84
|
+
'response-200': { status: 'succeeded', output: { status: 200, body: { ok: true } } },
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
const result = evaluateExpectation(run, flow, { status: 200 });
|
|
88
|
+
expect(result).toEqual({ ok: true, failures: [] });
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('fails when status mismatches', () => {
|
|
92
|
+
const flow = makeFlow();
|
|
93
|
+
const run = makeRun({
|
|
94
|
+
nodeStates: {
|
|
95
|
+
'response-200': { status: 'succeeded', output: { status: 200, body: {} } },
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
const result = evaluateExpectation(run, flow, { status: 401 });
|
|
99
|
+
expect(result.ok).toBe(false);
|
|
100
|
+
expect(result.failures).toEqual(['status: expected 401, got 200']);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('fails and lists the dotted path when body is not a subset match', () => {
|
|
104
|
+
const flow = makeFlow();
|
|
105
|
+
const run = makeRun({
|
|
106
|
+
nodeStates: {
|
|
107
|
+
'response-200': {
|
|
108
|
+
status: 'succeeded',
|
|
109
|
+
output: { status: 200, body: { user: { name: 'Bob' } } },
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
const result = evaluateExpectation(run, flow, { body: { user: { name: 'Ada' } } });
|
|
114
|
+
expect(result.ok).toBe(false);
|
|
115
|
+
expect(result.failures).toEqual(['body.user.name: expected "Ada", got "Bob"']);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('checks executedNodes against nodeStates, reporting skipped nodes', () => {
|
|
119
|
+
const flow = makeFlow();
|
|
120
|
+
const run = makeRun({
|
|
121
|
+
nodeStates: {
|
|
122
|
+
'response-200': { status: 'succeeded', output: { status: 200, body: {} } },
|
|
123
|
+
'response-401': { status: 'skipped' },
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
const result = evaluateExpectation(run, flow, { executedNodes: ['response-200', 'response-401'] });
|
|
127
|
+
expect(result.ok).toBe(false);
|
|
128
|
+
expect(result.failures).toEqual(['executedNodes: response-401 did not execute (status: skipped)']);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('passes when status, body, and executedNodes all match', () => {
|
|
132
|
+
const flow = makeFlow();
|
|
133
|
+
const run = makeRun({
|
|
134
|
+
nodeStates: {
|
|
135
|
+
'response-200': {
|
|
136
|
+
status: 'succeeded',
|
|
137
|
+
output: { status: 200, body: { user: { name: 'Ada' } } },
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
const result = evaluateExpectation(run, flow, {
|
|
142
|
+
status: 200,
|
|
143
|
+
body: { user: { name: 'Ada' } },
|
|
144
|
+
executedNodes: ['response-200'],
|
|
145
|
+
});
|
|
146
|
+
expect(result).toEqual({ ok: true, failures: [] });
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { ScenarioExpectation, WorkflowDefinition, WorkflowRun } from '../src/engine';
|
|
2
|
+
import { extractResponse } from './operationResult';
|
|
3
|
+
|
|
4
|
+
function fmt(value: unknown): string {
|
|
5
|
+
if (value === undefined) return 'undefined';
|
|
6
|
+
return JSON.stringify(value);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Deep-subset match: every key/index present in `expected` must match the
|
|
10
|
+
* corresponding key/index in `actual`, recursively. Extra keys/indices in
|
|
11
|
+
* `actual` are ignored. `null` matches only `null`. Returns human-readable
|
|
12
|
+
* failure strings with dotted (and `[i]`) paths; empty array = match. */
|
|
13
|
+
export function deepSubsetMatch(expected: unknown, actual: unknown, path = ''): string[] {
|
|
14
|
+
const label = path === '' ? '' : `${path}: `;
|
|
15
|
+
|
|
16
|
+
if (expected === null) {
|
|
17
|
+
return actual === null ? [] : [`${label}expected null, got ${fmt(actual)}`];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (Array.isArray(expected)) {
|
|
21
|
+
if (!Array.isArray(actual)) {
|
|
22
|
+
return [`${label}expected array, got ${fmt(actual)}`];
|
|
23
|
+
}
|
|
24
|
+
const failures: string[] = [];
|
|
25
|
+
for (let i = 0; i < expected.length; i++) {
|
|
26
|
+
failures.push(...deepSubsetMatch(expected[i], actual[i], `${path}[${i}]`));
|
|
27
|
+
}
|
|
28
|
+
return failures;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (typeof expected === 'object') {
|
|
32
|
+
if (typeof actual !== 'object' || actual === null || Array.isArray(actual)) {
|
|
33
|
+
return [`${label}expected object, got ${fmt(actual)}`];
|
|
34
|
+
}
|
|
35
|
+
const failures: string[] = [];
|
|
36
|
+
for (const key of Object.keys(expected as Record<string, unknown>)) {
|
|
37
|
+
const childPath = path === '' ? key : `${path}.${key}`;
|
|
38
|
+
failures.push(
|
|
39
|
+
...deepSubsetMatch(
|
|
40
|
+
(expected as Record<string, unknown>)[key],
|
|
41
|
+
(actual as Record<string, unknown>)[key],
|
|
42
|
+
childPath,
|
|
43
|
+
),
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return failures;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Primitive.
|
|
50
|
+
return expected === actual ? [] : [`${label}expected ${fmt(expected)}, got ${fmt(actual)}`];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Check a scenario's finished run against its `expect` assertion. `status`/
|
|
54
|
+
* `body` come from `extractResponse` (the flow's Response-node mapping,
|
|
55
|
+
* defaulting to status 200); `executedNodes` checks each id's node state is
|
|
56
|
+
* `succeeded`. */
|
|
57
|
+
export function evaluateExpectation(
|
|
58
|
+
run: WorkflowRun,
|
|
59
|
+
flow: WorkflowDefinition,
|
|
60
|
+
expect: ScenarioExpectation,
|
|
61
|
+
): { ok: boolean; failures: string[] } {
|
|
62
|
+
const failures: string[] = [];
|
|
63
|
+
|
|
64
|
+
if (expect.status !== undefined || expect.body !== undefined) {
|
|
65
|
+
const { status, body } = extractResponse(run, flow);
|
|
66
|
+
|
|
67
|
+
if (expect.status !== undefined && expect.status !== status) {
|
|
68
|
+
failures.push(`status: expected ${expect.status}, got ${status}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (expect.body !== undefined) {
|
|
72
|
+
for (const failure of deepSubsetMatch(expect.body, body)) {
|
|
73
|
+
failures.push(`body.${failure}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (expect.executedNodes) {
|
|
79
|
+
for (const id of expect.executedNodes) {
|
|
80
|
+
const state = run.nodeStates[id];
|
|
81
|
+
if (state?.status !== 'succeeded') {
|
|
82
|
+
failures.push(`executedNodes: ${id} did not execute (status: ${state?.status ?? 'not run'})`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { ok: failures.length === 0, failures };
|
|
88
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
2
|
+
import { spawn, type ChildProcess } from 'node:child_process';
|
|
3
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
// Boots a real runner subprocess over a temp project (the agentRoute.test.ts
|
|
8
|
+
// pattern) and drives GET /setup-status. Because the route re-reads
|
|
9
|
+
// environments per request, one runner covers both a fresh, unconfigured
|
|
10
|
+
// project AND a project that has just gained an environments file: we assert
|
|
11
|
+
// the fresh shape, then write emberflow.environments.json and re-fetch.
|
|
12
|
+
|
|
13
|
+
const PORT = 8153;
|
|
14
|
+
const base = `http://127.0.0.1:${PORT}`;
|
|
15
|
+
|
|
16
|
+
async function waitHealthy(url: string, tries = 40): Promise<void> {
|
|
17
|
+
for (let i = 0; i < tries; i++) {
|
|
18
|
+
try {
|
|
19
|
+
if ((await fetch(url)).ok) return;
|
|
20
|
+
} catch {
|
|
21
|
+
/* not up yet */
|
|
22
|
+
}
|
|
23
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
24
|
+
}
|
|
25
|
+
throw new Error('runner did not become healthy');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let proc: ChildProcess;
|
|
29
|
+
let projectDir: string;
|
|
30
|
+
|
|
31
|
+
beforeAll(async () => {
|
|
32
|
+
projectDir = mkdtempSync(join(tmpdir(), 'setupstatus-'));
|
|
33
|
+
mkdirSync(join(projectDir, 'emberflow', 'apis', 'default'), { recursive: true });
|
|
34
|
+
writeFileSync(join(projectDir, 'emberflow.config.mjs'), 'export default {};\n');
|
|
35
|
+
// A pristine project ships exactly the single default/hello example op.
|
|
36
|
+
writeFileSync(
|
|
37
|
+
join(projectDir, 'emberflow', 'apis', 'default', 'hello.json'),
|
|
38
|
+
JSON.stringify({
|
|
39
|
+
id: 'default/hello',
|
|
40
|
+
name: 'Hello',
|
|
41
|
+
version: 1,
|
|
42
|
+
nodes: [
|
|
43
|
+
{ id: 'in', type: 'Input', label: 'Input', position: { x: 0, y: 0 }, config: {} },
|
|
44
|
+
{ id: 'out', type: 'Result', label: 'Result', position: { x: 300, y: 0 }, config: {}, inputMap: { value: { sourceNodeId: 'in', sourceField: '$' } } },
|
|
45
|
+
],
|
|
46
|
+
edges: [{ id: 'e1', source: 'in', target: 'out' }],
|
|
47
|
+
createdAt: '2026-07-12T00:00:00Z',
|
|
48
|
+
updatedAt: '2026-07-12T00:00:00Z',
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
proc = spawn('npx', ['tsx', 'server/index.ts'], {
|
|
53
|
+
env: {
|
|
54
|
+
...process.env,
|
|
55
|
+
EMBERFLOW_RUNNER_PORT: String(PORT),
|
|
56
|
+
EMBERFLOW_PROJECT: projectDir,
|
|
57
|
+
// The skills check also looks in the user's home dir (`init --global`);
|
|
58
|
+
// point homedir() at the temp project so a dev machine with real global
|
|
59
|
+
// skills doesn't flip the fresh-project assertion.
|
|
60
|
+
HOME: projectDir,
|
|
61
|
+
USERPROFILE: projectDir,
|
|
62
|
+
},
|
|
63
|
+
stdio: 'ignore',
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
await waitHealthy(`${base}/healthz`);
|
|
67
|
+
}, 20_000);
|
|
68
|
+
|
|
69
|
+
afterAll(() => {
|
|
70
|
+
proc?.kill();
|
|
71
|
+
if (projectDir) rmSync(projectDir, { recursive: true, force: true });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('GET /setup-status', () => {
|
|
75
|
+
it('reports the fresh, unconfigured shape for an init-like project', async () => {
|
|
76
|
+
const res = await fetch(`${base}/setup-status`);
|
|
77
|
+
expect(res.status).toBe(200);
|
|
78
|
+
const body = await res.json();
|
|
79
|
+
|
|
80
|
+
expect(Array.isArray(body.agents)).toBe(true);
|
|
81
|
+
// A fresh project has no environments file → the synthesized fallback is
|
|
82
|
+
// reported as unconfigured with zero real environments.
|
|
83
|
+
expect(body.environments).toMatchObject({
|
|
84
|
+
configured: false,
|
|
85
|
+
count: 0,
|
|
86
|
+
protectedCount: 0,
|
|
87
|
+
anyAuthConfigured: false,
|
|
88
|
+
});
|
|
89
|
+
expect(body.skills).toEqual({ claude: false, codex: false });
|
|
90
|
+
expect(body.ops).toEqual({ count: 1, onlyHello: true });
|
|
91
|
+
// Inferred from the .mjs config extension (project.language).
|
|
92
|
+
expect(body.language).toBe('javascript');
|
|
93
|
+
// Unconfigured projects default to mock serving.
|
|
94
|
+
expect(body.servingMode).toBe('mock');
|
|
95
|
+
expect(body.infrastructure).toEqual({ present: false });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('reflects an environments file written after boot (re-read per request)', async () => {
|
|
99
|
+
writeFileSync(
|
|
100
|
+
join(projectDir, 'emberflow.environments.json'),
|
|
101
|
+
JSON.stringify({
|
|
102
|
+
defaultEnvironment: 'dev',
|
|
103
|
+
environments: {
|
|
104
|
+
dev: { vars: {}, secrets: [] },
|
|
105
|
+
prod: {
|
|
106
|
+
vars: {},
|
|
107
|
+
secrets: ['API_KEY'],
|
|
108
|
+
protected: true,
|
|
109
|
+
auth: { attach: { as: 'header', name: 'Authorization', secretRef: 'API_KEY' } },
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
}),
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const res = await fetch(`${base}/setup-status`);
|
|
116
|
+
expect(res.status).toBe(200);
|
|
117
|
+
const body = await res.json();
|
|
118
|
+
expect(body.environments).toEqual({
|
|
119
|
+
configured: true,
|
|
120
|
+
count: 2,
|
|
121
|
+
protectedCount: 1,
|
|
122
|
+
anyAuthConfigured: true,
|
|
123
|
+
});
|
|
124
|
+
// ops are unchanged — still just the hello example.
|
|
125
|
+
expect(body.ops).toEqual({ count: 1, onlyHello: true });
|
|
126
|
+
});
|
|
127
|
+
});
|