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
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
"""Contract tests for the agentBuilder multi-op plugin.
|
|
2
|
+
|
|
3
|
+
agentBuilder is a single multi-op tool node exposing 5 canvas-mutation
|
|
4
|
+
operations through the standard `@Operation` plugin pattern (matching
|
|
5
|
+
gmail / calendar / drive). The LLM sees one tool with an `operation`
|
|
6
|
+
discriminator; we test each operation's happy + reject paths directly
|
|
7
|
+
on the node instance, mocking the broadcaster and registry lookups so
|
|
8
|
+
no live state is touched.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from types import SimpleNamespace
|
|
14
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
15
|
+
|
|
16
|
+
import pytest
|
|
17
|
+
|
|
18
|
+
from nodes.tool import agent_builder as ab
|
|
19
|
+
from services.plugin import NodeContext
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
pytestmark = pytest.mark.node_contract
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ============================================================================
|
|
26
|
+
# Helpers
|
|
27
|
+
# ============================================================================
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _make_ctx(
|
|
31
|
+
*,
|
|
32
|
+
nodes=None,
|
|
33
|
+
edges=None,
|
|
34
|
+
workflow_id: str = "wf-test",
|
|
35
|
+
self_id: str = "ab-1",
|
|
36
|
+
) -> NodeContext:
|
|
37
|
+
return NodeContext(
|
|
38
|
+
node_id=self_id,
|
|
39
|
+
node_type="agentBuilder",
|
|
40
|
+
workflow_id=workflow_id,
|
|
41
|
+
nodes=nodes or [],
|
|
42
|
+
edges=edges or [],
|
|
43
|
+
raw={"workflow_id": workflow_id},
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _agent_node(node_id: str, node_type: str = "aiAgent", **params) -> dict:
|
|
48
|
+
return {
|
|
49
|
+
"id": node_id,
|
|
50
|
+
"type": node_type,
|
|
51
|
+
"data": {"label": node_type, "parameters": params},
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _edge(source: str, target: str, target_handle: str = "input-tools") -> dict:
|
|
56
|
+
return {
|
|
57
|
+
"source": source,
|
|
58
|
+
"target": target,
|
|
59
|
+
"sourceHandle": "output-main",
|
|
60
|
+
"targetHandle": target_handle,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _node_class(component_kind: str) -> SimpleNamespace:
|
|
65
|
+
return SimpleNamespace(component_kind=component_kind)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _registry(**type_to_kind) -> dict:
|
|
69
|
+
return {ntype: _node_class(kind) for ntype, kind in type_to_kind.items()}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ============================================================================
|
|
73
|
+
# Plugin registration
|
|
74
|
+
# ============================================================================
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class TestRegistration:
|
|
78
|
+
def test_node_is_registered(self):
|
|
79
|
+
from services.node_registry import get_node_class
|
|
80
|
+
cls = get_node_class("agentBuilder")
|
|
81
|
+
assert cls is ab.AgentBuilderNode
|
|
82
|
+
|
|
83
|
+
def test_has_five_operations(self):
|
|
84
|
+
ops = set(ab.AgentBuilderNode._operations.keys())
|
|
85
|
+
assert ops == {
|
|
86
|
+
"inspect_canvas", "add_tool", "add_skill",
|
|
87
|
+
"add_subagent", "create_workflow",
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
def test_default_operation_is_inspect_canvas(self):
|
|
91
|
+
params = ab.AgentBuilderParams()
|
|
92
|
+
assert params.operation == "inspect_canvas"
|
|
93
|
+
|
|
94
|
+
def test_is_tool_node(self):
|
|
95
|
+
"""agentBuilder is a tool-only plugin; subclasses ToolNode (not
|
|
96
|
+
ActionNode + usable_as_tool=True). The ToolNode base IS the
|
|
97
|
+
contract for nodes that exist solely to be invoked by an LLM
|
|
98
|
+
through an agent's input-tools handle."""
|
|
99
|
+
from services.plugin import ToolNode
|
|
100
|
+
assert issubclass(ab.AgentBuilderNode, ToolNode)
|
|
101
|
+
|
|
102
|
+
def test_handle_topology_matches_canonical_tool_shape(self):
|
|
103
|
+
"""Canonical tool node: input-main left + output-tool top role=tools.
|
|
104
|
+
The output-tool shape is what wires into an agent's input-tools handle."""
|
|
105
|
+
handles = list(ab.AgentBuilderNode.handles)
|
|
106
|
+
assert len(handles) == 2
|
|
107
|
+
names = {h["name"] for h in handles}
|
|
108
|
+
assert names == {"input-main", "output-tool"}
|
|
109
|
+
out = next(h for h in handles if h["name"] == "output-tool")
|
|
110
|
+
assert out["kind"] == "output"
|
|
111
|
+
assert out["position"] == "top"
|
|
112
|
+
assert out["role"] == "tools"
|
|
113
|
+
|
|
114
|
+
def test_no_provided_tools_dict(self):
|
|
115
|
+
"""Sanity check: the tribal PROVIDED_TOOLS attr from the original
|
|
116
|
+
design must NOT exist on the multi-op rewrite."""
|
|
117
|
+
assert not hasattr(ab.AgentBuilderNode, "PROVIDED_TOOLS")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ============================================================================
|
|
121
|
+
# inspect_canvas
|
|
122
|
+
# ============================================================================
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class TestInspectCanvas:
|
|
126
|
+
async def test_returns_node_and_edge_summaries(self):
|
|
127
|
+
nodes = [
|
|
128
|
+
_agent_node("agent-1"),
|
|
129
|
+
{"id": "ab-1", "type": "agentBuilder", "data": {"label": "AB"}},
|
|
130
|
+
{"id": "tool-1", "type": "httpRequest",
|
|
131
|
+
"data": {"label": "HTTP", "parameters": {"url": "https://x"}}},
|
|
132
|
+
]
|
|
133
|
+
edges = [
|
|
134
|
+
_edge("ab-1", "agent-1", "input-tools"),
|
|
135
|
+
_edge("tool-1", "agent-1", "input-tools"),
|
|
136
|
+
]
|
|
137
|
+
node = ab.AgentBuilderNode()
|
|
138
|
+
ctx = _make_ctx(nodes=nodes, edges=edges)
|
|
139
|
+
params = ab.AgentBuilderParams(operation="inspect_canvas")
|
|
140
|
+
|
|
141
|
+
result = await node.inspect_canvas(ctx, params)
|
|
142
|
+
|
|
143
|
+
assert result.operation == "inspect_canvas"
|
|
144
|
+
assert len(result.nodes) == 3
|
|
145
|
+
assert len(result.edges) == 2
|
|
146
|
+
assert result.you["node_id"] == "agent-1" # caller resolved via input-tools edge
|
|
147
|
+
# Both agentBuilder and httpRequest are wired to agent-1's input-tools.
|
|
148
|
+
assert "2 tool(s) wired" in result.summary
|
|
149
|
+
assert "httpRequest" in result.summary
|
|
150
|
+
|
|
151
|
+
async def test_falls_back_to_self_when_no_caller_wired(self):
|
|
152
|
+
node = ab.AgentBuilderNode()
|
|
153
|
+
ctx = _make_ctx(nodes=[], edges=[], self_id="ab-1")
|
|
154
|
+
params = ab.AgentBuilderParams(operation="inspect_canvas")
|
|
155
|
+
|
|
156
|
+
result = await node.inspect_canvas(ctx, params)
|
|
157
|
+
|
|
158
|
+
assert result.you["node_id"] == "ab-1"
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# ============================================================================
|
|
162
|
+
# add_tool
|
|
163
|
+
# ============================================================================
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class TestAddTool:
|
|
167
|
+
async def test_rejects_empty_node_type(self):
|
|
168
|
+
node = ab.AgentBuilderNode()
|
|
169
|
+
ctx = _make_ctx()
|
|
170
|
+
params = ab.AgentBuilderParams(operation="add_tool", node_type="")
|
|
171
|
+
|
|
172
|
+
result = await node.add_tool(ctx, params)
|
|
173
|
+
|
|
174
|
+
assert result.operations == []
|
|
175
|
+
assert "node_type is required" in result.summary
|
|
176
|
+
|
|
177
|
+
async def test_rejects_disallowed_type(self):
|
|
178
|
+
node = ab.AgentBuilderNode()
|
|
179
|
+
ctx = _make_ctx()
|
|
180
|
+
params = ab.AgentBuilderParams(operation="add_tool", node_type="unknownTool")
|
|
181
|
+
|
|
182
|
+
with patch.object(ab, "registered_node_classes",
|
|
183
|
+
return_value=_registry(httpRequest="tool")):
|
|
184
|
+
result = await node.add_tool(ctx, params)
|
|
185
|
+
|
|
186
|
+
assert result.operations == []
|
|
187
|
+
assert "not an allowed tool type" in result.summary
|
|
188
|
+
|
|
189
|
+
async def test_rejects_self_and_master_skill(self):
|
|
190
|
+
node = ab.AgentBuilderNode()
|
|
191
|
+
ctx = _make_ctx()
|
|
192
|
+
|
|
193
|
+
with patch.object(ab, "registered_node_classes",
|
|
194
|
+
return_value=_registry(
|
|
195
|
+
agentBuilder="tool", masterSkill="tool",
|
|
196
|
+
httpRequest="tool")):
|
|
197
|
+
for forbidden in ("agentBuilder", "masterSkill"):
|
|
198
|
+
params = ab.AgentBuilderParams(operation="add_tool", node_type=forbidden)
|
|
199
|
+
result = await node.add_tool(ctx, params)
|
|
200
|
+
assert result.operations == []
|
|
201
|
+
assert "not an allowed tool type" in result.summary
|
|
202
|
+
|
|
203
|
+
async def test_emits_add_node_and_add_edge_ops(self):
|
|
204
|
+
node = ab.AgentBuilderNode()
|
|
205
|
+
edges = [_edge("ab-1", "agent-1", "input-tools")]
|
|
206
|
+
ctx = _make_ctx(edges=edges)
|
|
207
|
+
params = ab.AgentBuilderParams(operation="add_tool", node_type="httpRequest")
|
|
208
|
+
|
|
209
|
+
with patch.object(ab, "registered_node_classes",
|
|
210
|
+
return_value=_registry(httpRequest="tool")), \
|
|
211
|
+
patch.object(ab, "_broadcast", new_callable=AsyncMock) as mock_bcast:
|
|
212
|
+
result = await node.add_tool(ctx, params)
|
|
213
|
+
|
|
214
|
+
assert len(result.operations) == 2
|
|
215
|
+
assert result.operations[0]["type"] == "add_node"
|
|
216
|
+
assert result.operations[0]["node_type"] == "httpRequest"
|
|
217
|
+
assert result.operations[1]["type"] == "add_edge"
|
|
218
|
+
assert result.operations[1]["target"] == "agent-1"
|
|
219
|
+
assert result.operations[1]["target_handle"] == "input-tools"
|
|
220
|
+
mock_bcast.assert_awaited_once()
|
|
221
|
+
assert "Available on your next turn" in result.summary
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# ============================================================================
|
|
225
|
+
# add_skill
|
|
226
|
+
# ============================================================================
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class TestAddSkill:
|
|
230
|
+
async def test_rejects_empty_skill_folder(self):
|
|
231
|
+
node = ab.AgentBuilderNode()
|
|
232
|
+
ctx = _make_ctx()
|
|
233
|
+
params = ab.AgentBuilderParams(operation="add_skill", skill_folder="")
|
|
234
|
+
|
|
235
|
+
result = await node.add_skill(ctx, params)
|
|
236
|
+
|
|
237
|
+
assert result.operations == []
|
|
238
|
+
assert "skill_folder is required" in result.summary
|
|
239
|
+
|
|
240
|
+
async def test_rejects_unknown_skill_folder(self):
|
|
241
|
+
node = ab.AgentBuilderNode()
|
|
242
|
+
ctx = _make_ctx()
|
|
243
|
+
params = ab.AgentBuilderParams(
|
|
244
|
+
operation="add_skill", skill_folder="nonexistent-skill",
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
with patch.object(ab, "_skill_folder_exists", return_value=False):
|
|
248
|
+
result = await node.add_skill(ctx, params)
|
|
249
|
+
|
|
250
|
+
assert result.operations == []
|
|
251
|
+
assert "not found" in result.summary
|
|
252
|
+
|
|
253
|
+
async def test_toggles_on_existing_master_skill(self):
|
|
254
|
+
master = {
|
|
255
|
+
"id": "ms-1", "type": "masterSkill",
|
|
256
|
+
"data": {"parameters": {"skills_config": {"old-skill": {"enabled": True}}}},
|
|
257
|
+
}
|
|
258
|
+
agent = _agent_node("agent-1")
|
|
259
|
+
edges = [
|
|
260
|
+
_edge("ab-1", "agent-1", "input-tools"),
|
|
261
|
+
_edge("ms-1", "agent-1", "input-skill"),
|
|
262
|
+
]
|
|
263
|
+
node = ab.AgentBuilderNode()
|
|
264
|
+
ctx = _make_ctx(nodes=[agent, master], edges=edges)
|
|
265
|
+
params = ab.AgentBuilderParams(
|
|
266
|
+
operation="add_skill", skill_folder="http-request-skill",
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
with patch.object(ab, "_skill_folder_exists", return_value=True), \
|
|
270
|
+
patch.object(ab, "_broadcast", new_callable=AsyncMock):
|
|
271
|
+
result = await node.add_skill(ctx, params)
|
|
272
|
+
|
|
273
|
+
assert len(result.operations) == 1
|
|
274
|
+
op = result.operations[0]
|
|
275
|
+
assert op["type"] == "set_node_parameters"
|
|
276
|
+
assert op["node_id"] == "ms-1"
|
|
277
|
+
new_cfg = op["parameters"]["skills_config"]
|
|
278
|
+
assert new_cfg["http-request-skill"]["enabled"] is True
|
|
279
|
+
assert new_cfg["old-skill"]["enabled"] is True # preserved
|
|
280
|
+
|
|
281
|
+
async def test_spawns_master_skill_when_absent(self):
|
|
282
|
+
agent = _agent_node("agent-1")
|
|
283
|
+
edges = [_edge("ab-1", "agent-1", "input-tools")]
|
|
284
|
+
node = ab.AgentBuilderNode()
|
|
285
|
+
ctx = _make_ctx(nodes=[agent], edges=edges)
|
|
286
|
+
params = ab.AgentBuilderParams(
|
|
287
|
+
operation="add_skill", skill_folder="memory-skill",
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
with patch.object(ab, "_skill_folder_exists", return_value=True), \
|
|
291
|
+
patch.object(ab, "_broadcast", new_callable=AsyncMock):
|
|
292
|
+
result = await node.add_skill(ctx, params)
|
|
293
|
+
|
|
294
|
+
assert len(result.operations) == 2
|
|
295
|
+
assert result.operations[0]["type"] == "add_node"
|
|
296
|
+
assert result.operations[0]["node_type"] == "masterSkill"
|
|
297
|
+
seeded = result.operations[0]["parameters"]["skills_config"]
|
|
298
|
+
assert seeded["memory-skill"]["enabled"] is True
|
|
299
|
+
assert result.operations[1]["type"] == "add_edge"
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
# ============================================================================
|
|
303
|
+
# add_subagent
|
|
304
|
+
# ============================================================================
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
class TestAddSubagent:
|
|
308
|
+
async def test_rejects_empty_agent_type(self):
|
|
309
|
+
node = ab.AgentBuilderNode()
|
|
310
|
+
ctx = _make_ctx()
|
|
311
|
+
params = ab.AgentBuilderParams(operation="add_subagent", agent_type="")
|
|
312
|
+
|
|
313
|
+
result = await node.add_subagent(ctx, params)
|
|
314
|
+
|
|
315
|
+
assert result.operations == []
|
|
316
|
+
assert "agent_type is required" in result.summary
|
|
317
|
+
|
|
318
|
+
async def test_rejects_when_caller_is_not_team_lead(self):
|
|
319
|
+
agent = _agent_node("agent-1", "aiAgent") # aiAgent is NOT a team-lead
|
|
320
|
+
edges = [_edge("ab-1", "agent-1", "input-tools")]
|
|
321
|
+
node = ab.AgentBuilderNode()
|
|
322
|
+
ctx = _make_ctx(nodes=[agent], edges=edges)
|
|
323
|
+
params = ab.AgentBuilderParams(
|
|
324
|
+
operation="add_subagent", agent_type="coding_agent",
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
with patch.object(ab, "registered_node_classes",
|
|
328
|
+
return_value=_registry(coding_agent="agent")):
|
|
329
|
+
result = await node.add_subagent(ctx, params)
|
|
330
|
+
|
|
331
|
+
assert result.operations == []
|
|
332
|
+
assert "team-lead" in result.summary
|
|
333
|
+
|
|
334
|
+
async def test_rejects_disallowed_agent_type(self):
|
|
335
|
+
agent = _agent_node("agent-1", "orchestrator_agent")
|
|
336
|
+
edges = [_edge("ab-1", "agent-1", "input-tools")]
|
|
337
|
+
node = ab.AgentBuilderNode()
|
|
338
|
+
ctx = _make_ctx(nodes=[agent], edges=edges)
|
|
339
|
+
params = ab.AgentBuilderParams(
|
|
340
|
+
operation="add_subagent", agent_type="not_a_real_agent",
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
with patch.object(ab, "registered_node_classes",
|
|
344
|
+
return_value=_registry(coding_agent="agent")):
|
|
345
|
+
result = await node.add_subagent(ctx, params)
|
|
346
|
+
|
|
347
|
+
assert result.operations == []
|
|
348
|
+
assert "not an allowed agent type" in result.summary
|
|
349
|
+
|
|
350
|
+
async def test_rejects_spawning_another_team_lead(self):
|
|
351
|
+
agent = _agent_node("agent-1", "orchestrator_agent")
|
|
352
|
+
edges = [_edge("ab-1", "agent-1", "input-tools")]
|
|
353
|
+
node = ab.AgentBuilderNode()
|
|
354
|
+
ctx = _make_ctx(nodes=[agent], edges=edges)
|
|
355
|
+
params = ab.AgentBuilderParams(
|
|
356
|
+
operation="add_subagent", agent_type="ai_employee",
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
with patch.object(ab, "registered_node_classes",
|
|
360
|
+
return_value=_registry(ai_employee="agent")):
|
|
361
|
+
result = await node.add_subagent(ctx, params)
|
|
362
|
+
|
|
363
|
+
assert result.operations == []
|
|
364
|
+
assert "cannot spawn another team-lead" in result.summary
|
|
365
|
+
|
|
366
|
+
async def test_emits_add_node_and_teammate_edge(self):
|
|
367
|
+
agent = _agent_node("agent-1", "orchestrator_agent")
|
|
368
|
+
edges = [_edge("ab-1", "agent-1", "input-tools")]
|
|
369
|
+
node = ab.AgentBuilderNode()
|
|
370
|
+
ctx = _make_ctx(nodes=[agent], edges=edges)
|
|
371
|
+
params = ab.AgentBuilderParams(
|
|
372
|
+
operation="add_subagent", agent_type="coding_agent",
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
with patch.object(ab, "registered_node_classes",
|
|
376
|
+
return_value=_registry(coding_agent="agent")), \
|
|
377
|
+
patch.object(ab, "_broadcast", new_callable=AsyncMock):
|
|
378
|
+
result = await node.add_subagent(ctx, params)
|
|
379
|
+
|
|
380
|
+
assert len(result.operations) == 2
|
|
381
|
+
assert result.operations[0]["node_type"] == "coding_agent"
|
|
382
|
+
assert result.operations[1]["target_handle"] == "input-teammates"
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
# ============================================================================
|
|
386
|
+
# create_workflow
|
|
387
|
+
# ============================================================================
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
class TestCreateWorkflow:
|
|
391
|
+
async def test_rejects_empty_name(self):
|
|
392
|
+
node = ab.AgentBuilderNode()
|
|
393
|
+
ctx = _make_ctx()
|
|
394
|
+
params = ab.AgentBuilderParams(operation="create_workflow", workflow_name="")
|
|
395
|
+
|
|
396
|
+
result = await node.create_workflow(ctx, params)
|
|
397
|
+
|
|
398
|
+
assert result.workflow_id is None
|
|
399
|
+
assert "workflow_name is required" in result.summary
|
|
400
|
+
|
|
401
|
+
async def test_persists_via_database_and_returns_id(self):
|
|
402
|
+
node = ab.AgentBuilderNode()
|
|
403
|
+
ctx = _make_ctx()
|
|
404
|
+
params = ab.AgentBuilderParams(
|
|
405
|
+
operation="create_workflow",
|
|
406
|
+
workflow_name="My New Workflow",
|
|
407
|
+
workflow_description="An optional description",
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
mock_db = MagicMock()
|
|
411
|
+
mock_db.save_workflow = AsyncMock(return_value=True)
|
|
412
|
+
mock_container = MagicMock()
|
|
413
|
+
mock_container.database.return_value = mock_db
|
|
414
|
+
|
|
415
|
+
with patch("core.container.container", mock_container):
|
|
416
|
+
result = await node.create_workflow(ctx, params)
|
|
417
|
+
|
|
418
|
+
assert result.workflow_id is not None
|
|
419
|
+
assert result.workflow_id.startswith("wf_")
|
|
420
|
+
assert "My New Workflow" in result.summary
|
|
421
|
+
mock_db.save_workflow.assert_awaited_once()
|
|
422
|
+
|
|
423
|
+
async def test_returns_failure_summary_when_persist_fails(self):
|
|
424
|
+
node = ab.AgentBuilderNode()
|
|
425
|
+
ctx = _make_ctx()
|
|
426
|
+
params = ab.AgentBuilderParams(
|
|
427
|
+
operation="create_workflow", workflow_name="Doomed Workflow",
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
mock_db = MagicMock()
|
|
431
|
+
mock_db.save_workflow = AsyncMock(return_value=False)
|
|
432
|
+
mock_container = MagicMock()
|
|
433
|
+
mock_container.database.return_value = mock_db
|
|
434
|
+
|
|
435
|
+
with patch("core.container.container", mock_container):
|
|
436
|
+
result = await node.create_workflow(ctx, params)
|
|
437
|
+
|
|
438
|
+
assert result.workflow_id is None
|
|
439
|
+
assert "failed to persist" in result.summary
|
|
@@ -180,8 +180,9 @@ class TestDuckDuckGoSearch:
|
|
|
180
180
|
fake_module = MagicMock()
|
|
181
181
|
fake_module.DDGS = fake_ddgs_class
|
|
182
182
|
|
|
183
|
+
from tests.nodes._compat import _execute_duckduckgo_search as _flat_search
|
|
183
184
|
with patch.dict("sys.modules", {"ddgs": fake_module}):
|
|
184
|
-
result = await
|
|
185
|
+
result = await _flat_search(
|
|
185
186
|
{"query": "python"},
|
|
186
187
|
{"provider": "duckduckgo", "max_results": 2},
|
|
187
188
|
)
|
|
@@ -201,7 +202,7 @@ class TestDuckDuckGoSearch:
|
|
|
201
202
|
assert result == {"error": "No search query provided"}
|
|
202
203
|
|
|
203
204
|
async def test_missing_keys_in_ddgs_output_default_to_empty_string(self):
|
|
204
|
-
from
|
|
205
|
+
from tests.nodes._compat import _execute_duckduckgo_search as _flat_search
|
|
205
206
|
|
|
206
207
|
fake_ddgs_instance = MagicMock()
|
|
207
208
|
fake_ddgs_instance.text.return_value = [{"title": "only-title"}]
|
|
@@ -209,15 +210,12 @@ class TestDuckDuckGoSearch:
|
|
|
209
210
|
fake_module.DDGS = MagicMock(return_value=fake_ddgs_instance)
|
|
210
211
|
|
|
211
212
|
with patch.dict("sys.modules", {"ddgs": fake_module}):
|
|
212
|
-
result = await
|
|
213
|
-
{"query": "q"},
|
|
214
|
-
{"max_results": 5},
|
|
215
|
-
)
|
|
213
|
+
result = await _flat_search({"query": "q"}, {"max_results": 5})
|
|
216
214
|
|
|
217
215
|
assert result["results"] == [{"title": "only-title", "snippet": "", "url": ""}]
|
|
218
216
|
|
|
219
217
|
async def test_max_results_defaults_to_5_when_missing(self):
|
|
220
|
-
from
|
|
218
|
+
from tests.nodes._compat import _execute_duckduckgo_search as _flat_search
|
|
221
219
|
|
|
222
220
|
fake_ddgs_instance = MagicMock()
|
|
223
221
|
fake_ddgs_instance.text.return_value = []
|
|
@@ -225,7 +223,7 @@ class TestDuckDuckGoSearch:
|
|
|
225
223
|
fake_module.DDGS = MagicMock(return_value=fake_ddgs_instance)
|
|
226
224
|
|
|
227
225
|
with patch.dict("sys.modules", {"ddgs": fake_module}):
|
|
228
|
-
await
|
|
226
|
+
await _flat_search({"query": "q"}, {})
|
|
229
227
|
|
|
230
228
|
fake_ddgs_instance.text.assert_called_once_with("q", max_results=5)
|
|
231
229
|
|
|
@@ -269,7 +267,8 @@ class TestTaskManager:
|
|
|
269
267
|
"result": "x" * 500, # will be truncated to 200 chars
|
|
270
268
|
}
|
|
271
269
|
|
|
272
|
-
|
|
270
|
+
from nodes.tool.task_manager import _execute_task_manager
|
|
271
|
+
result = await _execute_task_manager(
|
|
273
272
|
{"operation": "list_tasks"}, {"parameters": {}}
|
|
274
273
|
)
|
|
275
274
|
|
|
@@ -294,7 +293,8 @@ class TestTaskManager:
|
|
|
294
293
|
tools_mod._delegated_tasks["r1"] = running
|
|
295
294
|
tools_mod._delegation_results["c1"] = {"status": "completed"}
|
|
296
295
|
|
|
297
|
-
|
|
296
|
+
from nodes.tool.task_manager import _execute_task_manager
|
|
297
|
+
result = await _execute_task_manager(
|
|
298
298
|
{"operation": "list_tasks", "status_filter": "running"},
|
|
299
299
|
{"parameters": {}},
|
|
300
300
|
)
|
|
@@ -305,7 +305,8 @@ class TestTaskManager:
|
|
|
305
305
|
async def test_get_task_without_id_errors(self, _reset_registry):
|
|
306
306
|
tools_mod = _reset_registry
|
|
307
307
|
|
|
308
|
-
|
|
308
|
+
from nodes.tool.task_manager import _execute_task_manager
|
|
309
|
+
result = await _execute_task_manager(
|
|
309
310
|
{"operation": "get_task"}, {"parameters": {}}
|
|
310
311
|
)
|
|
311
312
|
assert result["success"] is False
|
|
@@ -315,7 +316,8 @@ class TestTaskManager:
|
|
|
315
316
|
tools_mod = _reset_registry
|
|
316
317
|
tools_mod._delegation_results["gone"] = {"status": "completed"}
|
|
317
318
|
|
|
318
|
-
|
|
319
|
+
from nodes.tool.task_manager import _execute_task_manager
|
|
320
|
+
result = await _execute_task_manager(
|
|
319
321
|
{"operation": "mark_done", "task_id": "gone"},
|
|
320
322
|
{"parameters": {}},
|
|
321
323
|
)
|
|
@@ -327,7 +329,8 @@ class TestTaskManager:
|
|
|
327
329
|
async def test_mark_done_untracked_id_returns_removed_false(self, _reset_registry):
|
|
328
330
|
tools_mod = _reset_registry
|
|
329
331
|
|
|
330
|
-
|
|
332
|
+
from nodes.tool.task_manager import _execute_task_manager
|
|
333
|
+
result = await _execute_task_manager(
|
|
331
334
|
{"operation": "mark_done", "task_id": "never-seen"},
|
|
332
335
|
{"parameters": {}},
|
|
333
336
|
)
|
|
@@ -339,7 +342,8 @@ class TestTaskManager:
|
|
|
339
342
|
async def test_unknown_operation_returns_failure_envelope(self, _reset_registry):
|
|
340
343
|
tools_mod = _reset_registry
|
|
341
344
|
|
|
342
|
-
|
|
345
|
+
from nodes.tool.task_manager import _execute_task_manager
|
|
346
|
+
result = await _execute_task_manager(
|
|
343
347
|
{"operation": "self_destruct"}, {"parameters": {}}
|
|
344
348
|
)
|
|
345
349
|
assert result["success"] is False
|
|
@@ -313,8 +313,10 @@ class TestFileRead:
|
|
|
313
313
|
harness.assert_output_shape(result, ["content", "file_path"])
|
|
314
314
|
payload = result["result"]
|
|
315
315
|
assert payload["content"] == "line1\nline2\n"
|
|
316
|
-
|
|
317
|
-
|
|
316
|
+
# ``normalize_virtual_path`` prepends ``/`` to relative inputs so the
|
|
317
|
+
# path reaches deepagents in its canonical virtual-mode form.
|
|
318
|
+
assert payload["file_path"] == "/notes.txt"
|
|
319
|
+
backend.read.assert_called_once_with("/notes.txt", offset=0, limit=100)
|
|
318
320
|
|
|
319
321
|
async def test_missing_file_path_short_circuits(self, harness):
|
|
320
322
|
# No backend patch: we should short-circuit before reaching it.
|
|
@@ -333,7 +335,10 @@ class TestFileRead:
|
|
|
333
335
|
)
|
|
334
336
|
|
|
335
337
|
harness.assert_envelope(result, success=False)
|
|
336
|
-
|
|
338
|
+
# ``normalize_virtual_path`` rejects ``..`` segments before the
|
|
339
|
+
# backend is even called, so the error comes from deepagents'
|
|
340
|
+
# ``validate_path`` helper, not the (unreached) FileNotFoundError.
|
|
341
|
+
assert "path traversal not allowed" in result["error"].lower()
|
|
337
342
|
|
|
338
343
|
|
|
339
344
|
# ============================================================================
|
|
@@ -344,7 +349,11 @@ class TestFileRead:
|
|
|
344
349
|
class TestFileModify:
|
|
345
350
|
async def test_write_happy_path(self, harness):
|
|
346
351
|
backend = MagicMock(name="LocalShellBackend")
|
|
347
|
-
backend.write = MagicMock(return_value=_FakeWriteResult(path="hello.txt"))
|
|
352
|
+
backend.write = MagicMock(return_value=_FakeWriteResult(path="/hello.txt"))
|
|
353
|
+
# ``write`` now does a pre-flight ``backend._resolve_path(...).exists()``
|
|
354
|
+
# check so it can unlink-and-replace; teach the mock that the target
|
|
355
|
+
# doesn't exist yet so the wholesale-write path proceeds.
|
|
356
|
+
backend._resolve_path.return_value.exists.return_value = False
|
|
348
357
|
|
|
349
358
|
with _patch_fs_backend(backend):
|
|
350
359
|
result = await harness.execute(
|
|
@@ -359,8 +368,8 @@ class TestFileModify:
|
|
|
359
368
|
harness.assert_envelope(result, success=True)
|
|
360
369
|
harness.assert_output_shape(result, ["operation", "file_path"])
|
|
361
370
|
assert result["result"]["operation"] == "write"
|
|
362
|
-
assert result["result"]["file_path"] == "hello.txt"
|
|
363
|
-
backend.write.assert_called_once_with("hello.txt", "hi there")
|
|
371
|
+
assert result["result"]["file_path"] == "/hello.txt"
|
|
372
|
+
backend.write.assert_called_once_with("/hello.txt", "hi there")
|
|
364
373
|
|
|
365
374
|
async def test_edit_happy_path_returns_occurrences(self, harness):
|
|
366
375
|
backend = MagicMock(name="LocalShellBackend")
|
|
@@ -386,7 +395,7 @@ class TestFileModify:
|
|
|
386
395
|
)
|
|
387
396
|
assert result["result"]["occurrences"] == 3
|
|
388
397
|
backend.edit.assert_called_once_with(
|
|
389
|
-
"README.md", "foo", "bar", replace_all=True
|
|
398
|
+
"/README.md", "foo", "bar", replace_all=True
|
|
390
399
|
)
|
|
391
400
|
|
|
392
401
|
async def test_edit_missing_old_string_short_circuits(self, harness):
|
|
@@ -559,7 +568,7 @@ class TestFsSearch:
|
|
|
559
568
|
result, ["path", "pattern", "matches", "count"]
|
|
560
569
|
)
|
|
561
570
|
assert result["result"]["count"] == 1
|
|
562
|
-
backend.glob_info.assert_called_once_with("**/*.py", path="src")
|
|
571
|
+
backend.glob_info.assert_called_once_with("**/*.py", path="/src")
|
|
563
572
|
|
|
564
573
|
async def test_grep_returns_string_error_as_error_envelope(self, harness):
|
|
565
574
|
# Per doc: grep_raw returns a str on error, list on success.
|