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,358 @@
1
+ """Unit tests for `AnthropicClaudeProvider` and `OpenAICodexProvider`.
2
+
3
+ Covers:
4
+ - `headless_argv` shape (every flag in the right place, defaults
5
+ applied, optional fields omitted when unset)
6
+ - `parse_event` round-trips JSON correctly, returns None for garbage
7
+ - `is_final_event` matches the right event types
8
+ - `event_to_session_result` reconstructs cost / session_id /
9
+ canonical_usage / response from vendored NDJSON
10
+ - `detect_auth_error` matches "not logged in" stderr patterns
11
+ - `supports()` flags align with `ai_cli_providers.json`
12
+ - Factory raises NotImplementedError for gemini
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ from pathlib import Path
19
+
20
+ import pytest
21
+
22
+ from services.cli_agent import (
23
+ ClaudeTaskSpec,
24
+ CodexTaskSpec,
25
+ create_cli_provider,
26
+ )
27
+ from services.cli_agent.factory import is_supported
28
+ from services.cli_agent.protocol import AICliProvider, CanonicalUsage
29
+
30
+
31
+ # ---------------------------------------------------------------------------
32
+ # Factory contract
33
+ # ---------------------------------------------------------------------------
34
+
35
+ class TestFactory:
36
+ def test_claude_creates_provider(self):
37
+ p = create_cli_provider("claude")
38
+ assert p.name == "claude"
39
+ assert isinstance(p, AICliProvider)
40
+
41
+ def test_codex_creates_provider(self):
42
+ p = create_cli_provider("codex")
43
+ assert p.name == "codex"
44
+ assert isinstance(p, AICliProvider)
45
+
46
+ def test_gemini_raises_not_implemented(self):
47
+ with pytest.raises(NotImplementedError, match="deferred to v2"):
48
+ create_cli_provider("gemini")
49
+
50
+ def test_unknown_raises_value_error(self):
51
+ with pytest.raises(ValueError, match="Unknown CLI provider"):
52
+ create_cli_provider("openai")
53
+
54
+ def test_is_supported(self):
55
+ assert is_supported("claude") is True
56
+ assert is_supported("codex") is True
57
+ assert is_supported("gemini") is False
58
+ assert is_supported("nope") is False
59
+
60
+
61
+ # ---------------------------------------------------------------------------
62
+ # Claude provider
63
+ # ---------------------------------------------------------------------------
64
+
65
+ @pytest.fixture
66
+ def claude_provider():
67
+ return create_cli_provider("claude")
68
+
69
+
70
+ class TestClaudeArgv:
71
+ def test_minimum_required_flags(self, claude_provider):
72
+ task = ClaudeTaskSpec(prompt="hello world")
73
+ argv = claude_provider.headless_argv(task, defaults={})
74
+ # `-p hello world` must be present
75
+ assert "-p" in argv
76
+ assert "hello world" in argv
77
+ # stream-json output format
78
+ assert "--output-format" in argv
79
+ assert "stream-json" in argv
80
+ # Defaults applied
81
+ assert "--model" in argv
82
+ assert "claude-sonnet-4-6" in argv # default_model from JSON
83
+ assert "--max-turns" in argv
84
+ assert "10" in argv # default_max_turns
85
+ assert "--max-budget-usd" in argv
86
+ assert "5.0" in argv # default_max_budget_usd
87
+ assert "--allowedTools" in argv
88
+ assert "--permission-mode" in argv
89
+
90
+ def test_session_id_flag(self, claude_provider):
91
+ task = ClaudeTaskSpec(prompt="x", session_id="sess-abc-123")
92
+ argv = claude_provider.headless_argv(task, defaults={})
93
+ assert "--session-id" in argv
94
+ assert "sess-abc-123" in argv
95
+ assert "--resume" not in argv
96
+
97
+ def test_resume_wins_over_session_id(self, claude_provider):
98
+ task = ClaudeTaskSpec(
99
+ prompt="x",
100
+ session_id="new-sess",
101
+ resume_session_id="prior-sess",
102
+ )
103
+ argv = claude_provider.headless_argv(task, defaults={})
104
+ assert "--resume" in argv
105
+ assert "prior-sess" in argv
106
+ assert "--session-id" not in argv
107
+
108
+ def test_zero_budget_omits_flag(self, claude_provider):
109
+ task = ClaudeTaskSpec(prompt="x", max_budget_usd=0.0)
110
+ argv = claude_provider.headless_argv(task, defaults={})
111
+ assert "--max-budget-usd" not in argv
112
+
113
+ def test_system_prompt_propagates(self, claude_provider):
114
+ task = ClaudeTaskSpec(prompt="x", system_prompt="be concise")
115
+ argv = claude_provider.headless_argv(task, defaults={})
116
+ assert "--append-system-prompt" in argv
117
+ assert "be concise" in argv
118
+
119
+ def test_wrong_task_type_raises(self, claude_provider):
120
+ codex_task = CodexTaskSpec(prompt="x")
121
+ with pytest.raises(TypeError, match="ClaudeTaskSpec"):
122
+ claude_provider.headless_argv(codex_task, defaults={})
123
+
124
+
125
+ class TestClaudeParseEvent:
126
+ def test_valid_json_round_trips(self, claude_provider):
127
+ line = json.dumps({"type": "result", "result": "42", "session_id": "abc"})
128
+ event = claude_provider.parse_event(line)
129
+ assert event is not None
130
+ assert event["type"] == "result"
131
+ assert event["result"] == "42"
132
+
133
+ def test_garbage_returns_none(self, claude_provider):
134
+ assert claude_provider.parse_event("not json") is None
135
+ assert claude_provider.parse_event("") is None
136
+ assert claude_provider.parse_event(" ") is None
137
+
138
+ def test_is_final_event(self, claude_provider):
139
+ assert claude_provider.is_final_event({"type": "result"}) is True
140
+ assert claude_provider.is_final_event({"type": "assistant"}) is False
141
+ assert claude_provider.is_final_event({"type": "system"}) is False
142
+
143
+
144
+ class TestClaudeEventToSessionResult:
145
+ """Vendored Claude stream-json fixture verified end-to-end."""
146
+
147
+ @pytest.fixture
148
+ def fixture_events(self):
149
+ return [
150
+ {"type": "system", "subtype": "init", "session_id": "sess-1"},
151
+ {
152
+ "type": "assistant",
153
+ "message": {
154
+ "content": [
155
+ {"type": "text", "text": "Sure, here's..."},
156
+ {"type": "tool_use", "id": "t1", "name": "Read", "input": {}},
157
+ ],
158
+ },
159
+ "session_id": "sess-1",
160
+ },
161
+ {
162
+ "type": "tool_use",
163
+ "tool_name": "Edit",
164
+ "tool_input": {"file_path": "x.py"},
165
+ },
166
+ {
167
+ "type": "result",
168
+ "subtype": "success",
169
+ "result": "Done — refactored to async.",
170
+ "total_cost_usd": 0.4231,
171
+ "duration_ms": 18234,
172
+ "num_turns": 7,
173
+ "session_id": "sess-1",
174
+ "usage": {
175
+ "input_tokens": 12000,
176
+ "output_tokens": 3500,
177
+ "cache_creation_input_tokens": 500,
178
+ "cache_read_input_tokens": 8000,
179
+ },
180
+ },
181
+ ]
182
+
183
+ def test_reconstructs_response(self, claude_provider, fixture_events):
184
+ result = claude_provider.event_to_session_result(fixture_events, "", 0)
185
+ assert result["response"] == "Done — refactored to async."
186
+
187
+ def test_reconstructs_cost(self, claude_provider, fixture_events):
188
+ result = claude_provider.event_to_session_result(fixture_events, "", 0)
189
+ assert result["cost_usd"] == pytest.approx(0.4231)
190
+
191
+ def test_reconstructs_session_id(self, claude_provider, fixture_events):
192
+ result = claude_provider.event_to_session_result(fixture_events, "", 0)
193
+ assert result["session_id"] == "sess-1"
194
+
195
+ def test_reconstructs_duration_and_turns(self, claude_provider, fixture_events):
196
+ result = claude_provider.event_to_session_result(fixture_events, "", 0)
197
+ assert result["duration_ms"] == 18234
198
+ assert result["num_turns"] == 7
199
+
200
+ def test_canonical_usage_normalises(self, claude_provider, fixture_events):
201
+ cu: CanonicalUsage = claude_provider.canonical_usage(fixture_events)
202
+ assert cu.input_tokens == 12000
203
+ assert cu.output_tokens == 3500
204
+ assert cu.cache_read == 8000 # remapped from cache_read_input_tokens
205
+ assert cu.cache_write == 500 # remapped from cache_creation_input_tokens
206
+ assert cu.request_count == 7 # from num_turns
207
+
208
+ def test_counts_tool_calls(self, claude_provider, fixture_events):
209
+ # Two tool_use events: one inside an assistant message, one standalone
210
+ result = claude_provider.event_to_session_result(fixture_events, "", 0)
211
+ assert result["tool_calls"] >= 2
212
+
213
+ def test_success_on_zero_exit_with_result_event(self, claude_provider, fixture_events):
214
+ result = claude_provider.event_to_session_result(fixture_events, "", 0)
215
+ assert result["success"] is True
216
+ assert result["error"] is None
217
+
218
+ def test_failure_on_non_zero_exit(self, claude_provider, fixture_events):
219
+ result = claude_provider.event_to_session_result(
220
+ fixture_events, "exploded", 1,
221
+ )
222
+ assert result["success"] is False
223
+ assert "exploded" in (result["error"] or "")
224
+
225
+ def test_failure_on_missing_result_event(self, claude_provider):
226
+ events = [
227
+ {"type": "system", "subtype": "init", "session_id": "s"},
228
+ {"type": "assistant", "message": {}, "session_id": "s"},
229
+ ]
230
+ result = claude_provider.event_to_session_result(events, "", 0)
231
+ assert result["success"] is False
232
+ assert "no result event" in (result["error"] or "")
233
+
234
+
235
+ class TestClaudeAuthDetection:
236
+ def test_logged_out_marker(self, claude_provider):
237
+ assert claude_provider.detect_auth_error(
238
+ "Please run 'claude login' first.", 1,
239
+ ) is True
240
+
241
+ def test_clean_run_not_auth_error(self, claude_provider):
242
+ assert claude_provider.detect_auth_error("", 0) is False
243
+
244
+ def test_unrelated_stderr_not_auth_error(self, claude_provider):
245
+ assert claude_provider.detect_auth_error(
246
+ "git: pathspec 'x' did not match any files\n", 1,
247
+ ) is False
248
+
249
+
250
+ class TestClaudeSupports:
251
+ def test_supports_full_feature_set(self, claude_provider):
252
+ for feature in (
253
+ "max_budget", "max_turns", "session_id", "resume",
254
+ "mcp_runtime", "json_cost", "ide_lockfile",
255
+ ):
256
+ assert claude_provider.supports(feature), feature
257
+
258
+ def test_does_not_support_sandbox(self, claude_provider):
259
+ assert claude_provider.supports("sandbox") is False
260
+
261
+
262
+ # ---------------------------------------------------------------------------
263
+ # Codex provider
264
+ # ---------------------------------------------------------------------------
265
+
266
+ @pytest.fixture
267
+ def codex_provider():
268
+ return create_cli_provider("codex")
269
+
270
+
271
+ class TestCodexArgv:
272
+ def test_no_session_no_budget_no_turns(self, codex_provider):
273
+ task = CodexTaskSpec(prompt="hello")
274
+ argv = codex_provider.headless_argv(task, defaults={})
275
+ assert "--max-turns" not in argv
276
+ assert "--max-budget-usd" not in argv
277
+ assert "--session-id" not in argv
278
+ assert "--resume" not in argv
279
+ assert "--allowedTools" not in argv
280
+
281
+ def test_sandbox_flag(self, codex_provider):
282
+ task = CodexTaskSpec(prompt="x", sandbox="read-only")
283
+ argv = codex_provider.headless_argv(task, defaults={})
284
+ assert "--sandbox" in argv
285
+ assert "read-only" in argv
286
+
287
+ def test_ask_for_approval_flag(self, codex_provider):
288
+ task = CodexTaskSpec(prompt="x", ask_for_approval="on-request")
289
+ argv = codex_provider.headless_argv(task, defaults={})
290
+ assert "--ask-for-approval" in argv
291
+ assert "on-request" in argv
292
+
293
+ def test_default_sandbox_workspace_write(self, codex_provider):
294
+ task = CodexTaskSpec(prompt="x")
295
+ argv = codex_provider.headless_argv(task, defaults={})
296
+ idx = argv.index("--sandbox")
297
+ assert argv[idx + 1] == "workspace-write"
298
+
299
+ def test_system_prompt_prepended(self, codex_provider):
300
+ task = CodexTaskSpec(prompt="user thing", system_prompt="be careful")
301
+ argv = codex_provider.headless_argv(task, defaults={})
302
+ # Codex has no --system-prompt flag; we prepend with <system> tags
303
+ prompt_arg = argv[-1] # last arg is the prompt
304
+ assert "<system>" in prompt_arg
305
+ assert "be careful" in prompt_arg
306
+ assert "user thing" in prompt_arg
307
+
308
+ def test_wrong_task_type_raises(self, codex_provider):
309
+ claude_task = ClaudeTaskSpec(prompt="x")
310
+ with pytest.raises(TypeError, match="CodexTaskSpec"):
311
+ codex_provider.headless_argv(claude_task, defaults={})
312
+
313
+
314
+ class TestCodexEventReconstruction:
315
+ @pytest.fixture
316
+ def fixture_events(self):
317
+ # Codex has no public stream-json schema; this is a plausible
318
+ # synthetic stream that exercises our best-effort matchers.
319
+ return [
320
+ {"type": "message", "text": "Working on it..."},
321
+ {"type": "assistant", "text": "Final answer is X."},
322
+ {"type": "complete", "duration_ms": 5000, "stats": {"turns": 3}},
323
+ ]
324
+
325
+ def test_extracts_response_from_final_event(self, codex_provider, fixture_events):
326
+ result = codex_provider.event_to_session_result(fixture_events, "", 0)
327
+ # Response comes from final event's `text`/`response`/`result`/`content`
328
+ # if present; otherwise the last assistant message.
329
+ assert "Final answer is X." in result["response"]
330
+
331
+ def test_cost_always_none_for_codex(self, codex_provider, fixture_events):
332
+ result = codex_provider.event_to_session_result(fixture_events, "", 0)
333
+ assert result["cost_usd"] is None
334
+
335
+ def test_canonical_usage_zeros(self, codex_provider, fixture_events):
336
+ cu = codex_provider.canonical_usage(fixture_events)
337
+ assert cu.input_tokens == 0
338
+ assert cu.output_tokens == 0
339
+
340
+
341
+ class TestCodexAuthDetection:
342
+ def test_openai_api_key_missing(self, codex_provider):
343
+ assert codex_provider.detect_auth_error(
344
+ "Error: OPENAI_API_KEY not set.\n", 1,
345
+ ) is True
346
+
347
+ def test_401_marker(self, codex_provider):
348
+ assert codex_provider.detect_auth_error("HTTP 401 Unauthorized\n", 1) is True
349
+
350
+
351
+ class TestCodexSupports:
352
+ def test_supports_only_sandbox(self, codex_provider):
353
+ assert codex_provider.supports("sandbox") is True
354
+ for feature in (
355
+ "max_budget", "max_turns", "session_id", "resume",
356
+ "json_cost", "ide_lockfile",
357
+ ):
358
+ assert codex_provider.supports(feature) is False, feature
@@ -0,0 +1,298 @@
1
+ """`AICliService` tests — fail-fast paths that don't spawn the CLI.
2
+
3
+ The full subprocess-driven path is covered by live verification (see
4
+ `docs-internal/cli_agent_framework.md` → Verification §5–7). These unit
5
+ tests cover:
6
+ - `working_directory_not_git_repo` abort (no pool constructed)
7
+ - resolver contract: explicit `repo_root` doesn't fall back to cwd
8
+ - factory NotImplementedError surfaces cleanly
9
+ - cancel_workflow / cancel_node return zero when nothing's running
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import asyncio
15
+ import tempfile
16
+ from pathlib import Path
17
+ from unittest.mock import MagicMock
18
+
19
+ import pytest
20
+
21
+ from services.cli_agent import ClaudeTaskSpec, CodexTaskSpec
22
+ from services.cli_agent.service import AICliService, get_ai_cli_service
23
+
24
+
25
+ @pytest.mark.asyncio
26
+ async def test_not_git_repo_returns_structured_failure():
27
+ """Caller-supplied repo_root that isn't a git repo → fail-fast,
28
+ every task surfaces `working_directory_not_git_repo`."""
29
+ svc = AICliService()
30
+ with tempfile.TemporaryDirectory() as tmp:
31
+ result = await svc.run_batch(
32
+ "claude",
33
+ tasks=[
34
+ ClaudeTaskSpec(prompt="task A"),
35
+ ClaudeTaskSpec(prompt="task B"),
36
+ ],
37
+ node_id="n",
38
+ workflow_id="wf",
39
+ workspace_dir=Path(tmp),
40
+ broadcaster=None,
41
+ repo_root=Path(tmp), # explicit non-git
42
+ )
43
+
44
+ assert result.n_tasks == 2
45
+ assert result.n_succeeded == 0
46
+ assert result.n_failed == 2
47
+ assert all(t.error == "working_directory_not_git_repo" for t in result.tasks)
48
+ assert result.total_cost_usd is None
49
+
50
+
51
+ @pytest.mark.asyncio
52
+ async def test_explicit_repo_root_does_not_fallback_to_cwd():
53
+ """When the caller passes an explicit `repo_root`, the resolver
54
+ must NOT walk to cwd. This is the bug we fixed during Phase 5
55
+ smoke-testing."""
56
+ # cwd is the framework worktree (a git repo). If the resolver fell
57
+ # back, it would silently succeed — bad.
58
+ svc = AICliService()
59
+ with tempfile.TemporaryDirectory() as tmp:
60
+ result = await svc.run_batch(
61
+ "claude",
62
+ tasks=[ClaudeTaskSpec(prompt="x")],
63
+ node_id="n",
64
+ workflow_id="wf",
65
+ workspace_dir=Path(tmp),
66
+ broadcaster=None,
67
+ repo_root=Path(tmp),
68
+ )
69
+ assert result.n_failed == 1
70
+ assert result.tasks[0].error == "working_directory_not_git_repo"
71
+
72
+
73
+ @pytest.mark.asyncio
74
+ async def test_codex_provider_works():
75
+ """Codex factory must build a provider; happy-path argv was already
76
+ covered in test_providers.py."""
77
+ svc = AICliService()
78
+ with tempfile.TemporaryDirectory() as tmp:
79
+ # Same not-git-repo path with Codex — confirms the provider
80
+ # discrim works through the service.
81
+ result = await svc.run_batch(
82
+ "codex",
83
+ tasks=[CodexTaskSpec(prompt="x", sandbox="read-only")],
84
+ node_id="n",
85
+ workflow_id="wf",
86
+ workspace_dir=Path(tmp),
87
+ broadcaster=None,
88
+ repo_root=Path(tmp),
89
+ )
90
+ assert result.provider == "codex"
91
+ assert result.tasks[0].provider == "codex"
92
+
93
+
94
+ @pytest.mark.asyncio
95
+ async def test_gemini_factory_raises_not_implemented():
96
+ svc = AICliService()
97
+ with tempfile.TemporaryDirectory() as tmp:
98
+ with pytest.raises(NotImplementedError, match="deferred to v2"):
99
+ await svc.run_batch(
100
+ "gemini",
101
+ tasks=[], # spec irrelevant — factory raises before construction
102
+ node_id="n",
103
+ workflow_id="wf",
104
+ workspace_dir=Path(tmp),
105
+ broadcaster=None,
106
+ repo_root=Path(tmp),
107
+ )
108
+
109
+
110
+ @pytest.mark.asyncio
111
+ async def test_cancel_when_no_active_pools():
112
+ svc = AICliService()
113
+ assert await svc.cancel_workflow("nothing") == 0
114
+ assert await svc.cancel_node("nothing") == 0
115
+
116
+
117
+ def test_singleton_accessor_returns_same_instance():
118
+ a = get_ai_cli_service()
119
+ b = get_ai_cli_service()
120
+ assert a is b
121
+
122
+
123
+ @pytest.mark.asyncio
124
+ async def test_resolver_walks_upward_to_find_git():
125
+ """Without `override`, the resolver tries workspace_dir then cwd.
126
+
127
+ The cli-agent-framework worktree is a git repo. A deep child path
128
+ should resolve via `git rev-parse --show-toplevel` to the worktree root.
129
+ """
130
+ deep = Path(__file__).resolve().parent / "deep" / "deeper"
131
+ root = await AICliService._resolve_repo_root(workspace_dir=deep, override=None)
132
+ assert root is not None
133
+ assert (root / ".git").exists()
134
+
135
+
136
+ @pytest.mark.asyncio
137
+ async def test_resolver_returns_none_when_override_not_git():
138
+ import tempfile
139
+ with tempfile.TemporaryDirectory() as tmp:
140
+ root = await AICliService._resolve_repo_root(
141
+ workspace_dir=Path(tmp), # ignored when override is set
142
+ override=Path(tmp),
143
+ )
144
+ assert root is None
145
+
146
+
147
+ # ---------------------------------------------------------------------------
148
+ # Execution-engine integration: drive the real `run_batch` (via the DI
149
+ # accessor) and assert the `[CC-Agent ...]` log lines fire at the right
150
+ # transitions. This replaces direct contextvar pokes — every assertion
151
+ # walks the same resolver / register_batch / session-start path the
152
+ # production code does.
153
+ #
154
+ # The top-level `tests/conftest.py` stubs `core.logging.get_logger` with
155
+ # a shared MagicMock, so we can't use `caplog`. Instead each module's
156
+ # module-level ``logger`` IS that shared MagicMock — we inspect its
157
+ # ``info``/``warning`` ``call_args_list`` to verify the diagnostic
158
+ # chain fires.
159
+ # ---------------------------------------------------------------------------
160
+
161
+
162
+ def _logger_messages(*module_loggers) -> str:
163
+ """Concatenate every ``.info`` / ``.warning`` / ``.error`` call's
164
+ rendered template+args across one or more module-level loggers."""
165
+ out: list[str] = []
166
+ for lg in module_loggers:
167
+ for level in ("info", "warning", "error", "exception", "debug"):
168
+ method = getattr(lg, level, None)
169
+ if method is None or not hasattr(method, "call_args_list"):
170
+ continue
171
+ for call in method.call_args_list:
172
+ args = call.args or ()
173
+ if not args:
174
+ continue
175
+ template = args[0]
176
+ if not isinstance(template, str):
177
+ out.append(repr(template))
178
+ continue
179
+ try:
180
+ out.append(template % args[1:] if len(args) > 1 else template)
181
+ except (TypeError, ValueError):
182
+ out.append(template + " | " + repr(args[1:]))
183
+ return "\n".join(out)
184
+
185
+
186
+ @pytest.mark.asyncio
187
+ async def test_run_batch_emits_diagnostic_logs_when_workspace_not_git(monkeypatch):
188
+ """Abort path: `run_batch` enters, fails the resolver, returns
189
+ structured failure. The `[CC-Agent run_batch] enter` and
190
+ `[CC-Agent run_batch] aborting` log lines must fire so the operator
191
+ sees WHY the batch never reached `register_batch`."""
192
+ # Force the module-level logger to a Mock for THIS test, regardless of
193
+ # whether conftest's `core.logging.get_logger` stub was active at the
194
+ # moment the service module was first imported. In the full-suite
195
+ # ordering some sibling test imports `core.logging` after the stub is
196
+ # placed, leaving the cli_agent module with a real structlog
197
+ # BoundLogger that has no `reset_mock`. Locally patching makes the test
198
+ # robust to import-order pollution.
199
+ from services.cli_agent import service as svc_mod
200
+ svc_logger = MagicMock()
201
+ monkeypatch.setattr(svc_mod, "logger", svc_logger)
202
+
203
+ svc = get_ai_cli_service() # DI singleton — same accessor production uses
204
+ import tempfile
205
+ with tempfile.TemporaryDirectory() as tmp:
206
+ result = await svc.run_batch(
207
+ "claude",
208
+ tasks=[ClaudeTaskSpec(prompt="hello")],
209
+ node_id="ccode_test_abort",
210
+ workflow_id="wf_abort",
211
+ workspace_dir=Path(tmp),
212
+ broadcaster=None,
213
+ repo_root=Path(tmp),
214
+ connected_tools=[
215
+ {"node_id": "ddg_1", "node_type": "duckduckgoSearch",
216
+ "label": "DDG", "parameters": {}},
217
+ ],
218
+ )
219
+
220
+ text = _logger_messages(svc_logger)
221
+ assert "[CC-Agent run_batch] enter" in text, text
222
+ assert "duckduckgoSearch" in text, text
223
+ assert "[CC-Agent run_batch] aborting" in text, text
224
+ # The abort path MUST NOT register a batch (no MCP tokens leaked):
225
+ assert "[CC-Agent MCP register_batch]" not in text, text
226
+ assert result.n_failed == 1
227
+
228
+
229
+ @pytest.mark.asyncio
230
+ async def test_run_batch_registers_mcp_batch_on_happy_path(monkeypatch):
231
+ """Happy path: real resolver, real `register_batch`, but
232
+ `AICliSession.start` is short-circuited so we don't spawn `claude`.
233
+ Asserts the `[CC-Agent MCP register_batch]` log fires with the
234
+ expected tool list — proving the diagnostic chain is intact
235
+ end-to-end and the spawned CLI WOULD see the MCP server registered
236
+ for it."""
237
+ from services.cli_agent import session as session_mod
238
+ from services.cli_agent import service as svc_mod
239
+ from services.cli_agent import mcp_server as mcp_mod
240
+ from services.cli_agent.protocol import SessionResult
241
+
242
+ # See the abort-path test for the rationale — patch the module-level
243
+ # logger directly so the test does not depend on conftest's
244
+ # `core.logging.get_logger` stub being active at import time.
245
+ svc_logger = MagicMock()
246
+ mcp_logger = MagicMock()
247
+ monkeypatch.setattr(svc_mod, "logger", svc_logger)
248
+ monkeypatch.setattr(mcp_mod, "logger", mcp_logger)
249
+
250
+ started = {"count": 0}
251
+
252
+ async def _fake_start(self): # noqa: ANN001
253
+ started["count"] += 1
254
+ self._completed = True
255
+
256
+ async def _fake_wait(self, timeout): # noqa: ANN001, ARG002
257
+ return SessionResult(
258
+ task_id=self.task_id, provider=self._provider.name,
259
+ prompt=getattr(self._task, "prompt", ""),
260
+ success=True, response="stub",
261
+ )
262
+
263
+ async def _fake_cleanup(self): # noqa: ANN001
264
+ pass
265
+
266
+ monkeypatch.setattr(session_mod.AICliSession, "start", _fake_start)
267
+ monkeypatch.setattr(session_mod.AICliSession, "wait_for_completion", _fake_wait)
268
+ monkeypatch.setattr(session_mod.AICliSession, "cleanup", _fake_cleanup)
269
+
270
+ svc = get_ai_cli_service()
271
+ workspace = Path(__file__).resolve().parents[3] # the repo root (a git repo)
272
+ result = await svc.run_batch(
273
+ "claude",
274
+ tasks=[ClaudeTaskSpec(prompt="ping")],
275
+ node_id="ccode_test_happy",
276
+ workflow_id="wf_happy",
277
+ workspace_dir=workspace,
278
+ broadcaster=None,
279
+ repo_root=None, # let the resolver find the parent .git
280
+ connected_tools=[
281
+ {"node_id": "ddg_1", "node_type": "duckduckgoSearch",
282
+ "label": "DDG", "parameters": {}},
283
+ ],
284
+ connected_skill_names=["duckduckgo-search-skill"],
285
+ )
286
+
287
+ text = _logger_messages(svc_logger, mcp_logger)
288
+ # Engine-entry log:
289
+ assert "[CC-Agent run_batch] enter" in text, text
290
+ assert "duckduckgoSearch" in text, text
291
+ # Resolver succeeded:
292
+ assert "[CC-Agent run_batch] resolved repo_root=" in text, text
293
+ # MCP token registered with our connected tool:
294
+ assert "[CC-Agent MCP register_batch]" in text, text
295
+ # Session was actually exercised (proves the engine ran the inner
296
+ # gather, not just the abort path):
297
+ assert started["count"] == 1
298
+ assert result.n_succeeded == 1
File without changes