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,425 @@
|
|
|
1
|
+
"""Pytest invariants for the status-broadcast contract (Wave 11.I, milestone U).
|
|
2
|
+
|
|
3
|
+
Mirrors :mod:`tests.credentials.test_credential_broadcasts` for the
|
|
4
|
+
plugin-status / refresh / send_custom_event surface. Locks the
|
|
5
|
+
post-Wave-11.I contract:
|
|
6
|
+
|
|
7
|
+
* The credential-mutation path uses :class:`WorkflowEvent`
|
|
8
|
+
(CloudEvents v1.0 envelope) -- already enforced by
|
|
9
|
+
:mod:`tests.credentials.test_credential_broadcasts`. Re-asserted
|
|
10
|
+
here for completeness.
|
|
11
|
+
* :func:`services.event_waiter.dispatch` /
|
|
12
|
+
:func:`dispatch_async` accept either a ``WorkflowEvent`` or a
|
|
13
|
+
``(str, dict)`` pair via :func:`_unpack_event` (post-Q signature).
|
|
14
|
+
* ``send_custom_event`` callsites: enforced as a ratchet -- the
|
|
15
|
+
``_LEGACY_RAW_DICT_CALLSITES`` allowlist enumerates the existing
|
|
16
|
+
raw-dict callers with documented WHY. New callsites must use
|
|
17
|
+
``WorkflowEvent`` unless they're added to the allowlist with a
|
|
18
|
+
rationale.
|
|
19
|
+
|
|
20
|
+
Two carve-outs:
|
|
21
|
+
|
|
22
|
+
* ``_TELEMETRY_CARVE_OUT`` -- permanent exempts. High-frequency
|
|
23
|
+
/ paired-wire / log streams that won't ever move to typed
|
|
24
|
+
envelopes (per-node-status broadcasts fire hundreds of times per
|
|
25
|
+
workflow run; envelope wrapping is pure overhead there).
|
|
26
|
+
* ``_LEGACY_RAW_DICT_BROADCASTS`` / ``_LEGACY_RAW_DICT_CALLSITES`` --
|
|
27
|
+
grandfathered until per-plugin migration. Each entry documents
|
|
28
|
+
WHY it's still raw and what would unlock it.
|
|
29
|
+
|
|
30
|
+
Companion: :mod:`tests.credentials.test_credential_broadcasts` (the
|
|
31
|
+
older sibling that locks the credential-broadcast contract via the
|
|
32
|
+
same ``inspect.getsource`` introspection style).
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import inspect
|
|
38
|
+
import re
|
|
39
|
+
from typing import FrozenSet
|
|
40
|
+
|
|
41
|
+
import pytest
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ============================================================================
|
|
45
|
+
# CARVE-OUTS
|
|
46
|
+
# ============================================================================
|
|
47
|
+
#
|
|
48
|
+
# Telemetry: permanent exempts. These methods broadcast on every
|
|
49
|
+
# user-action / log-line / lifecycle-tick. Wrapping each emission in a
|
|
50
|
+
# CloudEvents envelope adds bytes + JSON encoding overhead at sites that
|
|
51
|
+
# fire hundreds of times per workflow run -- not worth it. The wire
|
|
52
|
+
# frames are paired with their dual (e.g. ``api_key_status`` is paired
|
|
53
|
+
# with ``credential_catalogue_updated`` in the credential mutation
|
|
54
|
+
# path) so frontend listeners always have a typed channel available
|
|
55
|
+
# for state changes.
|
|
56
|
+
_TELEMETRY_CARVE_OUT: FrozenSet[str] = frozenset({
|
|
57
|
+
"update_api_key_status", # paired-wire with credential_catalogue_updated
|
|
58
|
+
"update_node_status", # ~hundreds per workflow run
|
|
59
|
+
"update_node_output", # paired with update_node_status
|
|
60
|
+
"update_variable", # per-write
|
|
61
|
+
"update_variables", # batch per-write
|
|
62
|
+
"update_workflow_status", # lifecycle ticks
|
|
63
|
+
"update_deployment_status", # lifecycle ticks
|
|
64
|
+
"broadcast_console_log", # log stream
|
|
65
|
+
"broadcast_terminal_log", # log stream
|
|
66
|
+
# Pure dispatcher -- iterates plugin-registered callbacks via
|
|
67
|
+
# TaskGroup, does not emit any broadcast itself. Per-plugin
|
|
68
|
+
# refresh callbacks live in nodes/<plugin>/_refresh.py and are
|
|
69
|
+
# subject to their own typed-envelope migration tracked outside
|
|
70
|
+
# the StatusBroadcaster class.
|
|
71
|
+
"_refresh_all_services",
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
# Plugin status updates that still emit raw ``{type: 'X_status', data:
|
|
75
|
+
# {...}}`` frames. The frontend's per-plugin status panels listen for
|
|
76
|
+
# these wire-frames directly today; switching to a CloudEvents-typed
|
|
77
|
+
# envelope is a frontend change too. Migration is per-plugin, not
|
|
78
|
+
# included in milestone U scope.
|
|
79
|
+
_LEGACY_RAW_DICT_BROADCASTS: FrozenSet[str] = frozenset({
|
|
80
|
+
"update_android_status", # FE: AndroidStatusPanel reads `android_status` wire-frame
|
|
81
|
+
"update_whatsapp_status", # FE: WhatsAppStatusPanel reads `whatsapp_status` wire-frame
|
|
82
|
+
"update_telegram_status", # FE: TelegramStatusPanel reads `telegram_status` wire-frame
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
# ``send_custom_event`` callers that still pass raw dicts. Each entry
|
|
86
|
+
# documents WHY. Per-plugin migration unlocks each one.
|
|
87
|
+
_LEGACY_RAW_DICT_CALLSITES: FrozenSet[str] = frozenset({
|
|
88
|
+
# WhatsApp event router emits 7 distinct wire types. Migration would
|
|
89
|
+
# also rename ``whatsapp_message_received`` etc. to
|
|
90
|
+
# ``whatsapp.message.received`` namespaced types, requiring FE
|
|
91
|
+
# listener updates.
|
|
92
|
+
"nodes/whatsapp/_service.py",
|
|
93
|
+
# Webhook router emits ``webhook_received``. Webhook payload is
|
|
94
|
+
# arbitrary JSON; the consumer (webhookTrigger node) shape is
|
|
95
|
+
# already abstract, so a typed envelope adds little value.
|
|
96
|
+
"routers/webhook.py",
|
|
97
|
+
# Task-delegation completion. Migration would namespace as
|
|
98
|
+
# ``agent.task.{succeeded,failed}``; matched by taskTrigger.
|
|
99
|
+
"services/handlers/tools.py",
|
|
100
|
+
# Agent Builder canvas-mutation broadcast. Emits
|
|
101
|
+
# ``workflow_ops_apply`` carrying a flat
|
|
102
|
+
# ``{workflow_id, caller_node_id, operations}`` shape consumed by
|
|
103
|
+
# ``client/src/hooks/useWorkflowOpsListener.ts``. Migration to
|
|
104
|
+
# WorkflowEvent would change the wire format (envelope vs flat),
|
|
105
|
+
# requiring a parallel rewrite of the frontend listener +
|
|
106
|
+
# invariant test fixtures. Deferred — typed-envelope migration
|
|
107
|
+
# tracked alongside the credentials.* / agent.task.* migrations.
|
|
108
|
+
"nodes/tool/agent_builder.py",
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# ============================================================================
|
|
113
|
+
# REGEXES
|
|
114
|
+
# ============================================================================
|
|
115
|
+
|
|
116
|
+
# Inside a ``_refresh_*`` / ``update_*_status`` method body, the
|
|
117
|
+
# ``broadcast({...})`` call MUST consume a WorkflowEvent (looking for
|
|
118
|
+
# ``WorkflowEvent`` token in a 200-char window before the ``broadcast(``
|
|
119
|
+
# call) OR call ``broadcast_credential_event`` (which wraps WorkflowEvent
|
|
120
|
+
# internally).
|
|
121
|
+
_REFRESH_OR_STATUS_NAME = re.compile(r"^_refresh_|^update_\w+_status$")
|
|
122
|
+
|
|
123
|
+
# Function-defining line in source. Captures method name.
|
|
124
|
+
_METHOD_DEF = re.compile(r"^\s+(?:async )?def (\w+)\b")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# ============================================================================
|
|
128
|
+
# Helpers
|
|
129
|
+
# ============================================================================
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _is_typed_broadcast(method_source: str) -> bool:
|
|
133
|
+
"""Heuristic: method source either references ``WorkflowEvent``
|
|
134
|
+
OR calls a typed-broadcast helper (``broadcast_credential_event``).
|
|
135
|
+
"""
|
|
136
|
+
if "WorkflowEvent" in method_source:
|
|
137
|
+
return True
|
|
138
|
+
if "broadcast_credential_event" in method_source:
|
|
139
|
+
return True
|
|
140
|
+
return False
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _split_methods(class_source: str) -> dict[str, str]:
|
|
144
|
+
"""Split a class body's source into ``{method_name: method_source}``.
|
|
145
|
+
|
|
146
|
+
Anchors on ``\\n (?:async )?def `` which marks method starts at
|
|
147
|
+
class-level indentation. Module-level functions are ignored.
|
|
148
|
+
"""
|
|
149
|
+
methods: dict[str, str] = {}
|
|
150
|
+
parts = re.split(r"(\n (?:async )?def \w+)", class_source)
|
|
151
|
+
# Re-stitch: parts looks like [preamble, " def foo", body0, " def bar", body1, ...]
|
|
152
|
+
for i in range(1, len(parts), 2):
|
|
153
|
+
header = parts[i]
|
|
154
|
+
body = parts[i + 1] if i + 1 < len(parts) else ""
|
|
155
|
+
match = _METHOD_DEF.match(header.lstrip("\n"))
|
|
156
|
+
if match:
|
|
157
|
+
methods[match.group(1)] = header + body
|
|
158
|
+
return methods
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# ============================================================================
|
|
162
|
+
# 1) Credential event uses WorkflowEvent (already locked elsewhere; re-pinned)
|
|
163
|
+
# ============================================================================
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class TestCredentialBroadcastUsesWorkflowEvent:
|
|
167
|
+
"""``broadcast_credential_event`` is the canonical typed-builder
|
|
168
|
+
helper. Pinned here too so a regression in either invariant file
|
|
169
|
+
catches the break.
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
def test_uses_workflow_event(self):
|
|
173
|
+
from services.status_broadcaster import StatusBroadcaster
|
|
174
|
+
|
|
175
|
+
src = inspect.getsource(StatusBroadcaster.broadcast_credential_event)
|
|
176
|
+
assert "WorkflowEvent" in src, (
|
|
177
|
+
"broadcast_credential_event must wrap WorkflowEvent (CloudEvents "
|
|
178
|
+
"v1.0). Removing the typed envelope breaks the cross-tab "
|
|
179
|
+
"catalogue refresh contract."
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# ============================================================================
|
|
184
|
+
# 2) event_waiter.dispatch accepts WorkflowEvent (post-Q signature)
|
|
185
|
+
# ============================================================================
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class TestEventWaiterDispatchAcceptsEnvelope:
|
|
189
|
+
"""Wave 11.I, milestone Q. ``dispatch`` and ``dispatch_async``
|
|
190
|
+
accept either a ``WorkflowEvent`` or a ``(str, dict)`` pair via
|
|
191
|
+
``_unpack_event`` -- the (str, dict) shape stays supported via
|
|
192
|
+
``WorkflowEvent.from_legacy`` upstream of the dispatcher.
|
|
193
|
+
|
|
194
|
+
Locks the contract so future refactors don't drop the envelope
|
|
195
|
+
path silently.
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
@pytest.fixture
|
|
199
|
+
def event_waiter_module(self):
|
|
200
|
+
from services import event_waiter
|
|
201
|
+
|
|
202
|
+
return event_waiter
|
|
203
|
+
|
|
204
|
+
def test_unpack_event_present(self, event_waiter_module):
|
|
205
|
+
assert hasattr(event_waiter_module, "_unpack_event"), (
|
|
206
|
+
"event_waiter must expose _unpack_event -- the normalisation "
|
|
207
|
+
"helper that lets dispatch() accept a WorkflowEvent directly."
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
def test_dispatch_calls_unpack_event(self, event_waiter_module):
|
|
211
|
+
src = inspect.getsource(event_waiter_module.dispatch)
|
|
212
|
+
assert "_unpack_event" in src, (
|
|
213
|
+
"dispatch() must route through _unpack_event() so a "
|
|
214
|
+
"WorkflowEvent argument is normalised before the dispatch loop."
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
def test_dispatch_async_calls_unpack_event(self, event_waiter_module):
|
|
218
|
+
src = inspect.getsource(event_waiter_module.dispatch_async)
|
|
219
|
+
assert "_unpack_event" in src, (
|
|
220
|
+
"dispatch_async() must route through _unpack_event() for the "
|
|
221
|
+
"same reason as dispatch()."
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
def test_unpack_event_handles_workflow_event(self, event_waiter_module):
|
|
225
|
+
"""Functional check: a WorkflowEvent input round-trips to
|
|
226
|
+
``(event_type, data)``. The legacy tuple-form path is exercised
|
|
227
|
+
by the rest of the test suite."""
|
|
228
|
+
from services.events.envelope import WorkflowEvent
|
|
229
|
+
|
|
230
|
+
event = WorkflowEvent(
|
|
231
|
+
source="machinaos://test",
|
|
232
|
+
type="test.something.happened",
|
|
233
|
+
subject="test-subject",
|
|
234
|
+
data={"k": "v"},
|
|
235
|
+
)
|
|
236
|
+
event_type, data = event_waiter_module._unpack_event(event)
|
|
237
|
+
assert event_type == "test.something.happened"
|
|
238
|
+
assert data == {"k": "v"}
|
|
239
|
+
|
|
240
|
+
def test_unpack_event_handles_legacy_tuple_form(self, event_waiter_module):
|
|
241
|
+
event_type, data = event_waiter_module._unpack_event(
|
|
242
|
+
"legacy.event.type", {"k": "v"},
|
|
243
|
+
)
|
|
244
|
+
assert event_type == "legacy.event.type"
|
|
245
|
+
assert data == {"k": "v"}
|
|
246
|
+
|
|
247
|
+
def test_unpack_event_rejects_unsupported_form(self, event_waiter_module):
|
|
248
|
+
with pytest.raises(TypeError):
|
|
249
|
+
event_waiter_module._unpack_event(42)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
# ============================================================================
|
|
253
|
+
# 3) Plugin status broadcasts: typed builder enforced (with carve-outs)
|
|
254
|
+
# ============================================================================
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class TestStatusBroadcastsUseTypedBuilder:
|
|
258
|
+
"""Every ``_refresh_*`` / ``update_*_status`` method on
|
|
259
|
+
:class:`StatusBroadcaster` MUST broadcast via a ``WorkflowEvent``
|
|
260
|
+
OR be listed in :data:`_TELEMETRY_CARVE_OUT` (permanent exempt)
|
|
261
|
+
OR :data:`_LEGACY_RAW_DICT_BROADCASTS` (per-plugin migration TODO).
|
|
262
|
+
|
|
263
|
+
The carve-outs exist so the test stays green during incremental
|
|
264
|
+
migration; new methods that don't fit either carve-out fail CI.
|
|
265
|
+
"""
|
|
266
|
+
|
|
267
|
+
@pytest.fixture
|
|
268
|
+
def broadcaster_methods(self) -> dict[str, str]:
|
|
269
|
+
from services import status_broadcaster as sb
|
|
270
|
+
|
|
271
|
+
cls_src = inspect.getsource(sb.StatusBroadcaster)
|
|
272
|
+
return _split_methods(cls_src)
|
|
273
|
+
|
|
274
|
+
def test_no_overlap_between_carve_outs(self):
|
|
275
|
+
"""A method can't be both telemetry-permanent-exempt and
|
|
276
|
+
legacy-grandfathered. If it appears in both, one of the two
|
|
277
|
+
lists is wrong."""
|
|
278
|
+
overlap = _TELEMETRY_CARVE_OUT & _LEGACY_RAW_DICT_BROADCASTS
|
|
279
|
+
assert not overlap, (
|
|
280
|
+
f"Methods in both _TELEMETRY_CARVE_OUT and "
|
|
281
|
+
f"_LEGACY_RAW_DICT_BROADCASTS: {sorted(overlap)}. Pick one."
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
def test_status_methods_use_typed_builder_or_listed(
|
|
285
|
+
self, broadcaster_methods,
|
|
286
|
+
):
|
|
287
|
+
offenders = []
|
|
288
|
+
for name, body in broadcaster_methods.items():
|
|
289
|
+
if not _REFRESH_OR_STATUS_NAME.match(name):
|
|
290
|
+
continue
|
|
291
|
+
if name in _TELEMETRY_CARVE_OUT:
|
|
292
|
+
continue
|
|
293
|
+
if name in _LEGACY_RAW_DICT_BROADCASTS:
|
|
294
|
+
continue
|
|
295
|
+
if not _is_typed_broadcast(body):
|
|
296
|
+
offenders.append(name)
|
|
297
|
+
assert not offenders, (
|
|
298
|
+
f"StatusBroadcaster methods {offenders} broadcast via raw "
|
|
299
|
+
f"{{type: ..., data: ...}} dicts. Either use WorkflowEvent / "
|
|
300
|
+
f"broadcast_credential_event, or document the exception in "
|
|
301
|
+
f"_TELEMETRY_CARVE_OUT (permanent exempt) or "
|
|
302
|
+
f"_LEGACY_RAW_DICT_BROADCASTS (per-plugin migration TODO) "
|
|
303
|
+
f"in tests/test_status_broadcasts.py."
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
def test_legacy_entries_actually_exist_on_class(self, broadcaster_methods):
|
|
307
|
+
"""If we drop a legacy method during migration, remove its
|
|
308
|
+
allowlist entry too. Catches stale carve-outs."""
|
|
309
|
+
missing = _LEGACY_RAW_DICT_BROADCASTS - set(broadcaster_methods.keys())
|
|
310
|
+
assert not missing, (
|
|
311
|
+
f"_LEGACY_RAW_DICT_BROADCASTS lists methods that don't exist "
|
|
312
|
+
f"on StatusBroadcaster: {sorted(missing)}. Remove the stale "
|
|
313
|
+
f"entries from tests/test_status_broadcasts.py."
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
def test_telemetry_entries_actually_exist_on_class(self, broadcaster_methods):
|
|
317
|
+
"""Same staleness check for the telemetry carve-out."""
|
|
318
|
+
missing = _TELEMETRY_CARVE_OUT - set(broadcaster_methods.keys())
|
|
319
|
+
assert not missing, (
|
|
320
|
+
f"_TELEMETRY_CARVE_OUT lists methods that don't exist "
|
|
321
|
+
f"on StatusBroadcaster: {sorted(missing)}. Remove the stale "
|
|
322
|
+
f"entries from tests/test_status_broadcasts.py."
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
# ============================================================================
|
|
327
|
+
# 4) send_custom_event callsite ratchet
|
|
328
|
+
# ============================================================================
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
class TestSendCustomEventPayload:
|
|
332
|
+
"""``broadcaster.send_custom_event(event_type, data)`` accepts
|
|
333
|
+
either a raw dict or a ``WorkflowEvent`` today. Wave 11.I,
|
|
334
|
+
milestone U adds this ratchet so NEW callsites must pass a
|
|
335
|
+
WorkflowEvent unless explicitly listed in
|
|
336
|
+
:data:`_LEGACY_RAW_DICT_CALLSITES` with a documented WHY.
|
|
337
|
+
"""
|
|
338
|
+
|
|
339
|
+
@pytest.fixture
|
|
340
|
+
def callsite_files(self) -> list[str]:
|
|
341
|
+
"""All files under ``server/`` containing ``send_custom_event``."""
|
|
342
|
+
from pathlib import Path
|
|
343
|
+
|
|
344
|
+
root = Path(__file__).resolve().parent.parent # server/
|
|
345
|
+
return [
|
|
346
|
+
str(p.relative_to(root)).replace("\\", "/")
|
|
347
|
+
for p in root.rglob("*.py")
|
|
348
|
+
if "send_custom_event" in p.read_text(encoding="utf-8")
|
|
349
|
+
and "tests/" not in str(p).replace("\\", "/")
|
|
350
|
+
and p.name != "test_status_broadcasts.py"
|
|
351
|
+
]
|
|
352
|
+
|
|
353
|
+
def test_definition_lives_on_status_broadcaster(self):
|
|
354
|
+
"""``send_custom_event`` is defined on the broadcaster --
|
|
355
|
+
anchor the symbol so allowlist entries can point to caller
|
|
356
|
+
files without colliding with the definition."""
|
|
357
|
+
from services.status_broadcaster import StatusBroadcaster
|
|
358
|
+
|
|
359
|
+
assert hasattr(StatusBroadcaster, "send_custom_event")
|
|
360
|
+
|
|
361
|
+
def test_callsites_match_allowlist(self, callsite_files):
|
|
362
|
+
"""Every file calling ``send_custom_event`` MUST be in the
|
|
363
|
+
legacy allowlist (with documented WHY) OR call it through a
|
|
364
|
+
WorkflowEvent envelope (caught by the next test).
|
|
365
|
+
|
|
366
|
+
New plugins that need to broadcast custom events should
|
|
367
|
+
either use :class:`WorkflowEvent` directly (preferred) or add
|
|
368
|
+
themselves to :data:`_LEGACY_RAW_DICT_CALLSITES` with a
|
|
369
|
+
comment explaining why typed-envelope migration is deferred.
|
|
370
|
+
"""
|
|
371
|
+
# The status broadcaster file owns the definition; not a "caller".
|
|
372
|
+
callers = {
|
|
373
|
+
f for f in callsite_files
|
|
374
|
+
if not f.endswith("status_broadcaster.py")
|
|
375
|
+
}
|
|
376
|
+
unknown = callers - _LEGACY_RAW_DICT_CALLSITES
|
|
377
|
+
# New callers must EITHER appear in the allowlist OR pass a
|
|
378
|
+
# WorkflowEvent -- the next test verifies the envelope
|
|
379
|
+
# path; here we only flag truly new files.
|
|
380
|
+
offenders = []
|
|
381
|
+
for path in unknown:
|
|
382
|
+
full = Path_resolve(path)
|
|
383
|
+
src = full.read_text(encoding="utf-8")
|
|
384
|
+
if not _passes_workflow_event(src):
|
|
385
|
+
offenders.append(path)
|
|
386
|
+
assert not offenders, (
|
|
387
|
+
f"send_custom_event callsites missing from typed-envelope "
|
|
388
|
+
f"path AND not listed in _LEGACY_RAW_DICT_CALLSITES: "
|
|
389
|
+
f"{sorted(offenders)}. Either pass a WorkflowEvent or "
|
|
390
|
+
f"add the file path to the allowlist with documented WHY "
|
|
391
|
+
f"in tests/test_status_broadcasts.py."
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
def test_legacy_callsites_actually_call_send_custom_event(self, callsite_files):
|
|
395
|
+
"""Stale-entry check: every path in
|
|
396
|
+
``_LEGACY_RAW_DICT_CALLSITES`` must still contain a
|
|
397
|
+
``send_custom_event`` call. Catches obsolete allowlist entries
|
|
398
|
+
that should have been removed during a migration."""
|
|
399
|
+
missing = _LEGACY_RAW_DICT_CALLSITES - set(callsite_files)
|
|
400
|
+
assert not missing, (
|
|
401
|
+
f"_LEGACY_RAW_DICT_CALLSITES lists files that no longer "
|
|
402
|
+
f"call send_custom_event: {sorted(missing)}. Remove the "
|
|
403
|
+
f"stale entries."
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
# ============================================================================
|
|
408
|
+
# Module-private helpers (used by the test bodies above)
|
|
409
|
+
# ============================================================================
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def Path_resolve(rel: str):
|
|
413
|
+
"""Resolve a server-relative path to an absolute :class:`pathlib.Path`."""
|
|
414
|
+
from pathlib import Path
|
|
415
|
+
|
|
416
|
+
return Path(__file__).resolve().parent.parent / rel
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _passes_workflow_event(src: str) -> bool:
|
|
420
|
+
"""Heuristic: file imports WorkflowEvent AND every send_custom_event
|
|
421
|
+
call is made through a typed envelope. The simplest pattern is to
|
|
422
|
+
import WorkflowEvent and pass a constructed instance. We accept any
|
|
423
|
+
file that imports WorkflowEvent at all -- the test is a nudge, not
|
|
424
|
+
an exhaustive contract."""
|
|
425
|
+
return "WorkflowEvent" in src
|