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.
Files changed (393) hide show
  1. package/README.md +143 -107
  2. package/client/dist/assets/ActionBar-Du2MSFSz.js +1 -0
  3. package/client/dist/assets/ApiKeyInput-k2LBmBjb.js +1 -0
  4. package/client/dist/assets/ApiKeyPanel-C_bV9U0X.js +1 -0
  5. package/client/dist/assets/ApiUsageSection-CmVfwZzL.js +1 -0
  6. package/client/dist/assets/EmailPanel-CeKIMGu-.js +1 -0
  7. package/client/dist/assets/OAuthPanel-KA3t3Q2K.js +1 -0
  8. package/client/dist/assets/QrPairingPanel-NgNpJNuk.js +1 -0
  9. package/client/dist/assets/RateLimitSection-Du5YNVIA.js +1 -0
  10. package/client/dist/assets/StatusCard-DNLyayXc.js +1 -0
  11. package/client/dist/assets/index-DQ0nwhec.js +257 -0
  12. package/client/dist/assets/index-DxmbVskS.css +1 -0
  13. package/client/dist/assets/vendor-flow-CZmBvHRo.js +1 -0
  14. package/client/dist/assets/vendor-icons-CVrPjN2Q.js +22 -0
  15. package/client/dist/assets/vendor-markdown-CRou3yQ5.js +62 -0
  16. package/client/dist/assets/vendor-misc-C4VxKHs5.js +1 -0
  17. package/client/dist/assets/vendor-query-SzWcOU0G.js +1 -0
  18. package/client/dist/assets/vendor-radix-Dnos29jG.js +56 -0
  19. package/client/dist/assets/vendor-react-DvWIbVx0.js +1 -0
  20. package/client/dist/index.html +37 -3
  21. package/client/index.html +28 -1
  22. package/client/package.json +44 -40
  23. package/client/src/App.tsx +2 -0
  24. package/client/src/Dashboard.tsx +157 -45
  25. package/client/src/ParameterPanel.tsx +3 -5
  26. package/client/src/adapters/nodeSpecToDescription.ts +1 -0
  27. package/client/src/assets/icons/NodeIcon.tsx +32 -0
  28. package/client/src/assets/icons/index.ts +4 -0
  29. package/client/src/assets/icons/stripe.svg +1 -0
  30. package/client/src/assets/icons/themedGlyphs.ts +404 -0
  31. package/client/src/components/AIAgentNode.tsx +77 -53
  32. package/client/src/components/GenericNode.tsx +34 -52
  33. package/client/src/components/OutputPanel.tsx +64 -147
  34. package/client/src/components/ParameterRenderer.tsx +5 -3
  35. package/client/src/components/SkillEditorModal.tsx +9 -18
  36. package/client/src/components/SquareNode.tsx +97 -115
  37. package/client/src/components/StartNode.tsx +32 -42
  38. package/client/src/components/SvgFilterDefs.tsx +54 -0
  39. package/client/src/components/TeamMonitorNode.tsx +12 -14
  40. package/client/src/components/ToolkitNode.tsx +35 -60
  41. package/client/src/components/TriggerNode.tsx +43 -77
  42. package/client/src/components/__tests__/CredentialsModal.test.tsx +49 -45
  43. package/client/src/components/credentials/CredentialsModal.tsx +98 -30
  44. package/client/src/components/credentials/CredentialsPalette.tsx +73 -5
  45. package/client/src/components/credentials/catalogueAdapter.ts +17 -1
  46. package/client/src/components/credentials/panels/ApiKeyPanel.tsx +102 -37
  47. package/client/src/components/credentials/panels/EmailPanel.tsx +7 -19
  48. package/client/src/components/credentials/panels/OAuthPanel.tsx +5 -1
  49. package/client/src/components/credentials/panels/QrPairingPanel.tsx +1 -3
  50. package/client/src/components/credentials/primitives/ActionBar.tsx +7 -11
  51. package/client/src/components/credentials/primitives/OAuthConnect.tsx +19 -28
  52. package/client/src/components/credentials/sections/ProviderDefaultsSection.tsx +24 -3
  53. package/client/src/components/credentials/types.ts +12 -2
  54. package/client/src/components/credentials/useCredentialPanel.ts +43 -19
  55. package/client/src/components/icons/AIProviderIcons.tsx +16 -0
  56. package/client/src/components/onboarding/OnboardingWizard.tsx +23 -63
  57. package/client/src/components/onboarding/nodeRoleClasses.ts +23 -0
  58. package/client/src/components/onboarding/steps/CanvasStep.tsx +15 -21
  59. package/client/src/components/onboarding/steps/ConceptsStep.tsx +2 -11
  60. package/client/src/components/onboarding/steps/GetStartedStep.tsx +2 -10
  61. package/client/src/components/parameterPanel/InputSection.tsx +9 -7
  62. package/client/src/components/parameterPanel/MasterSkillEditor.tsx +84 -198
  63. package/client/src/components/parameterPanel/MiddleSection.tsx +57 -80
  64. package/client/src/components/parameterPanel/ToolSchemaEditor.tsx +31 -25
  65. package/client/src/components/parameterPanel/__tests__/InputSection.test.tsx +7 -2
  66. package/client/src/components/ui/AIResultModal.tsx +1 -1
  67. package/client/src/components/ui/CollapsibleSection.tsx +9 -5
  68. package/client/src/components/ui/CommandPalette.tsx +147 -0
  69. package/client/src/components/ui/CommandPaletteHost.tsx +189 -0
  70. package/client/src/components/ui/ComponentItem.tsx +13 -7
  71. package/client/src/components/ui/ComponentPalette.tsx +24 -13
  72. package/client/src/components/ui/ConsolePanel.tsx +19 -11
  73. package/client/src/components/ui/DropCap.tsx +28 -0
  74. package/client/src/components/ui/EditableNodeLabel.tsx +10 -2
  75. package/client/src/components/ui/InputNodesPanel.tsx +1 -1
  76. package/client/src/components/ui/Modal.tsx +38 -6
  77. package/client/src/components/ui/OutputDisplayPanel.tsx +1 -1
  78. package/client/src/components/ui/SettingsPanel.tsx +42 -13
  79. package/client/src/components/ui/StatusBar.tsx +108 -0
  80. package/client/src/components/ui/ThemeSwitcher.tsx +109 -0
  81. package/client/src/components/ui/TopToolbar.tsx +42 -25
  82. package/client/src/components/ui/WorkflowSidebar.tsx +32 -16
  83. package/client/src/components/ui/action-button.tsx +40 -15
  84. package/client/src/components/ui/button.tsx +24 -1
  85. package/client/src/components/ui/dropdown-menu.tsx +24 -2
  86. package/client/src/components/ui/input.tsx +19 -2
  87. package/client/src/components/ui/select.tsx +15 -0
  88. package/client/src/components/ui/textarea.tsx +15 -2
  89. package/client/src/contexts/AuthContext.tsx +148 -109
  90. package/client/src/contexts/ThemeContext.tsx +93 -17
  91. package/client/src/contexts/WebSocketContext.tsx +373 -206
  92. package/client/src/contexts/__tests__/AuthContext.test.tsx +221 -0
  93. package/client/src/hooks/__tests__/useDragVariable.test.ts +7 -1
  94. package/client/src/hooks/__tests__/useWorkflowOpsListener.test.ts +142 -0
  95. package/client/src/hooks/useAppTheme.ts +209 -7
  96. package/client/src/hooks/useAutoSkillEdges.ts +7 -2
  97. package/client/src/hooks/useCatalogueQuery.ts +67 -1
  98. package/client/src/hooks/useDragVariable.ts +1 -1
  99. package/client/src/hooks/useNodeAllowlist.ts +115 -8
  100. package/client/src/hooks/useOnboarding.ts +20 -8
  101. package/client/src/hooks/useParameterPanel.ts +2 -1
  102. package/client/src/hooks/useReactFlowNodes.ts +2 -1
  103. package/client/src/hooks/useSound.ts +185 -0
  104. package/client/src/hooks/useWorkflowManagement.ts +6 -8
  105. package/client/src/hooks/useWorkflowOpsListener.ts +90 -0
  106. package/client/src/index.css +65 -3
  107. package/client/src/lib/__tests__/connectionConfig.test.ts +91 -0
  108. package/client/src/lib/aiModelProviders.ts +8 -0
  109. package/client/src/lib/connectionConfig.ts +107 -0
  110. package/client/src/lib/queryPersist.ts +13 -5
  111. package/client/src/lib/sound.ts +393 -0
  112. package/client/src/main.tsx +20 -0
  113. package/client/src/store/useAppStore.ts +26 -0
  114. package/client/src/styles/canvasAnimations.ts +37 -36
  115. package/client/src/styles/theme.ts +36 -20
  116. package/client/src/test/setup.ts +1 -0
  117. package/client/src/themes/atomic.css +253 -0
  118. package/client/src/themes/base.css +373 -0
  119. package/client/src/themes/cyber.css +890 -0
  120. package/client/src/themes/dark.css +70 -0
  121. package/client/src/themes/edo.css +246 -0
  122. package/client/src/themes/greek.css +293 -0
  123. package/client/src/themes/light.css +78 -0
  124. package/client/src/themes/plague.css +253 -0
  125. package/client/src/themes/renaissance.css +727 -0
  126. package/client/src/themes/rot.css +249 -0
  127. package/client/src/themes/steampunk.css +272 -0
  128. package/client/src/themes/surveillance.css +289 -0
  129. package/client/src/themes/wasteland.css +250 -0
  130. package/client/src/types/INodeProperties.ts +5 -0
  131. package/client/src/types/NodeTypes.ts +11 -1
  132. package/client/src/types/__tests__/cloudEvents.test.ts +99 -0
  133. package/client/src/types/cloudEvents.ts +78 -0
  134. package/client/src/vite-env.d.ts +7 -0
  135. package/client/tsconfig.json +1 -1
  136. package/client/vite.config.js +62 -2
  137. package/install.ps1 +1 -1
  138. package/install.sh +1 -1
  139. package/machina/commands/build.py +51 -7
  140. package/machina/pyproject.toml +4 -0
  141. package/machina/supervisor.py +12 -2
  142. package/machina/tree.py +71 -21
  143. package/package.json +4 -4
  144. package/scripts/install.js +16 -1
  145. package/server/config/ai_cli_providers.json +54 -0
  146. package/server/config/credential_providers.json +109 -2
  147. package/server/config/llm_defaults.json +24 -0
  148. package/server/config/model_registry.json +338 -499
  149. package/server/config/node_allowlist.json +16 -1
  150. package/server/config/pricing.json +8 -0
  151. package/server/constants.py +38 -15
  152. package/server/core/container.py +2 -2
  153. package/server/core/credentials_database.py +35 -2
  154. package/server/core/logging.py +4 -3
  155. package/server/main.py +99 -13
  156. package/server/models/node_metadata.py +1 -0
  157. package/server/nodejs/package.json +8 -6
  158. package/server/nodejs/src/index.ts +22 -5
  159. package/server/nodes/README.md +31 -4
  160. package/server/nodes/agent/_inline.py +2 -0
  161. package/server/nodes/agent/_specialized.py +6 -3
  162. package/server/nodes/agent/ai_agent.py +13 -3
  163. package/server/nodes/agent/chat_agent.py +6 -3
  164. package/server/nodes/agent/claude_code_agent.py +287 -75
  165. package/server/nodes/agent/codex_agent.py +239 -0
  166. package/server/nodes/agent/deep_agent.py +3 -3
  167. package/server/nodes/agent/rlm_agent.py +3 -3
  168. package/server/nodes/android/__init__.py +31 -1
  169. package/server/nodes/android/_base.py +9 -5
  170. package/server/{services/android_service.py → nodes/android/_dispatcher.py} +2 -2
  171. package/server/nodes/android/_handlers.py +154 -0
  172. package/server/nodes/android/_option_loaders.py +44 -0
  173. package/server/nodes/android/_refresh.py +127 -0
  174. package/server/{services/android → nodes/android/_relay}/client.py +4 -4
  175. package/server/{routers/android.py → nodes/android/_router.py} +27 -8
  176. package/server/nodes/browser/browser.py +2 -2
  177. package/server/nodes/code/_base.py +6 -2
  178. package/server/nodes/code/_claude_code.py +134 -0
  179. package/server/nodes/document/embedding_generator.py +3 -3
  180. package/server/nodes/document/http_scraper.py +3 -3
  181. package/server/nodes/document/vector_store.py +5 -5
  182. package/server/nodes/email/__init__.py +11 -1
  183. package/server/nodes/email/_filters.py +21 -0
  184. package/server/{services/himalaya_service.py → nodes/email/_himalaya.py} +6 -10
  185. package/server/{services/email_service.py → nodes/email/_service.py} +9 -13
  186. package/server/nodes/email/email_read.py +1 -1
  187. package/server/nodes/email/email_receive.py +54 -5
  188. package/server/nodes/email/email_send.py +1 -1
  189. package/server/nodes/filesystem/shell.py +24 -1
  190. package/server/nodes/google/__init__.py +55 -1
  191. package/server/{services/handlers/google_auth.py → nodes/google/_auth_helper.py} +8 -5
  192. package/server/nodes/google/_base.py +2 -2
  193. package/server/nodes/google/_credentials.py +5 -5
  194. package/server/nodes/google/_filters.py +25 -0
  195. package/server/nodes/google/_handlers.py +57 -0
  196. package/server/{services/google_oauth.py → nodes/google/_oauth.py} +195 -162
  197. package/server/nodes/google/_option_loaders.py +107 -0
  198. package/server/nodes/google/_refresh.py +66 -0
  199. package/server/nodes/google/_router.py +131 -0
  200. package/server/nodes/google/gmail_receive.py +41 -4
  201. package/server/nodes/groups.py +1 -0
  202. package/server/nodes/location/_credentials.py +45 -1
  203. package/server/{services/maps.py → nodes/location/_service.py} +18 -3
  204. package/server/nodes/location/gmaps_create.py +4 -4
  205. package/server/nodes/location/gmaps_locations.py +4 -4
  206. package/server/nodes/location/gmaps_nearby_places.py +4 -4
  207. package/server/nodes/model/_base.py +8 -3
  208. package/server/nodes/model/_credentials.py +96 -8
  209. package/server/nodes/model/_local_validator.py +345 -0
  210. package/server/nodes/model/lmstudio_chat_model.py +23 -0
  211. package/server/nodes/model/ollama_chat_model.py +25 -0
  212. package/server/nodes/proxy/_usage.py +2 -2
  213. package/server/nodes/proxy/proxy_config.py +14 -14
  214. package/server/nodes/proxy/proxy_request.py +4 -4
  215. package/server/nodes/scraper/_credentials.py +29 -1
  216. package/server/nodes/scraper/apify_actor.py +9 -9
  217. package/server/nodes/scraper/crawlee_scraper.py +5 -5
  218. package/server/nodes/search/brave_search.py +4 -0
  219. package/server/nodes/search/perplexity_search.py +9 -0
  220. package/server/nodes/search/serper_search.py +3 -0
  221. package/server/nodes/skill/simple_memory.py +12 -0
  222. package/server/nodes/social/_base.py +2 -2
  223. package/server/nodes/stripe/__init__.py +46 -0
  224. package/server/nodes/stripe/_credentials.py +33 -0
  225. package/server/nodes/stripe/_handlers.py +270 -0
  226. package/server/nodes/stripe/_install.py +127 -0
  227. package/server/nodes/stripe/_source.py +174 -0
  228. package/server/nodes/stripe/stripe_action.py +81 -0
  229. package/server/nodes/stripe/stripe_receive.py +92 -0
  230. package/server/nodes/telegram/_credentials.py +52 -1
  231. package/server/nodes/telegram/_handlers.py +19 -18
  232. package/server/nodes/telegram/_service.py +134 -32
  233. package/server/nodes/telegram/telegram_send.py +5 -6
  234. package/server/nodes/text/file_handler.py +2 -2
  235. package/server/nodes/text/text_generator.py +2 -2
  236. package/server/nodes/tool/agent_builder.py +630 -0
  237. package/server/nodes/tool/task_manager.py +144 -2
  238. package/server/nodes/twitter/__init__.py +38 -1
  239. package/server/nodes/twitter/_base.py +7 -7
  240. package/server/nodes/twitter/_credentials.py +1 -1
  241. package/server/nodes/twitter/_filters.py +37 -0
  242. package/server/nodes/twitter/_handlers.py +77 -0
  243. package/server/nodes/twitter/_oauth.py +124 -0
  244. package/server/nodes/twitter/_refresh.py +78 -0
  245. package/server/nodes/twitter/_router.py +29 -0
  246. package/server/nodes/twitter/twitter_receive.py +4 -0
  247. package/server/nodes/visuals.json +64 -19
  248. package/server/nodes/whatsapp/__init__.py +45 -5
  249. package/server/nodes/whatsapp/_base.py +3 -3
  250. package/server/nodes/whatsapp/_filters.py +137 -0
  251. package/server/nodes/whatsapp/_handlers.py +167 -0
  252. package/server/nodes/whatsapp/_option_loaders.py +68 -0
  253. package/server/nodes/whatsapp/_refresh.py +62 -0
  254. package/server/nodes/whatsapp/_runtime.py +1 -1
  255. package/server/pyproject.toml +29 -7
  256. package/server/routers/schemas.py +2 -2
  257. package/server/routers/webhook.py +26 -9
  258. package/server/routers/websocket.py +149 -810
  259. package/server/services/ai.py +89 -8
  260. package/server/services/auth.py +220 -43
  261. package/server/services/claude_oauth.py +126 -100
  262. package/server/services/cli_agent/__init__.py +78 -0
  263. package/server/services/cli_agent/_handlers.py +237 -0
  264. package/server/services/cli_agent/config.py +112 -0
  265. package/server/services/cli_agent/factory.py +48 -0
  266. package/server/services/cli_agent/lockfile.py +141 -0
  267. package/server/services/cli_agent/mcp_server.py +482 -0
  268. package/server/services/cli_agent/protocol.py +173 -0
  269. package/server/services/cli_agent/providers/__init__.py +9 -0
  270. package/server/services/cli_agent/providers/anthropic_claude.py +419 -0
  271. package/server/services/cli_agent/providers/google_gemini.py +80 -0
  272. package/server/services/cli_agent/providers/openai_codex.py +310 -0
  273. package/server/services/cli_agent/service.py +607 -0
  274. package/server/services/cli_agent/session.py +618 -0
  275. package/server/services/cli_agent/types.py +227 -0
  276. package/server/services/cli_agent/workflow_tools.py +233 -0
  277. package/server/services/credential_registry.py +26 -1
  278. package/server/services/deployment/manager.py +26 -145
  279. package/server/services/deployment/poll_registry.py +59 -0
  280. package/server/services/event_waiter.py +76 -246
  281. package/server/services/events/__init__.py +54 -0
  282. package/server/services/events/cli.py +78 -0
  283. package/server/services/events/daemon.py +163 -0
  284. package/server/services/events/envelope.py +281 -0
  285. package/server/services/events/lifecycle.py +99 -0
  286. package/server/services/events/oauth_lifecycle.py +534 -0
  287. package/server/services/events/polling.py +60 -0
  288. package/server/services/events/push.py +36 -0
  289. package/server/services/events/source.py +63 -0
  290. package/server/services/events/triggers.py +118 -0
  291. package/server/services/events/verifiers/__init__.py +25 -0
  292. package/server/services/events/verifiers/base.py +28 -0
  293. package/server/services/events/verifiers/github.py +25 -0
  294. package/server/services/events/verifiers/hmac_basic.py +32 -0
  295. package/server/services/events/verifiers/standard_webhooks.py +47 -0
  296. package/server/services/events/verifiers/stripe.py +42 -0
  297. package/server/services/events/webhook.py +105 -0
  298. package/server/services/handlers/tools.py +28 -186
  299. package/server/services/llm/config.py +7 -0
  300. package/server/services/llm/factory.py +8 -2
  301. package/server/services/memory/__init__.py +52 -0
  302. package/server/services/memory/jsonl.py +80 -0
  303. package/server/services/memory/markdown.py +65 -0
  304. package/server/services/memory/state.py +112 -0
  305. package/server/services/memory/vector_store.py +40 -0
  306. package/server/services/model_registry.py +76 -0
  307. package/server/services/node_allowlist.py +71 -15
  308. package/server/services/node_executor.py +2 -2
  309. package/server/services/node_output_schemas.py +21 -10
  310. package/server/services/node_spec.py +1 -1
  311. package/server/services/oauth_utils.py +1 -1
  312. package/server/services/plugin/__init__.py +2 -0
  313. package/server/services/plugin/base.py +44 -2
  314. package/server/services/plugin/credential.py +288 -1
  315. package/server/services/plugin/deps.py +105 -0
  316. package/server/services/plugin/edge_walker.py +12 -4
  317. package/server/services/plugin/oauth.py +381 -0
  318. package/server/services/plugin/polling.py +247 -0
  319. package/server/services/plugin/registry.py +145 -0
  320. package/server/services/plugin/singleton.py +65 -0
  321. package/server/services/plugin/ws.py +81 -0
  322. package/server/services/process_service.py +31 -2
  323. package/server/services/status_broadcaster.py +155 -238
  324. package/server/services/temporal/workflow.py +7 -7
  325. package/server/services/workflow.py +21 -3
  326. package/server/services/ws_handler_registry.py +111 -28
  327. package/server/skills/GUIDE.md +16 -1
  328. package/server/skills/assistant/agent-builder-skill/SKILL.md +166 -0
  329. package/server/skills/payments_agent/stripe-skill/SKILL.md +306 -0
  330. package/server/tests/credentials/test_auth_service.py +16 -9
  331. package/server/tests/credentials/test_credential_broadcasts.py +219 -0
  332. package/server/tests/credentials/test_google_oauth.py +6 -6
  333. package/server/tests/credentials/test_oauth_utils.py +1 -1
  334. package/server/tests/credentials/test_twitter_oauth.py +2 -2
  335. package/server/tests/credentials/test_websocket_handlers.py +44 -20
  336. package/server/tests/llm/test_factory.py +1 -0
  337. package/server/tests/llm/test_wiring.py +5 -1
  338. package/server/tests/nodes/_compat.py +24 -24
  339. package/server/tests/nodes/test_agent_builder.py +439 -0
  340. package/server/tests/nodes/test_ai_tools.py +18 -14
  341. package/server/tests/nodes/test_code_fs_process.py +17 -8
  342. package/server/tests/nodes/test_email.py +10 -9
  343. package/server/tests/nodes/test_google_workspace.py +2 -2
  344. package/server/tests/nodes/test_specialized_agents.py +100 -53
  345. package/server/tests/nodes/test_stripe_plugin.py +293 -0
  346. package/server/tests/nodes/test_telegram_social.py +4 -4
  347. package/server/tests/nodes/test_twitter.py +1 -1
  348. package/server/tests/nodes/test_web_automation.py +2 -2
  349. package/server/tests/nodes/test_whatsapp.py +9 -9
  350. package/server/tests/services/cli_agent/__init__.py +0 -0
  351. package/server/tests/services/cli_agent/test_mcp_server.py +432 -0
  352. package/server/tests/services/cli_agent/test_providers.py +358 -0
  353. package/server/tests/services/cli_agent/test_service.py +298 -0
  354. package/server/tests/services/memory/__init__.py +0 -0
  355. package/server/tests/services/memory/test_jsonl.py +188 -0
  356. package/server/tests/services/test_events.py +333 -0
  357. package/server/tests/test_node_spec.py +56 -16
  358. package/server/tests/test_plugin_helpers.py +116 -0
  359. package/server/tests/test_plugin_self_containment.py +486 -0
  360. package/server/tests/test_status_broadcasts.py +425 -0
  361. package/workflows/{AI Assistant_workflow-1777421105154-0m4snkzjf.json → AI Assistant_workflow-1778504793388-ou1m1tz2x.json } +70 -266
  362. package/workflows/{AI Employee_workflow-1777720598005-u4cm858dv.json → AI Employee_example_workflow-1777720598005-u4cm858dv.json } +112 -112
  363. package/workflows/Claude Assistant_workflow-1778380124051-mdibn807c.json +709 -0
  364. package/client/dist/assets/ActionBar-vzPpSR77.js +0 -1
  365. package/client/dist/assets/ApiKeyInput-Ds7AKFe8.js +0 -1
  366. package/client/dist/assets/ApiKeyPanel-gfblELep.js +0 -1
  367. package/client/dist/assets/ApiUsageSection-BMNWTe2r.js +0 -1
  368. package/client/dist/assets/EmailPanel-B1Om64p5.js +0 -1
  369. package/client/dist/assets/OAuthPanel-CXyQYGBz.js +0 -1
  370. package/client/dist/assets/QrPairingPanel-BgNuI1we.js +0 -1
  371. package/client/dist/assets/RateLimitSection-YYK8sx1T.js +0 -1
  372. package/client/dist/assets/StatusCard-DuYA5hJR.js +0 -1
  373. package/client/dist/assets/index-D9tZfgvi.js +0 -363
  374. package/client/dist/assets/index-al7snTkG.css +0 -1
  375. package/client/src/components/credentials/providers.tsx +0 -177
  376. package/server/routers/google.py +0 -277
  377. package/server/routers/maps.py +0 -142
  378. package/server/routers/twitter.py +0 -365
  379. package/server/services/claude_code_service.py +0 -106
  380. package/server/services/memory.py +0 -159
  381. package/server/services/node_option_loaders/__init__.py +0 -77
  382. package/server/services/node_option_loaders/android_loaders.py +0 -55
  383. package/server/services/node_option_loaders/google_loaders.py +0 -97
  384. package/server/services/node_option_loaders/whatsapp_loaders.py +0 -69
  385. package/server/services/twitter_oauth.py +0 -411
  386. package/server/services/websocket_client.py +0 -29
  387. /package/server/{services/android → nodes/android/_relay}/__init__.py +0 -0
  388. /package/server/{services/android → nodes/android/_relay}/broadcaster.py +0 -0
  389. /package/server/{services/android → nodes/android/_relay}/manager.py +0 -0
  390. /package/server/{services/android → nodes/android/_relay}/protocol.py +0 -0
  391. /package/server/{services/browser_service.py → nodes/browser/_service.py} +0 -0
  392. /package/server/{services/whatsapp_service.py → nodes/whatsapp/_service.py} +0 -0
  393. /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