@lobehub/lobehub 2.0.0-next.337 → 2.0.0-next.339

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 (270) hide show
  1. package/.gitattributes +35 -0
  2. package/CHANGELOG.md +69 -0
  3. package/changelog/v1.json +24 -0
  4. package/locales/ar/plugin.json +12 -2
  5. package/locales/ar/providers.json +1 -0
  6. package/locales/ar/setting.json +77 -1
  7. package/locales/bg-BG/models.json +5 -10
  8. package/locales/bg-BG/plugin.json +12 -2
  9. package/locales/bg-BG/providers.json +1 -0
  10. package/locales/bg-BG/setting.json +78 -2
  11. package/locales/de-DE/models.json +51 -9
  12. package/locales/de-DE/plugin.json +12 -2
  13. package/locales/de-DE/providers.json +1 -0
  14. package/locales/de-DE/setting.json +78 -2
  15. package/locales/en-US/models.json +11 -10
  16. package/locales/en-US/plugin.json +14 -4
  17. package/locales/en-US/providers.json +1 -0
  18. package/locales/en-US/setting.json +97 -2
  19. package/locales/es-ES/plugin.json +12 -2
  20. package/locales/es-ES/providers.json +1 -0
  21. package/locales/es-ES/setting.json +78 -2
  22. package/locales/fa-IR/plugin.json +12 -2
  23. package/locales/fa-IR/providers.json +1 -0
  24. package/locales/fa-IR/setting.json +78 -2
  25. package/locales/fr-FR/plugin.json +12 -2
  26. package/locales/fr-FR/providers.json +1 -0
  27. package/locales/fr-FR/setting.json +78 -2
  28. package/locales/it-IT/plugin.json +12 -2
  29. package/locales/it-IT/providers.json +1 -0
  30. package/locales/it-IT/setting.json +78 -2
  31. package/locales/ja-JP/plugin.json +12 -2
  32. package/locales/ja-JP/providers.json +1 -0
  33. package/locales/ja-JP/setting.json +78 -2
  34. package/locales/ko-KR/plugin.json +12 -2
  35. package/locales/ko-KR/providers.json +1 -0
  36. package/locales/ko-KR/setting.json +78 -2
  37. package/locales/nl-NL/models.json +4 -9
  38. package/locales/nl-NL/plugin.json +12 -2
  39. package/locales/nl-NL/providers.json +1 -0
  40. package/locales/nl-NL/setting.json +78 -2
  41. package/locales/pl-PL/plugin.json +12 -2
  42. package/locales/pl-PL/providers.json +1 -0
  43. package/locales/pl-PL/setting.json +78 -2
  44. package/locales/pt-BR/plugin.json +12 -2
  45. package/locales/pt-BR/providers.json +1 -0
  46. package/locales/pt-BR/setting.json +78 -2
  47. package/locales/ru-RU/plugin.json +12 -2
  48. package/locales/ru-RU/providers.json +1 -0
  49. package/locales/ru-RU/setting.json +78 -2
  50. package/locales/tr-TR/plugin.json +12 -2
  51. package/locales/tr-TR/providers.json +1 -0
  52. package/locales/tr-TR/setting.json +78 -2
  53. package/locales/vi-VN/plugin.json +12 -2
  54. package/locales/vi-VN/providers.json +1 -0
  55. package/locales/vi-VN/setting.json +77 -1
  56. package/locales/zh-CN/auth.json +1 -0
  57. package/locales/zh-CN/plugin.json +12 -2
  58. package/locales/zh-CN/providers.json +1 -0
  59. package/locales/zh-CN/setting.json +97 -2
  60. package/locales/zh-TW/plugin.json +12 -2
  61. package/locales/zh-TW/providers.json +1 -0
  62. package/locales/zh-TW/setting.json +78 -2
  63. package/package.json +1 -1
  64. package/packages/agent-runtime/src/groupOrchestration/GroupOrchestrationSupervisor.ts +2 -0
  65. package/packages/agent-runtime/src/groupOrchestration/__tests__/GroupOrchestrationSupervisor.test.ts +3 -1
  66. package/packages/agent-runtime/src/groupOrchestration/types.ts +5 -0
  67. package/packages/const/src/index.ts +1 -0
  68. package/packages/const/src/klavis.ts +144 -0
  69. package/packages/const/src/lobehubSkill.ts +34 -0
  70. package/packages/const/src/recommendedSkill.ts +17 -0
  71. package/packages/model-runtime/src/core/contextBuilders/anthropic.test.ts +38 -0
  72. package/packages/model-runtime/src/core/contextBuilders/anthropic.ts +20 -1
  73. package/packages/model-runtime/src/core/contextBuilders/google.test.ts +42 -0
  74. package/packages/model-runtime/src/core/contextBuilders/google.ts +17 -0
  75. package/packages/model-runtime/src/providers/google/index.ts +14 -14
  76. package/packages/model-runtime/src/providers/moonshot/index.ts +1 -1
  77. package/packages/model-runtime/src/providers/openai/index.ts +3 -3
  78. package/packages/types/src/discover/index.ts +1 -1
  79. package/scripts/electronWorkflow/modifiers/dynamicToStatic.mts +273 -0
  80. package/scripts/electronWorkflow/modifiers/index.mts +10 -0
  81. package/scripts/electronWorkflow/modifiers/nextConfig.mts +1 -0
  82. package/scripts/electronWorkflow/modifiers/nextDynamicToStatic.mts +233 -0
  83. package/scripts/electronWorkflow/modifiers/removeSuspense.mts +124 -0
  84. package/scripts/electronWorkflow/modifiers/routes.mts +14 -2
  85. package/scripts/electronWorkflow/modifiers/settingsContentToStatic.mts +148 -0
  86. package/scripts/electronWorkflow/modifiers/wrapChildrenWithClientOnly.mts +73 -0
  87. package/src/app/[variants]/(main)/agent/cron/[cronId]/CronConfig.ts +16 -16
  88. package/src/app/[variants]/(main)/agent/cron/[cronId]/features/CronJobSaveButton.tsx +1 -1
  89. package/src/app/[variants]/(main)/agent/cron/[cronId]/features/CronJobScheduleConfig.tsx +5 -2
  90. package/src/app/[variants]/(main)/community/features/Search.tsx +1 -1
  91. package/src/app/[variants]/(main)/home/features/InputArea/SkillInstallBanner.tsx +131 -0
  92. package/src/app/[variants]/(main)/home/features/InputArea/index.tsx +34 -27
  93. package/src/app/[variants]/(main)/settings/features/SettingHeader.tsx +8 -4
  94. package/src/app/[variants]/(main)/settings/features/SettingsContent.tsx +3 -0
  95. package/src/app/[variants]/(main)/settings/hooks/useCategory.tsx +6 -0
  96. package/src/{features/PluginStore/InstalledList/List/Item/Action.tsx → app/[variants]/(main)/settings/skill/features/Actions.tsx} +45 -40
  97. package/src/app/[variants]/(main)/settings/skill/features/KlavisSkillItem.tsx +353 -0
  98. package/src/app/[variants]/(main)/settings/skill/features/LobehubSkillItem.tsx +344 -0
  99. package/src/app/[variants]/(main)/settings/skill/features/McpSkillItem.tsx +116 -0
  100. package/src/app/[variants]/(main)/settings/skill/features/SkillList.tsx +244 -0
  101. package/src/app/[variants]/(main)/settings/skill/index.tsx +35 -0
  102. package/src/app/[variants]/(mobile)/router/mobileRouter.config.tsx +27 -0
  103. package/src/app/[variants]/(mobile)/settings/_layout/Header.tsx +8 -17
  104. package/src/app/[variants]/(mobile)/settings/_layout/index.tsx +6 -1
  105. package/src/app/[variants]/(mobile)/settings/provider/_layout/index.tsx +22 -0
  106. package/src/components/Plugins/PluginTag.tsx +23 -35
  107. package/src/components/client/ClientOnly.tsx +6 -2
  108. package/src/features/AgentSetting/AgentPlugin/index.tsx +2 -2
  109. package/src/features/ChatInput/ActionBar/Tools/KlavisServerItem.tsx +8 -32
  110. package/src/features/ChatInput/ActionBar/Tools/LobehubSkillServerItem.tsx +8 -30
  111. package/src/features/ChatInput/ActionBar/Tools/PopoverContent.tsx +48 -59
  112. package/src/features/ChatInput/ActionBar/Tools/index.tsx +5 -23
  113. package/src/features/ChatInput/ActionBar/Tools/useControls.tsx +158 -56
  114. package/src/features/IntegrationDetailModal/index.tsx +293 -0
  115. package/src/features/{PluginStore/McpList/Detail → MCP/MCPDetail}/index.tsx +15 -6
  116. package/src/features/MCP/MCPSettings/McpSettingsModal.tsx +58 -0
  117. package/src/features/{PluginStore/McpList/Detail/Settings → MCP/MCPSettings}/index.tsx +39 -27
  118. package/src/features/PluginDetailModal/index.tsx +2 -2
  119. package/src/features/PluginDevModal/index.tsx +16 -40
  120. package/src/features/ProfileEditor/AgentTool.tsx +2 -2
  121. package/src/features/ProtocolUrlHandler/InstallPlugin/OfficialPluginInstallModal/index.tsx +1 -1
  122. package/src/features/{PluginStore/AddPluginButton.tsx → SkillStore/AddSkillButton.tsx} +3 -3
  123. package/src/features/SkillStore/CommunityList/Item.tsx +158 -0
  124. package/src/features/SkillStore/CommunityList/index.tsx +101 -0
  125. package/src/features/SkillStore/Content.tsx +59 -0
  126. package/src/features/{PluginStore/PluginEmpty.tsx → SkillStore/Empty.tsx} +8 -8
  127. package/src/features/SkillStore/LobeHubList/Item.tsx +118 -0
  128. package/src/features/SkillStore/LobeHubList/index.tsx +187 -0
  129. package/src/features/SkillStore/LobeHubList/useSkillConnect.ts +239 -0
  130. package/src/features/SkillStore/Search/index.tsx +43 -0
  131. package/src/features/{PluginStore → SkillStore}/index.tsx +14 -10
  132. package/src/features/SkillStore/style.ts +27 -0
  133. package/src/locales/default/plugin.ts +15 -4
  134. package/src/locales/default/setting.ts +204 -2
  135. package/src/services/chat/mecha/agentConfigResolver.test.ts +197 -0
  136. package/src/services/chat/mecha/agentConfigResolver.ts +44 -17
  137. package/src/store/chat/agents/GroupOrchestration/createGroupOrchestrationExecutors.ts +40 -37
  138. package/src/store/chat/slices/aiChat/actions/__tests__/streamingExecutor.test.ts +78 -0
  139. package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +50 -16
  140. package/src/store/global/initialState.ts +1 -0
  141. package/src/store/tool/slices/lobehubSkillStore/action.test.ts +914 -0
  142. package/src/store/tool/slices/lobehubSkillStore/selectors.test.ts +548 -0
  143. package/.cursor/skills/vercel-react-best-practices/AGENTS.md +0 -2410
  144. package/.cursor/skills/vercel-react-best-practices/SKILL.md +0 -125
  145. package/.cursor/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +0 -55
  146. package/.cursor/skills/vercel-react-best-practices/rules/advanced-use-latest.md +0 -49
  147. package/.cursor/skills/vercel-react-best-practices/rules/async-api-routes.md +0 -38
  148. package/.cursor/skills/vercel-react-best-practices/rules/async-defer-await.md +0 -80
  149. package/.cursor/skills/vercel-react-best-practices/rules/async-dependencies.md +0 -36
  150. package/.cursor/skills/vercel-react-best-practices/rules/async-parallel.md +0 -28
  151. package/.cursor/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +0 -99
  152. package/.cursor/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +0 -59
  153. package/.cursor/skills/vercel-react-best-practices/rules/bundle-conditional.md +0 -31
  154. package/.cursor/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +0 -49
  155. package/.cursor/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +0 -35
  156. package/.cursor/skills/vercel-react-best-practices/rules/bundle-preload.md +0 -50
  157. package/.cursor/skills/vercel-react-best-practices/rules/client-event-listeners.md +0 -74
  158. package/.cursor/skills/vercel-react-best-practices/rules/client-localstorage-schema.md +0 -71
  159. package/.cursor/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md +0 -48
  160. package/.cursor/skills/vercel-react-best-practices/rules/client-swr-dedup.md +0 -56
  161. package/.cursor/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +0 -57
  162. package/.cursor/skills/vercel-react-best-practices/rules/js-cache-function-results.md +0 -80
  163. package/.cursor/skills/vercel-react-best-practices/rules/js-cache-property-access.md +0 -28
  164. package/.cursor/skills/vercel-react-best-practices/rules/js-cache-storage.md +0 -70
  165. package/.cursor/skills/vercel-react-best-practices/rules/js-combine-iterations.md +0 -32
  166. package/.cursor/skills/vercel-react-best-practices/rules/js-early-exit.md +0 -50
  167. package/.cursor/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +0 -45
  168. package/.cursor/skills/vercel-react-best-practices/rules/js-index-maps.md +0 -37
  169. package/.cursor/skills/vercel-react-best-practices/rules/js-length-check-first.md +0 -49
  170. package/.cursor/skills/vercel-react-best-practices/rules/js-min-max-loop.md +0 -82
  171. package/.cursor/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +0 -24
  172. package/.cursor/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +0 -57
  173. package/.cursor/skills/vercel-react-best-practices/rules/rendering-activity.md +0 -26
  174. package/.cursor/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +0 -47
  175. package/.cursor/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +0 -40
  176. package/.cursor/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +0 -38
  177. package/.cursor/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +0 -46
  178. package/.cursor/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +0 -82
  179. package/.cursor/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +0 -28
  180. package/.cursor/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +0 -39
  181. package/.cursor/skills/vercel-react-best-practices/rules/rerender-dependencies.md +0 -45
  182. package/.cursor/skills/vercel-react-best-practices/rules/rerender-derived-state.md +0 -29
  183. package/.cursor/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +0 -74
  184. package/.cursor/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +0 -58
  185. package/.cursor/skills/vercel-react-best-practices/rules/rerender-memo.md +0 -44
  186. package/.cursor/skills/vercel-react-best-practices/rules/rerender-transitions.md +0 -40
  187. package/.cursor/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +0 -73
  188. package/.cursor/skills/vercel-react-best-practices/rules/server-cache-lru.md +0 -41
  189. package/.cursor/skills/vercel-react-best-practices/rules/server-cache-react.md +0 -76
  190. package/.cursor/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +0 -83
  191. package/.cursor/skills/vercel-react-best-practices/rules/server-serialization.md +0 -38
  192. package/src/features/PluginStore/Content.tsx +0 -54
  193. package/src/features/PluginStore/InstalledList/Detail/CustomPluginEmptyState.tsx +0 -79
  194. package/src/features/PluginStore/InstalledList/Detail/index.tsx +0 -21
  195. package/src/features/PluginStore/InstalledList/List/Item/index.tsx +0 -61
  196. package/src/features/PluginStore/InstalledList/List/index.tsx +0 -72
  197. package/src/features/PluginStore/InstalledList/index.tsx +0 -90
  198. package/src/features/PluginStore/McpList/List/Action.tsx +0 -119
  199. package/src/features/PluginStore/McpList/List/Item.tsx +0 -83
  200. package/src/features/PluginStore/McpList/List/index.tsx +0 -93
  201. package/src/features/PluginStore/McpList/index.tsx +0 -58
  202. package/src/features/PluginStore/PluginList/Detail/DetailProvider.tsx +0 -19
  203. package/src/features/PluginStore/PluginList/Detail/EmptyState.tsx +0 -56
  204. package/src/features/PluginStore/PluginList/Detail/Header.tsx +0 -130
  205. package/src/features/PluginStore/PluginList/Detail/InstallDetail/Nav.tsx +0 -73
  206. package/src/features/PluginStore/PluginList/Detail/InstallDetail/Settings.tsx +0 -19
  207. package/src/features/PluginStore/PluginList/Detail/InstallDetail/Tools.tsx +0 -111
  208. package/src/features/PluginStore/PluginList/Detail/InstallDetail/index.tsx +0 -24
  209. package/src/features/PluginStore/PluginList/Detail/Loading.tsx +0 -42
  210. package/src/features/PluginStore/PluginList/Detail/TagList.tsx +0 -35
  211. package/src/features/PluginStore/PluginList/Detail/index.tsx +0 -39
  212. package/src/features/PluginStore/PluginList/Detail/useCategory.tsx +0 -76
  213. package/src/features/PluginStore/PluginList/List/Action.tsx +0 -78
  214. package/src/features/PluginStore/PluginList/List/Item.tsx +0 -92
  215. package/src/features/PluginStore/PluginList/List/index.tsx +0 -94
  216. package/src/features/PluginStore/PluginList/index.tsx +0 -46
  217. package/src/features/PluginStore/Search/index.tsx +0 -40
  218. /package/{.codex/skills → .agents}/vercel-react-best-practices/AGENTS.md +0 -0
  219. /package/{.codex/skills → .agents}/vercel-react-best-practices/SKILL.md +0 -0
  220. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/advanced-event-handler-refs.md +0 -0
  221. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/advanced-use-latest.md +0 -0
  222. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/async-api-routes.md +0 -0
  223. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/async-defer-await.md +0 -0
  224. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/async-dependencies.md +0 -0
  225. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/async-parallel.md +0 -0
  226. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/async-suspense-boundaries.md +0 -0
  227. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/bundle-barrel-imports.md +0 -0
  228. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/bundle-conditional.md +0 -0
  229. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/bundle-defer-third-party.md +0 -0
  230. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/bundle-dynamic-imports.md +0 -0
  231. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/bundle-preload.md +0 -0
  232. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/client-event-listeners.md +0 -0
  233. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/client-localstorage-schema.md +0 -0
  234. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/client-passive-event-listeners.md +0 -0
  235. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/client-swr-dedup.md +0 -0
  236. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/js-batch-dom-css.md +0 -0
  237. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/js-cache-function-results.md +0 -0
  238. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/js-cache-property-access.md +0 -0
  239. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/js-cache-storage.md +0 -0
  240. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/js-combine-iterations.md +0 -0
  241. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/js-early-exit.md +0 -0
  242. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/js-hoist-regexp.md +0 -0
  243. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/js-index-maps.md +0 -0
  244. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/js-length-check-first.md +0 -0
  245. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/js-min-max-loop.md +0 -0
  246. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/js-set-map-lookups.md +0 -0
  247. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/js-tosorted-immutable.md +0 -0
  248. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/rendering-activity.md +0 -0
  249. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +0 -0
  250. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/rendering-conditional-render.md +0 -0
  251. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/rendering-content-visibility.md +0 -0
  252. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/rendering-hoist-jsx.md +0 -0
  253. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +0 -0
  254. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/rendering-svg-precision.md +0 -0
  255. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/rerender-defer-reads.md +0 -0
  256. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/rerender-dependencies.md +0 -0
  257. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/rerender-derived-state.md +0 -0
  258. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/rerender-functional-setstate.md +0 -0
  259. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/rerender-lazy-state-init.md +0 -0
  260. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/rerender-memo.md +0 -0
  261. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/rerender-transitions.md +0 -0
  262. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/server-after-nonblocking.md +0 -0
  263. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/server-cache-lru.md +0 -0
  264. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/server-cache-react.md +0 -0
  265. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/server-parallel-fetching.md +0 -0
  266. /package/{.codex/skills → .agents}/vercel-react-best-practices/rules/server-serialization.md +0 -0
  267. /package/src/{features/PluginStore/InstalledList → app/[variants]/(main)/settings/skill/features}/EditCustomPlugin.tsx +0 -0
  268. /package/src/features/{PluginStore/McpList/Detail → MCP/MCPDetail}/Loading.tsx +0 -0
  269. /package/src/features/{PluginStore → SkillStore}/Loading.tsx +0 -0
  270. /package/src/features/{PluginStore → SkillStore}/VirtuosoLoading.tsx +0 -0
@@ -0,0 +1,914 @@
1
+ import { act, renderHook, waitFor } from '@testing-library/react';
2
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ import { toolsClient } from '@/libs/trpc/client';
5
+
6
+ import { useToolStore } from '../../store';
7
+ import { LobehubSkillStatus } from './types';
8
+
9
+ vi.mock('zustand/traditional');
10
+
11
+ beforeEach(() => {
12
+ vi.clearAllMocks();
13
+ });
14
+
15
+ vi.mock('@lobechat/const', async (importOriginal) => {
16
+ const actual = await importOriginal<typeof import('@lobechat/const')>();
17
+ return {
18
+ ...actual,
19
+ getLobehubSkillProviderById: vi.fn((id: string) => ({
20
+ id,
21
+ label: id.charAt(0).toUpperCase() + id.slice(1),
22
+ icon: '🔗',
23
+ })),
24
+ };
25
+ });
26
+
27
+ vi.mock('@/libs/trpc/client', () => ({
28
+ toolsClient: {
29
+ market: {
30
+ connectCallTool: { mutate: vi.fn() },
31
+ connectGetAuthorizeUrl: { query: vi.fn() },
32
+ connectGetStatus: { query: vi.fn() },
33
+ connectListConnections: { query: vi.fn() },
34
+ connectListTools: { query: vi.fn() },
35
+ connectRefresh: { mutate: vi.fn() },
36
+ connectRevoke: { mutate: vi.fn() },
37
+ },
38
+ },
39
+ }));
40
+
41
+ describe('lobehubSkillStore actions', () => {
42
+ describe('callLobehubSkillTool', () => {
43
+ it('should call tool successfully and return result', async () => {
44
+ const { result } = renderHook(() => useToolStore());
45
+
46
+ act(() => {
47
+ useToolStore.setState({
48
+ lobehubSkillServers: [],
49
+ lobehubSkillLoadingIds: new Set(),
50
+ lobehubSkillExecutingToolIds: new Set(),
51
+ });
52
+ });
53
+
54
+ const mockResponse = {
55
+ data: { result: 'success' },
56
+ };
57
+ vi.mocked(toolsClient.market.connectCallTool.mutate).mockResolvedValue(mockResponse as any);
58
+
59
+ let callResult;
60
+ await act(async () => {
61
+ callResult = await result.current.callLobehubSkillTool({
62
+ provider: 'linear',
63
+ toolName: 'createIssue',
64
+ args: { title: 'Test Issue' },
65
+ });
66
+ });
67
+
68
+ expect(callResult).toEqual({ data: mockResponse.data, success: true });
69
+ expect(toolsClient.market.connectCallTool.mutate).toHaveBeenCalledWith({
70
+ provider: 'linear',
71
+ toolName: 'createIssue',
72
+ args: { title: 'Test Issue' },
73
+ });
74
+ });
75
+
76
+ it('should track executing state during tool call', async () => {
77
+ const { result } = renderHook(() => useToolStore());
78
+
79
+ act(() => {
80
+ useToolStore.setState({
81
+ lobehubSkillServers: [],
82
+ lobehubSkillLoadingIds: new Set(),
83
+ lobehubSkillExecutingToolIds: new Set(),
84
+ });
85
+ });
86
+
87
+ let resolvePromise: (value: any) => void;
88
+ const promise = new Promise((resolve) => {
89
+ resolvePromise = resolve;
90
+ });
91
+ vi.mocked(toolsClient.market.connectCallTool.mutate).mockReturnValue(promise as any);
92
+
93
+ const callPromise = act(async () => {
94
+ return result.current.callLobehubSkillTool({
95
+ provider: 'linear',
96
+ toolName: 'createIssue',
97
+ });
98
+ });
99
+
100
+ // Tool should be marked as executing during the call
101
+ await waitFor(() => {
102
+ expect(result.current.lobehubSkillExecutingToolIds.has('linear:createIssue')).toBe(true);
103
+ });
104
+
105
+ // Resolve the promise
106
+ resolvePromise!({ data: {} });
107
+ await callPromise;
108
+
109
+ // Tool should no longer be executing after completion
110
+ expect(result.current.lobehubSkillExecutingToolIds.has('linear:createIssue')).toBe(false);
111
+ });
112
+
113
+ it('should handle NOT_CONNECTED error', async () => {
114
+ const { result } = renderHook(() => useToolStore());
115
+
116
+ act(() => {
117
+ useToolStore.setState({
118
+ lobehubSkillServers: [],
119
+ lobehubSkillLoadingIds: new Set(),
120
+ lobehubSkillExecutingToolIds: new Set(),
121
+ });
122
+ });
123
+
124
+ vi.mocked(toolsClient.market.connectCallTool.mutate).mockRejectedValue(
125
+ new Error('NOT_CONNECTED'),
126
+ );
127
+
128
+ let callResult;
129
+ await act(async () => {
130
+ callResult = await result.current.callLobehubSkillTool({
131
+ provider: 'linear',
132
+ toolName: 'createIssue',
133
+ });
134
+ });
135
+
136
+ expect(callResult).toEqual({
137
+ error: 'NOT_CONNECTED',
138
+ errorCode: 'NOT_CONNECTED',
139
+ success: false,
140
+ });
141
+ expect(result.current.lobehubSkillExecutingToolIds.has('linear:createIssue')).toBe(false);
142
+ });
143
+
144
+ it('should handle TOKEN_EXPIRED error', async () => {
145
+ const { result } = renderHook(() => useToolStore());
146
+
147
+ act(() => {
148
+ useToolStore.setState({
149
+ lobehubSkillServers: [],
150
+ lobehubSkillLoadingIds: new Set(),
151
+ lobehubSkillExecutingToolIds: new Set(),
152
+ });
153
+ });
154
+
155
+ vi.mocked(toolsClient.market.connectCallTool.mutate).mockRejectedValue(
156
+ new Error('TOKEN_EXPIRED'),
157
+ );
158
+
159
+ let callResult;
160
+ await act(async () => {
161
+ callResult = await result.current.callLobehubSkillTool({
162
+ provider: 'linear',
163
+ toolName: 'createIssue',
164
+ });
165
+ });
166
+
167
+ expect(callResult).toEqual({
168
+ error: 'TOKEN_EXPIRED',
169
+ errorCode: 'NOT_CONNECTED',
170
+ success: false,
171
+ });
172
+ });
173
+
174
+ it('should handle generic error', async () => {
175
+ const { result } = renderHook(() => useToolStore());
176
+
177
+ act(() => {
178
+ useToolStore.setState({
179
+ lobehubSkillServers: [],
180
+ lobehubSkillLoadingIds: new Set(),
181
+ lobehubSkillExecutingToolIds: new Set(),
182
+ });
183
+ });
184
+
185
+ vi.mocked(toolsClient.market.connectCallTool.mutate).mockRejectedValue(
186
+ new Error('Network error'),
187
+ );
188
+
189
+ let callResult;
190
+ await act(async () => {
191
+ callResult = await result.current.callLobehubSkillTool({
192
+ provider: 'linear',
193
+ toolName: 'createIssue',
194
+ });
195
+ });
196
+
197
+ expect(callResult).toEqual({
198
+ error: 'Network error',
199
+ success: false,
200
+ });
201
+ });
202
+ });
203
+
204
+ describe('checkLobehubSkillStatus', () => {
205
+ it('should check status and add server when connected', async () => {
206
+ const { result } = renderHook(() => useToolStore());
207
+
208
+ act(() => {
209
+ useToolStore.setState({
210
+ lobehubSkillServers: [],
211
+ lobehubSkillLoadingIds: new Set(),
212
+ lobehubSkillExecutingToolIds: new Set(),
213
+ });
214
+ });
215
+
216
+ const mockResponse = {
217
+ connected: true,
218
+ icon: 'linear-icon',
219
+ connection: {
220
+ providerUsername: 'testuser',
221
+ scopes: ['read', 'write'],
222
+ tokenExpiresAt: '2024-12-31T00:00:00Z',
223
+ },
224
+ };
225
+ vi.mocked(toolsClient.market.connectGetStatus.query).mockResolvedValue(mockResponse as any);
226
+ vi.mocked(toolsClient.market.connectListTools.query).mockResolvedValue({
227
+ provider: 'linear',
228
+ tools: [],
229
+ });
230
+
231
+ let server;
232
+ await act(async () => {
233
+ server = await result.current.checkLobehubSkillStatus('linear');
234
+ });
235
+
236
+ expect(server).toMatchObject({
237
+ identifier: 'linear',
238
+ name: 'Linear',
239
+ isConnected: true,
240
+ status: LobehubSkillStatus.CONNECTED,
241
+ providerUsername: 'testuser',
242
+ scopes: ['read', 'write'],
243
+ });
244
+ expect(result.current.lobehubSkillServers).toHaveLength(1);
245
+ expect(result.current.lobehubSkillLoadingIds.has('linear')).toBe(false);
246
+ });
247
+
248
+ it('should check status and add server when not connected', async () => {
249
+ const { result } = renderHook(() => useToolStore());
250
+
251
+ act(() => {
252
+ useToolStore.setState({
253
+ lobehubSkillServers: [],
254
+ lobehubSkillLoadingIds: new Set(),
255
+ lobehubSkillExecutingToolIds: new Set(),
256
+ });
257
+ });
258
+
259
+ const mockResponse = {
260
+ connected: false,
261
+ icon: 'linear-icon',
262
+ };
263
+ vi.mocked(toolsClient.market.connectGetStatus.query).mockResolvedValue(mockResponse as any);
264
+
265
+ let server;
266
+ await act(async () => {
267
+ server = await result.current.checkLobehubSkillStatus('linear');
268
+ });
269
+
270
+ expect(server).toMatchObject({
271
+ identifier: 'linear',
272
+ isConnected: false,
273
+ status: LobehubSkillStatus.NOT_CONNECTED,
274
+ });
275
+ expect(toolsClient.market.connectListTools.query).not.toHaveBeenCalled();
276
+ });
277
+
278
+ it('should update existing server instead of adding new one', async () => {
279
+ const { result } = renderHook(() => useToolStore());
280
+
281
+ act(() => {
282
+ useToolStore.setState({
283
+ lobehubSkillServers: [
284
+ {
285
+ identifier: 'linear',
286
+ name: 'Linear',
287
+ isConnected: false,
288
+ status: LobehubSkillStatus.NOT_CONNECTED,
289
+ },
290
+ ],
291
+ lobehubSkillLoadingIds: new Set(),
292
+ lobehubSkillExecutingToolIds: new Set(),
293
+ });
294
+ });
295
+
296
+ const mockResponse = {
297
+ connected: true,
298
+ icon: 'linear-icon',
299
+ connection: {
300
+ providerUsername: 'testuser',
301
+ scopes: ['read'],
302
+ tokenExpiresAt: '2024-12-31T00:00:00Z',
303
+ },
304
+ };
305
+ vi.mocked(toolsClient.market.connectGetStatus.query).mockResolvedValue(mockResponse as any);
306
+ vi.mocked(toolsClient.market.connectListTools.query).mockResolvedValue({
307
+ provider: 'linear',
308
+ tools: [],
309
+ });
310
+
311
+ await act(async () => {
312
+ await result.current.checkLobehubSkillStatus('linear');
313
+ });
314
+
315
+ expect(result.current.lobehubSkillServers).toHaveLength(1);
316
+ expect(result.current.lobehubSkillServers[0].isConnected).toBe(true);
317
+ expect(result.current.lobehubSkillServers[0].status).toBe(LobehubSkillStatus.CONNECTED);
318
+ });
319
+
320
+ it('should track loading state during status check', async () => {
321
+ const { result } = renderHook(() => useToolStore());
322
+
323
+ act(() => {
324
+ useToolStore.setState({
325
+ lobehubSkillServers: [],
326
+ lobehubSkillLoadingIds: new Set(),
327
+ lobehubSkillExecutingToolIds: new Set(),
328
+ });
329
+ });
330
+
331
+ let resolvePromise: (value: any) => void;
332
+ const promise = new Promise((resolve) => {
333
+ resolvePromise = resolve;
334
+ });
335
+ vi.mocked(toolsClient.market.connectGetStatus.query).mockReturnValue(promise as any);
336
+
337
+ const checkPromise = act(async () => {
338
+ return result.current.checkLobehubSkillStatus('linear');
339
+ });
340
+
341
+ // Should be loading during the check
342
+ await waitFor(() => {
343
+ expect(result.current.lobehubSkillLoadingIds.has('linear')).toBe(true);
344
+ });
345
+
346
+ // Resolve the promise
347
+ resolvePromise!({ connected: false, icon: '' });
348
+ await checkPromise;
349
+
350
+ // Should not be loading after completion
351
+ expect(result.current.lobehubSkillLoadingIds.has('linear')).toBe(false);
352
+ });
353
+
354
+ it('should handle error and return undefined', async () => {
355
+ const { result } = renderHook(() => useToolStore());
356
+
357
+ act(() => {
358
+ useToolStore.setState({
359
+ lobehubSkillServers: [],
360
+ lobehubSkillLoadingIds: new Set(),
361
+ lobehubSkillExecutingToolIds: new Set(),
362
+ });
363
+ });
364
+
365
+ vi.mocked(toolsClient.market.connectGetStatus.query).mockRejectedValue(
366
+ new Error('Network error'),
367
+ );
368
+
369
+ let server;
370
+ await act(async () => {
371
+ server = await result.current.checkLobehubSkillStatus('linear');
372
+ });
373
+
374
+ expect(server).toBeUndefined();
375
+ expect(result.current.lobehubSkillLoadingIds.has('linear')).toBe(false);
376
+ });
377
+ });
378
+
379
+ describe('getLobehubSkillAuthorizeUrl', () => {
380
+ it('should return authorization URL and code', async () => {
381
+ const { result } = renderHook(() => useToolStore());
382
+
383
+ const mockResponse = {
384
+ authorizeUrl: 'https://auth.linear.app/authorize?code=abc123',
385
+ code: 'abc123',
386
+ expiresIn: 600,
387
+ };
388
+ vi.mocked(toolsClient.market.connectGetAuthorizeUrl.query).mockResolvedValue(
389
+ mockResponse as any,
390
+ );
391
+
392
+ let authInfo;
393
+ await act(async () => {
394
+ authInfo = await result.current.getLobehubSkillAuthorizeUrl('linear');
395
+ });
396
+
397
+ expect(authInfo).toEqual({
398
+ authorizeUrl: 'https://auth.linear.app/authorize?code=abc123',
399
+ code: 'abc123',
400
+ expiresIn: 600,
401
+ });
402
+ expect(toolsClient.market.connectGetAuthorizeUrl.query).toHaveBeenCalledWith({
403
+ provider: 'linear',
404
+ redirectUri: undefined,
405
+ scopes: undefined,
406
+ });
407
+ });
408
+
409
+ it('should pass options to query', async () => {
410
+ const { result } = renderHook(() => useToolStore());
411
+
412
+ const mockResponse = {
413
+ authorizeUrl: 'https://auth.linear.app/authorize',
414
+ code: 'xyz789',
415
+ expiresIn: 300,
416
+ };
417
+ vi.mocked(toolsClient.market.connectGetAuthorizeUrl.query).mockResolvedValue(
418
+ mockResponse as any,
419
+ );
420
+
421
+ await act(async () => {
422
+ await result.current.getLobehubSkillAuthorizeUrl('linear', {
423
+ scopes: ['read', 'write'],
424
+ redirectUri: 'https://example.com/callback',
425
+ });
426
+ });
427
+
428
+ expect(toolsClient.market.connectGetAuthorizeUrl.query).toHaveBeenCalledWith({
429
+ provider: 'linear',
430
+ scopes: ['read', 'write'],
431
+ redirectUri: 'https://example.com/callback',
432
+ });
433
+ });
434
+ });
435
+
436
+ describe('internal_updateLobehubSkillServer', () => {
437
+ it('should update existing server', () => {
438
+ const { result } = renderHook(() => useToolStore());
439
+
440
+ act(() => {
441
+ useToolStore.setState({
442
+ lobehubSkillServers: [
443
+ {
444
+ identifier: 'linear',
445
+ name: 'Linear',
446
+ isConnected: true,
447
+ status: LobehubSkillStatus.CONNECTED,
448
+ },
449
+ ],
450
+ lobehubSkillLoadingIds: new Set(),
451
+ lobehubSkillExecutingToolIds: new Set(),
452
+ });
453
+ });
454
+
455
+ act(() => {
456
+ result.current.internal_updateLobehubSkillServer('linear', {
457
+ status: LobehubSkillStatus.ERROR,
458
+ errorMessage: 'Token expired',
459
+ });
460
+ });
461
+
462
+ expect(result.current.lobehubSkillServers[0]).toMatchObject({
463
+ identifier: 'linear',
464
+ status: LobehubSkillStatus.ERROR,
465
+ errorMessage: 'Token expired',
466
+ });
467
+ });
468
+
469
+ it('should do nothing when server not found', () => {
470
+ const { result } = renderHook(() => useToolStore());
471
+
472
+ act(() => {
473
+ useToolStore.setState({
474
+ lobehubSkillServers: [],
475
+ lobehubSkillLoadingIds: new Set(),
476
+ lobehubSkillExecutingToolIds: new Set(),
477
+ });
478
+ });
479
+
480
+ act(() => {
481
+ result.current.internal_updateLobehubSkillServer('non-existent', {
482
+ status: LobehubSkillStatus.ERROR,
483
+ });
484
+ });
485
+
486
+ expect(result.current.lobehubSkillServers).toHaveLength(0);
487
+ });
488
+ });
489
+
490
+ describe('refreshLobehubSkillToken', () => {
491
+ it('should refresh token successfully and update server', async () => {
492
+ const { result } = renderHook(() => useToolStore());
493
+
494
+ act(() => {
495
+ useToolStore.setState({
496
+ lobehubSkillServers: [
497
+ {
498
+ identifier: 'linear',
499
+ name: 'Linear',
500
+ isConnected: true,
501
+ status: LobehubSkillStatus.CONNECTED,
502
+ tokenExpiresAt: '2024-01-01T00:00:00Z',
503
+ },
504
+ ],
505
+ lobehubSkillLoadingIds: new Set(),
506
+ lobehubSkillExecutingToolIds: new Set(),
507
+ });
508
+ });
509
+
510
+ const mockResponse = {
511
+ refreshed: true,
512
+ connection: {
513
+ tokenExpiresAt: '2024-12-31T00:00:00Z',
514
+ },
515
+ };
516
+ vi.mocked(toolsClient.market.connectRefresh.mutate).mockResolvedValue(mockResponse as any);
517
+
518
+ let refreshed;
519
+ await act(async () => {
520
+ refreshed = await result.current.refreshLobehubSkillToken('linear');
521
+ });
522
+
523
+ expect(refreshed).toBe(true);
524
+ expect(result.current.lobehubSkillServers[0].tokenExpiresAt).toBe('2024-12-31T00:00:00Z');
525
+ expect(result.current.lobehubSkillServers[0].status).toBe(LobehubSkillStatus.CONNECTED);
526
+ });
527
+
528
+ it('should return false when refresh fails', async () => {
529
+ const { result } = renderHook(() => useToolStore());
530
+
531
+ act(() => {
532
+ useToolStore.setState({
533
+ lobehubSkillServers: [
534
+ {
535
+ identifier: 'linear',
536
+ name: 'Linear',
537
+ isConnected: true,
538
+ status: LobehubSkillStatus.CONNECTED,
539
+ },
540
+ ],
541
+ lobehubSkillLoadingIds: new Set(),
542
+ lobehubSkillExecutingToolIds: new Set(),
543
+ });
544
+ });
545
+
546
+ const mockResponse = {
547
+ refreshed: false,
548
+ };
549
+ vi.mocked(toolsClient.market.connectRefresh.mutate).mockResolvedValue(mockResponse as any);
550
+
551
+ let refreshed;
552
+ await act(async () => {
553
+ refreshed = await result.current.refreshLobehubSkillToken('linear');
554
+ });
555
+
556
+ expect(refreshed).toBe(false);
557
+ });
558
+
559
+ it('should return false on error', async () => {
560
+ const { result } = renderHook(() => useToolStore());
561
+
562
+ act(() => {
563
+ useToolStore.setState({
564
+ lobehubSkillServers: [],
565
+ lobehubSkillLoadingIds: new Set(),
566
+ lobehubSkillExecutingToolIds: new Set(),
567
+ });
568
+ });
569
+
570
+ vi.mocked(toolsClient.market.connectRefresh.mutate).mockRejectedValue(
571
+ new Error('Refresh failed'),
572
+ );
573
+
574
+ let refreshed;
575
+ await act(async () => {
576
+ refreshed = await result.current.refreshLobehubSkillToken('linear');
577
+ });
578
+
579
+ expect(refreshed).toBe(false);
580
+ });
581
+ });
582
+
583
+ describe('refreshLobehubSkillTools', () => {
584
+ it('should refresh tools for server', async () => {
585
+ const { result } = renderHook(() => useToolStore());
586
+
587
+ act(() => {
588
+ useToolStore.setState({
589
+ lobehubSkillServers: [
590
+ {
591
+ identifier: 'linear',
592
+ name: 'Linear',
593
+ isConnected: true,
594
+ status: LobehubSkillStatus.CONNECTED,
595
+ },
596
+ ],
597
+ lobehubSkillLoadingIds: new Set(),
598
+ lobehubSkillExecutingToolIds: new Set(),
599
+ });
600
+ });
601
+
602
+ const mockTools = {
603
+ tools: [
604
+ { name: 'createIssue', description: 'Create an issue', inputSchema: { type: 'object' } },
605
+ { name: 'listIssues', description: 'List issues', inputSchema: { type: 'object' } },
606
+ ],
607
+ };
608
+ vi.mocked(toolsClient.market.connectListTools.query).mockResolvedValue(mockTools as any);
609
+
610
+ await act(async () => {
611
+ await result.current.refreshLobehubSkillTools('linear');
612
+ });
613
+
614
+ expect(result.current.lobehubSkillServers[0].tools).toHaveLength(2);
615
+ expect(result.current.lobehubSkillServers[0].tools![0].name).toBe('createIssue');
616
+ expect(result.current.lobehubSkillServers[0].tools![1].name).toBe('listIssues');
617
+ });
618
+
619
+ it('should do nothing when server not found', async () => {
620
+ vi.mocked(toolsClient.market.connectListTools.query).mockClear();
621
+
622
+ const { result } = renderHook(() => useToolStore());
623
+
624
+ act(() => {
625
+ useToolStore.setState({
626
+ lobehubSkillServers: [],
627
+ lobehubSkillLoadingIds: new Set(),
628
+ lobehubSkillExecutingToolIds: new Set(),
629
+ });
630
+ });
631
+
632
+ await act(async () => {
633
+ await result.current.refreshLobehubSkillTools('non-existent');
634
+ });
635
+
636
+ // The action still calls the API, but the state update does nothing
637
+ // since server is not found
638
+ });
639
+
640
+ it('should handle error gracefully', async () => {
641
+ const { result } = renderHook(() => useToolStore());
642
+
643
+ act(() => {
644
+ useToolStore.setState({
645
+ lobehubSkillServers: [
646
+ {
647
+ identifier: 'linear',
648
+ name: 'Linear',
649
+ isConnected: true,
650
+ status: LobehubSkillStatus.CONNECTED,
651
+ },
652
+ ],
653
+ lobehubSkillLoadingIds: new Set(),
654
+ lobehubSkillExecutingToolIds: new Set(),
655
+ });
656
+ });
657
+
658
+ vi.mocked(toolsClient.market.connectListTools.query).mockRejectedValue(
659
+ new Error('Network error'),
660
+ );
661
+
662
+ await act(async () => {
663
+ await result.current.refreshLobehubSkillTools('linear');
664
+ });
665
+
666
+ // Should not crash and server should remain unchanged
667
+ expect(result.current.lobehubSkillServers[0].tools).toBeUndefined();
668
+ });
669
+ });
670
+
671
+ describe('revokeLobehubSkill', () => {
672
+ it('should revoke skill and remove server from state', async () => {
673
+ const { result } = renderHook(() => useToolStore());
674
+
675
+ act(() => {
676
+ useToolStore.setState({
677
+ lobehubSkillServers: [
678
+ {
679
+ identifier: 'linear',
680
+ name: 'Linear',
681
+ isConnected: true,
682
+ status: LobehubSkillStatus.CONNECTED,
683
+ },
684
+ {
685
+ identifier: 'github',
686
+ name: 'GitHub',
687
+ isConnected: true,
688
+ status: LobehubSkillStatus.CONNECTED,
689
+ },
690
+ ],
691
+ lobehubSkillLoadingIds: new Set(),
692
+ lobehubSkillExecutingToolIds: new Set(),
693
+ });
694
+ });
695
+
696
+ vi.mocked(toolsClient.market.connectRevoke.mutate).mockResolvedValue({} as any);
697
+
698
+ await act(async () => {
699
+ await result.current.revokeLobehubSkill('linear');
700
+ });
701
+
702
+ expect(result.current.lobehubSkillServers).toHaveLength(1);
703
+ expect(result.current.lobehubSkillServers[0].identifier).toBe('github');
704
+ expect(toolsClient.market.connectRevoke.mutate).toHaveBeenCalledWith({
705
+ provider: 'linear',
706
+ });
707
+ });
708
+
709
+ it('should track loading state during revoke', async () => {
710
+ const { result } = renderHook(() => useToolStore());
711
+
712
+ act(() => {
713
+ useToolStore.setState({
714
+ lobehubSkillServers: [
715
+ {
716
+ identifier: 'linear',
717
+ name: 'Linear',
718
+ isConnected: true,
719
+ status: LobehubSkillStatus.CONNECTED,
720
+ },
721
+ ],
722
+ lobehubSkillLoadingIds: new Set(),
723
+ lobehubSkillExecutingToolIds: new Set(),
724
+ });
725
+ });
726
+
727
+ let resolvePromise: (value: any) => void;
728
+ const promise = new Promise((resolve) => {
729
+ resolvePromise = resolve;
730
+ });
731
+ vi.mocked(toolsClient.market.connectRevoke.mutate).mockReturnValue(promise as any);
732
+
733
+ const revokePromise = act(async () => {
734
+ return result.current.revokeLobehubSkill('linear');
735
+ });
736
+
737
+ // Should be loading during revoke
738
+ await waitFor(() => {
739
+ expect(result.current.lobehubSkillLoadingIds.has('linear')).toBe(true);
740
+ });
741
+
742
+ // Resolve the promise
743
+ resolvePromise!({});
744
+ await revokePromise;
745
+
746
+ // Should not be loading after completion
747
+ expect(result.current.lobehubSkillLoadingIds.has('linear')).toBe(false);
748
+ });
749
+
750
+ it('should handle error gracefully', async () => {
751
+ const { result } = renderHook(() => useToolStore());
752
+
753
+ act(() => {
754
+ useToolStore.setState({
755
+ lobehubSkillServers: [
756
+ {
757
+ identifier: 'linear',
758
+ name: 'Linear',
759
+ isConnected: true,
760
+ status: LobehubSkillStatus.CONNECTED,
761
+ },
762
+ ],
763
+ lobehubSkillLoadingIds: new Set(),
764
+ lobehubSkillExecutingToolIds: new Set(),
765
+ });
766
+ });
767
+
768
+ vi.mocked(toolsClient.market.connectRevoke.mutate).mockRejectedValue(
769
+ new Error('Revoke failed'),
770
+ );
771
+
772
+ await act(async () => {
773
+ await result.current.revokeLobehubSkill('linear');
774
+ });
775
+
776
+ // Server should still be in state after error
777
+ expect(result.current.lobehubSkillServers).toHaveLength(1);
778
+ expect(result.current.lobehubSkillLoadingIds.has('linear')).toBe(false);
779
+ });
780
+ });
781
+
782
+ describe('useFetchLobehubSkillConnections', () => {
783
+ it('should not fetch when disabled', () => {
784
+ act(() => {
785
+ useToolStore.setState({
786
+ lobehubSkillServers: [],
787
+ lobehubSkillLoadingIds: new Set(),
788
+ lobehubSkillExecutingToolIds: new Set(),
789
+ });
790
+ });
791
+
792
+ vi.mocked(toolsClient.market.connectListConnections.query).mockClear();
793
+
794
+ renderHook(() => useToolStore.getState().useFetchLobehubSkillConnections(false));
795
+
796
+ expect(toolsClient.market.connectListConnections.query).not.toHaveBeenCalled();
797
+ });
798
+
799
+ it('should fetch connections when enabled', async () => {
800
+ act(() => {
801
+ useToolStore.setState({
802
+ lobehubSkillServers: [],
803
+ lobehubSkillLoadingIds: new Set(),
804
+ lobehubSkillExecutingToolIds: new Set(),
805
+ });
806
+ });
807
+
808
+ const mockConnections = {
809
+ connections: [
810
+ {
811
+ providerId: 'linear',
812
+ icon: 'linear-icon',
813
+ providerUsername: 'testuser',
814
+ scopes: ['read'],
815
+ tokenExpiresAt: '2024-12-31T00:00:00Z',
816
+ },
817
+ ],
818
+ };
819
+ vi.mocked(toolsClient.market.connectListConnections.query).mockResolvedValue(
820
+ mockConnections as any,
821
+ );
822
+ vi.mocked(toolsClient.market.connectListTools.query).mockResolvedValue({
823
+ provider: 'linear',
824
+ tools: [],
825
+ });
826
+
827
+ renderHook(() => useToolStore.getState().useFetchLobehubSkillConnections(true));
828
+
829
+ await waitFor(() => {
830
+ expect(toolsClient.market.connectListConnections.query).toHaveBeenCalled();
831
+ });
832
+ });
833
+ });
834
+
835
+ describe('server deduplication logic', () => {
836
+ it('should deduplicate servers by identifier when adding new servers', () => {
837
+ act(() => {
838
+ useToolStore.setState({
839
+ lobehubSkillServers: [
840
+ {
841
+ identifier: 'linear',
842
+ name: 'Linear',
843
+ isConnected: true,
844
+ status: LobehubSkillStatus.CONNECTED,
845
+ },
846
+ ],
847
+ lobehubSkillLoadingIds: new Set(),
848
+ lobehubSkillExecutingToolIds: new Set(),
849
+ });
850
+ });
851
+
852
+ const incomingServers = [
853
+ {
854
+ identifier: 'linear',
855
+ name: 'Linear',
856
+ isConnected: true,
857
+ status: LobehubSkillStatus.CONNECTED,
858
+ },
859
+ {
860
+ identifier: 'github',
861
+ name: 'GitHub',
862
+ isConnected: true,
863
+ status: LobehubSkillStatus.CONNECTED,
864
+ },
865
+ ];
866
+
867
+ act(() => {
868
+ const existingServers = useToolStore.getState().lobehubSkillServers;
869
+ const existingIdentifiers = new Set(existingServers.map((s) => s.identifier));
870
+ const newServers = incomingServers.filter((s) => !existingIdentifiers.has(s.identifier));
871
+
872
+ useToolStore.setState({
873
+ lobehubSkillServers: [...existingServers, ...newServers],
874
+ });
875
+ });
876
+
877
+ const finalServers = useToolStore.getState().lobehubSkillServers;
878
+ expect(finalServers).toHaveLength(2);
879
+ expect(finalServers.find((s) => s.identifier === 'linear')).toBeDefined();
880
+ expect(finalServers.find((s) => s.identifier === 'github')).toBeDefined();
881
+ });
882
+
883
+ it('should add all servers when none exist', () => {
884
+ act(() => {
885
+ useToolStore.setState({
886
+ lobehubSkillServers: [],
887
+ lobehubSkillLoadingIds: new Set(),
888
+ lobehubSkillExecutingToolIds: new Set(),
889
+ });
890
+ });
891
+
892
+ const incomingServers = [
893
+ {
894
+ identifier: 'linear',
895
+ name: 'Linear',
896
+ isConnected: true,
897
+ status: LobehubSkillStatus.CONNECTED,
898
+ },
899
+ ];
900
+
901
+ act(() => {
902
+ const existingServers = useToolStore.getState().lobehubSkillServers;
903
+ const existingIdentifiers = new Set(existingServers.map((s) => s.identifier));
904
+ const newServers = incomingServers.filter((s) => !existingIdentifiers.has(s.identifier));
905
+
906
+ useToolStore.setState({
907
+ lobehubSkillServers: [...existingServers, ...newServers],
908
+ });
909
+ });
910
+
911
+ expect(useToolStore.getState().lobehubSkillServers).toHaveLength(1);
912
+ });
913
+ });
914
+ });