@wopr-network/platform-ui-core 1.0.0

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 (543) hide show
  1. package/.env.paperclip +18 -0
  2. package/.env.wopr +18 -0
  3. package/README.md +36 -0
  4. package/biome.json +52 -0
  5. package/next.config.ts +45 -0
  6. package/package.json +84 -0
  7. package/postcss.config.mjs +7 -0
  8. package/public/file.svg +1 -0
  9. package/public/globe.svg +1 -0
  10. package/public/window.svg +1 -0
  11. package/src/__tests__/__snapshots__/layout-snapshots.test.tsx.snap +741 -0
  12. package/src/__tests__/account-page-redirect.test.tsx +73 -0
  13. package/src/__tests__/account-switcher.test.tsx +85 -0
  14. package/src/__tests__/activity-page.test.tsx +176 -0
  15. package/src/__tests__/add-payment-method-dialog.test.tsx +160 -0
  16. package/src/__tests__/admin-api.test.ts +244 -0
  17. package/src/__tests__/admin-gpu-api.test.ts +188 -0
  18. package/src/__tests__/admin-guard.test.tsx +79 -0
  19. package/src/__tests__/admin-marketplace-api.test.ts +179 -0
  20. package/src/__tests__/admin-middleware.test.ts +157 -0
  21. package/src/__tests__/admin-tenant-table.test.tsx +95 -0
  22. package/src/__tests__/affiliate-dashboard.test.tsx +178 -0
  23. package/src/__tests__/api-401-redirect.test.ts +78 -0
  24. package/src/__tests__/api-client.test.ts +316 -0
  25. package/src/__tests__/api-config.test.ts +89 -0
  26. package/src/__tests__/api-control-instance.test.ts +69 -0
  27. package/src/__tests__/api-fleet-resources.test.ts +52 -0
  28. package/src/__tests__/api-fleet-trpc.test.ts +252 -0
  29. package/src/__tests__/api-get-instance-config.test.ts +41 -0
  30. package/src/__tests__/api-null-guards.test.ts +244 -0
  31. package/src/__tests__/api-rename-instance.test.ts +60 -0
  32. package/src/__tests__/api-update-instance-config.test.ts +60 -0
  33. package/src/__tests__/audit-log-table-pagination.test.tsx +136 -0
  34. package/src/__tests__/auth-client.test.ts +87 -0
  35. package/src/__tests__/auth-password-reset.test.tsx +435 -0
  36. package/src/__tests__/auth-redirect.test.tsx +60 -0
  37. package/src/__tests__/auth.test.tsx +269 -0
  38. package/src/__tests__/auto-topup-card.test.tsx +257 -0
  39. package/src/__tests__/backups-tab.test.tsx +221 -0
  40. package/src/__tests__/billing-byok-callout.test.tsx +76 -0
  41. package/src/__tests__/billing-layout-nav-hidden.test.tsx +47 -0
  42. package/src/__tests__/billing-payment-org-invoices.test.tsx +123 -0
  43. package/src/__tests__/billing.test.tsx +509 -0
  44. package/src/__tests__/bot-settings/resources-tab.test.tsx +119 -0
  45. package/src/__tests__/bot-settings/storage-tab.test.tsx +80 -0
  46. package/src/__tests__/bot-settings/vps-info-panel.test.tsx +108 -0
  47. package/src/__tests__/bot-settings/vps-upgrade-card.test.tsx +52 -0
  48. package/src/__tests__/bot-settings-data-control.test.ts +49 -0
  49. package/src/__tests__/bot-settings-restart.test.tsx +149 -0
  50. package/src/__tests__/bot-settings.test.tsx +678 -0
  51. package/src/__tests__/brand.test.ts +335 -0
  52. package/src/__tests__/buy-credits-panel.test.tsx +249 -0
  53. package/src/__tests__/buy-crypto-credits-panel.test.tsx +178 -0
  54. package/src/__tests__/capability-conflicts.test.ts +88 -0
  55. package/src/__tests__/capability-resolver.test.tsx +173 -0
  56. package/src/__tests__/changeset-detail.test.tsx +156 -0
  57. package/src/__tests__/channel-setup-logger.test.ts +22 -0
  58. package/src/__tests__/channel-setup-toast.test.tsx +60 -0
  59. package/src/__tests__/channel-wizard.test.tsx +505 -0
  60. package/src/__tests__/chat/ambient-dot.test.tsx +35 -0
  61. package/src/__tests__/chat/chat-input.test.tsx +78 -0
  62. package/src/__tests__/chat/chat-message.test.tsx +45 -0
  63. package/src/__tests__/chat/chat-panel.test.tsx +111 -0
  64. package/src/__tests__/chat/chat-widget.test.tsx +82 -0
  65. package/src/__tests__/chat-store.test.ts +87 -0
  66. package/src/__tests__/command-center.test.tsx +246 -0
  67. package/src/__tests__/compliance-retention-edit.test.tsx +134 -0
  68. package/src/__tests__/coupon-input.test.tsx +119 -0
  69. package/src/__tests__/create-instance.test.tsx +96 -0
  70. package/src/__tests__/create-org-wizard.test.tsx +200 -0
  71. package/src/__tests__/credit-balance.test.tsx +103 -0
  72. package/src/__tests__/credits.test.tsx +376 -0
  73. package/src/__tests__/csrf-middleware.test.ts +198 -0
  74. package/src/__tests__/degraded-state-banner.test.tsx +130 -0
  75. package/src/__tests__/dividend-calculator.test.tsx +20 -0
  76. package/src/__tests__/dividend-stats.test.tsx +64 -0
  77. package/src/__tests__/dividend.test.tsx +169 -0
  78. package/src/__tests__/dockerfile.test.ts +110 -0
  79. package/src/__tests__/email-verification-banner.test.tsx +64 -0
  80. package/src/__tests__/env-example.test.ts +25 -0
  81. package/src/__tests__/error-boundaries.test.tsx +64 -0
  82. package/src/__tests__/fetch-pricing.test.ts +121 -0
  83. package/src/__tests__/field-oauth.test.tsx +302 -0
  84. package/src/__tests__/fixtures/mock-manifests-data.js +372 -0
  85. package/src/__tests__/fixtures/mock-manifests.ts +24 -0
  86. package/src/__tests__/fleet-health-timestamp.test.tsx +101 -0
  87. package/src/__tests__/fleet-health-update.test.tsx +83 -0
  88. package/src/__tests__/format-credit.test.ts +58 -0
  89. package/src/__tests__/gpu-dashboard.test.tsx +236 -0
  90. package/src/__tests__/hosted-usage-date-range.test.tsx +54 -0
  91. package/src/__tests__/instance-detail.test.tsx +571 -0
  92. package/src/__tests__/instance-list.test.tsx +230 -0
  93. package/src/__tests__/landing-hero.test.tsx +27 -0
  94. package/src/__tests__/landing-nav.test.tsx +24 -0
  95. package/src/__tests__/layout-snapshots.test.tsx +167 -0
  96. package/src/__tests__/logger.test.ts +54 -0
  97. package/src/__tests__/login-page-redirect.test.tsx +142 -0
  98. package/src/__tests__/manifest-validation.test.ts +126 -0
  99. package/src/__tests__/marketplace-admin.test.tsx +151 -0
  100. package/src/__tests__/marketplace.test.tsx +609 -0
  101. package/src/__tests__/merge-api-rates.test.ts +70 -0
  102. package/src/__tests__/middleware.test.ts +690 -0
  103. package/src/__tests__/network-page.test.tsx +100 -0
  104. package/src/__tests__/next-config-headers.test.ts +28 -0
  105. package/src/__tests__/not-found.test.tsx +26 -0
  106. package/src/__tests__/notifications.test.tsx +128 -0
  107. package/src/__tests__/oauth-buttons.test.tsx +101 -0
  108. package/src/__tests__/oauth-error-mapping.test.tsx +97 -0
  109. package/src/__tests__/observability.test.tsx +541 -0
  110. package/src/__tests__/onboarding-data.test.ts +363 -0
  111. package/src/__tests__/onboarding-page.test.tsx +113 -0
  112. package/src/__tests__/onboarding-store.test.ts +121 -0
  113. package/src/__tests__/org-billing-api.test.tsx +70 -0
  114. package/src/__tests__/org-billing-null-guards.test.ts +64 -0
  115. package/src/__tests__/org-billing-page.test.tsx +124 -0
  116. package/src/__tests__/plugin-definition.test.ts +43 -0
  117. package/src/__tests__/plugin-install-flow.test.tsx +535 -0
  118. package/src/__tests__/plugin-registry.test.tsx +475 -0
  119. package/src/__tests__/plugin-setup/setup-chat-panel.test.ts +142 -0
  120. package/src/__tests__/plugin-setup/use-plugin-setup-chat.test.ts +49 -0
  121. package/src/__tests__/plugin-tool-definitions.test.ts +51 -0
  122. package/src/__tests__/plugin-tool-sync.test.ts +59 -0
  123. package/src/__tests__/portfolio-chart.test.tsx +24 -0
  124. package/src/__tests__/pricing.test.tsx +107 -0
  125. package/src/__tests__/promotion-form.test.tsx +180 -0
  126. package/src/__tests__/promotions-list.test.tsx +194 -0
  127. package/src/__tests__/provider-key-api.test.ts +134 -0
  128. package/src/__tests__/resend-verification-button.test.tsx +104 -0
  129. package/src/__tests__/sanitize-redirect-url.test.ts +47 -0
  130. package/src/__tests__/secrets-audit-pagination.test.tsx +139 -0
  131. package/src/__tests__/settings.test.tsx +937 -0
  132. package/src/__tests__/setup-checklist.test.tsx +274 -0
  133. package/src/__tests__/setup.ts +82 -0
  134. package/src/__tests__/smoke.test.tsx +10 -0
  135. package/src/__tests__/snapshot-api.test.ts +104 -0
  136. package/src/__tests__/status-api.test.ts +46 -0
  137. package/src/__tests__/status-badge.test.tsx +33 -0
  138. package/src/__tests__/status-colors.test.ts +83 -0
  139. package/src/__tests__/status-page.test.tsx +86 -0
  140. package/src/__tests__/step-superpowers.test.tsx +218 -0
  141. package/src/__tests__/story-sections.test.tsx +24 -0
  142. package/src/__tests__/superpower-content-sanitize.test.tsx +87 -0
  143. package/src/__tests__/superpower-content.test.tsx +44 -0
  144. package/src/__tests__/suspension-banner.test.tsx +140 -0
  145. package/src/__tests__/tenant-context.test.tsx +146 -0
  146. package/src/__tests__/tenant-keys-api.test.ts +114 -0
  147. package/src/__tests__/tenant-table-pagination.test.tsx +124 -0
  148. package/src/__tests__/terminal-log-cleanup.test.tsx +51 -0
  149. package/src/__tests__/terminal-sequence.test.tsx +28 -0
  150. package/src/__tests__/transaction-history.test.tsx +325 -0
  151. package/src/__tests__/trpc-types.test.ts +102 -0
  152. package/src/__tests__/use-capability-meta.test.ts +161 -0
  153. package/src/__tests__/use-chat.test.ts +616 -0
  154. package/src/__tests__/use-has-org.test.ts +44 -0
  155. package/src/__tests__/use-image-status.test.ts +77 -0
  156. package/src/__tests__/use-pagination-params.test.ts +88 -0
  157. package/src/__tests__/use-plugin-setup-chat-stale-closure.test.ts +53 -0
  158. package/src/__tests__/use-webmcp.test.ts +119 -0
  159. package/src/__tests__/validate-elevenlabs-key.test.ts +95 -0
  160. package/src/__tests__/validate-redirect-url.test.ts +61 -0
  161. package/src/__tests__/verify-page.test.tsx +140 -0
  162. package/src/__tests__/verify-redirect.test.tsx +41 -0
  163. package/src/__tests__/verify-result-banner.test.tsx +66 -0
  164. package/src/__tests__/webmcp-feature-detect.test.ts +54 -0
  165. package/src/__tests__/webmcp-hook.test.tsx +72 -0
  166. package/src/__tests__/webmcp-marketplace-onboarding-tools.test.ts +185 -0
  167. package/src/__tests__/webmcp-register.test.ts +103 -0
  168. package/src/__tests__/webmcp-set-provider.test.ts +47 -0
  169. package/src/__tests__/webmcp-tools.test.ts +348 -0
  170. package/src/app/(auth)/error.tsx +72 -0
  171. package/src/app/(auth)/forgot-password/page.tsx +137 -0
  172. package/src/app/(auth)/layout.tsx +14 -0
  173. package/src/app/(auth)/loading.tsx +26 -0
  174. package/src/app/(auth)/login/page.tsx +188 -0
  175. package/src/app/(auth)/reset-password/page.tsx +169 -0
  176. package/src/app/(auth)/signup/page.tsx +309 -0
  177. package/src/app/(dashboard)/billing/credits/page.tsx +209 -0
  178. package/src/app/(dashboard)/billing/error.tsx +72 -0
  179. package/src/app/(dashboard)/billing/layout.tsx +73 -0
  180. package/src/app/(dashboard)/billing/loading.tsx +41 -0
  181. package/src/app/(dashboard)/billing/payment/page.tsx +639 -0
  182. package/src/app/(dashboard)/billing/plans/page.tsx +58 -0
  183. package/src/app/(dashboard)/billing/referrals/page.tsx +7 -0
  184. package/src/app/(dashboard)/billing/usage/hosted/page.tsx +348 -0
  185. package/src/app/(dashboard)/billing/usage/page.tsx +663 -0
  186. package/src/app/(dashboard)/changesets/[id]/changeset-detail-client.tsx +400 -0
  187. package/src/app/(dashboard)/changesets/[id]/error.tsx +57 -0
  188. package/src/app/(dashboard)/changesets/[id]/loading.tsx +23 -0
  189. package/src/app/(dashboard)/changesets/[id]/page.tsx +10 -0
  190. package/src/app/(dashboard)/changesets/error.tsx +72 -0
  191. package/src/app/(dashboard)/changesets/page.tsx +10 -0
  192. package/src/app/(dashboard)/chat/page.tsx +74 -0
  193. package/src/app/(dashboard)/dashboard/bots/[id]/settings/page.tsx +10 -0
  194. package/src/app/(dashboard)/dashboard/network/page.tsx +97 -0
  195. package/src/app/(dashboard)/dashboard/page.tsx +13 -0
  196. package/src/app/(dashboard)/error.tsx +72 -0
  197. package/src/app/(dashboard)/layout.tsx +113 -0
  198. package/src/app/(dashboard)/loading.tsx +27 -0
  199. package/src/app/(dashboard)/marketplace/[plugin]/page.tsx +548 -0
  200. package/src/app/(dashboard)/marketplace/error.tsx +72 -0
  201. package/src/app/(dashboard)/marketplace/loading.tsx +27 -0
  202. package/src/app/(dashboard)/marketplace/page.tsx +268 -0
  203. package/src/app/(dashboard)/not-found.tsx +46 -0
  204. package/src/app/(dashboard)/onboarding/page.tsx +267 -0
  205. package/src/app/(dashboard)/settings/account/page.tsx +132 -0
  206. package/src/app/(dashboard)/settings/activity/page.tsx +280 -0
  207. package/src/app/(dashboard)/settings/api-keys/page.tsx +530 -0
  208. package/src/app/(dashboard)/settings/brain/page.tsx +412 -0
  209. package/src/app/(dashboard)/settings/error.tsx +72 -0
  210. package/src/app/(dashboard)/settings/layout.tsx +114 -0
  211. package/src/app/(dashboard)/settings/loading.tsx +31 -0
  212. package/src/app/(dashboard)/settings/notifications/page.tsx +216 -0
  213. package/src/app/(dashboard)/settings/org/page.tsx +617 -0
  214. package/src/app/(dashboard)/settings/profile/page.tsx +510 -0
  215. package/src/app/(dashboard)/settings/providers/page.tsx +842 -0
  216. package/src/app/(dashboard)/settings/secrets/page.tsx +658 -0
  217. package/src/app/(dashboard)/settings/security/page.tsx +1133 -0
  218. package/src/app/admin/accounting/loading.tsx +32 -0
  219. package/src/app/admin/accounting/page.tsx +5 -0
  220. package/src/app/admin/affiliates/loading.tsx +32 -0
  221. package/src/app/admin/affiliates/page.tsx +5 -0
  222. package/src/app/admin/audit/loading.tsx +32 -0
  223. package/src/app/admin/audit/page.tsx +5 -0
  224. package/src/app/admin/billing-health/loading.tsx +17 -0
  225. package/src/app/admin/billing-health/page.tsx +10 -0
  226. package/src/app/admin/compliance/page.tsx +5 -0
  227. package/src/app/admin/error.tsx +72 -0
  228. package/src/app/admin/gpu/loading.tsx +38 -0
  229. package/src/app/admin/gpu/page.tsx +5 -0
  230. package/src/app/admin/incidents/page.tsx +10 -0
  231. package/src/app/admin/inference/loading.tsx +32 -0
  232. package/src/app/admin/inference/page.tsx +5 -0
  233. package/src/app/admin/layout.tsx +44 -0
  234. package/src/app/admin/loading.tsx +32 -0
  235. package/src/app/admin/marketplace/loading.tsx +32 -0
  236. package/src/app/admin/marketplace/page.tsx +5 -0
  237. package/src/app/admin/migrations/loading.tsx +22 -0
  238. package/src/app/admin/migrations/page.tsx +5 -0
  239. package/src/app/admin/onboarding/loading.tsx +18 -0
  240. package/src/app/admin/onboarding/page.tsx +5 -0
  241. package/src/app/admin/promotions/[id]/edit/loading.tsx +16 -0
  242. package/src/app/admin/promotions/[id]/edit/page.tsx +56 -0
  243. package/src/app/admin/promotions/[id]/loading.tsx +15 -0
  244. package/src/app/admin/promotions/[id]/page.tsx +311 -0
  245. package/src/app/admin/promotions/loading.tsx +21 -0
  246. package/src/app/admin/promotions/new/loading.tsx +16 -0
  247. package/src/app/admin/promotions/new/page.tsx +12 -0
  248. package/src/app/admin/promotions/page.tsx +266 -0
  249. package/src/app/admin/rate-overrides/loading.tsx +17 -0
  250. package/src/app/admin/rate-overrides/page.tsx +290 -0
  251. package/src/app/admin/roles/loading.tsx +27 -0
  252. package/src/app/admin/roles/page.tsx +5 -0
  253. package/src/app/admin/tenants/loading.tsx +32 -0
  254. package/src/app/admin/tenants/page.tsx +5 -0
  255. package/src/app/apple-icon.tsx +32 -0
  256. package/src/app/auth/callback/[provider]/page.tsx +104 -0
  257. package/src/app/auth/verify/page.tsx +224 -0
  258. package/src/app/channels/error.tsx +72 -0
  259. package/src/app/channels/loading.tsx +29 -0
  260. package/src/app/channels/page.tsx +262 -0
  261. package/src/app/channels/setup/[plugin]/page.tsx +136 -0
  262. package/src/app/error.tsx +72 -0
  263. package/src/app/favicon.ico +0 -0
  264. package/src/app/fleet/error.tsx +72 -0
  265. package/src/app/fleet/health/page.tsx +9 -0
  266. package/src/app/fleet/layout.tsx +14 -0
  267. package/src/app/fleet/loading.tsx +33 -0
  268. package/src/app/fleet/page.tsx +5 -0
  269. package/src/app/global-error.tsx +96 -0
  270. package/src/app/globals.css +251 -0
  271. package/src/app/icon.svg +4 -0
  272. package/src/app/instances/[id]/instance-detail-client.tsx +1298 -0
  273. package/src/app/instances/[id]/page.tsx +10 -0
  274. package/src/app/instances/error.tsx +72 -0
  275. package/src/app/instances/instance-list-client.tsx +540 -0
  276. package/src/app/instances/loading.tsx +33 -0
  277. package/src/app/instances/new/create-instance-client.tsx +377 -0
  278. package/src/app/instances/new/page.tsx +9 -0
  279. package/src/app/instances/page.tsx +9 -0
  280. package/src/app/layout.tsx +83 -0
  281. package/src/app/not-found.tsx +38 -0
  282. package/src/app/og/route.tsx +50 -0
  283. package/src/app/page.tsx +39 -0
  284. package/src/app/plugins/error.tsx +72 -0
  285. package/src/app/plugins/layout.tsx +14 -0
  286. package/src/app/plugins/loading.tsx +30 -0
  287. package/src/app/plugins/page.tsx +555 -0
  288. package/src/app/pricing/error.tsx +72 -0
  289. package/src/app/pricing/loading.tsx +25 -0
  290. package/src/app/pricing/page.tsx +20 -0
  291. package/src/app/privacy/page.tsx +406 -0
  292. package/src/app/robots.ts +9 -0
  293. package/src/app/sitemap.ts +11 -0
  294. package/src/app/status/error.tsx +72 -0
  295. package/src/app/status/loading.tsx +21 -0
  296. package/src/app/status/page.tsx +20 -0
  297. package/src/app/terms/page.tsx +414 -0
  298. package/src/components/account-switcher.tsx +82 -0
  299. package/src/components/admin/accounting-dashboard.tsx +190 -0
  300. package/src/components/admin/admin-guard.tsx +36 -0
  301. package/src/components/admin/admin-nav.tsx +71 -0
  302. package/src/components/admin/affiliate-dashboard.tsx +564 -0
  303. package/src/components/admin/audit-log-table.tsx +336 -0
  304. package/src/components/admin/billing-health-dashboard.test.tsx +40 -0
  305. package/src/components/admin/billing-health-dashboard.tsx +416 -0
  306. package/src/components/admin/bulk-actions-bar.test.tsx +92 -0
  307. package/src/components/admin/bulk-actions-bar.tsx +80 -0
  308. package/src/components/admin/bulk-export-dialog.test.tsx +75 -0
  309. package/src/components/admin/bulk-export-dialog.tsx +189 -0
  310. package/src/components/admin/bulk-grant-dialog.test.tsx +81 -0
  311. package/src/components/admin/bulk-grant-dialog.tsx +147 -0
  312. package/src/components/admin/bulk-preview-dialog.test.tsx +72 -0
  313. package/src/components/admin/bulk-preview-dialog.tsx +106 -0
  314. package/src/components/admin/bulk-reactivate-dialog.test.tsx +51 -0
  315. package/src/components/admin/bulk-reactivate-dialog.tsx +55 -0
  316. package/src/components/admin/bulk-select-all-banner.test.tsx +36 -0
  317. package/src/components/admin/bulk-select-all-banner.tsx +44 -0
  318. package/src/components/admin/bulk-suspend-dialog.test.tsx +77 -0
  319. package/src/components/admin/bulk-suspend-dialog.tsx +129 -0
  320. package/src/components/admin/bulk-undo-toast.test.tsx +66 -0
  321. package/src/components/admin/bulk-undo-toast.tsx +121 -0
  322. package/src/components/admin/compliance-dashboard.tsx +1341 -0
  323. package/src/components/admin/gpu-dashboard.tsx +552 -0
  324. package/src/components/admin/grant-credits-dialog.tsx +121 -0
  325. package/src/components/admin/incident-dashboard.test.tsx +44 -0
  326. package/src/components/admin/incident-dashboard.tsx +717 -0
  327. package/src/components/admin/inference-dashboard.tsx +415 -0
  328. package/src/components/admin/marketplace-admin.tsx +765 -0
  329. package/src/components/admin/migrations-dashboard.tsx +404 -0
  330. package/src/components/admin/onboarding-dashboard.tsx +422 -0
  331. package/src/components/admin/promotions/promotion-form.tsx +440 -0
  332. package/src/components/admin/roles-dashboard.tsx +278 -0
  333. package/src/components/admin/suspend-dialog.tsx +98 -0
  334. package/src/components/admin/tenant-notes-panel.tsx +134 -0
  335. package/src/components/admin/tenant-row-actions.tsx +78 -0
  336. package/src/components/admin/tenant-table.tsx +339 -0
  337. package/src/components/auth/auth-error.tsx +22 -0
  338. package/src/components/auth/auth-redirect.tsx +18 -0
  339. package/src/components/auth/auth-shell.tsx +25 -0
  340. package/src/components/auth/email-verification-banner.tsx +25 -0
  341. package/src/components/auth/email-verification-result-banner.tsx +70 -0
  342. package/src/components/auth/resend-verification-button.tsx +94 -0
  343. package/src/components/auth/wopr-wordmark.tsx +19 -0
  344. package/src/components/billing/add-payment-method-dialog.tsx +267 -0
  345. package/src/components/billing/affiliate-dashboard.tsx +300 -0
  346. package/src/components/billing/auto-topup-card.tsx +432 -0
  347. package/src/components/billing/buy-credits-panel.tsx +180 -0
  348. package/src/components/billing/buy-crypto-credits-panel.tsx +96 -0
  349. package/src/components/billing/byok-callout.tsx +87 -0
  350. package/src/components/billing/coupon-input.tsx +86 -0
  351. package/src/components/billing/credit-balance.tsx +79 -0
  352. package/src/components/billing/degraded-state-banner.tsx +95 -0
  353. package/src/components/billing/dividend-banner.tsx +97 -0
  354. package/src/components/billing/dividend-eligibility.tsx +86 -0
  355. package/src/components/billing/dividend-pool-stats.tsx +86 -0
  356. package/src/components/billing/first-dividend-dialog.tsx +109 -0
  357. package/src/components/billing/low-balance-banner.tsx +50 -0
  358. package/src/components/billing/org-billing-page.tsx +360 -0
  359. package/src/components/billing/suspension-banner.tsx +53 -0
  360. package/src/components/billing/transaction-history.tsx +239 -0
  361. package/src/components/bot-settings/__tests__/bot-settings-client.test.tsx +205 -0
  362. package/src/components/bot-settings/backups-tab.tsx +377 -0
  363. package/src/components/bot-settings/bot-settings-client.tsx +1712 -0
  364. package/src/components/bot-settings/resources-tab.tsx +203 -0
  365. package/src/components/bot-settings/storage-tab.tsx +248 -0
  366. package/src/components/bot-settings/vps-info-panel.tsx +132 -0
  367. package/src/components/bot-settings/vps-upgrade-card.tsx +110 -0
  368. package/src/components/capability/CapabilityResolver.tsx +113 -0
  369. package/src/components/channel-wizard/field-interactive.tsx +48 -0
  370. package/src/components/channel-wizard/field-oauth.tsx +181 -0
  371. package/src/components/channel-wizard/field-paste.tsx +47 -0
  372. package/src/components/channel-wizard/field-qr.tsx +302 -0
  373. package/src/components/channel-wizard/index.ts +6 -0
  374. package/src/components/channel-wizard/step-renderer.tsx +103 -0
  375. package/src/components/channel-wizard/wizard.tsx +200 -0
  376. package/src/components/chat/ambient-dot.tsx +32 -0
  377. package/src/components/chat/chat-input.tsx +56 -0
  378. package/src/components/chat/chat-message.tsx +36 -0
  379. package/src/components/chat/chat-panel.tsx +138 -0
  380. package/src/components/chat/chat-widget.tsx +41 -0
  381. package/src/components/chat/index.ts +5 -0
  382. package/src/components/dashboard/command-center.tsx +614 -0
  383. package/src/components/instances/friends-tab.test.tsx +265 -0
  384. package/src/components/instances/friends-tab.tsx +721 -0
  385. package/src/components/landing/hero.tsx +53 -0
  386. package/src/components/landing/landing-nav.tsx +21 -0
  387. package/src/components/landing/landing-page.tsx +71 -0
  388. package/src/components/landing/portfolio-chart.tsx +349 -0
  389. package/src/components/landing/story-sections.tsx +50 -0
  390. package/src/components/landing/terminal-lines.ts +99 -0
  391. package/src/components/landing/terminal-sequence.tsx +453 -0
  392. package/src/components/landing/typing-effect.tsx +43 -0
  393. package/src/components/marketplace/category-filter.tsx +61 -0
  394. package/src/components/marketplace/empty-state.tsx +61 -0
  395. package/src/components/marketplace/featured-heroes.tsx +84 -0
  396. package/src/components/marketplace/first-visit-hero.tsx +110 -0
  397. package/src/components/marketplace/index.ts +9 -0
  398. package/src/components/marketplace/install-wizard.tsx +782 -0
  399. package/src/components/marketplace/marketplace-tabs.tsx +54 -0
  400. package/src/components/marketplace/plugin-card.tsx +129 -0
  401. package/src/components/marketplace/superpower-card.tsx +104 -0
  402. package/src/components/marketplace/superpower-content.tsx +117 -0
  403. package/src/components/marketplace/terminal-search.tsx +67 -0
  404. package/src/components/oauth-buttons.tsx +75 -0
  405. package/src/components/observability/fleet-health.tsx +370 -0
  406. package/src/components/observability/health-overview.tsx +246 -0
  407. package/src/components/observability/logs-viewer.tsx +215 -0
  408. package/src/components/observability/metrics-dashboard.tsx +288 -0
  409. package/src/components/onboarding/fallback-setup.tsx +137 -0
  410. package/src/components/onboarding/index.ts +3 -0
  411. package/src/components/onboarding/setup-checklist.tsx +333 -0
  412. package/src/components/onboarding/step-superpowers.tsx +122 -0
  413. package/src/components/plugin-setup/index.ts +1 -0
  414. package/src/components/plugin-setup/setup-chat-panel.tsx +188 -0
  415. package/src/components/pricing/dividend-calculator.tsx +47 -0
  416. package/src/components/pricing/dividend-stats.tsx +117 -0
  417. package/src/components/pricing/pricing-page.tsx +229 -0
  418. package/src/components/settings/create-org-wizard.tsx +225 -0
  419. package/src/components/sidebar.tsx +202 -0
  420. package/src/components/status/status-page.tsx +209 -0
  421. package/src/components/status-badge.tsx +28 -0
  422. package/src/components/theme-provider.tsx +8 -0
  423. package/src/components/ui/alert-dialog.tsx +141 -0
  424. package/src/components/ui/badge.tsx +47 -0
  425. package/src/components/ui/banner.tsx +36 -0
  426. package/src/components/ui/button.tsx +64 -0
  427. package/src/components/ui/card.tsx +75 -0
  428. package/src/components/ui/checkbox.tsx +52 -0
  429. package/src/components/ui/collapsible.tsx +31 -0
  430. package/src/components/ui/credit-detailed.tsx +33 -0
  431. package/src/components/ui/dialog.tsx +143 -0
  432. package/src/components/ui/dropdown-menu.tsx +228 -0
  433. package/src/components/ui/form.tsx +151 -0
  434. package/src/components/ui/input.tsx +21 -0
  435. package/src/components/ui/label.tsx +21 -0
  436. package/src/components/ui/popover.tsx +74 -0
  437. package/src/components/ui/progress.tsx +28 -0
  438. package/src/components/ui/radio-group.tsx +45 -0
  439. package/src/components/ui/select.tsx +175 -0
  440. package/src/components/ui/separator.tsx +28 -0
  441. package/src/components/ui/sheet.tsx +125 -0
  442. package/src/components/ui/skeleton.tsx +15 -0
  443. package/src/components/ui/switch.tsx +35 -0
  444. package/src/components/ui/table.tsx +92 -0
  445. package/src/components/ui/tabs.tsx +81 -0
  446. package/src/components/ui/textarea.tsx +18 -0
  447. package/src/components/ui/tooltip.tsx +44 -0
  448. package/src/config/provider-docs.ts +17 -0
  449. package/src/hooks/__tests__/use-async.test.ts +127 -0
  450. package/src/hooks/__tests__/use-count-up.test.ts +129 -0
  451. package/src/hooks/__tests__/use-debounce.test.ts +105 -0
  452. package/src/hooks/__tests__/use-fleet-sse.test.ts +216 -0
  453. package/src/hooks/__tests__/use-local-storage.test.ts +74 -0
  454. package/src/hooks/__tests__/use-mobile.test.ts +86 -0
  455. package/src/hooks/__tests__/use-save-queue.test.ts +159 -0
  456. package/src/hooks/use-async.ts +54 -0
  457. package/src/hooks/use-capability-meta.ts +99 -0
  458. package/src/hooks/use-count-up.ts +23 -0
  459. package/src/hooks/use-debounce.ts +12 -0
  460. package/src/hooks/use-fleet-sse.ts +47 -0
  461. package/src/hooks/use-has-org.ts +18 -0
  462. package/src/hooks/use-image-status.ts +36 -0
  463. package/src/hooks/use-local-storage.ts +36 -0
  464. package/src/hooks/use-mobile.ts +17 -0
  465. package/src/hooks/use-page-context.ts +24 -0
  466. package/src/hooks/use-pagination-params.ts +30 -0
  467. package/src/hooks/use-plugin-registry.ts +247 -0
  468. package/src/hooks/use-plugin-setup-chat.ts +211 -0
  469. package/src/hooks/use-save-queue.ts +54 -0
  470. package/src/hooks/use-webmcp.ts +40 -0
  471. package/src/lib/__tests__/__snapshots__/pricing-data.test.ts.snap +112 -0
  472. package/src/lib/__tests__/admin-api.test.ts +487 -0
  473. package/src/lib/__tests__/api-bot-crud.test.ts +391 -0
  474. package/src/lib/__tests__/api-fetch.test.ts +196 -0
  475. package/src/lib/__tests__/bot-settings-data.test.ts +352 -0
  476. package/src/lib/__tests__/org-api.test.ts +281 -0
  477. package/src/lib/__tests__/org-billing-api.test.ts +242 -0
  478. package/src/lib/__tests__/pricing-data.test.ts +32 -0
  479. package/src/lib/__tests__/settings-api.test.ts +272 -0
  480. package/src/lib/admin-affiliate-api.ts +51 -0
  481. package/src/lib/admin-api.ts +325 -0
  482. package/src/lib/admin-compliance-api.ts +127 -0
  483. package/src/lib/admin-gpu-api.ts +82 -0
  484. package/src/lib/admin-incident-api.ts +121 -0
  485. package/src/lib/admin-inference-api.ts +47 -0
  486. package/src/lib/admin-marketplace-api.ts +97 -0
  487. package/src/lib/api-config.test.ts +111 -0
  488. package/src/lib/api-config.ts +65 -0
  489. package/src/lib/api-errors.test.ts +43 -0
  490. package/src/lib/api.ts +2011 -0
  491. package/src/lib/auth-client.ts +11 -0
  492. package/src/lib/bot-settings-data.ts +342 -0
  493. package/src/lib/brand-config.ts +145 -0
  494. package/src/lib/brand.ts +669 -0
  495. package/src/lib/changeset-api.ts +29 -0
  496. package/src/lib/changeset-types.ts +56 -0
  497. package/src/lib/channel-manifests.ts +50 -0
  498. package/src/lib/chat/chat-context.tsx +70 -0
  499. package/src/lib/chat/chat-store.ts +62 -0
  500. package/src/lib/chat/types.ts +35 -0
  501. package/src/lib/chat/use-chat.ts +255 -0
  502. package/src/lib/cost-comparison-data.test.ts +95 -0
  503. package/src/lib/cost-comparison-data.ts +54 -0
  504. package/src/lib/errors.test.ts +64 -0
  505. package/src/lib/errors.ts +52 -0
  506. package/src/lib/fetch-utils.test.ts +57 -0
  507. package/src/lib/fetch-utils.ts +25 -0
  508. package/src/lib/format-credit.test.ts +66 -0
  509. package/src/lib/format-credit.ts +24 -0
  510. package/src/lib/format.test.ts +62 -0
  511. package/src/lib/format.ts +17 -0
  512. package/src/lib/logger.ts +28 -0
  513. package/src/lib/marketplace-data.ts +346 -0
  514. package/src/lib/oauth-errors.ts +19 -0
  515. package/src/lib/onboarding-data.ts +1265 -0
  516. package/src/lib/onboarding-store.ts +233 -0
  517. package/src/lib/org-api.ts +74 -0
  518. package/src/lib/org-billing-api.ts +81 -0
  519. package/src/lib/page-prompts.test.ts +32 -0
  520. package/src/lib/page-prompts.ts +23 -0
  521. package/src/lib/plugin/index.ts +32 -0
  522. package/src/lib/plugin/tool-definitions.ts +306 -0
  523. package/src/lib/pricing-data.ts +115 -0
  524. package/src/lib/promotions-types.ts +58 -0
  525. package/src/lib/settings-api.ts +63 -0
  526. package/src/lib/status-colors.ts +38 -0
  527. package/src/lib/tenant-context.tsx +134 -0
  528. package/src/lib/trpc-types.ts +173 -0
  529. package/src/lib/trpc.tsx +86 -0
  530. package/src/lib/utils.test.ts +55 -0
  531. package/src/lib/utils.ts +18 -0
  532. package/src/lib/validate-redirect-url.ts +39 -0
  533. package/src/lib/webmcp/feature-detect.ts +13 -0
  534. package/src/lib/webmcp/marketplace-onboarding-tools.ts +202 -0
  535. package/src/lib/webmcp/register.ts +44 -0
  536. package/src/lib/webmcp/tools.ts +422 -0
  537. package/src/proxy.ts +258 -0
  538. package/src/types/missing-deps.d.ts +160 -0
  539. package/src/types/motion-dom.d.ts +162 -0
  540. package/src/types/vitest-matchers.d.ts +40 -0
  541. package/src/types/web-mcp.d.ts +22 -0
  542. package/tsconfig.json +34 -0
  543. package/vitest.config.ts +26 -0
@@ -0,0 +1,609 @@
1
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
2
+ import { render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
+ import type { PluginManifest } from "../lib/marketplace-data";
6
+ import {
7
+ ALL_CATEGORIES,
8
+ formatInstallCount,
9
+ getHostedAdaptersForCapabilities,
10
+ HOSTED_ADAPTERS,
11
+ hasHostedOption,
12
+ listInstalledPlugins,
13
+ } from "../lib/marketplace-data";
14
+ import { findManifest as findManifestFixture } from "./fixtures/mock-manifests";
15
+
16
+ // vi.hoisted runs before module imports so TEST_PLUGINS is available in vi.mock factories
17
+ const { TEST_PLUGINS } = vi.hoisted(() => {
18
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
19
+ const { MARKETPLACE_TEST_PLUGINS } =
20
+ require("./fixtures/mock-manifests-data") as typeof import("./fixtures/mock-manifests");
21
+ return { TEST_PLUGINS: MARKETPLACE_TEST_PLUGINS };
22
+ });
23
+
24
+ // --- Mock next/navigation for page components ---
25
+ const mockPush = vi.fn();
26
+ const mockParams: { plugin?: string } = {};
27
+ vi.mock("next/navigation", () => ({
28
+ useRouter: () => ({ push: mockPush }),
29
+ usePathname: () => "/marketplace",
30
+ useParams: () => mockParams,
31
+ useSearchParams: () => ({ get: () => null }),
32
+ }));
33
+
34
+ // --- Mock next/link ---
35
+ vi.mock("next/link", () => ({
36
+ default: ({
37
+ children,
38
+ href,
39
+ ...props
40
+ }: { children: React.ReactNode; href: string } & Record<string, unknown>) => (
41
+ <a href={href} {...props}>
42
+ {children}
43
+ </a>
44
+ ),
45
+ }));
46
+
47
+ // --- Mock useCapabilityMeta so CapabilityProviderPicker resolves stt/llm to hosted entries ---
48
+ vi.mock("@/hooks/use-capability-meta", () => ({
49
+ useCapabilityMeta: () => ({
50
+ meta: [
51
+ {
52
+ capability: "llm",
53
+ label: "LLM",
54
+ description: "200+ models via OpenRouter.",
55
+ pricing: "$0.002/1K tokens",
56
+ hostedProvider: "OpenRouter",
57
+ icon: "bot",
58
+ sortOrder: 0,
59
+ },
60
+ {
61
+ capability: "stt",
62
+ label: "STT",
63
+ description: "Speech-to-text powered by Deepgram.",
64
+ pricing: "$0.005/min",
65
+ hostedProvider: "Deepgram",
66
+ icon: "mic",
67
+ sortOrder: 1,
68
+ },
69
+ ],
70
+ loading: false,
71
+ error: false,
72
+ getMeta: (cap: string) => {
73
+ const entries: Record<
74
+ string,
75
+ {
76
+ capability: string;
77
+ label: string;
78
+ description: string;
79
+ pricing: string;
80
+ hostedProvider: string;
81
+ icon: string;
82
+ sortOrder: number;
83
+ }
84
+ > = {
85
+ llm: {
86
+ capability: "llm",
87
+ label: "LLM",
88
+ description: "200+ models via OpenRouter.",
89
+ pricing: "$0.002/1K tokens",
90
+ hostedProvider: "OpenRouter",
91
+ icon: "bot",
92
+ sortOrder: 0,
93
+ },
94
+ stt: {
95
+ capability: "stt",
96
+ label: "STT",
97
+ description: "Speech-to-text powered by Deepgram.",
98
+ pricing: "$0.005/min",
99
+ hostedProvider: "Deepgram",
100
+ icon: "mic",
101
+ sortOrder: 1,
102
+ },
103
+ };
104
+ return (
105
+ entries[cap] ?? {
106
+ capability: cap,
107
+ label: cap.replace(/[-_]/g, " ").replace(/\b\w/g, (c: string) => c.toUpperCase()),
108
+ description: "",
109
+ pricing: "",
110
+ hostedProvider: "",
111
+ icon: "sparkles",
112
+ sortOrder: 999,
113
+ }
114
+ );
115
+ },
116
+ }),
117
+ }));
118
+
119
+ // --- Mock marketplace-data API functions ---
120
+ vi.mock("../lib/marketplace-data", async () => {
121
+ const actual = await vi.importActual("../lib/marketplace-data");
122
+ return {
123
+ ...actual,
124
+ listMarketplacePlugins: vi.fn().mockResolvedValue(TEST_PLUGINS),
125
+ getMarketplacePlugin: vi.fn().mockImplementation(async (id: string) => {
126
+ return TEST_PLUGINS.find((p) => p.id === id) ?? null;
127
+ }),
128
+ getPluginContent: vi.fn().mockResolvedValue(null),
129
+ listInstalledPlugins: vi.fn().mockResolvedValue([]),
130
+ };
131
+ });
132
+
133
+ function findManifest(id: string): PluginManifest {
134
+ return findManifestFixture(id) as unknown as PluginManifest;
135
+ }
136
+
137
+ function renderWithQueryClient(ui: React.ReactElement) {
138
+ const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
139
+ return render(<QueryClientProvider client={queryClient}>{ui}</QueryClientProvider>);
140
+ }
141
+
142
+ function closestButton(el: HTMLElement): HTMLElement {
143
+ const btn = el.closest("button");
144
+ if (!btn) throw new Error("No parent button found");
145
+ return btn;
146
+ }
147
+
148
+ // --- Data layer tests ---
149
+ describe("marketplace-data", () => {
150
+ describe("formatInstallCount", () => {
151
+ it("formats thousands with k suffix", () => {
152
+ expect(formatInstallCount(12400)).toBe("12.4k");
153
+ expect(formatInstallCount(8200)).toBe("8.2k");
154
+ expect(formatInstallCount(1000)).toBe("1.0k");
155
+ });
156
+
157
+ it("returns plain number for counts under 1000", () => {
158
+ expect(formatInstallCount(500)).toBe("500");
159
+ expect(formatInstallCount(0)).toBe("0");
160
+ });
161
+ });
162
+
163
+ describe("hasHostedOption", () => {
164
+ it("returns true when capabilities match hosted adapters", () => {
165
+ expect(hasHostedOption(["llm"])).toBe(true);
166
+ expect(hasHostedOption(["tts"])).toBe(true);
167
+ expect(hasHostedOption(["stt"])).toBe(true);
168
+ expect(hasHostedOption(["embeddings"])).toBe(true);
169
+ expect(hasHostedOption(["image-gen"])).toBe(true);
170
+ });
171
+
172
+ it("returns false when no capabilities match", () => {
173
+ expect(hasHostedOption(["channel"])).toBe(false);
174
+ expect(hasHostedOption(["webhook"])).toBe(false);
175
+ expect(hasHostedOption(["ui"])).toBe(false);
176
+ expect(hasHostedOption([])).toBe(false);
177
+ });
178
+
179
+ it("returns true if any capability matches", () => {
180
+ expect(hasHostedOption(["channel", "llm"])).toBe(true);
181
+ expect(hasHostedOption(["stt", "webhook"])).toBe(true);
182
+ });
183
+ });
184
+
185
+ describe("getHostedAdaptersForCapabilities", () => {
186
+ it("returns matching hosted adapters", () => {
187
+ const adapters = getHostedAdaptersForCapabilities(["stt", "llm"]);
188
+ expect(adapters).toHaveLength(2);
189
+ expect(adapters.map((a) => a.capability)).toContain("llm");
190
+ expect(adapters.map((a) => a.capability)).toContain("stt");
191
+ });
192
+
193
+ it("returns empty array when no match", () => {
194
+ expect(getHostedAdaptersForCapabilities(["channel"])).toHaveLength(0);
195
+ });
196
+ });
197
+
198
+ describe("HOSTED_ADAPTERS", () => {
199
+ it("has entries for key hosted capabilities", () => {
200
+ const caps = HOSTED_ADAPTERS.map((a) => a.capability);
201
+ expect(caps).toContain("llm");
202
+ expect(caps).toContain("tts");
203
+ expect(caps).toContain("stt");
204
+ expect(caps).toContain("embeddings");
205
+ expect(caps).toContain("image-gen");
206
+ });
207
+ });
208
+
209
+ describe("ALL_CATEGORIES", () => {
210
+ it("includes standard plugin categories", () => {
211
+ const ids = ALL_CATEGORIES.map((c) => c.id);
212
+ expect(ids).toContain("channel");
213
+ expect(ids).toContain("voice");
214
+ expect(ids).toContain("memory");
215
+ expect(ids).toContain("integration");
216
+ expect(ids).toContain("webhook");
217
+ });
218
+ });
219
+ });
220
+
221
+ // --- Component tests ---
222
+ describe("MarketplacePage", () => {
223
+ beforeEach(() => {
224
+ mockPush.mockClear();
225
+ mockParams.plugin = undefined;
226
+ });
227
+
228
+ it("renders marketplace page with heading and plugin cards", async () => {
229
+ const { default: MarketplacePage } = await import("../app/(dashboard)/marketplace/page");
230
+ render(<MarketplacePage />);
231
+
232
+ // Initially shows skeleton loading state
233
+ expect(document.querySelector('[data-slot="skeleton"]')).toBeInTheDocument();
234
+
235
+ // After loading, shows heading and featured section
236
+ expect(await screen.findByText("Browse Superpowers")).toBeInTheDocument();
237
+ expect(await screen.findByText("Featured Superpowers")).toBeInTheDocument();
238
+ });
239
+
240
+ it("filters plugins by search term", async () => {
241
+ const user = userEvent.setup();
242
+ const { default: MarketplacePage } = await import("../app/(dashboard)/marketplace/page");
243
+ render(<MarketplacePage />);
244
+
245
+ await screen.findByText("Browse Superpowers");
246
+
247
+ const searchInput = screen.getByPlaceholderText("Search superpowers...");
248
+ await user.type(searchInput, "nonexistentterm12345");
249
+
250
+ // No results in the grid
251
+ expect(screen.getByText(/No results for/)).toBeInTheDocument();
252
+ });
253
+
254
+ it("filters plugins by tab", async () => {
255
+ const user = userEvent.setup();
256
+ const { default: MarketplacePage } = await import("../app/(dashboard)/marketplace/page");
257
+ render(<MarketplacePage />);
258
+
259
+ await screen.findByText("Browse Superpowers");
260
+
261
+ // Click "Channels" tab
262
+ const channelsButton = closestButton(screen.getByText("Channels"));
263
+ await user.click(channelsButton);
264
+
265
+ // Should show channel plugins in the grid
266
+ expect(screen.getByText("Discord")).toBeInTheDocument();
267
+ expect(screen.getByText("Slack")).toBeInTheDocument();
268
+
269
+ // Should not show non-channel plugins like webhooks in the grid
270
+ expect(screen.queryByText("Webhooks")).not.toBeInTheDocument();
271
+ });
272
+
273
+ it("shows Platform Hosted Available badge for eligible plugins", async () => {
274
+ const user = userEvent.setup();
275
+ const { default: MarketplacePage } = await import("../app/(dashboard)/marketplace/page");
276
+ render(<MarketplacePage />);
277
+
278
+ await screen.findByText("Browse Superpowers");
279
+
280
+ // Switch to Capabilities tab — Deepgram STT has 'stt' capability which matches a hosted adapter
281
+ const capabilitiesButton = closestButton(screen.getByText("Capabilities"));
282
+ await user.click(capabilitiesButton);
283
+
284
+ // PluginCard shows "Platform Hosted" badge for eligible plugins
285
+ const hostedBadges = screen.getAllByText("Platform Hosted");
286
+ expect(hostedBadges.length).toBeGreaterThan(0);
287
+ });
288
+
289
+ it("shows empty state when no plugins match search", async () => {
290
+ const user = userEvent.setup();
291
+ const { default: MarketplacePage } = await import("../app/(dashboard)/marketplace/page");
292
+ render(<MarketplacePage />);
293
+
294
+ await screen.findByText("Browse Superpowers");
295
+
296
+ const searchInput = screen.getByPlaceholderText("Search superpowers...");
297
+ await user.type(searchInput, "nonexistentplugin12345");
298
+
299
+ expect(screen.getByText(/No results for/)).toBeInTheDocument();
300
+ });
301
+ });
302
+
303
+ describe("PluginDetailPage", () => {
304
+ beforeEach(() => {
305
+ mockPush.mockClear();
306
+ });
307
+
308
+ it("renders plugin detail page with manifest info", async () => {
309
+ mockParams.plugin = "discord";
310
+ const { default: PluginDetailPage } = await import(
311
+ "../app/(dashboard)/marketplace/[plugin]/page"
312
+ );
313
+ renderWithQueryClient(<PluginDetailPage />);
314
+
315
+ // Wait for loading
316
+ expect(await screen.findByText("Discord")).toBeInTheDocument();
317
+ expect(screen.getByText("v3.2.0")).toBeInTheDocument();
318
+ expect(screen.getByText("Give my bot this superpower")).toBeInTheDocument();
319
+ expect(screen.getByText(/12\.4k installs/)).toBeInTheDocument();
320
+ });
321
+
322
+ it("shows not found for invalid plugin id", async () => {
323
+ mockParams.plugin = "nonexistent-plugin";
324
+ const { default: PluginDetailPage } = await import(
325
+ "../app/(dashboard)/marketplace/[plugin]/page"
326
+ );
327
+ renderWithQueryClient(<PluginDetailPage />);
328
+
329
+ expect(await screen.findByText("Plugin not found.")).toBeInTheDocument();
330
+ });
331
+
332
+ it("shows hosted adapter info for eligible plugins", async () => {
333
+ mockParams.plugin = "semantic-memory";
334
+ const { default: PluginDetailPage } = await import(
335
+ "../app/(dashboard)/marketplace/[plugin]/page"
336
+ );
337
+ renderWithQueryClient(<PluginDetailPage />);
338
+
339
+ await screen.findByText("A Bot That Never Forgets");
340
+ expect(screen.getByText("Platform Hosted")).toBeInTheDocument();
341
+ expect(screen.getByText("Platform Hosted Options")).toBeInTheDocument();
342
+ expect(screen.getByText("Platform Hosted Embeddings")).toBeInTheDocument();
343
+ });
344
+
345
+ it("shows requirements for plugins with dependencies", async () => {
346
+ mockParams.plugin = "meeting-transcriber";
347
+ const { default: PluginDetailPage } = await import(
348
+ "../app/(dashboard)/marketplace/[plugin]/page"
349
+ );
350
+ renderWithQueryClient(<PluginDetailPage />);
351
+
352
+ await screen.findByText("Fire Your Secretary");
353
+ expect(screen.getByText("Discord (for voice channels)")).toBeInTheDocument();
354
+ });
355
+
356
+ it("opens install wizard when Install button is clicked", async () => {
357
+ const user = userEvent.setup();
358
+ mockParams.plugin = "webhooks";
359
+ const { default: PluginDetailPage } = await import(
360
+ "../app/(dashboard)/marketplace/[plugin]/page"
361
+ );
362
+ renderWithQueryClient(<PluginDetailPage />);
363
+
364
+ await screen.findByText("Webhooks");
365
+ const installButton = screen.getByText("Give my bot this superpower");
366
+ await user.click(installButton);
367
+
368
+ // Should now show install wizard
369
+ expect(screen.getByText("Install Webhooks")).toBeInTheDocument();
370
+ });
371
+
372
+ it("shows changelog entries", async () => {
373
+ mockParams.plugin = "discord";
374
+ const { default: PluginDetailPage } = await import(
375
+ "../app/(dashboard)/marketplace/[plugin]/page"
376
+ );
377
+ renderWithQueryClient(<PluginDetailPage />);
378
+
379
+ await screen.findByText("Discord");
380
+
381
+ // Click changelog tab
382
+ const user = userEvent.setup();
383
+ await user.click(screen.getByText("Changelog"));
384
+
385
+ expect(screen.getByText("Added thread support and slash commands.")).toBeInTheDocument();
386
+ // v3.2.0 appears in the changelog entry badge (overview tab version footer is hidden)
387
+ expect(screen.getAllByText("v3.2.0").length).toBeGreaterThanOrEqual(1);
388
+ });
389
+
390
+ it("shows configuration schema", async () => {
391
+ mockParams.plugin = "discord";
392
+ const { default: PluginDetailPage } = await import(
393
+ "../app/(dashboard)/marketplace/[plugin]/page"
394
+ );
395
+ renderWithQueryClient(<PluginDetailPage />);
396
+
397
+ await screen.findByText("Discord");
398
+
399
+ const user = userEvent.setup();
400
+ await user.click(screen.getByText("Configuration"));
401
+
402
+ expect(screen.getByText("Bot Token")).toBeInTheDocument();
403
+ expect(screen.getByText("Server ID")).toBeInTheDocument();
404
+ });
405
+ });
406
+
407
+ describe("InstallWizard", () => {
408
+ const mockBots = [
409
+ { id: "00000000-0000-4000-8000-000000000001", name: "My Bot", state: "running" },
410
+ ];
411
+
412
+ beforeEach(() => {
413
+ // Mock fetch for listBots so the wizard doesn't hang on network calls
414
+ vi.stubGlobal(
415
+ "fetch",
416
+ vi.fn().mockResolvedValue({
417
+ ok: true,
418
+ status: 200,
419
+ headers: { get: vi.fn().mockReturnValue(null) },
420
+ json: async () => ({ bots: mockBots }),
421
+ }),
422
+ );
423
+ });
424
+
425
+ afterEach(() => {
426
+ vi.unstubAllGlobals();
427
+ });
428
+
429
+ it("renders wizard with cancel and continue buttons", async () => {
430
+ const { InstallWizard } = await import("../components/marketplace/install-wizard");
431
+ const plugin = findManifest("webhooks");
432
+
433
+ render(<InstallWizard plugin={plugin} onComplete={vi.fn()} onCancel={vi.fn()} />);
434
+
435
+ expect(screen.getByText("Install Webhooks")).toBeInTheDocument();
436
+ expect(screen.getByText("Cancel")).toBeInTheDocument();
437
+ expect(screen.getByText("Continue")).toBeInTheDocument();
438
+ });
439
+
440
+ it("shows bot selector as first phase", async () => {
441
+ const { InstallWizard } = await import("../components/marketplace/install-wizard");
442
+ const plugin = findManifest("webhooks");
443
+
444
+ render(<InstallWizard plugin={plugin} onComplete={vi.fn()} onCancel={vi.fn()} />);
445
+
446
+ // First phase is bot-select
447
+ expect(screen.getByText("Select which bot to install this plugin on")).toBeInTheDocument();
448
+ });
449
+
450
+ it("shows provider selector for plugins with hosted capabilities", async () => {
451
+ const { InstallWizard } = await import("../components/marketplace/install-wizard");
452
+ // meeting-transcriber has stt and llm capabilities
453
+ const plugin = findManifest("meeting-transcriber");
454
+ // meeting-transcriber requires discord — mock it as installed so requirements phase passes
455
+ vi.mocked(listInstalledPlugins).mockResolvedValueOnce([{ pluginId: "discord", enabled: true }]);
456
+
457
+ render(<InstallWizard plugin={plugin} onComplete={vi.fn()} onCancel={vi.fn()} />);
458
+
459
+ // First phase is bot-select — wait for bots to load and select one
460
+ const user = userEvent.setup();
461
+ expect(
462
+ await screen.findByText("Select which bot to install this plugin on"),
463
+ ).toBeInTheDocument();
464
+
465
+ // Wait for bots to load
466
+ const botButton = await screen.findByText("My Bot");
467
+ await user.click(botButton);
468
+
469
+ // Advance past bot-select
470
+ await user.click(screen.getByText("Continue"));
471
+
472
+ // Next phase is requirements
473
+ expect(screen.getByText("Check plugin requirements")).toBeInTheDocument();
474
+
475
+ // Advance past requirements
476
+ await user.click(screen.getByText("Continue"));
477
+
478
+ // Should now show provider selector
479
+ expect(
480
+ screen.getByText(
481
+ "Some capabilities can be provided by Platform Hosted services. Choose for each:",
482
+ ),
483
+ ).toBeInTheDocument();
484
+ expect(screen.getByText("LLM")).toBeInTheDocument();
485
+ expect(screen.getByText("STT")).toBeInTheDocument();
486
+ });
487
+
488
+ it("calls onCancel when cancel button is clicked", async () => {
489
+ const { InstallWizard } = await import("../components/marketplace/install-wizard");
490
+ const plugin = findManifest("webhooks");
491
+ const onCancel = vi.fn();
492
+
493
+ render(<InstallWizard plugin={plugin} onComplete={vi.fn()} onCancel={onCancel} />);
494
+
495
+ const user = userEvent.setup();
496
+ await user.click(screen.getByText("Cancel"));
497
+
498
+ expect(onCancel).toHaveBeenCalledOnce();
499
+ });
500
+ });
501
+
502
+ describe("PluginCard", () => {
503
+ it("renders plugin info with link to detail page", async () => {
504
+ const { PluginCard } = await import("../components/marketplace/plugin-card");
505
+ const plugin = TEST_PLUGINS[0] as unknown as PluginManifest; // discord
506
+
507
+ render(<PluginCard plugin={plugin} />);
508
+
509
+ expect(screen.getByText("Discord")).toBeInTheDocument();
510
+ expect(screen.getByText("v3.2.0")).toBeInTheDocument();
511
+ expect(screen.getByText("12.4k installs")).toBeInTheDocument();
512
+ expect(screen.getByText("Platform Team")).toBeInTheDocument();
513
+
514
+ // Should have a link to the detail page
515
+ const link = screen.getByRole("link");
516
+ expect(link).toHaveAttribute("href", "/marketplace/discord");
517
+ });
518
+
519
+ it("shows Platform Hosted badge for eligible plugins", async () => {
520
+ const { PluginCard } = await import("../components/marketplace/plugin-card");
521
+ // semantic-memory has embeddings capability
522
+ const plugin = findManifest("semantic-memory");
523
+
524
+ render(<PluginCard plugin={plugin} />);
525
+
526
+ // Cards show shortened "Platform Hosted" badge text
527
+ expect(screen.getByText("Platform Hosted")).toBeInTheDocument();
528
+ });
529
+
530
+ it("does not show Platform Hosted badge for plugins without hosted capabilities", async () => {
531
+ const { PluginCard } = await import("../components/marketplace/plugin-card");
532
+ // discord only has 'channel' capability, no hosted adapter for that
533
+ const plugin = findManifest("discord");
534
+
535
+ render(<PluginCard plugin={plugin} />);
536
+
537
+ expect(screen.queryByText("Platform Hosted")).not.toBeInTheDocument();
538
+ });
539
+
540
+ it("shows Installed badge when installed prop is true", async () => {
541
+ const { PluginCard } = await import("../components/marketplace/plugin-card");
542
+ const plugin = findManifest("discord");
543
+
544
+ render(<PluginCard plugin={plugin} installed={true} />);
545
+
546
+ expect(screen.getByText("Installed")).toBeInTheDocument();
547
+ });
548
+
549
+ it("does not show Installed badge when installed prop is false", async () => {
550
+ const { PluginCard } = await import("../components/marketplace/plugin-card");
551
+ const plugin = findManifest("discord");
552
+
553
+ render(<PluginCard plugin={plugin} installed={false} />);
554
+
555
+ expect(screen.queryByText("Installed")).not.toBeInTheDocument();
556
+ });
557
+ });
558
+
559
+ describe("CategoryFilter", () => {
560
+ it("renders All button and category buttons with counts", async () => {
561
+ const { CategoryFilter } = await import("../components/marketplace/category-filter");
562
+ const counts = { channel: 2, voice: 3, memory: 1 };
563
+
564
+ render(<CategoryFilter selected={null} onSelect={vi.fn()} counts={counts} />);
565
+
566
+ expect(screen.getByText("All")).toBeInTheDocument();
567
+ expect(screen.getByText("Channel")).toBeInTheDocument();
568
+ expect(screen.getByText("Voice")).toBeInTheDocument();
569
+ expect(screen.getByText("Memory")).toBeInTheDocument();
570
+ });
571
+
572
+ it("calls onSelect with category when clicked", async () => {
573
+ const { CategoryFilter } = await import("../components/marketplace/category-filter");
574
+ const onSelect = vi.fn();
575
+ const counts = { channel: 2, voice: 3 };
576
+
577
+ render(<CategoryFilter selected={null} onSelect={onSelect} counts={counts} />);
578
+
579
+ const user = userEvent.setup();
580
+ await user.click(closestButton(screen.getByText("Voice")));
581
+
582
+ expect(onSelect).toHaveBeenCalledWith("voice");
583
+ });
584
+
585
+ it("calls onSelect with null when All is clicked", async () => {
586
+ const { CategoryFilter } = await import("../components/marketplace/category-filter");
587
+ const onSelect = vi.fn();
588
+ const counts = { channel: 2 };
589
+
590
+ render(<CategoryFilter selected="channel" onSelect={onSelect} counts={counts} />);
591
+
592
+ const user = userEvent.setup();
593
+ await user.click(closestButton(screen.getByText("All")));
594
+
595
+ expect(onSelect).toHaveBeenCalledWith(null);
596
+ });
597
+
598
+ it("hides categories with zero plugins", async () => {
599
+ const { CategoryFilter } = await import("../components/marketplace/category-filter");
600
+ const counts = { channel: 2 };
601
+
602
+ render(<CategoryFilter selected={null} onSelect={vi.fn()} counts={counts} />);
603
+
604
+ // Only Channel and All should be visible
605
+ expect(screen.getByText("Channel")).toBeInTheDocument();
606
+ expect(screen.queryByText("Voice")).not.toBeInTheDocument();
607
+ expect(screen.queryByText("Memory")).not.toBeInTheDocument();
608
+ });
609
+ });
@@ -0,0 +1,70 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { mergeApiRates } from "../lib/pricing-data";
3
+
4
+ describe("mergeApiRates", () => {
5
+ it("groups tts and stt under Voice", () => {
6
+ const result = mergeApiRates({
7
+ tts: [{ name: "TTS Standard", unit: "1K chars", price: 0.2 }],
8
+ stt: [{ name: "Whisper", unit: "minute", price: 0.02 }],
9
+ });
10
+
11
+ const voice = result.find((c) => c.category === "Voice");
12
+ expect(voice?.models).toHaveLength(2);
13
+ expect(voice?.icon).toBe("mic");
14
+ });
15
+
16
+ it("orders categories: Text Gen, Voice, Image Gen, Messaging", () => {
17
+ const result = mergeApiRates({
18
+ sms: [{ name: "SMS", unit: "message", price: 0.01 }],
19
+ llm: [{ name: "GPT-4o", unit: "1M tokens", price: 2.5 }],
20
+ image_gen: [{ name: "SDXL", unit: "image", price: 0.03 }],
21
+ tts: [{ name: "TTS", unit: "1K chars", price: 0.2 }],
22
+ });
23
+
24
+ expect(result.map((c) => c.category)).toEqual([
25
+ "Text Generation",
26
+ "Voice",
27
+ "Image Generation",
28
+ "Messaging",
29
+ ]);
30
+ });
31
+
32
+ it("handles unknown capability keys with fallback", () => {
33
+ const result = mergeApiRates({
34
+ video_gen: [{ name: "Sora", unit: "second", price: 0.5 }],
35
+ });
36
+
37
+ expect(result).toHaveLength(1);
38
+ expect(result[0].category).toBe("Video Gen");
39
+ expect(result[0].icon).toBe("bot");
40
+ expect(result[0].models[0].name).toBe("Sora");
41
+ });
42
+
43
+ it("returns empty array for empty input", () => {
44
+ expect(mergeApiRates({})).toEqual([]);
45
+ });
46
+
47
+ it("unknown categories sort after known ones", () => {
48
+ const result = mergeApiRates({
49
+ llm: [{ name: "GPT-4o", unit: "1M tokens", price: 2.5 }],
50
+ video_gen: [{ name: "Sora", unit: "second", price: 0.5 }],
51
+ });
52
+
53
+ expect(result[0].category).toBe("Text Generation");
54
+ expect(result[1].category).toBe("Video Gen");
55
+ });
56
+
57
+ it("preserves all models within a grouped category", () => {
58
+ const result = mergeApiRates({
59
+ tts: [
60
+ { name: "TTS Standard", unit: "1K chars", price: 0.2 },
61
+ { name: "TTS Premium", unit: "1K chars", price: 0.5 },
62
+ ],
63
+ stt: [{ name: "Whisper", unit: "minute", price: 0.02 }],
64
+ });
65
+
66
+ const voice = result.find((c) => c.category === "Voice");
67
+ expect(voice?.models).toHaveLength(3);
68
+ expect(voice?.models.map((m) => m.name)).toEqual(["TTS Standard", "TTS Premium", "Whisper"]);
69
+ });
70
+ });