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
|
@@ -5,9 +5,9 @@ These tests freeze the input -> output behaviour documented in
|
|
|
5
5
|
to the Go `whatsapp-rpc` service via a WebSocket RPC client in
|
|
6
6
|
`routers/whatsapp.py`. We patch the two entry points the handlers use:
|
|
7
7
|
|
|
8
|
-
- `
|
|
9
|
-
- `
|
|
10
|
-
- `
|
|
8
|
+
- `nodes.whatsapp._service.handle_whatsapp_send` - sending messages
|
|
9
|
+
- `nodes.whatsapp._service.whatsapp_rpc_call` - generic RPC calls
|
|
10
|
+
- `nodes.whatsapp._service.handle_whatsapp_chat_history` - chat history RPC
|
|
11
11
|
|
|
12
12
|
For `whatsappReceive` we use the same event-waiter stub pattern as
|
|
13
13
|
`test_telegram_social.py` - patching the module reference imported by both
|
|
@@ -32,15 +32,15 @@ pytestmark = pytest.mark.node_contract
|
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
def _patch_whatsapp_send(return_value):
|
|
35
|
-
"""Patch
|
|
35
|
+
"""Patch nodes.whatsapp._service.handle_whatsapp_send at the import site."""
|
|
36
36
|
return patch(
|
|
37
|
-
"
|
|
37
|
+
"nodes.whatsapp._service.handle_whatsapp_send",
|
|
38
38
|
new=AsyncMock(return_value=return_value),
|
|
39
39
|
)
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
def _patch_rpc_call(return_value=None, *, side_effect=None):
|
|
43
|
-
"""Patch
|
|
43
|
+
"""Patch nodes.whatsapp._service.whatsapp_rpc_call.
|
|
44
44
|
|
|
45
45
|
``return_value`` may be a dict or list. ``side_effect`` may be a callable
|
|
46
46
|
for per-method routing.
|
|
@@ -50,12 +50,12 @@ def _patch_rpc_call(return_value=None, *, side_effect=None):
|
|
|
50
50
|
kwargs["side_effect"] = side_effect
|
|
51
51
|
else:
|
|
52
52
|
kwargs["return_value"] = return_value if return_value is not None else {}
|
|
53
|
-
return patch("
|
|
53
|
+
return patch("nodes.whatsapp._service.whatsapp_rpc_call", new=AsyncMock(**kwargs))
|
|
54
54
|
|
|
55
55
|
|
|
56
56
|
def _patch_chat_history_handler(return_value):
|
|
57
57
|
return patch(
|
|
58
|
-
"
|
|
58
|
+
"nodes.whatsapp._service.handle_whatsapp_chat_history",
|
|
59
59
|
new=AsyncMock(return_value=return_value),
|
|
60
60
|
)
|
|
61
61
|
|
|
@@ -95,7 +95,7 @@ class TestWhatsappSend:
|
|
|
95
95
|
return {"success": True}
|
|
96
96
|
|
|
97
97
|
with patch(
|
|
98
|
-
"
|
|
98
|
+
"nodes.whatsapp._service.handle_whatsapp_send",
|
|
99
99
|
new=AsyncMock(side_effect=fake_send),
|
|
100
100
|
), patch(
|
|
101
101
|
"services.markdown_formatter.to_whatsapp",
|
|
File without changes
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
"""MCP server integration tests.
|
|
2
|
+
|
|
3
|
+
Covers:
|
|
4
|
+
- Bearer-token registry: register/lookup/unregister
|
|
5
|
+
- 401 on missing / malformed / wrong / expired tokens
|
|
6
|
+
- Per-batch scoping: tools see the right `BatchContext`
|
|
7
|
+
- Lockfile format (VSCode-shape) + stale-PID sweep
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import tempfile
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
import pytest
|
|
18
|
+
from httpx import ASGITransport, AsyncClient
|
|
19
|
+
|
|
20
|
+
from services.cli_agent.lockfile import (
|
|
21
|
+
list_active_lockfiles,
|
|
22
|
+
lockfile_path,
|
|
23
|
+
remove_ide_lockfile,
|
|
24
|
+
sweep_stale_lockfiles,
|
|
25
|
+
write_ide_lockfile,
|
|
26
|
+
)
|
|
27
|
+
from services.cli_agent.mcp_server import (
|
|
28
|
+
BatchContext,
|
|
29
|
+
_reset_for_tests,
|
|
30
|
+
active_batch_count,
|
|
31
|
+
get_mcp_app,
|
|
32
|
+
issue_token,
|
|
33
|
+
lookup_batch,
|
|
34
|
+
register_batch,
|
|
35
|
+
unregister_batch,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
# Token registry
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
class TestTokenRegistry:
|
|
44
|
+
def setup_method(self):
|
|
45
|
+
_reset_for_tests()
|
|
46
|
+
|
|
47
|
+
def test_issue_token_is_random_64_hex(self):
|
|
48
|
+
a = issue_token()
|
|
49
|
+
b = issue_token()
|
|
50
|
+
assert a != b
|
|
51
|
+
assert len(a) == 64
|
|
52
|
+
int(a, 16) # parses as hex
|
|
53
|
+
|
|
54
|
+
def test_register_lookup_unregister(self):
|
|
55
|
+
token = issue_token()
|
|
56
|
+
ctx = BatchContext(
|
|
57
|
+
workflow_id="wf", node_id="n",
|
|
58
|
+
workspace_dir=Path("."),
|
|
59
|
+
)
|
|
60
|
+
assert lookup_batch(token) is None
|
|
61
|
+
register_batch(token, ctx)
|
|
62
|
+
assert lookup_batch(token) is ctx
|
|
63
|
+
assert active_batch_count() == 1
|
|
64
|
+
unregister_batch(token)
|
|
65
|
+
assert lookup_batch(token) is None
|
|
66
|
+
assert active_batch_count() == 0
|
|
67
|
+
|
|
68
|
+
def test_unregister_idempotent(self):
|
|
69
|
+
unregister_batch("nonexistent") # should not raise
|
|
70
|
+
|
|
71
|
+
def test_token_collision_with_different_ctx_raises(self):
|
|
72
|
+
token = issue_token()
|
|
73
|
+
ctx1 = BatchContext(workflow_id="a", node_id="n", workspace_dir=Path("."))
|
|
74
|
+
ctx2 = BatchContext(workflow_id="b", node_id="n", workspace_dir=Path("."))
|
|
75
|
+
register_batch(token, ctx1)
|
|
76
|
+
with pytest.raises(ValueError, match="collision"):
|
|
77
|
+
register_batch(token, ctx2)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# ---------------------------------------------------------------------------
|
|
81
|
+
# ASGI auth middleware
|
|
82
|
+
# ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
class TestAuthMiddleware:
|
|
85
|
+
def setup_method(self):
|
|
86
|
+
_reset_for_tests()
|
|
87
|
+
|
|
88
|
+
@pytest.mark.asyncio
|
|
89
|
+
async def test_no_authorization_header_returns_401(self):
|
|
90
|
+
app = get_mcp_app()
|
|
91
|
+
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://t") as c:
|
|
92
|
+
r = await c.post(
|
|
93
|
+
"/mcp/",
|
|
94
|
+
json={"jsonrpc": "2.0", "id": 1, "method": "tools/list"},
|
|
95
|
+
)
|
|
96
|
+
assert r.status_code == 401
|
|
97
|
+
|
|
98
|
+
@pytest.mark.asyncio
|
|
99
|
+
async def test_malformed_authorization_returns_401(self):
|
|
100
|
+
app = get_mcp_app()
|
|
101
|
+
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://t") as c:
|
|
102
|
+
r = await c.post(
|
|
103
|
+
"/mcp/",
|
|
104
|
+
json={"jsonrpc": "2.0", "id": 1, "method": "tools/list"},
|
|
105
|
+
headers={"Authorization": "Token abc"},
|
|
106
|
+
)
|
|
107
|
+
assert r.status_code == 401
|
|
108
|
+
|
|
109
|
+
@pytest.mark.asyncio
|
|
110
|
+
async def test_unknown_token_returns_401(self):
|
|
111
|
+
app = get_mcp_app()
|
|
112
|
+
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://t") as c:
|
|
113
|
+
r = await c.post(
|
|
114
|
+
"/mcp/",
|
|
115
|
+
json={"jsonrpc": "2.0", "id": 1, "method": "tools/list"},
|
|
116
|
+
headers={"Authorization": "Bearer unknown_token"},
|
|
117
|
+
)
|
|
118
|
+
assert r.status_code == 401
|
|
119
|
+
|
|
120
|
+
@pytest.mark.asyncio
|
|
121
|
+
async def test_expired_token_returns_401(self):
|
|
122
|
+
"""Token registered then unregistered behaves as unknown."""
|
|
123
|
+
token = issue_token()
|
|
124
|
+
register_batch(token, BatchContext(
|
|
125
|
+
workflow_id="wf", node_id="n", workspace_dir=Path("."),
|
|
126
|
+
))
|
|
127
|
+
unregister_batch(token)
|
|
128
|
+
app = get_mcp_app()
|
|
129
|
+
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://t") as c:
|
|
130
|
+
r = await c.post(
|
|
131
|
+
"/mcp/",
|
|
132
|
+
json={"jsonrpc": "2.0", "id": 1, "method": "tools/list"},
|
|
133
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
134
|
+
)
|
|
135
|
+
assert r.status_code == 401
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# ---------------------------------------------------------------------------
|
|
139
|
+
# Lockfile format
|
|
140
|
+
# ---------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
class TestLockfile:
|
|
143
|
+
def test_claude_lockfile_path_pid_lock(self, tmp_path):
|
|
144
|
+
path = lockfile_path(
|
|
145
|
+
ide_lockfile_dir=tmp_path,
|
|
146
|
+
pid=12345,
|
|
147
|
+
port=3010,
|
|
148
|
+
ide_name="claude",
|
|
149
|
+
)
|
|
150
|
+
assert path.name == "12345.lock"
|
|
151
|
+
|
|
152
|
+
def test_gemini_lockfile_path_includes_port(self, tmp_path):
|
|
153
|
+
path = lockfile_path(
|
|
154
|
+
ide_lockfile_dir=tmp_path,
|
|
155
|
+
pid=12345,
|
|
156
|
+
port=3010,
|
|
157
|
+
ide_name="gemini",
|
|
158
|
+
)
|
|
159
|
+
assert "12345" in path.name
|
|
160
|
+
assert "3010" in path.name
|
|
161
|
+
assert path.name.endswith(".json")
|
|
162
|
+
|
|
163
|
+
def test_lockfile_payload_matches_vscode_shape(self, tmp_path):
|
|
164
|
+
path = write_ide_lockfile(
|
|
165
|
+
ide_lockfile_dir=tmp_path,
|
|
166
|
+
pid=99999,
|
|
167
|
+
port=3010,
|
|
168
|
+
token="abc123",
|
|
169
|
+
workspace_dir=tmp_path / "ws",
|
|
170
|
+
ide_name="claude",
|
|
171
|
+
)
|
|
172
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
173
|
+
# VSCode-style fields
|
|
174
|
+
assert payload["port"] == 3010
|
|
175
|
+
assert payload["authToken"] == "abc123"
|
|
176
|
+
assert payload["ideName"] == "claude"
|
|
177
|
+
assert "url" in payload
|
|
178
|
+
assert "workspaceFolders" in payload
|
|
179
|
+
assert payload["transport"] == "http"
|
|
180
|
+
# Our extra: pid for the stale-sweep
|
|
181
|
+
assert payload["pid"] == 99999
|
|
182
|
+
|
|
183
|
+
def test_default_url_constructed(self, tmp_path):
|
|
184
|
+
path = write_ide_lockfile(
|
|
185
|
+
ide_lockfile_dir=tmp_path,
|
|
186
|
+
pid=1, port=3010, token="t",
|
|
187
|
+
workspace_dir=tmp_path, ide_name="claude",
|
|
188
|
+
)
|
|
189
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
190
|
+
# FastMCP serves at `/mcp` of the sub-app, mounted at `/mcp/ide`.
|
|
191
|
+
# The lockfile must advertise the absolute JSON-RPC endpoint.
|
|
192
|
+
assert payload["url"] == "http://127.0.0.1:3010/mcp/ide/mcp"
|
|
193
|
+
|
|
194
|
+
def test_remove_lockfile_safe_when_missing(self, tmp_path):
|
|
195
|
+
# Should never raise
|
|
196
|
+
remove_ide_lockfile(None)
|
|
197
|
+
remove_ide_lockfile(tmp_path / "does-not-exist.lock")
|
|
198
|
+
|
|
199
|
+
def test_sweep_removes_dead_pid_lockfile(self, tmp_path):
|
|
200
|
+
# Find a guaranteed-dead PID by walking up high. PID 0 is the
|
|
201
|
+
# system idle process on Windows and the swapper on POSIX, so
|
|
202
|
+
# `psutil.pid_exists(0)` returns True everywhere — DON'T use 0.
|
|
203
|
+
import psutil
|
|
204
|
+
dead_pid = 99_999_999
|
|
205
|
+
while psutil.pid_exists(dead_pid):
|
|
206
|
+
dead_pid += 1
|
|
207
|
+
if dead_pid > 999_999_999: # paranoid bound
|
|
208
|
+
pytest.skip("could not find a dead PID")
|
|
209
|
+
|
|
210
|
+
write_ide_lockfile(
|
|
211
|
+
ide_lockfile_dir=tmp_path, pid=dead_pid, port=3010, token="t",
|
|
212
|
+
workspace_dir=tmp_path, ide_name="claude",
|
|
213
|
+
)
|
|
214
|
+
# Write one with the live PID
|
|
215
|
+
live_path = write_ide_lockfile(
|
|
216
|
+
ide_lockfile_dir=tmp_path, pid=os.getpid(), port=3010, token="t",
|
|
217
|
+
workspace_dir=tmp_path, ide_name="claude",
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
before = list_active_lockfiles(tmp_path)
|
|
221
|
+
assert len(before) >= 2
|
|
222
|
+
|
|
223
|
+
n = sweep_stale_lockfiles(tmp_path)
|
|
224
|
+
# Should have removed at least the dead-PID one
|
|
225
|
+
assert n >= 1
|
|
226
|
+
|
|
227
|
+
after = list_active_lockfiles(tmp_path)
|
|
228
|
+
# Live one survives
|
|
229
|
+
assert live_path in after
|
|
230
|
+
|
|
231
|
+
def test_sweep_safe_on_nonexistent_dir(self, tmp_path):
|
|
232
|
+
assert sweep_stale_lockfiles(tmp_path / "no") == 0
|
|
233
|
+
assert sweep_stale_lockfiles(None) == 0
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
# ---------------------------------------------------------------------------
|
|
237
|
+
# MachinaOs workflow-tool bridge — per-batch dynamic FastMCP exposure.
|
|
238
|
+
# Each wired node lands as `mcp__machinaos__<node_type>`; FastMCP infers
|
|
239
|
+
# the schema from the typed `params` annotation (no custom translation).
|
|
240
|
+
#
|
|
241
|
+
# These tests drive FastMCP's own ``list_tools`` / ``call_tool`` API so
|
|
242
|
+
# we exercise the same code path the spawned ``claude`` CLI hits over
|
|
243
|
+
# JSON-RPC.
|
|
244
|
+
# ---------------------------------------------------------------------------
|
|
245
|
+
|
|
246
|
+
class TestMachinaOsToolBridge:
|
|
247
|
+
def setup_method(self):
|
|
248
|
+
_reset_for_tests()
|
|
249
|
+
import nodes # noqa: F401 — populate plugin registry
|
|
250
|
+
|
|
251
|
+
@staticmethod
|
|
252
|
+
def _ctx(node_type: str, label: str) -> BatchContext:
|
|
253
|
+
return BatchContext(
|
|
254
|
+
workflow_id="wf_test", node_id="claude_code_agent_1",
|
|
255
|
+
workspace_dir=Path("."),
|
|
256
|
+
connected_tools=[{
|
|
257
|
+
"node_id": f"{node_type}_1", "node_type": node_type,
|
|
258
|
+
"label": label, "parameters": {},
|
|
259
|
+
}],
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
@pytest.mark.asyncio
|
|
263
|
+
async def test_register_batch_exposes_tool_on_fastmcp(self):
|
|
264
|
+
"""The connected workflow node must surface in ``list_tools`` so
|
|
265
|
+
claude sees ``mcp__machinaos__<node_type>`` on first
|
|
266
|
+
``tools/list``."""
|
|
267
|
+
from services.cli_agent.mcp_server import _mcp_singleton, get_mcp_app
|
|
268
|
+
get_mcp_app() # ensure FastMCP singleton built
|
|
269
|
+
from services.cli_agent.mcp_server import _mcp_singleton as mcp
|
|
270
|
+
assert mcp is not None
|
|
271
|
+
|
|
272
|
+
ctx = self._ctx("calculatorTool", "Calculator")
|
|
273
|
+
token = issue_token()
|
|
274
|
+
register_batch(token, ctx)
|
|
275
|
+
try:
|
|
276
|
+
tools = await mcp.list_tools()
|
|
277
|
+
names = {t.name for t in tools}
|
|
278
|
+
assert "calculatorTool" in names
|
|
279
|
+
calc = next(t for t in tools if t.name == "calculatorTool")
|
|
280
|
+
# FastMCP infers the inputSchema from the typed `params` arg
|
|
281
|
+
# (Pydantic-v2 → JSON Schema 2020-12 wire format).
|
|
282
|
+
assert {"operation", "a", "b"}.issubset(
|
|
283
|
+
calc.inputSchema["properties"].keys()
|
|
284
|
+
)
|
|
285
|
+
finally:
|
|
286
|
+
unregister_batch(token)
|
|
287
|
+
|
|
288
|
+
# After unregister the tool refcount hits zero and FastMCP drops it.
|
|
289
|
+
tools_after = await mcp.list_tools()
|
|
290
|
+
assert "calculatorTool" not in {t.name for t in tools_after}
|
|
291
|
+
|
|
292
|
+
@pytest.mark.asyncio
|
|
293
|
+
async def test_call_tool_dispatches_via_execute_tool(self):
|
|
294
|
+
"""``mcp.call_tool`` for a wired node routes through
|
|
295
|
+
``services.handlers.tools.execute_tool`` and returns the
|
|
296
|
+
plugin's result envelope."""
|
|
297
|
+
from services.cli_agent.mcp_server import (
|
|
298
|
+
_current_batch, get_mcp_app,
|
|
299
|
+
)
|
|
300
|
+
get_mcp_app()
|
|
301
|
+
from services.cli_agent.mcp_server import _mcp_singleton as mcp
|
|
302
|
+
|
|
303
|
+
ctx = self._ctx("calculatorTool", "Calculator")
|
|
304
|
+
token = issue_token()
|
|
305
|
+
register_batch(token, ctx)
|
|
306
|
+
reset = _current_batch.set(ctx)
|
|
307
|
+
try:
|
|
308
|
+
out = await mcp.call_tool(
|
|
309
|
+
"calculatorTool",
|
|
310
|
+
{"operation": "multiply", "a": 2, "b": 21},
|
|
311
|
+
)
|
|
312
|
+
finally:
|
|
313
|
+
_current_batch.reset(reset)
|
|
314
|
+
unregister_batch(token)
|
|
315
|
+
|
|
316
|
+
# FastMCP's `call_tool` returns either a content sequence or a
|
|
317
|
+
# dict, depending on `structured_output`. Either way the
|
|
318
|
+
# calculator's `result` (42) must be in there.
|
|
319
|
+
text = repr(out)
|
|
320
|
+
assert "42" in text, text
|
|
321
|
+
|
|
322
|
+
@pytest.mark.asyncio
|
|
323
|
+
async def test_unconnected_tool_call_is_blocked_by_batch_scope(self):
|
|
324
|
+
"""If a tool is registered globally (because some other batch
|
|
325
|
+
wired it) but the calling batch didn't, the per-handler
|
|
326
|
+
``_require_batch`` check returns 403."""
|
|
327
|
+
from services.cli_agent.mcp_server import _current_batch, get_mcp_app
|
|
328
|
+
get_mcp_app()
|
|
329
|
+
from services.cli_agent.mcp_server import _mcp_singleton as mcp
|
|
330
|
+
|
|
331
|
+
# Batch A wires both tools — registers them globally.
|
|
332
|
+
token_a = issue_token()
|
|
333
|
+
register_batch(token_a, BatchContext(
|
|
334
|
+
workflow_id="wf_a", node_id="cc_a", workspace_dir=Path("."),
|
|
335
|
+
connected_tools=[
|
|
336
|
+
{"node_id": "c1", "node_type": "calculatorTool", "label": "C", "parameters": {}},
|
|
337
|
+
{"node_id": "h1", "node_type": "httpRequest", "label": "H", "parameters": {}},
|
|
338
|
+
],
|
|
339
|
+
))
|
|
340
|
+
# Batch B wires only one. Even though `httpRequest` is exposed
|
|
341
|
+
# globally (refcount=1), batch B's ctx forbids it.
|
|
342
|
+
ctx_b = self._ctx("calculatorTool", "C")
|
|
343
|
+
token_b = issue_token()
|
|
344
|
+
register_batch(token_b, ctx_b)
|
|
345
|
+
reset = _current_batch.set(ctx_b)
|
|
346
|
+
try:
|
|
347
|
+
out = await mcp.call_tool(
|
|
348
|
+
"httpRequest", {"method": "GET", "url": "x"},
|
|
349
|
+
)
|
|
350
|
+
finally:
|
|
351
|
+
_current_batch.reset(reset)
|
|
352
|
+
unregister_batch(token_b)
|
|
353
|
+
unregister_batch(token_a)
|
|
354
|
+
|
|
355
|
+
text = repr(out)
|
|
356
|
+
assert "403" in text or "not connected" in text, text
|
|
357
|
+
|
|
358
|
+
# -- live, real-tool runs (no mocks) ----------------------------------
|
|
359
|
+
|
|
360
|
+
@pytest.mark.asyncio
|
|
361
|
+
async def test_currentTimeTool_real(self):
|
|
362
|
+
from services.cli_agent.mcp_server import _current_batch, get_mcp_app
|
|
363
|
+
get_mcp_app()
|
|
364
|
+
from services.cli_agent.mcp_server import _mcp_singleton as mcp
|
|
365
|
+
|
|
366
|
+
ctx = self._ctx("currentTimeTool", "Time")
|
|
367
|
+
token = issue_token()
|
|
368
|
+
register_batch(token, ctx)
|
|
369
|
+
reset = _current_batch.set(ctx)
|
|
370
|
+
try:
|
|
371
|
+
tools = {t.name for t in await mcp.list_tools()}
|
|
372
|
+
assert "currentTimeTool" in tools
|
|
373
|
+
out = await mcp.call_tool("currentTimeTool", {"timezone": "UTC"})
|
|
374
|
+
finally:
|
|
375
|
+
_current_batch.reset(reset)
|
|
376
|
+
unregister_batch(token)
|
|
377
|
+
text = repr(out)
|
|
378
|
+
assert "UTC" in text, text
|
|
379
|
+
|
|
380
|
+
@pytest.mark.slow
|
|
381
|
+
@pytest.mark.asyncio
|
|
382
|
+
async def test_duckduckgoSearch_real(self):
|
|
383
|
+
from services.cli_agent.mcp_server import _current_batch, get_mcp_app
|
|
384
|
+
get_mcp_app()
|
|
385
|
+
from services.cli_agent.mcp_server import _mcp_singleton as mcp
|
|
386
|
+
|
|
387
|
+
ctx = self._ctx("duckduckgoSearch", "DDG")
|
|
388
|
+
token = issue_token()
|
|
389
|
+
register_batch(token, ctx)
|
|
390
|
+
reset = _current_batch.set(ctx)
|
|
391
|
+
try:
|
|
392
|
+
try:
|
|
393
|
+
out = await mcp.call_tool(
|
|
394
|
+
"duckduckgoSearch",
|
|
395
|
+
{"query": "MachinaOs Anthropic Claude", "max_results": 3},
|
|
396
|
+
)
|
|
397
|
+
except OSError as exc:
|
|
398
|
+
pytest.skip(f"network unavailable: {exc}")
|
|
399
|
+
finally:
|
|
400
|
+
_current_batch.reset(reset)
|
|
401
|
+
unregister_batch(token)
|
|
402
|
+
text = repr(out)
|
|
403
|
+
if "error" in text and "duckduckgo" in text.lower():
|
|
404
|
+
pytest.skip(f"ddgs unavailable: {text[:200]}")
|
|
405
|
+
assert "duckduckgo" in text.lower(), text[:200]
|
|
406
|
+
|
|
407
|
+
@pytest.mark.slow
|
|
408
|
+
@pytest.mark.asyncio
|
|
409
|
+
async def test_httpRequest_real(self):
|
|
410
|
+
from services.cli_agent.mcp_server import _current_batch, get_mcp_app
|
|
411
|
+
get_mcp_app()
|
|
412
|
+
from services.cli_agent.mcp_server import _mcp_singleton as mcp
|
|
413
|
+
|
|
414
|
+
ctx = self._ctx("httpRequest", "HTTP")
|
|
415
|
+
token = issue_token()
|
|
416
|
+
register_batch(token, ctx)
|
|
417
|
+
reset = _current_batch.set(ctx)
|
|
418
|
+
try:
|
|
419
|
+
try:
|
|
420
|
+
out = await mcp.call_tool(
|
|
421
|
+
"httpRequest",
|
|
422
|
+
{"method": "GET", "url": "https://httpbin.org/get?bridge=ok"},
|
|
423
|
+
)
|
|
424
|
+
except OSError as exc:
|
|
425
|
+
pytest.skip(f"network unavailable: {exc}")
|
|
426
|
+
finally:
|
|
427
|
+
_current_batch.reset(reset)
|
|
428
|
+
unregister_batch(token)
|
|
429
|
+
text = repr(out)
|
|
430
|
+
if '"status": 200' not in text and "'status': 200" not in text:
|
|
431
|
+
pytest.skip(f"httpbin unavailable: {text[:200]}")
|
|
432
|
+
assert "bridge" in text and "ok" in text, text[:300]
|