@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,186 @@
|
|
|
1
|
+
import { NodeRegistry } from '../engine';
|
|
2
|
+
|
|
3
|
+
const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));
|
|
4
|
+
|
|
5
|
+
/** Strip the `user-` prefix from a userId to get the username part. */
|
|
6
|
+
function usernamePart(userId: string): string {
|
|
7
|
+
return userId.startsWith('user-') ? userId.slice('user-'.length) : userId;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Build a NodeRegistry populated with the five login example nodes.
|
|
12
|
+
* `delayMs` is injected into every implementation via `await sleep(delayMs)`
|
|
13
|
+
* so tests can run with no delay by passing `0`.
|
|
14
|
+
*/
|
|
15
|
+
export function createLoginRegistry(delayMs = 300): NodeRegistry {
|
|
16
|
+
const registry = new NodeRegistry();
|
|
17
|
+
|
|
18
|
+
registry.register(
|
|
19
|
+
{
|
|
20
|
+
type: 'ValidateCredentials',
|
|
21
|
+
label: 'Validate Credentials',
|
|
22
|
+
description: 'Validates a username and password.',
|
|
23
|
+
category: 'auth',
|
|
24
|
+
traceKind: 'compute',
|
|
25
|
+
inputSchema: {
|
|
26
|
+
fields: [
|
|
27
|
+
{ name: 'username', type: 'string', required: true },
|
|
28
|
+
{ name: 'password', type: 'string', required: true },
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
outputSchema: {
|
|
32
|
+
fields: [
|
|
33
|
+
{ name: 'userId', type: 'string' },
|
|
34
|
+
{ name: 'username', type: 'string' },
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
async (ctx) => {
|
|
39
|
+
await sleep(delayMs);
|
|
40
|
+
const username = String(ctx.input.username ?? '');
|
|
41
|
+
const password = String(ctx.input.password ?? '');
|
|
42
|
+
if (password.length < 4) {
|
|
43
|
+
throw new Error('Password too short');
|
|
44
|
+
}
|
|
45
|
+
ctx.log('info', `Credentials accepted for ${username}`);
|
|
46
|
+
return { userId: `user-${username}`, username };
|
|
47
|
+
},
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
registry.register(
|
|
51
|
+
{
|
|
52
|
+
type: 'FetchUser',
|
|
53
|
+
label: 'Fetch User',
|
|
54
|
+
description: 'Fetches a user record derived from a userId.',
|
|
55
|
+
category: 'data',
|
|
56
|
+
traceKind: 'compute',
|
|
57
|
+
inputSchema: {
|
|
58
|
+
fields: [{ name: 'userId', type: 'string', required: true }],
|
|
59
|
+
},
|
|
60
|
+
outputSchema: {
|
|
61
|
+
fields: [
|
|
62
|
+
{ name: 'id', type: 'string' },
|
|
63
|
+
{ name: 'name', type: 'string' },
|
|
64
|
+
{ name: 'plan', type: 'enum', enumValues: ['free', 'pro'] },
|
|
65
|
+
{ name: 'isNew', type: 'boolean' },
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
async (ctx) => {
|
|
70
|
+
await sleep(delayMs);
|
|
71
|
+
const userId = String(ctx.input.userId ?? '');
|
|
72
|
+
const name = usernamePart(userId);
|
|
73
|
+
const plan = name.includes('pro') ? 'pro' : 'free';
|
|
74
|
+
const isNew = name.startsWith('new');
|
|
75
|
+
ctx.log('info', `Fetched user ${name}: plan=${plan}, isNew=${isNew}`);
|
|
76
|
+
return { id: userId, name, plan, isNew };
|
|
77
|
+
},
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
registry.register(
|
|
81
|
+
{
|
|
82
|
+
type: 'CheckPlan',
|
|
83
|
+
label: 'Check Plan',
|
|
84
|
+
description: 'Resolves the feature set for a user plan.',
|
|
85
|
+
category: 'logic',
|
|
86
|
+
traceKind: 'compute',
|
|
87
|
+
inputSchema: {
|
|
88
|
+
fields: [{ name: 'user', type: 'object', required: true }],
|
|
89
|
+
},
|
|
90
|
+
outputSchema: {
|
|
91
|
+
fields: [
|
|
92
|
+
{ name: 'plan', type: 'enum', enumValues: ['free', 'pro'] },
|
|
93
|
+
{ name: 'features', type: 'array' },
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
async (ctx) => {
|
|
98
|
+
await sleep(delayMs);
|
|
99
|
+
const user = (ctx.input.user ?? {}) as { plan?: string };
|
|
100
|
+
const plan = user.plan ?? 'free';
|
|
101
|
+
const features = plan === 'pro' ? ['sso', 'audit-log', 'priority-support'] : ['basic'];
|
|
102
|
+
ctx.log('info', `Plan ${plan} resolves to features: ${features.join(', ')}`);
|
|
103
|
+
return { plan, features };
|
|
104
|
+
},
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
registry.register(
|
|
108
|
+
{
|
|
109
|
+
type: 'WelcomeUser',
|
|
110
|
+
label: 'Welcome User',
|
|
111
|
+
description: 'Greets a freshly created user by name.',
|
|
112
|
+
category: 'auth',
|
|
113
|
+
traceKind: 'compute',
|
|
114
|
+
inputSchema: {
|
|
115
|
+
fields: [{ name: 'user', type: 'object', required: true }],
|
|
116
|
+
},
|
|
117
|
+
outputSchema: {
|
|
118
|
+
fields: [
|
|
119
|
+
{ name: 'message', type: 'string' },
|
|
120
|
+
{ name: 'userId', type: 'string' },
|
|
121
|
+
],
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
async (ctx) => {
|
|
125
|
+
await sleep(delayMs);
|
|
126
|
+
const user = (ctx.input.user ?? {}) as { id?: string; name?: string };
|
|
127
|
+
const message = `Welcome aboard, ${user.name}!`;
|
|
128
|
+
ctx.log('info', message);
|
|
129
|
+
return { message, userId: user.id };
|
|
130
|
+
},
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
registry.register(
|
|
134
|
+
{
|
|
135
|
+
type: 'IssueToken',
|
|
136
|
+
label: 'Issue Token',
|
|
137
|
+
description: 'Issues an access token for a userId.',
|
|
138
|
+
category: 'auth',
|
|
139
|
+
traceKind: 'compute',
|
|
140
|
+
inputSchema: {
|
|
141
|
+
fields: [
|
|
142
|
+
{ name: 'userId', type: 'string', required: true },
|
|
143
|
+
{ name: 'plan', type: 'enum', enumValues: ['free', 'pro'] },
|
|
144
|
+
],
|
|
145
|
+
},
|
|
146
|
+
outputSchema: {
|
|
147
|
+
fields: [
|
|
148
|
+
{ name: 'token', type: 'string' },
|
|
149
|
+
{ name: 'issuedTo', type: 'string' },
|
|
150
|
+
{ name: 'plan', type: 'string' },
|
|
151
|
+
],
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
async (ctx) => {
|
|
155
|
+
await sleep(delayMs);
|
|
156
|
+
const userId = String(ctx.input.userId ?? '');
|
|
157
|
+
const plan = String(ctx.input.plan ?? 'free');
|
|
158
|
+
ctx.log('info', `Token issued for plan=${plan}`);
|
|
159
|
+
return { token: `tok_${userId}`, issuedTo: userId, plan };
|
|
160
|
+
},
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
registry.register(
|
|
164
|
+
{
|
|
165
|
+
type: 'Result',
|
|
166
|
+
label: 'Result',
|
|
167
|
+
description: 'Terminal node that passes its input through and displays it on the canvas.',
|
|
168
|
+
category: 'output',
|
|
169
|
+
traceKind: 'compute',
|
|
170
|
+
tags: ['display'],
|
|
171
|
+
inputSchema: {
|
|
172
|
+
fields: [{ name: 'data', type: 'object' }],
|
|
173
|
+
},
|
|
174
|
+
outputSchema: {
|
|
175
|
+
fields: [{ name: 'data', type: 'object' }],
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
async (ctx) => {
|
|
179
|
+
await sleep(delayMs);
|
|
180
|
+
ctx.log('info', 'Result collected');
|
|
181
|
+
return ctx.input;
|
|
182
|
+
},
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
return registry;
|
|
186
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { NodeRegistry } from '../engine';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ForEach / Collect loop-region nodes. Iteration itself is owned by the
|
|
5
|
+
* executor (see FlowRun's loop machinery in ../engine/executor.ts) — these
|
|
6
|
+
* implementations exist so the node type is registered (canvas palette,
|
|
7
|
+
* schema-driven inspector fields, isolated single-node runs) and so an
|
|
8
|
+
* isolated run of just this node doesn't crash. In a real flow run the
|
|
9
|
+
* executor never calls these; it drives iteration itself and writes
|
|
10
|
+
* per-iteration / final output directly onto the node's run state.
|
|
11
|
+
*/
|
|
12
|
+
export function registerLoopNodes(registry: NodeRegistry): void {
|
|
13
|
+
registry.register(
|
|
14
|
+
{
|
|
15
|
+
type: 'ForEach',
|
|
16
|
+
label: 'For Each',
|
|
17
|
+
description:
|
|
18
|
+
'Iterates over an array, running the downstream loop body once per item (or per batch). Paired with exactly one Collect node.',
|
|
19
|
+
simpleDescription: 'Repeats the next steps for each item in a list',
|
|
20
|
+
category: 'flow',
|
|
21
|
+
traceKind: 'compute',
|
|
22
|
+
tags: ['branching', 'loop'],
|
|
23
|
+
inputSchema: {
|
|
24
|
+
fields: [{ name: 'items', type: 'array', required: true }],
|
|
25
|
+
},
|
|
26
|
+
configSchema: {
|
|
27
|
+
fields: [
|
|
28
|
+
{ name: 'batchSize', type: 'number', description: 'Items per iteration (default 1).' },
|
|
29
|
+
{ name: 'maxIterations', type: 'number', description: 'Optional cap; extra iterations are truncated with a warning.' },
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
outputSchema: {
|
|
33
|
+
fields: [
|
|
34
|
+
{ name: 'item', type: 'object', description: 'Single element (batchSize 1) or a chunk array otherwise.' },
|
|
35
|
+
{ name: 'index', type: 'number' },
|
|
36
|
+
{ name: 'total', type: 'number' },
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
// Passthrough stub only — the executor owns real iteration and never
|
|
41
|
+
// invokes this for a node of type ForEach inside a valid loop region.
|
|
42
|
+
async (ctx) => ctx.input,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
registry.register(
|
|
46
|
+
{
|
|
47
|
+
type: 'Collect',
|
|
48
|
+
label: 'Collect',
|
|
49
|
+
description:
|
|
50
|
+
'Gathers each loop iteration\'s mapped value into an array once its paired ForEach finishes iterating.',
|
|
51
|
+
simpleDescription: 'Collects the results from the repeated steps into a list',
|
|
52
|
+
category: 'flow',
|
|
53
|
+
traceKind: 'compute',
|
|
54
|
+
tags: ['loop'],
|
|
55
|
+
inputSchema: {
|
|
56
|
+
fields: [{ name: 'value', type: 'object', required: false }],
|
|
57
|
+
},
|
|
58
|
+
outputSchema: {
|
|
59
|
+
fields: [
|
|
60
|
+
{ name: 'items', type: 'array' },
|
|
61
|
+
{ name: 'count', type: 'number' },
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
// Stub — the executor overrides this with the accumulated results in a
|
|
66
|
+
// real run. Only exercised for an isolated run of this node in isolation.
|
|
67
|
+
async () => ({ items: [], count: 0 }),
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { NodeRegistry } from '../../engine';
|
|
2
|
+
import { registerPradarNodes } from './nodes';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* EV charge scheduler nodes for the EV charging demo. Pure decision logic lives
|
|
6
|
+
* in ./logic; ./nodes wraps each function as a registry node. The vehicle /
|
|
7
|
+
* push-notification effect nodes are simulated (see their PORT NOTEs).
|
|
8
|
+
*/
|
|
9
|
+
export function registerPradarNodesModule(registry: NodeRegistry): void {
|
|
10
|
+
registerPradarNodes(registry);
|
|
11
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
checkTarget,
|
|
4
|
+
computeSurplus,
|
|
5
|
+
computeTrendWindow,
|
|
6
|
+
decideCharge,
|
|
7
|
+
estimateSessionKwh,
|
|
8
|
+
evKwhNeeded,
|
|
9
|
+
getCooldownMs,
|
|
10
|
+
getNightChargeDecision,
|
|
11
|
+
getThresholds,
|
|
12
|
+
getTrend,
|
|
13
|
+
morningSummary,
|
|
14
|
+
nightChargeEstimate,
|
|
15
|
+
reconcile,
|
|
16
|
+
shouldStopUnscheduledCharge,
|
|
17
|
+
sunTimes,
|
|
18
|
+
type Reading,
|
|
19
|
+
} from './logic';
|
|
20
|
+
|
|
21
|
+
// Demo location (Berlin). Instants below fall in solar / golden / night windows.
|
|
22
|
+
const DAY = '2026-07-02T13:00:00Z'; // solar hours, ~7h to sunset
|
|
23
|
+
const GOLDEN = '2026-07-02T19:06:00Z'; // ~1h to sunset
|
|
24
|
+
const AFTER_SUNSET = '2026-07-02T22:00:00Z'; // dark, hour 22 (not night window)
|
|
25
|
+
const NIGHT = '2026-07-02T23:30:00Z'; // hour 23, night window
|
|
26
|
+
const DAY_HOUR = '2026-07-02T14:00:00Z'; // hour 14, day
|
|
27
|
+
|
|
28
|
+
describe('sunTimes', () => {
|
|
29
|
+
it('reports solar hours mid-afternoon', () => {
|
|
30
|
+
const c = sunTimes(DAY);
|
|
31
|
+
expect(c.isSolarHours).toBe(true);
|
|
32
|
+
expect(c.isNightWindow).toBe(false);
|
|
33
|
+
expect(c.hoursUntilSunset).toBeGreaterThan(6);
|
|
34
|
+
expect(c.hoursUntilSunset).toBeLessThan(8);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('flags the golden hour (≤2h to sunset) while still in solar hours', () => {
|
|
38
|
+
const c = sunTimes(GOLDEN);
|
|
39
|
+
expect(c.isSolarHours).toBe(true);
|
|
40
|
+
expect(c.hoursUntilSunset).toBeGreaterThan(0);
|
|
41
|
+
expect(c.hoursUntilSunset).toBeLessThanOrEqual(2);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('is outside solar hours after sunset without being in the night window at 22:00', () => {
|
|
45
|
+
const c = sunTimes(AFTER_SUNSET);
|
|
46
|
+
expect(c.isSolarHours).toBe(false);
|
|
47
|
+
expect(c.hour).toBe(22);
|
|
48
|
+
expect(c.isNightWindow).toBe(false);
|
|
49
|
+
expect(c.hoursUntilSunset).toBe(0);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('enters the night window at 23:30 and stays in it before 05:00', () => {
|
|
53
|
+
expect(sunTimes(NIGHT).isNightWindow).toBe(true);
|
|
54
|
+
expect(sunTimes('2026-07-02T04:30:00Z').isNightWindow).toBe(true);
|
|
55
|
+
expect(sunTimes(DAY_HOUR).isNightWindow).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('computeSurplus', () => {
|
|
60
|
+
it('uses grid export when exporting', () => {
|
|
61
|
+
expect(computeSurplus(3000, 500, 1200)).toEqual({ surplus: 1200, branch: 'export' });
|
|
62
|
+
});
|
|
63
|
+
it('falls back to ppv−load when not exporting', () => {
|
|
64
|
+
expect(computeSurplus(3000, 500, 0)).toEqual({ surplus: 2500, branch: 'ppv-load' });
|
|
65
|
+
});
|
|
66
|
+
it('clamps ppv−load at zero', () => {
|
|
67
|
+
expect(computeSurplus(200, 900, 0)).toEqual({ surplus: 0, branch: 'ppv-load' });
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('evKwhNeeded / estimateSessionKwh', () => {
|
|
72
|
+
it('kWh needed scales from the 77.4kWh pack', () => {
|
|
73
|
+
expect(evKwhNeeded(60)).toBeCloseTo((20 / 100) * 77.4, 5); // to 80%
|
|
74
|
+
expect(evKwhNeeded(90, 90)).toBe(0);
|
|
75
|
+
expect(evKwhNeeded(95, 90)).toBe(0); // never negative
|
|
76
|
+
});
|
|
77
|
+
it('forecast path caps remaining kWh by charge rate over hours left', () => {
|
|
78
|
+
expect(estimateSessionKwh(9999, 4, { remainingKwh: 5, strongHoursLeft: 3 })).toBe(5);
|
|
79
|
+
expect(estimateSessionKwh(9999, 1, { remainingKwh: 5, strongHoursLeft: 3 })).toBeCloseTo(2.3, 5);
|
|
80
|
+
});
|
|
81
|
+
it('naive path assumes current surplus sustains, capped at charge rate', () => {
|
|
82
|
+
expect(estimateSessionKwh(1000, 2, null)).toBeCloseTo(2, 5); // 1kW*2h
|
|
83
|
+
expect(estimateSessionKwh(5000, 2, null)).toBeCloseTo(4.6, 5); // capped at 2.3kW
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('getThresholds — bands (verbatim)', () => {
|
|
88
|
+
it('flat home battery (no buffer, EV not urgent) keeps the 2000/800 default', () => {
|
|
89
|
+
const t = getThresholds(10, 1500, 4, 70, null);
|
|
90
|
+
expect(t.startW).toBe(2000);
|
|
91
|
+
expect(t.stopW).toBe(800);
|
|
92
|
+
expect(t.skipReason).toBeNull();
|
|
93
|
+
expect(t.reasons[0]).toContain('flat');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('home buffer → 1200/400 (ported case)', () => {
|
|
97
|
+
const t = getThresholds(99, 300, 4, 60, null);
|
|
98
|
+
expect(t.startW).toBe(1200);
|
|
99
|
+
expect(t.stopW).toBe(400);
|
|
100
|
+
expect(t.reasons).toEqual(['home 99% has buffer — EV priority, home tops up EV shortfall']);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('EV urgent (<50%) → 1200/400 accepting grid', () => {
|
|
104
|
+
const t = getThresholds(10, 1500, 4, 40, null);
|
|
105
|
+
expect(t.startW).toBe(1200);
|
|
106
|
+
expect(t.stopW).toBe(400);
|
|
107
|
+
expect(t.reasons[0]).toContain('ev urgent');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('predictive: full battery + strong ppv → 500/200 (ported case)', () => {
|
|
111
|
+
const t = getThresholds(99, 2200, 4, 60, null);
|
|
112
|
+
expect(t.startW).toBe(500);
|
|
113
|
+
expect(t.stopW).toBe(200);
|
|
114
|
+
expect(t.reasons).toEqual([
|
|
115
|
+
'home 99% has buffer — EV priority, home tops up EV shortfall',
|
|
116
|
+
'predictive: home SOC 99% + ppv 2200W → export imminent',
|
|
117
|
+
]);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('golden hour (≤2h to sunset) pulls thresholds to ≤800/200', () => {
|
|
121
|
+
const t = getThresholds(10, 500, 1.5, 70, null);
|
|
122
|
+
expect(t.startW).toBe(800);
|
|
123
|
+
expect(t.stopW).toBe(200);
|
|
124
|
+
expect(t.reasons.some((r) => r.startsWith('golden hour'))).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('worth-it skip: tiny remaining solar with real need', () => {
|
|
128
|
+
const t = getThresholds(50, 100, 0.1, 60, { remainingKwh: 0.2, strongHoursLeft: 0 });
|
|
129
|
+
expect(t.skipReason).toContain('not worth it');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('worth-it skip: EV ≥65% with <1.5kWh remaining', () => {
|
|
133
|
+
const t = getThresholds(50, 100, 1, 70, { remainingKwh: 1.0, strongHoursLeft: 0 });
|
|
134
|
+
expect(t.skipReason).toContain('not worth a session');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('trend override: surplus rising fast → start ≤1000', () => {
|
|
138
|
+
const t = getThresholds(10, 1500, 4, 70, null, { surplusTrend: 300, socTrend: 0 });
|
|
139
|
+
expect(t.startW).toBe(1000);
|
|
140
|
+
expect(t.reasons.some((r) => r.startsWith('trend: surplus rising'))).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('trend override: home SOC climbing fast above 75% → start ≤600', () => {
|
|
144
|
+
const t = getThresholds(80, 1500, 4, 70, null, { surplusTrend: 0, socTrend: 3 });
|
|
145
|
+
expect(t.startW).toBe(600);
|
|
146
|
+
expect(t.reasons.some((r) => r.startsWith('trend: home SOC rising'))).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('shouldStopUnscheduledCharge (verbatim, ported cases)', () => {
|
|
151
|
+
const base = { charging: true, state: 'idle' as const, surplus: 700, startW: 1200, skipReason: null, homeSoc: 80, effectiveMinSoc: 0 };
|
|
152
|
+
it('stops an unscheduled charge when start conditions are not met', () => {
|
|
153
|
+
expect(shouldStopUnscheduledCharge(base)).toBe(true);
|
|
154
|
+
});
|
|
155
|
+
it('never touches a scheduler-owned charging session (hysteresis)', () => {
|
|
156
|
+
expect(shouldStopUnscheduledCharge({ ...base, state: 'charging' })).toBe(false);
|
|
157
|
+
});
|
|
158
|
+
it('leaves it alone when not charging', () => {
|
|
159
|
+
expect(shouldStopUnscheduledCharge({ ...base, charging: false })).toBe(false);
|
|
160
|
+
});
|
|
161
|
+
it('stops on a skip reason even when surplus clears the start threshold', () => {
|
|
162
|
+
expect(shouldStopUnscheduledCharge({ ...base, surplus: 5000, skipReason: 'not worth it' })).toBe(true);
|
|
163
|
+
});
|
|
164
|
+
it('stops when home SOC is below the effective min', () => {
|
|
165
|
+
expect(shouldStopUnscheduledCharge({ ...base, surplus: 5000, homeSoc: 5, effectiveMinSoc: 10 })).toBe(true);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('getNightChargeDecision (verbatim, ported cases)', () => {
|
|
170
|
+
it('does not charge a moderate battery with a useful forecast', () => {
|
|
171
|
+
expect(getNightChargeDecision(50, 24)).toEqual({ shouldCharge: false, factors: [] });
|
|
172
|
+
expect(getNightChargeDecision(40, 24)).toEqual({ shouldCharge: false, factors: [] });
|
|
173
|
+
});
|
|
174
|
+
it('charges below 35% regardless of forecast', () => {
|
|
175
|
+
expect(getNightChargeDecision(34, 24)).toEqual({ shouldCharge: true, factors: ['EV critically low at 34% (< 35%)'] });
|
|
176
|
+
});
|
|
177
|
+
it('charges a moderate battery only with a poor forecast', () => {
|
|
178
|
+
expect(getNightChargeDecision(50, 8)).toEqual({ shouldCharge: true, factors: ['EV at 50% with poor forecast tomorrow (8.0kWh)'] });
|
|
179
|
+
});
|
|
180
|
+
it('treats a null forecast as not-poor (moderate battery skips)', () => {
|
|
181
|
+
expect(getNightChargeDecision(50, null).shouldCharge).toBe(false);
|
|
182
|
+
});
|
|
183
|
+
it('sizes the session at the 2.3kW home rate', () => {
|
|
184
|
+
const est = nightChargeEstimate(30);
|
|
185
|
+
expect(est.neededKwh).toBeCloseTo((50 / 100) * 77.4, 5);
|
|
186
|
+
expect(est.hoursToCharge).toBeCloseTo(est.neededKwh / 2.3, 5);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe('getCooldownMs / getTrend (verbatim stddev bands)', () => {
|
|
191
|
+
const mk = (surpluses: number[]): Reading[] => surpluses.map((s) => ({ surplus: s, homeSoc: 50, ppv: 1000 }));
|
|
192
|
+
it('defaults to 10min with fewer than 3 readings', () => {
|
|
193
|
+
expect(getCooldownMs(mk([100, 200]))).toBe(10 * 60_000);
|
|
194
|
+
});
|
|
195
|
+
it('stable (stdDev < 200) → 5min', () => {
|
|
196
|
+
expect(getCooldownMs(mk([1000, 1010, 990]))).toBe(5 * 60_000);
|
|
197
|
+
});
|
|
198
|
+
it('volatile (stdDev > 800) → 15min', () => {
|
|
199
|
+
expect(getCooldownMs(mk([0, 2500, 0, 2500]))).toBe(15 * 60_000);
|
|
200
|
+
});
|
|
201
|
+
it('mid-volatility → default 10min', () => {
|
|
202
|
+
expect(getCooldownMs(mk([1000, 1500, 700]))).toBe(10 * 60_000);
|
|
203
|
+
});
|
|
204
|
+
it('getTrend is the per-cycle slope from first to last', () => {
|
|
205
|
+
const r = mk([100, 400, 700]); // +300/cycle over 2 cycles
|
|
206
|
+
expect(getTrend(r, 'surplus')).toBe(300);
|
|
207
|
+
expect(getTrend([], 'surplus')).toBe(0);
|
|
208
|
+
});
|
|
209
|
+
it('computeTrendWindow bundles slopes + cooldown', () => {
|
|
210
|
+
const tw = computeTrendWindow([
|
|
211
|
+
{ surplus: 100, homeSoc: 70, ppv: 800 },
|
|
212
|
+
{ surplus: 400, homeSoc: 72, ppv: 1100 },
|
|
213
|
+
{ surplus: 700, homeSoc: 74, ppv: 1400 },
|
|
214
|
+
]);
|
|
215
|
+
expect(tw.surplusTrend).toBe(300);
|
|
216
|
+
expect(tw.socTrend).toBe(2);
|
|
217
|
+
expect(tw.ppvTrend).toBe(300);
|
|
218
|
+
expect(tw.cooldownMs).toBe(10 * 60_000); // stdDev of [100,400,700] ≈ 245 → default band
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe('decideCharge (verbatim evaluateCharge branch)', () => {
|
|
223
|
+
it('starts from idle when surplus clears startW', () => {
|
|
224
|
+
const d = decideCharge({ state: 'idle', surplus: 2500, startW: 1200, stopW: 400, homeSoc: 80, skipReason: null });
|
|
225
|
+
expect(d.action).toBe('start');
|
|
226
|
+
expect(d.nextState).toBe('cooldown');
|
|
227
|
+
});
|
|
228
|
+
it('holds in waiting when surplus is below startW', () => {
|
|
229
|
+
const d = decideCharge({ state: 'waiting', surplus: 900, startW: 1200, stopW: 400, homeSoc: 80, skipReason: null });
|
|
230
|
+
expect(d.action).toBe('hold_waiting');
|
|
231
|
+
});
|
|
232
|
+
it('a skip reason forces hold_waiting even above startW', () => {
|
|
233
|
+
const d = decideCharge({ state: 'idle', surplus: 5000, startW: 1200, stopW: 400, homeSoc: 80, skipReason: 'not worth it' });
|
|
234
|
+
expect(d.action).toBe('hold_waiting');
|
|
235
|
+
expect(d.reason).toBe('not worth it');
|
|
236
|
+
});
|
|
237
|
+
it('continues charging while surplus stays above stopW', () => {
|
|
238
|
+
const d = decideCharge({ state: 'charging', surplus: 900, startW: 1200, stopW: 400, homeSoc: 80, skipReason: null });
|
|
239
|
+
expect(d.action).toBe('hold_charging');
|
|
240
|
+
});
|
|
241
|
+
it('stops charging once surplus drops below stopW', () => {
|
|
242
|
+
const d = decideCharge({ state: 'charging', surplus: 300, startW: 1200, stopW: 400, homeSoc: 80, skipReason: null });
|
|
243
|
+
expect(d.action).toBe('stop');
|
|
244
|
+
expect(d.nextState).toBe('cooldown');
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe('checkTarget (verbatim)', () => {
|
|
249
|
+
it('90% effective target during solar hours, 80% otherwise', () => {
|
|
250
|
+
expect(checkTarget(true, 85, true).branch).toBe('belowTarget');
|
|
251
|
+
expect(checkTarget(true, 85, false).branch).toBe('atTarget');
|
|
252
|
+
expect(checkTarget(true, 91, true).branch).toBe('atTarget');
|
|
253
|
+
});
|
|
254
|
+
it('not plugged in short-circuits', () => {
|
|
255
|
+
expect(checkTarget(false, 20, true).branch).toBe('notPlugged');
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
describe('reconcile (verbatim)', () => {
|
|
260
|
+
it('adopts an external charge when idle/waiting', () => {
|
|
261
|
+
expect(reconcile(true, 'waiting').decision).toBe('adopt');
|
|
262
|
+
expect(reconcile(true, 'idle').decision).toBe('adopt');
|
|
263
|
+
});
|
|
264
|
+
it('holds when already charging or not charging', () => {
|
|
265
|
+
expect(reconcile(true, 'charging').decision).toBe('hold');
|
|
266
|
+
expect(reconcile(false, 'idle').decision).toBe('hold');
|
|
267
|
+
expect(reconcile(true, 'cooldown').decision).toBe('hold');
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
describe('morningSummary (verbatim outlook)', () => {
|
|
272
|
+
it('Good when remaining > need×1.5', () => {
|
|
273
|
+
// EV 60% → need to 90% = 23.22kWh; remaining 40 > 34.8
|
|
274
|
+
expect(morningSummary(80, 60, true, 40, 40, 5).outlook).toBe('good');
|
|
275
|
+
});
|
|
276
|
+
it('Tight when remaining > need but ≤ need×1.5', () => {
|
|
277
|
+
expect(morningSummary(80, 60, true, 25, 25, 3).outlook).toBe('tight');
|
|
278
|
+
});
|
|
279
|
+
it('Unlikely when remaining ≤ need', () => {
|
|
280
|
+
expect(morningSummary(80, 60, true, 10, 10, 1).outlook).toBe('unlikely');
|
|
281
|
+
});
|
|
282
|
+
it('at-target when EV ≥ 90%', () => {
|
|
283
|
+
expect(morningSummary(80, 92, true, 40, 40, 5).outlook).toBe('at-target');
|
|
284
|
+
});
|
|
285
|
+
it('not-applicable when unplugged', () => {
|
|
286
|
+
expect(morningSummary(80, 60, false, 40, 40, 5).outlook).toBe('not-applicable');
|
|
287
|
+
});
|
|
288
|
+
});
|