@taskforcehq/taskforce 0.3.304 → 0.3.306

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 (383) hide show
  1. package/README.md +140 -247
  2. package/dist/Taskforce.module.css +295 -89
  3. package/dist/TaskforceCore.js +378 -111
  4. package/dist/TaskforceCore.test.js +1221 -107
  5. package/dist/assets/fonts/CourierPrime-Bold.woff2 +0 -0
  6. package/dist/assets/fonts/CourierPrime-BoldItalic.woff2 +0 -0
  7. package/dist/assets/fonts/CourierPrime-Italic.woff2 +0 -0
  8. package/dist/assets/fonts/CourierPrime-Regular.woff2 +0 -0
  9. package/dist/assets/fonts/Noir_medium.woff2 +0 -0
  10. package/dist/assets/fonts/Noir_regular.woff2 +0 -0
  11. package/dist/assets/fonts/RussoOne-Regular.woff2 +0 -0
  12. package/dist/assets/fonts/SpecialElite-Regular.woff2 +0 -0
  13. package/dist/compat/workspaceSyncCompat.js +52 -2
  14. package/dist/compat/workspaceSyncCompat.test.js +38 -1
  15. package/dist/components/context/ContextAttachmentManager.d.ts +14 -0
  16. package/dist/components/context/ContextAttachmentManager.js +549 -0
  17. package/dist/components/features/AgentsModule.d.ts +10 -1
  18. package/dist/components/features/AgentsModule.js +260 -15
  19. package/dist/components/features/AgentsModule.test.js +255 -5
  20. package/dist/components/features/DocumentIndex.d.ts +1 -1
  21. package/dist/components/features/DocumentIndex.js +1 -1
  22. package/dist/components/features/DocumentViewer.d.ts +6 -2
  23. package/dist/components/features/DocumentViewer.js +90 -10
  24. package/dist/components/features/DocumentWorkspace.d.ts +5 -22
  25. package/dist/components/features/DocumentWorkspace.js +12 -155
  26. package/dist/components/features/DocumentWorkspace.test.js +226 -6
  27. package/dist/components/features/TaskSettings.js +29 -3
  28. package/dist/components/features/TaskSettings.test.js +42 -1
  29. package/dist/components/features/documentWorkspaceModel.d.ts +24 -0
  30. package/dist/components/features/documentWorkspaceModel.js +57 -0
  31. package/dist/components/features/useDocumentWorkspaceController.d.ts +34 -0
  32. package/dist/components/features/useDocumentWorkspaceController.js +113 -0
  33. package/dist/components/task/TaskCard.js +19 -28
  34. package/dist/components/task/TaskCard.test.js +38 -3
  35. package/dist/components/task/TaskContextUpload.js +4 -526
  36. package/dist/components/task/TaskForm.d.ts +8 -8
  37. package/dist/components/task/TaskForm.js +362 -230
  38. package/dist/components/task/TaskForm.test.js +558 -381
  39. package/dist/components/task/TaskKanban.d.ts +2 -1
  40. package/dist/components/task/TaskKanban.js +96 -14
  41. package/dist/components/task/TaskKanban.test.js +36 -0
  42. package/dist/components/ui/EditableAvatarButton.d.ts +18 -0
  43. package/dist/components/ui/EditableAvatarButton.js +18 -0
  44. package/dist/components/views/PlanComparisonPage.d.ts +9 -1
  45. package/dist/components/views/PlanComparisonPage.js +70 -17
  46. package/dist/components/views/PlanComparisonPage.test.js +289 -41
  47. package/dist/components/views/PlansPage.js +239 -106
  48. package/dist/components/views/PlansPage.test.js +593 -42
  49. package/dist/components/views/StandaloneLayout.js +310 -554
  50. package/dist/components/views/panels/FilterToolbar.test.js +6 -4
  51. package/dist/components/views/panels/PlanningDrawer.d.ts +8 -1
  52. package/dist/components/views/panels/PlanningDrawer.js +18 -11
  53. package/dist/components/views/panels/PlanningDrawer.test.d.ts +1 -0
  54. package/dist/components/views/panels/PlanningDrawer.test.js +141 -0
  55. package/dist/components/views/planningScope.d.ts +20 -0
  56. package/dist/components/views/planningScope.js +25 -0
  57. package/dist/components/views/planningScope.test.d.ts +1 -0
  58. package/dist/components/views/planningScope.test.js +60 -0
  59. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +27 -14
  60. package/dist/components/views/standalone/modals/AccountHubModal.js +87 -15
  61. package/dist/components/views/standalone/modals/AccountHubModal.test.js +42 -2
  62. package/dist/components/views/standalone/modals/AvatarImageManagerModal.d.ts +20 -0
  63. package/dist/components/views/standalone/modals/AvatarImageManagerModal.js +127 -0
  64. package/dist/components/views/standalone/modals/EditProfileModal.d.ts +2 -9
  65. package/dist/components/views/standalone/modals/EditProfileModal.js +3 -2
  66. package/dist/components/views/standalone/modals/SyncEnableWarningModal.d.ts +9 -0
  67. package/dist/components/views/standalone/modals/SyncEnableWarningModal.js +6 -0
  68. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.d.ts +1 -0
  69. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.js +24 -0
  70. package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +2 -1
  71. package/dist/components/views/standalone/modals/SyncStatusModal.js +2 -2
  72. package/dist/config/envSchema.js +1 -1
  73. package/dist/core/AiProfileService.d.ts +45 -3
  74. package/dist/core/AiProfileService.js +774 -63
  75. package/dist/core/AiProfileService.test.js +22 -2
  76. package/dist/core/AiProfiles.test.js +761 -2
  77. package/dist/core/AttachmentLinkService.js +57 -15
  78. package/dist/core/AuthIdentityService.d.ts +107 -0
  79. package/dist/core/AuthIdentityService.js +284 -0
  80. package/dist/core/AuthTokenService.d.ts +4 -0
  81. package/dist/core/AuthTokenService.js +34 -6
  82. package/dist/core/AuthTokenService.test.d.ts +1 -0
  83. package/dist/core/AuthTokenService.test.js +78 -0
  84. package/dist/core/EntitlementsPolicy.test.js +77 -15
  85. package/dist/core/GlobalSettingsService.js +115 -6
  86. package/dist/core/PlanEntitlementService.d.ts +30 -3
  87. package/dist/core/PlanEntitlementService.js +454 -93
  88. package/dist/core/PlanFeatureCatalog.test.d.ts +1 -0
  89. package/dist/core/PlanFeatureCatalog.test.js +84 -0
  90. package/dist/core/PlanVersionPolicy.test.js +54 -28
  91. package/dist/core/PlanningEntities.test.js +52 -0
  92. package/dist/core/SignupMonetizationSettings.test.js +241 -44
  93. package/dist/core/SyncReconciliationService.js +2 -65
  94. package/dist/core/SystemAdmin.test.js +244 -88
  95. package/dist/core/TaskAttachmentsCanonical.test.js +51 -1
  96. package/dist/core/TaskTaxonomyValidation.test.js +53 -0
  97. package/dist/core/Taskforce.d.ts +228 -14
  98. package/dist/core/Taskforce.js +814 -201
  99. package/dist/core/Taskforce.listTasksSlim.test.d.ts +1 -0
  100. package/dist/core/Taskforce.listTasksSlim.test.js +91 -0
  101. package/dist/core/UserProfileAvatarDrafts.test.js +30 -3
  102. package/dist/core/WorkspaceLifecycleService.js +0 -5
  103. package/dist/core/WorkspacePermissions.test.js +25 -1
  104. package/dist/core/WorkspacePlanMode.test.js +128 -27
  105. package/dist/core/shared.d.ts +3 -1
  106. package/dist/core/shared.js +16 -1
  107. package/dist/core/types.d.ts +68 -2
  108. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +26 -1
  109. package/dist/hooks/sync/auth.js +3 -1
  110. package/dist/hooks/sync/bootstrap.js +2 -0
  111. package/dist/hooks/sync/controlPlane.d.ts +37 -0
  112. package/dist/hooks/sync/controlPlane.js +78 -0
  113. package/dist/hooks/sync/controlPlane.test.d.ts +1 -0
  114. package/dist/hooks/sync/controlPlane.test.js +121 -0
  115. package/dist/hooks/sync/lifecyclePolicy.d.ts +45 -0
  116. package/dist/hooks/sync/lifecyclePolicy.js +93 -0
  117. package/dist/hooks/sync/lifecyclePolicy.test.d.ts +1 -0
  118. package/dist/hooks/sync/lifecyclePolicy.test.js +124 -0
  119. package/dist/hooks/sync/orchestratorShared.d.ts +10 -1
  120. package/dist/hooks/sync/orchestratorShared.js +68 -47
  121. package/dist/hooks/sync/orchestratorShared.test.js +50 -1
  122. package/dist/hooks/sync/pullLifecycle.d.ts +31 -0
  123. package/dist/hooks/sync/pullLifecycle.js +24 -0
  124. package/dist/hooks/sync/pullLifecycle.test.d.ts +1 -0
  125. package/dist/hooks/sync/pullLifecycle.test.js +80 -0
  126. package/dist/hooks/sync/recovery.d.ts +16 -2
  127. package/dist/hooks/sync/recovery.js +171 -53
  128. package/dist/hooks/sync/recovery.test.d.ts +1 -0
  129. package/dist/hooks/sync/recovery.test.js +292 -0
  130. package/dist/hooks/sync/transfers.d.ts +16 -3
  131. package/dist/hooks/sync/transfers.js +186 -65
  132. package/dist/hooks/sync/transfers.test.d.ts +1 -0
  133. package/dist/hooks/sync/transfers.test.js +396 -0
  134. package/dist/hooks/sync/useRecentSyncEvents.d.ts +21 -0
  135. package/dist/hooks/sync/useRecentSyncEvents.js +92 -0
  136. package/dist/hooks/sync/useRecentSyncEvents.test.d.ts +1 -0
  137. package/dist/hooks/sync/useRecentSyncEvents.test.js +124 -0
  138. package/dist/hooks/sync/useSyncStatusActions.d.ts +30 -0
  139. package/dist/hooks/sync/useSyncStatusActions.js +88 -0
  140. package/dist/hooks/sync/useSyncStatusActions.test.d.ts +1 -0
  141. package/dist/hooks/sync/useSyncStatusActions.test.js +133 -0
  142. package/dist/hooks/sync/useSyncStatusControls.d.ts +25 -0
  143. package/dist/hooks/sync/useSyncStatusControls.js +60 -0
  144. package/dist/hooks/sync/useSyncStatusControls.test.d.ts +1 -0
  145. package/dist/hooks/sync/useSyncStatusControls.test.js +88 -0
  146. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +12 -0
  147. package/dist/hooks/useSyncOrchestrator.d.ts +16 -6
  148. package/dist/hooks/useSyncOrchestrator.js +424 -175
  149. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +487 -10
  150. package/dist/hooks/useTaskData.js +8 -3
  151. package/dist/hooks/useTaskData.test.js +45 -0
  152. package/dist/hooks/useTaskMutations.d.ts +2 -1
  153. package/dist/hooks/useTaskMutations.js +12 -1
  154. package/dist/hooks/useTaskMutations.test.js +30 -1
  155. package/dist/hooks/useTaskforce.d.ts +65 -4
  156. package/dist/hooks/useTaskforce.js +564 -243
  157. package/dist/hooks/useTaskforce.runtime-routing.test.d.ts +1 -0
  158. package/dist/hooks/useTaskforce.runtime-routing.test.js +152 -0
  159. package/dist/hooks/useTaskforce.sync-behavior.test.js +2454 -208
  160. package/dist/hooks/useWorkspaceSyncController.d.ts +4 -1
  161. package/dist/hooks/useWorkspaceSyncController.js +17 -2
  162. package/dist/hooks/useWorkspaceSyncController.test.js +1 -0
  163. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +16 -0
  164. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +316 -35
  165. package/dist/localization/locales/en-US.d.ts +0 -1
  166. package/dist/localization/locales/en-US.js +0 -1
  167. package/dist/localization/locales/es-419.js +0 -1
  168. package/dist/localization/locales/pt-BR.js +0 -1
  169. package/dist/mcp/adminWorkspaceRegistrar.d.ts +2 -0
  170. package/dist/mcp/adminWorkspaceRegistrar.js +154 -0
  171. package/dist/mcp/collaborationRegistrar.d.ts +2 -0
  172. package/dist/mcp/collaborationRegistrar.js +71 -0
  173. package/dist/mcp/documentAssetRegistrar.d.ts +2 -0
  174. package/dist/mcp/documentAssetRegistrar.js +346 -0
  175. package/dist/mcp/runtime.d.ts +2 -0
  176. package/dist/mcp/runtime.js +2433 -3534
  177. package/dist/mcp/runtime.test.js +440 -2
  178. package/dist/mcp/taskPlanningRegistrar.d.ts +2 -0
  179. package/dist/mcp/taskPlanningRegistrar.js +418 -0
  180. package/dist/mcp/toolCatalog.d.ts +35 -0
  181. package/dist/mcp/toolCatalog.js +3 -0
  182. package/dist/migrations/taskSchemaMigrations.d.ts +1 -1
  183. package/dist/migrations/taskSchemaMigrations.js +171 -61
  184. package/dist/migrations/taskSchemaMigrations.test.js +15 -0
  185. package/dist/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
  186. package/dist/resources/templates/workflows/collaborate.yaml +1 -1
  187. package/dist/resources/templates/workflows/evaluate.yaml +2 -2
  188. package/dist/resources/templates/workflows/plan.yaml +1 -1
  189. package/dist/resources/templates/workflows/review.yaml +2 -2
  190. package/dist/server/annotatedAttachmentsRoutes.test.js +3 -3
  191. package/dist/server/auth/providers/apple.d.ts +33 -0
  192. package/dist/server/auth/providers/apple.js +82 -0
  193. package/dist/server/auth/providers/appleClientSecret.d.ts +37 -0
  194. package/dist/server/auth/providers/appleClientSecret.js +65 -0
  195. package/dist/server/auth/providers/github.d.ts +26 -0
  196. package/dist/server/auth/providers/github.js +86 -0
  197. package/dist/server/auth/providers/google.d.ts +21 -0
  198. package/dist/server/auth/providers/google.js +56 -0
  199. package/dist/server/auth/providers/types.d.ts +21 -0
  200. package/dist/server/auth/providers/types.js +1 -0
  201. package/dist/server/auth.d.ts +2 -0
  202. package/dist/server/auth.js +6 -3
  203. package/dist/server/documentReviewRoutes.test.js +36 -3
  204. package/dist/server/index.cookieProxy.test.js +1 -0
  205. package/dist/server/index.d.ts +12 -0
  206. package/dist/server/index.js +120 -9
  207. package/dist/server/index.rateLimit.test.js +3 -0
  208. package/dist/server/index.test.js +106 -3
  209. package/dist/server/routes/admin.d.ts +4 -5
  210. package/dist/server/routes/admin.js +410 -80
  211. package/dist/server/routes/annotatedAttachments.d.ts +1 -1
  212. package/dist/server/routes/annotatedAttachments.js +1 -1
  213. package/dist/server/routes/auth.d.ts +16 -6
  214. package/dist/server/routes/auth.js +1015 -118
  215. package/dist/server/routes/authSupport.d.ts +30 -0
  216. package/dist/server/routes/authSupport.js +81 -0
  217. package/dist/server/routes/billing.d.ts +1 -1
  218. package/dist/server/routes/billing.js +276 -34
  219. package/dist/server/routes/billing.test.js +760 -52
  220. package/dist/server/routes/documentReviews.d.ts +1 -1
  221. package/dist/server/routes/documentReviews.js +1 -1
  222. package/dist/server/routes/documents.d.ts +5 -2
  223. package/dist/server/routes/documents.js +242 -74
  224. package/dist/server/routes/primitives.d.ts +11 -0
  225. package/dist/server/routes/primitives.js +73 -0
  226. package/dist/server/routes/resources.d.ts +1 -1
  227. package/dist/server/routes/resources.js +1 -1
  228. package/dist/server/routes/shared.d.ts +29 -3
  229. package/dist/server/routes/shared.js +69 -4
  230. package/dist/server/routes/sync.d.ts +1 -1
  231. package/dist/server/routes/sync.integration.test.js +437 -39
  232. package/dist/server/routes/sync.js +62 -606
  233. package/dist/server/routes/syncAuxRoutes.d.ts +15 -0
  234. package/dist/server/routes/syncAuxRoutes.js +91 -0
  235. package/dist/server/routes/syncAuxRoutes.test.d.ts +1 -0
  236. package/dist/server/routes/syncAuxRoutes.test.js +158 -0
  237. package/dist/server/routes/syncPullApplyRoutes.d.ts +36 -0
  238. package/dist/server/routes/syncPullApplyRoutes.js +204 -0
  239. package/dist/server/routes/syncPushRoutes.d.ts +24 -0
  240. package/dist/server/routes/syncPushRoutes.js +212 -0
  241. package/dist/server/routes/syncRouteGuards.d.ts +36 -0
  242. package/dist/server/routes/syncRouteGuards.js +67 -0
  243. package/dist/server/routes/syncRouteGuards.test.d.ts +1 -0
  244. package/dist/server/routes/syncRouteGuards.test.js +94 -0
  245. package/dist/server/routes/syncRouteTypes.d.ts +13 -0
  246. package/dist/server/routes/syncRouteTypes.js +1 -0
  247. package/dist/server/routes/syncSnapshotRoutes.d.ts +66 -0
  248. package/dist/server/routes/syncSnapshotRoutes.js +180 -0
  249. package/dist/server/routes/tasks.d.ts +1 -1
  250. package/dist/server/routes/tasks.js +44 -2
  251. package/dist/server/routes/workspaces.d.ts +1 -1
  252. package/dist/server/routes/workspaces.js +37 -6
  253. package/dist/server/routes.d.ts +2 -9
  254. package/dist/server/routes.js +136 -165
  255. package/dist/server/routes.test.js +2457 -162
  256. package/dist/services/workflowExportSnapshots.test.js +2 -0
  257. package/dist/shared/aiProfileSeatScope.d.ts +5 -0
  258. package/dist/shared/aiProfileSeatScope.js +21 -0
  259. package/dist/shared/passwordPolicy.d.ts +13 -0
  260. package/dist/shared/passwordPolicy.js +84 -0
  261. package/dist/shared/passwordPolicy.test.d.ts +1 -0
  262. package/dist/shared/passwordPolicy.test.js +34 -0
  263. package/dist/storage/workspaceAssetStore.d.ts +15 -3
  264. package/dist/storage/workspaceAssetStore.js +132 -44
  265. package/dist/storage/workspaceAssetStore.test.js +93 -6
  266. package/dist/styles/fonts.css +70 -0
  267. package/dist/sync/collaborationSyncPayload.d.ts +6 -1
  268. package/dist/sync/collaborationSyncPayload.js +17 -11
  269. package/dist/sync/collaborationSyncPayload.test.js +13 -1
  270. package/dist/sync/contentSyncState.d.ts +4 -8
  271. package/dist/sync/contentSyncState.js +33 -77
  272. package/dist/sync/syncApplyHandlers.d.ts +7 -0
  273. package/dist/sync/syncApplyHandlers.js +28 -13
  274. package/dist/sync/syncApplyHandlers.test.js +72 -3
  275. package/dist/sync/syncFieldSemantics.d.ts +12 -0
  276. package/dist/sync/syncFieldSemantics.js +55 -0
  277. package/dist/sync/syncResourceAdapters.d.ts +44 -0
  278. package/dist/sync/syncResourceAdapters.js +77 -0
  279. package/dist/sync/syncService.d.ts +6 -0
  280. package/dist/sync/syncService.js +60 -0
  281. package/dist/sync/syncService.test.js +71 -3
  282. package/dist/sync/taskSyncChangeKey.d.ts +2 -0
  283. package/dist/sync/taskSyncChangeKey.js +143 -0
  284. package/dist/sync/taskSyncPayload.js +3 -8
  285. package/dist/sync/workspacePullFeed.js +4 -3
  286. package/dist/sync/workspacePullFeed.test.js +43 -0
  287. package/dist/sync/workspaceRepair.js +5 -2
  288. package/dist/sync/workspaceSyncModel.d.ts +48 -0
  289. package/dist/sync/workspaceSyncModel.js +195 -31
  290. package/dist/sync/workspaceSyncModel.test.js +325 -12
  291. package/dist/sync/workspaceSyncState.d.ts +19 -0
  292. package/dist/sync/workspaceSyncState.js +3 -1
  293. package/dist/sync/workspaceSyncSurface.js +3 -8
  294. package/dist/types.d.ts +17 -0
  295. package/dist/ui/assets/AgentsModule-DrLawwNl.js +1 -0
  296. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BlS-cqnl.js → AnnotatedAttachmentWorkspace-Z9_whf7F.js} +1 -1
  297. package/dist/ui/assets/CourierPrime-Bold-BuLj_dpw.woff2 +0 -0
  298. package/dist/ui/assets/CourierPrime-BoldItalic-BrK2pUkZ.woff2 +0 -0
  299. package/dist/ui/assets/CourierPrime-Italic-DFUZK5Ey.woff2 +0 -0
  300. package/dist/ui/assets/CourierPrime-Regular-BsKphzVf.woff2 +0 -0
  301. package/dist/ui/assets/DocumentWorkspace-B03MaSkw.js +252 -0
  302. package/dist/ui/assets/DocumentWorkspace-Dhdso07p.css +1 -0
  303. package/dist/ui/assets/{InitiativesModule-BjR6iBf9.js → InitiativesModule-9E6ParrY.js} +1 -1
  304. package/dist/ui/assets/Noir_medium-bxQwKGzB.woff2 +0 -0
  305. package/dist/ui/assets/Noir_regular-ojf2kxlG.woff2 +0 -0
  306. package/dist/ui/assets/PlansPage-CvfnMiWq.css +1 -0
  307. package/dist/ui/assets/PlansPage-DRXscYxH.js +1 -0
  308. package/dist/ui/assets/RussoOne-Regular-Cu_qq_qC.woff2 +0 -0
  309. package/dist/ui/assets/SpecialElite-Regular-Di6gSvAS.woff2 +0 -0
  310. package/dist/ui/assets/TaskSettings-DZX4jk7e.js +9 -0
  311. package/dist/ui/assets/{WorkflowsModule-DnFqdUME.js → WorkflowsModule-BA7jcEpH.js} +1 -1
  312. package/dist/ui/assets/index-9eT8e0Lu.css +1 -0
  313. package/dist/ui/assets/index-C6k75jr8.js +6 -0
  314. package/dist/ui/assets/{vendor-icons-I7ZwG1z_.js → vendor-icons-DuEd_65c.js} +1 -1
  315. package/dist/ui/fonts/CourierPrime-Bold.ttf +0 -0
  316. package/dist/ui/fonts/CourierPrime-Bold.woff2 +0 -0
  317. package/dist/ui/fonts/CourierPrime-BoldItalic.ttf +0 -0
  318. package/dist/ui/fonts/CourierPrime-BoldItalic.woff2 +0 -0
  319. package/dist/ui/fonts/CourierPrime-Italic.ttf +0 -0
  320. package/dist/ui/fonts/CourierPrime-Italic.woff2 +0 -0
  321. package/dist/ui/fonts/CourierPrime-Regular.ttf +0 -0
  322. package/dist/ui/fonts/CourierPrime-Regular.woff2 +0 -0
  323. package/dist/ui/fonts/Noir_medium.otf +0 -0
  324. package/dist/ui/fonts/Noir_medium.woff2 +0 -0
  325. package/dist/ui/fonts/Noir_regular.otf +0 -0
  326. package/dist/ui/fonts/Noir_regular.woff2 +0 -0
  327. package/dist/ui/fonts/RussoOne-Regular.woff2 +0 -0
  328. package/dist/ui/fonts/SpecialElite-Regular.ttf +0 -0
  329. package/dist/ui/fonts/SpecialElite-Regular.woff2 +0 -0
  330. package/dist/ui/index.html +3 -3
  331. package/dist/utils/accountProfileSummaryCache.d.ts +5 -0
  332. package/dist/utils/assignees.d.ts +2 -0
  333. package/dist/utils/assignees.js +2 -0
  334. package/dist/utils/avatarUpload.d.ts +27 -0
  335. package/dist/utils/avatarUpload.js +132 -0
  336. package/dist/utils/avatarUpload.test.d.ts +1 -0
  337. package/dist/utils/avatarUpload.test.js +40 -0
  338. package/dist/utils/bootstrapTrace.d.ts +7 -0
  339. package/dist/utils/bootstrapTrace.js +29 -0
  340. package/dist/utils/commercialLifecyclePresentation.d.ts +19 -0
  341. package/dist/utils/commercialLifecyclePresentation.js +199 -0
  342. package/dist/utils/contextAssetEvents.d.ts +2 -0
  343. package/dist/utils/syncEventDetails.d.ts +3 -0
  344. package/dist/utils/syncEventDetails.js +64 -0
  345. package/dist/utils/syncEventDetails.test.d.ts +1 -0
  346. package/dist/utils/syncEventDetails.test.js +23 -0
  347. package/dist/utils/syncEventPresentation.d.ts +15 -0
  348. package/dist/utils/syncEventPresentation.js +129 -0
  349. package/dist/utils/syncEventPresentation.test.d.ts +1 -0
  350. package/dist/utils/syncEventPresentation.test.js +64 -0
  351. package/dist/utils/syncStatusPresentation.d.ts +72 -0
  352. package/dist/utils/syncStatusPresentation.js +217 -0
  353. package/dist/utils/syncStatusPresentation.test.d.ts +1 -0
  354. package/dist/utils/syncStatusPresentation.test.js +164 -0
  355. package/dist/utils/taskActivity.d.ts +2 -0
  356. package/dist/utils/taskActivity.js +228 -22
  357. package/dist/utils/taskActivity.test.js +119 -8
  358. package/dist/utils/workspaceSyncPresentation.d.ts +4 -0
  359. package/dist/utils/workspaceSyncPresentation.js +68 -2
  360. package/dist/utils/workspaceSyncPresentation.test.js +36 -1
  361. package/package.json +10 -2
  362. package/scripts/check-cloud-mcp.mjs +83 -0
  363. package/scripts/export-sqlite-to-postgres.mjs +7 -9
  364. package/scripts/playwright-auth.sh +5 -1
  365. package/scripts/run-billing-performance-smoke.sh +24 -0
  366. package/scripts/run-e2e-realtime.sh +17 -3
  367. package/scripts/run-smoke.sh +17 -5
  368. package/scripts/run-soak.sh +17 -5
  369. package/src/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
  370. package/src/resources/templates/workflows/collaborate.yaml +1 -1
  371. package/src/resources/templates/workflows/evaluate.yaml +2 -2
  372. package/src/resources/templates/workflows/plan.yaml +1 -1
  373. package/src/resources/templates/workflows/review.yaml +2 -2
  374. package/dist/ui/assets/AgentsModule-BLWVbuKP.js +0 -1
  375. package/dist/ui/assets/DocumentWorkspace-BH_6DF6x.js +0 -250
  376. package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +0 -1
  377. package/dist/ui/assets/PlansPage-C2dd2n1X.css +0 -1
  378. package/dist/ui/assets/PlansPage-CSDQ4sLa.js +0 -1
  379. package/dist/ui/assets/RussoOne-Regular-C3BxZIj7.ttf +0 -0
  380. package/dist/ui/assets/TaskSettings-CEDy34Jo.js +0 -9
  381. package/dist/ui/assets/index-DXUdtH1B.js +0 -6
  382. package/dist/ui/assets/index-Ii0B0rTO.css +0 -1
  383. package/scripts/migrate-planning-model.ts +0 -233
@@ -7,6 +7,28 @@ import { TaskforceCore } from './TaskforceCore';
7
7
  import fs from 'fs';
8
8
  import path from 'path';
9
9
  import modalStyles from './components/ui/Modal.module.css';
10
+ vi.mock('react-avatar-editor', async () => {
11
+ const React = await import('react');
12
+ return {
13
+ default: React.forwardRef((_props, ref) => {
14
+ React.useImperativeHandle(ref, () => ({
15
+ getImageScaledToCanvas: () => ({
16
+ width: 1024,
17
+ height: 1024,
18
+ getContext: () => ({
19
+ drawImage: () => undefined
20
+ }),
21
+ toBlob: (callback, type) => {
22
+ callback(new Blob([new Uint8Array([1, 2, 3, 4])], { type: type || 'image/webp' }));
23
+ }
24
+ }),
25
+ getImage: () => document.createElement('canvas'),
26
+ getCroppingRect: () => ({ x: 0, y: 0, width: 1, height: 1 })
27
+ }));
28
+ return _jsx("div", { "data-testid": "avatar-editor" });
29
+ })
30
+ };
31
+ });
10
32
  const mockTasks = [
11
33
  {
12
34
  id: 'task-20260130-abc123',
@@ -830,6 +852,9 @@ describe('TaskforceCore Integration', () => {
830
852
  });
831
853
  it('shows full-page login when cloud auth is required and user is unauthenticated', async () => {
832
854
  const fetchMock = vi.fn((url) => {
855
+ if (url.includes('/api/taskforce/auth/runtime-config')) {
856
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'cloud' } }) });
857
+ }
833
858
  if (url.includes('/api/taskforce/auth/session')) {
834
859
  return Promise.resolve({
835
860
  ok: true,
@@ -929,8 +954,8 @@ describe('TaskforceCore Integration', () => {
929
954
  json: () => Promise.resolve({
930
955
  plans: [
931
956
  {
932
- planId: 'pro',
933
- displayName: 'Pro Plan test mode',
957
+ planId: 'basic',
958
+ displayName: 'Basic Plan test mode',
934
959
  status: 'active',
935
960
  features: [],
936
961
  pricing: { month: null, year: null },
@@ -946,7 +971,7 @@ describe('TaskforceCore Integration', () => {
946
971
  render(_jsx(MemoryRouter, { initialEntries: ['/pricing'], children: _jsx(TaskforceCore, {}) }));
947
972
  await waitFor(() => {
948
973
  expect(screen.queryByPlaceholderText('Email')).not.toBeInTheDocument();
949
- expect(screen.getByText('Pro Plan test mode')).toBeInTheDocument();
974
+ expect(screen.getByText('Basic Plan test mode')).toBeInTheDocument();
950
975
  expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
951
976
  });
952
977
  });
@@ -1019,8 +1044,8 @@ describe('TaskforceCore Integration', () => {
1019
1044
  json: () => Promise.resolve({
1020
1045
  plans: [
1021
1046
  {
1022
- planId: 'starter',
1023
- displayName: 'Starter',
1047
+ planId: 'free',
1048
+ displayName: 'Free',
1024
1049
  status: 'active',
1025
1050
  features: [],
1026
1051
  pricing: {
@@ -1028,7 +1053,7 @@ describe('TaskforceCore Integration', () => {
1028
1053
  year: null
1029
1054
  },
1030
1055
  defaultVersion: {
1031
- planVersionId: 'starter-v1',
1056
+ planVersionId: 'free-v1',
1032
1057
  versionNumber: 1,
1033
1058
  seatLimit: null,
1034
1059
  metadata: {}
@@ -1042,8 +1067,8 @@ describe('TaskforceCore Integration', () => {
1042
1067
  return Promise.resolve({
1043
1068
  ok: true,
1044
1069
  json: () => Promise.resolve({
1045
- planId: 'starter',
1046
- planName: 'Starter',
1070
+ planId: 'free',
1071
+ planName: 'Free',
1047
1072
  entitlementState: 'active'
1048
1073
  })
1049
1074
  });
@@ -1052,8 +1077,8 @@ describe('TaskforceCore Integration', () => {
1052
1077
  return Promise.resolve({
1053
1078
  ok: true,
1054
1079
  json: () => Promise.resolve({
1055
- planId: 'starter',
1056
- planName: 'Starter',
1080
+ planId: 'free',
1081
+ planName: 'Free',
1057
1082
  entitlementState: 'active'
1058
1083
  })
1059
1084
  });
@@ -1082,7 +1107,6 @@ describe('TaskforceCore Integration', () => {
1082
1107
  render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, {}) }));
1083
1108
  await waitFor(() => {
1084
1109
  expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
1085
- expect(screen.getByText('Choose a plan to continue.')).toBeInTheDocument();
1086
1110
  });
1087
1111
  });
1088
1112
  it('redirects authenticated users with a blocking account access gate to the plans page', async () => {
@@ -1154,8 +1178,8 @@ describe('TaskforceCore Integration', () => {
1154
1178
  json: () => Promise.resolve({
1155
1179
  plans: [
1156
1180
  {
1157
- planId: 'starter',
1158
- displayName: 'Starter',
1181
+ planId: 'free',
1182
+ displayName: 'Free',
1159
1183
  status: 'active',
1160
1184
  features: [],
1161
1185
  pricing: {
@@ -1163,7 +1187,7 @@ describe('TaskforceCore Integration', () => {
1163
1187
  year: null
1164
1188
  },
1165
1189
  defaultVersion: {
1166
- planVersionId: 'starter-v1',
1190
+ planVersionId: 'free-v1',
1167
1191
  versionNumber: 1,
1168
1192
  seatLimit: null,
1169
1193
  metadata: {}
@@ -1269,8 +1293,8 @@ describe('TaskforceCore Integration', () => {
1269
1293
  json: () => Promise.resolve({
1270
1294
  plans: [
1271
1295
  {
1272
- planId: 'starter',
1273
- displayName: 'Starter',
1296
+ planId: 'free',
1297
+ displayName: 'Free',
1274
1298
  status: 'active',
1275
1299
  features: [],
1276
1300
  pricing: {
@@ -1278,7 +1302,7 @@ describe('TaskforceCore Integration', () => {
1278
1302
  year: null
1279
1303
  },
1280
1304
  defaultVersion: {
1281
- planVersionId: 'starter-v1',
1305
+ planVersionId: 'free-v1',
1282
1306
  versionNumber: 1,
1283
1307
  seatLimit: null,
1284
1308
  metadata: {}
@@ -1292,9 +1316,9 @@ describe('TaskforceCore Integration', () => {
1292
1316
  return Promise.resolve({
1293
1317
  ok: true,
1294
1318
  json: () => Promise.resolve({
1295
- planId: 'starter',
1296
- planName: 'Starter',
1297
- planVersionId: 'starter-v1',
1319
+ planId: 'free',
1320
+ planName: 'Free',
1321
+ planVersionId: 'free-v1',
1298
1322
  entitlementState: 'active'
1299
1323
  })
1300
1324
  });
@@ -1323,7 +1347,7 @@ describe('TaskforceCore Integration', () => {
1323
1347
  render(_jsx(MemoryRouter, { initialEntries: ['/?screen=plans&gate=missing_entitlement'], children: _jsx(TaskforceCore, {}) }));
1324
1348
  await waitFor(() => {
1325
1349
  expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
1326
- expect(screen.getByText('Starter')).toBeInTheDocument();
1350
+ expect(screen.getByText('Free')).toBeInTheDocument();
1327
1351
  });
1328
1352
  await waitFor(() => {
1329
1353
  expect(screen.queryByText('No entitlement is linked to this account.')).not.toBeInTheDocument();
@@ -1393,8 +1417,8 @@ describe('TaskforceCore Integration', () => {
1393
1417
  return Promise.resolve({
1394
1418
  ok: true,
1395
1419
  json: () => Promise.resolve({
1396
- planId: 'starter',
1397
- planName: 'Starter',
1420
+ planId: 'free',
1421
+ planName: 'Free',
1398
1422
  entitlementState: 'active'
1399
1423
  })
1400
1424
  });
@@ -1450,14 +1474,14 @@ describe('TaskforceCore Integration', () => {
1450
1474
  });
1451
1475
  await waitFor(() => {
1452
1476
  expect(screen.getByText('Subscription')).toBeInTheDocument();
1453
- expect(screen.getByText('Starter')).toBeInTheDocument();
1477
+ expect(screen.getByText('Free')).toBeInTheDocument();
1454
1478
  expect(screen.getByText('Runtime')).toBeInTheDocument();
1455
1479
  expect(screen.getByText('LOCAL')).toBeInTheDocument();
1456
1480
  expect(screen.getByText('Environment')).toBeInTheDocument();
1457
1481
  expect(screen.getByText('CLOUD.EXAMPLE.COM')).toBeInTheDocument();
1458
1482
  expect(screen.queryByText(/Current Plan:/i)).not.toBeInTheDocument();
1459
1483
  });
1460
- await user.click(screen.getByRole('button', { name: /Subscription Starter/i }));
1484
+ await user.click(screen.getByRole('button', { name: /Subscription Free/i }));
1461
1485
  await waitFor(() => {
1462
1486
  expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
1463
1487
  });
@@ -1512,8 +1536,8 @@ describe('TaskforceCore Integration', () => {
1512
1536
  return Promise.resolve({
1513
1537
  ok: true,
1514
1538
  json: () => Promise.resolve({
1515
- planId: 'starter',
1516
- planName: 'Starter',
1539
+ planId: 'free',
1540
+ planName: 'Free',
1517
1541
  entitlementState: 'active'
1518
1542
  })
1519
1543
  });
@@ -1563,7 +1587,7 @@ describe('TaskforceCore Integration', () => {
1563
1587
  }
1564
1588
  await waitFor(() => {
1565
1589
  expect(screen.getByText('Subscription')).toBeInTheDocument();
1566
- expect(screen.getByText('Starter')).toBeInTheDocument();
1590
+ expect(screen.getByText('Free')).toBeInTheDocument();
1567
1591
  expect(screen.getByText('Runtime')).toBeInTheDocument();
1568
1592
  expect(screen.getByText('LOCAL')).toBeInTheDocument();
1569
1593
  expect(screen.getByText('Environment')).toBeInTheDocument();
@@ -1590,9 +1614,9 @@ describe('TaskforceCore Integration', () => {
1590
1614
  userId: 'user-new',
1591
1615
  displayName: 'New User',
1592
1616
  workspaceId: 'workspace-local-active',
1593
- planId: 'starter',
1594
- planVersionId: 'starter-v1',
1595
- planName: 'Starter',
1617
+ planId: 'free',
1618
+ planVersionId: 'free-v1',
1619
+ planName: 'Free',
1596
1620
  teamManagementAllowed: false,
1597
1621
  teamPlanMode: 'personal',
1598
1622
  workspaceRole: 'member'
@@ -1690,11 +1714,11 @@ describe('TaskforceCore Integration', () => {
1690
1714
  return jsonResponse({
1691
1715
  plans: [
1692
1716
  {
1693
- planId: 'starter',
1694
- displayName: 'Starter',
1717
+ planId: 'free',
1718
+ displayName: 'Free',
1695
1719
  status: 'active',
1696
1720
  defaultVersion: {
1697
- planVersionId: 'starter-v1',
1721
+ planVersionId: 'free-v1',
1698
1722
  versionNumber: 1,
1699
1723
  seatLimit: null,
1700
1724
  metadata: {}
@@ -1786,11 +1810,12 @@ describe('TaskforceCore Integration', () => {
1786
1810
  if (url.includes('/api/taskforce/workspace/assignee-options')) {
1787
1811
  return jsonResponse({ assignees: [] });
1788
1812
  }
1789
- if (url.includes('/api/taskforce/initiatives')) {
1790
- return jsonResponse([]);
1791
- }
1792
- if (url.includes('/api/taskforce/workstreams')) {
1793
- return jsonResponse([]);
1813
+ if (url.includes('/api/taskforce/planning/bootstrap')) {
1814
+ return jsonResponse({
1815
+ initiatives: [],
1816
+ workstreams: [],
1817
+ workstreamTaskSummaries: [],
1818
+ });
1794
1819
  }
1795
1820
  return jsonResponse({});
1796
1821
  });
@@ -1817,7 +1842,8 @@ describe('TaskforceCore Integration', () => {
1817
1842
  await user.click(screen.getByRole('button', { name: 'Register' }));
1818
1843
  await user.type(await screen.findByPlaceholderText('Email'), accounts.newcomer.email);
1819
1844
  await user.type(screen.getByPlaceholderText('Display name'), accounts.newcomer.displayName);
1820
- await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
1845
+ await user.type(screen.getByPlaceholderText('Password'), 'GoodPassphrase123!');
1846
+ await user.type(screen.getByPlaceholderText('Confirm password'), 'GoodPassphrase123!');
1821
1847
  await user.click(screen.getByRole('button', { name: 'Create Account' }));
1822
1848
  await waitFor(() => {
1823
1849
  expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
@@ -1825,7 +1851,7 @@ describe('TaskforceCore Integration', () => {
1825
1851
  await user.click(screen.getByTitle(/Account/i));
1826
1852
  await waitFor(() => {
1827
1853
  expect(screen.getByText(accounts.newcomer.email)).toBeInTheDocument();
1828
- expect(screen.getByText('Starter')).toBeInTheDocument();
1854
+ expect(screen.getByText('Free')).toBeInTheDocument();
1829
1855
  });
1830
1856
  await user.click(screen.getByRole('menuitem', { name: 'Sign Out' }));
1831
1857
  await waitFor(() => {
@@ -1846,7 +1872,7 @@ describe('TaskforceCore Integration', () => {
1846
1872
  await waitFor(() => {
1847
1873
  expect(screen.getByText(accounts.ross.email)).toBeInTheDocument();
1848
1874
  expect(screen.getByText('Team 5')).toBeInTheDocument();
1849
- expect(screen.queryByText('Starter')).not.toBeInTheDocument();
1875
+ expect(screen.queryByText('Free')).not.toBeInTheDocument();
1850
1876
  expect(screen.getByRole('menuitem', { name: 'Team Management' })).toBeInTheDocument();
1851
1877
  });
1852
1878
  await user.click(screen.getByRole('button', { name: /Subscription Team 5/i }));
@@ -1899,6 +1925,7 @@ describe('TaskforceCore Integration', () => {
1899
1925
  expect(screen.getByText('Create your Taskforce account.')).toBeInTheDocument();
1900
1926
  expect(screen.getByLabelText('Email')).toBeInTheDocument();
1901
1927
  expect(screen.getByLabelText('Display name')).toBeInTheDocument();
1928
+ expect(screen.getByLabelText('Confirm password')).toBeInTheDocument();
1902
1929
  expect(screen.getByPlaceholderText('Email')).toBeInTheDocument();
1903
1930
  expect(screen.getByPlaceholderText('Display name')).toBeInTheDocument();
1904
1931
  expect(screen.getByRole('button', { name: 'Create Account' })).toBeInTheDocument();
@@ -1910,6 +1937,56 @@ describe('TaskforceCore Integration', () => {
1910
1937
  });
1911
1938
  expect(container.querySelector('[aria-pressed]')).not.toBeInTheDocument();
1912
1939
  });
1940
+ it('blocks registration when the confirmation does not match', async () => {
1941
+ const user = userEvent.setup();
1942
+ const fetchMock = vi.fn((url) => {
1943
+ if (url.includes('/api/taskforce/auth/session')) {
1944
+ return Promise.resolve({
1945
+ ok: true,
1946
+ json: () => Promise.resolve({
1947
+ authenticated: false,
1948
+ runtimeMode: 'cloud',
1949
+ authRequiredForApi: false
1950
+ })
1951
+ });
1952
+ }
1953
+ if (url.includes('/api/taskforce/config')) {
1954
+ return Promise.resolve({
1955
+ ok: true,
1956
+ json: () => Promise.resolve({
1957
+ ...mockConfig,
1958
+ runtimeMode: 'cloud',
1959
+ authRequiredForApi: false,
1960
+ setupState: {
1961
+ globalSetupState: 'present',
1962
+ workspaceSetupState: 'present',
1963
+ runtimeMode: 'cloud',
1964
+ workspaceId: 'workspace-local-active',
1965
+ mode: 'core',
1966
+ forceSetup: false,
1967
+ forceGlobalSetup: false,
1968
+ forceWorkspaceSetup: false
1969
+ }
1970
+ })
1971
+ });
1972
+ }
1973
+ if (url.includes('/api/taskforce/workflow-templates')) {
1974
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
1975
+ }
1976
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
1977
+ });
1978
+ vi.stubGlobal('fetch', fetchMock);
1979
+ render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
1980
+ await user.type(await screen.findByPlaceholderText('Email'), 'mismatch@example.com');
1981
+ await user.type(screen.getByPlaceholderText('Display name'), 'Mismatch User');
1982
+ await user.type(screen.getByPlaceholderText('Password'), 'GoodPassphrase123!');
1983
+ await user.type(screen.getByPlaceholderText('Confirm password'), 'DifferentPassphrase123!');
1984
+ await user.click(screen.getByRole('button', { name: 'Create Account' }));
1985
+ await waitFor(() => {
1986
+ expect(screen.getByText('Passwords do not match.')).toBeInTheDocument();
1987
+ });
1988
+ expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('/api/taskforce/auth/register'))).toBe(false);
1989
+ });
1913
1990
  it('keeps the verification screen focused on verification and lets the user request a fresh email token', async () => {
1914
1991
  const fetchMock = vi.fn((url) => {
1915
1992
  if (url.includes('/api/taskforce/auth/session')) {
@@ -2034,6 +2111,69 @@ describe('TaskforceCore Integration', () => {
2034
2111
  expect(screen.getByRole('button', { name: /Resend Verification/ })).toBeDisabled();
2035
2112
  expect(fetchMock.mock.calls.some(([url]) => String(url).includes('/api/taskforce/auth/verify-email/request'))).toBe(true);
2036
2113
  });
2114
+ it('shows neutral verification copy when resend succeeds without attempting delivery', async () => {
2115
+ const user = userEvent.setup();
2116
+ const fetchMock = vi.fn((input) => {
2117
+ const url = String(input);
2118
+ if (url.includes('/api/taskforce/auth/session')) {
2119
+ return Promise.resolve({
2120
+ ok: true,
2121
+ json: () => Promise.resolve({
2122
+ authenticated: false,
2123
+ runtimeMode: 'cloud',
2124
+ authRequiredForApi: false
2125
+ })
2126
+ });
2127
+ }
2128
+ if (url.includes('/api/taskforce/config')) {
2129
+ return Promise.resolve({
2130
+ ok: true,
2131
+ json: () => Promise.resolve({
2132
+ ...mockConfig,
2133
+ runtimeMode: 'cloud',
2134
+ authRequiredForApi: false,
2135
+ setupState: {
2136
+ globalSetupState: 'present',
2137
+ workspaceSetupState: 'present',
2138
+ runtimeMode: 'cloud',
2139
+ workspaceId: 'workspace-local-active',
2140
+ mode: 'core',
2141
+ forceSetup: false,
2142
+ forceGlobalSetup: false,
2143
+ forceWorkspaceSetup: false
2144
+ }
2145
+ })
2146
+ });
2147
+ }
2148
+ if (url.includes('/api/taskforce/auth/verify-email/request')) {
2149
+ return Promise.resolve({
2150
+ ok: true,
2151
+ json: () => Promise.resolve({
2152
+ success: true,
2153
+ emailSent: false,
2154
+ deliveryAttempted: false,
2155
+ verificationToken: null
2156
+ })
2157
+ });
2158
+ }
2159
+ if (url.includes('/api/taskforce/public/pricing')) {
2160
+ return Promise.resolve({
2161
+ ok: true,
2162
+ json: () => Promise.resolve({
2163
+ plans: []
2164
+ })
2165
+ });
2166
+ }
2167
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
2168
+ });
2169
+ vi.stubGlobal('fetch', fetchMock);
2170
+ render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=verify'], children: _jsx(TaskforceCore, {}) }));
2171
+ await user.type(await screen.findByPlaceholderText('Email'), 'verify@example.com');
2172
+ await user.click(screen.getByRole('button', { name: 'Resend Verification' }));
2173
+ await waitFor(() => {
2174
+ expect(screen.getByText('If that account exists and still needs verification, we sent an email.')).toBeInTheDocument();
2175
+ });
2176
+ });
2037
2177
  it('does not switch to verify mode after registration when verification is not required', async () => {
2038
2178
  const user = userEvent.setup();
2039
2179
  let authenticated = false;
@@ -2138,7 +2278,8 @@ describe('TaskforceCore Integration', () => {
2138
2278
  render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2Fplans'], children: _jsx(TaskforceCore, {}) }));
2139
2279
  await user.type(await screen.findByPlaceholderText('Email'), 'local@example.com');
2140
2280
  await user.type(screen.getByPlaceholderText('Display name'), 'Local User');
2141
- await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
2281
+ await user.type(screen.getByPlaceholderText('Password'), 'GoodPassphrase123!');
2282
+ await user.type(screen.getByPlaceholderText('Confirm password'), 'GoodPassphrase123!');
2142
2283
  await user.click(screen.getByRole('button', { name: 'Create Account' }));
2143
2284
  await waitFor(() => {
2144
2285
  expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
@@ -2228,11 +2369,11 @@ describe('TaskforceCore Integration', () => {
2228
2369
  if (url.includes('/api/taskforce/public/pricing')) {
2229
2370
  return jsonResponse({
2230
2371
  plans: [{
2231
- planId: 'starter',
2232
- displayName: 'Starter',
2372
+ planId: 'free',
2373
+ displayName: 'Free',
2233
2374
  status: 'active',
2234
2375
  defaultVersion: {
2235
- planVersionId: 'starter-v1',
2376
+ planVersionId: 'free-v1',
2236
2377
  versionNumber: 1,
2237
2378
  seatLimit: null,
2238
2379
  metadata: {}
@@ -2240,10 +2381,10 @@ describe('TaskforceCore Integration', () => {
2240
2381
  features: [],
2241
2382
  pricing: {
2242
2383
  month: {
2243
- planVersionId: 'starter-v1',
2384
+ planVersionId: 'free-v1',
2244
2385
  interval: 'month',
2245
2386
  pricingType: 'free',
2246
- stripePriceId: 'free:starter-v1:month',
2387
+ stripePriceId: 'free:free-v1:month',
2247
2388
  displayAmount: '$0.00',
2248
2389
  unitAmount: 0,
2249
2390
  currency: 'usd'
@@ -2296,11 +2437,11 @@ describe('TaskforceCore Integration', () => {
2296
2437
  render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
2297
2438
  await user.type(await screen.findByPlaceholderText('Email'), 'pending@example.com');
2298
2439
  await user.type(screen.getByPlaceholderText('Display name'), 'Pending User');
2299
- await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
2440
+ await user.type(screen.getByPlaceholderText('Password'), 'GoodPassphrase123!');
2441
+ await user.type(screen.getByPlaceholderText('Confirm password'), 'GoodPassphrase123!');
2300
2442
  await user.click(screen.getByRole('button', { name: 'Create Account' }));
2301
2443
  await waitFor(() => {
2302
2444
  expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
2303
- expect(screen.getByText('Choose a plan to continue.')).toBeInTheDocument();
2304
2445
  });
2305
2446
  expect(fetchMock.mock.calls.some(([url]) => String(url).includes('/api/taskforce/billing/checkout-session'))).toBe(false);
2306
2447
  });
@@ -2412,11 +2553,148 @@ describe('TaskforceCore Integration', () => {
2412
2553
  render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
2413
2554
  await user.type(await screen.findByPlaceholderText('Email'), 'brandnew@example.com');
2414
2555
  await user.type(screen.getByPlaceholderText('Display name'), 'Brand New');
2415
- await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
2556
+ await user.type(screen.getByPlaceholderText('Password'), 'GoodPassphrase123!');
2557
+ await user.type(screen.getByPlaceholderText('Confirm password'), 'GoodPassphrase123!');
2416
2558
  await user.click(screen.getByRole('button', { name: 'Create Account' }));
2417
2559
  await waitFor(() => {
2418
2560
  expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
2419
- expect(screen.getByText('Choose a plan to continue.')).toBeInTheDocument();
2561
+ });
2562
+ expect(screen.queryByText('Configure your new workspace.')).not.toBeInTheDocument();
2563
+ });
2564
+ it('keeps registration on plan selection while the post-auth cloud session is still resolving for a commercially gated user with no workspace', async () => {
2565
+ const user = userEvent.setup();
2566
+ let authenticated = false;
2567
+ let authSessionCallCount = 0;
2568
+ let releaseInitialSession = null;
2569
+ const initialSessionGate = new Promise((resolve) => {
2570
+ releaseInitialSession = resolve;
2571
+ });
2572
+ const fetchMock = vi.fn((input) => {
2573
+ const url = String(input);
2574
+ if (url.includes('/api/taskforce/auth/runtime-config')) {
2575
+ return jsonResponse({
2576
+ config: {
2577
+ baseUrl: 'https://app.taskforce.example',
2578
+ apiBaseUrl: 'https://app.taskforce.example/api',
2579
+ cloudAuthBaseUrl: 'https://app.taskforce.example',
2580
+ runtimeMode: 'cloud',
2581
+ authSource: 'cloud',
2582
+ workspaceMode: 'multi-cloud',
2583
+ workspaceSwitchingEnabled: true,
2584
+ authRequiredForApi: true
2585
+ }
2586
+ });
2587
+ }
2588
+ if (url.includes('/api/taskforce/auth/session')) {
2589
+ authSessionCallCount += 1;
2590
+ const payload = () => ({
2591
+ authenticated,
2592
+ runtimeMode: 'cloud',
2593
+ authRequiredForApi: true,
2594
+ userId: authenticated ? 'user-gated-pending' : null,
2595
+ workspaceId: null,
2596
+ betaAccess: true
2597
+ });
2598
+ if (authSessionCallCount === 1) {
2599
+ return initialSessionGate.then(() => jsonResponse(payload()));
2600
+ }
2601
+ return jsonResponse(payload());
2602
+ }
2603
+ if (url.includes('/api/taskforce/config')) {
2604
+ return jsonResponse({
2605
+ ...mockConfig,
2606
+ runtimeMode: 'cloud',
2607
+ authRequiredForApi: true,
2608
+ workspaceSwitchingEnabled: true,
2609
+ setupState: {
2610
+ globalSetupState: 'present',
2611
+ workspaceSetupState: 'missing',
2612
+ runtimeMode: 'cloud',
2613
+ workspaceId: 'default',
2614
+ mode: 'core',
2615
+ forceSetup: true,
2616
+ forceGlobalSetup: false,
2617
+ forceWorkspaceSetup: true
2618
+ }
2619
+ });
2620
+ }
2621
+ if (url.includes('/api/taskforce/workspaces')) {
2622
+ return jsonResponse({
2623
+ success: true,
2624
+ workspaces: [],
2625
+ currentWorkspaceId: 'default'
2626
+ });
2627
+ }
2628
+ if (url.includes('/api/taskforce/account/access-status')) {
2629
+ return jsonResponse({
2630
+ gate: 'plan_selection_required',
2631
+ hasEntitlement: true,
2632
+ canAccessApp: false,
2633
+ canAccessPlans: true,
2634
+ message: 'Choose a plan to continue.'
2635
+ });
2636
+ }
2637
+ if (url.includes('/api/taskforce/account/profile-summary')) {
2638
+ return jsonResponse({
2639
+ entitlementState: 'pending_plan_selection',
2640
+ gate: 'plan_selection_required',
2641
+ message: 'Choose a plan to continue.',
2642
+ workspaceId: null
2643
+ });
2644
+ }
2645
+ if (url.includes('/api/taskforce/public/pricing')) {
2646
+ return jsonResponse({ plans: [] });
2647
+ }
2648
+ if (url.includes('/api/taskforce/auth/register')) {
2649
+ authenticated = true;
2650
+ return jsonResponse({
2651
+ success: true,
2652
+ workspaceSetupRequired: true,
2653
+ verificationRequired: false,
2654
+ planSelectionRequired: true,
2655
+ commercialState: 'pending_plan_selection'
2656
+ });
2657
+ }
2658
+ if (url.includes('/api/taskforce/categories'))
2659
+ return jsonResponse(mockCategories);
2660
+ if (url.includes('/api/taskforce/tasks'))
2661
+ return jsonResponse({ tasks: mockTasks });
2662
+ if (url.includes('/api/taskforce/archive'))
2663
+ return jsonResponse({ archived: [] });
2664
+ if (url.includes('/api/taskforce/data-version'))
2665
+ return jsonResponse({ dataVersion: 1 });
2666
+ if (url.includes('/api/taskforce/types'))
2667
+ return jsonResponse({ types: [] });
2668
+ if (url.includes('/api/taskforce/priorities'))
2669
+ return jsonResponse({ priorities: [] });
2670
+ if (url.includes('/api/taskforce/approaches'))
2671
+ return jsonResponse({ approaches: [] });
2672
+ if (url.includes('/api/taskforce/specialists'))
2673
+ return jsonResponse({ specialists: [] });
2674
+ if (url.includes('/api/taskforce/workflow-templates'))
2675
+ return jsonResponse({ templates: [] });
2676
+ return jsonResponse({});
2677
+ });
2678
+ vi.stubGlobal('fetch', fetchMock);
2679
+ render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
2680
+ await user.type(await screen.findByPlaceholderText('Email'), 'gated-pending@example.com');
2681
+ await user.type(screen.getByPlaceholderText('Display name'), 'Gated Pending');
2682
+ await user.type(screen.getByPlaceholderText('Password'), 'GoodPassphrase123!');
2683
+ await user.type(screen.getByPlaceholderText('Confirm password'), 'GoodPassphrase123!');
2684
+ await user.click(screen.getByRole('button', { name: 'Create Account' }));
2685
+ await act(async () => {
2686
+ await Promise.resolve();
2687
+ });
2688
+ await waitFor(() => {
2689
+ expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
2690
+ });
2691
+ expect(screen.queryByText('Configure your new workspace.')).not.toBeInTheDocument();
2692
+ await act(async () => {
2693
+ releaseInitialSession?.();
2694
+ await Promise.resolve();
2695
+ });
2696
+ await waitFor(() => {
2697
+ expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
2420
2698
  });
2421
2699
  expect(screen.queryByText('Configure your new workspace.')).not.toBeInTheDocument();
2422
2700
  });
@@ -2493,9 +2771,9 @@ describe('TaskforceCore Integration', () => {
2493
2771
  }
2494
2772
  if (url.includes('/api/taskforce/account/profile-summary')) {
2495
2773
  return jsonResponse({
2496
- planId: 'pro',
2497
- planName: 'Pro',
2498
- planVersionId: 'pro-v1',
2774
+ planId: 'basic',
2775
+ planName: 'Basic',
2776
+ planVersionId: 'basic-v1',
2499
2777
  entitlementState: 'checkout_pending',
2500
2778
  gate: 'checkout_pending',
2501
2779
  message: 'Complete checkout to continue.',
@@ -2505,11 +2783,11 @@ describe('TaskforceCore Integration', () => {
2505
2783
  if (url.includes('/api/taskforce/public/pricing')) {
2506
2784
  return jsonResponse({
2507
2785
  plans: [{
2508
- planId: 'pro',
2509
- displayName: 'Pro',
2786
+ planId: 'basic',
2787
+ displayName: 'Basic',
2510
2788
  status: 'active',
2511
2789
  defaultVersion: {
2512
- planVersionId: 'pro-v1',
2790
+ planVersionId: 'basic-v1',
2513
2791
  versionNumber: 1,
2514
2792
  seatLimit: null,
2515
2793
  metadata: {}
@@ -2517,19 +2795,19 @@ describe('TaskforceCore Integration', () => {
2517
2795
  features: [],
2518
2796
  pricing: {
2519
2797
  month: {
2520
- planVersionId: 'pro-v1',
2798
+ planVersionId: 'basic-v1',
2521
2799
  interval: 'month',
2522
2800
  pricingType: 'stripe',
2523
- stripePriceId: 'price_pro_month',
2801
+ stripePriceId: 'price_basic_month',
2524
2802
  displayAmount: '$19.00',
2525
2803
  unitAmount: 1900,
2526
2804
  currency: 'usd'
2527
2805
  },
2528
2806
  year: {
2529
- planVersionId: 'pro-v1',
2807
+ planVersionId: 'basic-v1',
2530
2808
  interval: 'year',
2531
2809
  pricingType: 'stripe',
2532
- stripePriceId: 'price_pro_year',
2810
+ stripePriceId: 'price_basic_year',
2533
2811
  displayAmount: '$190.00',
2534
2812
  unitAmount: 19000,
2535
2813
  currency: 'usd'
@@ -2584,10 +2862,11 @@ describe('TaskforceCore Integration', () => {
2584
2862
  return jsonResponse({});
2585
2863
  });
2586
2864
  vi.stubGlobal('fetch', fetchMock);
2587
- render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&planId=pro&planVersionId=pro-v1&interval=year&next=%2F'], children: _jsx(TaskforceCore, {}) }));
2865
+ render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&planId=basic&planVersionId=basic-v1&interval=year&next=%2F'], children: _jsx(TaskforceCore, {}) }));
2588
2866
  await user.type(await screen.findByPlaceholderText('Email'), 'checkout@example.com');
2589
2867
  await user.type(screen.getByPlaceholderText('Display name'), 'Checkout User');
2590
- await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
2868
+ await user.type(screen.getByPlaceholderText('Password'), 'GoodPassphrase123!');
2869
+ await user.type(screen.getByPlaceholderText('Confirm password'), 'GoodPassphrase123!');
2591
2870
  await user.click(screen.getByRole('button', { name: 'Create Account' }));
2592
2871
  await waitFor(() => {
2593
2872
  expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
@@ -2720,7 +2999,8 @@ describe('TaskforceCore Integration', () => {
2720
2999
  render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
2721
3000
  await user.type(await screen.findByPlaceholderText('Email'), 'free-user@example.com');
2722
3001
  await user.type(screen.getByPlaceholderText('Display name'), 'Free User');
2723
- await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
3002
+ await user.type(screen.getByPlaceholderText('Password'), 'GoodPassphrase123!');
3003
+ await user.type(screen.getByPlaceholderText('Confirm password'), 'GoodPassphrase123!');
2724
3004
  await user.click(screen.getByRole('button', { name: 'Create Account' }));
2725
3005
  await act(async () => {
2726
3006
  await Promise.resolve();
@@ -2846,7 +3126,8 @@ describe('TaskforceCore Integration', () => {
2846
3126
  render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
2847
3127
  await user.type(await screen.findByPlaceholderText('Email'), 'existing-workspace@example.com');
2848
3128
  await user.type(screen.getByPlaceholderText('Display name'), 'Existing Workspace User');
2849
- await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
3129
+ await user.type(screen.getByPlaceholderText('Password'), 'GoodPassphrase123!');
3130
+ await user.type(screen.getByPlaceholderText('Confirm password'), 'GoodPassphrase123!');
2850
3131
  await user.click(screen.getByRole('button', { name: 'Create Account' }));
2851
3132
  await act(async () => {
2852
3133
  await Promise.resolve();
@@ -2916,7 +3197,8 @@ describe('TaskforceCore Integration', () => {
2916
3197
  render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2Fplans'], children: _jsx(TaskforceCore, {}) }));
2917
3198
  await user.type(await screen.findByPlaceholderText('Email'), 'local@example.com');
2918
3199
  await user.type(screen.getByPlaceholderText('Display name'), 'Local User');
2919
- await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
3200
+ await user.type(screen.getByPlaceholderText('Password'), 'GoodPassphrase123!');
3201
+ await user.type(screen.getByPlaceholderText('Confirm password'), 'GoodPassphrase123!');
2920
3202
  await user.click(screen.getByRole('button', { name: 'Create Account' }));
2921
3203
  await waitFor(() => {
2922
3204
  expect(screen.getByPlaceholderText('Verification token')).toBeInTheDocument();
@@ -2924,6 +3206,156 @@ describe('TaskforceCore Integration', () => {
2924
3206
  expect(screen.queryByRole('button', { name: 'Create Account' })).not.toBeInTheDocument();
2925
3207
  expect(screen.queryByText('Subscription Plans')).not.toBeInTheDocument();
2926
3208
  });
3209
+ it('keeps verified registration on plan selection when commercial onboarding is still blocked', async () => {
3210
+ const user = userEvent.setup();
3211
+ let authenticated = false;
3212
+ const fetchMock = vi.fn((input) => {
3213
+ const url = String(input);
3214
+ if (url.includes('/api/taskforce/auth/runtime-config')) {
3215
+ return jsonResponse({
3216
+ config: {
3217
+ baseUrl: 'https://app.taskforce.example',
3218
+ apiBaseUrl: 'https://app.taskforce.example/api',
3219
+ cloudAuthBaseUrl: 'https://app.taskforce.example',
3220
+ runtimeMode: 'cloud',
3221
+ authSource: 'cloud',
3222
+ workspaceMode: 'multi-cloud',
3223
+ workspaceSwitchingEnabled: true,
3224
+ authRequiredForApi: true
3225
+ }
3226
+ });
3227
+ }
3228
+ if (url.includes('/api/taskforce/auth/session')) {
3229
+ return jsonResponse({
3230
+ authenticated,
3231
+ runtimeMode: 'cloud',
3232
+ authRequiredForApi: true,
3233
+ userId: authenticated ? 'user-verify' : null,
3234
+ workspaceId: authenticated ? 'default' : null,
3235
+ betaAccess: true
3236
+ });
3237
+ }
3238
+ if (url.includes('/api/taskforce/config')) {
3239
+ return jsonResponse({
3240
+ ...mockConfig,
3241
+ runtimeMode: 'cloud',
3242
+ authRequiredForApi: true,
3243
+ workspaceSwitchingEnabled: true,
3244
+ setupState: {
3245
+ globalSetupState: 'present',
3246
+ workspaceSetupState: authenticated ? 'missing' : 'present',
3247
+ runtimeMode: 'cloud',
3248
+ workspaceId: 'default',
3249
+ mode: 'core',
3250
+ forceSetup: false,
3251
+ forceGlobalSetup: false,
3252
+ forceWorkspaceSetup: false
3253
+ }
3254
+ });
3255
+ }
3256
+ if (url.includes('/api/taskforce/workspaces')) {
3257
+ return jsonResponse({
3258
+ success: true,
3259
+ workspaces: [],
3260
+ currentWorkspaceId: null
3261
+ });
3262
+ }
3263
+ if (url.includes('/api/taskforce/account/access-status')) {
3264
+ return jsonResponse({
3265
+ gate: 'plan_selection_required',
3266
+ hasEntitlement: true,
3267
+ canAccessApp: false,
3268
+ canAccessPlans: true,
3269
+ message: 'Choose a plan to continue.'
3270
+ });
3271
+ }
3272
+ if (url.includes('/api/taskforce/account/profile-summary')) {
3273
+ return jsonResponse({
3274
+ entitlementState: 'pending_plan_selection',
3275
+ gate: 'plan_selection_required',
3276
+ message: 'Choose a plan to continue.',
3277
+ workspaceId: null
3278
+ });
3279
+ }
3280
+ if (url.includes('/api/taskforce/public/pricing')) {
3281
+ return jsonResponse({
3282
+ plans: [{
3283
+ planId: 'free',
3284
+ displayName: 'Free',
3285
+ status: 'active',
3286
+ defaultVersion: {
3287
+ planVersionId: 'free-v1',
3288
+ versionNumber: 1,
3289
+ seatLimit: null,
3290
+ metadata: {}
3291
+ },
3292
+ features: [],
3293
+ pricing: {
3294
+ month: {
3295
+ planVersionId: 'free-v1',
3296
+ interval: 'month',
3297
+ pricingType: 'free',
3298
+ stripePriceId: 'free:free-v1:month',
3299
+ displayAmount: '$0.00',
3300
+ unitAmount: 0,
3301
+ currency: 'usd'
3302
+ },
3303
+ year: null
3304
+ }
3305
+ }]
3306
+ });
3307
+ }
3308
+ if (url.includes('/api/taskforce/auth/register')) {
3309
+ return jsonResponse({
3310
+ success: true,
3311
+ workspaceSetupRequired: true,
3312
+ verificationRequired: true,
3313
+ verificationToken: 'verify-token',
3314
+ emailSent: true,
3315
+ planSelectionRequired: true,
3316
+ commercialState: 'pending_plan_selection'
3317
+ });
3318
+ }
3319
+ if (url.includes('/api/taskforce/auth/verify-email/confirm')) {
3320
+ authenticated = true;
3321
+ return jsonResponse({ success: true });
3322
+ }
3323
+ if (url.includes('/api/taskforce/categories'))
3324
+ return jsonResponse(mockCategories);
3325
+ if (url.includes('/api/taskforce/tasks'))
3326
+ return jsonResponse({ tasks: mockTasks });
3327
+ if (url.includes('/api/taskforce/archive'))
3328
+ return jsonResponse({ archived: [] });
3329
+ if (url.includes('/api/taskforce/data-version'))
3330
+ return jsonResponse({ dataVersion: 1 });
3331
+ if (url.includes('/api/taskforce/types'))
3332
+ return jsonResponse({ types: [] });
3333
+ if (url.includes('/api/taskforce/priorities'))
3334
+ return jsonResponse({ priorities: [] });
3335
+ if (url.includes('/api/taskforce/approaches'))
3336
+ return jsonResponse({ approaches: [] });
3337
+ if (url.includes('/api/taskforce/specialists'))
3338
+ return jsonResponse({ specialists: [] });
3339
+ if (url.includes('/api/taskforce/workflow-templates'))
3340
+ return jsonResponse({ templates: [] });
3341
+ return jsonResponse({});
3342
+ });
3343
+ vi.stubGlobal('fetch', fetchMock);
3344
+ render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
3345
+ await user.type(await screen.findByPlaceholderText('Email'), 'verify-gated@example.com');
3346
+ await user.type(screen.getByPlaceholderText('Display name'), 'Verify Gated User');
3347
+ await user.type(screen.getByPlaceholderText('Password'), 'GoodPassphrase123!');
3348
+ await user.type(screen.getByPlaceholderText('Confirm password'), 'GoodPassphrase123!');
3349
+ await user.click(screen.getByRole('button', { name: 'Create Account' }));
3350
+ await waitFor(() => {
3351
+ expect(screen.getByPlaceholderText('Verification token')).toHaveValue('verify-token');
3352
+ });
3353
+ await user.click(screen.getByRole('button', { name: 'Verify Email' }));
3354
+ await waitFor(() => {
3355
+ expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
3356
+ });
3357
+ expect(screen.queryByText('Configure your new workspace.')).not.toBeInTheDocument();
3358
+ });
2927
3359
  it('allows returning to sign-in/register from forgot mode in local runtime', async () => {
2928
3360
  const user = userEvent.setup();
2929
3361
  const fetchMock = vi.fn((url) => {
@@ -2977,6 +3409,162 @@ describe('TaskforceCore Integration', () => {
2977
3409
  expect(screen.getByRole('button', { name: 'Register' })).toBeInTheDocument();
2978
3410
  });
2979
3411
  });
3412
+ it('shows neutral password reset copy when reset delivery was not attempted', async () => {
3413
+ const user = userEvent.setup();
3414
+ const fetchMock = vi.fn((input) => {
3415
+ const url = String(input);
3416
+ if (url.includes('/api/taskforce/auth/runtime-config')) {
3417
+ return Promise.resolve({
3418
+ ok: true,
3419
+ json: () => Promise.resolve({
3420
+ success: true,
3421
+ config: {
3422
+ baseUrl: 'https://app.taskforce.example',
3423
+ apiBaseUrl: 'https://app.taskforce.example/api',
3424
+ cloudAuthBaseUrl: 'https://app.taskforce.example',
3425
+ runtimeMode: 'cloud',
3426
+ authSource: 'cloud',
3427
+ workspaceMode: 'multi-cloud',
3428
+ workspaceSwitchingEnabled: true,
3429
+ cloudAuthViaLocalProxy: false
3430
+ }
3431
+ })
3432
+ });
3433
+ }
3434
+ if (url.includes('/api/taskforce/auth/session')) {
3435
+ return Promise.resolve({
3436
+ ok: true,
3437
+ json: () => Promise.resolve({
3438
+ authenticated: false,
3439
+ runtimeMode: 'cloud',
3440
+ authRequiredForApi: false
3441
+ })
3442
+ });
3443
+ }
3444
+ if (url.includes('/api/taskforce/config')) {
3445
+ return Promise.resolve({
3446
+ ok: true,
3447
+ json: () => Promise.resolve({
3448
+ ...mockConfig,
3449
+ runtimeMode: 'cloud',
3450
+ authRequiredForApi: false,
3451
+ setupState: {
3452
+ globalSetupState: 'present',
3453
+ workspaceSetupState: 'present',
3454
+ runtimeMode: 'cloud',
3455
+ workspaceId: 'workspace-local-active',
3456
+ mode: 'core',
3457
+ forceSetup: false,
3458
+ forceGlobalSetup: false,
3459
+ forceWorkspaceSetup: false
3460
+ }
3461
+ })
3462
+ });
3463
+ }
3464
+ if (url.includes('/api/taskforce/auth/password-reset/request')) {
3465
+ return Promise.resolve({
3466
+ ok: true,
3467
+ json: () => Promise.resolve({
3468
+ success: true,
3469
+ emailSent: false,
3470
+ deliveryAttempted: false,
3471
+ resetToken: null
3472
+ })
3473
+ });
3474
+ }
3475
+ if (url.includes('/api/taskforce/workflow-templates')) {
3476
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
3477
+ }
3478
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
3479
+ });
3480
+ vi.stubGlobal('fetch', fetchMock);
3481
+ render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=forgot&next=%2F'], children: _jsx(TaskforceCore, {}) }));
3482
+ await user.type(await screen.findByPlaceholderText('Email'), 'nobody@example.com');
3483
+ await user.click(screen.getByRole('button', { name: 'Send Reset Link' }));
3484
+ await waitFor(() => {
3485
+ expect(screen.getByText('If that account exists, we sent password reset instructions.')).toBeInTheDocument();
3486
+ });
3487
+ });
3488
+ it('keeps forgot-password users on the request form when reset delivery fails', async () => {
3489
+ const user = userEvent.setup();
3490
+ const fetchMock = vi.fn((input) => {
3491
+ const url = String(input);
3492
+ if (url.includes('/api/taskforce/auth/runtime-config')) {
3493
+ return Promise.resolve({
3494
+ ok: true,
3495
+ json: () => Promise.resolve({
3496
+ success: true,
3497
+ config: {
3498
+ baseUrl: 'https://app.taskforce.example',
3499
+ apiBaseUrl: 'https://app.taskforce.example/api',
3500
+ cloudAuthBaseUrl: 'https://app.taskforce.example',
3501
+ runtimeMode: 'cloud',
3502
+ authSource: 'cloud',
3503
+ workspaceMode: 'multi-cloud',
3504
+ workspaceSwitchingEnabled: true,
3505
+ cloudAuthViaLocalProxy: false
3506
+ }
3507
+ })
3508
+ });
3509
+ }
3510
+ if (url.includes('/api/taskforce/auth/session')) {
3511
+ return Promise.resolve({
3512
+ ok: true,
3513
+ json: () => Promise.resolve({
3514
+ authenticated: false,
3515
+ runtimeMode: 'cloud',
3516
+ authRequiredForApi: false
3517
+ })
3518
+ });
3519
+ }
3520
+ if (url.includes('/api/taskforce/config')) {
3521
+ return Promise.resolve({
3522
+ ok: true,
3523
+ json: () => Promise.resolve({
3524
+ ...mockConfig,
3525
+ runtimeMode: 'cloud',
3526
+ authRequiredForApi: false,
3527
+ setupState: {
3528
+ globalSetupState: 'present',
3529
+ workspaceSetupState: 'present',
3530
+ runtimeMode: 'cloud',
3531
+ workspaceId: 'workspace-local-active',
3532
+ mode: 'core',
3533
+ forceSetup: false,
3534
+ forceGlobalSetup: false,
3535
+ forceWorkspaceSetup: false
3536
+ }
3537
+ })
3538
+ });
3539
+ }
3540
+ if (url.includes('/api/taskforce/auth/password-reset/request')) {
3541
+ return Promise.resolve({
3542
+ ok: true,
3543
+ json: () => Promise.resolve({
3544
+ success: true,
3545
+ emailSent: false,
3546
+ deliveryAttempted: true,
3547
+ emailError: 'SMTP temporary failure',
3548
+ resetToken: null
3549
+ })
3550
+ });
3551
+ }
3552
+ if (url.includes('/api/taskforce/workflow-templates')) {
3553
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
3554
+ }
3555
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
3556
+ });
3557
+ vi.stubGlobal('fetch', fetchMock);
3558
+ render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=forgot&next=%2F'], children: _jsx(TaskforceCore, {}) }));
3559
+ await user.type(await screen.findByPlaceholderText('Email'), 'owner@example.com');
3560
+ await user.click(screen.getByRole('button', { name: 'Send Reset Link' }));
3561
+ await waitFor(() => {
3562
+ expect(screen.getByText('Password reset email failed to send. SMTP temporary failure')).toBeInTheDocument();
3563
+ });
3564
+ expect(screen.getByText('Request a password reset link.')).toBeInTheDocument();
3565
+ expect(screen.queryByText('Enter the password reset token and choose a new password.')).not.toBeInTheDocument();
3566
+ expect(screen.getByRole('button', { name: 'Send Reset Link' })).toBeInTheDocument();
3567
+ });
2980
3568
  it('redirects register users to plans when signup plan configuration blocks account creation', async () => {
2981
3569
  const user = userEvent.setup();
2982
3570
  const fetchMock = vi.fn((input) => {
@@ -3069,7 +3657,8 @@ describe('TaskforceCore Integration', () => {
3069
3657
  render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
3070
3658
  await user.type(await screen.findByPlaceholderText('Email'), 'local@example.com');
3071
3659
  await user.type(screen.getByPlaceholderText('Display name'), 'Local User');
3072
- await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
3660
+ await user.type(screen.getByPlaceholderText('Password'), 'GoodPassphrase123!');
3661
+ await user.type(screen.getByPlaceholderText('Confirm password'), 'GoodPassphrase123!');
3073
3662
  await user.click(screen.getByRole('button', { name: 'Create Account' }));
3074
3663
  await waitFor(() => {
3075
3664
  expect(screen.getByText('Choose a plan to continue.')).toBeInTheDocument();
@@ -3088,39 +3677,295 @@ describe('TaskforceCore Integration', () => {
3088
3677
  })
3089
3678
  });
3090
3679
  }
3091
- if (url.includes('/api/taskforce/config')) {
3680
+ if (url.includes('/api/taskforce/config')) {
3681
+ return Promise.resolve({
3682
+ ok: true,
3683
+ json: () => Promise.resolve({
3684
+ ...mockConfig,
3685
+ runtimeMode: 'cloud',
3686
+ authRequiredForApi: true,
3687
+ setupState: {
3688
+ globalSetupState: 'present',
3689
+ workspaceSetupState: 'present',
3690
+ runtimeMode: 'cloud',
3691
+ workspaceId: 'workspace-1',
3692
+ mode: 'core',
3693
+ forceSetup: false,
3694
+ forceGlobalSetup: false,
3695
+ forceWorkspaceSetup: false
3696
+ }
3697
+ })
3698
+ });
3699
+ }
3700
+ if (url.includes('/api/taskforce/auth/invite/inspect')) {
3701
+ return Promise.resolve({
3702
+ ok: true,
3703
+ json: () => Promise.resolve({
3704
+ success: true,
3705
+ state: 'valid',
3706
+ email: 'existing@example.com',
3707
+ workspaceId: 'workspace-1',
3708
+ inviteeKind: 'existing_user',
3709
+ passwordRequired: false
3710
+ })
3711
+ });
3712
+ }
3713
+ if (url.includes('/api/taskforce/workflow-templates')) {
3714
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
3715
+ }
3716
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
3717
+ });
3718
+ vi.stubGlobal('fetch', fetchMock);
3719
+ render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=invite&token=invite-token&next=%2F'], children: _jsx(TaskforceCore, {}) }));
3720
+ await waitFor(() => {
3721
+ expect(screen.getByText('Sign in to join this workspace.')).toBeInTheDocument();
3722
+ expect(screen.getByDisplayValue('existing@example.com')).toBeInTheDocument();
3723
+ expect(screen.getByPlaceholderText('Password')).toBeInTheDocument();
3724
+ expect(screen.getByRole('button', { name: 'Sign In to Continue' })).toBeInTheDocument();
3725
+ });
3726
+ });
3727
+ it('uses invite join after invite-mode sign-in even if invite reinspection has not refreshed yet', async () => {
3728
+ let authenticated = false;
3729
+ let inviteInspectCalls = 0;
3730
+ const fetchMock = vi.fn((url) => {
3731
+ if (url.includes('/api/taskforce/auth/session')) {
3732
+ return Promise.resolve({
3733
+ ok: true,
3734
+ json: () => Promise.resolve({
3735
+ authenticated,
3736
+ runtimeMode: 'cloud',
3737
+ authRequiredForApi: true,
3738
+ userId: authenticated ? 'user-existing' : 'anonymous',
3739
+ workspaceId: authenticated ? 'workspace-home' : 'default',
3740
+ email: authenticated ? 'existing@example.com' : null
3741
+ })
3742
+ });
3743
+ }
3744
+ if (url.includes('/api/taskforce/config')) {
3745
+ return Promise.resolve({
3746
+ ok: true,
3747
+ json: () => Promise.resolve({
3748
+ ...mockConfig,
3749
+ runtimeMode: 'cloud',
3750
+ authRequiredForApi: true,
3751
+ setupState: {
3752
+ globalSetupState: 'present',
3753
+ workspaceSetupState: 'present',
3754
+ runtimeMode: 'cloud',
3755
+ workspaceId: authenticated ? 'workspace-home' : 'default',
3756
+ mode: 'core',
3757
+ forceSetup: false,
3758
+ forceGlobalSetup: false,
3759
+ forceWorkspaceSetup: false
3760
+ }
3761
+ })
3762
+ });
3763
+ }
3764
+ if (url.includes('/api/taskforce/auth/invite/inspect')) {
3765
+ inviteInspectCalls += 1;
3766
+ if (authenticated && inviteInspectCalls > 1) {
3767
+ return new Promise(() => undefined);
3768
+ }
3769
+ return Promise.resolve({
3770
+ ok: true,
3771
+ json: () => Promise.resolve({
3772
+ success: true,
3773
+ state: 'valid',
3774
+ email: 'existing@example.com',
3775
+ workspaceId: 'workspace-1',
3776
+ inviteeKind: 'existing_user',
3777
+ passwordRequired: false
3778
+ })
3779
+ });
3780
+ }
3781
+ if (url.includes('/api/taskforce/auth/login')) {
3782
+ authenticated = true;
3783
+ return Promise.resolve({
3784
+ ok: true,
3785
+ json: () => Promise.resolve({ success: true })
3786
+ });
3787
+ }
3788
+ if (url.includes('/api/taskforce/auth/invite/join')) {
3789
+ return Promise.resolve({
3790
+ ok: true,
3791
+ json: () => Promise.resolve({ success: true, workspaceId: 'workspace-1' })
3792
+ });
3793
+ }
3794
+ if (url.includes('/api/taskforce/workspaces')) {
3795
+ return Promise.resolve({
3796
+ ok: true,
3797
+ json: () => Promise.resolve({
3798
+ currentWorkspaceId: authenticated ? 'workspace-home' : null,
3799
+ workspaces: authenticated
3800
+ ? [{ id: 'workspace-home', name: 'Home workspace', role: 'owner' }]
3801
+ : []
3802
+ })
3803
+ });
3804
+ }
3805
+ if (url.includes('/api/taskforce/tasks')) {
3806
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: [], config: {} }) });
3807
+ }
3808
+ if (url.includes('/api/taskforce/account/access-status')) {
3809
+ return Promise.resolve({
3810
+ ok: true,
3811
+ json: () => Promise.resolve({
3812
+ success: true,
3813
+ authenticated,
3814
+ commercialState: 'active',
3815
+ planSelectionRequired: false,
3816
+ checkoutPending: false
3817
+ })
3818
+ });
3819
+ }
3820
+ if (url.includes('/api/taskforce/account/profile-summary')) {
3821
+ return Promise.resolve({
3822
+ ok: true,
3823
+ json: () => Promise.resolve({
3824
+ success: true,
3825
+ currentWorkspaceId: authenticated ? 'workspace-home' : null,
3826
+ workspaces: authenticated ? [{ id: 'workspace-home', name: 'Home workspace' }] : []
3827
+ })
3828
+ });
3829
+ }
3830
+ if (url.includes('/api/taskforce/session/workspace')) {
3831
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true, workspaceId: 'workspace-1' }) });
3832
+ }
3833
+ if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
3834
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true, state: null }) });
3835
+ }
3836
+ if (url.includes('/api/taskforce/workflow-templates')) {
3837
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
3838
+ }
3839
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
3840
+ });
3841
+ vi.stubGlobal('fetch', fetchMock);
3842
+ render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=invite&token=invite-token&next=%2F'], children: _jsx(TaskforceCore, {}) }));
3843
+ await waitFor(() => {
3844
+ expect(screen.getByRole('button', { name: 'Sign In to Continue' })).toBeInTheDocument();
3845
+ });
3846
+ await userEvent.type(screen.getByPlaceholderText('Password'), 'ExistingUserPass123!');
3847
+ await userEvent.click(screen.getByTestId('auth-submit-button'));
3848
+ await waitFor(() => {
3849
+ expect(screen.getByText('Signed in. Review the invite details to continue.')).toBeInTheDocument();
3850
+ expect(screen.getByRole('button', { name: 'Join Workspace' })).toBeInTheDocument();
3851
+ });
3852
+ await userEvent.click(screen.getByTestId('auth-submit-button'));
3853
+ await waitFor(() => {
3854
+ expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('/api/taskforce/auth/invite/join'))).toBe(true);
3855
+ });
3856
+ expect(fetchMock.mock.calls.filter((call) => String(call[0]).includes('/api/taskforce/auth/login'))).toHaveLength(1);
3857
+ });
3858
+ it('keeps existing-user invites recoverable when the wrong account signs in first', async () => {
3859
+ let authenticated = false;
3860
+ let sessionEmail = null;
3861
+ const fetchMock = vi.fn((url, init) => {
3862
+ if (url.includes('/api/taskforce/auth/session')) {
3863
+ return Promise.resolve({
3864
+ ok: true,
3865
+ json: () => Promise.resolve({
3866
+ authenticated,
3867
+ runtimeMode: 'cloud',
3868
+ authRequiredForApi: true,
3869
+ userId: authenticated ? 'user-other' : 'anonymous',
3870
+ workspaceId: authenticated ? 'workspace-home' : 'default',
3871
+ email: sessionEmail
3872
+ })
3873
+ });
3874
+ }
3875
+ if (url.includes('/api/taskforce/config')) {
3876
+ return Promise.resolve({
3877
+ ok: true,
3878
+ json: () => Promise.resolve({
3879
+ ...mockConfig,
3880
+ runtimeMode: 'cloud',
3881
+ authRequiredForApi: true,
3882
+ setupState: {
3883
+ globalSetupState: 'present',
3884
+ workspaceSetupState: 'present',
3885
+ runtimeMode: 'cloud',
3886
+ workspaceId: authenticated ? 'workspace-home' : 'default',
3887
+ mode: 'core',
3888
+ forceSetup: false,
3889
+ forceGlobalSetup: false,
3890
+ forceWorkspaceSetup: false
3891
+ }
3892
+ })
3893
+ });
3894
+ }
3895
+ if (url.includes('/api/taskforce/auth/invite/inspect')) {
3896
+ return Promise.resolve({
3897
+ ok: true,
3898
+ json: () => Promise.resolve({
3899
+ success: true,
3900
+ state: 'valid',
3901
+ email: 'existing@example.com',
3902
+ workspaceId: 'workspace-1',
3903
+ inviteeKind: 'existing_user',
3904
+ passwordRequired: false
3905
+ })
3906
+ });
3907
+ }
3908
+ if (url.includes('/api/taskforce/auth/login')) {
3909
+ const body = typeof init?.body === 'string' ? JSON.parse(init.body) : {};
3910
+ authenticated = true;
3911
+ sessionEmail = String(body?.email || '').trim().toLowerCase() || null;
3912
+ return Promise.resolve({
3913
+ ok: true,
3914
+ json: () => Promise.resolve({ success: true })
3915
+ });
3916
+ }
3917
+ if (url.includes('/api/taskforce/auth/invite/join')) {
3918
+ return Promise.resolve({
3919
+ ok: false,
3920
+ json: () => Promise.resolve({
3921
+ success: false,
3922
+ code: 'INVITE_ACCOUNT_MISMATCH',
3923
+ error: 'Sign in with the invited account to join this workspace.'
3924
+ })
3925
+ });
3926
+ }
3927
+ if (url.includes('/api/taskforce/workspaces')) {
3928
+ return Promise.resolve({
3929
+ ok: true,
3930
+ json: () => Promise.resolve({
3931
+ currentWorkspaceId: authenticated ? 'workspace-home' : null,
3932
+ workspaces: authenticated
3933
+ ? [{ id: 'workspace-home', name: 'Home workspace', role: 'owner' }]
3934
+ : []
3935
+ })
3936
+ });
3937
+ }
3938
+ if (url.includes('/api/taskforce/tasks')) {
3939
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: [], config: {} }) });
3940
+ }
3941
+ if (url.includes('/api/taskforce/account/access-status')) {
3092
3942
  return Promise.resolve({
3093
3943
  ok: true,
3094
3944
  json: () => Promise.resolve({
3095
- ...mockConfig,
3096
- runtimeMode: 'cloud',
3097
- authRequiredForApi: true,
3098
- setupState: {
3099
- globalSetupState: 'present',
3100
- workspaceSetupState: 'present',
3101
- runtimeMode: 'cloud',
3102
- workspaceId: 'workspace-1',
3103
- mode: 'core',
3104
- forceSetup: false,
3105
- forceGlobalSetup: false,
3106
- forceWorkspaceSetup: false
3107
- }
3945
+ success: true,
3946
+ authenticated,
3947
+ commercialState: 'active',
3948
+ planSelectionRequired: false,
3949
+ checkoutPending: false
3108
3950
  })
3109
3951
  });
3110
3952
  }
3111
- if (url.includes('/api/taskforce/auth/invite/inspect')) {
3953
+ if (url.includes('/api/taskforce/account/profile-summary')) {
3112
3954
  return Promise.resolve({
3113
3955
  ok: true,
3114
3956
  json: () => Promise.resolve({
3115
3957
  success: true,
3116
- state: 'valid',
3117
- email: 'existing@example.com',
3118
- workspaceId: 'workspace-1',
3119
- inviteeKind: 'existing_user',
3120
- passwordRequired: false
3958
+ currentWorkspaceId: authenticated ? 'workspace-home' : null,
3959
+ workspaces: authenticated ? [{ id: 'workspace-home', name: 'Home workspace' }] : []
3121
3960
  })
3122
3961
  });
3123
3962
  }
3963
+ if (url.includes('/api/taskforce/session/workspace')) {
3964
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true, workspaceId: 'workspace-home' }) });
3965
+ }
3966
+ if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
3967
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true, state: null }) });
3968
+ }
3124
3969
  if (url.includes('/api/taskforce/workflow-templates')) {
3125
3970
  return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
3126
3971
  }
@@ -3129,11 +3974,20 @@ describe('TaskforceCore Integration', () => {
3129
3974
  vi.stubGlobal('fetch', fetchMock);
3130
3975
  render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=invite&token=invite-token&next=%2F'], children: _jsx(TaskforceCore, {}) }));
3131
3976
  await waitFor(() => {
3132
- expect(screen.getByText('Sign in to join this workspace.')).toBeInTheDocument();
3977
+ expect(screen.getByRole('button', { name: 'Sign In to Continue' })).toBeInTheDocument();
3978
+ });
3979
+ await userEvent.clear(screen.getByPlaceholderText('Email'));
3980
+ await userEvent.type(screen.getByPlaceholderText('Email'), 'other@example.com');
3981
+ await userEvent.type(screen.getByPlaceholderText('Password'), 'OtherUserPass123!');
3982
+ await userEvent.click(screen.getByTestId('auth-submit-button'));
3983
+ await waitFor(() => {
3984
+ expect(screen.getByText('Sign in with the invited account to join this workspace.')).toBeInTheDocument();
3133
3985
  expect(screen.getByDisplayValue('existing@example.com')).toBeInTheDocument();
3134
3986
  expect(screen.getByPlaceholderText('Password')).toBeInTheDocument();
3135
- expect(screen.getByRole('button', { name: 'Sign In to Continue' })).toBeInTheDocument();
3987
+ expect(screen.queryByRole('button', { name: 'Join Workspace' })).not.toBeInTheDocument();
3136
3988
  });
3989
+ expect(fetchMock.mock.calls.filter((call) => String(call[0]).includes('/api/taskforce/auth/login'))).toHaveLength(1);
3990
+ expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('/api/taskforce/auth/invite/join'))).toBe(false);
3137
3991
  });
3138
3992
  it('shows password creation for new-user invites', async () => {
3139
3993
  const fetchMock = vi.fn((url) => {
@@ -3190,6 +4044,7 @@ describe('TaskforceCore Integration', () => {
3190
4044
  await waitFor(() => {
3191
4045
  expect(screen.getByText('Create your account to join this workspace.')).toBeInTheDocument();
3192
4046
  expect(screen.getByPlaceholderText('Create password')).toBeInTheDocument();
4047
+ expect(screen.getByPlaceholderText('Confirm password')).toBeInTheDocument();
3193
4048
  expect(screen.getByRole('button', { name: 'Create Account and Join' })).toBeInTheDocument();
3194
4049
  });
3195
4050
  });
@@ -3887,9 +4742,229 @@ describe('TaskforceCore Integration', () => {
3887
4742
  expect(workspaceSyncStateBody.stateKey).toBe('workspace-sync');
3888
4743
  expect(workspaceSyncStateBody.patch?.enabled).toBe(true);
3889
4744
  expect(workspaceSyncStateBody.patch?.phase).toBe('attach-cloud');
4745
+ expect(workspaceSyncStateBody.patch?.setupIntent).toBe('attach-cloud-import');
3890
4746
  expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('https://cloud.example.com/api/taskforce/workspaces'))).toBe(true);
3891
4747
  expect(screen.queryByTitle(/Account/i)).not.toBeInTheDocument();
3892
4748
  });
4749
+ it('keeps attach-cloud import blocking after refresh when setup intent is persisted', async () => {
4750
+ const fetchMock = vi.fn((url) => {
4751
+ if (url.includes('/api/taskforce/auth/runtime-config')) {
4752
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'cloud' } }) });
4753
+ }
4754
+ if (url.includes('/api/taskforce/auth/session')) {
4755
+ return Promise.resolve({
4756
+ ok: true,
4757
+ json: () => Promise.resolve({
4758
+ authenticated: true,
4759
+ runtimeMode: 'local',
4760
+ authRequiredForApi: false
4761
+ })
4762
+ });
4763
+ }
4764
+ if (url.includes('/api/taskforce/config')) {
4765
+ return Promise.resolve({
4766
+ ok: true,
4767
+ json: () => Promise.resolve({
4768
+ ...mockConfig,
4769
+ runtimeMode: 'local',
4770
+ authRequiredForApi: false,
4771
+ workspaceId: 'workspace-cloud-123',
4772
+ setupState: {
4773
+ globalSetupState: 'present',
4774
+ workspaceSetupState: 'missing',
4775
+ runtimeMode: 'local',
4776
+ workspaceId: 'workspace-cloud-123',
4777
+ mode: 'core',
4778
+ forceSetup: false,
4779
+ forceGlobalSetup: false,
4780
+ forceWorkspaceSetup: true,
4781
+ workspace: null,
4782
+ suggestedWorkspaceName: 'Cloud Project 123'
4783
+ },
4784
+ runtimeCapabilities: {
4785
+ runtimeMode: 'local',
4786
+ supportsCloudAuth: true,
4787
+ supportsAdminManagedSetupOverrides: false,
4788
+ supportsPackageScriptSetupOverrides: true
4789
+ }
4790
+ })
4791
+ });
4792
+ }
4793
+ if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
4794
+ return Promise.resolve({
4795
+ ok: true,
4796
+ json: () => Promise.resolve({
4797
+ success: true,
4798
+ state: {
4799
+ version: 2,
4800
+ enabled: true,
4801
+ phase: 'attach-cloud',
4802
+ setupIntent: 'attach-cloud-import',
4803
+ pullCursor: null,
4804
+ lastPullAt: null,
4805
+ lastPushAt: null,
4806
+ lastSyncedAt: null
4807
+ }
4808
+ })
4809
+ });
4810
+ }
4811
+ if (url.includes('https://cloud.example.com/api/taskforce/workspaces')) {
4812
+ return Promise.resolve({
4813
+ ok: true,
4814
+ json: () => Promise.resolve({
4815
+ success: true,
4816
+ workspaces: [
4817
+ {
4818
+ id: 'workspace-cloud-123',
4819
+ name: 'Cloud Project 123',
4820
+ description: 'Cloud desc'
4821
+ }
4822
+ ]
4823
+ })
4824
+ });
4825
+ }
4826
+ if (url.includes('/api/taskforce/ui-state?key=app')) {
4827
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true, state: {} }) });
4828
+ }
4829
+ if (url.includes('/api/taskforce/tasks')) {
4830
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
4831
+ }
4832
+ if (url.includes('/api/taskforce/archive')) {
4833
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ archived: [] }) });
4834
+ }
4835
+ if (url.includes('/api/taskforce/categories')) {
4836
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(mockCategories) });
4837
+ }
4838
+ if (url.includes('/api/taskforce/global-settings')) {
4839
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
4840
+ }
4841
+ if (url.includes('/api/taskforce/data-version')) {
4842
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ dataVersion: 1 }) });
4843
+ }
4844
+ if (url.includes('/api/taskforce/types'))
4845
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ types: [] }) });
4846
+ if (url.includes('/api/taskforce/priorities'))
4847
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ priorities: [] }) });
4848
+ if (url.includes('/api/taskforce/approaches'))
4849
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ approaches: [] }) });
4850
+ if (url.includes('/api/taskforce/specialists'))
4851
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
4852
+ if (url.includes('/api/taskforce/workflow-templates'))
4853
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
4854
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
4855
+ });
4856
+ vi.stubGlobal('fetch', fetchMock);
4857
+ render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
4858
+ await waitFor(() => {
4859
+ expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
4860
+ });
4861
+ await waitFor(() => {
4862
+ expect(screen.getByText('Importing "Cloud Project 123" from cloud into local.')).toBeInTheDocument();
4863
+ });
4864
+ });
4865
+ it('clears persisted attach-cloud import intent after the first successful import', async () => {
4866
+ const uiStatePosts = [];
4867
+ const fetchMock = vi.fn((url, init) => {
4868
+ if (url.includes('/api/taskforce/auth/runtime-config')) {
4869
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'cloud' } }) });
4870
+ }
4871
+ if (url.includes('/api/taskforce/auth/session')) {
4872
+ return Promise.resolve({
4873
+ ok: true,
4874
+ json: () => Promise.resolve({
4875
+ authenticated: true,
4876
+ runtimeMode: 'local',
4877
+ authRequiredForApi: false
4878
+ })
4879
+ });
4880
+ }
4881
+ if (url.includes('/api/taskforce/config')) {
4882
+ return Promise.resolve({
4883
+ ok: true,
4884
+ json: () => Promise.resolve({
4885
+ ...mockConfig,
4886
+ runtimeMode: 'local',
4887
+ authRequiredForApi: false,
4888
+ workspaceId: 'workspace-cloud-123'
4889
+ })
4890
+ });
4891
+ }
4892
+ if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
4893
+ return Promise.resolve({
4894
+ ok: true,
4895
+ json: () => Promise.resolve({
4896
+ success: true,
4897
+ state: {
4898
+ version: 2,
4899
+ enabled: true,
4900
+ phase: 'active',
4901
+ setupIntent: 'attach-cloud-import',
4902
+ pullCursor: 'cursor-1',
4903
+ lastPullAt: '2026-04-15T12:00:00.000Z',
4904
+ lastPushAt: null,
4905
+ lastSyncedAt: '2026-04-15T12:00:00.000Z'
4906
+ }
4907
+ })
4908
+ });
4909
+ }
4910
+ if (url.includes('/api/taskforce/ui-state') && String(init?.method || '').toUpperCase() === 'POST') {
4911
+ uiStatePosts.push(JSON.parse(String(init?.body || '{}')));
4912
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
4913
+ }
4914
+ if (url.includes('https://cloud.example.com/api/taskforce/workspaces')) {
4915
+ return Promise.resolve({
4916
+ ok: true,
4917
+ json: () => Promise.resolve({
4918
+ success: true,
4919
+ workspaces: [
4920
+ {
4921
+ id: 'workspace-cloud-123',
4922
+ name: 'Cloud Project 123',
4923
+ description: 'Cloud desc'
4924
+ }
4925
+ ]
4926
+ })
4927
+ });
4928
+ }
4929
+ if (url.includes('/api/taskforce/ui-state?key=app')) {
4930
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true, state: {} }) });
4931
+ }
4932
+ if (url.includes('/api/taskforce/tasks')) {
4933
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
4934
+ }
4935
+ if (url.includes('/api/taskforce/archive')) {
4936
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ archived: [] }) });
4937
+ }
4938
+ if (url.includes('/api/taskforce/categories')) {
4939
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(mockCategories) });
4940
+ }
4941
+ if (url.includes('/api/taskforce/global-settings')) {
4942
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
4943
+ }
4944
+ if (url.includes('/api/taskforce/data-version')) {
4945
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ dataVersion: 1 }) });
4946
+ }
4947
+ if (url.includes('/api/taskforce/types'))
4948
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ types: [] }) });
4949
+ if (url.includes('/api/taskforce/priorities'))
4950
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ priorities: [] }) });
4951
+ if (url.includes('/api/taskforce/approaches'))
4952
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ approaches: [] }) });
4953
+ if (url.includes('/api/taskforce/specialists'))
4954
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
4955
+ if (url.includes('/api/taskforce/workflow-templates'))
4956
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
4957
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
4958
+ });
4959
+ vi.stubGlobal('fetch', fetchMock);
4960
+ render(_jsx(MemoryRouter, { initialEntries: ['/setup?step=workspace&source=cloud'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
4961
+ await waitFor(() => {
4962
+ expect(screen.getByText('Test Project')).toBeInTheDocument();
4963
+ });
4964
+ await waitFor(() => {
4965
+ expect(uiStatePosts.some((entry) => entry?.stateKey === 'workspace-sync' && entry?.patch?.setupIntent === null)).toBe(true);
4966
+ });
4967
+ });
3893
4968
  it('shows sign-in and register actions on signed-out cloud workspace setup and disables connect', async () => {
3894
4969
  const fetchMock = vi.fn((url) => {
3895
4970
  if (url.includes('/api/taskforce/auth/session')) {
@@ -4214,10 +5289,11 @@ describe('TaskforceCore Integration', () => {
4214
5289
  expect(screen.getByRole('checkbox', { name: /sync to cloud after setup/i })).toBeInTheDocument();
4215
5290
  });
4216
5291
  await user.click(screen.getByRole('checkbox', { name: /sync to cloud after setup/i }));
4217
- const nameInput = screen.getByPlaceholderText('Project name');
5292
+ const nameInput = screen.getByPlaceholderText('Workspace name');
4218
5293
  await user.clear(nameInput);
4219
5294
  await user.type(nameInput, 'Local Project');
4220
- await user.click(screen.getByRole('button', { name: 'Save Project Setup' }));
5295
+ await user.click(screen.getByRole('button', { name: 'Continue' }));
5296
+ await user.click(screen.getByRole('button', { name: 'Create Workspace' }));
4221
5297
  await waitFor(() => {
4222
5298
  expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
4223
5299
  });
@@ -4231,10 +5307,11 @@ describe('TaskforceCore Integration', () => {
4231
5307
  return body?.stateKey === 'workspace-sync';
4232
5308
  });
4233
5309
  const workspaceSyncStateBody = JSON.parse(String(workspaceSyncStateCall?.[1]?.body || '{}'));
4234
- expect(workspaceSyncStateBody.patch?.cloudSyncEnabled).toBe(true);
4235
- expect(workspaceSyncStateBody.patch?.sourceOfTruth).toBe('local');
4236
- // I6: onboarding now starts as false; the orchestrator marks it true after first pull.
4237
- expect(workspaceSyncStateBody.patch?.onboardingCompleted).toBe(false);
5310
+ expect(workspaceSyncStateBody.patch?.version).toBe(2);
5311
+ expect(workspaceSyncStateBody.patch?.enabled).toBe(true);
5312
+ expect(workspaceSyncStateBody.patch?.phase).toBe('provision-local');
5313
+ expect(workspaceSyncStateBody.patch?.setupIntent).toBeNull();
5314
+ expect(workspaceSyncStateBody.patch?.pullCursor).toBeNull();
4238
5315
  });
4239
5316
  it('creates a new cloud workspace only after guided setup submit', async () => {
4240
5317
  const user = userEvent.setup();
@@ -4708,6 +5785,15 @@ describe('TaskforceCore Integration', () => {
4708
5785
  expect(useTaskforceSource).toContain("|| path.startsWith('/api/taskforce/account/')");
4709
5786
  expect(useTaskforceSource).toContain("|| path.startsWith('/api/taskforce/billing/')");
4710
5787
  });
5788
+ it('keeps plans on a dedicated surface while preserving the faster exit flow', () => {
5789
+ const standaloneLayoutPath = path.join(process.cwd(), 'src/components/views/StandaloneLayout.tsx');
5790
+ const standaloneLayoutSource = fs.readFileSync(standaloneLayoutPath, 'utf8');
5791
+ expect(standaloneLayoutSource).toContain('{!isPlansScreen && <WorkspaceHeaderActions actions={universalFilterActions} />}');
5792
+ expect(standaloneLayoutSource).toContain('{!isPlansScreen && showFilters && workspaceFilterBar}');
5793
+ expect(standaloneLayoutSource).not.toMatch(/\{isPlansScreen \? \([\s\S]*\{workspaceToolRail\}[\s\S]*<PlansPage/);
5794
+ expect(standaloneLayoutSource).toContain('closePlansScreen();');
5795
+ expect(standaloneLayoutSource).toContain('void (async () => {');
5796
+ });
4711
5797
  it('uses shared /login flow for local sign in instead of a duplicate modal', async () => {
4712
5798
  const standaloneLayoutPath = path.join(process.cwd(), 'src/components/views/StandaloneLayout.tsx');
4713
5799
  const standaloneLayoutSource = fs.readFileSync(standaloneLayoutPath, 'utf8');
@@ -4892,11 +5978,12 @@ describe('TaskforceCore Integration', () => {
4892
5978
  if (url.includes('/api/taskforce/workflow-templates')) {
4893
5979
  return jsonResponse({ templates: [] });
4894
5980
  }
4895
- if (url.includes('/api/taskforce/initiatives')) {
4896
- return jsonResponse([]);
4897
- }
4898
- if (url.includes('/api/taskforce/workstreams')) {
4899
- return jsonResponse([]);
5981
+ if (url.includes('/api/taskforce/planning/bootstrap')) {
5982
+ return jsonResponse({
5983
+ initiatives: [],
5984
+ workstreams: [],
5985
+ workstreamTaskSummaries: [],
5986
+ });
4900
5987
  }
4901
5988
  return jsonResponse({});
4902
5989
  });
@@ -5238,9 +6325,9 @@ describe('TaskforceCore Integration', () => {
5238
6325
  occurredAt: '2026-03-15T14:28:17.000Z',
5239
6326
  eventType: 'apply',
5240
6327
  status: 'error',
5241
- errorMessage: 'Detected asset reference number mismatch during normal sync.',
6328
+ errorMessage: 'Detected task reference number mismatch during normal sync.',
5242
6329
  details: {
5243
- path: 'workspaces/workspace-local-active/assets/images/hero.png',
6330
+ taskId: 'task-1',
5244
6331
  existingReferenceNumber: 12,
5245
6332
  incomingReferenceNumber: 18
5246
6333
  }
@@ -6474,6 +7561,7 @@ describe('TaskforceCore Integration', () => {
6474
7561
  const user = userEvent.setup();
6475
7562
  let savedDisplayName = 'Original Name';
6476
7563
  let savedAvatarUrl = '';
7564
+ const cloudAuthBaseUrl = 'https://performance-app.taskforcehq.ai';
6477
7565
  const fetchMock = vi.fn((input, init) => {
6478
7566
  const url = String(input);
6479
7567
  if (url.includes('/api/taskforce/auth/runtime-config')) {
@@ -6513,6 +7601,9 @@ describe('TaskforceCore Integration', () => {
6513
7601
  ...mockConfig,
6514
7602
  runtimeMode: 'local',
6515
7603
  authRequiredForApi: false,
7604
+ apiBaseUrl: cloudAuthBaseUrl,
7605
+ cloudAuthBaseUrl,
7606
+ cloudAuthViaLocalProxy: true,
6516
7607
  workspaceId: 'workspace-local-active',
6517
7608
  setupState: {
6518
7609
  globalSetupState: 'present',
@@ -6548,13 +7639,12 @@ describe('TaskforceCore Integration', () => {
6548
7639
  return jsonResponse({
6549
7640
  success: true,
6550
7641
  draftId: 'avatar-draft-1',
6551
- uploadUrl: 'https://upload.example.com/avatar-draft-1',
6552
7642
  relativePath: 'users/user-1/profile/avatar/avatar-draft-1-avatar.png',
6553
7643
  path: '/api/taskforce/context/users%2Fuser-1%2Fprofile%2Favatar%2Favatar-draft-1-avatar.png'
6554
7644
  });
6555
7645
  }
6556
- if (url.includes('https://upload.example.com/avatar-draft-1')) {
6557
- return Promise.resolve({ ok: true, status: 200, json: async () => ({}) });
7646
+ if (url.includes('/api/taskforce/auth/profile/avatar/upload')) {
7647
+ return jsonResponse({ success: true, draftId: 'avatar-draft-1' });
6558
7648
  }
6559
7649
  if (url.includes('/api/taskforce/auth/profile/avatar/finalize')) {
6560
7650
  return jsonResponse({
@@ -6595,8 +7685,21 @@ describe('TaskforceCore Integration', () => {
6595
7685
  const displayNameInput = within(modal).getByPlaceholderText('Display name');
6596
7686
  await user.clear(displayNameInput);
6597
7687
  await user.type(displayNameInput, 'Updated Name');
6598
- const avatarInput = within(modal).getByLabelText('Upload profile photo');
7688
+ const getContextSpy = vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue({
7689
+ drawImage: () => undefined
7690
+ });
7691
+ const toBlobSpy = vi.spyOn(HTMLCanvasElement.prototype, 'toBlob').mockImplementation(function toBlob(callback, type) {
7692
+ callback(new Blob([new Uint8Array([1, 2, 3, 4])], { type: type || 'image/webp' }));
7693
+ });
7694
+ await user.click(within(modal).getByLabelText('Edit profile photo'));
7695
+ const avatarManager = await screen.findByText('Edit Profile Photo');
7696
+ const avatarManagerModal = avatarManager.closest(`.${modalStyles.modal}`);
7697
+ const avatarInput = within(avatarManagerModal).getByLabelText('Choose profile photo');
6599
7698
  await user.upload(avatarInput, new File(['avatar'], 'avatar.png', { type: 'image/png' }));
7699
+ await user.click(within(avatarManagerModal).getByRole('button', { name: 'Apply Photo' }));
7700
+ await waitFor(() => {
7701
+ expect(screen.queryByText('Edit Profile Photo')).not.toBeInTheDocument();
7702
+ });
6600
7703
  await user.click(within(modal).getByRole('button', { name: 'Save Profile' }));
6601
7704
  await waitFor(() => {
6602
7705
  const profileCall = fetchMock.mock.calls.find((call) => {
@@ -6608,6 +7711,17 @@ describe('TaskforceCore Integration', () => {
6608
7711
  expect(payload.displayName).toBe('Updated Name');
6609
7712
  expect(payload.avatarDraftId).toBe('avatar-draft-1');
6610
7713
  });
7714
+ expect(fetchMock).toHaveBeenCalledWith('/api/taskforce/auth/profile/avatar/init', expect.objectContaining({ method: 'POST' }));
7715
+ const avatarUploadCall = fetchMock.mock.calls.find((call) => String(call[0]) === '/api/taskforce/auth/profile/avatar/upload');
7716
+ expect(avatarUploadCall).toBeTruthy();
7717
+ expect(avatarUploadCall?.[1]).toEqual(expect.objectContaining({ method: 'POST' }));
7718
+ const avatarUploadHeaders = new Headers(avatarUploadCall?.[1]?.headers);
7719
+ expect(avatarUploadHeaders.get('Content-Type')).toBe('image/webp');
7720
+ expect(avatarUploadHeaders.get('x-taskforce-avatar-draft-id')).toBe('avatar-draft-1');
7721
+ expect(fetchMock).toHaveBeenCalledWith('/api/taskforce/auth/profile/avatar/finalize', expect.objectContaining({ method: 'POST' }));
7722
+ expect(fetchMock.mock.calls.some((call) => String(call[0]).startsWith(`${cloudAuthBaseUrl}/api/taskforce/auth/profile/avatar/`))).toBe(false);
7723
+ getContextSpy.mockRestore();
7724
+ toBlobSpy.mockRestore();
6611
7725
  await waitFor(() => {
6612
7726
  expect(screen.queryByText('Edit Profile')).not.toBeInTheDocument();
6613
7727
  });