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
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
Locks in invariants 7 and 8 from docs-internal/credentials_panel.md:
|
|
4
4
|
- store_api_key requires models=[]
|
|
5
5
|
- Memory cache hit path does not query DB on every call
|
|
6
|
-
- clear_cache wipes
|
|
6
|
+
- clear_cache wipes _api_key_cache, _oauth_cache (one merged cache
|
|
7
|
+
per RFC 9700 + post-Wave-12 dedup; previously the API-key half was
|
|
8
|
+
split across _memory_cache + _models_cache).
|
|
7
9
|
"""
|
|
8
10
|
|
|
9
11
|
from __future__ import annotations
|
|
@@ -91,8 +93,7 @@ class TestMemoryCache:
|
|
|
91
93
|
await auth_service.store_api_key("openai", "sk-foo", models=[])
|
|
92
94
|
|
|
93
95
|
# Wipe memory cache to simulate fresh process
|
|
94
|
-
auth_service.
|
|
95
|
-
auth_service._models_cache.clear()
|
|
96
|
+
auth_service._api_key_cache.clear()
|
|
96
97
|
|
|
97
98
|
call_count = {"n": 0}
|
|
98
99
|
original = auth_service.credentials_db.get_api_key
|
|
@@ -111,19 +112,17 @@ class TestMemoryCache:
|
|
|
111
112
|
assert await auth_service.get_api_key("openai") == "sk-foo"
|
|
112
113
|
assert call_count["n"] == 1
|
|
113
114
|
|
|
114
|
-
async def
|
|
115
|
+
async def test_clear_cache_wipes_both_caches(self, auth_service):
|
|
115
116
|
await auth_service.store_api_key("openai", "sk-foo", models=["gpt-4"])
|
|
116
117
|
await auth_service.store_oauth_tokens("google", "a", "r", email="u@x.com")
|
|
117
118
|
|
|
118
119
|
# Sanity: caches populated
|
|
119
|
-
assert auth_service.
|
|
120
|
-
assert auth_service._models_cache
|
|
120
|
+
assert auth_service._api_key_cache
|
|
121
121
|
assert auth_service._oauth_cache
|
|
122
122
|
|
|
123
123
|
auth_service.clear_cache()
|
|
124
124
|
|
|
125
|
-
assert auth_service.
|
|
126
|
-
assert auth_service._models_cache == {}
|
|
125
|
+
assert auth_service._api_key_cache == {}
|
|
127
126
|
assert auth_service._oauth_cache == {}
|
|
128
127
|
|
|
129
128
|
async def test_clear_cache_does_not_delete_db_data(self, auth_service):
|
|
@@ -149,10 +148,18 @@ class TestOAuthOps:
|
|
|
149
148
|
tokens = await auth_service.get_oauth_tokens("google")
|
|
150
149
|
assert tokens is not None
|
|
151
150
|
assert tokens["access_token"] == "access-1"
|
|
152
|
-
|
|
151
|
+
# Per RFC 9700 the refresh token is NOT returned by
|
|
152
|
+
# get_oauth_tokens — read via get_oauth_refresh_token() instead.
|
|
153
|
+
assert "refresh_token" not in tokens
|
|
153
154
|
assert tokens["email"] == "u@example.com"
|
|
154
155
|
assert tokens["scopes"] == "openid email"
|
|
155
156
|
|
|
157
|
+
# The refresh token is reachable through the dedicated helper
|
|
158
|
+
# that always reads from the encrypted DB (no in-memory cache).
|
|
159
|
+
assert (
|
|
160
|
+
await auth_service.get_oauth_refresh_token("google") == "refresh-1"
|
|
161
|
+
)
|
|
162
|
+
|
|
156
163
|
async def test_remove_oauth_tokens(self, auth_service):
|
|
157
164
|
await auth_service.store_oauth_tokens("google", "a", "r")
|
|
158
165
|
ok = await auth_service.remove_oauth_tokens("google")
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""Pytest invariant: every credential-mutation handler MUST broadcast.
|
|
2
|
+
|
|
3
|
+
The frontend's `useCatalogueQuery` cache + `apiKeyStatuses` map both go
|
|
4
|
+
stale unless the backend explicitly invalidates via:
|
|
5
|
+
|
|
6
|
+
- ``broadcaster.update_api_key_status(...)`` — per-provider validation
|
|
7
|
+
state change (carries the validation result payload).
|
|
8
|
+
- ``broadcaster.broadcast_credential_event("credential.*", ...)`` —
|
|
9
|
+
CloudEvents-typed mutation (refetch signal). Wraps ``WorkflowEvent``
|
|
10
|
+
from ``services.events.envelope``.
|
|
11
|
+
|
|
12
|
+
Cross-tab visibility breaks the moment a handler omits both. This test
|
|
13
|
+
locks the contract by reading each handler's source via
|
|
14
|
+
``inspect.getsource`` and asserting it contains at least one of the two
|
|
15
|
+
broadcast call patterns. New credential mutations that forget to
|
|
16
|
+
broadcast fail CI before they ship.
|
|
17
|
+
|
|
18
|
+
Companion: ``test_auth_service.py`` locks the DB-write-then-cache-update
|
|
19
|
+
ordering inside AuthService.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import inspect
|
|
25
|
+
import re
|
|
26
|
+
|
|
27
|
+
import pytest
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
pytestmark = pytest.mark.credentials
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Match either:
|
|
34
|
+
# broadcaster.update_api_key_status(...)
|
|
35
|
+
# broadcaster.broadcast_credential_event("credential.<anything>", ...)
|
|
36
|
+
# Anchor on `.update_api_key_status(` or `.broadcast_credential_event(`
|
|
37
|
+
# so renames force a test update (intentional).
|
|
38
|
+
_BROADCAST_PATTERN = re.compile(
|
|
39
|
+
r"\.(?:update_api_key_status|broadcast_credential_event)\s*\("
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _handler_source(handler) -> str:
|
|
44
|
+
"""Return the unwrapped source of a handler decorated by `@ws_handler`."""
|
|
45
|
+
fn = handler
|
|
46
|
+
while hasattr(fn, "__wrapped__"):
|
|
47
|
+
fn = fn.__wrapped__
|
|
48
|
+
return inspect.getsource(fn)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class TestCredentialMutationHandlersBroadcast:
|
|
52
|
+
"""Every handler that mutates credential state MUST broadcast.
|
|
53
|
+
|
|
54
|
+
Failing this test means a frontend cache will go stale across tabs
|
|
55
|
+
after the mutation; users will see the wrong stored / connected
|
|
56
|
+
state until they manually refresh.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def test_validate_api_key_broadcasts(self):
|
|
60
|
+
"""Validation broadcasts come from ``Credential.validate`` now.
|
|
61
|
+
|
|
62
|
+
``handle_validate_api_key`` is a pure dispatcher that calls
|
|
63
|
+
``CREDENTIAL_REGISTRY[provider].validate(...)``. The broadcast
|
|
64
|
+
happens inside ``Credential.validate`` (the shared scaffold in
|
|
65
|
+
``services/plugin/credential.py``). This test checks the
|
|
66
|
+
invariant at the point where it actually lives.
|
|
67
|
+
"""
|
|
68
|
+
from services.plugin.credential import Credential
|
|
69
|
+
|
|
70
|
+
src = inspect.getsource(Credential.validate)
|
|
71
|
+
assert _BROADCAST_PATTERN.search(src), (
|
|
72
|
+
"Credential.validate must broadcast via update_api_key_status — "
|
|
73
|
+
"every credential mutation must surface to connected clients."
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def test_save_api_key_broadcasts(self):
|
|
77
|
+
from routers import websocket as ws_module
|
|
78
|
+
|
|
79
|
+
src = _handler_source(ws_module.handle_save_api_key)
|
|
80
|
+
assert _BROADCAST_PATTERN.search(src), (
|
|
81
|
+
"handle_save_api_key must broadcast credential.api_key.saved "
|
|
82
|
+
"via broadcast_credential_event so the catalogue's stored "
|
|
83
|
+
"flag flips on every connected client"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def test_delete_api_key_broadcasts(self):
|
|
87
|
+
from routers import websocket as ws_module
|
|
88
|
+
|
|
89
|
+
src = _handler_source(ws_module.handle_delete_api_key)
|
|
90
|
+
# Delete must trigger BOTH broadcasts: api_key_status (clears
|
|
91
|
+
# apiKeyStatuses) + credential.api_key.deleted (catalogue refetch).
|
|
92
|
+
matches = _BROADCAST_PATTERN.findall(src)
|
|
93
|
+
assert len(matches) >= 2, (
|
|
94
|
+
"handle_delete_api_key must broadcast both update_api_key_status "
|
|
95
|
+
"(clears apiKeyStatuses[provider]) AND broadcast_credential_event "
|
|
96
|
+
"('credential.api_key.deleted') (catalogue refetch). "
|
|
97
|
+
f"Found {len(matches)} broadcast call(s)."
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def test_twitter_logout_broadcasts(self):
|
|
101
|
+
# Twitter handler moved to ``nodes/twitter/_handlers.py`` as
|
|
102
|
+
# part of the plugin-extraction migration. The invariant now
|
|
103
|
+
# asserts on the plugin-owned source.
|
|
104
|
+
from nodes.twitter._handlers import handle_twitter_logout
|
|
105
|
+
|
|
106
|
+
src = _handler_source(handle_twitter_logout)
|
|
107
|
+
assert _BROADCAST_PATTERN.search(src), (
|
|
108
|
+
"handle_twitter_logout must broadcast credential.oauth.disconnected"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def test_google_logout_broadcasts(self):
|
|
112
|
+
from nodes.google._handlers import handle_google_logout
|
|
113
|
+
|
|
114
|
+
src = _handler_source(handle_google_logout)
|
|
115
|
+
assert _BROADCAST_PATTERN.search(src), (
|
|
116
|
+
"handle_google_logout must broadcast credential.oauth.disconnected"
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class TestCredentialEventCloudEventsShape:
|
|
121
|
+
"""The credential broadcast helper wraps `WorkflowEvent` so the
|
|
122
|
+
on-the-wire body is a CloudEvents v1.0 envelope. Locks the spec
|
|
123
|
+
fields so future refactors don't quietly drop required keys.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
@pytest.fixture
|
|
127
|
+
def envelope(self):
|
|
128
|
+
from services.events.envelope import WorkflowEvent
|
|
129
|
+
|
|
130
|
+
event = WorkflowEvent(
|
|
131
|
+
source="machinaos://services/credentials",
|
|
132
|
+
type="credential.api_key.saved",
|
|
133
|
+
subject="openai",
|
|
134
|
+
data={"provider": "openai"},
|
|
135
|
+
)
|
|
136
|
+
return event.model_dump(mode="json")
|
|
137
|
+
|
|
138
|
+
def test_specversion_pinned_to_1(self, envelope):
|
|
139
|
+
assert envelope["specversion"] == "1.0"
|
|
140
|
+
|
|
141
|
+
def test_required_fields_present(self, envelope):
|
|
142
|
+
# CloudEvents 1.0 mandatory: id, source, specversion, type
|
|
143
|
+
for field in ("id", "source", "specversion", "type"):
|
|
144
|
+
assert envelope.get(field), f"missing required field: {field}"
|
|
145
|
+
|
|
146
|
+
def test_credential_subject_is_provider(self, envelope):
|
|
147
|
+
assert envelope["subject"] == "openai"
|
|
148
|
+
|
|
149
|
+
def test_event_type_namespaced(self, envelope):
|
|
150
|
+
# Convention: credential.<area>.<action>
|
|
151
|
+
assert envelope["type"].startswith("credential."), envelope["type"]
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class TestAuthServiceDbCanonicalInvariant:
|
|
155
|
+
"""AuthService.store_*/remove_* must do the DB call before touching
|
|
156
|
+
the in-memory cache. Reverse order leaves a stale-cache window.
|
|
157
|
+
|
|
158
|
+
Static-source check; doesn't run the methods.
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
@pytest.fixture
|
|
162
|
+
def auth_source(self):
|
|
163
|
+
from services import auth as auth_module
|
|
164
|
+
|
|
165
|
+
return inspect.getsource(auth_module.AuthService)
|
|
166
|
+
|
|
167
|
+
@pytest.mark.parametrize(
|
|
168
|
+
"method_name,db_call",
|
|
169
|
+
[
|
|
170
|
+
("store_api_key", "credentials_db.save_api_key"),
|
|
171
|
+
("remove_api_key", "credentials_db.delete_api_key"),
|
|
172
|
+
("store_oauth_tokens", "credentials_db.save_oauth_tokens"),
|
|
173
|
+
("remove_oauth_tokens", "credentials_db.delete_oauth_tokens"),
|
|
174
|
+
],
|
|
175
|
+
)
|
|
176
|
+
def test_method_calls_db(self, auth_source, method_name, db_call):
|
|
177
|
+
"""Every store_*/remove_* method must call the canonical DB
|
|
178
|
+
method. Invariant breaks if a refactor accidentally bypasses
|
|
179
|
+
the DB and only updates the cache."""
|
|
180
|
+
# Find the method body in the AuthService source. Anchor end on
|
|
181
|
+
# the next method/class start OR end-of-string (the last method
|
|
182
|
+
# in the class has no trailing sibling).
|
|
183
|
+
method_re = re.compile(
|
|
184
|
+
rf"async def {method_name}\b.*?(?=\n async def |\n def |\nclass |\Z)",
|
|
185
|
+
re.DOTALL,
|
|
186
|
+
)
|
|
187
|
+
match = method_re.search(auth_source)
|
|
188
|
+
assert match, f"AuthService.{method_name} not found in source"
|
|
189
|
+
body = match.group(0)
|
|
190
|
+
assert db_call in body, (
|
|
191
|
+
f"AuthService.{method_name} must call self.{db_call}() "
|
|
192
|
+
f"before touching the in-memory cache (DB is canonical "
|
|
193
|
+
f"source of truth)"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def test_oauth_cache_does_not_carry_refresh_token(self, auth_source):
|
|
197
|
+
"""Per RFC 9700, refresh tokens must not live in process memory.
|
|
198
|
+
store_oauth_tokens caches only the access token + display fields.
|
|
199
|
+
"""
|
|
200
|
+
match = re.search(
|
|
201
|
+
r"async def store_oauth_tokens\b.*?(?=\n async def |\n def |\nclass )",
|
|
202
|
+
auth_source,
|
|
203
|
+
re.DOTALL,
|
|
204
|
+
)
|
|
205
|
+
assert match, "AuthService.store_oauth_tokens not found"
|
|
206
|
+
body = match.group(0)
|
|
207
|
+
|
|
208
|
+
# Find the cache-write block. It assigns to self._oauth_cache[...].
|
|
209
|
+
cache_block = re.search(
|
|
210
|
+
r"self\._oauth_cache\[\w+\]\s*=\s*\{[^}]+\}", body, re.DOTALL
|
|
211
|
+
)
|
|
212
|
+
assert cache_block, (
|
|
213
|
+
"AuthService.store_oauth_tokens must populate _oauth_cache"
|
|
214
|
+
)
|
|
215
|
+
assert '"refresh_token"' not in cache_block.group(0), (
|
|
216
|
+
"RFC 9700 violation: _oauth_cache entry must not carry "
|
|
217
|
+
"refresh_token. Use get_oauth_refresh_token() helper that "
|
|
218
|
+
"always reads from the encrypted DB."
|
|
219
|
+
)
|
|
@@ -11,8 +11,8 @@ from urllib.parse import parse_qs, urlparse
|
|
|
11
11
|
|
|
12
12
|
import pytest
|
|
13
13
|
|
|
14
|
-
from
|
|
15
|
-
from
|
|
14
|
+
from nodes.google import _oauth as google_oauth
|
|
15
|
+
from nodes.google._oauth import GoogleOAuth, get_callback_paths
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
pytestmark = pytest.mark.credentials
|
|
@@ -88,18 +88,18 @@ class TestAuthorizationUrl:
|
|
|
88
88
|
|
|
89
89
|
|
|
90
90
|
class TestExchangeCode:
|
|
91
|
-
def test_unknown_state_fails(self, oauth):
|
|
91
|
+
async def test_unknown_state_fails(self, oauth):
|
|
92
92
|
# Don't even register a state -- exchange must fail without hitting Google.
|
|
93
|
-
result = oauth.exchange_code("code", "never-generated-state")
|
|
93
|
+
result = await oauth.exchange_code("code", "never-generated-state")
|
|
94
94
|
assert result["success"] is False
|
|
95
95
|
assert "state" in result["error"].lower()
|
|
96
96
|
|
|
97
|
-
def test_state_consumed_on_failure(self, oauth):
|
|
97
|
+
async def test_state_consumed_on_failure(self, oauth):
|
|
98
98
|
"""Even if the token exchange fails, the state must be popped (single use)."""
|
|
99
99
|
auth = oauth.generate_authorization_url()
|
|
100
100
|
state = auth["state"]
|
|
101
101
|
# Exchange will fail because we haven't mocked Google -- network error
|
|
102
|
-
oauth.exchange_code("invalid-code", state)
|
|
102
|
+
await oauth.exchange_code("invalid-code", state)
|
|
103
103
|
# State must be gone regardless of success
|
|
104
104
|
assert state not in google_oauth._oauth_states
|
|
105
105
|
|
|
@@ -80,7 +80,7 @@ class TestGetRedirectUri:
|
|
|
80
80
|
|
|
81
81
|
def test_paths_loaded_from_config_not_hardcoded(self):
|
|
82
82
|
"""Smoke test: the real config must expose google + twitter paths."""
|
|
83
|
-
from
|
|
83
|
+
from nodes.google._oauth import get_callback_paths
|
|
84
84
|
|
|
85
85
|
paths = get_callback_paths()
|
|
86
86
|
assert "google" in paths
|
|
@@ -16,8 +16,8 @@ import httpx
|
|
|
16
16
|
import pytest
|
|
17
17
|
import respx
|
|
18
18
|
|
|
19
|
-
from
|
|
20
|
-
from
|
|
19
|
+
from nodes.twitter import _oauth as twitter_oauth
|
|
20
|
+
from nodes.twitter._oauth import (
|
|
21
21
|
TwitterOAuth,
|
|
22
22
|
TOKEN_URL,
|
|
23
23
|
REVOKE_URL,
|
|
@@ -60,6 +60,10 @@ def patched_container(monkeypatch, auth_service):
|
|
|
60
60
|
# not on services.status_broadcaster (the bound name in the handler module).
|
|
61
61
|
fake_broadcaster = MagicMock()
|
|
62
62
|
fake_broadcaster.update_api_key_status = AsyncMock()
|
|
63
|
+
# Wave-12 credential broadcast helper used by save / delete /
|
|
64
|
+
# oauth-logout handlers. Wraps WorkflowEvent (CloudEvents v1.0)
|
|
65
|
+
# and broadcasts as `credential_catalogue_updated`.
|
|
66
|
+
fake_broadcaster.broadcast_credential_event = AsyncMock()
|
|
63
67
|
fake_broadcaster.broadcast = AsyncMock()
|
|
64
68
|
fake_broadcaster._status = {}
|
|
65
69
|
monkeypatch.setattr(ws_module, "get_status_broadcaster", lambda: fake_broadcaster)
|
|
@@ -110,22 +114,23 @@ class TestValidateApiKey:
|
|
|
110
114
|
# Broadcaster notified with hasKey + models
|
|
111
115
|
patched_container.broadcaster.update_api_key_status.assert_awaited_once()
|
|
112
116
|
|
|
113
|
-
|
|
114
|
-
@patch.object(ws_module, "_SPECIAL_PROVIDER_VALIDATORS", {})
|
|
115
|
-
async def test_validate_skips_model_fetch_for_non_llm(
|
|
117
|
+
async def test_validate_unknown_provider_returns_error(
|
|
116
118
|
self, patched_container, fake_ws
|
|
117
119
|
):
|
|
118
|
-
#
|
|
119
|
-
#
|
|
120
|
-
#
|
|
120
|
+
# ``handle_validate_api_key`` dispatches via
|
|
121
|
+
# ``CREDENTIAL_REGISTRY``; a provider without a registered
|
|
122
|
+
# ``Credential`` subclass is an explicit error (no fall-through
|
|
123
|
+
# to the default LLM probe — every supported provider must own
|
|
124
|
+
# its credential class).
|
|
121
125
|
result = await _call(
|
|
122
126
|
ws_module.handle_validate_api_key,
|
|
123
127
|
{"provider": "anonymous_provider", "api_key": "any-test"},
|
|
124
128
|
fake_ws,
|
|
125
129
|
)
|
|
126
130
|
|
|
127
|
-
assert result["
|
|
128
|
-
assert result["
|
|
131
|
+
assert result["success"] is False
|
|
132
|
+
assert result["valid"] is False
|
|
133
|
+
assert "anonymous_provider" in result["error"]
|
|
129
134
|
patched_container.ai.fetch_models.assert_not_called()
|
|
130
135
|
|
|
131
136
|
async def test_missing_required_fields_returns_error(
|
|
@@ -214,14 +219,22 @@ class TestDeleteApiKey:
|
|
|
214
219
|
|
|
215
220
|
|
|
216
221
|
class TestTwitterOAuthHandlers:
|
|
222
|
+
# Twitter handlers moved to ``nodes/twitter/_handlers.py`` as part
|
|
223
|
+
# of the plugin-extraction migration. The tests now import
|
|
224
|
+
# directly from the plugin folder; the dispatch contract via
|
|
225
|
+
# ``register_ws_handlers`` is exercised by
|
|
226
|
+
# ``test_plugin_self_containment.py``.
|
|
227
|
+
|
|
217
228
|
async def test_login_fails_without_client_id(self, patched_container, fake_ws):
|
|
218
|
-
|
|
229
|
+
from nodes.twitter._handlers import handle_twitter_oauth_login
|
|
230
|
+
result = await _call(handle_twitter_oauth_login, {}, fake_ws)
|
|
219
231
|
assert result["success"] is False
|
|
220
232
|
assert "Client ID" in result["error"]
|
|
221
233
|
|
|
222
234
|
async def test_login_returns_authorization_url(
|
|
223
235
|
self, patched_container, fake_ws
|
|
224
236
|
):
|
|
237
|
+
from nodes.twitter._handlers import handle_twitter_oauth_login
|
|
225
238
|
await patched_container.auth.store_api_key(
|
|
226
239
|
"twitter_client_id", "ci-test", models=[]
|
|
227
240
|
)
|
|
@@ -229,41 +242,51 @@ class TestTwitterOAuthHandlers:
|
|
|
229
242
|
"twitter_client_secret", "cs-test", models=[]
|
|
230
243
|
)
|
|
231
244
|
|
|
232
|
-
result = await _call(
|
|
245
|
+
result = await _call(handle_twitter_oauth_login, {}, fake_ws)
|
|
233
246
|
|
|
234
247
|
assert result["success"] is True
|
|
235
248
|
assert result["url"].startswith("https://x.com/i/oauth2/authorize")
|
|
236
249
|
assert "state" in result
|
|
237
250
|
|
|
238
251
|
async def test_status_when_disconnected(self, patched_container, fake_ws):
|
|
239
|
-
|
|
252
|
+
from nodes.twitter._handlers import handle_twitter_oauth_status
|
|
253
|
+
result = await _call(handle_twitter_oauth_status, {}, fake_ws)
|
|
240
254
|
assert result["connected"] is False
|
|
241
255
|
assert result["username"] is None
|
|
242
256
|
|
|
243
257
|
async def test_logout_clears_oauth_tokens(self, patched_container, fake_ws):
|
|
244
|
-
|
|
258
|
+
from nodes.twitter._handlers import handle_twitter_logout
|
|
245
259
|
await patched_container.auth.store_oauth_tokens(
|
|
246
260
|
"twitter", "access", "refresh"
|
|
247
261
|
)
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
262
|
+
with patch(
|
|
263
|
+
"nodes.twitter._oauth.TwitterOAuth.revoke_token",
|
|
264
|
+
new=AsyncMock(return_value={"success": True}),
|
|
265
|
+
):
|
|
266
|
+
result = await _call(handle_twitter_logout, {}, fake_ws)
|
|
251
267
|
|
|
252
268
|
assert result["success"] is True
|
|
253
269
|
assert await patched_container.auth.get_oauth_tokens("twitter") is None
|
|
254
270
|
|
|
255
271
|
|
|
256
272
|
class TestGoogleOAuthHandlers:
|
|
273
|
+
# Google OAuth handlers moved to ``nodes/google/_handlers.py`` as
|
|
274
|
+
# part of the plugin-extraction migration. Tests now import from
|
|
275
|
+
# the plugin folder; the dispatch contract is exercised by
|
|
276
|
+
# ``test_plugin_self_containment.py``.
|
|
277
|
+
|
|
257
278
|
async def test_login_fails_without_client_credentials(
|
|
258
279
|
self, patched_container, fake_ws
|
|
259
280
|
):
|
|
260
|
-
|
|
281
|
+
from nodes.google._handlers import handle_google_oauth_login
|
|
282
|
+
result = await _call(handle_google_oauth_login, {}, fake_ws)
|
|
261
283
|
assert result["success"] is False
|
|
262
284
|
assert "Client ID" in result["error"]
|
|
263
285
|
|
|
264
286
|
async def test_login_returns_authorization_url(
|
|
265
287
|
self, patched_container, fake_ws
|
|
266
288
|
):
|
|
289
|
+
from nodes.google._handlers import handle_google_oauth_login
|
|
267
290
|
await patched_container.auth.store_api_key(
|
|
268
291
|
"google_client_id", "ci.apps.googleusercontent.com", models=[]
|
|
269
292
|
)
|
|
@@ -271,7 +294,7 @@ class TestGoogleOAuthHandlers:
|
|
|
271
294
|
"google_client_secret", "cs-test", models=[]
|
|
272
295
|
)
|
|
273
296
|
|
|
274
|
-
result = await _call(
|
|
297
|
+
result = await _call(handle_google_oauth_login, {}, fake_ws)
|
|
275
298
|
|
|
276
299
|
assert result["success"] is True
|
|
277
300
|
assert result["url"].startswith("https://accounts.google.com/o/oauth2/auth")
|
|
@@ -279,15 +302,16 @@ class TestGoogleOAuthHandlers:
|
|
|
279
302
|
assert "prompt=consent" in result["url"]
|
|
280
303
|
|
|
281
304
|
async def test_status_when_disconnected(self, patched_container, fake_ws):
|
|
282
|
-
|
|
283
|
-
|
|
305
|
+
from nodes.google._handlers import handle_google_oauth_status
|
|
306
|
+
result = await _call(handle_google_oauth_status, {}, fake_ws)
|
|
284
307
|
assert result["connected"] is False
|
|
285
308
|
assert result["email"] is None
|
|
286
309
|
|
|
287
310
|
async def test_logout_removes_oauth_tokens(self, patched_container, fake_ws):
|
|
311
|
+
from nodes.google._handlers import handle_google_logout
|
|
288
312
|
await patched_container.auth.store_oauth_tokens(
|
|
289
313
|
"google", "access", "refresh", email="user@example.com"
|
|
290
314
|
)
|
|
291
|
-
result = await _call(
|
|
315
|
+
result = await _call(handle_google_logout, {}, fake_ws)
|
|
292
316
|
assert result["success"] is True
|
|
293
317
|
assert await patched_container.auth.get_oauth_tokens("google") is None
|
|
@@ -107,7 +107,11 @@ async def test_fetch_models_uses_native_for_anthropic(ai_service):
|
|
|
107
107
|
models = await ai_service.fetch_models("anthropic", "sk-ant-test")
|
|
108
108
|
|
|
109
109
|
assert models == expected_models
|
|
110
|
-
|
|
110
|
+
# ai.fetch_models forwards `proxy_url` (defaulting to None) to the native
|
|
111
|
+
# factory so the Ollama-pattern proxy override path stays uniform with
|
|
112
|
+
# execute_chat. Asserting the full call signature including the explicit
|
|
113
|
+
# `None` kwarg ensures the proxy path is not silently dropped.
|
|
114
|
+
mock_factory.assert_called_once_with("anthropic", "sk-ant-test", proxy_url=None)
|
|
111
115
|
|
|
112
116
|
|
|
113
117
|
@pytest.mark.asyncio
|
|
@@ -91,33 +91,33 @@ async def _execute_current_time(
|
|
|
91
91
|
|
|
92
92
|
async def _execute_duckduckgo_search(
|
|
93
93
|
args: Dict[str, Any],
|
|
94
|
-
|
|
94
|
+
config: Dict[str, Any] = None,
|
|
95
95
|
) -> Dict[str, Any]:
|
|
96
|
-
"""
|
|
97
|
-
|
|
98
|
-
# Plugin layout under scaling branch: nodes/search/duckduckgo_search.py
|
|
99
|
-
from nodes.search.duckduckgo_search import (
|
|
100
|
-
DuckDuckGoSearchNode,
|
|
101
|
-
DuckDuckGoSearchParams,
|
|
102
|
-
)
|
|
103
|
-
except ImportError:
|
|
104
|
-
return {"error": "duckduckgo search plugin missing"}
|
|
96
|
+
"""Flat (args, config) -> dict shim for the deleted
|
|
97
|
+
``services.handlers.tools._execute_duckduckgo_search``.
|
|
105
98
|
|
|
106
|
-
|
|
107
|
-
|
|
99
|
+
Wave 11.I milestone O moved this here from production code -- it
|
|
100
|
+
only ever served contract tests that patch ``sys.modules['ddgs']``
|
|
101
|
+
and assert the flat output shape. Production callers go through
|
|
102
|
+
:class:`nodes.search.duckduckgo_search.DuckDuckGoSearchNode`.
|
|
103
|
+
"""
|
|
104
|
+
config = config or {}
|
|
105
|
+
query = str(args.get("query", "")).strip()
|
|
106
|
+
if not query:
|
|
108
107
|
return {"error": "No search query provided"}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
108
|
+
max_results = int(config.get("max_results", args.get("max_results", 5)))
|
|
109
|
+
provider = config.get("provider", "duckduckgo")
|
|
110
|
+
from ddgs import DDGS
|
|
111
|
+
raw = list(DDGS().text(query, max_results=max_results))
|
|
112
|
+
results = [
|
|
113
|
+
{
|
|
114
|
+
"title": item.get("title", ""),
|
|
115
|
+
"snippet": item.get("body", ""),
|
|
116
|
+
"url": item.get("href", ""),
|
|
117
|
+
}
|
|
118
|
+
for item in raw
|
|
119
|
+
]
|
|
120
|
+
return {"query": query, "provider": provider, "results": results}
|
|
121
121
|
|
|
122
122
|
|
|
123
123
|
async def handle_write_todos(
|