@taskforcehq/taskforce 0.3.303 → 0.3.305

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 (237) hide show
  1. package/README.md +140 -247
  2. package/dist/Taskforce.module.css +102 -5
  3. package/dist/TaskforceCore.js +635 -238
  4. package/dist/TaskforceCore.test.js +2243 -495
  5. package/dist/annotatedAttachments/service.d.ts +1 -1
  6. package/dist/annotatedAttachments/types.d.ts +1 -1
  7. package/dist/compat/workspaceSyncCompat.js +6 -0
  8. package/dist/components/features/AgentsModule.js +1 -1
  9. package/dist/components/features/AgentsModule.test.js +1 -1
  10. package/dist/components/features/DocumentIndex.d.ts +1 -1
  11. package/dist/components/features/DocumentIndex.js +1 -1
  12. package/dist/components/features/DocumentViewer.d.ts +1 -1
  13. package/dist/components/features/DocumentViewer.js +32 -9
  14. package/dist/components/features/DocumentWorkspace.d.ts +5 -22
  15. package/dist/components/features/DocumentWorkspace.js +12 -155
  16. package/dist/components/features/DocumentWorkspace.test.js +193 -6
  17. package/dist/components/features/TaskSettings.js +87 -41
  18. package/dist/components/features/TaskSettings.test.js +180 -40
  19. package/dist/components/features/documentWorkspaceModel.d.ts +24 -0
  20. package/dist/components/features/documentWorkspaceModel.js +57 -0
  21. package/dist/components/features/useDocumentWorkspaceController.d.ts +34 -0
  22. package/dist/components/features/useDocumentWorkspaceController.js +113 -0
  23. package/dist/components/task/TaskActionHeader.d.ts +2 -1
  24. package/dist/components/task/TaskActionHeader.js +3 -3
  25. package/dist/components/task/TaskActionHeader.test.js +8 -0
  26. package/dist/components/task/TaskCard.js +16 -1
  27. package/dist/components/task/TaskCard.test.js +31 -0
  28. package/dist/components/task/TaskForm.d.ts +9 -8
  29. package/dist/components/task/TaskForm.js +374 -219
  30. package/dist/components/task/TaskForm.test.js +482 -361
  31. package/dist/components/task/TaskKanban.d.ts +1 -1
  32. package/dist/components/task/TaskKanban.js +33 -161
  33. package/dist/components/task/TaskStatusActions.d.ts +6 -1
  34. package/dist/components/task/TaskStatusActions.js +8 -3
  35. package/dist/components/task/TaskStatusActions.test.js +9 -0
  36. package/dist/components/task/taskKanbanDropRules.d.ts +58 -0
  37. package/dist/components/task/taskKanbanDropRules.js +187 -0
  38. package/dist/components/task/taskKanbanDropRules.test.d.ts +1 -0
  39. package/dist/components/task/taskKanbanDropRules.test.js +114 -0
  40. package/dist/components/ui/Modal.d.ts +1 -1
  41. package/dist/components/ui/Modal.js +1 -0
  42. package/dist/components/views/AppShellHeader.js +5 -1
  43. package/dist/components/views/AppShellHeader.test.js +4 -0
  44. package/dist/components/views/PlanComparisonPage.d.ts +3 -2
  45. package/dist/components/views/PlanComparisonPage.js +9 -10
  46. package/dist/components/views/PlanComparisonPage.test.js +55 -41
  47. package/dist/components/views/PlansPage.d.ts +5 -1
  48. package/dist/components/views/PlansPage.js +150 -81
  49. package/dist/components/views/PlansPage.test.js +367 -41
  50. package/dist/components/views/StandaloneLayout.js +135 -54
  51. package/dist/components/views/WidgetView.js +11 -2
  52. package/dist/components/views/panels/FilterToolbar.test.js +6 -4
  53. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +2 -14
  54. package/dist/components/views/standalone/modals/AccountHubModal.js +12 -14
  55. package/dist/components/views/standalone/modals/AccountHubModal.test.js +24 -1
  56. package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
  57. package/dist/config/envSchema.js +1 -1
  58. package/dist/core/AiProfileService.d.ts +6 -1
  59. package/dist/core/AiProfileService.js +161 -16
  60. package/dist/core/AiProfileService.test.js +3 -1
  61. package/dist/core/AiProfiles.test.js +200 -0
  62. package/dist/core/AuthTokenService.test.d.ts +1 -0
  63. package/dist/core/AuthTokenService.test.js +78 -0
  64. package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
  65. package/dist/core/EntitlementsPolicy.test.js +75 -13
  66. package/dist/core/PlanEntitlementService.d.ts +9 -1
  67. package/dist/core/PlanEntitlementService.js +144 -19
  68. package/dist/core/PlanVersionPolicy.test.js +19 -26
  69. package/dist/core/SignupMonetizationSettings.test.js +75 -15
  70. package/dist/core/SystemAdmin.test.js +212 -56
  71. package/dist/core/TaskActivityService.d.ts +4 -1
  72. package/dist/core/TaskActivityService.js +45 -0
  73. package/dist/core/TaskAuditTimeline.test.js +52 -1
  74. package/dist/core/TaskTaxonomyValidation.test.js +53 -0
  75. package/dist/core/Taskforce.d.ts +18 -11
  76. package/dist/core/Taskforce.js +93 -12
  77. package/dist/core/WorkspacePlanMode.test.js +73 -8
  78. package/dist/core/shared.d.ts +2 -0
  79. package/dist/core/shared.js +11 -0
  80. package/dist/documentReviews/service.d.ts +1 -1
  81. package/dist/documentReviews/types.d.ts +1 -1
  82. package/dist/hooks/auth/useTaskforceAuthBootstrap.d.ts +48 -0
  83. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +219 -0
  84. package/dist/hooks/sync/auth.d.ts +37 -0
  85. package/dist/hooks/sync/auth.js +63 -0
  86. package/dist/hooks/sync/bootstrap.d.ts +34 -0
  87. package/dist/hooks/sync/bootstrap.js +52 -0
  88. package/dist/hooks/sync/orchestratorShared.d.ts +36 -0
  89. package/dist/hooks/sync/orchestratorShared.js +209 -0
  90. package/dist/hooks/sync/orchestratorShared.test.d.ts +1 -0
  91. package/dist/hooks/sync/orchestratorShared.test.js +49 -0
  92. package/dist/hooks/sync/realtime.d.ts +25 -0
  93. package/dist/hooks/sync/realtime.js +60 -0
  94. package/dist/hooks/sync/recovery.d.ts +69 -0
  95. package/dist/hooks/sync/recovery.js +118 -0
  96. package/dist/hooks/sync/transfers.d.ts +123 -0
  97. package/dist/hooks/sync/transfers.js +435 -0
  98. package/dist/hooks/sync/useDataVersionRefresh.d.ts +16 -0
  99. package/dist/hooks/sync/useDataVersionRefresh.js +73 -0
  100. package/dist/hooks/ui/useResourceExport.d.ts +3 -1
  101. package/dist/hooks/ui/useResourceExport.js +5 -2
  102. package/dist/hooks/ui/useWorkflowTemplates.js +11 -6
  103. package/dist/hooks/useRealtimeSync.js +2 -1
  104. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +8 -0
  105. package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +1 -0
  106. package/dist/hooks/useSyncOrchestrator.context-assets.test.js +228 -0
  107. package/dist/hooks/useSyncOrchestrator.d.ts +12 -8
  108. package/dist/hooks/useSyncOrchestrator.js +388 -879
  109. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +641 -6
  110. package/dist/hooks/useTaskMutations.d.ts +2 -1
  111. package/dist/hooks/useTaskMutations.js +57 -7
  112. package/dist/hooks/useTaskMutations.test.js +176 -0
  113. package/dist/hooks/useTaskforce.d.ts +13 -4
  114. package/dist/hooks/useTaskforce.js +302 -817
  115. package/dist/hooks/useTaskforce.sync-behavior.test.js +78 -0
  116. package/dist/hooks/useWorkspaceSyncController.d.ts +4 -1
  117. package/dist/hooks/useWorkspaceSyncController.js +33 -11
  118. package/dist/hooks/useWorkspaceSyncController.test.js +1 -0
  119. package/dist/hooks/workspace/useTaskforceApiRouting.test.js +17 -0
  120. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +166 -0
  121. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +434 -0
  122. package/dist/hooks/workspace/workspaceLocalState.d.ts +19 -0
  123. package/dist/hooks/workspace/workspaceLocalState.js +100 -0
  124. package/dist/hooks/workspace/workspaceLocalState.test.d.ts +1 -0
  125. package/dist/hooks/workspace/workspaceLocalState.test.js +59 -0
  126. package/dist/localization/locales/en-US.js +2 -2
  127. package/dist/mcp/adminWorkspaceRegistrar.d.ts +2 -0
  128. package/dist/mcp/adminWorkspaceRegistrar.js +154 -0
  129. package/dist/mcp/canonicalAssetHelpers.d.ts +70 -0
  130. package/dist/mcp/canonicalAssetHelpers.js +259 -0
  131. package/dist/mcp/clientIntegrations.d.ts +12 -1
  132. package/dist/mcp/clientIntegrations.js +161 -6
  133. package/dist/mcp/clientIntegrations.test.js +76 -4
  134. package/dist/mcp/collaborationRegistrar.d.ts +2 -0
  135. package/dist/mcp/collaborationRegistrar.js +71 -0
  136. package/dist/mcp/documentAssetRegistrar.d.ts +2 -0
  137. package/dist/mcp/documentAssetRegistrar.js +346 -0
  138. package/dist/mcp/documentHelpers.d.ts +100 -0
  139. package/dist/mcp/documentHelpers.js +161 -0
  140. package/dist/mcp/runtime.js +2455 -3925
  141. package/dist/mcp/runtime.test.js +240 -8
  142. package/dist/mcp/taskAttachmentHelpers.d.ts +41 -0
  143. package/dist/mcp/taskAttachmentHelpers.js +60 -0
  144. package/dist/mcp/taskAttachmentTypes.d.ts +37 -0
  145. package/dist/mcp/taskAttachmentTypes.js +1 -0
  146. package/dist/mcp/taskPlanningRegistrar.d.ts +2 -0
  147. package/dist/mcp/taskPlanningRegistrar.js +418 -0
  148. package/dist/mcp/toolCatalog.d.ts +35 -0
  149. package/dist/mcp/toolCatalog.js +3 -0
  150. package/dist/plugins/vite.js +1 -1
  151. package/dist/runtime/appGateDefinitions.d.ts +1 -1
  152. package/dist/runtime/appGateDefinitions.js +1 -1
  153. package/dist/server/index.d.ts +1 -0
  154. package/dist/server/index.js +46 -3
  155. package/dist/server/index.test.js +12 -1
  156. package/dist/server/routes/admin.d.ts +1 -5
  157. package/dist/server/routes/admin.js +151 -80
  158. package/dist/server/routes/annotatedAttachments.d.ts +1 -1
  159. package/dist/server/routes/annotatedAttachments.js +1 -1
  160. package/dist/server/routes/auth.d.ts +1 -4
  161. package/dist/server/routes/auth.js +59 -64
  162. package/dist/server/routes/authSupport.d.ts +30 -0
  163. package/dist/server/routes/authSupport.js +81 -0
  164. package/dist/server/routes/billing.d.ts +1 -1
  165. package/dist/server/routes/billing.js +122 -255
  166. package/dist/server/routes/billing.test.js +257 -86
  167. package/dist/server/routes/documentReviews.d.ts +1 -1
  168. package/dist/server/routes/documentReviews.js +1 -1
  169. package/dist/server/routes/documents.d.ts +4 -2
  170. package/dist/server/routes/documents.js +73 -7
  171. package/dist/server/routes/primitives.d.ts +11 -0
  172. package/dist/server/routes/primitives.js +73 -0
  173. package/dist/server/routes/resources.d.ts +1 -1
  174. package/dist/server/routes/resources.js +1 -1
  175. package/dist/server/routes/shared.d.ts +31 -1
  176. package/dist/server/routes/shared.js +26 -0
  177. package/dist/server/routes/sync.d.ts +1 -1
  178. package/dist/server/routes/sync.integration.test.js +20 -0
  179. package/dist/server/routes/sync.js +111 -238
  180. package/dist/server/routes/tasks.d.ts +1 -1
  181. package/dist/server/routes/tasks.js +50 -8
  182. package/dist/server/routes/workspaces.d.ts +1 -1
  183. package/dist/server/routes/workspaces.js +1 -1
  184. package/dist/server/routes.d.ts +2 -9
  185. package/dist/server/routes.js +84 -181
  186. package/dist/server/routes.test.js +912 -32
  187. package/dist/shared/taskActor.d.ts +10 -0
  188. package/dist/shared/taskActor.js +1 -0
  189. package/dist/sync/collaborationSyncPayload.js +4 -0
  190. package/dist/sync/collaborationSyncPayload.test.js +8 -0
  191. package/dist/sync/collaborationSyncState.d.ts +3 -1
  192. package/dist/sync/collaborationSyncState.js +19 -0
  193. package/dist/sync/contentSyncState.js +28 -17
  194. package/dist/sync/syncApplyHandlers.d.ts +87 -1
  195. package/dist/sync/syncApplyHandlers.js +370 -8
  196. package/dist/sync/syncApplyHandlers.test.d.ts +1 -0
  197. package/dist/sync/syncApplyHandlers.test.js +379 -0
  198. package/dist/sync/syncService.d.ts +9 -1
  199. package/dist/sync/syncService.js +44 -2
  200. package/dist/sync/syncService.test.js +62 -0
  201. package/dist/sync/workspacePullFeed.d.ts +1 -0
  202. package/dist/sync/workspacePullFeed.js +66 -59
  203. package/dist/sync/workspacePullFeed.test.js +72 -4
  204. package/dist/sync/workspaceSyncModel.d.ts +25 -1
  205. package/dist/sync/workspaceSyncModel.js +77 -3
  206. package/dist/sync/workspaceSyncModel.test.js +153 -0
  207. package/dist/sync/workspaceSyncState.d.ts +2 -0
  208. package/dist/sync/workspaceSyncState.js +1 -0
  209. package/dist/types.d.ts +2 -10
  210. package/dist/ui/assets/AgentsModule-N2MnQBZk.js +1 -0
  211. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-C_TmXZwA.js → AnnotatedAttachmentWorkspace-BG6P_GVW.js} +1 -1
  212. package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +1 -0
  213. package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +252 -0
  214. package/dist/ui/assets/{InitiativesModule-4-Rh2K2n.js → InitiativesModule-Dhm7M8e7.js} +1 -1
  215. package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
  216. package/dist/ui/assets/PlansPage-Cq0zXj3I.js +1 -0
  217. package/dist/ui/assets/TaskSettings-ln0aF7xc.js +9 -0
  218. package/dist/ui/assets/{WorkflowsModule-CGk9s3NJ.js → WorkflowsModule-BcS4afRn.js} +1 -1
  219. package/dist/ui/assets/index-C2-OXZV7.css +1 -0
  220. package/dist/ui/assets/index-DcSI5F86.js +6 -0
  221. package/dist/ui/assets/{vendor-icons-pWocEipT.js → vendor-icons-BSUaRwF8.js} +1 -1
  222. package/dist/ui/index.html +4 -4
  223. package/dist/utils/commercialLifecyclePresentation.d.ts +19 -0
  224. package/dist/utils/commercialLifecyclePresentation.js +196 -0
  225. package/dist/utils/taskActivity.js +4 -0
  226. package/dist/utils/taskActivity.test.js +25 -1
  227. package/package.json +10 -1
  228. package/scripts/check-cloud-mcp.mjs +83 -0
  229. package/scripts/playwright-auth.sh +9 -1
  230. package/scripts/run-billing-performance-smoke.sh +24 -0
  231. package/dist/ui/assets/AgentsModule-CpsTmrIW.js +0 -1
  232. package/dist/ui/assets/DocumentWorkspace-C3GyzrWm.js +0 -250
  233. package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +0 -1
  234. package/dist/ui/assets/PlansPage-BP7AYOqr.js +0 -1
  235. package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +0 -8
  236. package/dist/ui/assets/index-CLIMgR6g.js +0 -6
  237. package/dist/ui/assets/index-DneETxcu.css +0 -1
@@ -830,6 +830,9 @@ describe('TaskforceCore Integration', () => {
830
830
  });
831
831
  it('shows full-page login when cloud auth is required and user is unauthenticated', async () => {
832
832
  const fetchMock = vi.fn((url) => {
833
+ if (url.includes('/api/taskforce/auth/runtime-config')) {
834
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'cloud' } }) });
835
+ }
833
836
  if (url.includes('/api/taskforce/auth/session')) {
834
837
  return Promise.resolve({
835
838
  ok: true,
@@ -929,8 +932,8 @@ describe('TaskforceCore Integration', () => {
929
932
  json: () => Promise.resolve({
930
933
  plans: [
931
934
  {
932
- planId: 'pro',
933
- displayName: 'Pro Plan test mode',
935
+ planId: 'basic',
936
+ displayName: 'Basic Plan test mode',
934
937
  status: 'active',
935
938
  features: [],
936
939
  pricing: { month: null, year: null },
@@ -946,7 +949,7 @@ describe('TaskforceCore Integration', () => {
946
949
  render(_jsx(MemoryRouter, { initialEntries: ['/pricing'], children: _jsx(TaskforceCore, {}) }));
947
950
  await waitFor(() => {
948
951
  expect(screen.queryByPlaceholderText('Email')).not.toBeInTheDocument();
949
- expect(screen.getByText('Pro Plan test mode')).toBeInTheDocument();
952
+ expect(screen.getByText('Basic Plan test mode')).toBeInTheDocument();
950
953
  expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
951
954
  });
952
955
  });
@@ -1019,8 +1022,8 @@ describe('TaskforceCore Integration', () => {
1019
1022
  json: () => Promise.resolve({
1020
1023
  plans: [
1021
1024
  {
1022
- planId: 'starter',
1023
- displayName: 'Starter',
1025
+ planId: 'free',
1026
+ displayName: 'Free',
1024
1027
  status: 'active',
1025
1028
  features: [],
1026
1029
  pricing: {
@@ -1028,7 +1031,7 @@ describe('TaskforceCore Integration', () => {
1028
1031
  year: null
1029
1032
  },
1030
1033
  defaultVersion: {
1031
- planVersionId: 'starter-v1',
1034
+ planVersionId: 'free-v1',
1032
1035
  versionNumber: 1,
1033
1036
  seatLimit: null,
1034
1037
  metadata: {}
@@ -1042,8 +1045,8 @@ describe('TaskforceCore Integration', () => {
1042
1045
  return Promise.resolve({
1043
1046
  ok: true,
1044
1047
  json: () => Promise.resolve({
1045
- planId: 'starter',
1046
- planName: 'Starter',
1048
+ planId: 'free',
1049
+ planName: 'Free',
1047
1050
  entitlementState: 'active'
1048
1051
  })
1049
1052
  });
@@ -1052,8 +1055,8 @@ describe('TaskforceCore Integration', () => {
1052
1055
  return Promise.resolve({
1053
1056
  ok: true,
1054
1057
  json: () => Promise.resolve({
1055
- planId: 'starter',
1056
- planName: 'Starter',
1058
+ planId: 'free',
1059
+ planName: 'Free',
1057
1060
  entitlementState: 'active'
1058
1061
  })
1059
1062
  });
@@ -1154,8 +1157,8 @@ describe('TaskforceCore Integration', () => {
1154
1157
  json: () => Promise.resolve({
1155
1158
  plans: [
1156
1159
  {
1157
- planId: 'starter',
1158
- displayName: 'Starter',
1160
+ planId: 'free',
1161
+ displayName: 'Free',
1159
1162
  status: 'active',
1160
1163
  features: [],
1161
1164
  pricing: {
@@ -1163,7 +1166,7 @@ describe('TaskforceCore Integration', () => {
1163
1166
  year: null
1164
1167
  },
1165
1168
  defaultVersion: {
1166
- planVersionId: 'starter-v1',
1169
+ planVersionId: 'free-v1',
1167
1170
  versionNumber: 1,
1168
1171
  seatLimit: null,
1169
1172
  metadata: {}
@@ -1269,8 +1272,8 @@ describe('TaskforceCore Integration', () => {
1269
1272
  json: () => Promise.resolve({
1270
1273
  plans: [
1271
1274
  {
1272
- planId: 'starter',
1273
- displayName: 'Starter',
1275
+ planId: 'free',
1276
+ displayName: 'Free',
1274
1277
  status: 'active',
1275
1278
  features: [],
1276
1279
  pricing: {
@@ -1278,7 +1281,7 @@ describe('TaskforceCore Integration', () => {
1278
1281
  year: null
1279
1282
  },
1280
1283
  defaultVersion: {
1281
- planVersionId: 'starter-v1',
1284
+ planVersionId: 'free-v1',
1282
1285
  versionNumber: 1,
1283
1286
  seatLimit: null,
1284
1287
  metadata: {}
@@ -1292,9 +1295,9 @@ describe('TaskforceCore Integration', () => {
1292
1295
  return Promise.resolve({
1293
1296
  ok: true,
1294
1297
  json: () => Promise.resolve({
1295
- planId: 'starter',
1296
- planName: 'Starter',
1297
- planVersionId: 'starter-v1',
1298
+ planId: 'free',
1299
+ planName: 'Free',
1300
+ planVersionId: 'free-v1',
1298
1301
  entitlementState: 'active'
1299
1302
  })
1300
1303
  });
@@ -1323,7 +1326,7 @@ describe('TaskforceCore Integration', () => {
1323
1326
  render(_jsx(MemoryRouter, { initialEntries: ['/?screen=plans&gate=missing_entitlement'], children: _jsx(TaskforceCore, {}) }));
1324
1327
  await waitFor(() => {
1325
1328
  expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
1326
- expect(screen.getByText('Starter')).toBeInTheDocument();
1329
+ expect(screen.getByText('Free')).toBeInTheDocument();
1327
1330
  });
1328
1331
  await waitFor(() => {
1329
1332
  expect(screen.queryByText('No entitlement is linked to this account.')).not.toBeInTheDocument();
@@ -1393,8 +1396,8 @@ describe('TaskforceCore Integration', () => {
1393
1396
  return Promise.resolve({
1394
1397
  ok: true,
1395
1398
  json: () => Promise.resolve({
1396
- planId: 'starter',
1397
- planName: 'Starter',
1399
+ planId: 'free',
1400
+ planName: 'Free',
1398
1401
  entitlementState: 'active'
1399
1402
  })
1400
1403
  });
@@ -1450,14 +1453,14 @@ describe('TaskforceCore Integration', () => {
1450
1453
  });
1451
1454
  await waitFor(() => {
1452
1455
  expect(screen.getByText('Subscription')).toBeInTheDocument();
1453
- expect(screen.getByText('Starter')).toBeInTheDocument();
1456
+ expect(screen.getByText('Free')).toBeInTheDocument();
1454
1457
  expect(screen.getByText('Runtime')).toBeInTheDocument();
1455
1458
  expect(screen.getByText('LOCAL')).toBeInTheDocument();
1456
1459
  expect(screen.getByText('Environment')).toBeInTheDocument();
1457
1460
  expect(screen.getByText('CLOUD.EXAMPLE.COM')).toBeInTheDocument();
1458
1461
  expect(screen.queryByText(/Current Plan:/i)).not.toBeInTheDocument();
1459
1462
  });
1460
- await user.click(screen.getByRole('button', { name: /Subscription Starter/i }));
1463
+ await user.click(screen.getByRole('button', { name: /Subscription Free/i }));
1461
1464
  await waitFor(() => {
1462
1465
  expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
1463
1466
  });
@@ -1512,8 +1515,8 @@ describe('TaskforceCore Integration', () => {
1512
1515
  return Promise.resolve({
1513
1516
  ok: true,
1514
1517
  json: () => Promise.resolve({
1515
- planId: 'starter',
1516
- planName: 'Starter',
1518
+ planId: 'free',
1519
+ planName: 'Free',
1517
1520
  entitlementState: 'active'
1518
1521
  })
1519
1522
  });
@@ -1563,7 +1566,7 @@ describe('TaskforceCore Integration', () => {
1563
1566
  }
1564
1567
  await waitFor(() => {
1565
1568
  expect(screen.getByText('Subscription')).toBeInTheDocument();
1566
- expect(screen.getByText('Starter')).toBeInTheDocument();
1569
+ expect(screen.getByText('Free')).toBeInTheDocument();
1567
1570
  expect(screen.getByText('Runtime')).toBeInTheDocument();
1568
1571
  expect(screen.getByText('LOCAL')).toBeInTheDocument();
1569
1572
  expect(screen.getByText('Environment')).toBeInTheDocument();
@@ -1590,9 +1593,9 @@ describe('TaskforceCore Integration', () => {
1590
1593
  userId: 'user-new',
1591
1594
  displayName: 'New User',
1592
1595
  workspaceId: 'workspace-local-active',
1593
- planId: 'starter',
1594
- planVersionId: 'starter-v1',
1595
- planName: 'Starter',
1596
+ planId: 'free',
1597
+ planVersionId: 'free-v1',
1598
+ planName: 'Free',
1596
1599
  teamManagementAllowed: false,
1597
1600
  teamPlanMode: 'personal',
1598
1601
  workspaceRole: 'member'
@@ -1690,11 +1693,11 @@ describe('TaskforceCore Integration', () => {
1690
1693
  return jsonResponse({
1691
1694
  plans: [
1692
1695
  {
1693
- planId: 'starter',
1694
- displayName: 'Starter',
1696
+ planId: 'free',
1697
+ displayName: 'Free',
1695
1698
  status: 'active',
1696
1699
  defaultVersion: {
1697
- planVersionId: 'starter-v1',
1700
+ planVersionId: 'free-v1',
1698
1701
  versionNumber: 1,
1699
1702
  seatLimit: null,
1700
1703
  metadata: {}
@@ -1825,7 +1828,7 @@ describe('TaskforceCore Integration', () => {
1825
1828
  await user.click(screen.getByTitle(/Account/i));
1826
1829
  await waitFor(() => {
1827
1830
  expect(screen.getByText(accounts.newcomer.email)).toBeInTheDocument();
1828
- expect(screen.getByText('Starter')).toBeInTheDocument();
1831
+ expect(screen.getByText('Free')).toBeInTheDocument();
1829
1832
  });
1830
1833
  await user.click(screen.getByRole('menuitem', { name: 'Sign Out' }));
1831
1834
  await waitFor(() => {
@@ -1846,7 +1849,7 @@ describe('TaskforceCore Integration', () => {
1846
1849
  await waitFor(() => {
1847
1850
  expect(screen.getByText(accounts.ross.email)).toBeInTheDocument();
1848
1851
  expect(screen.getByText('Team 5')).toBeInTheDocument();
1849
- expect(screen.queryByText('Starter')).not.toBeInTheDocument();
1852
+ expect(screen.queryByText('Free')).not.toBeInTheDocument();
1850
1853
  expect(screen.getByRole('menuitem', { name: 'Team Management' })).toBeInTheDocument();
1851
1854
  });
1852
1855
  await user.click(screen.getByRole('button', { name: /Subscription Team 5/i }));
@@ -1897,16 +1900,144 @@ describe('TaskforceCore Integration', () => {
1897
1900
  const { container } = render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
1898
1901
  await waitFor(() => {
1899
1902
  expect(screen.getByText('Create your Taskforce account.')).toBeInTheDocument();
1903
+ expect(screen.getByLabelText('Email')).toBeInTheDocument();
1904
+ expect(screen.getByLabelText('Display name')).toBeInTheDocument();
1900
1905
  expect(screen.getByPlaceholderText('Email')).toBeInTheDocument();
1901
1906
  expect(screen.getByPlaceholderText('Display name')).toBeInTheDocument();
1902
1907
  expect(screen.getByRole('button', { name: 'Create Account' })).toBeInTheDocument();
1903
1908
  expect(screen.getByRole('button', { name: 'Sign In' })).toBeInTheDocument();
1904
1909
  expect(screen.queryByRole('button', { name: 'Register' })).not.toBeInTheDocument();
1905
1910
  expect(screen.getByRole('button', { name: 'Close sign in' })).toBeInTheDocument();
1911
+ expect(screen.getByTestId('auth-submit-button')).toBeInTheDocument();
1912
+ expect(document.title).toBe('Create Account | Taskforce');
1906
1913
  });
1907
1914
  expect(container.querySelector('[aria-pressed]')).not.toBeInTheDocument();
1908
1915
  });
1909
- it('does not switch to verify mode after registration when verification is not required', async () => {
1916
+ it('keeps the verification screen focused on verification and lets the user request a fresh email token', async () => {
1917
+ const fetchMock = vi.fn((url) => {
1918
+ if (url.includes('/api/taskforce/auth/session')) {
1919
+ return Promise.resolve({
1920
+ ok: true,
1921
+ json: () => Promise.resolve({
1922
+ authenticated: false,
1923
+ runtimeMode: 'cloud',
1924
+ authRequiredForApi: false
1925
+ })
1926
+ });
1927
+ }
1928
+ if (url.includes('/api/taskforce/config')) {
1929
+ return Promise.resolve({
1930
+ ok: true,
1931
+ json: () => Promise.resolve({
1932
+ ...mockConfig,
1933
+ runtimeMode: 'cloud',
1934
+ authRequiredForApi: false,
1935
+ setupState: {
1936
+ globalSetupState: 'present',
1937
+ workspaceSetupState: 'present',
1938
+ runtimeMode: 'cloud',
1939
+ workspaceId: 'workspace-local-active',
1940
+ mode: 'core',
1941
+ forceSetup: false,
1942
+ forceGlobalSetup: false,
1943
+ forceWorkspaceSetup: false
1944
+ }
1945
+ })
1946
+ });
1947
+ }
1948
+ if (url.includes('/api/taskforce/workflow-templates')) {
1949
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
1950
+ }
1951
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
1952
+ });
1953
+ vi.stubGlobal('fetch', fetchMock);
1954
+ render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=verify'], children: _jsx(TaskforceCore, {}) }));
1955
+ await waitFor(() => {
1956
+ expect(screen.getByText('Enter your verification token.')).toBeInTheDocument();
1957
+ expect(screen.getByPlaceholderText('Email')).toBeInTheDocument();
1958
+ expect(screen.getByRole('button', { name: 'Verify Email' })).toBeInTheDocument();
1959
+ expect(screen.getByRole('button', { name: 'Back to Sign In' })).toBeInTheDocument();
1960
+ expect(screen.queryByRole('button', { name: 'Register' })).not.toBeInTheDocument();
1961
+ expect(screen.getByRole('button', { name: 'Resend Verification' })).toBeDisabled();
1962
+ });
1963
+ });
1964
+ it('allows the verify screen to resend a verification email after the user re-enters their email', async () => {
1965
+ const user = userEvent.setup();
1966
+ const fetchMock = vi.fn((input) => {
1967
+ const url = String(input);
1968
+ if (url.includes('/api/taskforce/auth/session')) {
1969
+ return Promise.resolve({
1970
+ ok: true,
1971
+ json: () => Promise.resolve({
1972
+ authenticated: false,
1973
+ runtimeMode: 'cloud',
1974
+ authRequiredForApi: false
1975
+ })
1976
+ });
1977
+ }
1978
+ if (url.includes('/api/taskforce/config')) {
1979
+ return Promise.resolve({
1980
+ ok: true,
1981
+ json: () => Promise.resolve({
1982
+ ...mockConfig,
1983
+ runtimeMode: 'cloud',
1984
+ authRequiredForApi: false,
1985
+ setupState: {
1986
+ globalSetupState: 'present',
1987
+ workspaceSetupState: 'present',
1988
+ runtimeMode: 'cloud',
1989
+ workspaceId: 'workspace-local-active',
1990
+ mode: 'core',
1991
+ forceSetup: false,
1992
+ forceGlobalSetup: false,
1993
+ forceWorkspaceSetup: false
1994
+ }
1995
+ })
1996
+ });
1997
+ }
1998
+ if (url.includes('/api/taskforce/auth/register')) {
1999
+ return Promise.resolve({
2000
+ ok: true,
2001
+ json: () => Promise.resolve({
2002
+ success: true,
2003
+ verificationRequired: true,
2004
+ workspaceSetupRequired: false,
2005
+ emailSent: true
2006
+ })
2007
+ });
2008
+ }
2009
+ if (url.includes('/api/taskforce/auth/verify-email/request')) {
2010
+ return Promise.resolve({
2011
+ ok: true,
2012
+ json: () => Promise.resolve({
2013
+ success: true,
2014
+ verificationToken: 'resent-token'
2015
+ })
2016
+ });
2017
+ }
2018
+ if (url.includes('/api/taskforce/public/pricing')) {
2019
+ return Promise.resolve({
2020
+ ok: true,
2021
+ json: () => Promise.resolve({
2022
+ plans: []
2023
+ })
2024
+ });
2025
+ }
2026
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
2027
+ });
2028
+ vi.stubGlobal('fetch', fetchMock);
2029
+ render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=verify'], children: _jsx(TaskforceCore, {}) }));
2030
+ await user.type(await screen.findByPlaceholderText('Email'), 'verify@example.com');
2031
+ await user.click(screen.getByRole('button', { name: 'Resend Verification' }));
2032
+ await waitFor(() => {
2033
+ expect(screen.getByRole('button', { name: /Resend Verification/ })).toBeInTheDocument();
2034
+ expect(screen.getByDisplayValue('resent-token')).toBeInTheDocument();
2035
+ expect(screen.getByText('Verification email resent.')).toBeInTheDocument();
2036
+ });
2037
+ expect(screen.getByRole('button', { name: /Resend Verification/ })).toBeDisabled();
2038
+ expect(fetchMock.mock.calls.some(([url]) => String(url).includes('/api/taskforce/auth/verify-email/request'))).toBe(true);
2039
+ });
2040
+ it('shows neutral verification copy when resend succeeds without attempting delivery', async () => {
1910
2041
  const user = userEvent.setup();
1911
2042
  const fetchMock = vi.fn((input) => {
1912
2043
  const url = String(input);
@@ -1940,6 +2071,113 @@ describe('TaskforceCore Integration', () => {
1940
2071
  })
1941
2072
  });
1942
2073
  }
2074
+ if (url.includes('/api/taskforce/auth/verify-email/request')) {
2075
+ return Promise.resolve({
2076
+ ok: true,
2077
+ json: () => Promise.resolve({
2078
+ success: true,
2079
+ emailSent: false,
2080
+ deliveryAttempted: false,
2081
+ verificationToken: null
2082
+ })
2083
+ });
2084
+ }
2085
+ if (url.includes('/api/taskforce/public/pricing')) {
2086
+ return Promise.resolve({
2087
+ ok: true,
2088
+ json: () => Promise.resolve({
2089
+ plans: []
2090
+ })
2091
+ });
2092
+ }
2093
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
2094
+ });
2095
+ vi.stubGlobal('fetch', fetchMock);
2096
+ render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=verify'], children: _jsx(TaskforceCore, {}) }));
2097
+ await user.type(await screen.findByPlaceholderText('Email'), 'verify@example.com');
2098
+ await user.click(screen.getByRole('button', { name: 'Resend Verification' }));
2099
+ await waitFor(() => {
2100
+ expect(screen.getByText('If that account exists and still needs verification, we sent an email.')).toBeInTheDocument();
2101
+ });
2102
+ });
2103
+ it('does not switch to verify mode after registration when verification is not required', async () => {
2104
+ const user = userEvent.setup();
2105
+ let authenticated = false;
2106
+ const fetchMock = vi.fn((input) => {
2107
+ const url = String(input);
2108
+ if (url.includes('/api/taskforce/auth/runtime-config')) {
2109
+ return Promise.resolve({
2110
+ ok: true,
2111
+ json: () => Promise.resolve({
2112
+ config: {
2113
+ baseUrl: 'https://app.taskforce.example',
2114
+ apiBaseUrl: 'https://app.taskforce.example/api',
2115
+ cloudAuthBaseUrl: 'https://app.taskforce.example',
2116
+ runtimeMode: 'cloud',
2117
+ authSource: 'cloud',
2118
+ workspaceMode: 'multi-cloud',
2119
+ workspaceSwitchingEnabled: true,
2120
+ authRequiredForApi: true
2121
+ }
2122
+ })
2123
+ });
2124
+ }
2125
+ if (url.includes('/api/taskforce/auth/session')) {
2126
+ return Promise.resolve({
2127
+ ok: true,
2128
+ json: () => Promise.resolve({
2129
+ authenticated,
2130
+ runtimeMode: 'cloud',
2131
+ authRequiredForApi: true,
2132
+ userId: authenticated ? 'user-no-verify' : null,
2133
+ workspaceId: null,
2134
+ betaAccess: true
2135
+ })
2136
+ });
2137
+ }
2138
+ if (url.includes('/api/taskforce/config')) {
2139
+ return Promise.resolve({
2140
+ ok: true,
2141
+ json: () => Promise.resolve({
2142
+ ...mockConfig,
2143
+ runtimeMode: 'cloud',
2144
+ authRequiredForApi: true,
2145
+ workspaceSwitchingEnabled: true,
2146
+ setupState: {
2147
+ globalSetupState: 'present',
2148
+ workspaceSetupState: 'missing',
2149
+ runtimeMode: 'cloud',
2150
+ workspaceId: 'default',
2151
+ mode: 'core',
2152
+ forceSetup: true,
2153
+ forceGlobalSetup: false,
2154
+ forceWorkspaceSetup: true
2155
+ }
2156
+ })
2157
+ });
2158
+ }
2159
+ if (url.includes('/api/taskforce/workspaces')) {
2160
+ return Promise.resolve({
2161
+ ok: true,
2162
+ json: () => Promise.resolve({
2163
+ success: true,
2164
+ workspaces: [],
2165
+ currentWorkspaceId: 'default'
2166
+ })
2167
+ });
2168
+ }
2169
+ if (url.includes('/api/taskforce/account/access-status')) {
2170
+ return Promise.resolve({
2171
+ ok: true,
2172
+ json: () => Promise.resolve({
2173
+ gate: 'ok',
2174
+ hasEntitlement: true,
2175
+ canAccessApp: true,
2176
+ canAccessPlans: true,
2177
+ message: null
2178
+ })
2179
+ });
2180
+ }
1943
2181
  if (url.includes('/api/taskforce/public/pricing')) {
1944
2182
  return Promise.resolve({
1945
2183
  ok: true,
@@ -1949,12 +2187,14 @@ describe('TaskforceCore Integration', () => {
1949
2187
  });
1950
2188
  }
1951
2189
  if (url.includes('/api/taskforce/auth/register')) {
2190
+ authenticated = true;
1952
2191
  return Promise.resolve({
1953
2192
  ok: true,
1954
2193
  json: () => Promise.resolve({
1955
2194
  success: true,
1956
- workspaceSetupRequired: false,
1957
- verificationRequired: false
2195
+ workspaceSetupRequired: true,
2196
+ verificationRequired: false,
2197
+ commercialState: 'active'
1958
2198
  })
1959
2199
  });
1960
2200
  }
@@ -1967,7 +2207,7 @@ describe('TaskforceCore Integration', () => {
1967
2207
  await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
1968
2208
  await user.click(screen.getByRole('button', { name: 'Create Account' }));
1969
2209
  await waitFor(() => {
1970
- expect(screen.getByText('Subscription Plans')).toBeInTheDocument();
2210
+ expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
1971
2211
  });
1972
2212
  expect(screen.queryByPlaceholderText('Verification token')).not.toBeInTheDocument();
1973
2213
  expect(screen.queryByRole('button', { name: 'Create Account' })).not.toBeInTheDocument();
@@ -2054,11 +2294,11 @@ describe('TaskforceCore Integration', () => {
2054
2294
  if (url.includes('/api/taskforce/public/pricing')) {
2055
2295
  return jsonResponse({
2056
2296
  plans: [{
2057
- planId: 'starter',
2058
- displayName: 'Starter',
2297
+ planId: 'free',
2298
+ displayName: 'Free',
2059
2299
  status: 'active',
2060
2300
  defaultVersion: {
2061
- planVersionId: 'starter-v1',
2301
+ planVersionId: 'free-v1',
2062
2302
  versionNumber: 1,
2063
2303
  seatLimit: null,
2064
2304
  metadata: {}
@@ -2066,10 +2306,10 @@ describe('TaskforceCore Integration', () => {
2066
2306
  features: [],
2067
2307
  pricing: {
2068
2308
  month: {
2069
- planVersionId: 'starter-v1',
2309
+ planVersionId: 'free-v1',
2070
2310
  interval: 'month',
2071
2311
  pricingType: 'free',
2072
- stripePriceId: 'free:starter-v1:month',
2312
+ stripePriceId: 'free:free-v1:month',
2073
2313
  displayAmount: '$0.00',
2074
2314
  unitAmount: 0,
2075
2315
  currency: 'usd'
@@ -2130,7 +2370,7 @@ describe('TaskforceCore Integration', () => {
2130
2370
  });
2131
2371
  expect(fetchMock.mock.calls.some(([url]) => String(url).includes('/api/taskforce/billing/checkout-session'))).toBe(false);
2132
2372
  });
2133
- it('routes successful registration into checkout when onboarding leaves the account checkout-pending', async () => {
2373
+ it('keeps registration on plan selection when commercial onboarding is blocked even if the new cloud user has no workspace yet', async () => {
2134
2374
  const user = userEvent.setup();
2135
2375
  let authenticated = false;
2136
2376
  const fetchMock = vi.fn((input) => {
@@ -2154,8 +2394,8 @@ describe('TaskforceCore Integration', () => {
2154
2394
  authenticated,
2155
2395
  runtimeMode: 'cloud',
2156
2396
  authRequiredForApi: true,
2157
- userId: authenticated ? 'user-1' : null,
2158
- workspaceId: authenticated ? 'workspace-1' : null,
2397
+ userId: authenticated ? 'user-new' : null,
2398
+ workspaceId: null,
2159
2399
  betaAccess: true
2160
2400
  });
2161
2401
  }
@@ -2167,9 +2407,9 @@ describe('TaskforceCore Integration', () => {
2167
2407
  workspaceSwitchingEnabled: true,
2168
2408
  setupState: {
2169
2409
  globalSetupState: 'present',
2170
- workspaceSetupState: 'present',
2410
+ workspaceSetupState: 'missing',
2171
2411
  runtimeMode: 'cloud',
2172
- workspaceId: 'workspace-1',
2412
+ workspaceId: 'default',
2173
2413
  mode: 'core',
2174
2414
  forceSetup: false,
2175
2415
  forceGlobalSetup: false,
@@ -2180,67 +2420,183 @@ describe('TaskforceCore Integration', () => {
2180
2420
  if (url.includes('/api/taskforce/workspaces')) {
2181
2421
  return jsonResponse({
2182
2422
  success: true,
2183
- workspaces: [{
2184
- id: 'workspace-1',
2185
- slug: 'workspace-1',
2186
- name: 'Workspace 1',
2187
- description: null,
2188
- role: 'owner',
2189
- status: 'active',
2190
- betaAccess: true
2191
- }],
2192
- currentWorkspaceId: 'workspace-1'
2423
+ workspaces: [],
2424
+ currentWorkspaceId: 'default'
2193
2425
  });
2194
2426
  }
2195
2427
  if (url.includes('/api/taskforce/account/access-status')) {
2196
2428
  return jsonResponse({
2197
- gate: 'checkout_pending',
2429
+ gate: 'plan_selection_required',
2198
2430
  hasEntitlement: true,
2199
2431
  canAccessApp: false,
2200
2432
  canAccessPlans: true,
2201
- message: 'Complete checkout to continue.'
2433
+ message: 'Choose a plan to continue.'
2202
2434
  });
2203
2435
  }
2204
2436
  if (url.includes('/api/taskforce/account/profile-summary')) {
2205
2437
  return jsonResponse({
2206
- planId: 'pro',
2207
- planName: 'Pro',
2208
- planVersionId: 'pro-v1',
2209
- entitlementState: 'checkout_pending',
2210
- gate: 'checkout_pending',
2211
- message: 'Complete checkout to continue.',
2212
- workspaceId: 'workspace-1'
2438
+ entitlementState: 'pending_plan_selection',
2439
+ gate: 'plan_selection_required',
2440
+ message: 'Choose a plan to continue.',
2441
+ workspaceId: null
2213
2442
  });
2214
2443
  }
2215
2444
  if (url.includes('/api/taskforce/public/pricing')) {
2445
+ return jsonResponse({ plans: [] });
2446
+ }
2447
+ if (url.includes('/api/taskforce/auth/register')) {
2448
+ authenticated = true;
2216
2449
  return jsonResponse({
2217
- plans: [{
2218
- planId: 'pro',
2219
- displayName: 'Pro',
2220
- status: 'active',
2221
- defaultVersion: {
2222
- planVersionId: 'pro-v1',
2223
- versionNumber: 1,
2224
- seatLimit: null,
2225
- metadata: {}
2226
- },
2227
- features: [],
2228
- pricing: {
2229
- month: {
2230
- planVersionId: 'pro-v1',
2231
- interval: 'month',
2232
- pricingType: 'stripe',
2233
- stripePriceId: 'price_pro_month',
2234
- displayAmount: '$19.00',
2235
- unitAmount: 1900,
2236
- currency: 'usd'
2237
- },
2238
- year: {
2239
- planVersionId: 'pro-v1',
2240
- interval: 'year',
2241
- pricingType: 'stripe',
2242
- stripePriceId: 'price_pro_year',
2243
- displayAmount: '$190.00',
2450
+ success: true,
2451
+ workspaceSetupRequired: true,
2452
+ verificationRequired: false,
2453
+ planSelectionRequired: true,
2454
+ commercialState: 'pending_plan_selection'
2455
+ });
2456
+ }
2457
+ if (url.includes('/api/taskforce/categories'))
2458
+ return jsonResponse(mockCategories);
2459
+ if (url.includes('/api/taskforce/tasks'))
2460
+ return jsonResponse({ tasks: mockTasks });
2461
+ if (url.includes('/api/taskforce/archive'))
2462
+ return jsonResponse({ archived: [] });
2463
+ if (url.includes('/api/taskforce/data-version'))
2464
+ return jsonResponse({ dataVersion: 1 });
2465
+ if (url.includes('/api/taskforce/types'))
2466
+ return jsonResponse({ types: [] });
2467
+ if (url.includes('/api/taskforce/priorities'))
2468
+ return jsonResponse({ priorities: [] });
2469
+ if (url.includes('/api/taskforce/approaches'))
2470
+ return jsonResponse({ approaches: [] });
2471
+ if (url.includes('/api/taskforce/specialists'))
2472
+ return jsonResponse({ specialists: [] });
2473
+ if (url.includes('/api/taskforce/workflow-templates'))
2474
+ return jsonResponse({ templates: [] });
2475
+ return jsonResponse({});
2476
+ });
2477
+ vi.stubGlobal('fetch', fetchMock);
2478
+ render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
2479
+ await user.type(await screen.findByPlaceholderText('Email'), 'brandnew@example.com');
2480
+ await user.type(screen.getByPlaceholderText('Display name'), 'Brand New');
2481
+ await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
2482
+ await user.click(screen.getByRole('button', { name: 'Create Account' }));
2483
+ await waitFor(() => {
2484
+ expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
2485
+ expect(screen.getByText('Choose a plan to continue.')).toBeInTheDocument();
2486
+ });
2487
+ expect(screen.queryByText('Configure your new workspace.')).not.toBeInTheDocument();
2488
+ });
2489
+ it('routes successful registration into checkout when onboarding leaves the account checkout-pending', async () => {
2490
+ const user = userEvent.setup();
2491
+ let authenticated = false;
2492
+ const fetchMock = vi.fn((input) => {
2493
+ const url = String(input);
2494
+ if (url.includes('/api/taskforce/auth/runtime-config')) {
2495
+ return jsonResponse({
2496
+ config: {
2497
+ baseUrl: 'https://app.taskforce.example',
2498
+ apiBaseUrl: 'https://app.taskforce.example/api',
2499
+ cloudAuthBaseUrl: 'https://app.taskforce.example',
2500
+ runtimeMode: 'cloud',
2501
+ authSource: 'cloud',
2502
+ workspaceMode: 'multi-cloud',
2503
+ workspaceSwitchingEnabled: true,
2504
+ authRequiredForApi: true
2505
+ }
2506
+ });
2507
+ }
2508
+ if (url.includes('/api/taskforce/auth/session')) {
2509
+ return jsonResponse({
2510
+ authenticated,
2511
+ runtimeMode: 'cloud',
2512
+ authRequiredForApi: true,
2513
+ userId: authenticated ? 'user-1' : null,
2514
+ workspaceId: authenticated ? 'workspace-1' : null,
2515
+ betaAccess: true
2516
+ });
2517
+ }
2518
+ if (url.includes('/api/taskforce/config')) {
2519
+ return jsonResponse({
2520
+ ...mockConfig,
2521
+ runtimeMode: 'cloud',
2522
+ authRequiredForApi: true,
2523
+ workspaceSwitchingEnabled: true,
2524
+ setupState: {
2525
+ globalSetupState: 'present',
2526
+ workspaceSetupState: 'present',
2527
+ runtimeMode: 'cloud',
2528
+ workspaceId: 'workspace-1',
2529
+ mode: 'core',
2530
+ forceSetup: false,
2531
+ forceGlobalSetup: false,
2532
+ forceWorkspaceSetup: false
2533
+ }
2534
+ });
2535
+ }
2536
+ if (url.includes('/api/taskforce/workspaces')) {
2537
+ return jsonResponse({
2538
+ success: true,
2539
+ workspaces: [{
2540
+ id: 'workspace-1',
2541
+ slug: 'workspace-1',
2542
+ name: 'Workspace 1',
2543
+ description: null,
2544
+ role: 'owner',
2545
+ status: 'active',
2546
+ betaAccess: true
2547
+ }],
2548
+ currentWorkspaceId: 'workspace-1'
2549
+ });
2550
+ }
2551
+ if (url.includes('/api/taskforce/account/access-status')) {
2552
+ return jsonResponse({
2553
+ gate: 'checkout_pending',
2554
+ hasEntitlement: true,
2555
+ canAccessApp: false,
2556
+ canAccessPlans: true,
2557
+ message: 'Complete checkout to continue.'
2558
+ });
2559
+ }
2560
+ if (url.includes('/api/taskforce/account/profile-summary')) {
2561
+ return jsonResponse({
2562
+ planId: 'basic',
2563
+ planName: 'Basic',
2564
+ planVersionId: 'basic-v1',
2565
+ entitlementState: 'checkout_pending',
2566
+ gate: 'checkout_pending',
2567
+ message: 'Complete checkout to continue.',
2568
+ workspaceId: 'workspace-1'
2569
+ });
2570
+ }
2571
+ if (url.includes('/api/taskforce/public/pricing')) {
2572
+ return jsonResponse({
2573
+ plans: [{
2574
+ planId: 'basic',
2575
+ displayName: 'Basic',
2576
+ status: 'active',
2577
+ defaultVersion: {
2578
+ planVersionId: 'basic-v1',
2579
+ versionNumber: 1,
2580
+ seatLimit: null,
2581
+ metadata: {}
2582
+ },
2583
+ features: [],
2584
+ pricing: {
2585
+ month: {
2586
+ planVersionId: 'basic-v1',
2587
+ interval: 'month',
2588
+ pricingType: 'stripe',
2589
+ stripePriceId: 'price_basic_month',
2590
+ displayAmount: '$19.00',
2591
+ unitAmount: 1900,
2592
+ currency: 'usd'
2593
+ },
2594
+ year: {
2595
+ planVersionId: 'basic-v1',
2596
+ interval: 'year',
2597
+ pricingType: 'stripe',
2598
+ stripePriceId: 'price_basic_year',
2599
+ displayAmount: '$190.00',
2244
2600
  unitAmount: 19000,
2245
2601
  currency: 'usd'
2246
2602
  }
@@ -2294,7 +2650,7 @@ describe('TaskforceCore Integration', () => {
2294
2650
  return jsonResponse({});
2295
2651
  });
2296
2652
  vi.stubGlobal('fetch', fetchMock);
2297
- render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&planId=pro&planVersionId=pro-v1&interval=year&next=%2F'], children: _jsx(TaskforceCore, {}) }));
2653
+ render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&planId=basic&planVersionId=basic-v1&interval=year&next=%2F'], children: _jsx(TaskforceCore, {}) }));
2298
2654
  await user.type(await screen.findByPlaceholderText('Email'), 'checkout@example.com');
2299
2655
  await user.type(screen.getByPlaceholderText('Display name'), 'Checkout User');
2300
2656
  await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
@@ -2306,69 +2662,330 @@ describe('TaskforceCore Integration', () => {
2306
2662
  expect(fetchMock.mock.calls.some(([url]) => String(url).includes('/api/taskforce/billing/checkout-session'))).toBe(true);
2307
2663
  });
2308
2664
  });
2309
- it('shows verification before workspace setup when registration still requires email verification', async () => {
2665
+ it('routes successful registration into workspace setup when onboarding auto-grants free, even if a stale pre-registration session check resolves later', async () => {
2310
2666
  const user = userEvent.setup();
2667
+ let authenticated = false;
2668
+ let authSessionCallCount = 0;
2669
+ let releaseInitialSession = null;
2670
+ const initialSessionGate = new Promise((resolve) => {
2671
+ releaseInitialSession = resolve;
2672
+ });
2311
2673
  const fetchMock = vi.fn((input) => {
2312
2674
  const url = String(input);
2313
- if (url.includes('/api/taskforce/auth/session')) {
2314
- return Promise.resolve({
2315
- ok: true,
2316
- json: () => Promise.resolve({
2317
- authenticated: false,
2675
+ if (url.includes('/api/taskforce/auth/runtime-config')) {
2676
+ return jsonResponse({
2677
+ config: {
2678
+ baseUrl: 'https://app.taskforce.example',
2679
+ apiBaseUrl: 'https://app.taskforce.example/api',
2680
+ cloudAuthBaseUrl: 'https://app.taskforce.example',
2318
2681
  runtimeMode: 'cloud',
2682
+ authSource: 'cloud',
2683
+ workspaceMode: 'multi-cloud',
2684
+ workspaceSwitchingEnabled: true,
2319
2685
  authRequiredForApi: true
2320
- })
2686
+ }
2687
+ });
2688
+ }
2689
+ if (url.includes('/api/taskforce/auth/session')) {
2690
+ authSessionCallCount += 1;
2691
+ const payload = () => ({
2692
+ authenticated,
2693
+ runtimeMode: 'cloud',
2694
+ authRequiredForApi: true,
2695
+ userId: authenticated ? 'user-free-1' : null,
2696
+ workspaceId: null,
2697
+ betaAccess: true
2321
2698
  });
2699
+ if (authSessionCallCount === 1) {
2700
+ return initialSessionGate.then(() => jsonResponse(payload()));
2701
+ }
2702
+ return jsonResponse(payload());
2322
2703
  }
2323
2704
  if (url.includes('/api/taskforce/config')) {
2324
- return Promise.resolve({
2325
- ok: true,
2326
- json: () => Promise.resolve({
2327
- ...mockConfig,
2705
+ return jsonResponse({
2706
+ ...mockConfig,
2707
+ runtimeMode: 'cloud',
2708
+ authRequiredForApi: true,
2709
+ workspaceSwitchingEnabled: true,
2710
+ setupState: {
2711
+ globalSetupState: 'present',
2712
+ workspaceSetupState: 'missing',
2328
2713
  runtimeMode: 'cloud',
2329
- authRequiredForApi: true,
2330
- setupState: {
2331
- globalSetupState: 'present',
2332
- workspaceSetupState: 'present',
2333
- runtimeMode: 'cloud',
2334
- workspaceId: 'default',
2335
- mode: 'core',
2336
- forceSetup: false,
2337
- forceGlobalSetup: false,
2338
- forceWorkspaceSetup: false
2339
- }
2340
- })
2714
+ workspaceId: 'default',
2715
+ mode: 'core',
2716
+ forceSetup: true,
2717
+ forceGlobalSetup: false,
2718
+ forceWorkspaceSetup: true
2719
+ }
2341
2720
  });
2342
2721
  }
2343
- if (url.includes('/api/taskforce/public/pricing')) {
2344
- return Promise.resolve({
2345
- ok: true,
2346
- json: () => Promise.resolve({
2347
- plans: []
2348
- })
2722
+ if (url.includes('/api/taskforce/workspaces')) {
2723
+ return jsonResponse({
2724
+ success: true,
2725
+ workspaces: [],
2726
+ currentWorkspaceId: 'default'
2727
+ });
2728
+ }
2729
+ if (url.includes('/api/taskforce/account/access-status')) {
2730
+ return jsonResponse({
2731
+ gate: 'ok',
2732
+ hasEntitlement: true,
2733
+ canAccessApp: true,
2734
+ canAccessPlans: true,
2735
+ message: null
2349
2736
  });
2350
2737
  }
2738
+ if (url.includes('/api/taskforce/public/pricing')) {
2739
+ return jsonResponse({ plans: [] });
2740
+ }
2351
2741
  if (url.includes('/api/taskforce/auth/register')) {
2352
- return Promise.resolve({
2353
- ok: true,
2354
- json: () => Promise.resolve({
2355
- success: true,
2356
- workspaceSetupRequired: true,
2357
- verificationRequired: true,
2358
- emailSent: true
2359
- })
2742
+ authenticated = true;
2743
+ return jsonResponse({
2744
+ success: true,
2745
+ workspaceSetupRequired: true,
2746
+ verificationRequired: false,
2747
+ commercialState: 'active'
2360
2748
  });
2361
2749
  }
2362
- return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
2750
+ if (url.includes('/api/taskforce/categories'))
2751
+ return jsonResponse(mockCategories);
2752
+ if (url.includes('/api/taskforce/tasks'))
2753
+ return jsonResponse({ tasks: mockTasks });
2754
+ if (url.includes('/api/taskforce/archive'))
2755
+ return jsonResponse({ archived: [] });
2756
+ if (url.includes('/api/taskforce/data-version'))
2757
+ return jsonResponse({ dataVersion: 1 });
2758
+ if (url.includes('/api/taskforce/types'))
2759
+ return jsonResponse({ types: [] });
2760
+ if (url.includes('/api/taskforce/priorities'))
2761
+ return jsonResponse({ priorities: [] });
2762
+ if (url.includes('/api/taskforce/approaches'))
2763
+ return jsonResponse({ approaches: [] });
2764
+ if (url.includes('/api/taskforce/specialists'))
2765
+ return jsonResponse({ specialists: [] });
2766
+ if (url.includes('/api/taskforce/workflow-templates'))
2767
+ return jsonResponse({ templates: [] });
2768
+ if (url.includes('/api/taskforce/taxonomy-library')) {
2769
+ return jsonResponse({
2770
+ packs: [{
2771
+ id: 'general',
2772
+ label: 'General',
2773
+ description: 'Starter',
2774
+ isDefaultStarter: true,
2775
+ sections: {
2776
+ categories: [{ value: 'general', label: 'General' }],
2777
+ taskTypes: [{ value: 'task', label: 'Task' }],
2778
+ priorities: [{ value: 1, label: 'Low' }]
2779
+ }
2780
+ }]
2781
+ });
2782
+ }
2783
+ return jsonResponse({});
2363
2784
  });
2364
2785
  vi.stubGlobal('fetch', fetchMock);
2365
- render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2Fplans'], children: _jsx(TaskforceCore, {}) }));
2366
- await user.type(await screen.findByPlaceholderText('Email'), 'local@example.com');
2367
- await user.type(screen.getByPlaceholderText('Display name'), 'Local User');
2786
+ render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
2787
+ await user.type(await screen.findByPlaceholderText('Email'), 'free-user@example.com');
2788
+ await user.type(screen.getByPlaceholderText('Display name'), 'Free User');
2368
2789
  await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
2369
2790
  await user.click(screen.getByRole('button', { name: 'Create Account' }));
2791
+ await act(async () => {
2792
+ await Promise.resolve();
2793
+ });
2370
2794
  await waitFor(() => {
2371
- expect(screen.getByPlaceholderText('Verification token')).toBeInTheDocument();
2795
+ expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
2796
+ });
2797
+ expect(fetchMock.mock.calls.filter(([url]) => String(url).includes('/api/taskforce/auth/session')).length).toBeGreaterThanOrEqual(2);
2798
+ await act(async () => {
2799
+ releaseInitialSession?.();
2800
+ await Promise.resolve();
2801
+ });
2802
+ await waitFor(() => {
2803
+ expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
2804
+ });
2805
+ expect(screen.queryByText('Sign in with your account credentials.')).not.toBeInTheDocument();
2806
+ });
2807
+ it('routes successful registration directly into the app when onboarding auto-grants free and the new cloud user already has an accessible workspace', async () => {
2808
+ const user = userEvent.setup();
2809
+ let authenticated = false;
2810
+ const fetchMock = vi.fn((input) => {
2811
+ const url = String(input);
2812
+ if (url.includes('/api/taskforce/auth/runtime-config')) {
2813
+ return jsonResponse({
2814
+ config: {
2815
+ baseUrl: 'https://app.taskforce.example',
2816
+ apiBaseUrl: 'https://app.taskforce.example/api',
2817
+ cloudAuthBaseUrl: 'https://app.taskforce.example',
2818
+ runtimeMode: 'cloud',
2819
+ authSource: 'cloud',
2820
+ workspaceMode: 'multi-cloud',
2821
+ workspaceSwitchingEnabled: true,
2822
+ authRequiredForApi: true
2823
+ }
2824
+ });
2825
+ }
2826
+ if (url.includes('/api/taskforce/auth/session')) {
2827
+ return jsonResponse({
2828
+ authenticated,
2829
+ runtimeMode: 'cloud',
2830
+ authRequiredForApi: true,
2831
+ userId: authenticated ? 'user-free-2' : null,
2832
+ workspaceId: authenticated ? 'workspace-1' : null,
2833
+ betaAccess: true
2834
+ });
2835
+ }
2836
+ if (url.includes('/api/taskforce/config')) {
2837
+ return jsonResponse({
2838
+ ...mockConfig,
2839
+ runtimeMode: 'cloud',
2840
+ authRequiredForApi: true,
2841
+ workspaceSwitchingEnabled: true,
2842
+ workspaceId: 'workspace-1',
2843
+ setupState: {
2844
+ globalSetupState: 'present',
2845
+ workspaceSetupState: 'present',
2846
+ runtimeMode: 'cloud',
2847
+ workspaceId: 'workspace-1',
2848
+ mode: 'core',
2849
+ forceSetup: false,
2850
+ forceGlobalSetup: false,
2851
+ forceWorkspaceSetup: false
2852
+ }
2853
+ });
2854
+ }
2855
+ if (url.includes('/api/taskforce/workspaces')) {
2856
+ return jsonResponse({
2857
+ success: true,
2858
+ workspaces: [{
2859
+ id: 'workspace-1',
2860
+ slug: 'workspace-1',
2861
+ name: 'Workspace 1',
2862
+ description: null,
2863
+ role: 'owner',
2864
+ status: 'active',
2865
+ betaAccess: true
2866
+ }],
2867
+ currentWorkspaceId: 'workspace-1'
2868
+ });
2869
+ }
2870
+ if (url.includes('/api/taskforce/account/access-status')) {
2871
+ return jsonResponse({
2872
+ gate: 'ok',
2873
+ hasEntitlement: true,
2874
+ canAccessApp: true,
2875
+ canAccessPlans: true,
2876
+ message: null
2877
+ });
2878
+ }
2879
+ if (url.includes('/api/taskforce/public/pricing')) {
2880
+ return jsonResponse({ plans: [] });
2881
+ }
2882
+ if (url.includes('/api/taskforce/auth/register')) {
2883
+ authenticated = true;
2884
+ return jsonResponse({
2885
+ success: true,
2886
+ workspaceSetupRequired: false,
2887
+ verificationRequired: false,
2888
+ commercialState: 'active'
2889
+ });
2890
+ }
2891
+ if (url.includes('/api/taskforce/categories'))
2892
+ return jsonResponse(mockCategories);
2893
+ if (url.includes('/api/taskforce/tasks'))
2894
+ return jsonResponse({ tasks: mockTasks });
2895
+ if (url.includes('/api/taskforce/archive'))
2896
+ return jsonResponse({ archived: [] });
2897
+ if (url.includes('/api/taskforce/data-version'))
2898
+ return jsonResponse({ dataVersion: 1 });
2899
+ if (url.includes('/api/taskforce/types'))
2900
+ return jsonResponse({ types: [] });
2901
+ if (url.includes('/api/taskforce/priorities'))
2902
+ return jsonResponse({ priorities: [] });
2903
+ if (url.includes('/api/taskforce/approaches'))
2904
+ return jsonResponse({ approaches: [] });
2905
+ if (url.includes('/api/taskforce/specialists'))
2906
+ return jsonResponse({ specialists: [] });
2907
+ if (url.includes('/api/taskforce/workflow-templates'))
2908
+ return jsonResponse({ templates: [] });
2909
+ return jsonResponse({});
2910
+ });
2911
+ vi.stubGlobal('fetch', fetchMock);
2912
+ render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
2913
+ await user.type(await screen.findByPlaceholderText('Email'), 'existing-workspace@example.com');
2914
+ await user.type(screen.getByPlaceholderText('Display name'), 'Existing Workspace User');
2915
+ await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
2916
+ await user.click(screen.getByRole('button', { name: 'Create Account' }));
2917
+ await act(async () => {
2918
+ await Promise.resolve();
2919
+ });
2920
+ await waitFor(() => {
2921
+ expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
2922
+ });
2923
+ expect(screen.queryByText('Configure your new workspace.')).not.toBeInTheDocument();
2924
+ expect(screen.queryByRole('heading', { name: 'Subscription Plans' })).not.toBeInTheDocument();
2925
+ });
2926
+ it('shows verification before workspace setup when registration still requires email verification', async () => {
2927
+ const user = userEvent.setup();
2928
+ const fetchMock = vi.fn((input) => {
2929
+ const url = String(input);
2930
+ if (url.includes('/api/taskforce/auth/session')) {
2931
+ return Promise.resolve({
2932
+ ok: true,
2933
+ json: () => Promise.resolve({
2934
+ authenticated: false,
2935
+ runtimeMode: 'cloud',
2936
+ authRequiredForApi: true
2937
+ })
2938
+ });
2939
+ }
2940
+ if (url.includes('/api/taskforce/config')) {
2941
+ return Promise.resolve({
2942
+ ok: true,
2943
+ json: () => Promise.resolve({
2944
+ ...mockConfig,
2945
+ runtimeMode: 'cloud',
2946
+ authRequiredForApi: true,
2947
+ setupState: {
2948
+ globalSetupState: 'present',
2949
+ workspaceSetupState: 'present',
2950
+ runtimeMode: 'cloud',
2951
+ workspaceId: 'default',
2952
+ mode: 'core',
2953
+ forceSetup: false,
2954
+ forceGlobalSetup: false,
2955
+ forceWorkspaceSetup: false
2956
+ }
2957
+ })
2958
+ });
2959
+ }
2960
+ if (url.includes('/api/taskforce/public/pricing')) {
2961
+ return Promise.resolve({
2962
+ ok: true,
2963
+ json: () => Promise.resolve({
2964
+ plans: []
2965
+ })
2966
+ });
2967
+ }
2968
+ if (url.includes('/api/taskforce/auth/register')) {
2969
+ return Promise.resolve({
2970
+ ok: true,
2971
+ json: () => Promise.resolve({
2972
+ success: true,
2973
+ workspaceSetupRequired: true,
2974
+ verificationRequired: true,
2975
+ emailSent: true
2976
+ })
2977
+ });
2978
+ }
2979
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
2980
+ });
2981
+ vi.stubGlobal('fetch', fetchMock);
2982
+ render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2Fplans'], children: _jsx(TaskforceCore, {}) }));
2983
+ await user.type(await screen.findByPlaceholderText('Email'), 'local@example.com');
2984
+ await user.type(screen.getByPlaceholderText('Display name'), 'Local User');
2985
+ await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
2986
+ await user.click(screen.getByRole('button', { name: 'Create Account' }));
2987
+ await waitFor(() => {
2988
+ expect(screen.getByPlaceholderText('Verification token')).toBeInTheDocument();
2372
2989
  });
2373
2990
  expect(screen.queryByRole('button', { name: 'Create Account' })).not.toBeInTheDocument();
2374
2991
  expect(screen.queryByText('Subscription Plans')).not.toBeInTheDocument();
@@ -2426,7 +3043,7 @@ describe('TaskforceCore Integration', () => {
2426
3043
  expect(screen.getByRole('button', { name: 'Register' })).toBeInTheDocument();
2427
3044
  });
2428
3045
  });
2429
- it('redirects register users to plans when signup plan configuration blocks account creation', async () => {
3046
+ it('shows neutral password reset copy when reset delivery was not attempted', async () => {
2430
3047
  const user = userEvent.setup();
2431
3048
  const fetchMock = vi.fn((input) => {
2432
3049
  const url = String(input);
@@ -2454,7 +3071,7 @@ describe('TaskforceCore Integration', () => {
2454
3071
  json: () => Promise.resolve({
2455
3072
  authenticated: false,
2456
3073
  runtimeMode: 'cloud',
2457
- authRequiredForApi: true
3074
+ authRequiredForApi: false
2458
3075
  })
2459
3076
  });
2460
3077
  }
@@ -2464,7 +3081,7 @@ describe('TaskforceCore Integration', () => {
2464
3081
  json: () => Promise.resolve({
2465
3082
  ...mockConfig,
2466
3083
  runtimeMode: 'cloud',
2467
- authRequiredForApi: true,
3084
+ authRequiredForApi: false,
2468
3085
  setupState: {
2469
3086
  globalSetupState: 'present',
2470
3087
  workspaceSetupState: 'present',
@@ -2478,62 +3095,59 @@ describe('TaskforceCore Integration', () => {
2478
3095
  })
2479
3096
  });
2480
3097
  }
2481
- if (url.includes('/api/taskforce/workflow-templates')) {
2482
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
2483
- }
2484
- if (url.includes('/api/taskforce/public/pricing')) {
3098
+ if (url.includes('/api/taskforce/auth/password-reset/request')) {
2485
3099
  return Promise.resolve({
2486
3100
  ok: true,
2487
3101
  json: () => Promise.resolve({
2488
- asOf: '2026-03-16T00:00:00.000Z',
2489
- plans: [
2490
- {
2491
- planId: 'free',
2492
- displayName: 'Free',
2493
- status: 'active',
2494
- defaultVersion: { planVersionId: 'free-v1', versionNumber: 1, seatLimit: 1, metadata: {} },
2495
- features: [],
2496
- pricing: {
2497
- month: { planVersionId: 'free-v1', interval: 'month', pricingType: 'free', stripePriceId: 'free:free-v1:month', displayAmount: '$0.00', unitAmount: 0, currency: 'usd' },
2498
- year: { planVersionId: 'free-v1', interval: 'year', pricingType: 'free', stripePriceId: 'free:free-v1:year', displayAmount: '$0.00', unitAmount: 0, currency: 'usd' }
2499
- }
2500
- }
2501
- ]
3102
+ success: true,
3103
+ emailSent: false,
3104
+ deliveryAttempted: false,
3105
+ resetToken: null
2502
3106
  })
2503
3107
  });
2504
3108
  }
2505
- if (url.includes('/api/taskforce/auth/register')) {
2506
- return Promise.resolve({
2507
- ok: false,
2508
- json: () => Promise.resolve({
2509
- success: false,
2510
- code: 'SIGNUP_PLAN_NOT_ENABLED',
2511
- error: 'Configured onboarding plan version \"team-10-v1\" belongs to a disabled plan.'
2512
- })
2513
- });
3109
+ if (url.includes('/api/taskforce/workflow-templates')) {
3110
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
2514
3111
  }
2515
3112
  return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
2516
3113
  });
2517
3114
  vi.stubGlobal('fetch', fetchMock);
2518
- render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
2519
- await user.type(await screen.findByPlaceholderText('Email'), 'local@example.com');
2520
- await user.type(screen.getByPlaceholderText('Display name'), 'Local User');
2521
- await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
2522
- await user.click(screen.getByRole('button', { name: 'Create Account' }));
3115
+ render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=forgot&next=%2F'], children: _jsx(TaskforceCore, {}) }));
3116
+ await user.type(await screen.findByPlaceholderText('Email'), 'nobody@example.com');
3117
+ await user.click(screen.getByRole('button', { name: 'Send Reset Link' }));
2523
3118
  await waitFor(() => {
2524
- expect(screen.getByText('Choose a plan to continue.')).toBeInTheDocument();
2525
- expect(screen.getByText('Subscription Plans')).toBeInTheDocument();
3119
+ expect(screen.getByText('If that account exists, we sent password reset instructions.')).toBeInTheDocument();
2526
3120
  });
2527
3121
  });
2528
- it('shows sign-in continuation for existing-user invites', async () => {
2529
- const fetchMock = vi.fn((url) => {
3122
+ it('keeps forgot-password users on the request form when reset delivery fails', async () => {
3123
+ const user = userEvent.setup();
3124
+ const fetchMock = vi.fn((input) => {
3125
+ const url = String(input);
3126
+ if (url.includes('/api/taskforce/auth/runtime-config')) {
3127
+ return Promise.resolve({
3128
+ ok: true,
3129
+ json: () => Promise.resolve({
3130
+ success: true,
3131
+ config: {
3132
+ baseUrl: 'https://app.taskforce.example',
3133
+ apiBaseUrl: 'https://app.taskforce.example/api',
3134
+ cloudAuthBaseUrl: 'https://app.taskforce.example',
3135
+ runtimeMode: 'cloud',
3136
+ authSource: 'cloud',
3137
+ workspaceMode: 'multi-cloud',
3138
+ workspaceSwitchingEnabled: true,
3139
+ cloudAuthViaLocalProxy: false
3140
+ }
3141
+ })
3142
+ });
3143
+ }
2530
3144
  if (url.includes('/api/taskforce/auth/session')) {
2531
3145
  return Promise.resolve({
2532
3146
  ok: true,
2533
3147
  json: () => Promise.resolve({
2534
3148
  authenticated: false,
2535
3149
  runtimeMode: 'cloud',
2536
- authRequiredForApi: true
3150
+ authRequiredForApi: false
2537
3151
  })
2538
3152
  });
2539
3153
  }
@@ -2543,12 +3157,12 @@ describe('TaskforceCore Integration', () => {
2543
3157
  json: () => Promise.resolve({
2544
3158
  ...mockConfig,
2545
3159
  runtimeMode: 'cloud',
2546
- authRequiredForApi: true,
3160
+ authRequiredForApi: false,
2547
3161
  setupState: {
2548
3162
  globalSetupState: 'present',
2549
3163
  workspaceSetupState: 'present',
2550
3164
  runtimeMode: 'cloud',
2551
- workspaceId: 'workspace-1',
3165
+ workspaceId: 'workspace-local-active',
2552
3166
  mode: 'core',
2553
3167
  forceSetup: false,
2554
3168
  forceGlobalSetup: false,
@@ -2557,16 +3171,15 @@ describe('TaskforceCore Integration', () => {
2557
3171
  })
2558
3172
  });
2559
3173
  }
2560
- if (url.includes('/api/taskforce/auth/invite/inspect')) {
3174
+ if (url.includes('/api/taskforce/auth/password-reset/request')) {
2561
3175
  return Promise.resolve({
2562
3176
  ok: true,
2563
3177
  json: () => Promise.resolve({
2564
3178
  success: true,
2565
- state: 'valid',
2566
- email: 'existing@example.com',
2567
- workspaceId: 'workspace-1',
2568
- inviteeKind: 'existing_user',
2569
- passwordRequired: false
3179
+ emailSent: false,
3180
+ deliveryAttempted: true,
3181
+ emailError: 'SMTP temporary failure',
3182
+ resetToken: null
2570
3183
  })
2571
3184
  });
2572
3185
  }
@@ -2576,16 +3189,38 @@ describe('TaskforceCore Integration', () => {
2576
3189
  return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
2577
3190
  });
2578
3191
  vi.stubGlobal('fetch', fetchMock);
2579
- render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=invite&token=invite-token&next=%2F'], children: _jsx(TaskforceCore, {}) }));
3192
+ render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=forgot&next=%2F'], children: _jsx(TaskforceCore, {}) }));
3193
+ await user.type(await screen.findByPlaceholderText('Email'), 'owner@example.com');
3194
+ await user.click(screen.getByRole('button', { name: 'Send Reset Link' }));
2580
3195
  await waitFor(() => {
2581
- expect(screen.getByText('Sign in to join this workspace.')).toBeInTheDocument();
2582
- expect(screen.getByDisplayValue('existing@example.com')).toBeInTheDocument();
2583
- expect(screen.getByPlaceholderText('Password')).toBeInTheDocument();
2584
- expect(screen.getByRole('button', { name: 'Sign In to Continue' })).toBeInTheDocument();
3196
+ expect(screen.getByText('Password reset email failed to send. SMTP temporary failure')).toBeInTheDocument();
2585
3197
  });
3198
+ expect(screen.getByText('Request a password reset link.')).toBeInTheDocument();
3199
+ expect(screen.queryByText('Enter the password reset token and choose a new password.')).not.toBeInTheDocument();
3200
+ expect(screen.getByRole('button', { name: 'Send Reset Link' })).toBeInTheDocument();
2586
3201
  });
2587
- it('shows password creation for new-user invites', async () => {
2588
- const fetchMock = vi.fn((url) => {
3202
+ it('redirects register users to plans when signup plan configuration blocks account creation', async () => {
3203
+ const user = userEvent.setup();
3204
+ const fetchMock = vi.fn((input) => {
3205
+ const url = String(input);
3206
+ if (url.includes('/api/taskforce/auth/runtime-config')) {
3207
+ return Promise.resolve({
3208
+ ok: true,
3209
+ json: () => Promise.resolve({
3210
+ success: true,
3211
+ config: {
3212
+ baseUrl: 'https://app.taskforce.example',
3213
+ apiBaseUrl: 'https://app.taskforce.example/api',
3214
+ cloudAuthBaseUrl: 'https://app.taskforce.example',
3215
+ runtimeMode: 'cloud',
3216
+ authSource: 'cloud',
3217
+ workspaceMode: 'multi-cloud',
3218
+ workspaceSwitchingEnabled: true,
3219
+ cloudAuthViaLocalProxy: false
3220
+ }
3221
+ })
3222
+ });
3223
+ }
2589
3224
  if (url.includes('/api/taskforce/auth/session')) {
2590
3225
  return Promise.resolve({
2591
3226
  ok: true,
@@ -2607,7 +3242,7 @@ describe('TaskforceCore Integration', () => {
2607
3242
  globalSetupState: 'present',
2608
3243
  workspaceSetupState: 'present',
2609
3244
  runtimeMode: 'cloud',
2610
- workspaceId: 'workspace-1',
3245
+ workspaceId: 'workspace-local-active',
2611
3246
  mode: 'core',
2612
3247
  forceSetup: false,
2613
3248
  forceGlobalSetup: false,
@@ -2616,80 +3251,97 @@ describe('TaskforceCore Integration', () => {
2616
3251
  })
2617
3252
  });
2618
3253
  }
2619
- if (url.includes('/api/taskforce/auth/invite/inspect')) {
3254
+ if (url.includes('/api/taskforce/workflow-templates')) {
3255
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
3256
+ }
3257
+ if (url.includes('/api/taskforce/public/pricing')) {
2620
3258
  return Promise.resolve({
2621
3259
  ok: true,
2622
3260
  json: () => Promise.resolve({
2623
- success: true,
2624
- state: 'valid',
2625
- email: 'new@example.com',
2626
- workspaceId: 'workspace-1',
2627
- inviteeKind: 'new_user',
2628
- passwordRequired: true
3261
+ asOf: '2026-03-16T00:00:00.000Z',
3262
+ plans: [
3263
+ {
3264
+ planId: 'free',
3265
+ displayName: 'Free',
3266
+ status: 'active',
3267
+ defaultVersion: { planVersionId: 'free-v1', versionNumber: 1, seatLimit: 1, metadata: {} },
3268
+ features: [],
3269
+ pricing: {
3270
+ month: { planVersionId: 'free-v1', interval: 'month', pricingType: 'free', stripePriceId: 'free:free-v1:month', displayAmount: '$0.00', unitAmount: 0, currency: 'usd' },
3271
+ year: { planVersionId: 'free-v1', interval: 'year', pricingType: 'free', stripePriceId: 'free:free-v1:year', displayAmount: '$0.00', unitAmount: 0, currency: 'usd' }
3272
+ }
3273
+ }
3274
+ ]
2629
3275
  })
2630
3276
  });
2631
3277
  }
2632
- if (url.includes('/api/taskforce/workflow-templates')) {
2633
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
3278
+ if (url.includes('/api/taskforce/auth/register')) {
3279
+ return Promise.resolve({
3280
+ ok: false,
3281
+ json: () => Promise.resolve({
3282
+ success: false,
3283
+ code: 'SIGNUP_PLAN_NOT_ENABLED',
3284
+ error: 'Configured onboarding plan version \"team-10-v1\" belongs to a disabled plan.'
3285
+ })
3286
+ });
2634
3287
  }
2635
3288
  return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
2636
3289
  });
2637
3290
  vi.stubGlobal('fetch', fetchMock);
2638
- render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=invite&token=invite-token&next=%2F'], children: _jsx(TaskforceCore, {}) }));
3291
+ render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
3292
+ await user.type(await screen.findByPlaceholderText('Email'), 'local@example.com');
3293
+ await user.type(screen.getByPlaceholderText('Display name'), 'Local User');
3294
+ await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
3295
+ await user.click(screen.getByRole('button', { name: 'Create Account' }));
2639
3296
  await waitFor(() => {
2640
- expect(screen.getByText('Create your account to join this workspace.')).toBeInTheDocument();
2641
- expect(screen.getByPlaceholderText('Create password')).toBeInTheDocument();
2642
- expect(screen.getByRole('button', { name: 'Create Account and Join' })).toBeInTheDocument();
3297
+ expect(screen.getByText('Choose a plan to continue.')).toBeInTheDocument();
3298
+ expect(screen.getByText('Subscription Plans')).toBeInTheDocument();
2643
3299
  });
2644
3300
  });
2645
- it('shows global setup route when forced and persists selected mode', async () => {
2646
- const user = userEvent.setup();
2647
- let selectedMode = null;
2648
- const fetchMock = vi.fn((url, init) => {
3301
+ it('shows sign-in continuation for existing-user invites', async () => {
3302
+ const fetchMock = vi.fn((url) => {
2649
3303
  if (url.includes('/api/taskforce/auth/session')) {
2650
3304
  return Promise.resolve({
2651
3305
  ok: true,
2652
3306
  json: () => Promise.resolve({
2653
- authenticated: true,
2654
- runtimeMode: 'local',
2655
- authRequiredForApi: false
3307
+ authenticated: false,
3308
+ runtimeMode: 'cloud',
3309
+ authRequiredForApi: true
2656
3310
  })
2657
3311
  });
2658
3312
  }
2659
3313
  if (url.includes('/api/taskforce/config')) {
2660
- const mode = selectedMode;
2661
3314
  return Promise.resolve({
2662
3315
  ok: true,
2663
3316
  json: () => Promise.resolve({
2664
3317
  ...mockConfig,
2665
- runtimeMode: 'local',
2666
- authRequiredForApi: false,
3318
+ runtimeMode: 'cloud',
3319
+ authRequiredForApi: true,
2667
3320
  setupState: {
2668
- globalSetupState: mode ? 'present' : 'missing',
3321
+ globalSetupState: 'present',
2669
3322
  workspaceSetupState: 'present',
2670
- runtimeMode: 'local',
2671
- workspaceId: 'default',
2672
- mode: mode || 'core',
3323
+ runtimeMode: 'cloud',
3324
+ workspaceId: 'workspace-1',
3325
+ mode: 'core',
2673
3326
  forceSetup: false,
2674
- forceGlobalSetup: true,
3327
+ forceGlobalSetup: false,
2675
3328
  forceWorkspaceSetup: false
2676
- },
2677
- runtimeCapabilities: {
2678
- runtimeMode: 'local',
2679
- supportsCloudAuth: false,
2680
- supportsAdminManagedSetupOverrides: false,
2681
- supportsPackageScriptSetupOverrides: true
2682
3329
  }
2683
3330
  })
2684
3331
  });
2685
3332
  }
2686
- if (url.includes('/api/taskforce/global-settings')) {
2687
- const body = JSON.parse(String(init?.body || '{}'));
2688
- selectedMode = body?.setup?.mode === 'operations' ? 'operations' : 'core';
2689
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
2690
- }
2691
- if (url.includes('/api/taskforce/tasks')) {
2692
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
3333
+ if (url.includes('/api/taskforce/auth/invite/inspect')) {
3334
+ return Promise.resolve({
3335
+ ok: true,
3336
+ json: () => Promise.resolve({
3337
+ success: true,
3338
+ state: 'valid',
3339
+ email: 'existing@example.com',
3340
+ workspaceId: 'workspace-1',
3341
+ inviteeKind: 'existing_user',
3342
+ passwordRequired: false
3343
+ })
3344
+ });
2693
3345
  }
2694
3346
  if (url.includes('/api/taskforce/workflow-templates')) {
2695
3347
  return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
@@ -2697,28 +3349,28 @@ describe('TaskforceCore Integration', () => {
2697
3349
  return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
2698
3350
  });
2699
3351
  vi.stubGlobal('fetch', fetchMock);
2700
- render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, {}) }));
2701
- await waitFor(() => {
2702
- expect(screen.getByText('Global Setup Required')).toBeInTheDocument();
2703
- });
2704
- await user.click(screen.getByRole('radio', { name: /operations mode/i }));
2705
- await user.click(screen.getByRole('button', { name: 'Save Global Setup' }));
3352
+ render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=invite&token=invite-token&next=%2F'], children: _jsx(TaskforceCore, {}) }));
2706
3353
  await waitFor(() => {
2707
- expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
3354
+ expect(screen.getByText('Sign in to join this workspace.')).toBeInTheDocument();
3355
+ expect(screen.getByDisplayValue('existing@example.com')).toBeInTheDocument();
3356
+ expect(screen.getByPlaceholderText('Password')).toBeInTheDocument();
3357
+ expect(screen.getByRole('button', { name: 'Sign In to Continue' })).toBeInTheDocument();
2708
3358
  });
2709
- expect(fetchMock).toHaveBeenCalledWith('/api/taskforce/global-settings', expect.objectContaining({ method: 'POST' }));
2710
3359
  });
2711
- it('shows workspace setup route when required and persists workspace profile', async () => {
2712
- const user = userEvent.setup();
2713
- let workspaceSaved = false;
2714
- const fetchMock = vi.fn((url, init) => {
3360
+ it('uses invite join after invite-mode sign-in even if invite reinspection has not refreshed yet', async () => {
3361
+ let authenticated = false;
3362
+ let inviteInspectCalls = 0;
3363
+ const fetchMock = vi.fn((url) => {
2715
3364
  if (url.includes('/api/taskforce/auth/session')) {
2716
3365
  return Promise.resolve({
2717
3366
  ok: true,
2718
3367
  json: () => Promise.resolve({
2719
- authenticated: true,
2720
- runtimeMode: 'local',
2721
- authRequiredForApi: false
3368
+ authenticated,
3369
+ runtimeMode: 'cloud',
3370
+ authRequiredForApi: true,
3371
+ userId: authenticated ? 'user-existing' : 'anonymous',
3372
+ workspaceId: authenticated ? 'workspace-home' : 'default',
3373
+ email: authenticated ? 'existing@example.com' : null
2722
3374
  })
2723
3375
  });
2724
3376
  }
@@ -2727,37 +3379,92 @@ describe('TaskforceCore Integration', () => {
2727
3379
  ok: true,
2728
3380
  json: () => Promise.resolve({
2729
3381
  ...mockConfig,
2730
- runtimeMode: 'local',
2731
- authRequiredForApi: false,
3382
+ runtimeMode: 'cloud',
3383
+ authRequiredForApi: true,
2732
3384
  setupState: {
2733
3385
  globalSetupState: 'present',
2734
- workspaceSetupState: workspaceSaved ? 'present' : 'missing',
2735
- runtimeMode: 'local',
2736
- workspaceId: 'default',
3386
+ workspaceSetupState: 'present',
3387
+ runtimeMode: 'cloud',
3388
+ workspaceId: authenticated ? 'workspace-home' : 'default',
2737
3389
  mode: 'core',
2738
3390
  forceSetup: false,
2739
3391
  forceGlobalSetup: false,
2740
- forceWorkspaceSetup: true,
2741
- workspace: workspaceSaved ? { id: 'default', slug: 'default', name: 'Delta Project', description: 'Workspace desc' } : null,
2742
- suggestedWorkspaceName: 'Suggested Project'
2743
- },
2744
- runtimeCapabilities: {
2745
- runtimeMode: 'local',
2746
- supportsCloudAuth: false,
2747
- supportsAdminManagedSetupOverrides: false,
2748
- supportsPackageScriptSetupOverrides: true
3392
+ forceWorkspaceSetup: false
2749
3393
  }
2750
3394
  })
2751
3395
  });
2752
3396
  }
2753
- if (url.includes('/api/taskforce/workspace-profile')) {
2754
- const body = JSON.parse(String(init?.body || '{}'));
2755
- if (body?.name === 'Delta Project')
2756
- workspaceSaved = true;
2757
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
3397
+ if (url.includes('/api/taskforce/auth/invite/inspect')) {
3398
+ inviteInspectCalls += 1;
3399
+ if (authenticated && inviteInspectCalls > 1) {
3400
+ return new Promise(() => undefined);
3401
+ }
3402
+ return Promise.resolve({
3403
+ ok: true,
3404
+ json: () => Promise.resolve({
3405
+ success: true,
3406
+ state: 'valid',
3407
+ email: 'existing@example.com',
3408
+ workspaceId: 'workspace-1',
3409
+ inviteeKind: 'existing_user',
3410
+ passwordRequired: false
3411
+ })
3412
+ });
3413
+ }
3414
+ if (url.includes('/api/taskforce/auth/login')) {
3415
+ authenticated = true;
3416
+ return Promise.resolve({
3417
+ ok: true,
3418
+ json: () => Promise.resolve({ success: true })
3419
+ });
3420
+ }
3421
+ if (url.includes('/api/taskforce/auth/invite/join')) {
3422
+ return Promise.resolve({
3423
+ ok: true,
3424
+ json: () => Promise.resolve({ success: true, workspaceId: 'workspace-1' })
3425
+ });
3426
+ }
3427
+ if (url.includes('/api/taskforce/workspaces')) {
3428
+ return Promise.resolve({
3429
+ ok: true,
3430
+ json: () => Promise.resolve({
3431
+ currentWorkspaceId: authenticated ? 'workspace-home' : null,
3432
+ workspaces: authenticated
3433
+ ? [{ id: 'workspace-home', name: 'Home workspace', role: 'owner' }]
3434
+ : []
3435
+ })
3436
+ });
2758
3437
  }
2759
3438
  if (url.includes('/api/taskforce/tasks')) {
2760
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
3439
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: [], config: {} }) });
3440
+ }
3441
+ if (url.includes('/api/taskforce/account/access-status')) {
3442
+ return Promise.resolve({
3443
+ ok: true,
3444
+ json: () => Promise.resolve({
3445
+ success: true,
3446
+ authenticated,
3447
+ commercialState: 'active',
3448
+ planSelectionRequired: false,
3449
+ checkoutPending: false
3450
+ })
3451
+ });
3452
+ }
3453
+ if (url.includes('/api/taskforce/account/profile-summary')) {
3454
+ return Promise.resolve({
3455
+ ok: true,
3456
+ json: () => Promise.resolve({
3457
+ success: true,
3458
+ currentWorkspaceId: authenticated ? 'workspace-home' : null,
3459
+ workspaces: authenticated ? [{ id: 'workspace-home', name: 'Home workspace' }] : []
3460
+ })
3461
+ });
3462
+ }
3463
+ if (url.includes('/api/taskforce/session/workspace')) {
3464
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true, workspaceId: 'workspace-1' }) });
3465
+ }
3466
+ if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
3467
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true, state: null }) });
2761
3468
  }
2762
3469
  if (url.includes('/api/taskforce/workflow-templates')) {
2763
3470
  return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
@@ -2765,160 +3472,1132 @@ describe('TaskforceCore Integration', () => {
2765
3472
  return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
2766
3473
  });
2767
3474
  vi.stubGlobal('fetch', fetchMock);
2768
- render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, {}) }));
3475
+ render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=invite&token=invite-token&next=%2F'], children: _jsx(TaskforceCore, {}) }));
2769
3476
  await waitFor(() => {
2770
- expect(screen.getByText('Workspace Setup Required')).toBeInTheDocument();
3477
+ expect(screen.getByRole('button', { name: 'Sign In to Continue' })).toBeInTheDocument();
2771
3478
  });
2772
- const nameInput = screen.getByPlaceholderText('Project name');
2773
- await user.clear(nameInput);
2774
- await user.type(nameInput, 'Delta Project');
2775
- await user.type(screen.getByPlaceholderText('Project description (optional)'), 'Workspace desc');
2776
- await user.click(screen.getByRole('button', { name: 'Save Project Setup' }));
3479
+ await userEvent.type(screen.getByPlaceholderText('Password'), 'ExistingUserPass123!');
3480
+ await userEvent.click(screen.getByTestId('auth-submit-button'));
2777
3481
  await waitFor(() => {
2778
- expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
3482
+ expect(screen.getByText('Signed in. Review the invite details to continue.')).toBeInTheDocument();
3483
+ expect(screen.getByRole('button', { name: 'Join Workspace' })).toBeInTheDocument();
2779
3484
  });
2780
- expect(fetchMock).toHaveBeenCalledWith('/api/taskforce/workspace-profile', expect.objectContaining({ method: 'POST' }));
2781
- const workspaceSyncStateCall = fetchMock.mock.calls.find((call) => {
2782
- if (!String(call[0]).includes('/api/taskforce/ui-state'))
2783
- return false;
2784
- const init = call[1];
2785
- if (String(init?.method || '').toUpperCase() !== 'POST')
2786
- return false;
2787
- const body = JSON.parse(String(init?.body || '{}'));
2788
- return body?.stateKey === 'workspace-sync';
3485
+ await userEvent.click(screen.getByTestId('auth-submit-button'));
3486
+ await waitFor(() => {
3487
+ expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('/api/taskforce/auth/invite/join'))).toBe(true);
2789
3488
  });
2790
- const workspaceSyncStateBody = JSON.parse(String(workspaceSyncStateCall?.[1]?.body || '{}'));
2791
- expect(workspaceSyncStateBody.stateKey).toBe('workspace-sync');
2792
- expect(workspaceSyncStateBody.patch?.enabled).toBe(false);
3489
+ expect(fetchMock.mock.calls.filter((call) => String(call[0]).includes('/api/taskforce/auth/login'))).toHaveLength(1);
2793
3490
  });
2794
- it('applies the selected starter taxonomy pack during workspace setup', async () => {
2795
- const user = userEvent.setup();
2796
- let workspaceSaved = false;
3491
+ it('keeps existing-user invites recoverable when the wrong account signs in first', async () => {
3492
+ let authenticated = false;
3493
+ let sessionEmail = null;
2797
3494
  const fetchMock = vi.fn((url, init) => {
2798
3495
  if (url.includes('/api/taskforce/auth/session')) {
2799
- return jsonResponse({
2800
- authenticated: true,
2801
- runtimeMode: 'local',
2802
- authRequiredForApi: false
3496
+ return Promise.resolve({
3497
+ ok: true,
3498
+ json: () => Promise.resolve({
3499
+ authenticated,
3500
+ runtimeMode: 'cloud',
3501
+ authRequiredForApi: true,
3502
+ userId: authenticated ? 'user-other' : 'anonymous',
3503
+ workspaceId: authenticated ? 'workspace-home' : 'default',
3504
+ email: sessionEmail
3505
+ })
2803
3506
  });
2804
3507
  }
2805
3508
  if (url.includes('/api/taskforce/config')) {
2806
- return jsonResponse({
2807
- ...mockConfig,
2808
- runtimeMode: 'local',
2809
- authRequiredForApi: false,
2810
- setupState: {
2811
- globalSetupState: 'present',
2812
- workspaceSetupState: workspaceSaved ? 'present' : 'missing',
2813
- runtimeMode: 'local',
2814
- workspaceId: workspaceSaved ? 'workspace-starter-1' : 'default',
2815
- mode: 'core',
2816
- forceSetup: false,
2817
- forceGlobalSetup: false,
2818
- forceWorkspaceSetup: true,
2819
- workspace: workspaceSaved
2820
- ? { id: 'workspace-starter-1', slug: 'workspace-starter-1', name: 'Starter Workspace', description: 'Starter desc' }
2821
- : null,
2822
- suggestedWorkspaceName: 'Starter Workspace'
2823
- },
2824
- runtimeCapabilities: {
3509
+ return Promise.resolve({
3510
+ ok: true,
3511
+ json: () => Promise.resolve({
3512
+ ...mockConfig,
3513
+ runtimeMode: 'cloud',
3514
+ authRequiredForApi: true,
3515
+ setupState: {
3516
+ globalSetupState: 'present',
3517
+ workspaceSetupState: 'present',
3518
+ runtimeMode: 'cloud',
3519
+ workspaceId: authenticated ? 'workspace-home' : 'default',
3520
+ mode: 'core',
3521
+ forceSetup: false,
3522
+ forceGlobalSetup: false,
3523
+ forceWorkspaceSetup: false
3524
+ }
3525
+ })
3526
+ });
3527
+ }
3528
+ if (url.includes('/api/taskforce/auth/invite/inspect')) {
3529
+ return Promise.resolve({
3530
+ ok: true,
3531
+ json: () => Promise.resolve({
3532
+ success: true,
3533
+ state: 'valid',
3534
+ email: 'existing@example.com',
3535
+ workspaceId: 'workspace-1',
3536
+ inviteeKind: 'existing_user',
3537
+ passwordRequired: false
3538
+ })
3539
+ });
3540
+ }
3541
+ if (url.includes('/api/taskforce/auth/login')) {
3542
+ const body = typeof init?.body === 'string' ? JSON.parse(init.body) : {};
3543
+ authenticated = true;
3544
+ sessionEmail = String(body?.email || '').trim().toLowerCase() || null;
3545
+ return Promise.resolve({
3546
+ ok: true,
3547
+ json: () => Promise.resolve({ success: true })
3548
+ });
3549
+ }
3550
+ if (url.includes('/api/taskforce/auth/invite/join')) {
3551
+ return Promise.resolve({
3552
+ ok: false,
3553
+ json: () => Promise.resolve({
3554
+ success: false,
3555
+ code: 'INVITE_ACCOUNT_MISMATCH',
3556
+ error: 'Sign in with the invited account to join this workspace.'
3557
+ })
3558
+ });
3559
+ }
3560
+ if (url.includes('/api/taskforce/workspaces')) {
3561
+ return Promise.resolve({
3562
+ ok: true,
3563
+ json: () => Promise.resolve({
3564
+ currentWorkspaceId: authenticated ? 'workspace-home' : null,
3565
+ workspaces: authenticated
3566
+ ? [{ id: 'workspace-home', name: 'Home workspace', role: 'owner' }]
3567
+ : []
3568
+ })
3569
+ });
3570
+ }
3571
+ if (url.includes('/api/taskforce/tasks')) {
3572
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: [], config: {} }) });
3573
+ }
3574
+ if (url.includes('/api/taskforce/account/access-status')) {
3575
+ return Promise.resolve({
3576
+ ok: true,
3577
+ json: () => Promise.resolve({
3578
+ success: true,
3579
+ authenticated,
3580
+ commercialState: 'active',
3581
+ planSelectionRequired: false,
3582
+ checkoutPending: false
3583
+ })
3584
+ });
3585
+ }
3586
+ if (url.includes('/api/taskforce/account/profile-summary')) {
3587
+ return Promise.resolve({
3588
+ ok: true,
3589
+ json: () => Promise.resolve({
3590
+ success: true,
3591
+ currentWorkspaceId: authenticated ? 'workspace-home' : null,
3592
+ workspaces: authenticated ? [{ id: 'workspace-home', name: 'Home workspace' }] : []
3593
+ })
3594
+ });
3595
+ }
3596
+ if (url.includes('/api/taskforce/session/workspace')) {
3597
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true, workspaceId: 'workspace-home' }) });
3598
+ }
3599
+ if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
3600
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true, state: null }) });
3601
+ }
3602
+ if (url.includes('/api/taskforce/workflow-templates')) {
3603
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
3604
+ }
3605
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
3606
+ });
3607
+ vi.stubGlobal('fetch', fetchMock);
3608
+ render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=invite&token=invite-token&next=%2F'], children: _jsx(TaskforceCore, {}) }));
3609
+ await waitFor(() => {
3610
+ expect(screen.getByRole('button', { name: 'Sign In to Continue' })).toBeInTheDocument();
3611
+ });
3612
+ await userEvent.clear(screen.getByPlaceholderText('Email'));
3613
+ await userEvent.type(screen.getByPlaceholderText('Email'), 'other@example.com');
3614
+ await userEvent.type(screen.getByPlaceholderText('Password'), 'OtherUserPass123!');
3615
+ await userEvent.click(screen.getByTestId('auth-submit-button'));
3616
+ await waitFor(() => {
3617
+ expect(screen.getByText('Sign in with the invited account to join this workspace.')).toBeInTheDocument();
3618
+ expect(screen.getByDisplayValue('existing@example.com')).toBeInTheDocument();
3619
+ expect(screen.getByPlaceholderText('Password')).toBeInTheDocument();
3620
+ expect(screen.queryByRole('button', { name: 'Join Workspace' })).not.toBeInTheDocument();
3621
+ });
3622
+ expect(fetchMock.mock.calls.filter((call) => String(call[0]).includes('/api/taskforce/auth/login'))).toHaveLength(1);
3623
+ expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('/api/taskforce/auth/invite/join'))).toBe(false);
3624
+ });
3625
+ it('shows password creation for new-user invites', async () => {
3626
+ const fetchMock = vi.fn((url) => {
3627
+ if (url.includes('/api/taskforce/auth/session')) {
3628
+ return Promise.resolve({
3629
+ ok: true,
3630
+ json: () => Promise.resolve({
3631
+ authenticated: false,
3632
+ runtimeMode: 'cloud',
3633
+ authRequiredForApi: true
3634
+ })
3635
+ });
3636
+ }
3637
+ if (url.includes('/api/taskforce/config')) {
3638
+ return Promise.resolve({
3639
+ ok: true,
3640
+ json: () => Promise.resolve({
3641
+ ...mockConfig,
3642
+ runtimeMode: 'cloud',
3643
+ authRequiredForApi: true,
3644
+ setupState: {
3645
+ globalSetupState: 'present',
3646
+ workspaceSetupState: 'present',
3647
+ runtimeMode: 'cloud',
3648
+ workspaceId: 'workspace-1',
3649
+ mode: 'core',
3650
+ forceSetup: false,
3651
+ forceGlobalSetup: false,
3652
+ forceWorkspaceSetup: false
3653
+ }
3654
+ })
3655
+ });
3656
+ }
3657
+ if (url.includes('/api/taskforce/auth/invite/inspect')) {
3658
+ return Promise.resolve({
3659
+ ok: true,
3660
+ json: () => Promise.resolve({
3661
+ success: true,
3662
+ state: 'valid',
3663
+ email: 'new@example.com',
3664
+ workspaceId: 'workspace-1',
3665
+ inviteeKind: 'new_user',
3666
+ passwordRequired: true
3667
+ })
3668
+ });
3669
+ }
3670
+ if (url.includes('/api/taskforce/workflow-templates')) {
3671
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
3672
+ }
3673
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
3674
+ });
3675
+ vi.stubGlobal('fetch', fetchMock);
3676
+ render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=invite&token=invite-token&next=%2F'], children: _jsx(TaskforceCore, {}) }));
3677
+ await waitFor(() => {
3678
+ expect(screen.getByText('Create your account to join this workspace.')).toBeInTheDocument();
3679
+ expect(screen.getByPlaceholderText('Create password')).toBeInTheDocument();
3680
+ expect(screen.getByRole('button', { name: 'Create Account and Join' })).toBeInTheDocument();
3681
+ });
3682
+ });
3683
+ it('shows global setup route when forced and persists selected mode', async () => {
3684
+ const user = userEvent.setup();
3685
+ let selectedMode = null;
3686
+ const fetchMock = vi.fn((url, init) => {
3687
+ if (url.includes('/api/taskforce/auth/session')) {
3688
+ return Promise.resolve({
3689
+ ok: true,
3690
+ json: () => Promise.resolve({
3691
+ authenticated: true,
2825
3692
  runtimeMode: 'local',
2826
- supportsCloudAuth: false,
2827
- supportsAdminManagedSetupOverrides: false,
2828
- supportsPackageScriptSetupOverrides: true
2829
- }
3693
+ authRequiredForApi: false
3694
+ })
2830
3695
  });
2831
3696
  }
2832
- if (url.includes('/api/taskforce/taxonomy-library')) {
2833
- return jsonResponse({
2834
- packs: [
2835
- {
2836
- id: 'general',
2837
- label: 'General',
2838
- description: 'Neutral starter pack.',
2839
- isDefaultStarter: true,
2840
- categories: [{ value: 'general', label: 'General', icon: 'Inbox', color: 'slate-400' }],
2841
- types: [{ value: 'task', label: 'Task', icon: 'CheckSquare', color: 'blue-500' }],
2842
- priorities: [{ value: 1, label: 'Low', icon: 'ArrowDown', color: 'amber-200' }]
3697
+ if (url.includes('/api/taskforce/config')) {
3698
+ const mode = selectedMode;
3699
+ return Promise.resolve({
3700
+ ok: true,
3701
+ json: () => Promise.resolve({
3702
+ ...mockConfig,
3703
+ runtimeMode: 'local',
3704
+ authRequiredForApi: false,
3705
+ setupState: {
3706
+ globalSetupState: mode ? 'present' : 'missing',
3707
+ workspaceSetupState: 'present',
3708
+ runtimeMode: 'local',
3709
+ workspaceId: 'default',
3710
+ mode: mode || 'core',
3711
+ forceSetup: false,
3712
+ forceGlobalSetup: true,
3713
+ forceWorkspaceSetup: false
2843
3714
  },
2844
- {
2845
- id: 'software-development',
2846
- label: 'Software Development',
2847
- description: 'Software starter pack.',
2848
- categories: [{ value: 'engineering', label: 'Engineering', icon: 'Code', color: 'blue-500' }],
2849
- types: [
2850
- { value: 'feature', label: 'Feature', icon: 'Star', color: 'teal-500' },
2851
- { value: 'bug', label: 'Bug', icon: 'Bug', color: 'red-500' }
2852
- ],
2853
- priorities: [
2854
- { value: 1, label: 'Low', icon: 'ArrowDown', color: 'amber-200' },
2855
- { value: 2, label: 'Medium', icon: 'Minus', color: 'yellow-500' },
2856
- { value: 3, label: 'High', icon: 'ArrowUp', color: 'orange-500' },
2857
- { value: 4, label: 'Critical', icon: 'AlertTriangle', color: 'red-500' }
2858
- ]
3715
+ runtimeCapabilities: {
3716
+ runtimeMode: 'local',
3717
+ supportsCloudAuth: false,
3718
+ supportsAdminManagedSetupOverrides: false,
3719
+ supportsPackageScriptSetupOverrides: true
2859
3720
  }
2860
- ]
3721
+ })
2861
3722
  });
2862
3723
  }
2863
- if (url.includes('/api/taskforce/workspace-profile')) {
3724
+ if (url.includes('/api/taskforce/global-settings')) {
2864
3725
  const body = JSON.parse(String(init?.body || '{}'));
2865
- if (body?.name === 'Starter Workspace')
2866
- workspaceSaved = true;
2867
- return jsonResponse({
2868
- success: true,
2869
- workspace: {
2870
- id: 'workspace-starter-1'
2871
- }
3726
+ selectedMode = body?.setup?.mode === 'operations' ? 'operations' : 'core';
3727
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
3728
+ }
3729
+ if (url.includes('/api/taskforce/tasks')) {
3730
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
3731
+ }
3732
+ if (url.includes('/api/taskforce/workflow-templates')) {
3733
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
3734
+ }
3735
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
3736
+ });
3737
+ vi.stubGlobal('fetch', fetchMock);
3738
+ render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, {}) }));
3739
+ await waitFor(() => {
3740
+ expect(screen.getByText('Global Setup Required')).toBeInTheDocument();
3741
+ });
3742
+ await user.click(screen.getByRole('radio', { name: /operations mode/i }));
3743
+ await user.click(screen.getByRole('button', { name: 'Save Global Setup' }));
3744
+ await waitFor(() => {
3745
+ expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
3746
+ });
3747
+ expect(fetchMock).toHaveBeenCalledWith('/api/taskforce/global-settings', expect.objectContaining({ method: 'POST' }));
3748
+ });
3749
+ it('shows workspace setup route when required and persists workspace profile', async () => {
3750
+ const user = userEvent.setup();
3751
+ let workspaceSaved = false;
3752
+ const fetchMock = vi.fn((url, init) => {
3753
+ if (url.includes('/api/taskforce/auth/session')) {
3754
+ return Promise.resolve({
3755
+ ok: true,
3756
+ json: () => Promise.resolve({
3757
+ authenticated: true,
3758
+ runtimeMode: 'local',
3759
+ authRequiredForApi: false
3760
+ })
3761
+ });
3762
+ }
3763
+ if (url.includes('/api/taskforce/config')) {
3764
+ return Promise.resolve({
3765
+ ok: true,
3766
+ json: () => Promise.resolve({
3767
+ ...mockConfig,
3768
+ runtimeMode: 'local',
3769
+ authRequiredForApi: false,
3770
+ setupState: {
3771
+ globalSetupState: 'present',
3772
+ workspaceSetupState: workspaceSaved ? 'present' : 'missing',
3773
+ runtimeMode: 'local',
3774
+ workspaceId: 'default',
3775
+ mode: 'core',
3776
+ forceSetup: false,
3777
+ forceGlobalSetup: false,
3778
+ forceWorkspaceSetup: true,
3779
+ workspace: workspaceSaved ? { id: 'default', slug: 'default', name: 'Delta Project', description: 'Workspace desc' } : null,
3780
+ suggestedWorkspaceName: 'Suggested Project'
3781
+ },
3782
+ runtimeCapabilities: {
3783
+ runtimeMode: 'local',
3784
+ supportsCloudAuth: false,
3785
+ supportsAdminManagedSetupOverrides: false,
3786
+ supportsPackageScriptSetupOverrides: true
3787
+ }
3788
+ })
3789
+ });
3790
+ }
3791
+ if (url.includes('/api/taskforce/workspace-profile')) {
3792
+ const body = JSON.parse(String(init?.body || '{}'));
3793
+ if (body?.name === 'Delta Project')
3794
+ workspaceSaved = true;
3795
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
3796
+ }
3797
+ if (url.includes('/api/taskforce/tasks')) {
3798
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
3799
+ }
3800
+ if (url.includes('/api/taskforce/workflow-templates')) {
3801
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
3802
+ }
3803
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
3804
+ });
3805
+ vi.stubGlobal('fetch', fetchMock);
3806
+ render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, {}) }));
3807
+ await waitFor(() => {
3808
+ expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
3809
+ });
3810
+ const nameInput = screen.getByPlaceholderText('Workspace name');
3811
+ await user.clear(nameInput);
3812
+ await user.type(nameInput, 'Delta Project');
3813
+ await user.type(screen.getByPlaceholderText('Workspace description (optional)'), 'Workspace desc');
3814
+ await user.click(screen.getByRole('button', { name: 'Continue' }));
3815
+ await user.click(screen.getByRole('button', { name: 'Create Workspace' }));
3816
+ await waitFor(() => {
3817
+ expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
3818
+ });
3819
+ expect(fetchMock).toHaveBeenCalledWith('/api/taskforce/workspace-profile', expect.objectContaining({ method: 'POST' }));
3820
+ const workspaceSyncStateCall = fetchMock.mock.calls.find((call) => {
3821
+ if (!String(call[0]).includes('/api/taskforce/ui-state'))
3822
+ return false;
3823
+ const init = call[1];
3824
+ if (String(init?.method || '').toUpperCase() !== 'POST')
3825
+ return false;
3826
+ const body = JSON.parse(String(init?.body || '{}'));
3827
+ return body?.stateKey === 'workspace-sync';
3828
+ });
3829
+ const workspaceSyncStateBody = JSON.parse(String(workspaceSyncStateCall?.[1]?.body || '{}'));
3830
+ expect(workspaceSyncStateBody.stateKey).toBe('workspace-sync');
3831
+ expect(workspaceSyncStateBody.patch?.enabled).toBe(false);
3832
+ });
3833
+ it('creates the first cloud workspace through the workspace creation flow during setup', async () => {
3834
+ const user = userEvent.setup();
3835
+ let workspaceCreated = false;
3836
+ const fetchMock = vi.fn((input, init) => {
3837
+ const url = String(input);
3838
+ if (url.includes('/api/taskforce/auth/runtime-config')) {
3839
+ return jsonResponse({
3840
+ success: true,
3841
+ config: {
3842
+ baseUrl: 'https://app.taskforce.example',
3843
+ apiBaseUrl: 'https://app.taskforce.example/api',
3844
+ cloudAuthBaseUrl: 'https://app.taskforce.example',
3845
+ runtimeMode: 'cloud',
3846
+ authSource: 'cloud',
3847
+ workspaceMode: 'multi-cloud',
3848
+ workspaceSwitchingEnabled: true,
3849
+ cloudAuthViaLocalProxy: false
3850
+ }
3851
+ });
3852
+ }
3853
+ if (url.includes('/api/taskforce/auth/session')) {
3854
+ return jsonResponse({
3855
+ authenticated: true,
3856
+ runtimeMode: 'cloud',
3857
+ authRequiredForApi: true,
3858
+ userId: 'user-cloud-1',
3859
+ workspaceId: workspaceCreated ? 'workspace-cloud-new' : 'default'
3860
+ });
3861
+ }
3862
+ if (url.includes('/api/taskforce/account/access-status')) {
3863
+ return jsonResponse({
3864
+ gate: 'ok',
3865
+ hasEntitlement: true,
3866
+ canAccessApp: true,
3867
+ canAccessPlans: true,
3868
+ message: null
3869
+ });
3870
+ }
3871
+ if (url.includes('/api/taskforce/config')) {
3872
+ return jsonResponse({
3873
+ ...mockConfig,
3874
+ runtimeMode: 'cloud',
3875
+ authRequiredForApi: true,
3876
+ userId: 'user-cloud-1',
3877
+ projectName: workspaceCreated ? 'Cloud Mission' : '',
3878
+ setupState: {
3879
+ globalSetupState: 'present',
3880
+ workspaceSetupState: workspaceCreated ? 'present' : 'missing',
3881
+ runtimeMode: 'cloud',
3882
+ workspaceId: workspaceCreated ? 'workspace-cloud-new' : 'default',
3883
+ mode: 'core',
3884
+ forceSetup: false,
3885
+ forceGlobalSetup: false,
3886
+ forceWorkspaceSetup: false,
3887
+ workspace: workspaceCreated
3888
+ ? { id: 'workspace-cloud-new', slug: 'workspace-cloud-new', name: 'Cloud Mission', description: 'Cloud workspace desc' }
3889
+ : null,
3890
+ suggestedWorkspaceName: ''
3891
+ },
3892
+ runtimeCapabilities: {
3893
+ runtimeMode: 'cloud',
3894
+ supportsCloudAuth: true,
3895
+ supportsAdminManagedSetupOverrides: true,
3896
+ supportsPackageScriptSetupOverrides: false
3897
+ }
3898
+ });
3899
+ }
3900
+ if (url.includes('/api/taskforce/workspaces') && String(init?.method || 'GET').toUpperCase() === 'GET') {
3901
+ return jsonResponse({
3902
+ success: true,
3903
+ workspaces: workspaceCreated
3904
+ ? [{
3905
+ id: 'workspace-cloud-new',
3906
+ slug: 'workspace-cloud-new',
3907
+ name: 'Cloud Mission',
3908
+ description: 'Cloud workspace desc',
3909
+ role: 'owner',
3910
+ status: 'active',
3911
+ betaAccess: true
3912
+ }]
3913
+ : [],
3914
+ currentWorkspaceId: workspaceCreated ? 'workspace-cloud-new' : 'default'
3915
+ });
3916
+ }
3917
+ if (url.includes('/api/taskforce/workspaces') && String(init?.method || '').toUpperCase() === 'POST') {
3918
+ workspaceCreated = true;
3919
+ return jsonResponse({
3920
+ success: true,
3921
+ workspace: {
3922
+ id: 'workspace-cloud-new',
3923
+ slug: 'workspace-cloud-new',
3924
+ name: 'Cloud Mission',
3925
+ description: 'Cloud workspace desc',
3926
+ role: 'owner',
3927
+ status: 'active',
3928
+ betaAccess: true
3929
+ }
3930
+ }, 201);
3931
+ }
3932
+ if (url.includes('/api/taskforce/session/workspace')) {
3933
+ return jsonResponse({
3934
+ success: true,
3935
+ workspaceId: 'workspace-cloud-new',
3936
+ role: 'owner'
3937
+ });
3938
+ }
3939
+ if (url.includes('/api/taskforce/ui-state?key=app')) {
3940
+ return jsonResponse({ success: true, state: {} });
3941
+ }
3942
+ if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
3943
+ return jsonResponse({
3944
+ success: true,
3945
+ state: {
3946
+ version: 2,
3947
+ enabled: false,
3948
+ phase: 'idle',
3949
+ pullCursor: null,
3950
+ lastPullAt: null,
3951
+ lastPushAt: null,
3952
+ lastSyncedAt: null
3953
+ }
3954
+ });
3955
+ }
3956
+ if (url.includes('/api/taskforce/ui-state')) {
3957
+ return jsonResponse({ success: true });
3958
+ }
3959
+ if (url.includes('/api/taskforce/categories')) {
3960
+ return jsonResponse(mockCategories);
3961
+ }
3962
+ if (url.includes('/api/taskforce/tasks')) {
3963
+ return jsonResponse({ tasks: mockTasks });
3964
+ }
3965
+ if (url.includes('/api/taskforce/archive')) {
3966
+ return jsonResponse({ archived: [] });
3967
+ }
3968
+ if (url.includes('/api/taskforce/data-version')) {
3969
+ return jsonResponse({ dataVersion: 1 });
3970
+ }
3971
+ if (url.includes('/api/taskforce/types')) {
3972
+ return jsonResponse({ types: [] });
3973
+ }
3974
+ if (url.includes('/api/taskforce/priorities')) {
3975
+ return jsonResponse({ priorities: [] });
3976
+ }
3977
+ if (url.includes('/api/taskforce/approaches')) {
3978
+ return jsonResponse({ approaches: [] });
3979
+ }
3980
+ if (url.includes('/api/taskforce/specialists')) {
3981
+ return jsonResponse({ specialists: [] });
3982
+ }
3983
+ if (url.includes('/api/taskforce/workflow-templates')) {
3984
+ return jsonResponse({ templates: [] });
3985
+ }
3986
+ return jsonResponse({});
3987
+ });
3988
+ vi.stubGlobal('fetch', fetchMock);
3989
+ render(_jsx(MemoryRouter, { initialEntries: ['/setup?step=workspace'], children: _jsx(TaskforceCore, {}) }));
3990
+ await waitFor(() => {
3991
+ expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
3992
+ });
3993
+ expect(screen.getByLabelText('Cloud runtime')).toBeInTheDocument();
3994
+ const nameInput = screen.getByPlaceholderText('Workspace name');
3995
+ await user.clear(nameInput);
3996
+ await user.type(nameInput, 'Cloud Mission');
3997
+ await user.type(screen.getByPlaceholderText('Workspace description (optional)'), 'Cloud workspace desc');
3998
+ await user.click(screen.getByRole('button', { name: 'Continue' }));
3999
+ await user.click(screen.getByRole('button', { name: 'Create Workspace' }));
4000
+ await waitFor(() => {
4001
+ expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
4002
+ });
4003
+ expect(fetchMock.mock.calls.some((call) => (String(call[0]).includes('/api/taskforce/workspaces')
4004
+ && String(call[1]?.method || '').toUpperCase() === 'POST'))).toBe(true);
4005
+ expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('/api/taskforce/workspace-profile'))).toBe(false);
4006
+ });
4007
+ it('applies the selected starter taxonomy pack during workspace setup', async () => {
4008
+ const user = userEvent.setup();
4009
+ let workspaceSaved = false;
4010
+ const fetchMock = vi.fn((url, init) => {
4011
+ if (url.includes('/api/taskforce/auth/session')) {
4012
+ return jsonResponse({
4013
+ authenticated: true,
4014
+ runtimeMode: 'local',
4015
+ authRequiredForApi: false
4016
+ });
4017
+ }
4018
+ if (url.includes('/api/taskforce/config')) {
4019
+ return jsonResponse({
4020
+ ...mockConfig,
4021
+ runtimeMode: 'local',
4022
+ authRequiredForApi: false,
4023
+ setupState: {
4024
+ globalSetupState: 'present',
4025
+ workspaceSetupState: workspaceSaved ? 'present' : 'missing',
4026
+ runtimeMode: 'local',
4027
+ workspaceId: workspaceSaved ? 'workspace-starter-1' : 'default',
4028
+ mode: 'core',
4029
+ forceSetup: false,
4030
+ forceGlobalSetup: false,
4031
+ forceWorkspaceSetup: true,
4032
+ workspace: workspaceSaved
4033
+ ? { id: 'workspace-starter-1', slug: 'workspace-starter-1', name: 'Starter Workspace', description: 'Starter desc' }
4034
+ : null,
4035
+ suggestedWorkspaceName: 'Starter Workspace'
4036
+ },
4037
+ runtimeCapabilities: {
4038
+ runtimeMode: 'local',
4039
+ supportsCloudAuth: false,
4040
+ supportsAdminManagedSetupOverrides: false,
4041
+ supportsPackageScriptSetupOverrides: true
4042
+ }
4043
+ });
4044
+ }
4045
+ if (url.includes('/api/taskforce/taxonomy-library')) {
4046
+ return jsonResponse({
4047
+ packs: [
4048
+ {
4049
+ id: 'general',
4050
+ label: 'General',
4051
+ description: 'Neutral starter pack.',
4052
+ isDefaultStarter: true,
4053
+ categories: [{ value: 'general', label: 'General', icon: 'Inbox', color: 'slate-400' }],
4054
+ types: [{ value: 'task', label: 'Task', icon: 'CheckSquare', color: 'blue-500' }],
4055
+ priorities: [{ value: 1, label: 'Low', icon: 'ArrowDown', color: 'amber-200' }]
4056
+ },
4057
+ {
4058
+ id: 'software-development',
4059
+ label: 'Software Development',
4060
+ description: 'Software starter pack.',
4061
+ categories: [{ value: 'engineering', label: 'Engineering', icon: 'Code', color: 'blue-500' }],
4062
+ types: [
4063
+ { value: 'feature', label: 'Feature', icon: 'Star', color: 'teal-500' },
4064
+ { value: 'bug', label: 'Bug', icon: 'Bug', color: 'red-500' }
4065
+ ],
4066
+ priorities: [
4067
+ { value: 1, label: 'Low', icon: 'ArrowDown', color: 'amber-200' },
4068
+ { value: 2, label: 'Medium', icon: 'Minus', color: 'yellow-500' },
4069
+ { value: 3, label: 'High', icon: 'ArrowUp', color: 'orange-500' },
4070
+ { value: 4, label: 'Critical', icon: 'AlertTriangle', color: 'red-500' }
4071
+ ]
4072
+ }
4073
+ ]
4074
+ });
4075
+ }
4076
+ if (url.includes('/api/taskforce/workspace-profile')) {
4077
+ const body = JSON.parse(String(init?.body || '{}'));
4078
+ if (body?.name === 'Starter Workspace')
4079
+ workspaceSaved = true;
4080
+ return jsonResponse({
4081
+ success: true,
4082
+ workspace: {
4083
+ id: 'workspace-starter-1'
4084
+ }
4085
+ });
4086
+ }
4087
+ if (url.includes('/api/taskforce/categories?workspaceId=workspace-starter-1')) {
4088
+ return jsonResponse({ success: true });
4089
+ }
4090
+ if (url.includes('/api/taskforce/types?workspaceId=workspace-starter-1')) {
4091
+ return jsonResponse({ success: true });
4092
+ }
4093
+ if (url.includes('/api/taskforce/priorities?workspaceId=workspace-starter-1')) {
4094
+ return jsonResponse({ success: true });
4095
+ }
4096
+ if (url.includes('/api/taskforce/ui-state')) {
4097
+ return jsonResponse({ success: true });
4098
+ }
4099
+ if (url.includes('/api/taskforce/tasks')) {
4100
+ return jsonResponse({ tasks: mockTasks });
4101
+ }
4102
+ if (url.includes('/api/taskforce/archive')) {
4103
+ return jsonResponse({ archived: [] });
4104
+ }
4105
+ if (url.includes('/api/taskforce/workflow-templates')) {
4106
+ return jsonResponse({ templates: [] });
4107
+ }
4108
+ return jsonResponse({});
4109
+ });
4110
+ vi.stubGlobal('fetch', fetchMock);
4111
+ render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, {}) }));
4112
+ await waitFor(() => {
4113
+ expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
4114
+ });
4115
+ expect(screen.getByLabelText('Local runtime')).toBeInTheDocument();
4116
+ await user.click(screen.getByRole('button', { name: 'Continue' }));
4117
+ expect(screen.queryByRole('radio', { name: /create new local workspace/i })).not.toBeInTheDocument();
4118
+ expect(screen.queryByRole('radio', { name: /sync existing cloud workspace/i })).not.toBeInTheDocument();
4119
+ expect(screen.queryByText(/Includes \d+ categories?/i)).not.toBeInTheDocument();
4120
+ expect(screen.getByRole('option', { name: 'General' })).toBeInTheDocument();
4121
+ expect(screen.getByText('Task')).toBeInTheDocument();
4122
+ expect(screen.getByText('Low')).toBeInTheDocument();
4123
+ await user.selectOptions(screen.getByRole('combobox'), 'software-development');
4124
+ expect(screen.getByText('Engineering')).toBeInTheDocument();
4125
+ expect(screen.getByText('Feature, Bug')).toBeInTheDocument();
4126
+ expect(screen.getByText('Low, Medium, High, Critical')).toBeInTheDocument();
4127
+ await user.click(screen.getByRole('button', { name: 'Continue' }));
4128
+ await user.click(screen.getByRole('button', { name: 'Create Workspace' }));
4129
+ await waitFor(() => {
4130
+ expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
4131
+ });
4132
+ const categoryApplyCall = fetchMock.mock.calls.find((call) => (String(call[0]).includes('/api/taskforce/categories?workspaceId=workspace-starter-1')
4133
+ && String(call[1]?.method || '').toUpperCase() === 'POST'));
4134
+ const typeApplyCall = fetchMock.mock.calls.find((call) => (String(call[0]).includes('/api/taskforce/types?workspaceId=workspace-starter-1')
4135
+ && String(call[1]?.method || '').toUpperCase() === 'POST'));
4136
+ const priorityApplyCall = fetchMock.mock.calls.find((call) => (String(call[0]).includes('/api/taskforce/priorities?workspaceId=workspace-starter-1')
4137
+ && String(call[1]?.method || '').toUpperCase() === 'POST'));
4138
+ expect(categoryApplyCall).toBeDefined();
4139
+ expect(typeApplyCall).toBeDefined();
4140
+ expect(priorityApplyCall).toBeDefined();
4141
+ expect(new Headers(categoryApplyCall?.[1]?.headers).get('x-taskforce-workspace-id')).toBe('workspace-starter-1');
4142
+ expect(new Headers(typeApplyCall?.[1]?.headers).get('x-taskforce-workspace-id')).toBe('workspace-starter-1');
4143
+ expect(new Headers(priorityApplyCall?.[1]?.headers).get('x-taskforce-workspace-id')).toBe('workspace-starter-1');
4144
+ });
4145
+ it('recovers from transient local workspace-missing setup state after scope resolves on refresh', async () => {
4146
+ let configCalls = 0;
4147
+ const fetchMock = vi.fn((url) => {
4148
+ if (url.includes('/api/taskforce/auth/session')) {
4149
+ return Promise.resolve({
4150
+ ok: true,
4151
+ json: () => Promise.resolve({
4152
+ authenticated: false,
4153
+ runtimeMode: 'local',
4154
+ authRequiredForApi: false
4155
+ })
4156
+ });
4157
+ }
4158
+ if (url.includes('/api/taskforce/config')) {
4159
+ configCalls += 1;
4160
+ const firstPass = configCalls === 1;
4161
+ return Promise.resolve({
4162
+ ok: true,
4163
+ json: () => Promise.resolve({
4164
+ ...mockConfig,
4165
+ runtimeMode: 'local',
4166
+ authRequiredForApi: false,
4167
+ projectRoot: '/mock/local-project-refresh',
4168
+ workspaceId: 'workspace-local-refresh',
4169
+ setupState: {
4170
+ globalSetupState: 'present',
4171
+ workspaceSetupState: firstPass ? 'missing' : 'present',
4172
+ runtimeMode: 'local',
4173
+ workspaceId: 'workspace-local-refresh',
4174
+ mode: 'core',
4175
+ forceSetup: false,
4176
+ forceGlobalSetup: false,
4177
+ forceWorkspaceSetup: firstPass,
4178
+ workspace: firstPass
4179
+ ? null
4180
+ : { id: 'workspace-local-refresh', slug: 'workspace-local-refresh', name: 'Local Refresh Project', description: null },
4181
+ suggestedWorkspaceName: 'Local Refresh Project'
4182
+ },
4183
+ runtimeCapabilities: {
4184
+ runtimeMode: 'local',
4185
+ supportsCloudAuth: true,
4186
+ supportsAdminManagedSetupOverrides: false,
4187
+ supportsPackageScriptSetupOverrides: true
4188
+ }
4189
+ })
4190
+ });
4191
+ }
4192
+ if (url.includes('/api/taskforce/categories')) {
4193
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(mockCategories) });
4194
+ }
4195
+ if (url.includes('/api/taskforce/tasks')) {
4196
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
4197
+ }
4198
+ if (url.includes('/api/taskforce/types')) {
4199
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ types: [] }) });
4200
+ }
4201
+ if (url.includes('/api/taskforce/priorities')) {
4202
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ priorities: [] }) });
4203
+ }
4204
+ if (url.includes('/api/taskforce/approaches')) {
4205
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ approaches: [] }) });
4206
+ }
4207
+ if (url.includes('/api/taskforce/specialists')) {
4208
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
4209
+ }
4210
+ if (url.includes('/api/taskforce/workflow-templates')) {
4211
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
4212
+ }
4213
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
4214
+ });
4215
+ vi.stubGlobal('fetch', fetchMock);
4216
+ render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, {}) }));
4217
+ await waitFor(() => {
4218
+ expect(configCalls).toBeGreaterThan(1);
4219
+ });
4220
+ expect(screen.getByRole('button', { name: /add task/i })).toBeInTheDocument();
4221
+ expect(screen.queryByText('Configure your new workspace.')).not.toBeInTheDocument();
4222
+ });
4223
+ it('keeps setup blocking while the selected cloud workspace is performing its first local import', async () => {
4224
+ const user = userEvent.setup();
4225
+ let workspaceSaved = false;
4226
+ const fetchMock = vi.fn((url, init) => {
4227
+ if (url.includes('/api/taskforce/auth/session')) {
4228
+ return Promise.resolve({
4229
+ ok: true,
4230
+ json: () => Promise.resolve({
4231
+ authenticated: true,
4232
+ runtimeMode: 'local',
4233
+ authRequiredForApi: false
4234
+ })
4235
+ });
4236
+ }
4237
+ if (url.includes('/api/taskforce/config')) {
4238
+ return Promise.resolve({
4239
+ ok: true,
4240
+ json: () => Promise.resolve({
4241
+ ...mockConfig,
4242
+ runtimeMode: 'local',
4243
+ authRequiredForApi: false,
4244
+ setupState: {
4245
+ globalSetupState: 'present',
4246
+ workspaceSetupState: workspaceSaved ? 'present' : 'missing',
4247
+ runtimeMode: 'local',
4248
+ workspaceId: workspaceSaved ? 'workspace-cloud-123' : 'default',
4249
+ mode: 'core',
4250
+ forceSetup: false,
4251
+ forceGlobalSetup: false,
4252
+ forceWorkspaceSetup: true,
4253
+ workspace: workspaceSaved
4254
+ ? { id: 'workspace-cloud-123', slug: 'workspace-cloud-123', name: 'Cloud Project 123', description: 'Cloud desc' }
4255
+ : null,
4256
+ suggestedWorkspaceName: 'Suggested Project'
4257
+ },
4258
+ runtimeCapabilities: {
4259
+ runtimeMode: 'local',
4260
+ supportsCloudAuth: true,
4261
+ supportsAdminManagedSetupOverrides: false,
4262
+ supportsPackageScriptSetupOverrides: true
4263
+ }
4264
+ })
4265
+ });
4266
+ }
4267
+ if (url.includes('https://cloud.example.com/api/taskforce/workspaces')) {
4268
+ return Promise.resolve({
4269
+ ok: true,
4270
+ json: () => Promise.resolve({
4271
+ success: true,
4272
+ workspaces: [
4273
+ {
4274
+ id: 'workspace-cloud-123',
4275
+ name: 'Cloud Project 123',
4276
+ description: 'Cloud desc'
4277
+ }
4278
+ ]
4279
+ })
4280
+ });
4281
+ }
4282
+ if (url.includes('/api/taskforce/workspace-profile')) {
4283
+ const body = JSON.parse(String(init?.body || '{}'));
4284
+ if (body?.workspaceId === 'workspace-cloud-123')
4285
+ workspaceSaved = true;
4286
+ return Promise.resolve({
4287
+ ok: true,
4288
+ json: () => Promise.resolve({
4289
+ success: true,
4290
+ workspace: {
4291
+ id: 'workspace-cloud-123'
4292
+ }
4293
+ })
4294
+ });
4295
+ }
4296
+ if (url.includes('/api/taskforce/session/workspace')) {
4297
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
4298
+ }
4299
+ if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
4300
+ return Promise.resolve({
4301
+ ok: true,
4302
+ json: () => Promise.resolve({
4303
+ success: true,
4304
+ state: workspaceSaved
4305
+ ? {
4306
+ version: 2,
4307
+ enabled: true,
4308
+ phase: 'attach-cloud',
4309
+ pullCursor: null,
4310
+ lastPullAt: null,
4311
+ lastPushAt: null,
4312
+ lastSyncedAt: null
4313
+ }
4314
+ : {
4315
+ version: 2,
4316
+ enabled: false,
4317
+ phase: 'idle',
4318
+ pullCursor: null,
4319
+ lastPullAt: null,
4320
+ lastPushAt: null,
4321
+ lastSyncedAt: null
4322
+ }
4323
+ })
4324
+ });
4325
+ }
4326
+ if (url.includes('/api/taskforce/ui-state')) {
4327
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
4328
+ }
4329
+ if (url.includes('https://cloud.example.com/api/taskforce/sync/workspace/handshake')) {
4330
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
4331
+ }
4332
+ if (url.includes('https://cloud.example.com/api/taskforce/sync/workspace/pull')) {
4333
+ return new Promise(() => { });
4334
+ }
4335
+ if (url === '/api/taskforce/sync/workspace/apply-local') {
4336
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
4337
+ }
4338
+ if (url.includes('/api/taskforce/tasks')) {
4339
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
4340
+ }
4341
+ if (url.includes('/api/taskforce/workflow-templates')) {
4342
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
4343
+ }
4344
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
4345
+ });
4346
+ vi.stubGlobal('fetch', fetchMock);
4347
+ render(_jsx(MemoryRouter, { initialEntries: ['/setup?step=workspace&source=cloud'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
4348
+ await waitFor(() => {
4349
+ expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
4350
+ });
4351
+ await waitFor(() => {
4352
+ expect(screen.getByRole('option', { name: /Cloud Project 123/ })).toBeInTheDocument();
4353
+ });
4354
+ await user.selectOptions(screen.getByRole('combobox'), 'workspace-cloud-123');
4355
+ await user.click(screen.getByRole('button', { name: 'Connect Workspace' }));
4356
+ await waitFor(() => {
4357
+ expect(screen.getByText('Importing "Cloud Project 123" from cloud into local.')).toBeInTheDocument();
4358
+ });
4359
+ expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
4360
+ expect(screen.getByRole('button', { name: 'Importing Workspace...' })).toBeInTheDocument();
4361
+ const workspaceProfileCall = fetchMock.mock.calls.find((call) => String(call[0]).includes('/api/taskforce/workspace-profile'));
4362
+ const workspaceProfileBody = JSON.parse(String(workspaceProfileCall?.[1]?.body || '{}'));
4363
+ expect(workspaceProfileBody.workspaceId).toBe('workspace-cloud-123');
4364
+ const workspaceSyncStateCall = fetchMock.mock.calls.find((call) => {
4365
+ if (!String(call[0]).includes('/api/taskforce/ui-state'))
4366
+ return false;
4367
+ const init = call[1];
4368
+ if (String(init?.method || '').toUpperCase() !== 'POST')
4369
+ return false;
4370
+ const body = JSON.parse(String(init?.body || '{}'));
4371
+ return body?.stateKey === 'workspace-sync';
4372
+ });
4373
+ const workspaceSyncStateBody = JSON.parse(String(workspaceSyncStateCall?.[1]?.body || '{}'));
4374
+ expect(workspaceSyncStateBody.stateKey).toBe('workspace-sync');
4375
+ expect(workspaceSyncStateBody.patch?.enabled).toBe(true);
4376
+ expect(workspaceSyncStateBody.patch?.phase).toBe('attach-cloud');
4377
+ expect(workspaceSyncStateBody.patch?.setupIntent).toBe('attach-cloud-import');
4378
+ expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('https://cloud.example.com/api/taskforce/workspaces'))).toBe(true);
4379
+ expect(screen.queryByTitle(/Account/i)).not.toBeInTheDocument();
4380
+ });
4381
+ it('keeps attach-cloud import blocking after refresh when setup intent is persisted', async () => {
4382
+ const fetchMock = vi.fn((url) => {
4383
+ if (url.includes('/api/taskforce/auth/runtime-config')) {
4384
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'cloud' } }) });
4385
+ }
4386
+ if (url.includes('/api/taskforce/auth/session')) {
4387
+ return Promise.resolve({
4388
+ ok: true,
4389
+ json: () => Promise.resolve({
4390
+ authenticated: true,
4391
+ runtimeMode: 'local',
4392
+ authRequiredForApi: false
4393
+ })
4394
+ });
4395
+ }
4396
+ if (url.includes('/api/taskforce/config')) {
4397
+ return Promise.resolve({
4398
+ ok: true,
4399
+ json: () => Promise.resolve({
4400
+ ...mockConfig,
4401
+ runtimeMode: 'local',
4402
+ authRequiredForApi: false,
4403
+ workspaceId: 'workspace-cloud-123',
4404
+ setupState: {
4405
+ globalSetupState: 'present',
4406
+ workspaceSetupState: 'missing',
4407
+ runtimeMode: 'local',
4408
+ workspaceId: 'workspace-cloud-123',
4409
+ mode: 'core',
4410
+ forceSetup: false,
4411
+ forceGlobalSetup: false,
4412
+ forceWorkspaceSetup: true,
4413
+ workspace: null,
4414
+ suggestedWorkspaceName: 'Cloud Project 123'
4415
+ },
4416
+ runtimeCapabilities: {
4417
+ runtimeMode: 'local',
4418
+ supportsCloudAuth: true,
4419
+ supportsAdminManagedSetupOverrides: false,
4420
+ supportsPackageScriptSetupOverrides: true
4421
+ }
4422
+ })
4423
+ });
4424
+ }
4425
+ if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
4426
+ return Promise.resolve({
4427
+ ok: true,
4428
+ json: () => Promise.resolve({
4429
+ success: true,
4430
+ state: {
4431
+ version: 2,
4432
+ enabled: true,
4433
+ phase: 'attach-cloud',
4434
+ setupIntent: 'attach-cloud-import',
4435
+ pullCursor: null,
4436
+ lastPullAt: null,
4437
+ lastPushAt: null,
4438
+ lastSyncedAt: null
4439
+ }
4440
+ })
4441
+ });
4442
+ }
4443
+ if (url.includes('https://cloud.example.com/api/taskforce/workspaces')) {
4444
+ return Promise.resolve({
4445
+ ok: true,
4446
+ json: () => Promise.resolve({
4447
+ success: true,
4448
+ workspaces: [
4449
+ {
4450
+ id: 'workspace-cloud-123',
4451
+ name: 'Cloud Project 123',
4452
+ description: 'Cloud desc'
4453
+ }
4454
+ ]
4455
+ })
4456
+ });
4457
+ }
4458
+ if (url.includes('/api/taskforce/ui-state?key=app')) {
4459
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true, state: {} }) });
4460
+ }
4461
+ if (url.includes('/api/taskforce/tasks')) {
4462
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
4463
+ }
4464
+ if (url.includes('/api/taskforce/archive')) {
4465
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ archived: [] }) });
4466
+ }
4467
+ if (url.includes('/api/taskforce/categories')) {
4468
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(mockCategories) });
4469
+ }
4470
+ if (url.includes('/api/taskforce/global-settings')) {
4471
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
4472
+ }
4473
+ if (url.includes('/api/taskforce/data-version')) {
4474
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ dataVersion: 1 }) });
4475
+ }
4476
+ if (url.includes('/api/taskforce/types'))
4477
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ types: [] }) });
4478
+ if (url.includes('/api/taskforce/priorities'))
4479
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ priorities: [] }) });
4480
+ if (url.includes('/api/taskforce/approaches'))
4481
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ approaches: [] }) });
4482
+ if (url.includes('/api/taskforce/specialists'))
4483
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
4484
+ if (url.includes('/api/taskforce/workflow-templates'))
4485
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
4486
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
4487
+ });
4488
+ vi.stubGlobal('fetch', fetchMock);
4489
+ render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
4490
+ await waitFor(() => {
4491
+ expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
4492
+ });
4493
+ await waitFor(() => {
4494
+ expect(screen.getByText('Importing "Cloud Project 123" from cloud into local.')).toBeInTheDocument();
4495
+ });
4496
+ });
4497
+ it('clears persisted attach-cloud import intent after the first successful import', async () => {
4498
+ const uiStatePosts = [];
4499
+ const fetchMock = vi.fn((url, init) => {
4500
+ if (url.includes('/api/taskforce/auth/runtime-config')) {
4501
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'cloud' } }) });
4502
+ }
4503
+ if (url.includes('/api/taskforce/auth/session')) {
4504
+ return Promise.resolve({
4505
+ ok: true,
4506
+ json: () => Promise.resolve({
4507
+ authenticated: true,
4508
+ runtimeMode: 'local',
4509
+ authRequiredForApi: false
4510
+ })
4511
+ });
4512
+ }
4513
+ if (url.includes('/api/taskforce/config')) {
4514
+ return Promise.resolve({
4515
+ ok: true,
4516
+ json: () => Promise.resolve({
4517
+ ...mockConfig,
4518
+ runtimeMode: 'local',
4519
+ authRequiredForApi: false,
4520
+ workspaceId: 'workspace-cloud-123'
4521
+ })
4522
+ });
4523
+ }
4524
+ if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
4525
+ return Promise.resolve({
4526
+ ok: true,
4527
+ json: () => Promise.resolve({
4528
+ success: true,
4529
+ state: {
4530
+ version: 2,
4531
+ enabled: true,
4532
+ phase: 'active',
4533
+ setupIntent: 'attach-cloud-import',
4534
+ pullCursor: 'cursor-1',
4535
+ lastPullAt: '2026-04-15T12:00:00.000Z',
4536
+ lastPushAt: null,
4537
+ lastSyncedAt: '2026-04-15T12:00:00.000Z'
4538
+ }
4539
+ })
4540
+ });
4541
+ }
4542
+ if (url.includes('/api/taskforce/ui-state') && String(init?.method || '').toUpperCase() === 'POST') {
4543
+ uiStatePosts.push(JSON.parse(String(init?.body || '{}')));
4544
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
4545
+ }
4546
+ if (url.includes('https://cloud.example.com/api/taskforce/workspaces')) {
4547
+ return Promise.resolve({
4548
+ ok: true,
4549
+ json: () => Promise.resolve({
4550
+ success: true,
4551
+ workspaces: [
4552
+ {
4553
+ id: 'workspace-cloud-123',
4554
+ name: 'Cloud Project 123',
4555
+ description: 'Cloud desc'
4556
+ }
4557
+ ]
4558
+ })
2872
4559
  });
2873
4560
  }
2874
- if (url.includes('/api/taskforce/categories?workspaceId=workspace-starter-1')) {
2875
- return jsonResponse({ success: true });
2876
- }
2877
- if (url.includes('/api/taskforce/types?workspaceId=workspace-starter-1')) {
2878
- return jsonResponse({ success: true });
2879
- }
2880
- if (url.includes('/api/taskforce/priorities?workspaceId=workspace-starter-1')) {
2881
- return jsonResponse({ success: true });
2882
- }
2883
- if (url.includes('/api/taskforce/ui-state')) {
2884
- return jsonResponse({ success: true });
4561
+ if (url.includes('/api/taskforce/ui-state?key=app')) {
4562
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true, state: {} }) });
2885
4563
  }
2886
4564
  if (url.includes('/api/taskforce/tasks')) {
2887
- return jsonResponse({ tasks: mockTasks });
4565
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
2888
4566
  }
2889
4567
  if (url.includes('/api/taskforce/archive')) {
2890
- return jsonResponse({ archived: [] });
4568
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ archived: [] }) });
2891
4569
  }
2892
- if (url.includes('/api/taskforce/workflow-templates')) {
2893
- return jsonResponse({ templates: [] });
4570
+ if (url.includes('/api/taskforce/categories')) {
4571
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(mockCategories) });
2894
4572
  }
2895
- return jsonResponse({});
4573
+ if (url.includes('/api/taskforce/global-settings')) {
4574
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
4575
+ }
4576
+ if (url.includes('/api/taskforce/data-version')) {
4577
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ dataVersion: 1 }) });
4578
+ }
4579
+ if (url.includes('/api/taskforce/types'))
4580
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ types: [] }) });
4581
+ if (url.includes('/api/taskforce/priorities'))
4582
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ priorities: [] }) });
4583
+ if (url.includes('/api/taskforce/approaches'))
4584
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ approaches: [] }) });
4585
+ if (url.includes('/api/taskforce/specialists'))
4586
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
4587
+ if (url.includes('/api/taskforce/workflow-templates'))
4588
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
4589
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
2896
4590
  });
2897
4591
  vi.stubGlobal('fetch', fetchMock);
2898
- render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, {}) }));
4592
+ render(_jsx(MemoryRouter, { initialEntries: ['/setup?step=workspace&source=cloud'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
2899
4593
  await waitFor(() => {
2900
- expect(screen.getByText('Workspace Setup Required')).toBeInTheDocument();
4594
+ expect(screen.getByText('Test Project')).toBeInTheDocument();
2901
4595
  });
2902
- await user.click(screen.getByRole('radio', { name: /software development/i }));
2903
- await user.click(screen.getByRole('button', { name: 'Save Project Setup' }));
2904
4596
  await waitFor(() => {
2905
- expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
4597
+ expect(uiStatePosts.some((entry) => entry?.stateKey === 'workspace-sync' && entry?.patch?.setupIntent === null)).toBe(true);
2906
4598
  });
2907
- const categoryApplyCall = fetchMock.mock.calls.find((call) => (String(call[0]).includes('/api/taskforce/categories?workspaceId=workspace-starter-1')
2908
- && String(call[1]?.method || '').toUpperCase() === 'POST'));
2909
- const typeApplyCall = fetchMock.mock.calls.find((call) => (String(call[0]).includes('/api/taskforce/types?workspaceId=workspace-starter-1')
2910
- && String(call[1]?.method || '').toUpperCase() === 'POST'));
2911
- const priorityApplyCall = fetchMock.mock.calls.find((call) => (String(call[0]).includes('/api/taskforce/priorities?workspaceId=workspace-starter-1')
2912
- && String(call[1]?.method || '').toUpperCase() === 'POST'));
2913
- expect(categoryApplyCall).toBeDefined();
2914
- expect(typeApplyCall).toBeDefined();
2915
- expect(priorityApplyCall).toBeDefined();
2916
- expect(new Headers(categoryApplyCall?.[1]?.headers).get('x-taskforce-workspace-id')).toBe('workspace-starter-1');
2917
- expect(new Headers(typeApplyCall?.[1]?.headers).get('x-taskforce-workspace-id')).toBe('workspace-starter-1');
2918
- expect(new Headers(priorityApplyCall?.[1]?.headers).get('x-taskforce-workspace-id')).toBe('workspace-starter-1');
2919
4599
  });
2920
- it('recovers from transient local workspace-missing setup state after scope resolves on refresh', async () => {
2921
- let configCalls = 0;
4600
+ it('shows sign-in and register actions on signed-out cloud workspace setup and disables connect', async () => {
2922
4601
  const fetchMock = vi.fn((url) => {
2923
4602
  if (url.includes('/api/taskforce/auth/session')) {
2924
4603
  return Promise.resolve({
@@ -2931,29 +4610,23 @@ describe('TaskforceCore Integration', () => {
2931
4610
  });
2932
4611
  }
2933
4612
  if (url.includes('/api/taskforce/config')) {
2934
- configCalls += 1;
2935
- const firstPass = configCalls === 1;
2936
4613
  return Promise.resolve({
2937
4614
  ok: true,
2938
4615
  json: () => Promise.resolve({
2939
4616
  ...mockConfig,
2940
4617
  runtimeMode: 'local',
2941
4618
  authRequiredForApi: false,
2942
- projectRoot: '/mock/local-project-refresh',
2943
- workspaceId: 'workspace-local-refresh',
2944
4619
  setupState: {
2945
4620
  globalSetupState: 'present',
2946
- workspaceSetupState: firstPass ? 'missing' : 'present',
4621
+ workspaceSetupState: 'missing',
2947
4622
  runtimeMode: 'local',
2948
- workspaceId: 'workspace-local-refresh',
4623
+ workspaceId: 'default',
2949
4624
  mode: 'core',
2950
4625
  forceSetup: false,
2951
4626
  forceGlobalSetup: false,
2952
- forceWorkspaceSetup: firstPass,
2953
- workspace: firstPass
2954
- ? null
2955
- : { id: 'workspace-local-refresh', slug: 'workspace-local-refresh', name: 'Local Refresh Project', description: null },
2956
- suggestedWorkspaceName: 'Local Refresh Project'
4627
+ forceWorkspaceSetup: true,
4628
+ workspace: null,
4629
+ suggestedWorkspaceName: 'Suggested Project'
2957
4630
  },
2958
4631
  runtimeCapabilities: {
2959
4632
  runtimeMode: 'local',
@@ -2964,46 +4637,26 @@ describe('TaskforceCore Integration', () => {
2964
4637
  })
2965
4638
  });
2966
4639
  }
2967
- if (url.includes('/api/taskforce/categories')) {
2968
- return Promise.resolve({ ok: true, json: () => Promise.resolve(mockCategories) });
2969
- }
2970
- if (url.includes('/api/taskforce/tasks')) {
2971
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
2972
- }
2973
- if (url.includes('/api/taskforce/types')) {
2974
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ types: [] }) });
2975
- }
2976
- if (url.includes('/api/taskforce/priorities')) {
2977
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ priorities: [] }) });
2978
- }
2979
- if (url.includes('/api/taskforce/approaches')) {
2980
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ approaches: [] }) });
2981
- }
2982
- if (url.includes('/api/taskforce/specialists')) {
2983
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
2984
- }
2985
- if (url.includes('/api/taskforce/workflow-templates')) {
2986
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
2987
- }
2988
4640
  return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
2989
4641
  });
2990
4642
  vi.stubGlobal('fetch', fetchMock);
2991
- render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, {}) }));
4643
+ render(_jsx(MemoryRouter, { initialEntries: ['/setup?step=workspace&source=cloud&postAuth=select-cloud-workspace'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
2992
4644
  await waitFor(() => {
2993
- expect(configCalls).toBeGreaterThan(1);
4645
+ expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
2994
4646
  });
2995
- expect(screen.getByRole('button', { name: /add task/i })).toBeInTheDocument();
2996
- expect(screen.queryByText('Workspace Setup Required')).not.toBeInTheDocument();
4647
+ expect(screen.getByRole('button', { name: 'Sign In' })).toBeInTheDocument();
4648
+ expect(screen.getByRole('button', { name: 'Register' })).toBeInTheDocument();
4649
+ expect(screen.getByRole('button', { name: 'Connect Workspace' })).toBeDisabled();
2997
4650
  });
2998
- it('allows workspace setup to sync from selected cloud project into matching local workspace id', async () => {
4651
+ it('returns signed-in users to the cloud workspace picker after setup sign-in', async () => {
2999
4652
  const user = userEvent.setup();
3000
- let workspaceSaved = false;
4653
+ let authenticated = false;
3001
4654
  const fetchMock = vi.fn((url, init) => {
3002
4655
  if (url.includes('/api/taskforce/auth/session')) {
3003
4656
  return Promise.resolve({
3004
4657
  ok: true,
3005
4658
  json: () => Promise.resolve({
3006
- authenticated: true,
4659
+ authenticated,
3007
4660
  runtimeMode: 'local',
3008
4661
  authRequiredForApi: false
3009
4662
  })
@@ -3018,16 +4671,14 @@ describe('TaskforceCore Integration', () => {
3018
4671
  authRequiredForApi: false,
3019
4672
  setupState: {
3020
4673
  globalSetupState: 'present',
3021
- workspaceSetupState: workspaceSaved ? 'present' : 'missing',
4674
+ workspaceSetupState: 'missing',
3022
4675
  runtimeMode: 'local',
3023
- workspaceId: workspaceSaved ? 'workspace-cloud-123' : 'default',
4676
+ workspaceId: 'default',
3024
4677
  mode: 'core',
3025
4678
  forceSetup: false,
3026
4679
  forceGlobalSetup: false,
3027
4680
  forceWorkspaceSetup: true,
3028
- workspace: workspaceSaved
3029
- ? { id: 'workspace-cloud-123', slug: 'workspace-cloud-123', name: 'Cloud Project 123', description: 'Cloud desc' }
3030
- : null,
4681
+ workspace: null,
3031
4682
  suggestedWorkspaceName: 'Suggested Project'
3032
4683
  },
3033
4684
  runtimeCapabilities: {
@@ -3039,6 +4690,16 @@ describe('TaskforceCore Integration', () => {
3039
4690
  })
3040
4691
  });
3041
4692
  }
4693
+ if (url.includes('/api/taskforce/auth/login') && String(init?.method || '').toUpperCase() === 'POST') {
4694
+ authenticated = true;
4695
+ return Promise.resolve({
4696
+ ok: true,
4697
+ json: () => Promise.resolve({
4698
+ success: true,
4699
+ workspaceSetupRequired: true
4700
+ })
4701
+ });
4702
+ }
3042
4703
  if (url.includes('https://cloud.example.com/api/taskforce/workspaces')) {
3043
4704
  return Promise.resolve({
3044
4705
  ok: true,
@@ -3054,64 +4715,135 @@ describe('TaskforceCore Integration', () => {
3054
4715
  })
3055
4716
  });
3056
4717
  }
3057
- if (url.includes('/api/taskforce/workspace-profile')) {
3058
- const body = JSON.parse(String(init?.body || '{}'));
3059
- if (body?.workspaceId === 'workspace-cloud-123')
3060
- workspaceSaved = true;
3061
- return Promise.resolve({
3062
- ok: true,
3063
- json: () => Promise.resolve({
3064
- success: true,
3065
- workspace: {
3066
- id: 'workspace-cloud-123'
3067
- }
3068
- })
4718
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
4719
+ });
4720
+ vi.stubGlobal('fetch', fetchMock);
4721
+ render(_jsx(MemoryRouter, { initialEntries: ['/setup?step=workspace&source=cloud&postAuth=select-cloud-workspace'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
4722
+ await waitFor(() => {
4723
+ expect(screen.getByRole('button', { name: 'Sign In' })).toBeInTheDocument();
4724
+ });
4725
+ await user.click(screen.getByRole('button', { name: 'Sign In' }));
4726
+ await waitFor(() => {
4727
+ expect(screen.getByTestId('auth-email-input')).toBeInTheDocument();
4728
+ });
4729
+ await user.type(screen.getByTestId('auth-email-input'), 'agent@example.com');
4730
+ await user.type(screen.getByTestId('auth-password-input'), 'password123');
4731
+ await user.click(screen.getByTestId('auth-submit-button'));
4732
+ await waitFor(() => {
4733
+ expect(screen.getByRole('option', { name: /Cloud Project 123/ })).toBeInTheDocument();
4734
+ });
4735
+ await waitFor(() => {
4736
+ expect(screen.queryByRole('button', { name: 'Register' })).not.toBeInTheDocument();
4737
+ expect(screen.getByRole('button', { name: 'Connect Workspace' })).toBeEnabled();
4738
+ });
4739
+ });
4740
+ it('loads workspace setup after cloud sign-in when the user has no workspaces', async () => {
4741
+ const user = userEvent.setup();
4742
+ let authenticated = false;
4743
+ const fetchMock = vi.fn((input, init) => {
4744
+ const url = String(input);
4745
+ if (url.includes('/api/taskforce/auth/runtime-config')) {
4746
+ return jsonResponse({
4747
+ success: true,
4748
+ config: {
4749
+ baseUrl: 'https://app.taskforce.example',
4750
+ apiBaseUrl: 'https://app.taskforce.example/api',
4751
+ cloudAuthBaseUrl: 'https://app.taskforce.example',
4752
+ runtimeMode: 'cloud',
4753
+ authSource: 'cloud',
4754
+ workspaceMode: 'multi-cloud',
4755
+ workspaceSwitchingEnabled: true,
4756
+ cloudAuthViaLocalProxy: false
4757
+ }
3069
4758
  });
3070
4759
  }
3071
- if (url.includes('/api/taskforce/session/workspace')) {
3072
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
4760
+ if (url.includes('/api/taskforce/auth/session')) {
4761
+ return jsonResponse({
4762
+ authenticated,
4763
+ runtimeMode: 'cloud',
4764
+ authRequiredForApi: true,
4765
+ userId: authenticated ? 'user-cloud-setup' : 'anonymous',
4766
+ workspaceId: 'default'
4767
+ });
3073
4768
  }
3074
- if (url.includes('/api/taskforce/ui-state')) {
3075
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
4769
+ if (url.includes('/api/taskforce/auth/login')) {
4770
+ authenticated = true;
4771
+ return jsonResponse({ success: true });
3076
4772
  }
3077
- if (url.includes('/api/taskforce/tasks')) {
3078
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
4773
+ if (url.includes('/api/taskforce/workspaces')) {
4774
+ return jsonResponse({
4775
+ success: true,
4776
+ workspaces: [],
4777
+ currentWorkspaceId: 'default'
4778
+ });
4779
+ }
4780
+ if (url.includes('/api/taskforce/account/access-status')) {
4781
+ return jsonResponse({
4782
+ gate: 'ok',
4783
+ hasEntitlement: true,
4784
+ canAccessApp: true,
4785
+ canAccessPlans: true,
4786
+ message: null
4787
+ });
4788
+ }
4789
+ if (url.includes('/api/taskforce/config')) {
4790
+ return jsonResponse({
4791
+ ...mockConfig,
4792
+ runtimeMode: 'cloud',
4793
+ authRequiredForApi: true,
4794
+ userId: authenticated ? 'user-cloud-setup' : 'anonymous',
4795
+ setupState: {
4796
+ globalSetupState: 'present',
4797
+ workspaceSetupState: authenticated ? 'missing' : 'present',
4798
+ runtimeMode: 'cloud',
4799
+ workspaceId: 'default',
4800
+ mode: 'core',
4801
+ forceSetup: false,
4802
+ forceGlobalSetup: false,
4803
+ forceWorkspaceSetup: false,
4804
+ workspace: null,
4805
+ suggestedWorkspaceName: ''
4806
+ },
4807
+ runtimeCapabilities: {
4808
+ runtimeMode: 'cloud',
4809
+ supportsCloudAuth: true,
4810
+ supportsAdminManagedSetupOverrides: true,
4811
+ supportsPackageScriptSetupOverrides: false
4812
+ }
4813
+ });
4814
+ }
4815
+ if (url.includes('/api/taskforce/ui-state?key=app')) {
4816
+ return jsonResponse({ success: true, state: {} });
4817
+ }
4818
+ if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
4819
+ return jsonResponse({
4820
+ success: true,
4821
+ state: {
4822
+ version: 2,
4823
+ enabled: false,
4824
+ phase: 'idle',
4825
+ pullCursor: null,
4826
+ lastPullAt: null,
4827
+ lastPushAt: null,
4828
+ lastSyncedAt: null
4829
+ }
4830
+ });
3079
4831
  }
3080
4832
  if (url.includes('/api/taskforce/workflow-templates')) {
3081
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
4833
+ return jsonResponse({ templates: [] });
3082
4834
  }
3083
- return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
4835
+ return jsonResponse({});
3084
4836
  });
3085
4837
  vi.stubGlobal('fetch', fetchMock);
3086
- render(_jsx(MemoryRouter, { initialEntries: ['/setup?step=workspace&source=cloud'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
3087
- await waitFor(() => {
3088
- expect(screen.getByText('Workspace Setup Required')).toBeInTheDocument();
3089
- });
3090
- await user.click(screen.getByRole('radio', { name: /sync existing cloud project/i }));
3091
- await waitFor(() => {
3092
- expect(screen.getByRole('option', { name: /Cloud Project 123/ })).toBeInTheDocument();
3093
- });
3094
- await user.selectOptions(screen.getByRole('combobox'), 'workspace-cloud-123');
3095
- await user.click(screen.getByRole('button', { name: 'Save Project Setup' }));
4838
+ render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=login&next=%2F'], children: _jsx(TaskforceCore, {}) }));
4839
+ await user.type(await screen.findByPlaceholderText('Email'), 'setup@example.com');
4840
+ await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
4841
+ await user.click(screen.getByRole('button', { name: 'Sign In' }));
3096
4842
  await waitFor(() => {
3097
- expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
3098
- });
3099
- const workspaceProfileCall = fetchMock.mock.calls.find((call) => String(call[0]).includes('/api/taskforce/workspace-profile'));
3100
- const workspaceProfileBody = JSON.parse(String(workspaceProfileCall?.[1]?.body || '{}'));
3101
- expect(workspaceProfileBody.workspaceId).toBe('workspace-cloud-123');
3102
- const workspaceSyncStateCall = fetchMock.mock.calls.find((call) => {
3103
- if (!String(call[0]).includes('/api/taskforce/ui-state'))
3104
- return false;
3105
- const init = call[1];
3106
- if (String(init?.method || '').toUpperCase() !== 'POST')
3107
- return false;
3108
- const body = JSON.parse(String(init?.body || '{}'));
3109
- return body?.stateKey === 'workspace-sync';
4843
+ expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
3110
4844
  });
3111
- const workspaceSyncStateBody = JSON.parse(String(workspaceSyncStateCall?.[1]?.body || '{}'));
3112
- expect(workspaceSyncStateBody.stateKey).toBe('workspace-sync');
3113
- expect(workspaceSyncStateBody.patch?.cloudSyncEnabled).toBe(true);
3114
- expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('https://cloud.example.com/api/taskforce/workspaces'))).toBe(true);
4845
+ expect(screen.queryByText('Resolving workspace access...')).not.toBeInTheDocument();
4846
+ expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('/api/taskforce/config'))).toBe(true);
3115
4847
  });
3116
4848
  it('enables workspace cloud sync from local setup when signed in and checkbox is selected', async () => {
3117
4849
  const user = userEvent.setup();
@@ -3183,16 +4915,17 @@ describe('TaskforceCore Integration', () => {
3183
4915
  vi.stubGlobal('fetch', fetchMock);
3184
4916
  render(_jsx(MemoryRouter, { initialEntries: ['/setup?step=workspace'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
3185
4917
  await waitFor(() => {
3186
- expect(screen.getByText('Workspace Setup Required')).toBeInTheDocument();
4918
+ expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
3187
4919
  });
3188
4920
  await waitFor(() => {
3189
4921
  expect(screen.getByRole('checkbox', { name: /sync to cloud after setup/i })).toBeInTheDocument();
3190
4922
  });
3191
4923
  await user.click(screen.getByRole('checkbox', { name: /sync to cloud after setup/i }));
3192
- const nameInput = screen.getByPlaceholderText('Project name');
4924
+ const nameInput = screen.getByPlaceholderText('Workspace name');
3193
4925
  await user.clear(nameInput);
3194
4926
  await user.type(nameInput, 'Local Project');
3195
- await user.click(screen.getByRole('button', { name: 'Save Project Setup' }));
4927
+ await user.click(screen.getByRole('button', { name: 'Continue' }));
4928
+ await user.click(screen.getByRole('button', { name: 'Create Workspace' }));
3196
4929
  await waitFor(() => {
3197
4930
  expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
3198
4931
  });
@@ -3206,10 +4939,11 @@ describe('TaskforceCore Integration', () => {
3206
4939
  return body?.stateKey === 'workspace-sync';
3207
4940
  });
3208
4941
  const workspaceSyncStateBody = JSON.parse(String(workspaceSyncStateCall?.[1]?.body || '{}'));
3209
- expect(workspaceSyncStateBody.patch?.cloudSyncEnabled).toBe(true);
3210
- expect(workspaceSyncStateBody.patch?.sourceOfTruth).toBe('local');
3211
- // I6: onboarding now starts as false; the orchestrator marks it true after first pull.
3212
- expect(workspaceSyncStateBody.patch?.onboardingCompleted).toBe(false);
4942
+ expect(workspaceSyncStateBody.patch?.version).toBe(2);
4943
+ expect(workspaceSyncStateBody.patch?.enabled).toBe(true);
4944
+ expect(workspaceSyncStateBody.patch?.phase).toBe('provision-local');
4945
+ expect(workspaceSyncStateBody.patch?.setupIntent).toBeNull();
4946
+ expect(workspaceSyncStateBody.patch?.pullCursor).toBeNull();
3213
4947
  });
3214
4948
  it('creates a new cloud workspace only after guided setup submit', async () => {
3215
4949
  const user = userEvent.setup();
@@ -3621,7 +5355,7 @@ describe('TaskforceCore Integration', () => {
3621
5355
  vi.stubGlobal('fetch', fetchMock);
3622
5356
  const initialRender = render(_jsx(MemoryRouter, { initialEntries: ['/setup?step=workspace'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
3623
5357
  await waitFor(() => {
3624
- expect(screen.getByText('Workspace Setup Required')).toBeInTheDocument();
5358
+ expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
3625
5359
  });
3626
5360
  await waitFor(() => {
3627
5361
  expect(screen.getByRole('checkbox', { name: /sync to cloud after setup/i })).toBeInTheDocument();
@@ -3637,7 +5371,7 @@ describe('TaskforceCore Integration', () => {
3637
5371
  initialRender.unmount();
3638
5372
  render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
3639
5373
  await waitFor(() => {
3640
- expect(screen.queryByText('Workspace Setup Required')).not.toBeInTheDocument();
5374
+ expect(screen.queryByText('Configure your new workspace.')).not.toBeInTheDocument();
3641
5375
  expect(screen.getByRole('button', { name: /add task/i })).toBeInTheDocument();
3642
5376
  });
3643
5377
  expect(cloudWorkspaceCreated).toBe(false);
@@ -3878,18 +5612,31 @@ describe('TaskforceCore Integration', () => {
3878
5612
  vi.stubGlobal('fetch', fetchMock);
3879
5613
  render(_jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }));
3880
5614
  await waitFor(() => {
3881
- expect(screen.getByTitle('Open tasks matching current filters')).toHaveTextContent('1/1');
5615
+ expect(screen.getByTitle(/Open tasks/)).toHaveTextContent(/1/);
3882
5616
  expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
3883
5617
  });
3884
5618
  await user.click(screen.getByTitle(/Account/i));
3885
5619
  await user.click(await screen.findByRole('menuitem', { name: 'Sign Out' }));
3886
5620
  await waitFor(() => {
3887
5621
  expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('https://cloud.example.com/api/taskforce/auth/logout'))).toBe(true);
3888
- expect(screen.getByTitle('Open tasks matching current filters')).toHaveTextContent('1/1');
5622
+ expect(screen.getByTitle(/Open tasks/)).toHaveTextContent(/1/);
3889
5623
  });
3890
5624
  await user.click(screen.getByTitle(/Account/i));
3891
5625
  expect(await screen.findByText('Sign In / Register')).toBeInTheDocument();
3892
5626
  });
5627
+ it('uses an explicit cloud sign-out redirect and broader cloud logout reset path', () => {
5628
+ const standaloneLayoutSource = fs.readFileSync(path.join(__dirname, 'components/views/StandaloneLayout.tsx'), 'utf8');
5629
+ const useTaskforceSource = fs.readFileSync(path.join(__dirname, 'hooks/useTaskforce.ts'), 'utf8');
5630
+ expect(standaloneLayoutSource).toContain("await logout();");
5631
+ expect(standaloneLayoutSource).toContain("if (runtimeMode === 'cloud') {");
5632
+ expect(standaloneLayoutSource).toContain("navigate('/login', { replace: true });");
5633
+ expect(useTaskforceSource).toContain('const resetCloudLogoutClientState = useCallback(() => {');
5634
+ expect(useTaskforceSource).toContain('setArchivedTasks([]);');
5635
+ expect(useTaskforceSource).toContain('setDeletedTasks([]);');
5636
+ expect(useTaskforceSource).toContain('setAssigneeOptions(createDefaultAssigneeOptions());');
5637
+ expect(useTaskforceSource).toContain('setUiStateReady(false);');
5638
+ expect(useTaskforceSource).toContain('resetCloudLogoutClientState();');
5639
+ });
3893
5640
  it('does not pull from cloud when workspace sync toggle is disabled', async () => {
3894
5641
  const fetchMock = vi.fn((url) => {
3895
5642
  if (url.includes('/api/taskforce/auth/session')) {
@@ -4758,9 +6505,10 @@ describe('TaskforceCore Integration', () => {
4758
6505
  it('keeps local workspace context authoritative over cloud auth session workspace id', async () => {
4759
6506
  const useTaskforcePath = path.join(process.cwd(), 'src/hooks/useTaskforce.ts');
4760
6507
  const useTaskforceSource = fs.readFileSync(useTaskforcePath, 'utf8');
4761
- expect(useTaskforceSource).toContain("const keepLocalWorkspaceContext = runtimeMode === 'local' || authOnlyCloudMode;");
4762
- expect(useTaskforceSource).toContain('Local runtime resolves workspace identity from local DB via /api/taskforce/config.');
4763
- expect(useTaskforceSource).toContain('Do not override it from cloud auth session payloads.');
6508
+ expect(useTaskforceSource).toContain("const authOnlyCloudMode = !isCloudHost && Boolean(normalizedCloudAuthBaseUrl) && !normalizedApiBaseUrl;");
6509
+ expect(useTaskforceSource).toContain('Local runtime workspace identity must be sourced from local DB (/api/taskforce/config),');
6510
+ expect(useTaskforceSource).toContain('not from persisted browser context or cloud session payloads.');
6511
+ expect(useTaskforceSource).toContain("if (initialRuntimeMode === 'local') return 'default';");
4764
6512
  });
4765
6513
  it.skip('blocks local cloud-sync pull/apply when workspaceId remains default', async () => {
4766
6514
  const fetchMock = vi.fn((url, init) => {