@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,136 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+ import { describe, expect, it, vi } from "vitest";
4
+
5
+ const mockPush = vi.fn();
6
+ const mockReplace = vi.fn();
7
+ const mockSearchParams = new URLSearchParams();
8
+
9
+ vi.mock("next/navigation", () => ({
10
+ useRouter: () => ({ push: mockPush, replace: mockReplace }),
11
+ useSearchParams: () => mockSearchParams,
12
+ usePathname: () => "/admin",
13
+ }));
14
+
15
+ vi.mock("better-auth/react", () => ({
16
+ createAuthClient: () => ({
17
+ useSession: () => ({ data: null, isPending: false, error: null }),
18
+ signIn: { email: vi.fn(), social: vi.fn() },
19
+ signUp: { email: vi.fn() },
20
+ signOut: vi.fn(),
21
+ }),
22
+ }));
23
+
24
+ vi.mock("@/lib/api", async (importOriginal) => {
25
+ const actual = await importOriginal<typeof import("@/lib/api")>();
26
+ return {
27
+ ...actual,
28
+ fetchAuditLog: vi.fn(),
29
+ };
30
+ });
31
+
32
+ import { AuditLogTable } from "@/components/admin/audit-log-table";
33
+ import { fetchAuditLog } from "@/lib/api";
34
+
35
+ const mockFetchAuditLog = vi.mocked(fetchAuditLog);
36
+
37
+ function makeEvents(count: number, startIndex: number) {
38
+ return Array.from({ length: count }, (_, i) => ({
39
+ id: `evt-${startIndex + i}`,
40
+ action: `bot.action${startIndex + i}`,
41
+ resourceType: "bot",
42
+ resourceId: `bot-${startIndex + i}`,
43
+ resourceName: `Bot ${startIndex + i}`,
44
+ details: `Detail ${startIndex + i}`,
45
+ createdAt: new Date(Date.now() - (startIndex + i) * 3600000).toISOString(),
46
+ }));
47
+ }
48
+
49
+ describe("AuditLogTable pagination", () => {
50
+ it("initial load requests offset 0", async () => {
51
+ mockFetchAuditLog.mockResolvedValueOnce({
52
+ events: makeEvents(50, 0),
53
+ total: 120,
54
+ hasMore: true,
55
+ });
56
+ render(<AuditLogTable />);
57
+
58
+ await screen.findByText("Bot 0");
59
+ expect(mockFetchAuditLog).toHaveBeenCalledWith(
60
+ expect.objectContaining({ offset: 0, limit: 50 }),
61
+ );
62
+ });
63
+
64
+ it("clicking Next requests offset 50 (page 2)", async () => {
65
+ mockFetchAuditLog.mockResolvedValueOnce({
66
+ events: makeEvents(50, 0),
67
+ total: 120,
68
+ hasMore: true,
69
+ });
70
+ const user = userEvent.setup();
71
+ render(<AuditLogTable />);
72
+ await screen.findByText("Bot 0");
73
+
74
+ mockFetchAuditLog.mockResolvedValueOnce({
75
+ events: makeEvents(50, 50),
76
+ total: 120,
77
+ hasMore: true,
78
+ });
79
+ await user.click(screen.getByRole("button", { name: "Next" }));
80
+ await screen.findByText("Bot 50");
81
+
82
+ expect(mockFetchAuditLog).toHaveBeenLastCalledWith(
83
+ expect.objectContaining({ offset: 50, limit: 50 }),
84
+ );
85
+ });
86
+
87
+ it("disables Next when hasMore is false", async () => {
88
+ mockFetchAuditLog.mockResolvedValueOnce({
89
+ events: makeEvents(50, 0),
90
+ total: 120,
91
+ hasMore: true,
92
+ });
93
+ const user = userEvent.setup();
94
+ render(<AuditLogTable />);
95
+ await screen.findByText("Bot 0");
96
+
97
+ mockFetchAuditLog.mockResolvedValueOnce({
98
+ events: makeEvents(50, 50),
99
+ total: 120,
100
+ hasMore: true,
101
+ });
102
+ await user.click(screen.getByRole("button", { name: "Next" }));
103
+ await screen.findByText("Bot 50");
104
+
105
+ mockFetchAuditLog.mockResolvedValueOnce({
106
+ events: makeEvents(20, 100),
107
+ total: 120,
108
+ hasMore: false,
109
+ });
110
+ await user.click(screen.getByRole("button", { name: "Next" }));
111
+ await screen.findByText("Bot 100");
112
+
113
+ expect(screen.getByRole("button", { name: "Next" })).toBeDisabled();
114
+ });
115
+
116
+ it("shows correct range on page 2", async () => {
117
+ mockFetchAuditLog.mockResolvedValueOnce({
118
+ events: makeEvents(50, 0),
119
+ total: 120,
120
+ hasMore: true,
121
+ });
122
+ const user = userEvent.setup();
123
+ render(<AuditLogTable />);
124
+ await screen.findByText("Bot 0");
125
+
126
+ mockFetchAuditLog.mockResolvedValueOnce({
127
+ events: makeEvents(50, 50),
128
+ total: 120,
129
+ hasMore: true,
130
+ });
131
+ await user.click(screen.getByRole("button", { name: "Next" }));
132
+ await screen.findByText("Bot 50");
133
+
134
+ expect(screen.getByText(/Showing 51-100 of 120/)).toBeInTheDocument();
135
+ });
136
+ });
@@ -0,0 +1,87 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const mockTwoFactorPlugin = { id: "two-factor" };
4
+
5
+ vi.mock("better-auth/client/plugins", () => ({
6
+ twoFactorClient: vi.fn(() => mockTwoFactorPlugin),
7
+ }));
8
+
9
+ vi.mock("@/lib/api-config", () => ({
10
+ PLATFORM_BASE_URL: "https://test-api.local",
11
+ }));
12
+
13
+ const mockAuthClient = {
14
+ useSession: vi.fn(),
15
+ signIn: vi.fn(),
16
+ signUp: vi.fn(),
17
+ signOut: vi.fn(),
18
+ linkSocial: vi.fn(),
19
+ unlinkAccount: vi.fn(),
20
+ listAccounts: vi.fn(),
21
+ $fetch: vi.fn(),
22
+ };
23
+
24
+ vi.mock("better-auth/react", () => ({
25
+ createAuthClient: vi.fn(() => mockAuthClient),
26
+ }));
27
+
28
+ describe("auth-client", () => {
29
+ beforeEach(() => {
30
+ vi.clearAllMocks();
31
+ });
32
+
33
+ it("calls createAuthClient with PLATFORM_BASE_URL", async () => {
34
+ const { createAuthClient } = await import("better-auth/react");
35
+ await import("@/lib/auth-client");
36
+
37
+ expect(createAuthClient).toHaveBeenCalledWith({
38
+ baseURL: "https://test-api.local",
39
+ plugins: [mockTwoFactorPlugin],
40
+ });
41
+ });
42
+
43
+ it("authClient is the instance returned by createAuthClient", async () => {
44
+ const mod = await import("@/lib/auth-client");
45
+ expect(mod.authClient).toBe(mockAuthClient);
46
+ });
47
+
48
+ it("exports authClient object", async () => {
49
+ const mod = await import("@/lib/auth-client");
50
+ expect(mod.authClient).toBe(mockAuthClient);
51
+ });
52
+
53
+ it("exports useSession from authClient", async () => {
54
+ const mod = await import("@/lib/auth-client");
55
+ expect(mod.useSession).toBe(mockAuthClient.useSession);
56
+ });
57
+
58
+ it("exports signIn from authClient", async () => {
59
+ const mod = await import("@/lib/auth-client");
60
+ expect(mod.signIn).toBe(mockAuthClient.signIn);
61
+ });
62
+
63
+ it("exports signUp from authClient", async () => {
64
+ const mod = await import("@/lib/auth-client");
65
+ expect(mod.signUp).toBe(mockAuthClient.signUp);
66
+ });
67
+
68
+ it("exports signOut from authClient", async () => {
69
+ const mod = await import("@/lib/auth-client");
70
+ expect(mod.signOut).toBe(mockAuthClient.signOut);
71
+ });
72
+
73
+ it("exports linkSocial from authClient", async () => {
74
+ const mod = await import("@/lib/auth-client");
75
+ expect(mod.linkSocial).toBe(mockAuthClient.linkSocial);
76
+ });
77
+
78
+ it("exports unlinkAccount from authClient", async () => {
79
+ const mod = await import("@/lib/auth-client");
80
+ expect(mod.unlinkAccount).toBe(mockAuthClient.unlinkAccount);
81
+ });
82
+
83
+ it("exports listAccounts from authClient", async () => {
84
+ const mod = await import("@/lib/auth-client");
85
+ expect(mod.listAccounts).toBe(mockAuthClient.listAccounts);
86
+ });
87
+ });
@@ -0,0 +1,435 @@
1
+ import { render, screen, waitFor } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+ import { useRouter, useSearchParams } from "next/navigation";
4
+ import type { Mock } from "vitest";
5
+ import { beforeEach, describe, expect, it, vi } from "vitest";
6
+
7
+ let mockFetch: Mock<(...args: unknown[]) => Promise<unknown>>;
8
+
9
+ vi.mock("next/navigation", () => ({
10
+ useRouter: vi.fn(() => ({ push: vi.fn() })),
11
+ useSearchParams: vi.fn(() => new URLSearchParams()),
12
+ }));
13
+
14
+ vi.mock("better-auth/react", () => ({
15
+ createAuthClient: () => ({
16
+ $fetch: (...args: unknown[]) => mockFetch(...args),
17
+ useSession: () => ({ data: null, isPending: false, error: null }),
18
+ signIn: { email: vi.fn(), social: vi.fn() },
19
+ signUp: { email: vi.fn() },
20
+ signOut: vi.fn(),
21
+ forgetPassword: vi.fn(),
22
+ resetPassword: vi.fn(),
23
+ sendVerificationEmail: vi.fn().mockResolvedValue({ data: {}, error: null }),
24
+ }),
25
+ }));
26
+
27
+ vi.mock("framer-motion", () => ({
28
+ motion: {
29
+ div: ({ children, ...props }: React.PropsWithChildren<Record<string, unknown>>) => (
30
+ <div {...props}>{children}</div>
31
+ ),
32
+ p: ({ children, ...props }: React.PropsWithChildren<Record<string, unknown>>) => (
33
+ <p {...props}>{children}</p>
34
+ ),
35
+ },
36
+ }));
37
+
38
+ describe("Forgot password page", () => {
39
+ beforeEach(() => {
40
+ mockFetch = vi.fn();
41
+ });
42
+
43
+ it("renders email input and submit button", async () => {
44
+ const { default: ForgotPasswordPage } = await import("../app/(auth)/forgot-password/page");
45
+ render(<ForgotPasswordPage />);
46
+
47
+ expect(screen.getByLabelText("Email")).toBeInTheDocument();
48
+ expect(screen.getByRole("button", { name: "Send reset link" })).toBeInTheDocument();
49
+ });
50
+
51
+ it("renders back to sign in link", async () => {
52
+ const { default: ForgotPasswordPage } = await import("../app/(auth)/forgot-password/page");
53
+ render(<ForgotPasswordPage />);
54
+
55
+ const link = screen.getByText("Back to sign in");
56
+ expect(link).toBeInTheDocument();
57
+ expect(link.closest("a")).toHaveAttribute("href", "/login");
58
+ });
59
+
60
+ it("shows success message after successful submission", async () => {
61
+ mockFetch.mockResolvedValueOnce({ error: undefined });
62
+ const user = userEvent.setup();
63
+ const { default: ForgotPasswordPage } = await import("../app/(auth)/forgot-password/page");
64
+ render(<ForgotPasswordPage />);
65
+
66
+ await user.type(screen.getByLabelText("Email"), "test@example.com");
67
+ await user.click(screen.getByRole("button", { name: "Send reset link" }));
68
+
69
+ await waitFor(() => {
70
+ expect(screen.getByText("Transmission sent")).toBeInTheDocument();
71
+ });
72
+ expect(screen.getByText(/test@example\.com/)).toBeInTheDocument();
73
+ expect(mockFetch).toHaveBeenCalledWith("/request-password-reset", {
74
+ method: "POST",
75
+ body: { email: "test@example.com", redirectTo: "/reset-password" },
76
+ });
77
+ });
78
+
79
+ it("shows error message on API error", async () => {
80
+ mockFetch.mockResolvedValueOnce({ error: { message: "User not found" } });
81
+ const user = userEvent.setup();
82
+ const { default: ForgotPasswordPage } = await import("../app/(auth)/forgot-password/page");
83
+ render(<ForgotPasswordPage />);
84
+
85
+ await user.type(screen.getByLabelText("Email"), "bad@example.com");
86
+ await user.click(screen.getByRole("button", { name: "Send reset link" }));
87
+
88
+ await waitFor(() => {
89
+ expect(screen.getByText("User not found")).toBeInTheDocument();
90
+ });
91
+ });
92
+
93
+ it("shows fallback error when API error has no message", async () => {
94
+ mockFetch.mockResolvedValueOnce({ error: {} });
95
+ const user = userEvent.setup();
96
+ const { default: ForgotPasswordPage } = await import("../app/(auth)/forgot-password/page");
97
+ render(<ForgotPasswordPage />);
98
+
99
+ await user.type(screen.getByLabelText("Email"), "bad@example.com");
100
+ await user.click(screen.getByRole("button", { name: "Send reset link" }));
101
+
102
+ await waitFor(() => {
103
+ expect(screen.getByText("Failed to send reset email")).toBeInTheDocument();
104
+ });
105
+ });
106
+
107
+ it("shows network error on fetch rejection", async () => {
108
+ mockFetch.mockRejectedValueOnce(new Error("Network failure"));
109
+ const user = userEvent.setup();
110
+ const { default: ForgotPasswordPage } = await import("../app/(auth)/forgot-password/page");
111
+ render(<ForgotPasswordPage />);
112
+
113
+ await user.type(screen.getByLabelText("Email"), "test@example.com");
114
+ await user.click(screen.getByRole("button", { name: "Send reset link" }));
115
+
116
+ await waitFor(() => {
117
+ expect(screen.getByText("A network error occurred. Please try again.")).toBeInTheDocument();
118
+ });
119
+ });
120
+
121
+ it("disables button during submission", async () => {
122
+ let resolveFetch!: (value: unknown) => void;
123
+ mockFetch.mockReturnValueOnce(
124
+ new Promise((r) => {
125
+ resolveFetch = r;
126
+ }),
127
+ );
128
+ const user = userEvent.setup();
129
+ const { default: ForgotPasswordPage } = await import("../app/(auth)/forgot-password/page");
130
+ render(<ForgotPasswordPage />);
131
+
132
+ await user.type(screen.getByLabelText("Email"), "test@example.com");
133
+ await user.click(screen.getByRole("button", { name: "Send reset link" }));
134
+
135
+ await waitFor(() => {
136
+ expect(screen.getByText("TRANSMITTING")).toBeInTheDocument();
137
+ });
138
+ expect(screen.getByRole("button")).toBeDisabled();
139
+
140
+ resolveFetch({ error: undefined });
141
+ await waitFor(() => {
142
+ expect(screen.getByText("Transmission sent")).toBeInTheDocument();
143
+ });
144
+ });
145
+ });
146
+
147
+ describe("Reset password page", () => {
148
+ const mockPush = vi.fn();
149
+
150
+ beforeEach(() => {
151
+ mockFetch = vi.fn();
152
+ mockPush.mockClear();
153
+ vi.mocked(useRouter).mockReturnValue({ push: mockPush } as unknown as ReturnType<
154
+ typeof useRouter
155
+ >);
156
+ vi.mocked(useSearchParams).mockReturnValue(
157
+ new URLSearchParams() as ReturnType<typeof useSearchParams>,
158
+ );
159
+ });
160
+
161
+ it("shows access denied when no token is present", async () => {
162
+ const { default: ResetPasswordPage } = await import("../app/(auth)/reset-password/page");
163
+ render(<ResetPasswordPage />);
164
+
165
+ expect(screen.getByText("Access denied")).toBeInTheDocument();
166
+ expect(screen.getByText(/invalid or has expired/)).toBeInTheDocument();
167
+ const link = screen.getByText("Request a new reset link");
168
+ expect(link.closest("a")).toHaveAttribute("href", "/forgot-password");
169
+ });
170
+
171
+ it("renders password and confirm inputs when token is present", async () => {
172
+ vi.mocked(useSearchParams).mockReturnValue(
173
+ new URLSearchParams("token=valid-token") as ReturnType<typeof useSearchParams>,
174
+ );
175
+ const { default: ResetPasswordPage } = await import("../app/(auth)/reset-password/page");
176
+ render(<ResetPasswordPage />);
177
+
178
+ expect(screen.getByLabelText("New password")).toBeInTheDocument();
179
+ expect(screen.getByLabelText("Confirm password")).toBeInTheDocument();
180
+ expect(screen.getByRole("button", { name: "Reset password" })).toBeInTheDocument();
181
+ });
182
+
183
+ it("shows error when passwords do not match", async () => {
184
+ vi.mocked(useSearchParams).mockReturnValue(
185
+ new URLSearchParams("token=valid-token") as ReturnType<typeof useSearchParams>,
186
+ );
187
+ const user = userEvent.setup();
188
+ const { default: ResetPasswordPage } = await import("../app/(auth)/reset-password/page");
189
+ render(<ResetPasswordPage />);
190
+
191
+ await user.type(screen.getByLabelText("New password"), "Password123!");
192
+ await user.type(screen.getByLabelText("Confirm password"), "DifferentPassword!");
193
+ await user.click(screen.getByRole("button", { name: "Reset password" }));
194
+
195
+ await waitFor(() => {
196
+ expect(screen.getByText("Passwords do not match")).toBeInTheDocument();
197
+ });
198
+ expect(mockFetch).not.toHaveBeenCalled();
199
+ });
200
+
201
+ it("calls API and redirects to /login on success", async () => {
202
+ vi.mocked(useSearchParams).mockReturnValue(
203
+ new URLSearchParams("token=valid-token") as ReturnType<typeof useSearchParams>,
204
+ );
205
+ mockFetch.mockResolvedValueOnce({ error: undefined });
206
+ const user = userEvent.setup();
207
+ const { default: ResetPasswordPage } = await import("../app/(auth)/reset-password/page");
208
+ render(<ResetPasswordPage />);
209
+
210
+ await user.type(screen.getByLabelText("New password"), "NewSecurePass1!");
211
+ await user.type(screen.getByLabelText("Confirm password"), "NewSecurePass1!");
212
+ await user.click(screen.getByRole("button", { name: "Reset password" }));
213
+
214
+ await waitFor(() => {
215
+ expect(mockPush).toHaveBeenCalledWith("/login");
216
+ });
217
+ expect(mockFetch).toHaveBeenCalledWith("/reset-password", {
218
+ method: "POST",
219
+ body: { newPassword: "NewSecurePass1!", token: "valid-token" },
220
+ });
221
+ });
222
+
223
+ it("shows error on API failure (e.g. invalid/expired token)", async () => {
224
+ vi.mocked(useSearchParams).mockReturnValue(
225
+ new URLSearchParams("token=expired-token") as ReturnType<typeof useSearchParams>,
226
+ );
227
+ mockFetch.mockResolvedValueOnce({ error: { message: "Token expired" } });
228
+ const user = userEvent.setup();
229
+ const { default: ResetPasswordPage } = await import("../app/(auth)/reset-password/page");
230
+ render(<ResetPasswordPage />);
231
+
232
+ await user.type(screen.getByLabelText("New password"), "NewSecurePass1!");
233
+ await user.type(screen.getByLabelText("Confirm password"), "NewSecurePass1!");
234
+ await user.click(screen.getByRole("button", { name: "Reset password" }));
235
+
236
+ await waitFor(() => {
237
+ expect(screen.getByText("Token expired")).toBeInTheDocument();
238
+ });
239
+ expect(mockPush).not.toHaveBeenCalled();
240
+ });
241
+
242
+ it("shows fallback error when API error has no message", async () => {
243
+ vi.mocked(useSearchParams).mockReturnValue(
244
+ new URLSearchParams("token=valid-token") as ReturnType<typeof useSearchParams>,
245
+ );
246
+ mockFetch.mockResolvedValueOnce({ error: {} });
247
+ const user = userEvent.setup();
248
+ const { default: ResetPasswordPage } = await import("../app/(auth)/reset-password/page");
249
+ render(<ResetPasswordPage />);
250
+
251
+ await user.type(screen.getByLabelText("New password"), "NewSecurePass1!");
252
+ await user.type(screen.getByLabelText("Confirm password"), "NewSecurePass1!");
253
+ await user.click(screen.getByRole("button", { name: "Reset password" }));
254
+
255
+ await waitFor(() => {
256
+ expect(screen.getByText("Failed to reset password")).toBeInTheDocument();
257
+ });
258
+ });
259
+
260
+ it("shows network error on fetch rejection", async () => {
261
+ vi.mocked(useSearchParams).mockReturnValue(
262
+ new URLSearchParams("token=valid-token") as ReturnType<typeof useSearchParams>,
263
+ );
264
+ mockFetch.mockRejectedValueOnce(new Error("Network failure"));
265
+ const user = userEvent.setup();
266
+ const { default: ResetPasswordPage } = await import("../app/(auth)/reset-password/page");
267
+ render(<ResetPasswordPage />);
268
+
269
+ await user.type(screen.getByLabelText("New password"), "NewSecurePass1!");
270
+ await user.type(screen.getByLabelText("Confirm password"), "NewSecurePass1!");
271
+ await user.click(screen.getByRole("button", { name: "Reset password" }));
272
+
273
+ await waitFor(() => {
274
+ expect(screen.getByText("A network error occurred. Please try again.")).toBeInTheDocument();
275
+ });
276
+ });
277
+
278
+ it("disables button during submission", async () => {
279
+ vi.mocked(useSearchParams).mockReturnValue(
280
+ new URLSearchParams("token=valid-token") as ReturnType<typeof useSearchParams>,
281
+ );
282
+ let resolveFetch!: (value: unknown) => void;
283
+ mockFetch.mockReturnValueOnce(
284
+ new Promise((r) => {
285
+ resolveFetch = r;
286
+ }),
287
+ );
288
+ const user = userEvent.setup();
289
+ const { default: ResetPasswordPage } = await import("../app/(auth)/reset-password/page");
290
+ render(<ResetPasswordPage />);
291
+
292
+ await user.type(screen.getByLabelText("New password"), "NewSecurePass1!");
293
+ await user.type(screen.getByLabelText("Confirm password"), "NewSecurePass1!");
294
+ await user.click(screen.getByRole("button", { name: "Reset password" }));
295
+
296
+ await waitFor(() => {
297
+ expect(screen.getByText("UPDATING")).toBeInTheDocument();
298
+ });
299
+ expect(screen.getByRole("button")).toBeDisabled();
300
+
301
+ resolveFetch({ error: undefined });
302
+ await waitFor(() => {
303
+ expect(mockPush).toHaveBeenCalledWith("/login");
304
+ });
305
+ });
306
+
307
+ it("renders back to sign in link when token is present", async () => {
308
+ vi.mocked(useSearchParams).mockReturnValue(
309
+ new URLSearchParams("token=valid-token") as ReturnType<typeof useSearchParams>,
310
+ );
311
+ const { default: ResetPasswordPage } = await import("../app/(auth)/reset-password/page");
312
+ render(<ResetPasswordPage />);
313
+
314
+ const link = screen.getByText("Back to sign in");
315
+ expect(link.closest("a")).toHaveAttribute("href", "/login");
316
+ });
317
+
318
+ it("shows error when password is shorter than 12 characters", async () => {
319
+ vi.mocked(useSearchParams).mockReturnValue(
320
+ new URLSearchParams("token=valid-token") as ReturnType<typeof useSearchParams>,
321
+ );
322
+ const user = userEvent.setup();
323
+ const { default: ResetPasswordPage } = await import("../app/(auth)/reset-password/page");
324
+ render(<ResetPasswordPage />);
325
+
326
+ await user.type(screen.getByLabelText("New password"), "Short1!");
327
+ await user.type(screen.getByLabelText("Confirm password"), "Short1!");
328
+ await user.click(screen.getByRole("button", { name: "Reset password" }));
329
+
330
+ await waitFor(() => {
331
+ expect(screen.getByText("Password must be at least 12 characters")).toBeInTheDocument();
332
+ });
333
+ expect(mockFetch).not.toHaveBeenCalled();
334
+ });
335
+
336
+ it("shows error when password has no uppercase letter", async () => {
337
+ vi.mocked(useSearchParams).mockReturnValue(
338
+ new URLSearchParams("token=valid-token") as ReturnType<typeof useSearchParams>,
339
+ );
340
+ const user = userEvent.setup();
341
+ const { default: ResetPasswordPage } = await import("../app/(auth)/reset-password/page");
342
+ render(<ResetPasswordPage />);
343
+
344
+ await user.type(screen.getByLabelText("New password"), "alllowercase123!");
345
+ await user.type(screen.getByLabelText("Confirm password"), "alllowercase123!");
346
+ await user.click(screen.getByRole("button", { name: "Reset password" }));
347
+
348
+ await waitFor(() => {
349
+ expect(
350
+ screen.getByText("Password must contain at least one uppercase letter"),
351
+ ).toBeInTheDocument();
352
+ });
353
+ expect(mockFetch).not.toHaveBeenCalled();
354
+ });
355
+
356
+ it("shows error when password has no lowercase letter", async () => {
357
+ vi.mocked(useSearchParams).mockReturnValue(
358
+ new URLSearchParams("token=valid-token") as ReturnType<typeof useSearchParams>,
359
+ );
360
+ const user = userEvent.setup();
361
+ const { default: ResetPasswordPage } = await import("../app/(auth)/reset-password/page");
362
+ render(<ResetPasswordPage />);
363
+
364
+ await user.type(screen.getByLabelText("New password"), "ALLUPPERCASE123!");
365
+ await user.type(screen.getByLabelText("Confirm password"), "ALLUPPERCASE123!");
366
+ await user.click(screen.getByRole("button", { name: "Reset password" }));
367
+
368
+ await waitFor(() => {
369
+ expect(
370
+ screen.getByText("Password must contain at least one lowercase letter"),
371
+ ).toBeInTheDocument();
372
+ });
373
+ expect(mockFetch).not.toHaveBeenCalled();
374
+ });
375
+
376
+ it("shows error when password has no digit", async () => {
377
+ vi.mocked(useSearchParams).mockReturnValue(
378
+ new URLSearchParams("token=valid-token") as ReturnType<typeof useSearchParams>,
379
+ );
380
+ const user = userEvent.setup();
381
+ const { default: ResetPasswordPage } = await import("../app/(auth)/reset-password/page");
382
+ render(<ResetPasswordPage />);
383
+
384
+ await user.type(screen.getByLabelText("New password"), "NoDigitsHere!!");
385
+ await user.type(screen.getByLabelText("Confirm password"), "NoDigitsHere!!");
386
+ await user.click(screen.getByRole("button", { name: "Reset password" }));
387
+
388
+ await waitFor(() => {
389
+ expect(screen.getByText("Password must contain at least one digit")).toBeInTheDocument();
390
+ });
391
+ expect(mockFetch).not.toHaveBeenCalled();
392
+ });
393
+
394
+ it("shows error when password has no special character", async () => {
395
+ vi.mocked(useSearchParams).mockReturnValue(
396
+ new URLSearchParams("token=valid-token") as ReturnType<typeof useSearchParams>,
397
+ );
398
+ const user = userEvent.setup();
399
+ const { default: ResetPasswordPage } = await import("../app/(auth)/reset-password/page");
400
+ render(<ResetPasswordPage />);
401
+
402
+ await user.type(screen.getByLabelText("New password"), "NoSpecialChar12");
403
+ await user.type(screen.getByLabelText("Confirm password"), "NoSpecialChar12");
404
+ await user.click(screen.getByRole("button", { name: "Reset password" }));
405
+
406
+ await waitFor(() => {
407
+ expect(
408
+ screen.getByText("Password must contain at least one special character"),
409
+ ).toBeInTheDocument();
410
+ });
411
+ expect(mockFetch).not.toHaveBeenCalled();
412
+ });
413
+
414
+ it("allows submission when password meets all complexity requirements", async () => {
415
+ vi.mocked(useSearchParams).mockReturnValue(
416
+ new URLSearchParams("token=valid-token") as ReturnType<typeof useSearchParams>,
417
+ );
418
+ mockFetch.mockResolvedValueOnce({ error: undefined });
419
+ const user = userEvent.setup();
420
+ const { default: ResetPasswordPage } = await import("../app/(auth)/reset-password/page");
421
+ render(<ResetPasswordPage />);
422
+
423
+ await user.type(screen.getByLabelText("New password"), "ValidPass12!@");
424
+ await user.type(screen.getByLabelText("Confirm password"), "ValidPass12!@");
425
+ await user.click(screen.getByRole("button", { name: "Reset password" }));
426
+
427
+ await waitFor(() => {
428
+ expect(mockPush).toHaveBeenCalledWith("/login");
429
+ });
430
+ expect(mockFetch).toHaveBeenCalledWith("/reset-password", {
431
+ method: "POST",
432
+ body: { newPassword: "ValidPass12!@", token: "valid-token" },
433
+ });
434
+ });
435
+ });