@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,229 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
2
|
+
import { spawn, type ChildProcess } from 'node:child_process';
|
|
3
|
+
import { createServer, type Server } from 'node:http';
|
|
4
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { runCli } from './cli';
|
|
8
|
+
|
|
9
|
+
// Verifies `login-environment` CLI command + auth-aware `list-environments`
|
|
10
|
+
// output (Task 5). Boots the actual runner subprocess (server/index.ts)
|
|
11
|
+
// against a scratch project dir, mirroring the harness in
|
|
12
|
+
// environmentsRoute.test.ts, and drives it via the in-process CLI (runCli)
|
|
13
|
+
// against EMBERFLOW_RUNNER_URL, like the register-API bin does.
|
|
14
|
+
|
|
15
|
+
let proc: ChildProcess;
|
|
16
|
+
const PORT = 8135;
|
|
17
|
+
const base = `http://127.0.0.1:${PORT}`;
|
|
18
|
+
let projectDir: string;
|
|
19
|
+
|
|
20
|
+
let loginServer: Server;
|
|
21
|
+
const LOGIN_PORT = 8136;
|
|
22
|
+
|
|
23
|
+
async function waitHealthy(url: string, tries = 40): Promise<void> {
|
|
24
|
+
for (let i = 0; i < tries; i++) {
|
|
25
|
+
try {
|
|
26
|
+
if ((await fetch(url)).ok) return;
|
|
27
|
+
} catch {
|
|
28
|
+
/* not up yet */
|
|
29
|
+
}
|
|
30
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
31
|
+
}
|
|
32
|
+
throw new Error('runner did not become healthy');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function bootRunner(port: number, env: Record<string, string | undefined>): ChildProcess {
|
|
36
|
+
return spawn('npx', ['tsx', 'server/index.ts'], {
|
|
37
|
+
env: { ...process.env, EMBERFLOW_RUNNER_PORT: String(port), ...env },
|
|
38
|
+
stdio: 'ignore',
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let logs: string[] = [];
|
|
43
|
+
let errs: string[] = [];
|
|
44
|
+
const origLog = process.stdout.write.bind(process.stdout);
|
|
45
|
+
const origErr = process.stderr.write.bind(process.stderr);
|
|
46
|
+
|
|
47
|
+
function captureOutput(): void {
|
|
48
|
+
logs = [];
|
|
49
|
+
errs = [];
|
|
50
|
+
process.stdout.write = ((chunk: unknown, ...rest: unknown[]) => {
|
|
51
|
+
logs.push(String(chunk));
|
|
52
|
+
return (origLog as unknown as (...a: unknown[]) => boolean)(chunk, ...rest);
|
|
53
|
+
}) as typeof process.stdout.write;
|
|
54
|
+
process.stderr.write = ((chunk: unknown, ...rest: unknown[]) => {
|
|
55
|
+
errs.push(String(chunk));
|
|
56
|
+
return (origErr as unknown as (...a: unknown[]) => boolean)(chunk, ...rest);
|
|
57
|
+
}) as typeof process.stderr.write;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function restoreOutput(): void {
|
|
61
|
+
process.stdout.write = origLog;
|
|
62
|
+
process.stderr.write = origErr;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
beforeAll(async () => {
|
|
66
|
+
// Tiny stub login target: responds 200 with a Set-Cookie header.
|
|
67
|
+
loginServer = createServer((_req, res) => {
|
|
68
|
+
res.setHeader('Set-Cookie', 'session=captured-token; Path=/');
|
|
69
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
70
|
+
res.end(JSON.stringify({ ok: true }));
|
|
71
|
+
});
|
|
72
|
+
await new Promise<void>((resolve) => loginServer.listen(LOGIN_PORT, resolve));
|
|
73
|
+
|
|
74
|
+
projectDir = mkdtempSync(join(tmpdir(), 'cliauth-'));
|
|
75
|
+
mkdirSync(join(projectDir, 'emberflow', 'apis', 'default'), { recursive: true });
|
|
76
|
+
writeFileSync(join(projectDir, 'emberflow.config.mjs'), 'export default {};\n');
|
|
77
|
+
writeFileSync(
|
|
78
|
+
join(projectDir, 'emberflow.environments.json'),
|
|
79
|
+
JSON.stringify({
|
|
80
|
+
defaultEnvironment: 'dev',
|
|
81
|
+
environments: {
|
|
82
|
+
dev: {
|
|
83
|
+
vars: {},
|
|
84
|
+
secrets: {},
|
|
85
|
+
auth: {
|
|
86
|
+
attach: { as: 'cookie', name: 'session', secretRef: 'sessionCookie' },
|
|
87
|
+
login: {
|
|
88
|
+
request: { method: 'POST', url: `http://127.0.0.1:${LOGIN_PORT}/login` },
|
|
89
|
+
capture: { from: 'set-cookie', cookieName: 'session' },
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
noauth: {
|
|
94
|
+
vars: {},
|
|
95
|
+
secrets: {},
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
}),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
proc = bootRunner(PORT, { EMBERFLOW_PROJECT: projectDir });
|
|
102
|
+
await waitHealthy(`${base}/healthz`);
|
|
103
|
+
|
|
104
|
+
// The CLI (client.ts) reads EMBERFLOW_RUNNER_URL per-call, so point it at
|
|
105
|
+
// this test's runner subprocess.
|
|
106
|
+
process.env.EMBERFLOW_RUNNER_URL = base;
|
|
107
|
+
}, 20_000);
|
|
108
|
+
|
|
109
|
+
afterAll(async () => {
|
|
110
|
+
proc?.kill();
|
|
111
|
+
await new Promise<void>((resolve) => loginServer.close(() => resolve()));
|
|
112
|
+
if (projectDir) rmSync(projectDir, { recursive: true, force: true });
|
|
113
|
+
delete process.env.EMBERFLOW_RUNNER_URL;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('login-environment CLI command', () => {
|
|
117
|
+
it('logs in successfully: exit 0, prints {environment, authenticated: true, secretRef}', async () => {
|
|
118
|
+
captureOutput();
|
|
119
|
+
let code: number;
|
|
120
|
+
try {
|
|
121
|
+
code = await runCli(['login-environment', 'dev']);
|
|
122
|
+
} finally {
|
|
123
|
+
restoreOutput();
|
|
124
|
+
}
|
|
125
|
+
expect(code).toBe(0);
|
|
126
|
+
const printed = JSON.parse(logs.join(''));
|
|
127
|
+
expect(printed).toEqual({ environment: 'dev', authenticated: true, secretRef: 'sessionCookie' });
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('unknown environment: exit 1, prints the server error', async () => {
|
|
131
|
+
captureOutput();
|
|
132
|
+
let code: number;
|
|
133
|
+
try {
|
|
134
|
+
code = await runCli(['login-environment', 'no-such-env']);
|
|
135
|
+
} finally {
|
|
136
|
+
restoreOutput();
|
|
137
|
+
}
|
|
138
|
+
expect(code).toBe(1);
|
|
139
|
+
const printed = JSON.parse(errs.join(''));
|
|
140
|
+
expect(printed.error).toMatch(/unknown environment/i);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('list-environments CLI command (auth-aware)', () => {
|
|
145
|
+
it('includes per-env auth: {configured, authenticated}', async () => {
|
|
146
|
+
captureOutput();
|
|
147
|
+
let code: number;
|
|
148
|
+
try {
|
|
149
|
+
code = await runCli(['list-environments']);
|
|
150
|
+
} finally {
|
|
151
|
+
restoreOutput();
|
|
152
|
+
}
|
|
153
|
+
expect(code).toBe(0);
|
|
154
|
+
const printed = JSON.parse(logs.join(''));
|
|
155
|
+
const dev = printed.environments.find((e: { name: string }) => e.name === 'dev');
|
|
156
|
+
const noauth = printed.environments.find((e: { name: string }) => e.name === 'noauth');
|
|
157
|
+
// dev was logged in by the earlier test in this file.
|
|
158
|
+
expect(dev.auth).toEqual({
|
|
159
|
+
configured: true,
|
|
160
|
+
authenticated: true,
|
|
161
|
+
secretRef: 'sessionCookie',
|
|
162
|
+
config: {
|
|
163
|
+
attach: { as: 'cookie', name: 'session', secretRef: 'sessionCookie' },
|
|
164
|
+
login: {
|
|
165
|
+
request: { method: 'POST', url: `http://127.0.0.1:${LOGIN_PORT}/login` },
|
|
166
|
+
capture: { from: 'set-cookie', cookieName: 'session' },
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
expect(noauth.auth).toEqual({ configured: false, authenticated: false });
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe('set-environment-auth CLI command', () => {
|
|
175
|
+
it('valid EnvAuth JSON: exit 0, prints {environment, configured: true}', async () => {
|
|
176
|
+
const auth = {
|
|
177
|
+
attach: { as: 'header', name: 'Authorization', secretRef: 'apiKey', prefix: 'Bearer ' },
|
|
178
|
+
};
|
|
179
|
+
captureOutput();
|
|
180
|
+
let code: number;
|
|
181
|
+
try {
|
|
182
|
+
code = await runCli(['set-environment-auth', 'noauth', '--json', JSON.stringify(auth)]);
|
|
183
|
+
} finally {
|
|
184
|
+
restoreOutput();
|
|
185
|
+
}
|
|
186
|
+
expect(code).toBe(0);
|
|
187
|
+
const printed = JSON.parse(logs.join(''));
|
|
188
|
+
expect(printed).toEqual({ environment: 'noauth', configured: true });
|
|
189
|
+
|
|
190
|
+
// Confirm it actually landed: list-environments now reports it configured.
|
|
191
|
+
captureOutput();
|
|
192
|
+
let listCode: number;
|
|
193
|
+
try {
|
|
194
|
+
listCode = await runCli(['list-environments']);
|
|
195
|
+
} finally {
|
|
196
|
+
restoreOutput();
|
|
197
|
+
}
|
|
198
|
+
expect(listCode).toBe(0);
|
|
199
|
+
const listed = JSON.parse(logs.join(''));
|
|
200
|
+
const noauthEnv = listed.environments.find((e: { name: string }) => e.name === 'noauth');
|
|
201
|
+
expect(noauthEnv.auth.configured).toBe(true);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('invalid auth shape: exit 1, prints the server error', async () => {
|
|
205
|
+
captureOutput();
|
|
206
|
+
let code: number;
|
|
207
|
+
try {
|
|
208
|
+
code = await runCli(['set-environment-auth', 'dev', '--json', '{}']);
|
|
209
|
+
} finally {
|
|
210
|
+
restoreOutput();
|
|
211
|
+
}
|
|
212
|
+
expect(code).toBe(1);
|
|
213
|
+
const printed = JSON.parse(errs.join(''));
|
|
214
|
+
expect(printed.error).toMatch(/auth/i);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('missing --json flag: exit 2, usage error', async () => {
|
|
218
|
+
captureOutput();
|
|
219
|
+
let code: number;
|
|
220
|
+
try {
|
|
221
|
+
code = await runCli(['set-environment-auth', 'dev']);
|
|
222
|
+
} finally {
|
|
223
|
+
restoreOutput();
|
|
224
|
+
}
|
|
225
|
+
expect(code).toBe(2);
|
|
226
|
+
const printed = JSON.parse(errs.join(''));
|
|
227
|
+
expect(printed.error).toMatch(/usage/i);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
2
|
+
import { spawn, type ChildProcess } from 'node:child_process';
|
|
3
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { runCli } from './cli';
|
|
7
|
+
|
|
8
|
+
// Verifies the `create` CLI command seeds Input node defaults for :params in
|
|
9
|
+
// the http path, so a plain "Run" (no scenario) doesn't crash the first node
|
|
10
|
+
// reading ctx.input.params.<name>. Boots the actual runner subprocess against
|
|
11
|
+
// a scratch project dir, same harness pattern as cliAuth.test.ts.
|
|
12
|
+
|
|
13
|
+
let proc: ChildProcess;
|
|
14
|
+
const PORT = 8137;
|
|
15
|
+
const base = `http://127.0.0.1:${PORT}`;
|
|
16
|
+
let projectDir: string;
|
|
17
|
+
|
|
18
|
+
async function waitHealthy(url: string, tries = 40): Promise<void> {
|
|
19
|
+
for (let i = 0; i < tries; i++) {
|
|
20
|
+
try {
|
|
21
|
+
if ((await fetch(url)).ok) return;
|
|
22
|
+
} catch {
|
|
23
|
+
/* not up yet */
|
|
24
|
+
}
|
|
25
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
26
|
+
}
|
|
27
|
+
throw new Error('runner did not become healthy');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function bootRunner(port: number, env: Record<string, string | undefined>): ChildProcess {
|
|
31
|
+
return spawn('npx', ['tsx', 'server/index.ts'], {
|
|
32
|
+
env: { ...process.env, EMBERFLOW_RUNNER_PORT: String(port), ...env },
|
|
33
|
+
stdio: 'ignore',
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let logs: string[] = [];
|
|
38
|
+
let errs: string[] = [];
|
|
39
|
+
const origLog = process.stdout.write.bind(process.stdout);
|
|
40
|
+
const origErr = process.stderr.write.bind(process.stderr);
|
|
41
|
+
|
|
42
|
+
function captureOutput(): void {
|
|
43
|
+
logs = [];
|
|
44
|
+
errs = [];
|
|
45
|
+
process.stdout.write = ((chunk: unknown, ...rest: unknown[]) => {
|
|
46
|
+
logs.push(String(chunk));
|
|
47
|
+
return (origLog as unknown as (...a: unknown[]) => boolean)(chunk, ...rest);
|
|
48
|
+
}) as typeof process.stdout.write;
|
|
49
|
+
process.stderr.write = ((chunk: unknown, ...rest: unknown[]) => {
|
|
50
|
+
errs.push(String(chunk));
|
|
51
|
+
return (origErr as unknown as (...a: unknown[]) => boolean)(chunk, ...rest);
|
|
52
|
+
}) as typeof process.stderr.write;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function restoreOutput(): void {
|
|
56
|
+
process.stdout.write = origLog;
|
|
57
|
+
process.stderr.write = origErr;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
beforeAll(async () => {
|
|
61
|
+
projectDir = mkdtempSync(join(tmpdir(), 'clicreate-'));
|
|
62
|
+
mkdirSync(join(projectDir, 'emberflow', 'apis', 'default'), { recursive: true });
|
|
63
|
+
writeFileSync(join(projectDir, 'emberflow.config.mjs'), 'export default {};\n');
|
|
64
|
+
writeFileSync(
|
|
65
|
+
join(projectDir, 'emberflow.environments.json'),
|
|
66
|
+
JSON.stringify({ defaultEnvironment: 'dev', environments: { dev: { vars: {}, secrets: {} } } }),
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
proc = bootRunner(PORT, { EMBERFLOW_PROJECT: projectDir });
|
|
70
|
+
await waitHealthy(`${base}/healthz`);
|
|
71
|
+
process.env.EMBERFLOW_RUNNER_URL = base;
|
|
72
|
+
}, 20_000);
|
|
73
|
+
|
|
74
|
+
afterAll(() => {
|
|
75
|
+
proc?.kill();
|
|
76
|
+
if (projectDir) rmSync(projectDir, { recursive: true, force: true });
|
|
77
|
+
delete process.env.EMBERFLOW_RUNNER_URL;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('create CLI command', () => {
|
|
81
|
+
it('seeds Input config.defaults.params for each :param in the http path', async () => {
|
|
82
|
+
captureOutput();
|
|
83
|
+
let code: number;
|
|
84
|
+
try {
|
|
85
|
+
code = await runCli([
|
|
86
|
+
'create',
|
|
87
|
+
'channels/get',
|
|
88
|
+
'--method',
|
|
89
|
+
'GET',
|
|
90
|
+
'--path',
|
|
91
|
+
'/api/channels/:id/approvals/:approvalId',
|
|
92
|
+
]);
|
|
93
|
+
} finally {
|
|
94
|
+
restoreOutput();
|
|
95
|
+
}
|
|
96
|
+
expect(code).toBe(0);
|
|
97
|
+
|
|
98
|
+
captureOutput();
|
|
99
|
+
try {
|
|
100
|
+
code = await runCli(['get-workflow', 'channels/get']);
|
|
101
|
+
} finally {
|
|
102
|
+
restoreOutput();
|
|
103
|
+
}
|
|
104
|
+
expect(code).toBe(0);
|
|
105
|
+
const flow = JSON.parse(logs.join(''));
|
|
106
|
+
const inputNode = flow.nodes.find((n: { type: string }) => n.type === 'Input');
|
|
107
|
+
expect(inputNode.config.defaults.params).toEqual({ id: '', approvalId: '' });
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('leaves Input config bare when the path has no params', async () => {
|
|
111
|
+
captureOutput();
|
|
112
|
+
let code: number;
|
|
113
|
+
try {
|
|
114
|
+
code = await runCli(['create', 'channels/list', '--method', 'GET', '--path', '/api/channels']);
|
|
115
|
+
} finally {
|
|
116
|
+
restoreOutput();
|
|
117
|
+
}
|
|
118
|
+
expect(code).toBe(0);
|
|
119
|
+
|
|
120
|
+
captureOutput();
|
|
121
|
+
try {
|
|
122
|
+
code = await runCli(['get-workflow', 'channels/list']);
|
|
123
|
+
} finally {
|
|
124
|
+
restoreOutput();
|
|
125
|
+
}
|
|
126
|
+
expect(code).toBe(0);
|
|
127
|
+
const flow = JSON.parse(logs.join(''));
|
|
128
|
+
const inputNode = flow.nodes.find((n: { type: string }) => n.type === 'Input');
|
|
129
|
+
expect(inputNode.config.defaults).toBeUndefined();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { runCli } from './cli';
|
|
6
|
+
|
|
7
|
+
// Verifies `emberflow doctor [id] [--fix]` (Task 3): reads op + scenario
|
|
8
|
+
// files from disk the same way `test` does (no runner needed), reports
|
|
9
|
+
// diagnoseOperation's findings, and with --fix seeds param defaults in-place
|
|
10
|
+
// then re-diagnoses. Also exercises the bin/commands.ts dispatcher path so
|
|
11
|
+
// `doctor` doesn't repeat the past `login-environment`-forgotten-in-dispatcher
|
|
12
|
+
// bug.
|
|
13
|
+
|
|
14
|
+
const dirs: string[] = [];
|
|
15
|
+
function scratch(): string {
|
|
16
|
+
const d = mkdtempSync(join(tmpdir(), 'ef-clidoctor-'));
|
|
17
|
+
dirs.push(d);
|
|
18
|
+
return d;
|
|
19
|
+
}
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true });
|
|
22
|
+
delete process.env.EMBERFLOW_PROJECT;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const savedEnv = { ...process.env };
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
process.env = { ...savedEnv };
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
function writeProjectConfig(root: string): void {
|
|
31
|
+
writeFileSync(join(root, 'emberflow.config.mjs'), 'export default {};\n');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** A project config registering a 'db'-traceKind node, for the
|
|
35
|
+
* missing-node-mock test — proves doctor builds a registry and passes
|
|
36
|
+
* infraNodes through to diagnoseOperation. */
|
|
37
|
+
function writeProjectConfigWithDbNode(root: string): void {
|
|
38
|
+
writeFileSync(
|
|
39
|
+
join(root, 'emberflow.config.mjs'),
|
|
40
|
+
`export default {
|
|
41
|
+
registerNodes(registry) {
|
|
42
|
+
registry.register(
|
|
43
|
+
{ type: 'DbRead', label: 'DB Read', traceKind: 'db', inputSchema: { fields: [] } },
|
|
44
|
+
async () => ({ rows: [] }),
|
|
45
|
+
);
|
|
46
|
+
},
|
|
47
|
+
};\n`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** An op whose only node is a registered 'db'-traceKind node, with no op-level
|
|
52
|
+
* mocks and no scenarios — should trigger missing-node-mock alongside no-expects. */
|
|
53
|
+
function writeUnmockedInfraOp(root: string): void {
|
|
54
|
+
const apisDir = join(root, 'emberflow', 'apis', 'default');
|
|
55
|
+
mkdirSync(apisDir, { recursive: true });
|
|
56
|
+
writeFileSync(
|
|
57
|
+
join(apisDir, 'infra.json'),
|
|
58
|
+
JSON.stringify(
|
|
59
|
+
{
|
|
60
|
+
id: 'infra',
|
|
61
|
+
name: 'Infra',
|
|
62
|
+
version: 1,
|
|
63
|
+
nodes: [{ id: 'db1', type: 'DbRead', label: 'Fetch Rows', position: { x: 0, y: 0 }, config: {} }],
|
|
64
|
+
edges: [],
|
|
65
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
66
|
+
updatedAt: '2026-01-01T00:00:00Z',
|
|
67
|
+
},
|
|
68
|
+
null,
|
|
69
|
+
2,
|
|
70
|
+
) + '\n',
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** An op with a path param `:id`, no default under the Input node, and no
|
|
75
|
+
* scenarios — should trigger all three v1 diagnostic codes. */
|
|
76
|
+
function writeBrokenOp(root: string): void {
|
|
77
|
+
const apisDir = join(root, 'emberflow', 'apis', 'default');
|
|
78
|
+
mkdirSync(apisDir, { recursive: true });
|
|
79
|
+
writeFileSync(
|
|
80
|
+
join(apisDir, 'broken.json'),
|
|
81
|
+
JSON.stringify(
|
|
82
|
+
{
|
|
83
|
+
id: 'broken',
|
|
84
|
+
name: 'Broken',
|
|
85
|
+
version: 1,
|
|
86
|
+
http: { method: 'GET', path: '/broken/:id' },
|
|
87
|
+
nodes: [
|
|
88
|
+
{ id: 'input', type: 'Input', label: 'Input', position: { x: 0, y: 0 }, config: {} },
|
|
89
|
+
{ id: 'result', type: 'Result', label: 'Result', position: { x: 200, y: 0 }, config: {} },
|
|
90
|
+
],
|
|
91
|
+
edges: [{ id: 'e1', source: 'input', target: 'result' }],
|
|
92
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
93
|
+
updatedAt: '2026-01-01T00:00:00Z',
|
|
94
|
+
},
|
|
95
|
+
null,
|
|
96
|
+
2,
|
|
97
|
+
) + '\n',
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** A clean op: no path params, one scenario carrying `expect`. */
|
|
102
|
+
function writeCleanOp(root: string): void {
|
|
103
|
+
const apisDir = join(root, 'emberflow', 'apis', 'default');
|
|
104
|
+
mkdirSync(apisDir, { recursive: true });
|
|
105
|
+
writeFileSync(
|
|
106
|
+
join(apisDir, 'clean.json'),
|
|
107
|
+
JSON.stringify(
|
|
108
|
+
{
|
|
109
|
+
id: 'clean',
|
|
110
|
+
name: 'Clean',
|
|
111
|
+
version: 1,
|
|
112
|
+
http: { method: 'GET', path: '/clean' },
|
|
113
|
+
nodes: [
|
|
114
|
+
{ id: 'input', type: 'Input', label: 'Input', position: { x: 0, y: 0 }, config: {} },
|
|
115
|
+
{ id: 'result', type: 'Result', label: 'Result', position: { x: 200, y: 0 }, config: {} },
|
|
116
|
+
],
|
|
117
|
+
edges: [{ id: 'e1', source: 'input', target: 'result' }],
|
|
118
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
119
|
+
updatedAt: '2026-01-01T00:00:00Z',
|
|
120
|
+
},
|
|
121
|
+
null,
|
|
122
|
+
2,
|
|
123
|
+
) + '\n',
|
|
124
|
+
);
|
|
125
|
+
writeFileSync(
|
|
126
|
+
join(apisDir, 'clean.scenarios.json'),
|
|
127
|
+
JSON.stringify(
|
|
128
|
+
[{ id: 's1', name: 'ok', input: {}, expect: { status: 200 } }],
|
|
129
|
+
null,
|
|
130
|
+
2,
|
|
131
|
+
) + '\n',
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
describe('emberflow doctor (CLI)', () => {
|
|
136
|
+
let logs: string[];
|
|
137
|
+
let errs: string[];
|
|
138
|
+
|
|
139
|
+
function captureOutput(): void {
|
|
140
|
+
logs = [];
|
|
141
|
+
errs = [];
|
|
142
|
+
const origOut = process.stdout.write.bind(process.stdout);
|
|
143
|
+
const origErr = process.stderr.write.bind(process.stderr);
|
|
144
|
+
process.stdout.write = ((chunk: unknown, ...rest: unknown[]) => {
|
|
145
|
+
logs.push(String(chunk));
|
|
146
|
+
return origOut(chunk as never, ...(rest as []));
|
|
147
|
+
}) as typeof process.stdout.write;
|
|
148
|
+
process.stderr.write = ((chunk: unknown, ...rest: unknown[]) => {
|
|
149
|
+
errs.push(String(chunk));
|
|
150
|
+
return origErr(chunk as never, ...(rest as []));
|
|
151
|
+
}) as typeof process.stderr.write;
|
|
152
|
+
restoreOutput = () => {
|
|
153
|
+
process.stdout.write = origOut;
|
|
154
|
+
process.stderr.write = origErr;
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
let restoreOutput: () => void = () => {};
|
|
158
|
+
|
|
159
|
+
beforeEach(() => {
|
|
160
|
+
captureOutput();
|
|
161
|
+
});
|
|
162
|
+
afterEach(() => {
|
|
163
|
+
restoreOutput();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('reports missing-param-default + param-no-real-scenario + no-expects for a broken op, exit 0 (no errors)', async () => {
|
|
167
|
+
const root = scratch();
|
|
168
|
+
writeProjectConfig(root);
|
|
169
|
+
writeBrokenOp(root);
|
|
170
|
+
process.env.EMBERFLOW_PROJECT = root;
|
|
171
|
+
|
|
172
|
+
const code = await runCli(['doctor', 'broken']);
|
|
173
|
+
|
|
174
|
+
expect(code).toBe(0);
|
|
175
|
+
const out = logs.join('');
|
|
176
|
+
expect(out).toContain('missing-param-default');
|
|
177
|
+
expect(out).toContain('param-no-real-scenario');
|
|
178
|
+
expect(out).toContain('no-expects');
|
|
179
|
+
expect(out).not.toMatch(/\berror\b/);
|
|
180
|
+
expect(out).toMatch(/\d+ errors, \d+ warnings, \d+ info across \d+ operations/);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('reports a clean op with none of the v1 diagnostic codes and exit 0', async () => {
|
|
184
|
+
const root = scratch();
|
|
185
|
+
writeProjectConfig(root);
|
|
186
|
+
writeCleanOp(root);
|
|
187
|
+
process.env.EMBERFLOW_PROJECT = root;
|
|
188
|
+
|
|
189
|
+
const code = await runCli(['doctor', 'clean']);
|
|
190
|
+
|
|
191
|
+
expect(code).toBe(0);
|
|
192
|
+
const out = logs.join('');
|
|
193
|
+
expect(out).not.toContain('missing-param-default');
|
|
194
|
+
expect(out).not.toContain('param-no-real-scenario');
|
|
195
|
+
expect(out).not.toContain('no-expects');
|
|
196
|
+
expect(out).toContain('0 errors, 0 warnings, 0 info across 1 operations');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('doctor (no id) reports across every operation in the project', async () => {
|
|
200
|
+
const root = scratch();
|
|
201
|
+
writeProjectConfig(root);
|
|
202
|
+
writeBrokenOp(root);
|
|
203
|
+
writeCleanOp(root);
|
|
204
|
+
process.env.EMBERFLOW_PROJECT = root;
|
|
205
|
+
|
|
206
|
+
const code = await runCli(['doctor']);
|
|
207
|
+
|
|
208
|
+
expect(code).toBe(0);
|
|
209
|
+
const out = logs.join('');
|
|
210
|
+
expect(out).toContain('broken');
|
|
211
|
+
expect(out).toContain('clean');
|
|
212
|
+
expect(out).toMatch(/across 2 operations/);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('--fix rewrites the broken op file with seeded param defaults and the re-report drops missing-param-default', async () => {
|
|
216
|
+
const root = scratch();
|
|
217
|
+
writeProjectConfig(root);
|
|
218
|
+
writeBrokenOp(root);
|
|
219
|
+
process.env.EMBERFLOW_PROJECT = root;
|
|
220
|
+
|
|
221
|
+
const code = await runCli(['doctor', 'broken', '--fix']);
|
|
222
|
+
|
|
223
|
+
expect(code).toBe(0);
|
|
224
|
+
const out = logs.join('');
|
|
225
|
+
expect(out).toContain('fixed broken: seeded params — id');
|
|
226
|
+
|
|
227
|
+
const onDisk = JSON.parse(
|
|
228
|
+
readFileSync(join(root, 'emberflow', 'apis', 'default', 'broken.json'), 'utf8'),
|
|
229
|
+
) as { nodes: Array<{ id: string; config?: { defaults?: { params?: Record<string, unknown> } } }> };
|
|
230
|
+
const inputNode = onDisk.nodes.find((n) => n.id === 'input')!;
|
|
231
|
+
expect(inputNode.config?.defaults?.params).toEqual({ id: '' });
|
|
232
|
+
|
|
233
|
+
// Ordering per op: header line (`<op.id>`) first, then the `fixed <id>: …`
|
|
234
|
+
// line, then the post-fix diagnostic lines.
|
|
235
|
+
const lines = out.split('\n');
|
|
236
|
+
const headerIdx = lines.indexOf('broken');
|
|
237
|
+
const fixedIdx = lines.findIndex((l) => l.startsWith('fixed broken: seeded params — id'));
|
|
238
|
+
const firstDiagnosticIdx = lines.findIndex((l) => /param-no-real-scenario|no-expects/.test(l));
|
|
239
|
+
expect(headerIdx).toBeGreaterThanOrEqual(0);
|
|
240
|
+
expect(fixedIdx).toBeGreaterThan(headerIdx);
|
|
241
|
+
expect(firstDiagnosticIdx).toBeGreaterThan(fixedIdx);
|
|
242
|
+
|
|
243
|
+
// Re-diagnosis after the fix: missing-param-default is gone, but the
|
|
244
|
+
// info-level diagnostics (no real scenario, no expects) remain since
|
|
245
|
+
// --fix only seeds defaults, it doesn't add scenarios.
|
|
246
|
+
const afterFixReport = out.split('fixed broken')[1] ?? out;
|
|
247
|
+
expect(afterFixReport).not.toContain('missing-param-default');
|
|
248
|
+
expect(afterFixReport).toContain('param-no-real-scenario');
|
|
249
|
+
expect(afterFixReport).toContain('no-expects');
|
|
250
|
+
|
|
251
|
+
// File format preserved: 2-space indent, trailing newline.
|
|
252
|
+
const raw = readFileSync(join(root, 'emberflow', 'apis', 'default', 'broken.json'), 'utf8');
|
|
253
|
+
expect(raw.endsWith('\n')).toBe(true);
|
|
254
|
+
expect(raw).toContain(' "id": "broken"');
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('--fix is a no-op (no "fixed" line) for an op that needs no seeding', async () => {
|
|
258
|
+
const root = scratch();
|
|
259
|
+
writeProjectConfig(root);
|
|
260
|
+
writeCleanOp(root);
|
|
261
|
+
process.env.EMBERFLOW_PROJECT = root;
|
|
262
|
+
|
|
263
|
+
const code = await runCli(['doctor', 'clean', '--fix']);
|
|
264
|
+
|
|
265
|
+
expect(code).toBe(0);
|
|
266
|
+
expect(logs.join('')).not.toContain('fixed clean');
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('unknown operation id exits 2', async () => {
|
|
270
|
+
const root = scratch();
|
|
271
|
+
writeProjectConfig(root);
|
|
272
|
+
writeCleanOp(root);
|
|
273
|
+
process.env.EMBERFLOW_PROJECT = root;
|
|
274
|
+
|
|
275
|
+
const code = await runCli(['doctor', 'no-such-op']);
|
|
276
|
+
expect(code).toBe(2);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('unknown flag exits 2 with usage', async () => {
|
|
280
|
+
const root = scratch();
|
|
281
|
+
writeProjectConfig(root);
|
|
282
|
+
writeCleanOp(root);
|
|
283
|
+
process.env.EMBERFLOW_PROJECT = root;
|
|
284
|
+
|
|
285
|
+
const code = await runCli(['doctor', '--bogus']);
|
|
286
|
+
expect(code).toBe(2);
|
|
287
|
+
expect(errs.join('')).toMatch(/usage/i);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('EMBERFLOW_PROJECT pointing at a config-less dir exits 2 (not 1)', async () => {
|
|
291
|
+
const root = scratch();
|
|
292
|
+
// No emberflow.config.* written — project loading fails.
|
|
293
|
+
process.env.EMBERFLOW_PROJECT = root;
|
|
294
|
+
|
|
295
|
+
const code = await runCli(['doctor']);
|
|
296
|
+
|
|
297
|
+
expect(code).toBe(2);
|
|
298
|
+
expect(errs.join('')).toMatch(/emberflow\.config/);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('reports missing-node-mock for an infra node (registry-registered traceKind db) with no mocks', async () => {
|
|
302
|
+
const root = scratch();
|
|
303
|
+
writeProjectConfigWithDbNode(root);
|
|
304
|
+
writeUnmockedInfraOp(root);
|
|
305
|
+
process.env.EMBERFLOW_PROJECT = root;
|
|
306
|
+
|
|
307
|
+
const code = await runCli(['doctor', 'infra']);
|
|
308
|
+
|
|
309
|
+
expect(code).toBe(0);
|
|
310
|
+
const out = logs.join('');
|
|
311
|
+
expect(out).toContain('missing-node-mock');
|
|
312
|
+
expect(out).toContain('"Fetch Rows" touches infrastructure (db) but has no mock');
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('dispatches through bin/commands.ts (the past login-environment-forgot-the-dispatcher bug must not repeat)', async () => {
|
|
316
|
+
const { runCommand } = await import('../bin/commands');
|
|
317
|
+
const root = scratch();
|
|
318
|
+
writeProjectConfig(root);
|
|
319
|
+
writeCleanOp(root);
|
|
320
|
+
|
|
321
|
+
const code = await runCommand({ command: 'doctor', project: root, rest: ['clean'] });
|
|
322
|
+
|
|
323
|
+
expect(code).toBe(0);
|
|
324
|
+
expect(logs.join('')).toContain('clean');
|
|
325
|
+
});
|
|
326
|
+
});
|