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,116 @@
|
|
|
1
|
+
"""Unit tests for :mod:`services.plugin.ws` + :mod:`services.plugin.deps`.
|
|
2
|
+
|
|
3
|
+
Locks the contract for the two T-residual helpers added in Wave 11.I:
|
|
4
|
+
|
|
5
|
+
* :func:`services.plugin.ws.ws_response` -- exception-to-envelope wrapper
|
|
6
|
+
with ``NodeUserError`` carve-out.
|
|
7
|
+
* :func:`services.plugin.deps.get_auth_service` /
|
|
8
|
+
``get_database`` / ``get_cache`` -- NOT memoised; re-resolve on
|
|
9
|
+
every call so test container overrides take effect.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from unittest.mock import MagicMock, patch
|
|
15
|
+
|
|
16
|
+
import pytest
|
|
17
|
+
|
|
18
|
+
from services.plugin.base import NodeUserError
|
|
19
|
+
from services.plugin.deps import get_auth_service, get_cache, get_database
|
|
20
|
+
from services.plugin.ws import ws_response
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# ----------------------------------------------------------------------------
|
|
24
|
+
# ws_response
|
|
25
|
+
# ----------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TestWsResponse:
|
|
29
|
+
"""``@ws_response`` -- opt-in exception-to-envelope decorator."""
|
|
30
|
+
|
|
31
|
+
async def test_success_path_passes_through_unchanged(self):
|
|
32
|
+
@ws_response
|
|
33
|
+
async def handler(data, websocket):
|
|
34
|
+
return {"success": True, "result": "ok", "extra": 42}
|
|
35
|
+
|
|
36
|
+
out = await handler({}, MagicMock())
|
|
37
|
+
assert out == {"success": True, "result": "ok", "extra": 42}
|
|
38
|
+
|
|
39
|
+
async def test_unexpected_exception_returns_error_envelope(self):
|
|
40
|
+
@ws_response
|
|
41
|
+
async def handler(data, websocket):
|
|
42
|
+
raise RuntimeError("server bug")
|
|
43
|
+
|
|
44
|
+
out = await handler({}, MagicMock())
|
|
45
|
+
assert out == {"success": False, "error": "server bug"}
|
|
46
|
+
|
|
47
|
+
async def test_node_user_error_returns_error_envelope(self):
|
|
48
|
+
@ws_response
|
|
49
|
+
async def handler(data, websocket):
|
|
50
|
+
raise NodeUserError("missing required field")
|
|
51
|
+
|
|
52
|
+
out = await handler({}, MagicMock())
|
|
53
|
+
assert out == {"success": False, "error": "missing required field"}
|
|
54
|
+
|
|
55
|
+
async def test_decorator_preserves_handler_metadata(self):
|
|
56
|
+
@ws_response
|
|
57
|
+
async def documented_handler(data, websocket):
|
|
58
|
+
"""Original docstring."""
|
|
59
|
+
return {"success": True}
|
|
60
|
+
|
|
61
|
+
assert documented_handler.__name__ == "documented_handler"
|
|
62
|
+
assert documented_handler.__doc__ == "Original docstring."
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ----------------------------------------------------------------------------
|
|
66
|
+
# deps -- get_auth_service / get_database / get_cache
|
|
67
|
+
# ----------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class TestLazyDependencyHelpers:
|
|
71
|
+
"""The lazy DI helpers MUST NOT memoise -- test fixtures rely on
|
|
72
|
+
call-time container resolution to swap singletons mid-test."""
|
|
73
|
+
|
|
74
|
+
def test_get_auth_service_returns_container_singleton(self):
|
|
75
|
+
fake_auth = MagicMock(name="auth_service_singleton")
|
|
76
|
+
with patch("core.container.container") as fake_container:
|
|
77
|
+
fake_container.auth_service.return_value = fake_auth
|
|
78
|
+
assert get_auth_service() is fake_auth
|
|
79
|
+
|
|
80
|
+
def test_get_database_returns_container_singleton(self):
|
|
81
|
+
fake_db = MagicMock(name="database_singleton")
|
|
82
|
+
with patch("core.container.container") as fake_container:
|
|
83
|
+
fake_container.database.return_value = fake_db
|
|
84
|
+
assert get_database() is fake_db
|
|
85
|
+
|
|
86
|
+
def test_get_cache_returns_container_singleton(self):
|
|
87
|
+
fake_cache = MagicMock(name="cache_singleton")
|
|
88
|
+
with patch("core.container.container") as fake_container:
|
|
89
|
+
fake_container.cache.return_value = fake_cache
|
|
90
|
+
assert get_cache() is fake_cache
|
|
91
|
+
|
|
92
|
+
def test_get_auth_service_is_not_memoised(self):
|
|
93
|
+
"""Two consecutive calls must each re-query the container.
|
|
94
|
+
|
|
95
|
+
Test monkeypatching swaps the container's auth-service mid-test.
|
|
96
|
+
A memoised cache would lock in the first instance and the swap
|
|
97
|
+
would be silently ignored.
|
|
98
|
+
"""
|
|
99
|
+
first = MagicMock(name="first_auth_service")
|
|
100
|
+
second = MagicMock(name="second_auth_service")
|
|
101
|
+
with patch("core.container.container") as fake_container:
|
|
102
|
+
fake_container.auth_service.side_effect = [first, second]
|
|
103
|
+
assert get_auth_service() is first
|
|
104
|
+
assert get_auth_service() is second
|
|
105
|
+
# Verify the container was queried twice -- not a single cached
|
|
106
|
+
# lookup (which would mean side_effect's second value is unused).
|
|
107
|
+
assert fake_container.auth_service.call_count == 2
|
|
108
|
+
|
|
109
|
+
def test_get_database_is_not_memoised(self):
|
|
110
|
+
first = MagicMock(name="first_db")
|
|
111
|
+
second = MagicMock(name="second_db")
|
|
112
|
+
with patch("core.container.container") as fake_container:
|
|
113
|
+
fake_container.database.side_effect = [first, second]
|
|
114
|
+
assert get_database() is first
|
|
115
|
+
assert get_database() is second
|
|
116
|
+
assert fake_container.database.call_count == 2
|
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
"""Plugin self-containment invariants (Wave 11.H plan, milestone H).
|
|
2
|
+
|
|
3
|
+
Nine invariant classes lock the contract that every migrated plugin
|
|
4
|
+
owns its full surface (handlers, router, service code) under
|
|
5
|
+
``server/nodes/<plugin>/`` and that nothing outside that folder
|
|
6
|
+
imports plugin internals by name.
|
|
7
|
+
|
|
8
|
+
Renaming a plugin file or moving plugin code back into ``services/``
|
|
9
|
+
or ``routers/`` will trip exactly one of these tests, the same
|
|
10
|
+
enforcement style as ``test_credential_broadcasts.py``.
|
|
11
|
+
|
|
12
|
+
Coverage map
|
|
13
|
+
------------
|
|
14
|
+
1. ``TestRoutersWebsocketHasNoPluginImports`` -- the central WS dispatch
|
|
15
|
+
table imports zero plugin internals. Forbidden-fragment list.
|
|
16
|
+
2. ``TestNoPluginRouterOutsideNodes`` -- migrated plugins' FastAPI
|
|
17
|
+
routers do not exist under ``server/routers/``. File-existence check.
|
|
18
|
+
3. ``TestPluginInitSelfRegisters`` -- every plugin folder with a
|
|
19
|
+
``_handlers.py`` or ``_router.py`` self-registers from its
|
|
20
|
+
``__init__.py``. Split into two parametrized tests against the
|
|
21
|
+
explicit ``_PLUGINS_WITH_HANDLERS`` / ``_PLUGINS_WITH_ROUTERS``
|
|
22
|
+
constants (no skips), plus a cross-check against the filesystem
|
|
23
|
+
so the constants can't drift silently.
|
|
24
|
+
4. ``TestRegistryLookupsExist`` -- registry public API sanity.
|
|
25
|
+
5. ``TestStaleServiceFilesAbsent`` -- the 11 migrated old service
|
|
26
|
+
paths must not be re-introduced. File-existence check.
|
|
27
|
+
6. ``TestMainPyDoesNotMountPluginRouters`` -- ``main.py`` does not
|
|
28
|
+
wire plugin routers explicitly; they flow in via the plugin loop.
|
|
29
|
+
7. ``TestPluginHandlersDictsArePopulated`` -- when a plugin ships
|
|
30
|
+
``_handlers.py``, its registered surface is non-empty.
|
|
31
|
+
8. ``TestPluginFolderHasNodeFile`` -- every migrated plugin folder
|
|
32
|
+
ships at least one public plugin file (a ``*.py`` not prefixed with
|
|
33
|
+
``_``). Parametrized; never skips. Covers the simple plugins
|
|
34
|
+
(browser / code / email) that the conditional tests above skip.
|
|
35
|
+
9. ``TestPluginPackageImportsCleanly`` -- importing each migrated
|
|
36
|
+
plugin package raises no exception. Parametrized; never skips.
|
|
37
|
+
Catches circular imports / missing-dependency regressions before
|
|
38
|
+
they hit a real startup.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
import inspect
|
|
44
|
+
from pathlib import Path
|
|
45
|
+
|
|
46
|
+
import pytest
|
|
47
|
+
|
|
48
|
+
import routers.websocket as ws_module
|
|
49
|
+
from services import ws_handler_registry
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# Plugins migrated through Wave 11.H (commits A through F):
|
|
53
|
+
# B = whatsapp (commit 72b4ae7)
|
|
54
|
+
# C = twitter (commit 7ed846b)
|
|
55
|
+
# D = google (commit 1392cbb)
|
|
56
|
+
# E = android (commit 8306f47)
|
|
57
|
+
# F = browser, email, code (commit 44e579e)
|
|
58
|
+
# telegram and stripe are the pre-Wave-11.H references.
|
|
59
|
+
_MIGRATED_PLUGINS = (
|
|
60
|
+
"android",
|
|
61
|
+
"browser",
|
|
62
|
+
"code",
|
|
63
|
+
"email",
|
|
64
|
+
"google",
|
|
65
|
+
"stripe",
|
|
66
|
+
"telegram",
|
|
67
|
+
"twitter",
|
|
68
|
+
"whatsapp",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Plugins that ship a ``_handlers.py`` (credentials-modal WebSocket
|
|
72
|
+
# commands beyond Save / Load / Delete). Each entry MUST register via
|
|
73
|
+
# ``register_ws_handlers`` from its package ``__init__.py``.
|
|
74
|
+
_PLUGINS_WITH_HANDLERS = (
|
|
75
|
+
"android",
|
|
76
|
+
"google",
|
|
77
|
+
"stripe",
|
|
78
|
+
"telegram",
|
|
79
|
+
"twitter",
|
|
80
|
+
"whatsapp",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Plugins that ship a ``_router.py`` (FastAPI router for OAuth
|
|
84
|
+
# callbacks etc.). Each entry MUST register via ``register_router``
|
|
85
|
+
# from its package ``__init__.py``.
|
|
86
|
+
_PLUGINS_WITH_ROUTERS = (
|
|
87
|
+
"android",
|
|
88
|
+
"google",
|
|
89
|
+
"twitter",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Forbidden import substrings: any module path that would mean the
|
|
93
|
+
# plugin's surface still lives outside its plugin folder.
|
|
94
|
+
_FORBIDDEN_IMPORT_FRAGMENTS = (
|
|
95
|
+
"services.whatsapp_service",
|
|
96
|
+
"services.twitter_oauth",
|
|
97
|
+
"services.google_oauth",
|
|
98
|
+
"services.android", # legacy relay sub-package
|
|
99
|
+
"services.android_service",
|
|
100
|
+
"services.browser_service",
|
|
101
|
+
"services.email_service",
|
|
102
|
+
"services.himalaya_service",
|
|
103
|
+
"services.claude_code_service",
|
|
104
|
+
"services.maps", # Wave 11.I, N: -> nodes/location/_service
|
|
105
|
+
"services.node_option_loaders", # Wave 11.I, M: -> nodes/<plugin>/_option_loaders
|
|
106
|
+
"routers.twitter",
|
|
107
|
+
"routers.google",
|
|
108
|
+
"routers.android",
|
|
109
|
+
"routers.whatsapp",
|
|
110
|
+
"routers.maps", # Wave 11.I, N: deleted
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
_SERVER_ROOT = Path(__file__).resolve().parent.parent
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class TestRoutersWebsocketHasNoPluginImports:
|
|
118
|
+
"""``routers/websocket.py`` is the central dispatch table only.
|
|
119
|
+
|
|
120
|
+
It must not import any plugin's service or HTTP-router module by
|
|
121
|
+
name. Plugin commands flow in via ``services.ws_handler_registry``.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
def test_no_plugin_imports_in_websocket_router(self):
|
|
125
|
+
src = inspect.getsource(ws_module)
|
|
126
|
+
offenders = [frag for frag in _FORBIDDEN_IMPORT_FRAGMENTS if frag in src]
|
|
127
|
+
assert not offenders, (
|
|
128
|
+
"routers/websocket.py must not import migrated plugin modules. "
|
|
129
|
+
f"Found references to: {offenders}. "
|
|
130
|
+
"Move handler bodies into nodes/<plugin>/_handlers.py and "
|
|
131
|
+
"self-register via register_ws_handlers."
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class TestNoPluginRouterOutsideNodes:
|
|
136
|
+
"""Once a plugin owns an HTTP router, the file must live under
|
|
137
|
+
``nodes/<plugin>/_router.py`` -- never in ``server/routers/``.
|
|
138
|
+
|
|
139
|
+
``server/routers/`` is reserved for cross-cutting routers
|
|
140
|
+
(auth, websocket, webhook, workflow, database, maps,
|
|
141
|
+
nodejs_compat, schemas, credentials). Maps/webhook are recorded
|
|
142
|
+
here as still-shared dispatchers pending a future design pass.
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
_MUST_NOT_EXIST = (
|
|
146
|
+
"twitter.py",
|
|
147
|
+
"google.py",
|
|
148
|
+
"android.py",
|
|
149
|
+
"whatsapp.py",
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
def test_migrated_plugins_have_no_router_file_in_routers(self):
|
|
153
|
+
routers_dir = _SERVER_ROOT / "routers"
|
|
154
|
+
present = [
|
|
155
|
+
name for name in self._MUST_NOT_EXIST
|
|
156
|
+
if (routers_dir / name).exists()
|
|
157
|
+
]
|
|
158
|
+
assert not present, (
|
|
159
|
+
f"server/routers/ contains files for migrated plugins: {present}. "
|
|
160
|
+
"These belong under nodes/<plugin>/_router.py and mount via "
|
|
161
|
+
"register_router from the plugin's __init__.py."
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class TestPluginInitSelfRegisters:
|
|
166
|
+
"""Every plugin folder that ships a ``_handlers.py`` or ``_router.py``
|
|
167
|
+
must self-register from its ``__init__.py``. The package-import
|
|
168
|
+
side effect is the single wiring point -- nothing elsewhere in the
|
|
169
|
+
tree should be doing the registration on the plugin's behalf.
|
|
170
|
+
|
|
171
|
+
Split into two parametrized tests against ``_PLUGINS_WITH_HANDLERS``
|
|
172
|
+
and ``_PLUGINS_WITH_ROUTERS`` so no plugin is skipped: the lists
|
|
173
|
+
explicitly enumerate which plugins ship which surfaces, and a new
|
|
174
|
+
plugin shipping a handler / router file MUST add itself to the
|
|
175
|
+
relevant list (otherwise the membership check below fails).
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
@pytest.mark.parametrize("plugin", _PLUGINS_WITH_HANDLERS)
|
|
179
|
+
def test_plugin_with_handlers_self_registers(self, plugin: str):
|
|
180
|
+
plugin_dir = _SERVER_ROOT / "nodes" / plugin
|
|
181
|
+
handlers_path = plugin_dir / "_handlers.py"
|
|
182
|
+
init_path = plugin_dir / "__init__.py"
|
|
183
|
+
|
|
184
|
+
assert handlers_path.exists(), (
|
|
185
|
+
f"nodes/{plugin}/_handlers.py missing -- remove {plugin!r} "
|
|
186
|
+
"from _PLUGINS_WITH_HANDLERS or restore the file."
|
|
187
|
+
)
|
|
188
|
+
assert init_path.exists(), f"nodes/{plugin}/__init__.py missing"
|
|
189
|
+
|
|
190
|
+
init_src = init_path.read_text(encoding="utf-8")
|
|
191
|
+
assert "register_ws_handlers(" in init_src, (
|
|
192
|
+
f"nodes/{plugin}/_handlers.py exists but "
|
|
193
|
+
f"nodes/{plugin}/__init__.py does not call "
|
|
194
|
+
"register_ws_handlers(...). The plugin's WS surface would "
|
|
195
|
+
"never be wired up at startup."
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
@pytest.mark.parametrize("plugin", _PLUGINS_WITH_ROUTERS)
|
|
199
|
+
def test_plugin_with_router_self_registers(self, plugin: str):
|
|
200
|
+
plugin_dir = _SERVER_ROOT / "nodes" / plugin
|
|
201
|
+
router_path = plugin_dir / "_router.py"
|
|
202
|
+
init_path = plugin_dir / "__init__.py"
|
|
203
|
+
|
|
204
|
+
assert router_path.exists(), (
|
|
205
|
+
f"nodes/{plugin}/_router.py missing -- remove {plugin!r} "
|
|
206
|
+
"from _PLUGINS_WITH_ROUTERS or restore the file."
|
|
207
|
+
)
|
|
208
|
+
assert init_path.exists(), f"nodes/{plugin}/__init__.py missing"
|
|
209
|
+
|
|
210
|
+
init_src = init_path.read_text(encoding="utf-8")
|
|
211
|
+
assert "register_router(" in init_src, (
|
|
212
|
+
f"nodes/{plugin}/_router.py exists but "
|
|
213
|
+
f"nodes/{plugin}/__init__.py does not call "
|
|
214
|
+
"register_router(...). The plugin's HTTP router would "
|
|
215
|
+
"never be mounted on the FastAPI app."
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
def test_handler_router_lists_match_filesystem(self):
|
|
219
|
+
"""Cross-check: every plugin folder with a ``_handlers.py`` or
|
|
220
|
+
``_router.py`` must appear in the corresponding constant.
|
|
221
|
+
Catches a new plugin that ships a handler / router but forgot
|
|
222
|
+
to add itself to the parametrize list.
|
|
223
|
+
"""
|
|
224
|
+
actual_handlers = {
|
|
225
|
+
p for p in _MIGRATED_PLUGINS
|
|
226
|
+
if (_SERVER_ROOT / "nodes" / p / "_handlers.py").exists()
|
|
227
|
+
}
|
|
228
|
+
actual_routers = {
|
|
229
|
+
p for p in _MIGRATED_PLUGINS
|
|
230
|
+
if (_SERVER_ROOT / "nodes" / p / "_router.py").exists()
|
|
231
|
+
}
|
|
232
|
+
assert set(_PLUGINS_WITH_HANDLERS) == actual_handlers, (
|
|
233
|
+
f"_PLUGINS_WITH_HANDLERS drifted from filesystem: "
|
|
234
|
+
f"declared={sorted(_PLUGINS_WITH_HANDLERS)}, "
|
|
235
|
+
f"actual={sorted(actual_handlers)}. Update the constant."
|
|
236
|
+
)
|
|
237
|
+
assert set(_PLUGINS_WITH_ROUTERS) == actual_routers, (
|
|
238
|
+
f"_PLUGINS_WITH_ROUTERS drifted from filesystem: "
|
|
239
|
+
f"declared={sorted(_PLUGINS_WITH_ROUTERS)}, "
|
|
240
|
+
f"actual={sorted(actual_routers)}. Update the constant."
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class TestRegistryLookupsExist:
|
|
245
|
+
"""Sanity: the registries the plugin __init__.py call into must
|
|
246
|
+
exist and expose the documented public functions. Catches accidental
|
|
247
|
+
renames of the registry surface itself.
|
|
248
|
+
"""
|
|
249
|
+
|
|
250
|
+
def test_register_ws_handlers_exists(self):
|
|
251
|
+
assert hasattr(ws_handler_registry, "register_ws_handlers")
|
|
252
|
+
assert callable(ws_handler_registry.register_ws_handlers)
|
|
253
|
+
|
|
254
|
+
def test_register_router_exists(self):
|
|
255
|
+
assert hasattr(ws_handler_registry, "register_router")
|
|
256
|
+
assert callable(ws_handler_registry.register_router)
|
|
257
|
+
|
|
258
|
+
def test_get_routers_exists(self):
|
|
259
|
+
assert hasattr(ws_handler_registry, "get_routers")
|
|
260
|
+
assert callable(ws_handler_registry.get_routers)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
# Old service paths that were `git mv`'d into nodes/<plugin>/ during
|
|
264
|
+
# the migration. None of these should ever be re-created -- if a future
|
|
265
|
+
# refactor "needs" one, the work belongs in the plugin folder.
|
|
266
|
+
_STALE_SERVICE_PATHS = (
|
|
267
|
+
"services/whatsapp_service.py",
|
|
268
|
+
"services/twitter_oauth.py",
|
|
269
|
+
"services/google_oauth.py",
|
|
270
|
+
"services/handlers/google_auth.py",
|
|
271
|
+
"services/android", # the relay sub-package
|
|
272
|
+
"services/android_service.py",
|
|
273
|
+
"services/browser_service.py",
|
|
274
|
+
"services/email_service.py",
|
|
275
|
+
"services/himalaya_service.py",
|
|
276
|
+
"services/claude_code_service.py",
|
|
277
|
+
"services/websocket_client.py", # dead re-export shim, deleted in E
|
|
278
|
+
"services/maps.py", # Wave 11.I, N: -> nodes/location/_service.py
|
|
279
|
+
"services/node_option_loaders", # Wave 11.I, M: -> nodes/<plugin>/_option_loaders.py
|
|
280
|
+
"routers/twitter.py",
|
|
281
|
+
"routers/google.py",
|
|
282
|
+
"routers/android.py",
|
|
283
|
+
"routers/maps.py", # Wave 11.I, N: deleted (all 4 endpoints dead)
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class TestStaleServiceFilesAbsent:
|
|
288
|
+
"""Files that were moved out of ``services/`` and ``routers/`` during
|
|
289
|
+
the migration must not be re-introduced. Guards against an accidental
|
|
290
|
+
revert via a fresh file (rather than a stale import, which test 1
|
|
291
|
+
catches).
|
|
292
|
+
"""
|
|
293
|
+
|
|
294
|
+
@pytest.mark.parametrize("relpath", _STALE_SERVICE_PATHS)
|
|
295
|
+
def test_stale_path_does_not_exist(self, relpath: str):
|
|
296
|
+
target = _SERVER_ROOT / relpath
|
|
297
|
+
assert not target.exists(), (
|
|
298
|
+
f"Stale path {relpath!r} re-appeared under server/. "
|
|
299
|
+
"Migrated plugin code lives in nodes/<plugin>/ -- do not "
|
|
300
|
+
"recreate the old location even with new contents."
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
class TestMainPyDoesNotMountPluginRouters:
|
|
305
|
+
"""``server/main.py`` mounts framework routers explicitly
|
|
306
|
+
(auth / websocket / workflow / database / maps / nodejs_compat /
|
|
307
|
+
schemas / credentials / webhook). Plugin routers flow in via the
|
|
308
|
+
``for r in get_routers(): app.include_router(r)`` loop.
|
|
309
|
+
|
|
310
|
+
Direct ``app.include_router(<plugin>.router)`` calls or
|
|
311
|
+
``from routers import <plugin>`` imports for migrated plugins are
|
|
312
|
+
a regression: they short-circuit the plugin loop and double-mount
|
|
313
|
+
the router under two different code paths.
|
|
314
|
+
"""
|
|
315
|
+
|
|
316
|
+
_MIGRATED_ROUTER_NAMES = ("twitter", "google", "android", "whatsapp")
|
|
317
|
+
|
|
318
|
+
def test_main_py_does_not_explicitly_mount_plugin_routers(self):
|
|
319
|
+
main_path = _SERVER_ROOT / "main.py"
|
|
320
|
+
assert main_path.exists(), "server/main.py missing"
|
|
321
|
+
src = main_path.read_text(encoding="utf-8")
|
|
322
|
+
offenders = [
|
|
323
|
+
name for name in self._MIGRATED_ROUTER_NAMES
|
|
324
|
+
if f"app.include_router({name}.router)" in src
|
|
325
|
+
or f"from routers import {name}" in src
|
|
326
|
+
or f"from routers.{name}" in src
|
|
327
|
+
]
|
|
328
|
+
assert not offenders, (
|
|
329
|
+
f"server/main.py explicitly mounts/imports migrated plugin routers: "
|
|
330
|
+
f"{offenders}. These must flow in via the get_routers() plugin loop. "
|
|
331
|
+
"Drop the explicit include_router(...) line and the routers.<name> "
|
|
332
|
+
"import; plugin's __init__.py registers via register_router(...)."
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
def test_main_py_does_not_wire_plugin_modules(self):
|
|
336
|
+
"""``container.wire(modules=[...])`` should not name modules
|
|
337
|
+
that have been migrated into nodes/<plugin>/. Stale wire entries
|
|
338
|
+
for absent modules raise at startup."""
|
|
339
|
+
main_path = _SERVER_ROOT / "main.py"
|
|
340
|
+
src = main_path.read_text(encoding="utf-8")
|
|
341
|
+
offenders = [
|
|
342
|
+
f"routers.{name}" for name in self._MIGRATED_ROUTER_NAMES
|
|
343
|
+
if f'"routers.{name}"' in src
|
|
344
|
+
]
|
|
345
|
+
assert not offenders, (
|
|
346
|
+
f"server/main.py container.wire(...) names removed plugin modules: "
|
|
347
|
+
f"{offenders}. Drop these entries -- the plugin packages wire their "
|
|
348
|
+
"own dependencies."
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
class TestPluginHandlersDictsArePopulated:
|
|
353
|
+
"""When a plugin ships a ``_handlers.py``, the ``WS_HANDLERS`` dict
|
|
354
|
+
(or whatever the package's ``__init__.py`` imports under that name)
|
|
355
|
+
must register at least one handler. An empty dict is the symptom of
|
|
356
|
+
a partial migration where the file was created but the body wasn't
|
|
357
|
+
moved over.
|
|
358
|
+
|
|
359
|
+
The check is loose: we look for the literal ``WS_HANDLERS`` symbol
|
|
360
|
+
in ``_handlers.py`` and assert it isn't an empty literal. This
|
|
361
|
+
catches the most common partial-migration shape without forcing a
|
|
362
|
+
specific dict-construction style.
|
|
363
|
+
"""
|
|
364
|
+
|
|
365
|
+
@pytest.mark.parametrize("plugin", _PLUGINS_WITH_HANDLERS)
|
|
366
|
+
def test_plugin_handlers_dict_non_empty(self, plugin: str):
|
|
367
|
+
handlers_path = _SERVER_ROOT / "nodes" / plugin / "_handlers.py"
|
|
368
|
+
assert handlers_path.exists(), (
|
|
369
|
+
f"nodes/{plugin}/_handlers.py missing -- remove {plugin!r} "
|
|
370
|
+
"from _PLUGINS_WITH_HANDLERS or restore the file."
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
src = handlers_path.read_text(encoding="utf-8")
|
|
374
|
+
# Must export WS_HANDLERS (the documented surface used by
|
|
375
|
+
# register_ws_handlers).
|
|
376
|
+
assert "WS_HANDLERS" in src, (
|
|
377
|
+
f"nodes/{plugin}/_handlers.py does not export WS_HANDLERS. "
|
|
378
|
+
"The plugin's __init__.py reads this symbol; absence means "
|
|
379
|
+
"the plugin self-registration is broken."
|
|
380
|
+
)
|
|
381
|
+
# Must not be the empty literal. Stripe builds via
|
|
382
|
+
# make_lifecycle_handlers(...) so we accept either {...} with
|
|
383
|
+
# at least one quoted key OR a function call.
|
|
384
|
+
empty_literal_patterns = (
|
|
385
|
+
"WS_HANDLERS = {}\n",
|
|
386
|
+
"WS_HANDLERS={}\n",
|
|
387
|
+
"WS_HANDLERS: dict = {}\n",
|
|
388
|
+
)
|
|
389
|
+
for pattern in empty_literal_patterns:
|
|
390
|
+
assert pattern not in src, (
|
|
391
|
+
f"nodes/{plugin}/_handlers.py defines an empty WS_HANDLERS dict. "
|
|
392
|
+
"Move the handler bodies into _handlers.py (or wire via "
|
|
393
|
+
"make_lifecycle_handlers) before declaring the migration done."
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
# Files the node-discovery walker treats as plugin entry points: any
|
|
398
|
+
# top-level ``*.py`` not prefixed with ``_`` (which marks
|
|
399
|
+
# package-private siblings like ``_service.py`` / ``_credentials.py``)
|
|
400
|
+
# and not the package ``__init__.py``.
|
|
401
|
+
def _public_plugin_files(plugin_dir: Path) -> list[Path]:
|
|
402
|
+
return [
|
|
403
|
+
p for p in plugin_dir.glob("*.py")
|
|
404
|
+
if not p.name.startswith("_") and p.name != "__init__.py"
|
|
405
|
+
]
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
class TestPluginFolderHasNodeFile:
|
|
409
|
+
"""Every migrated plugin folder must ship at least one public plugin
|
|
410
|
+
file (a ``*.py`` not prefixed with ``_``). Catches the partial-
|
|
411
|
+
extraction failure mode where the folder gets created with
|
|
412
|
+
``_service.py`` / ``_handlers.py`` but the actual ``BaseNode``
|
|
413
|
+
subclass is forgotten.
|
|
414
|
+
|
|
415
|
+
Unlike ``TestPluginInitSelfRegisters`` / ``TestPluginHandlersDictsArePopulated``
|
|
416
|
+
this test never skips -- browser / code / email all ship plugin
|
|
417
|
+
files even though they don't ship ``_handlers.py`` or ``_router.py``.
|
|
418
|
+
"""
|
|
419
|
+
|
|
420
|
+
@pytest.mark.parametrize("plugin", _MIGRATED_PLUGINS)
|
|
421
|
+
def test_plugin_folder_has_at_least_one_node_file(self, plugin: str):
|
|
422
|
+
plugin_dir = _SERVER_ROOT / "nodes" / plugin
|
|
423
|
+
assert plugin_dir.is_dir(), f"nodes/{plugin}/ is missing"
|
|
424
|
+
plugin_files = _public_plugin_files(plugin_dir)
|
|
425
|
+
assert plugin_files, (
|
|
426
|
+
f"nodes/{plugin}/ has no public plugin files. The folder "
|
|
427
|
+
"should contain at least one ``<name>.py`` declaring a "
|
|
428
|
+
"BaseNode subclass; underscore-prefixed siblings "
|
|
429
|
+
"(_service.py, _handlers.py, _credentials.py, ...) are "
|
|
430
|
+
"package-private and skipped by the node-discovery walker."
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
class TestPluginsUseTypedEventFactories:
|
|
435
|
+
"""Wave 11.I, milestone Q (locked in U).
|
|
436
|
+
|
|
437
|
+
Plugins must construct :class:`WorkflowEvent` directly via the
|
|
438
|
+
typed factory classmethods (``WorkflowEvent.credential(...)``,
|
|
439
|
+
``WorkflowEvent.message(...)``, etc.) -- not via the
|
|
440
|
+
``WorkflowEvent.from_legacy(event_type, data)`` shim. The shim
|
|
441
|
+
exists so :func:`event_waiter.dispatch` can still accept the legacy
|
|
442
|
+
``(str, dict)`` form at the framework boundary; lifting it inside
|
|
443
|
+
plugin code defeats the purpose.
|
|
444
|
+
"""
|
|
445
|
+
|
|
446
|
+
@pytest.mark.parametrize("plugin", _MIGRATED_PLUGINS)
|
|
447
|
+
def test_plugin_does_not_call_from_legacy(self, plugin: str):
|
|
448
|
+
plugin_dir = _SERVER_ROOT / "nodes" / plugin
|
|
449
|
+
offenders: list[str] = []
|
|
450
|
+
for py in plugin_dir.rglob("*.py"):
|
|
451
|
+
if "from_legacy" in py.read_text(encoding="utf-8"):
|
|
452
|
+
offenders.append(str(py.relative_to(_SERVER_ROOT)))
|
|
453
|
+
assert not offenders, (
|
|
454
|
+
f"Plugin {plugin!r} uses WorkflowEvent.from_legacy in: "
|
|
455
|
+
f"{offenders}. Use the typed factory classmethods "
|
|
456
|
+
"(WorkflowEvent.credential / .message / .oauth_completed / "
|
|
457
|
+
"etc.) for new dispatches; from_legacy is the framework-edge "
|
|
458
|
+
"shim only."
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
class TestPluginPackageImportsCleanly:
|
|
463
|
+
"""Every migrated plugin package must import without raising.
|
|
464
|
+
|
|
465
|
+
Catches:
|
|
466
|
+
- Circular imports introduced by mid-refactor ``from core.container``
|
|
467
|
+
at module load time (the same class of bug the plugin-router
|
|
468
|
+
cycle fix addressed in commit 9274072).
|
|
469
|
+
- Missing dependencies that only surface at startup.
|
|
470
|
+
- Syntax / decorator errors masked by lazy imports elsewhere.
|
|
471
|
+
|
|
472
|
+
Parametrized over all 9 migrated plugins; never skips.
|
|
473
|
+
"""
|
|
474
|
+
|
|
475
|
+
@pytest.mark.parametrize("plugin", _MIGRATED_PLUGINS)
|
|
476
|
+
def test_plugin_package_imports(self, plugin: str):
|
|
477
|
+
import importlib
|
|
478
|
+
|
|
479
|
+
# If the package is already imported (likely, since the test
|
|
480
|
+
# session does plugin discovery during setup), reload to
|
|
481
|
+
# exercise the import path again -- catches regressions where
|
|
482
|
+
# the original import succeeded only because of import order.
|
|
483
|
+
module_name = f"nodes.{plugin}"
|
|
484
|
+
module = importlib.import_module(module_name)
|
|
485
|
+
importlib.reload(module)
|
|
486
|
+
assert module.__name__ == module_name
|