machinaos 0.0.76 → 0.0.78
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/README.md +143 -107
- package/client/dist/assets/ActionBar-Du2MSFSz.js +1 -0
- package/client/dist/assets/ApiKeyInput-k2LBmBjb.js +1 -0
- package/client/dist/assets/ApiKeyPanel-C_bV9U0X.js +1 -0
- package/client/dist/assets/ApiUsageSection-CmVfwZzL.js +1 -0
- package/client/dist/assets/EmailPanel-CeKIMGu-.js +1 -0
- package/client/dist/assets/OAuthPanel-KA3t3Q2K.js +1 -0
- package/client/dist/assets/QrPairingPanel-NgNpJNuk.js +1 -0
- package/client/dist/assets/RateLimitSection-Du5YNVIA.js +1 -0
- package/client/dist/assets/StatusCard-DNLyayXc.js +1 -0
- package/client/dist/assets/index-DQ0nwhec.js +257 -0
- package/client/dist/assets/index-DxmbVskS.css +1 -0
- package/client/dist/assets/vendor-flow-CZmBvHRo.js +1 -0
- package/client/dist/assets/vendor-icons-CVrPjN2Q.js +22 -0
- package/client/dist/assets/vendor-markdown-CRou3yQ5.js +62 -0
- package/client/dist/assets/vendor-misc-C4VxKHs5.js +1 -0
- package/client/dist/assets/vendor-query-SzWcOU0G.js +1 -0
- package/client/dist/assets/vendor-radix-Dnos29jG.js +56 -0
- package/client/dist/assets/vendor-react-DvWIbVx0.js +1 -0
- package/client/dist/index.html +37 -3
- package/client/index.html +28 -1
- package/client/package.json +44 -40
- package/client/src/App.tsx +2 -0
- package/client/src/Dashboard.tsx +157 -45
- package/client/src/ParameterPanel.tsx +3 -5
- package/client/src/adapters/nodeSpecToDescription.ts +1 -0
- package/client/src/assets/icons/NodeIcon.tsx +32 -0
- package/client/src/assets/icons/index.ts +4 -0
- package/client/src/assets/icons/stripe.svg +1 -0
- package/client/src/assets/icons/themedGlyphs.ts +404 -0
- package/client/src/components/AIAgentNode.tsx +77 -53
- package/client/src/components/GenericNode.tsx +34 -52
- package/client/src/components/OutputPanel.tsx +64 -147
- package/client/src/components/ParameterRenderer.tsx +5 -3
- package/client/src/components/SkillEditorModal.tsx +9 -18
- package/client/src/components/SquareNode.tsx +97 -115
- package/client/src/components/StartNode.tsx +32 -42
- package/client/src/components/SvgFilterDefs.tsx +54 -0
- package/client/src/components/TeamMonitorNode.tsx +12 -14
- package/client/src/components/ToolkitNode.tsx +35 -60
- package/client/src/components/TriggerNode.tsx +43 -77
- package/client/src/components/__tests__/CredentialsModal.test.tsx +49 -45
- package/client/src/components/credentials/CredentialsModal.tsx +98 -30
- package/client/src/components/credentials/CredentialsPalette.tsx +73 -5
- package/client/src/components/credentials/catalogueAdapter.ts +17 -1
- package/client/src/components/credentials/panels/ApiKeyPanel.tsx +102 -37
- package/client/src/components/credentials/panels/EmailPanel.tsx +7 -19
- package/client/src/components/credentials/panels/OAuthPanel.tsx +5 -1
- package/client/src/components/credentials/panels/QrPairingPanel.tsx +1 -3
- package/client/src/components/credentials/primitives/ActionBar.tsx +7 -11
- package/client/src/components/credentials/primitives/OAuthConnect.tsx +19 -28
- package/client/src/components/credentials/sections/ProviderDefaultsSection.tsx +24 -3
- package/client/src/components/credentials/types.ts +12 -2
- package/client/src/components/credentials/useCredentialPanel.ts +43 -19
- package/client/src/components/icons/AIProviderIcons.tsx +16 -0
- package/client/src/components/onboarding/OnboardingWizard.tsx +23 -63
- package/client/src/components/onboarding/nodeRoleClasses.ts +23 -0
- package/client/src/components/onboarding/steps/CanvasStep.tsx +15 -21
- package/client/src/components/onboarding/steps/ConceptsStep.tsx +2 -11
- package/client/src/components/onboarding/steps/GetStartedStep.tsx +2 -10
- package/client/src/components/parameterPanel/InputSection.tsx +9 -7
- package/client/src/components/parameterPanel/MasterSkillEditor.tsx +84 -198
- package/client/src/components/parameterPanel/MiddleSection.tsx +57 -80
- package/client/src/components/parameterPanel/ToolSchemaEditor.tsx +31 -25
- package/client/src/components/parameterPanel/__tests__/InputSection.test.tsx +7 -2
- package/client/src/components/ui/AIResultModal.tsx +1 -1
- package/client/src/components/ui/CollapsibleSection.tsx +9 -5
- package/client/src/components/ui/CommandPalette.tsx +147 -0
- package/client/src/components/ui/CommandPaletteHost.tsx +189 -0
- package/client/src/components/ui/ComponentItem.tsx +13 -7
- package/client/src/components/ui/ComponentPalette.tsx +24 -13
- package/client/src/components/ui/ConsolePanel.tsx +19 -11
- package/client/src/components/ui/DropCap.tsx +28 -0
- package/client/src/components/ui/EditableNodeLabel.tsx +10 -2
- package/client/src/components/ui/InputNodesPanel.tsx +1 -1
- package/client/src/components/ui/Modal.tsx +38 -6
- package/client/src/components/ui/OutputDisplayPanel.tsx +1 -1
- package/client/src/components/ui/SettingsPanel.tsx +42 -13
- package/client/src/components/ui/StatusBar.tsx +108 -0
- package/client/src/components/ui/ThemeSwitcher.tsx +109 -0
- package/client/src/components/ui/TopToolbar.tsx +42 -25
- package/client/src/components/ui/WorkflowSidebar.tsx +32 -16
- package/client/src/components/ui/action-button.tsx +40 -15
- package/client/src/components/ui/button.tsx +24 -1
- package/client/src/components/ui/dropdown-menu.tsx +24 -2
- package/client/src/components/ui/input.tsx +19 -2
- package/client/src/components/ui/select.tsx +15 -0
- package/client/src/components/ui/textarea.tsx +15 -2
- package/client/src/contexts/AuthContext.tsx +148 -109
- package/client/src/contexts/ThemeContext.tsx +93 -17
- package/client/src/contexts/WebSocketContext.tsx +373 -206
- package/client/src/contexts/__tests__/AuthContext.test.tsx +221 -0
- package/client/src/hooks/__tests__/useDragVariable.test.ts +7 -1
- package/client/src/hooks/__tests__/useWorkflowOpsListener.test.ts +142 -0
- package/client/src/hooks/useAppTheme.ts +209 -7
- package/client/src/hooks/useAutoSkillEdges.ts +7 -2
- package/client/src/hooks/useCatalogueQuery.ts +67 -1
- package/client/src/hooks/useDragVariable.ts +1 -1
- package/client/src/hooks/useNodeAllowlist.ts +115 -8
- package/client/src/hooks/useOnboarding.ts +20 -8
- package/client/src/hooks/useParameterPanel.ts +2 -1
- package/client/src/hooks/useReactFlowNodes.ts +2 -1
- package/client/src/hooks/useSound.ts +185 -0
- package/client/src/hooks/useWorkflowManagement.ts +6 -8
- package/client/src/hooks/useWorkflowOpsListener.ts +90 -0
- package/client/src/index.css +65 -3
- package/client/src/lib/__tests__/connectionConfig.test.ts +91 -0
- package/client/src/lib/aiModelProviders.ts +8 -0
- package/client/src/lib/connectionConfig.ts +107 -0
- package/client/src/lib/queryPersist.ts +13 -5
- package/client/src/lib/sound.ts +393 -0
- package/client/src/main.tsx +20 -0
- package/client/src/store/useAppStore.ts +26 -0
- package/client/src/styles/canvasAnimations.ts +37 -36
- package/client/src/styles/theme.ts +36 -20
- package/client/src/test/setup.ts +1 -0
- package/client/src/themes/atomic.css +253 -0
- package/client/src/themes/base.css +373 -0
- package/client/src/themes/cyber.css +890 -0
- package/client/src/themes/dark.css +70 -0
- package/client/src/themes/edo.css +246 -0
- package/client/src/themes/greek.css +293 -0
- package/client/src/themes/light.css +78 -0
- package/client/src/themes/plague.css +253 -0
- package/client/src/themes/renaissance.css +727 -0
- package/client/src/themes/rot.css +249 -0
- package/client/src/themes/steampunk.css +272 -0
- package/client/src/themes/surveillance.css +289 -0
- package/client/src/themes/wasteland.css +250 -0
- package/client/src/types/INodeProperties.ts +5 -0
- package/client/src/types/NodeTypes.ts +11 -1
- package/client/src/types/__tests__/cloudEvents.test.ts +99 -0
- package/client/src/types/cloudEvents.ts +78 -0
- package/client/src/vite-env.d.ts +7 -0
- package/client/tsconfig.json +1 -1
- package/client/vite.config.js +62 -2
- package/install.ps1 +1 -1
- package/install.sh +1 -1
- package/machina/commands/build.py +51 -7
- package/machina/pyproject.toml +4 -0
- package/machina/supervisor.py +12 -2
- package/machina/tree.py +71 -21
- package/package.json +4 -4
- package/scripts/install.js +16 -1
- package/server/config/ai_cli_providers.json +54 -0
- package/server/config/credential_providers.json +109 -2
- package/server/config/llm_defaults.json +24 -0
- package/server/config/model_registry.json +338 -499
- package/server/config/node_allowlist.json +16 -1
- package/server/config/pricing.json +8 -0
- package/server/constants.py +38 -15
- package/server/core/container.py +2 -2
- package/server/core/credentials_database.py +35 -2
- package/server/core/logging.py +4 -3
- package/server/main.py +99 -13
- package/server/models/node_metadata.py +1 -0
- package/server/nodejs/package.json +8 -6
- package/server/nodejs/src/index.ts +22 -5
- package/server/nodes/README.md +31 -4
- package/server/nodes/agent/_inline.py +2 -0
- package/server/nodes/agent/_specialized.py +6 -3
- package/server/nodes/agent/ai_agent.py +13 -3
- package/server/nodes/agent/chat_agent.py +6 -3
- package/server/nodes/agent/claude_code_agent.py +287 -75
- package/server/nodes/agent/codex_agent.py +239 -0
- package/server/nodes/agent/deep_agent.py +3 -3
- package/server/nodes/agent/rlm_agent.py +3 -3
- package/server/nodes/android/__init__.py +31 -1
- package/server/nodes/android/_base.py +9 -5
- package/server/{services/android_service.py → nodes/android/_dispatcher.py} +2 -2
- package/server/nodes/android/_handlers.py +154 -0
- package/server/nodes/android/_option_loaders.py +44 -0
- package/server/nodes/android/_refresh.py +127 -0
- package/server/{services/android → nodes/android/_relay}/client.py +4 -4
- package/server/{routers/android.py → nodes/android/_router.py} +27 -8
- package/server/nodes/browser/browser.py +2 -2
- package/server/nodes/code/_base.py +6 -2
- package/server/nodes/code/_claude_code.py +134 -0
- package/server/nodes/document/embedding_generator.py +3 -3
- package/server/nodes/document/http_scraper.py +3 -3
- package/server/nodes/document/vector_store.py +5 -5
- package/server/nodes/email/__init__.py +11 -1
- package/server/nodes/email/_filters.py +21 -0
- package/server/{services/himalaya_service.py → nodes/email/_himalaya.py} +6 -10
- package/server/{services/email_service.py → nodes/email/_service.py} +9 -13
- package/server/nodes/email/email_read.py +1 -1
- package/server/nodes/email/email_receive.py +54 -5
- package/server/nodes/email/email_send.py +1 -1
- package/server/nodes/filesystem/shell.py +24 -1
- package/server/nodes/google/__init__.py +55 -1
- package/server/{services/handlers/google_auth.py → nodes/google/_auth_helper.py} +8 -5
- package/server/nodes/google/_base.py +2 -2
- package/server/nodes/google/_credentials.py +5 -5
- package/server/nodes/google/_filters.py +25 -0
- package/server/nodes/google/_handlers.py +57 -0
- package/server/{services/google_oauth.py → nodes/google/_oauth.py} +195 -162
- package/server/nodes/google/_option_loaders.py +107 -0
- package/server/nodes/google/_refresh.py +66 -0
- package/server/nodes/google/_router.py +131 -0
- package/server/nodes/google/gmail_receive.py +41 -4
- package/server/nodes/groups.py +1 -0
- package/server/nodes/location/_credentials.py +45 -1
- package/server/{services/maps.py → nodes/location/_service.py} +18 -3
- package/server/nodes/location/gmaps_create.py +4 -4
- package/server/nodes/location/gmaps_locations.py +4 -4
- package/server/nodes/location/gmaps_nearby_places.py +4 -4
- package/server/nodes/model/_base.py +8 -3
- package/server/nodes/model/_credentials.py +96 -8
- package/server/nodes/model/_local_validator.py +345 -0
- package/server/nodes/model/lmstudio_chat_model.py +23 -0
- package/server/nodes/model/ollama_chat_model.py +25 -0
- package/server/nodes/proxy/_usage.py +2 -2
- package/server/nodes/proxy/proxy_config.py +14 -14
- package/server/nodes/proxy/proxy_request.py +4 -4
- package/server/nodes/scraper/_credentials.py +29 -1
- package/server/nodes/scraper/apify_actor.py +9 -9
- package/server/nodes/scraper/crawlee_scraper.py +5 -5
- package/server/nodes/search/brave_search.py +4 -0
- package/server/nodes/search/perplexity_search.py +9 -0
- package/server/nodes/search/serper_search.py +3 -0
- package/server/nodes/skill/simple_memory.py +12 -0
- package/server/nodes/social/_base.py +2 -2
- package/server/nodes/stripe/__init__.py +46 -0
- package/server/nodes/stripe/_credentials.py +33 -0
- package/server/nodes/stripe/_handlers.py +270 -0
- package/server/nodes/stripe/_install.py +127 -0
- package/server/nodes/stripe/_source.py +174 -0
- package/server/nodes/stripe/stripe_action.py +81 -0
- package/server/nodes/stripe/stripe_receive.py +92 -0
- package/server/nodes/telegram/_credentials.py +52 -1
- package/server/nodes/telegram/_handlers.py +19 -18
- package/server/nodes/telegram/_service.py +134 -32
- package/server/nodes/telegram/telegram_send.py +5 -6
- package/server/nodes/text/file_handler.py +2 -2
- package/server/nodes/text/text_generator.py +2 -2
- package/server/nodes/tool/agent_builder.py +630 -0
- package/server/nodes/tool/task_manager.py +144 -2
- package/server/nodes/twitter/__init__.py +38 -1
- package/server/nodes/twitter/_base.py +7 -7
- package/server/nodes/twitter/_credentials.py +1 -1
- package/server/nodes/twitter/_filters.py +37 -0
- package/server/nodes/twitter/_handlers.py +77 -0
- package/server/nodes/twitter/_oauth.py +124 -0
- package/server/nodes/twitter/_refresh.py +78 -0
- package/server/nodes/twitter/_router.py +29 -0
- package/server/nodes/twitter/twitter_receive.py +4 -0
- package/server/nodes/visuals.json +64 -19
- package/server/nodes/whatsapp/__init__.py +45 -5
- package/server/nodes/whatsapp/_base.py +3 -3
- package/server/nodes/whatsapp/_filters.py +137 -0
- package/server/nodes/whatsapp/_handlers.py +167 -0
- package/server/nodes/whatsapp/_option_loaders.py +68 -0
- package/server/nodes/whatsapp/_refresh.py +62 -0
- package/server/nodes/whatsapp/_runtime.py +1 -1
- package/server/pyproject.toml +29 -7
- package/server/routers/schemas.py +2 -2
- package/server/routers/webhook.py +26 -9
- package/server/routers/websocket.py +149 -810
- package/server/services/ai.py +89 -8
- package/server/services/auth.py +220 -43
- package/server/services/claude_oauth.py +126 -100
- package/server/services/cli_agent/__init__.py +78 -0
- package/server/services/cli_agent/_handlers.py +237 -0
- package/server/services/cli_agent/config.py +112 -0
- package/server/services/cli_agent/factory.py +48 -0
- package/server/services/cli_agent/lockfile.py +141 -0
- package/server/services/cli_agent/mcp_server.py +482 -0
- package/server/services/cli_agent/protocol.py +173 -0
- package/server/services/cli_agent/providers/__init__.py +9 -0
- package/server/services/cli_agent/providers/anthropic_claude.py +419 -0
- package/server/services/cli_agent/providers/google_gemini.py +80 -0
- package/server/services/cli_agent/providers/openai_codex.py +310 -0
- package/server/services/cli_agent/service.py +607 -0
- package/server/services/cli_agent/session.py +618 -0
- package/server/services/cli_agent/types.py +227 -0
- package/server/services/cli_agent/workflow_tools.py +233 -0
- package/server/services/credential_registry.py +26 -1
- package/server/services/deployment/manager.py +26 -145
- package/server/services/deployment/poll_registry.py +59 -0
- package/server/services/event_waiter.py +76 -246
- package/server/services/events/__init__.py +54 -0
- package/server/services/events/cli.py +78 -0
- package/server/services/events/daemon.py +163 -0
- package/server/services/events/envelope.py +281 -0
- package/server/services/events/lifecycle.py +99 -0
- package/server/services/events/oauth_lifecycle.py +534 -0
- package/server/services/events/polling.py +60 -0
- package/server/services/events/push.py +36 -0
- package/server/services/events/source.py +63 -0
- package/server/services/events/triggers.py +118 -0
- package/server/services/events/verifiers/__init__.py +25 -0
- package/server/services/events/verifiers/base.py +28 -0
- package/server/services/events/verifiers/github.py +25 -0
- package/server/services/events/verifiers/hmac_basic.py +32 -0
- package/server/services/events/verifiers/standard_webhooks.py +47 -0
- package/server/services/events/verifiers/stripe.py +42 -0
- package/server/services/events/webhook.py +105 -0
- package/server/services/handlers/tools.py +28 -186
- package/server/services/llm/config.py +7 -0
- package/server/services/llm/factory.py +8 -2
- package/server/services/memory/__init__.py +52 -0
- package/server/services/memory/jsonl.py +80 -0
- package/server/services/memory/markdown.py +65 -0
- package/server/services/memory/state.py +112 -0
- package/server/services/memory/vector_store.py +40 -0
- package/server/services/model_registry.py +76 -0
- package/server/services/node_allowlist.py +71 -15
- package/server/services/node_executor.py +2 -2
- package/server/services/node_output_schemas.py +21 -10
- package/server/services/node_spec.py +1 -1
- package/server/services/oauth_utils.py +1 -1
- package/server/services/plugin/__init__.py +2 -0
- package/server/services/plugin/base.py +44 -2
- package/server/services/plugin/credential.py +288 -1
- package/server/services/plugin/deps.py +105 -0
- package/server/services/plugin/edge_walker.py +12 -4
- package/server/services/plugin/oauth.py +381 -0
- package/server/services/plugin/polling.py +247 -0
- package/server/services/plugin/registry.py +145 -0
- package/server/services/plugin/singleton.py +65 -0
- package/server/services/plugin/ws.py +81 -0
- package/server/services/process_service.py +31 -2
- package/server/services/status_broadcaster.py +155 -238
- package/server/services/temporal/workflow.py +7 -7
- package/server/services/workflow.py +21 -3
- package/server/services/ws_handler_registry.py +111 -28
- package/server/skills/GUIDE.md +16 -1
- package/server/skills/assistant/agent-builder-skill/SKILL.md +166 -0
- package/server/skills/payments_agent/stripe-skill/SKILL.md +306 -0
- package/server/tests/credentials/test_auth_service.py +16 -9
- package/server/tests/credentials/test_credential_broadcasts.py +219 -0
- package/server/tests/credentials/test_google_oauth.py +6 -6
- package/server/tests/credentials/test_oauth_utils.py +1 -1
- package/server/tests/credentials/test_twitter_oauth.py +2 -2
- package/server/tests/credentials/test_websocket_handlers.py +44 -20
- package/server/tests/llm/test_factory.py +1 -0
- package/server/tests/llm/test_wiring.py +5 -1
- package/server/tests/nodes/_compat.py +24 -24
- package/server/tests/nodes/test_agent_builder.py +439 -0
- package/server/tests/nodes/test_ai_tools.py +18 -14
- package/server/tests/nodes/test_code_fs_process.py +17 -8
- package/server/tests/nodes/test_email.py +10 -9
- package/server/tests/nodes/test_google_workspace.py +2 -2
- package/server/tests/nodes/test_specialized_agents.py +100 -53
- package/server/tests/nodes/test_stripe_plugin.py +293 -0
- package/server/tests/nodes/test_telegram_social.py +4 -4
- package/server/tests/nodes/test_twitter.py +1 -1
- package/server/tests/nodes/test_web_automation.py +2 -2
- package/server/tests/nodes/test_whatsapp.py +9 -9
- package/server/tests/services/cli_agent/__init__.py +0 -0
- package/server/tests/services/cli_agent/test_mcp_server.py +432 -0
- package/server/tests/services/cli_agent/test_providers.py +358 -0
- package/server/tests/services/cli_agent/test_service.py +298 -0
- package/server/tests/services/memory/__init__.py +0 -0
- package/server/tests/services/memory/test_jsonl.py +188 -0
- package/server/tests/services/test_events.py +333 -0
- package/server/tests/test_node_spec.py +56 -16
- package/server/tests/test_plugin_helpers.py +116 -0
- package/server/tests/test_plugin_self_containment.py +486 -0
- package/server/tests/test_status_broadcasts.py +425 -0
- package/workflows/{AI Assistant_workflow-1777421105154-0m4snkzjf.json → AI Assistant_workflow-1778504793388-ou1m1tz2x.json } +70 -266
- package/workflows/{AI Employee_workflow-1777720598005-u4cm858dv.json → AI Employee_example_workflow-1777720598005-u4cm858dv.json } +112 -112
- package/workflows/Claude Assistant_workflow-1778380124051-mdibn807c.json +709 -0
- package/client/dist/assets/ActionBar-vzPpSR77.js +0 -1
- package/client/dist/assets/ApiKeyInput-Ds7AKFe8.js +0 -1
- package/client/dist/assets/ApiKeyPanel-gfblELep.js +0 -1
- package/client/dist/assets/ApiUsageSection-BMNWTe2r.js +0 -1
- package/client/dist/assets/EmailPanel-B1Om64p5.js +0 -1
- package/client/dist/assets/OAuthPanel-CXyQYGBz.js +0 -1
- package/client/dist/assets/QrPairingPanel-BgNuI1we.js +0 -1
- package/client/dist/assets/RateLimitSection-YYK8sx1T.js +0 -1
- package/client/dist/assets/StatusCard-DuYA5hJR.js +0 -1
- package/client/dist/assets/index-D9tZfgvi.js +0 -363
- package/client/dist/assets/index-al7snTkG.css +0 -1
- package/client/src/components/credentials/providers.tsx +0 -177
- package/server/routers/google.py +0 -277
- package/server/routers/maps.py +0 -142
- package/server/routers/twitter.py +0 -365
- package/server/services/claude_code_service.py +0 -106
- package/server/services/memory.py +0 -159
- package/server/services/node_option_loaders/__init__.py +0 -77
- package/server/services/node_option_loaders/android_loaders.py +0 -55
- package/server/services/node_option_loaders/google_loaders.py +0 -97
- package/server/services/node_option_loaders/whatsapp_loaders.py +0 -69
- package/server/services/twitter_oauth.py +0 -411
- package/server/services/websocket_client.py +0 -29
- /package/server/{services/android → nodes/android/_relay}/__init__.py +0 -0
- /package/server/{services/android → nodes/android/_relay}/broadcaster.py +0 -0
- /package/server/{services/android → nodes/android/_relay}/manager.py +0 -0
- /package/server/{services/android → nodes/android/_relay}/protocol.py +0 -0
- /package/server/{services/browser_service.py → nodes/browser/_service.py} +0 -0
- /package/server/{services/whatsapp_service.py → nodes/whatsapp/_service.py} +0 -0
- /package/server/skills/{task_agent → assistant}/write-todos-skill/SKILL.md +0 -0
|
@@ -27,7 +27,8 @@ pytestmark = pytest.mark.node_contract
|
|
|
27
27
|
|
|
28
28
|
def _reset_singletons():
|
|
29
29
|
"""Wipe cached singletons so each test gets a clean EmailService / HimalayaService."""
|
|
30
|
-
from
|
|
30
|
+
from nodes.email import _service as email_service
|
|
31
|
+
from nodes.email import _himalaya as himalaya_service
|
|
31
32
|
|
|
32
33
|
email_service.EmailService._instance = None
|
|
33
34
|
himalaya_service.HimalayaService._instance = None
|
|
@@ -47,7 +48,7 @@ def _base_creds_params(**overrides):
|
|
|
47
48
|
def _patch_ensure_binary():
|
|
48
49
|
"""Patch HimalayaService.ensure_binary to return a fake path without shutil.which."""
|
|
49
50
|
return patch(
|
|
50
|
-
"
|
|
51
|
+
"nodes.email._himalaya.HimalayaService.ensure_binary",
|
|
51
52
|
new=AsyncMock(return_value="/usr/bin/himalaya"),
|
|
52
53
|
)
|
|
53
54
|
|
|
@@ -91,7 +92,7 @@ class TestEmailSend:
|
|
|
91
92
|
# Return empty JSON so himalaya "succeeds" with no extra fields
|
|
92
93
|
with _patch_ensure_binary(), patched_subprocess(stdout=b"{}", returncode=0), \
|
|
93
94
|
patched_container(auth_api_keys={"email_address": "alice@example.com", "email_password": "sekret", "email_provider": "gmail"}), patched_pricing(), \
|
|
94
|
-
patch("
|
|
95
|
+
patch("nodes.email._himalaya.HimalayaService.execute",
|
|
95
96
|
new=AsyncMock(return_value={})) as mock_exec:
|
|
96
97
|
result = await harness.execute(
|
|
97
98
|
"emailSend",
|
|
@@ -149,7 +150,7 @@ class TestEmailRead:
|
|
|
149
150
|
]
|
|
150
151
|
# Himalaya returns a list for envelope list -> wrapped in {data: ...}
|
|
151
152
|
with _patch_ensure_binary(), \
|
|
152
|
-
patch("
|
|
153
|
+
patch("nodes.email._himalaya.HimalayaService.execute",
|
|
153
154
|
new=AsyncMock(return_value=envelopes)), \
|
|
154
155
|
patched_container(auth_api_keys={"email_address": "alice@example.com", "email_password": "sekret", "email_provider": "gmail"}), patched_pricing():
|
|
155
156
|
result = await harness.execute(
|
|
@@ -166,7 +167,7 @@ class TestEmailRead:
|
|
|
166
167
|
async def test_read_merges_dict_output(self, harness):
|
|
167
168
|
message = {"subject": "hello", "body": "world", "from": "a@x.com"}
|
|
168
169
|
with _patch_ensure_binary(), \
|
|
169
|
-
patch("
|
|
170
|
+
patch("nodes.email._himalaya.HimalayaService.execute",
|
|
170
171
|
new=AsyncMock(return_value=message)), \
|
|
171
172
|
patched_container(auth_api_keys={"email_address": "alice@example.com", "email_password": "sekret", "email_provider": "gmail"}), patched_pricing():
|
|
172
173
|
result = await harness.execute(
|
|
@@ -213,7 +214,7 @@ class TestEmailRead:
|
|
|
213
214
|
class TestEmailReceive:
|
|
214
215
|
async def test_new_email_detected_on_second_poll(self, harness):
|
|
215
216
|
"""Baseline sees {id1}; next poll returns {id1,id2} -> dispatch id2."""
|
|
216
|
-
from
|
|
217
|
+
from nodes.email._service import EmailService
|
|
217
218
|
|
|
218
219
|
email_detail = {"from": "c@x.com", "subject": "new!", "body": "hi"}
|
|
219
220
|
|
|
@@ -248,8 +249,8 @@ class TestEmailReceive:
|
|
|
248
249
|
assert dispatch_mock.call_args.args[0] == "email_received"
|
|
249
250
|
|
|
250
251
|
async def test_mark_as_read_adds_seen_flag(self, harness):
|
|
251
|
-
from
|
|
252
|
-
from
|
|
252
|
+
from nodes.email._service import EmailService
|
|
253
|
+
from nodes.email._himalaya import HimalayaService
|
|
253
254
|
|
|
254
255
|
poll_ids_mock = AsyncMock(side_effect=[set(), {"42"}])
|
|
255
256
|
fetch_detail_mock = AsyncMock(return_value={"message_id": "42", "folder": "INBOX"})
|
|
@@ -291,7 +292,7 @@ class TestEmailReceive:
|
|
|
291
292
|
|
|
292
293
|
async def test_subprocess_error_surfaces_as_envelope(self, harness):
|
|
293
294
|
"""If poll_ids raises (e.g. Himalaya subprocess fails) the handler returns an error envelope."""
|
|
294
|
-
from
|
|
295
|
+
from nodes.email._service import EmailService
|
|
295
296
|
|
|
296
297
|
poll_ids_mock = AsyncMock(side_effect=RuntimeError("himalaya error: connection refused"))
|
|
297
298
|
|
|
@@ -42,10 +42,10 @@ def _patch_creds(module_name: str, creds_return=None, side_effect=None):
|
|
|
42
42
|
By default returns a MagicMock that stands in for a Credentials instance.
|
|
43
43
|
Pass `side_effect=ValueError(...)` to simulate missing credentials.
|
|
44
44
|
"""
|
|
45
|
-
# Scaling-branch: shared helper lives at
|
|
45
|
+
# Scaling-branch: shared helper lives at nodes.google._auth_helper.
|
|
46
46
|
# `module_name` is retained for API symmetry but not part of the patch path.
|
|
47
47
|
_ = module_name
|
|
48
|
-
target = "
|
|
48
|
+
target = "nodes.google._auth_helper.get_google_credentials"
|
|
49
49
|
kwargs = {}
|
|
50
50
|
if side_effect is not None:
|
|
51
51
|
kwargs["side_effect"] = side_effect
|
|
@@ -463,73 +463,120 @@ class TestRLMAgent:
|
|
|
463
463
|
|
|
464
464
|
|
|
465
465
|
class TestClaudeCodeAgent:
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
466
|
+
"""The claude_code_agent plugin now goes through `AICliService.run_batch`
|
|
467
|
+
(multi-task batch). Single-prompt input gets adapted to a one-task batch
|
|
468
|
+
for back-compat. These tests mock the new service path."""
|
|
469
|
+
|
|
470
|
+
def _wire_cli_service(
|
|
471
|
+
self,
|
|
472
|
+
*,
|
|
473
|
+
response: str = "cli response",
|
|
474
|
+
session_id: str = "sess-abc",
|
|
475
|
+
cost_usd: float = 0.012,
|
|
476
|
+
success: bool = True,
|
|
477
|
+
error: str | None = None,
|
|
478
|
+
):
|
|
479
|
+
"""Mock `AICliService` with a `run_batch` that returns a one-task BatchResult."""
|
|
480
|
+
from services.cli_agent.protocol import (
|
|
481
|
+
BatchResult,
|
|
482
|
+
CanonicalUsage,
|
|
483
|
+
SessionResult,
|
|
475
484
|
)
|
|
476
|
-
return service
|
|
477
485
|
|
|
478
|
-
|
|
479
|
-
|
|
486
|
+
async def fake_run_batch(provider, *, tasks, **kwargs):
|
|
487
|
+
tasks_list = list(tasks)
|
|
488
|
+
results = []
|
|
489
|
+
for t in tasks_list:
|
|
490
|
+
results.append(SessionResult(
|
|
491
|
+
task_id=t.task_id or "t_test",
|
|
492
|
+
session_id=session_id,
|
|
493
|
+
provider=provider,
|
|
494
|
+
prompt=t.prompt,
|
|
495
|
+
response=response,
|
|
496
|
+
cost_usd=cost_usd,
|
|
497
|
+
duration_ms=1234,
|
|
498
|
+
num_turns=2,
|
|
499
|
+
canonical_usage=CanonicalUsage(input_tokens=10, output_tokens=5),
|
|
500
|
+
success=success,
|
|
501
|
+
error=error,
|
|
502
|
+
))
|
|
503
|
+
n_succeeded = sum(1 for r in results if r.success)
|
|
504
|
+
return BatchResult(
|
|
505
|
+
tasks=results,
|
|
506
|
+
n_tasks=len(results),
|
|
507
|
+
n_succeeded=n_succeeded,
|
|
508
|
+
n_failed=len(results) - n_succeeded,
|
|
509
|
+
total_cost_usd=cost_usd if all(r.success for r in results) else None,
|
|
510
|
+
wall_clock_ms=1500,
|
|
511
|
+
provider=provider,
|
|
512
|
+
timestamp="2026-05-04T00:00:00Z",
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
svc = MagicMock(name="AICliService")
|
|
516
|
+
svc.run_batch = AsyncMock(side_effect=fake_run_batch)
|
|
517
|
+
svc.cancel_workflow = AsyncMock(return_value=0)
|
|
518
|
+
svc.cancel_node = AsyncMock(return_value=0)
|
|
519
|
+
return svc
|
|
520
|
+
|
|
521
|
+
async def test_happy_path_routes_through_run_batch(self, harness):
|
|
522
|
+
svc = self._wire_cli_service()
|
|
480
523
|
|
|
481
524
|
with patched_container(auth_api_keys={}), patched_broadcaster(), patch(
|
|
482
|
-
"services.
|
|
483
|
-
return_value=
|
|
525
|
+
"services.cli_agent.service.get_ai_cli_service",
|
|
526
|
+
return_value=svc,
|
|
484
527
|
):
|
|
485
528
|
result = await harness.execute(
|
|
486
529
|
"claude_code_agent",
|
|
487
530
|
{
|
|
488
531
|
"prompt": "write a hello world script",
|
|
489
532
|
"model": "claude-sonnet-4-6",
|
|
490
|
-
"max_turns": 5,
|
|
491
|
-
"max_budget_usd": 2.0,
|
|
492
533
|
},
|
|
493
534
|
)
|
|
494
535
|
|
|
495
536
|
harness.assert_envelope(result, success=True)
|
|
496
537
|
payload = result["result"]
|
|
497
538
|
assert payload["response"] == "cli response"
|
|
498
|
-
assert payload["provider"] == "anthropic"
|
|
499
539
|
assert payload["session_id"] == "sess-abc"
|
|
500
|
-
assert payload["
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
assert call.
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
540
|
+
assert payload["provider"] == "claude"
|
|
541
|
+
assert payload["n_tasks"] == 1
|
|
542
|
+
assert payload["n_succeeded"] == 1
|
|
543
|
+
|
|
544
|
+
# AICliService.run_batch was called with provider="claude" + 1 task
|
|
545
|
+
call = svc.run_batch.await_args
|
|
546
|
+
assert call.args[0] == "claude"
|
|
547
|
+
tasks = list(call.kwargs["tasks"])
|
|
548
|
+
assert len(tasks) == 1
|
|
549
|
+
assert tasks[0].prompt == "write a hello world script"
|
|
550
|
+
assert tasks[0].model == "claude-sonnet-4-6"
|
|
551
|
+
|
|
552
|
+
async def test_max_budget_usd_propagates_via_task_spec(self, harness):
|
|
553
|
+
"""Per-task max_budget_usd must reach the ClaudeTaskSpec."""
|
|
554
|
+
svc = self._wire_cli_service()
|
|
513
555
|
|
|
514
556
|
with patched_container(auth_api_keys={}), patched_broadcaster(), patch(
|
|
515
|
-
"services.
|
|
516
|
-
return_value=
|
|
557
|
+
"services.cli_agent.service.get_ai_cli_service",
|
|
558
|
+
return_value=svc,
|
|
517
559
|
):
|
|
518
560
|
await harness.execute(
|
|
519
561
|
"claude_code_agent",
|
|
520
|
-
{
|
|
562
|
+
{
|
|
563
|
+
"tasks": [
|
|
564
|
+
{"prompt": "x", "provider": "claude", "max_budget_usd": 7.5},
|
|
565
|
+
],
|
|
566
|
+
},
|
|
521
567
|
)
|
|
522
568
|
|
|
523
|
-
call =
|
|
524
|
-
|
|
569
|
+
call = svc.run_batch.await_args
|
|
570
|
+
tasks = list(call.kwargs["tasks"])
|
|
571
|
+
assert tasks[0].max_budget_usd == 7.5
|
|
525
572
|
|
|
526
573
|
async def test_no_prompt_returns_failure(self, harness):
|
|
527
|
-
"""
|
|
528
|
-
|
|
574
|
+
"""No prompt + no tasks must short-circuit before constructing a batch."""
|
|
575
|
+
svc = self._wire_cli_service()
|
|
529
576
|
|
|
530
577
|
with patched_container(auth_api_keys={}), patched_broadcaster(), patch(
|
|
531
|
-
"services.
|
|
532
|
-
return_value=
|
|
578
|
+
"services.cli_agent.service.get_ai_cli_service",
|
|
579
|
+
return_value=svc,
|
|
533
580
|
):
|
|
534
581
|
result = await harness.execute(
|
|
535
582
|
"claude_code_agent",
|
|
@@ -538,18 +585,17 @@ class TestClaudeCodeAgent:
|
|
|
538
585
|
|
|
539
586
|
harness.assert_envelope(result, success=False)
|
|
540
587
|
assert "prompt" in result["error"].lower()
|
|
541
|
-
#
|
|
542
|
-
assert
|
|
588
|
+
# AICliService must not have been engaged.
|
|
589
|
+
assert svc.run_batch.await_count == 0
|
|
543
590
|
|
|
544
|
-
async def
|
|
545
|
-
"""When
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
service.execute = AsyncMock(side_effect=RuntimeError("cli exit 1: boom"))
|
|
591
|
+
async def test_run_batch_failure_becomes_envelope(self, harness):
|
|
592
|
+
"""When AICliService.run_batch raises, the handler surfaces the error."""
|
|
593
|
+
svc = MagicMock(name="AICliService")
|
|
594
|
+
svc.run_batch = AsyncMock(side_effect=RuntimeError("cli exit 1: boom"))
|
|
549
595
|
|
|
550
596
|
with patched_container(auth_api_keys={}), patched_broadcaster(), patch(
|
|
551
|
-
"services.
|
|
552
|
-
return_value=
|
|
597
|
+
"services.cli_agent.service.get_ai_cli_service",
|
|
598
|
+
return_value=svc,
|
|
553
599
|
):
|
|
554
600
|
result = await harness.execute(
|
|
555
601
|
"claude_code_agent",
|
|
@@ -560,7 +606,7 @@ class TestClaudeCodeAgent:
|
|
|
560
606
|
assert "boom" in result["error"]
|
|
561
607
|
|
|
562
608
|
async def test_auto_prompt_fallback_from_input_main(self, harness):
|
|
563
|
-
|
|
609
|
+
svc = self._wire_cli_service()
|
|
564
610
|
|
|
565
611
|
nodes = [
|
|
566
612
|
{"id": "src-1", "type": "chatTrigger"},
|
|
@@ -576,8 +622,8 @@ class TestClaudeCodeAgent:
|
|
|
576
622
|
)
|
|
577
623
|
|
|
578
624
|
with patched_container(auth_api_keys={}), patched_broadcaster(), patch(
|
|
579
|
-
"services.
|
|
580
|
-
return_value=
|
|
625
|
+
"services.cli_agent.service.get_ai_cli_service",
|
|
626
|
+
return_value=svc,
|
|
581
627
|
):
|
|
582
628
|
await harness.execute(
|
|
583
629
|
"claude_code_agent",
|
|
@@ -586,5 +632,6 @@ class TestClaudeCodeAgent:
|
|
|
586
632
|
context=ctx,
|
|
587
633
|
)
|
|
588
634
|
|
|
589
|
-
call =
|
|
590
|
-
|
|
635
|
+
call = svc.run_batch.await_args
|
|
636
|
+
tasks = list(call.kwargs["tasks"])
|
|
637
|
+
assert tasks[0].prompt == "upstream text"
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"""Contract tests for the Stripe plugin (Wave 12 framework version).
|
|
2
|
+
|
|
3
|
+
The plugin is now a thin specialisation of ``services.events`` —
|
|
4
|
+
``StripeListenSource`` (DaemonEventSource) supervises ``stripe listen``
|
|
5
|
+
and ``StripeWebhookSource`` (WebhookSource) receives forwarded events.
|
|
6
|
+
These tests verify the plugin wiring and the receive-node reshape;
|
|
7
|
+
end-to-end smoke against the real CLI requires the binary on PATH.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import hashlib
|
|
14
|
+
import hmac
|
|
15
|
+
import json
|
|
16
|
+
import time
|
|
17
|
+
from unittest.mock import AsyncMock, patch
|
|
18
|
+
|
|
19
|
+
import pytest
|
|
20
|
+
|
|
21
|
+
pytestmark = pytest.mark.node_contract
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _run(coro):
|
|
25
|
+
return asyncio.new_event_loop().run_until_complete(coro)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _signed(body: bytes, secret: str) -> dict:
|
|
29
|
+
ts = int(time.time())
|
|
30
|
+
sig = hmac.new(secret.encode(), f"{ts}.".encode() + body, hashlib.sha256).hexdigest()
|
|
31
|
+
return {"Stripe-Signature": f"t={ts},v1={sig}"}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _fake_request(body: bytes, headers: dict | None = None):
|
|
35
|
+
"""Return an object with the two attrs WebhookSource.handle uses."""
|
|
36
|
+
|
|
37
|
+
class R:
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
r = R()
|
|
41
|
+
r.headers = headers or {}
|
|
42
|
+
|
|
43
|
+
async def _body():
|
|
44
|
+
return body
|
|
45
|
+
|
|
46
|
+
r.body = _body
|
|
47
|
+
return r
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ============================================================================
|
|
51
|
+
# StripeWebhookSource — shape() and end-to-end handle()
|
|
52
|
+
# ============================================================================
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class TestStripeWebhookShape:
|
|
56
|
+
SECRET = "whsec_test_xyz"
|
|
57
|
+
|
|
58
|
+
def _stub_secret_resolution(self):
|
|
59
|
+
return patch(
|
|
60
|
+
"nodes.stripe._source.StripeCredential.resolve",
|
|
61
|
+
AsyncMock(return_value={"api_key": "sk_test", "stripe_webhook_secret": self.SECRET}),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def test_shape_extracts_stripe_fields(self):
|
|
65
|
+
from nodes.stripe._source import get_webhook_source
|
|
66
|
+
src = get_webhook_source()
|
|
67
|
+
payload = {
|
|
68
|
+
"id": "evt_1",
|
|
69
|
+
"type": "charge.succeeded",
|
|
70
|
+
"created": 1700000000,
|
|
71
|
+
"livemode": False,
|
|
72
|
+
"account": "acct_test",
|
|
73
|
+
"data": {"object": {"amount": 1000}},
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
class FakeReq:
|
|
77
|
+
headers = {}
|
|
78
|
+
|
|
79
|
+
async def body(self):
|
|
80
|
+
return b""
|
|
81
|
+
|
|
82
|
+
ev = _run(src.shape(FakeReq(), b"", payload))
|
|
83
|
+
assert ev.id == "evt_1"
|
|
84
|
+
assert ev.type == "stripe.charge.succeeded"
|
|
85
|
+
assert ev.source == "stripe://acct_test"
|
|
86
|
+
assert ev.data == {"object": {"amount": 1000}}
|
|
87
|
+
|
|
88
|
+
def test_handle_dispatches_on_valid_signature(self):
|
|
89
|
+
from nodes.stripe._source import get_webhook_source
|
|
90
|
+
src = get_webhook_source()
|
|
91
|
+
body = json.dumps({"id": "evt_2", "type": "charge.succeeded", "data": {}}).encode()
|
|
92
|
+
req = _fake_request(body, headers=_signed(body, self.SECRET))
|
|
93
|
+
with self._stub_secret_resolution(), \
|
|
94
|
+
patch("services.event_waiter.dispatch") as dispatch:
|
|
95
|
+
ev = _run(src.handle(req))
|
|
96
|
+
assert ev.type == "stripe.charge.succeeded"
|
|
97
|
+
dispatch.assert_called_once()
|
|
98
|
+
ev_type, dispatched = dispatch.call_args[0]
|
|
99
|
+
assert ev_type == "stripe.webhook"
|
|
100
|
+
assert dispatched.id == "evt_2"
|
|
101
|
+
|
|
102
|
+
def test_handle_rejects_tampered_signature(self):
|
|
103
|
+
from fastapi import HTTPException
|
|
104
|
+
from nodes.stripe._source import get_webhook_source
|
|
105
|
+
src = get_webhook_source()
|
|
106
|
+
body = json.dumps({"id": "evt_3", "type": "charge.succeeded"}).encode()
|
|
107
|
+
req = _fake_request(body, headers=_signed(body, "whsec_other"))
|
|
108
|
+
with self._stub_secret_resolution():
|
|
109
|
+
with pytest.raises(HTTPException) as exc:
|
|
110
|
+
_run(src.handle(req))
|
|
111
|
+
assert exc.value.status_code == 400
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# ============================================================================
|
|
115
|
+
# StripeReceiveNode — filter + reshape
|
|
116
|
+
# ============================================================================
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class TestStripeReceiveFilter:
|
|
120
|
+
def _filter(self, params: dict | None = None):
|
|
121
|
+
from nodes.stripe.stripe_receive import StripeReceiveNode, StripeReceiveParams
|
|
122
|
+
node = StripeReceiveNode()
|
|
123
|
+
return node.build_filter(StripeReceiveParams(**(params or {})))
|
|
124
|
+
|
|
125
|
+
def _ev(self, stripe_type: str, livemode: bool = False):
|
|
126
|
+
from services.events import WorkflowEvent
|
|
127
|
+
return WorkflowEvent(
|
|
128
|
+
source="stripe://acct_1",
|
|
129
|
+
type=f"stripe.{stripe_type}",
|
|
130
|
+
data={"livemode": livemode},
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def test_all_matches_anything(self):
|
|
134
|
+
f = self._filter({"event_type_filter": "all"})
|
|
135
|
+
assert f(self._ev("charge.succeeded")) is True
|
|
136
|
+
assert f(self._ev("payment_intent.created")) is True
|
|
137
|
+
|
|
138
|
+
def test_exact_match(self):
|
|
139
|
+
f = self._filter({"event_type_filter": "charge.succeeded"})
|
|
140
|
+
assert f(self._ev("charge.succeeded")) is True
|
|
141
|
+
assert f(self._ev("charge.refunded")) is False
|
|
142
|
+
|
|
143
|
+
def test_wildcard_prefix(self):
|
|
144
|
+
f = self._filter({"event_type_filter": "charge.*"})
|
|
145
|
+
assert f(self._ev("charge.succeeded")) is True
|
|
146
|
+
assert f(self._ev("charge.refunded")) is True
|
|
147
|
+
assert f(self._ev("payment_intent.created")) is False
|
|
148
|
+
|
|
149
|
+
def test_livemode_filter(self):
|
|
150
|
+
live = self._filter({"livemode_filter": "live"})
|
|
151
|
+
test = self._filter({"livemode_filter": "test"})
|
|
152
|
+
assert live(self._ev("charge.succeeded", livemode=True)) is True
|
|
153
|
+
assert live(self._ev("charge.succeeded", livemode=False)) is False
|
|
154
|
+
assert test(self._ev("charge.succeeded", livemode=False)) is True
|
|
155
|
+
assert test(self._ev("charge.succeeded", livemode=True)) is False
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class TestStripeReceiveReshape:
|
|
159
|
+
def test_shape_output_extracts_stripe_fields(self):
|
|
160
|
+
from nodes.stripe.stripe_receive import StripeReceiveNode
|
|
161
|
+
from services.events import WorkflowEvent
|
|
162
|
+
ev = WorkflowEvent(
|
|
163
|
+
id="evt_1",
|
|
164
|
+
source="stripe://acct_42",
|
|
165
|
+
type="stripe.charge.succeeded",
|
|
166
|
+
data={
|
|
167
|
+
"request": {"id": "req_99"},
|
|
168
|
+
"data": {"object": {"amount": 2500}},
|
|
169
|
+
"livemode": True,
|
|
170
|
+
"api_version": "2024-04-10",
|
|
171
|
+
},
|
|
172
|
+
)
|
|
173
|
+
out = StripeReceiveNode().shape_output(ev)
|
|
174
|
+
assert out["event_id"] == "evt_1"
|
|
175
|
+
assert out["event_type"] == "charge.succeeded"
|
|
176
|
+
assert out["request_id"] == "req_99"
|
|
177
|
+
assert out["account"] == "acct_42"
|
|
178
|
+
assert out["livemode"] is True
|
|
179
|
+
assert out["data"] == {"object": {"amount": 2500}}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# ============================================================================
|
|
183
|
+
# StripeActionNode — pass-through over the CLI
|
|
184
|
+
# ============================================================================
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class TestStripeActionPassthrough:
|
|
188
|
+
@pytest.fixture
|
|
189
|
+
def cli_capture(self):
|
|
190
|
+
captured: list[list[str]] = []
|
|
191
|
+
|
|
192
|
+
async def fake_run(*, binary, argv, **kwargs):
|
|
193
|
+
# Stripe CLI uses ~/.config/stripe/config.toml — no credential injection.
|
|
194
|
+
captured.append(list(argv))
|
|
195
|
+
return {"success": True, "result": {"id": "x"}, "stdout": "{}"}
|
|
196
|
+
|
|
197
|
+
return captured, fake_run
|
|
198
|
+
|
|
199
|
+
def test_command_is_shlex_split(self, cli_capture):
|
|
200
|
+
captured, fake = cli_capture
|
|
201
|
+
with patch("nodes.stripe.stripe_action.run_cli_command", AsyncMock(side_effect=fake)):
|
|
202
|
+
from nodes.stripe.stripe_action import StripeActionNode, StripeActionParams
|
|
203
|
+
node = StripeActionNode()
|
|
204
|
+
result = _run(node.run(None, StripeActionParams(command="customers create --email a@b.com")))
|
|
205
|
+
assert result["success"] is True
|
|
206
|
+
assert captured == [["customers", "create", "--email", "a@b.com"]]
|
|
207
|
+
|
|
208
|
+
def test_quoted_args_preserved(self, cli_capture):
|
|
209
|
+
captured, fake = cli_capture
|
|
210
|
+
with patch("nodes.stripe.stripe_action.run_cli_command", AsyncMock(side_effect=fake)):
|
|
211
|
+
from nodes.stripe.stripe_action import StripeActionNode, StripeActionParams
|
|
212
|
+
node = StripeActionNode()
|
|
213
|
+
_run(node.run(None, StripeActionParams(command="customers create --name 'Acme Inc'")))
|
|
214
|
+
assert captured == [["customers", "create", "--name", "Acme Inc"]]
|
|
215
|
+
|
|
216
|
+
def test_empty_command_raises(self):
|
|
217
|
+
from nodes.stripe.stripe_action import StripeActionNode, StripeActionParams
|
|
218
|
+
node = StripeActionNode()
|
|
219
|
+
with pytest.raises(RuntimeError, match="command is required"):
|
|
220
|
+
_run(node.run(None, StripeActionParams(command=" ")))
|
|
221
|
+
|
|
222
|
+
def test_cli_failure_raises(self):
|
|
223
|
+
from nodes.stripe.stripe_action import StripeActionNode, StripeActionParams
|
|
224
|
+
|
|
225
|
+
async def fake_fail(*, binary, argv, **kwargs):
|
|
226
|
+
return {"success": False, "error": "stripe: unknown command 'frobnicate'"}
|
|
227
|
+
|
|
228
|
+
with patch("nodes.stripe.stripe_action.run_cli_command", AsyncMock(side_effect=fake_fail)):
|
|
229
|
+
node = StripeActionNode()
|
|
230
|
+
with pytest.raises(RuntimeError, match="frobnicate"):
|
|
231
|
+
_run(node.run(None, StripeActionParams(command="frobnicate")))
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
# ============================================================================
|
|
235
|
+
# Plugin self-registration
|
|
236
|
+
# ============================================================================
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class TestStripePluginRegistration:
|
|
240
|
+
def test_ws_handlers_registered(self):
|
|
241
|
+
import nodes.stripe # noqa: F401
|
|
242
|
+
from services.ws_handler_registry import get_ws_handlers
|
|
243
|
+
registered = get_ws_handlers()
|
|
244
|
+
for name in (
|
|
245
|
+
"stripe_login", "stripe_logout",
|
|
246
|
+
"stripe_connect", "stripe_disconnect", "stripe_reconnect",
|
|
247
|
+
"stripe_status", "stripe_trigger",
|
|
248
|
+
):
|
|
249
|
+
assert name in registered, f"WS handler '{name}' not registered"
|
|
250
|
+
|
|
251
|
+
def test_webhook_source_registered(self):
|
|
252
|
+
import nodes.stripe # noqa: F401
|
|
253
|
+
from services.events import WEBHOOK_SOURCES
|
|
254
|
+
assert "stripe" in WEBHOOK_SOURCES
|
|
255
|
+
assert WEBHOOK_SOURCES["stripe"].type == "stripe.webhook"
|
|
256
|
+
|
|
257
|
+
def test_node_classes_registered(self):
|
|
258
|
+
import nodes.stripe # noqa: F401
|
|
259
|
+
from services.node_registry import get_node_class
|
|
260
|
+
assert get_node_class("stripeReceive").__name__ == "StripeReceiveNode"
|
|
261
|
+
assert get_node_class("stripeAction").__name__ == "StripeActionNode"
|
|
262
|
+
|
|
263
|
+
def test_credential_registered(self):
|
|
264
|
+
import nodes.stripe # noqa: F401
|
|
265
|
+
from services.plugin.credential import CREDENTIAL_REGISTRY
|
|
266
|
+
# Stripe CLI manages auth at ~/.config/stripe/config.toml; the
|
|
267
|
+
# credential class is a thin marker keyed by "stripe" (no api_key).
|
|
268
|
+
assert "stripe" in CREDENTIAL_REGISTRY
|
|
269
|
+
|
|
270
|
+
def test_output_schemas_registered(self):
|
|
271
|
+
import nodes.stripe # noqa: F401
|
|
272
|
+
from services.node_output_schemas import NODE_OUTPUT_SCHEMAS
|
|
273
|
+
assert "stripeReceive" in NODE_OUTPUT_SCHEMAS
|
|
274
|
+
assert "stripeAction" in NODE_OUTPUT_SCHEMAS
|
|
275
|
+
|
|
276
|
+
def test_action_node_is_ai_tool(self):
|
|
277
|
+
from nodes.stripe.stripe_action import StripeActionNode
|
|
278
|
+
assert StripeActionNode.usable_as_tool is True
|
|
279
|
+
|
|
280
|
+
def test_receive_node_subscribes_to_stripe_webhook(self):
|
|
281
|
+
from nodes.stripe.stripe_receive import StripeReceiveNode
|
|
282
|
+
assert StripeReceiveNode.event_type == "stripe.webhook"
|
|
283
|
+
|
|
284
|
+
def test_listen_source_has_correct_namespace(self):
|
|
285
|
+
from nodes.stripe._source import get_listen_source
|
|
286
|
+
src = get_listen_source()
|
|
287
|
+
assert src.process_name == "stripe-listen"
|
|
288
|
+
assert src.workflow_namespace == "_stripe"
|
|
289
|
+
# Empty binary_name disables the framework's PATH check; the
|
|
290
|
+
# plugin resolves the binary itself via ensure_stripe_cli (which
|
|
291
|
+
# falls back to a workspace-local download on systems without
|
|
292
|
+
# a system install of the Stripe CLI).
|
|
293
|
+
assert src.binary_name == ""
|
|
@@ -355,7 +355,7 @@ class TestSocialSend:
|
|
|
355
355
|
return_value={"success": True, "message_id": "wamid.xyz"}
|
|
356
356
|
)
|
|
357
357
|
|
|
358
|
-
with patch("
|
|
358
|
+
with patch("nodes.whatsapp._service.handle_whatsapp_send", whatsapp_send):
|
|
359
359
|
result = await harness.execute(
|
|
360
360
|
"socialSend",
|
|
361
361
|
{
|
|
@@ -385,7 +385,7 @@ class TestSocialSend:
|
|
|
385
385
|
# recipientType=phone but no phone param -> ValueError inside handler
|
|
386
386
|
whatsapp_send = AsyncMock()
|
|
387
387
|
|
|
388
|
-
with patch("
|
|
388
|
+
with patch("nodes.whatsapp._service.handle_whatsapp_send", whatsapp_send):
|
|
389
389
|
result = await harness.execute(
|
|
390
390
|
"socialSend",
|
|
391
391
|
{
|
|
@@ -404,7 +404,7 @@ class TestSocialSend:
|
|
|
404
404
|
# Any non-whatsapp channel should surface as a failed envelope.
|
|
405
405
|
whatsapp_send = AsyncMock()
|
|
406
406
|
|
|
407
|
-
with patch("
|
|
407
|
+
with patch("nodes.whatsapp._service.handle_whatsapp_send", whatsapp_send):
|
|
408
408
|
result = await harness.execute(
|
|
409
409
|
"socialSend",
|
|
410
410
|
{
|
|
@@ -425,7 +425,7 @@ class TestSocialSend:
|
|
|
425
425
|
return_value={"success": False, "error": "rpc boom"}
|
|
426
426
|
)
|
|
427
427
|
|
|
428
|
-
with patch("
|
|
428
|
+
with patch("nodes.whatsapp._service.handle_whatsapp_send", whatsapp_send):
|
|
429
429
|
result = await harness.execute(
|
|
430
430
|
"socialSend",
|
|
431
431
|
{
|
|
@@ -242,7 +242,7 @@ class TestTwitterSend:
|
|
|
242
242
|
with _patch_client(stub_cls), patched_container(
|
|
243
243
|
auth_oauth_tokens=_ok_tokens()
|
|
244
244
|
), patched_pricing(), patch(
|
|
245
|
-
"
|
|
245
|
+
"nodes.twitter._oauth.TwitterOAuth"
|
|
246
246
|
) as oauth_cls:
|
|
247
247
|
oauth_instance = MagicMock()
|
|
248
248
|
oauth_instance.refresh_access_token = refresh_mock
|
|
@@ -50,7 +50,7 @@ class _FakeBrowserService:
|
|
|
50
50
|
|
|
51
51
|
def _patch_browser_service(svc):
|
|
52
52
|
"""Patch get_browser_service to return the fake."""
|
|
53
|
-
return patch("
|
|
53
|
+
return patch("nodes.browser._service.get_browser_service", return_value=svc)
|
|
54
54
|
|
|
55
55
|
|
|
56
56
|
# ============================================================================
|
|
@@ -140,7 +140,7 @@ class TestBrowser:
|
|
|
140
140
|
|
|
141
141
|
async def test_service_not_installed(self, harness):
|
|
142
142
|
with patch(
|
|
143
|
-
"
|
|
143
|
+
"nodes.browser._service.get_browser_service", return_value=None
|
|
144
144
|
):
|
|
145
145
|
result = await harness.execute("browser", {"operation": "navigate", "url": "https://x"})
|
|
146
146
|
|