@taskforcehq/taskforce 0.3.302 → 0.3.304

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 (192) hide show
  1. package/dist/Taskforce.module.css +113 -5
  2. package/dist/TaskforceCore.js +560 -208
  3. package/dist/TaskforceCore.test.js +1833 -91
  4. package/dist/annotatedAttachments/service.d.ts +1 -1
  5. package/dist/annotatedAttachments/types.d.ts +1 -1
  6. package/dist/components/features/AgentsModule.js +1 -1
  7. package/dist/components/features/AgentsModule.test.js +1 -1
  8. package/dist/components/features/TaskSettings.js +58 -38
  9. package/dist/components/features/TaskSettings.test.js +139 -40
  10. package/dist/components/task/TaskActionHeader.d.ts +2 -1
  11. package/dist/components/task/TaskActionHeader.js +3 -3
  12. package/dist/components/task/TaskActionHeader.test.js +8 -0
  13. package/dist/components/task/TaskCard.js +16 -1
  14. package/dist/components/task/TaskCard.test.js +31 -0
  15. package/dist/components/task/TaskForm.d.ts +2 -1
  16. package/dist/components/task/TaskForm.js +22 -4
  17. package/dist/components/task/TaskForm.test.js +46 -0
  18. package/dist/components/task/TaskKanban.d.ts +1 -1
  19. package/dist/components/task/TaskKanban.js +31 -160
  20. package/dist/components/task/TaskKanban.test.js +17 -0
  21. package/dist/components/task/TaskStatusActions.d.ts +6 -1
  22. package/dist/components/task/TaskStatusActions.js +8 -3
  23. package/dist/components/task/TaskStatusActions.test.js +9 -0
  24. package/dist/components/task/taskKanbanDropRules.d.ts +58 -0
  25. package/dist/components/task/taskKanbanDropRules.js +187 -0
  26. package/dist/components/task/taskKanbanDropRules.test.d.ts +1 -0
  27. package/dist/components/task/taskKanbanDropRules.test.js +114 -0
  28. package/dist/components/ui/Modal.d.ts +1 -1
  29. package/dist/components/ui/Modal.js +1 -0
  30. package/dist/components/views/AppShellHeader.js +5 -1
  31. package/dist/components/views/AppShellHeader.test.js +4 -0
  32. package/dist/components/views/PlanComparisonPage.d.ts +3 -2
  33. package/dist/components/views/PlanComparisonPage.js +4 -2
  34. package/dist/components/views/PlanComparisonPage.test.js +4 -0
  35. package/dist/components/views/PlansPage.d.ts +7 -1
  36. package/dist/components/views/PlansPage.js +139 -53
  37. package/dist/components/views/PlansPage.test.d.ts +1 -0
  38. package/dist/components/views/PlansPage.test.js +157 -0
  39. package/dist/components/views/StandaloneLayout.d.ts +1 -0
  40. package/dist/components/views/StandaloneLayout.js +216 -186
  41. package/dist/components/views/WidgetView.js +11 -2
  42. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +8 -3
  43. package/dist/components/views/standalone/modals/AccountHubModal.js +16 -2
  44. package/dist/components/views/standalone/modals/AccountHubModal.test.d.ts +1 -0
  45. package/dist/components/views/standalone/modals/AccountHubModal.test.js +26 -0
  46. package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
  47. package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
  48. package/dist/core/EntitlementsPolicy.test.js +2 -2
  49. package/dist/core/GlobalSettingsService.js +78 -18
  50. package/dist/core/PlanEntitlementService.d.ts +31 -5
  51. package/dist/core/PlanEntitlementService.js +350 -222
  52. package/dist/core/SignupMonetizationSettings.test.js +208 -49
  53. package/dist/core/SystemAdmin.test.js +186 -96
  54. package/dist/core/TaskActivityService.d.ts +4 -1
  55. package/dist/core/TaskActivityService.js +45 -0
  56. package/dist/core/TaskAuditTimeline.test.js +52 -1
  57. package/dist/core/Taskforce.d.ts +65 -20
  58. package/dist/core/Taskforce.js +261 -51
  59. package/dist/core/Taskforce.mcpAuth.test.js +0 -2
  60. package/dist/core/UserProfileAvatarDrafts.test.js +12 -3
  61. package/dist/core/WorkspacePlanMode.test.js +65 -0
  62. package/dist/core/types.d.ts +11 -12
  63. package/dist/documentReviews/service.d.ts +1 -1
  64. package/dist/documentReviews/types.d.ts +1 -1
  65. package/dist/hooks/auth/useTaskforceAuthBootstrap.d.ts +48 -0
  66. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +219 -0
  67. package/dist/hooks/sync/auth.d.ts +37 -0
  68. package/dist/hooks/sync/auth.js +61 -0
  69. package/dist/hooks/sync/bootstrap.d.ts +34 -0
  70. package/dist/hooks/sync/bootstrap.js +52 -0
  71. package/dist/hooks/sync/orchestratorShared.d.ts +36 -0
  72. package/dist/hooks/sync/orchestratorShared.js +209 -0
  73. package/dist/hooks/sync/orchestratorShared.test.d.ts +1 -0
  74. package/dist/hooks/sync/orchestratorShared.test.js +49 -0
  75. package/dist/hooks/sync/realtime.d.ts +25 -0
  76. package/dist/hooks/sync/realtime.js +60 -0
  77. package/dist/hooks/sync/recovery.d.ts +69 -0
  78. package/dist/hooks/sync/recovery.js +118 -0
  79. package/dist/hooks/sync/transfers.d.ts +122 -0
  80. package/dist/hooks/sync/transfers.js +431 -0
  81. package/dist/hooks/sync/useDataVersionRefresh.d.ts +16 -0
  82. package/dist/hooks/sync/useDataVersionRefresh.js +73 -0
  83. package/dist/hooks/ui/useResourceExport.d.ts +3 -1
  84. package/dist/hooks/ui/useResourceExport.js +5 -2
  85. package/dist/hooks/ui/useWorkflowTemplates.js +11 -6
  86. package/dist/hooks/useRealtimeSync.js +2 -1
  87. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +8 -0
  88. package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +1 -0
  89. package/dist/hooks/useSyncOrchestrator.context-assets.test.js +228 -0
  90. package/dist/hooks/useSyncOrchestrator.d.ts +5 -4
  91. package/dist/hooks/useSyncOrchestrator.js +315 -860
  92. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +665 -8
  93. package/dist/hooks/useTaskMutations.js +45 -6
  94. package/dist/hooks/useTaskMutations.test.js +147 -0
  95. package/dist/hooks/useTaskforce.d.ts +11 -2
  96. package/dist/hooks/useTaskforce.js +290 -812
  97. package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
  98. package/dist/hooks/useWorkspaceSyncController.js +22 -10
  99. package/dist/hooks/workspace/useTaskforceApiRouting.test.js +17 -0
  100. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +166 -0
  101. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +434 -0
  102. package/dist/hooks/workspace/workspaceLocalState.d.ts +19 -0
  103. package/dist/hooks/workspace/workspaceLocalState.js +100 -0
  104. package/dist/hooks/workspace/workspaceLocalState.test.d.ts +1 -0
  105. package/dist/hooks/workspace/workspaceLocalState.test.js +59 -0
  106. package/dist/localization/locales/en-US.js +2 -2
  107. package/dist/mcp/canonicalAssetHelpers.d.ts +70 -0
  108. package/dist/mcp/canonicalAssetHelpers.js +259 -0
  109. package/dist/mcp/clientIntegrations.d.ts +12 -1
  110. package/dist/mcp/clientIntegrations.js +161 -6
  111. package/dist/mcp/clientIntegrations.test.js +76 -4
  112. package/dist/mcp/documentHelpers.d.ts +100 -0
  113. package/dist/mcp/documentHelpers.js +161 -0
  114. package/dist/mcp/runtime.js +264 -505
  115. package/dist/mcp/runtime.test.js +353 -89
  116. package/dist/mcp/taskAttachmentHelpers.d.ts +41 -0
  117. package/dist/mcp/taskAttachmentHelpers.js +60 -0
  118. package/dist/mcp/taskAttachmentTypes.d.ts +37 -0
  119. package/dist/mcp/taskAttachmentTypes.js +1 -0
  120. package/dist/migrations/billingSchemaParity.test.js +3 -0
  121. package/dist/migrations/taskSchemaMigrations.js +15 -0
  122. package/dist/plugins/vite.js +1 -1
  123. package/dist/runtime/appGateDefinitions.d.ts +1 -1
  124. package/dist/runtime/appGateDefinitions.js +1 -1
  125. package/dist/server/index.cookieProxy.test.js +1 -0
  126. package/dist/server/index.d.ts +1 -0
  127. package/dist/server/index.js +48 -3
  128. package/dist/server/index.test.js +14 -2
  129. package/dist/server/mockStripe.d.ts +1 -0
  130. package/dist/server/mockStripe.js +2 -0
  131. package/dist/server/routes/admin.js +91 -40
  132. package/dist/server/routes/auth.d.ts +1 -0
  133. package/dist/server/routes/auth.js +231 -55
  134. package/dist/server/routes/billing.js +473 -39
  135. package/dist/server/routes/billing.test.js +972 -37
  136. package/dist/server/routes/shared.d.ts +30 -0
  137. package/dist/server/routes/shared.js +31 -3
  138. package/dist/server/routes/sync.integration.test.js +180 -0
  139. package/dist/server/routes/sync.js +125 -242
  140. package/dist/server/routes/tasks.js +49 -7
  141. package/dist/server/routes.js +11 -22
  142. package/dist/server/routes.test.js +530 -81
  143. package/dist/shared/taskActor.d.ts +10 -0
  144. package/dist/shared/taskActor.js +1 -0
  145. package/dist/sync/cloudSyncApi.d.ts +1 -0
  146. package/dist/sync/collaborationSyncPayload.js +4 -0
  147. package/dist/sync/collaborationSyncPayload.test.js +8 -0
  148. package/dist/sync/collaborationSyncState.d.ts +3 -1
  149. package/dist/sync/collaborationSyncState.js +19 -0
  150. package/dist/sync/contentSyncState.d.ts +10 -0
  151. package/dist/sync/contentSyncState.js +277 -14
  152. package/dist/sync/syncApplyHandlers.d.ts +87 -1
  153. package/dist/sync/syncApplyHandlers.js +370 -8
  154. package/dist/sync/syncApplyHandlers.test.d.ts +1 -0
  155. package/dist/sync/syncApplyHandlers.test.js +377 -0
  156. package/dist/sync/syncService.d.ts +9 -1
  157. package/dist/sync/syncService.js +57 -10
  158. package/dist/sync/syncService.test.js +48 -0
  159. package/dist/sync/workspacePullFeed.d.ts +11 -0
  160. package/dist/sync/workspacePullFeed.js +123 -68
  161. package/dist/sync/workspacePullFeed.test.js +180 -0
  162. package/dist/sync/workspaceSyncModel.d.ts +22 -1
  163. package/dist/sync/workspaceSyncModel.js +65 -0
  164. package/dist/sync/workspaceSyncModel.test.js +119 -0
  165. package/dist/types.d.ts +2 -10
  166. package/dist/ui/assets/AgentsModule-BLWVbuKP.js +1 -0
  167. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-Dvi02Y7q.js → AnnotatedAttachmentWorkspace-BlS-cqnl.js} +1 -1
  168. package/dist/ui/assets/{DocumentWorkspace-iys3zfca.js → DocumentWorkspace-BH_6DF6x.js} +1 -1
  169. package/dist/ui/assets/{InitiativesModule-DJvLJI3A.js → InitiativesModule-BjR6iBf9.js} +1 -1
  170. package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
  171. package/dist/ui/assets/PlansPage-CSDQ4sLa.js +1 -0
  172. package/dist/ui/assets/TaskSettings-CEDy34Jo.js +9 -0
  173. package/dist/ui/assets/{WorkflowsModule-BguHeQaV.js → WorkflowsModule-DnFqdUME.js} +1 -1
  174. package/dist/ui/assets/index-DXUdtH1B.js +6 -0
  175. package/dist/ui/assets/index-Ii0B0rTO.css +1 -0
  176. package/dist/ui/assets/{vendor-icons-C8EED3Oh.js → vendor-icons-I7ZwG1z_.js} +1 -1
  177. package/dist/ui/index.html +4 -4
  178. package/dist/utils/accountProfileSummaryCache.d.ts +31 -0
  179. package/dist/utils/accountProfileSummaryCache.js +97 -0
  180. package/dist/utils/accountProfileSummaryCache.test.d.ts +1 -0
  181. package/dist/utils/accountProfileSummaryCache.test.js +63 -0
  182. package/dist/utils/taskActivity.js +4 -0
  183. package/dist/utils/taskActivity.test.js +25 -1
  184. package/package.json +5 -3
  185. package/scripts/playwright-auth.sh +4 -0
  186. package/dist/ui/assets/AgentsModule-JG5jc3he.js +0 -1
  187. package/dist/ui/assets/PlansPage-BMPlpFpw.js +0 -1
  188. package/dist/ui/assets/TaskSettings-Bqd4cCzW.js +0 -8
  189. package/dist/ui/assets/index-MXeWoebC.css +0 -1
  190. package/dist/ui/assets/index-YJ4k5QNa.js +0 -6
  191. package/dist/utils/billingStatusCache.d.ts +0 -21
  192. package/dist/utils/billingStatusCache.js +0 -70
@@ -48,6 +48,7 @@ function jsonResponse(body, status = 200) {
48
48
  describe('TaskforceCore Integration', () => {
49
49
  beforeEach(() => {
50
50
  localStorage.clear();
51
+ sessionStorage.clear();
51
52
  vi.stubGlobal('fetch', vi.fn((url) => {
52
53
  if (url.includes('/api/taskforce/auth/runtime-config')) {
53
54
  return Promise.resolve({ ok: true, json: () => Promise.resolve({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'cloud' } }) });
@@ -1037,7 +1038,7 @@ describe('TaskforceCore Integration', () => {
1037
1038
  })
1038
1039
  });
1039
1040
  }
1040
- if (url.includes('/api/taskforce/billing/status')) {
1041
+ if (url.includes('/api/taskforce/account/profile-summary')) {
1041
1042
  return Promise.resolve({
1042
1043
  ok: true,
1043
1044
  json: () => Promise.resolve({
@@ -1047,7 +1048,7 @@ describe('TaskforceCore Integration', () => {
1047
1048
  })
1048
1049
  });
1049
1050
  }
1050
- if (url.includes('/api/taskforce/billing/status')) {
1051
+ if (url.includes('/api/taskforce/account/profile-summary')) {
1051
1052
  return Promise.resolve({
1052
1053
  ok: true,
1053
1054
  json: () => Promise.resolve({
@@ -1143,7 +1144,7 @@ describe('TaskforceCore Integration', () => {
1143
1144
  hasEntitlement: false,
1144
1145
  canAccessApp: false,
1145
1146
  canAccessPlans: true,
1146
- message: 'Signup monetization is misconfigured.'
1147
+ message: 'Onboarding policy is misconfigured.'
1147
1148
  })
1148
1149
  });
1149
1150
  }
@@ -1196,7 +1197,7 @@ describe('TaskforceCore Integration', () => {
1196
1197
  render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, {}) }));
1197
1198
  await waitFor(() => {
1198
1199
  expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
1199
- expect(screen.getByText('Signup monetization is misconfigured. Contact support or a system administrator.')).toBeInTheDocument();
1200
+ expect(screen.getByText('Onboarding policy is misconfigured. Contact support or a system administrator.')).toBeInTheDocument();
1200
1201
  });
1201
1202
  });
1202
1203
  it('suppresses stale missing-entitlement gate messaging when billing status is already active', async () => {
@@ -1287,7 +1288,7 @@ describe('TaskforceCore Integration', () => {
1287
1288
  })
1288
1289
  });
1289
1290
  }
1290
- if (url.includes('/api/taskforce/billing/status')) {
1291
+ if (url.includes('/api/taskforce/account/profile-summary')) {
1291
1292
  return Promise.resolve({
1292
1293
  ok: true,
1293
1294
  json: () => Promise.resolve({
@@ -1388,7 +1389,7 @@ describe('TaskforceCore Integration', () => {
1388
1389
  })
1389
1390
  });
1390
1391
  }
1391
- if (url.includes('/api/taskforce/billing/status')) {
1392
+ if (url.includes('/api/taskforce/account/profile-summary')) {
1392
1393
  return Promise.resolve({
1393
1394
  ok: true,
1394
1395
  json: () => Promise.resolve({
@@ -1445,7 +1446,7 @@ describe('TaskforceCore Integration', () => {
1445
1446
  await waitFor(() => {
1446
1447
  expect(screen.queryByText(/Signed in as/i)).not.toBeInTheDocument();
1447
1448
  expect(screen.getByText('member@example.com')).toBeInTheDocument();
1448
- expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('/api/taskforce/billing/status'))).toBe(true);
1449
+ expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('/api/taskforce/account/profile-summary'))).toBe(true);
1449
1450
  });
1450
1451
  await waitFor(() => {
1451
1452
  expect(screen.getByText('Subscription')).toBeInTheDocument();
@@ -1507,7 +1508,7 @@ describe('TaskforceCore Integration', () => {
1507
1508
  })
1508
1509
  });
1509
1510
  }
1510
- if (url.includes('/api/taskforce/billing/status')) {
1511
+ if (url.includes('/api/taskforce/account/profile-summary')) {
1511
1512
  return Promise.resolve({
1512
1513
  ok: true,
1513
1514
  json: () => Promise.resolve({
@@ -1569,6 +1570,292 @@ describe('TaskforceCore Integration', () => {
1569
1570
  expect(screen.getByText('CLOUD.EXAMPLE.COM')).toBeInTheDocument();
1570
1571
  });
1571
1572
  }, 10000);
1573
+ it('preserves the original user billing and team access after registering and switching back in local runtime', async () => {
1574
+ const user = userEvent.setup();
1575
+ const accounts = {
1576
+ ross: {
1577
+ email: 'ross.burke@gmail.com',
1578
+ userId: 'user-ross',
1579
+ displayName: 'Ross Burke',
1580
+ workspaceId: 'workspace-local-active',
1581
+ planId: 'team-5',
1582
+ planVersionId: 'team-5-v1',
1583
+ planName: 'Team 5',
1584
+ teamManagementAllowed: true,
1585
+ teamPlanMode: 'team',
1586
+ workspaceRole: 'owner'
1587
+ },
1588
+ newcomer: {
1589
+ email: 'new.user@example.com',
1590
+ userId: 'user-new',
1591
+ displayName: 'New User',
1592
+ workspaceId: 'workspace-local-active',
1593
+ planId: 'starter',
1594
+ planVersionId: 'starter-v1',
1595
+ planName: 'Starter',
1596
+ teamManagementAllowed: false,
1597
+ teamPlanMode: 'personal',
1598
+ workspaceRole: 'member'
1599
+ }
1600
+ };
1601
+ let currentAccountKey = 'ross';
1602
+ const currentAccount = () => (currentAccountKey ? accounts[currentAccountKey] : null);
1603
+ const fetchMock = vi.fn((input, init) => {
1604
+ const url = String(input);
1605
+ const account = currentAccount();
1606
+ if (url.includes('/api/taskforce/auth/runtime-config')) {
1607
+ return jsonResponse({
1608
+ success: true,
1609
+ config: {
1610
+ baseUrl: 'https://cloud.example.com',
1611
+ apiBaseUrl: 'https://cloud.example.com',
1612
+ cloudAuthBaseUrl: 'https://cloud.example.com',
1613
+ runtimeMode: 'local',
1614
+ authSource: 'cloud',
1615
+ workspaceMode: 'single-local',
1616
+ workspaceSwitchingEnabled: false,
1617
+ cloudAuthViaLocalProxy: false
1618
+ }
1619
+ });
1620
+ }
1621
+ if (url.includes('/api/taskforce/auth/session')) {
1622
+ return jsonResponse({
1623
+ authenticated: Boolean(account),
1624
+ runtimeMode: 'local',
1625
+ authRequiredForApi: false,
1626
+ email: account?.email || '',
1627
+ displayName: account?.displayName || '',
1628
+ userId: account?.userId || 'anonymous',
1629
+ workspaceId: account?.workspaceId || '',
1630
+ betaAccess: true
1631
+ });
1632
+ }
1633
+ if (url.includes('/api/taskforce/auth/logout')) {
1634
+ currentAccountKey = null;
1635
+ return jsonResponse({ success: true });
1636
+ }
1637
+ if (url.includes('/api/taskforce/auth/register')) {
1638
+ currentAccountKey = 'newcomer';
1639
+ return jsonResponse({
1640
+ success: true,
1641
+ workspaceSetupRequired: false,
1642
+ verificationRequired: false
1643
+ });
1644
+ }
1645
+ if (url.includes('/api/taskforce/auth/login')) {
1646
+ const body = typeof init?.body === 'string' ? JSON.parse(init.body) : {};
1647
+ currentAccountKey = String(body?.email || '').trim().toLowerCase() === accounts.ross.email
1648
+ ? 'ross'
1649
+ : 'newcomer';
1650
+ return jsonResponse({ success: true });
1651
+ }
1652
+ if (url.includes('/api/taskforce/account/profile-summary')) {
1653
+ return jsonResponse(account
1654
+ ? {
1655
+ planId: account.planId,
1656
+ planVersionId: account.planVersionId,
1657
+ planName: account.planName,
1658
+ entitlementState: 'active',
1659
+ workspaceId: account.workspaceId,
1660
+ workspaceRole: account.workspaceRole,
1661
+ canManageWorkspace: account.workspaceRole === 'owner' || account.workspaceRole === 'admin',
1662
+ teamPlanMode: account.teamPlanMode,
1663
+ teamManagementAllowed: account.teamManagementAllowed
1664
+ }
1665
+ : {
1666
+ planId: null,
1667
+ planVersionId: null,
1668
+ planName: null,
1669
+ entitlementState: null,
1670
+ workspaceId: 'workspace-local-active',
1671
+ workspaceRole: null,
1672
+ canManageWorkspace: false,
1673
+ teamPlanMode: 'unknown',
1674
+ teamManagementAllowed: false
1675
+ });
1676
+ }
1677
+ if (url.includes('/api/taskforce/auth/workspace-members')) {
1678
+ return jsonResponse({
1679
+ users: account ? [{
1680
+ userId: account.userId,
1681
+ email: account.email,
1682
+ displayName: account.displayName,
1683
+ role: account.workspaceRole,
1684
+ status: 'active',
1685
+ disabled: false
1686
+ }] : []
1687
+ });
1688
+ }
1689
+ if (url.includes('/api/taskforce/public/pricing')) {
1690
+ return jsonResponse({
1691
+ plans: [
1692
+ {
1693
+ planId: 'starter',
1694
+ displayName: 'Starter',
1695
+ status: 'active',
1696
+ defaultVersion: {
1697
+ planVersionId: 'starter-v1',
1698
+ versionNumber: 1,
1699
+ seatLimit: null,
1700
+ metadata: {}
1701
+ },
1702
+ features: [],
1703
+ pricing: {
1704
+ month: { pricingType: 'free', stripePriceId: null, unitAmount: 0, currency: null, interval: 'month' },
1705
+ year: null
1706
+ }
1707
+ },
1708
+ {
1709
+ planId: 'team-5',
1710
+ displayName: 'Team 5',
1711
+ status: 'active',
1712
+ defaultVersion: {
1713
+ planVersionId: 'team-5-v1',
1714
+ versionNumber: 1,
1715
+ seatLimit: 5,
1716
+ metadata: {}
1717
+ },
1718
+ features: [],
1719
+ pricing: {
1720
+ month: { pricingType: 'stripe', stripePriceId: 'price_team_5_month', unitAmount: 5000, currency: 'usd', interval: 'month' },
1721
+ year: { pricingType: 'stripe', stripePriceId: 'price_team_5_year', unitAmount: 50000, currency: 'usd', interval: 'year' }
1722
+ }
1723
+ }
1724
+ ]
1725
+ });
1726
+ }
1727
+ if (url.includes('/api/taskforce/ui-state?key=app')) {
1728
+ return jsonResponse({ success: true, state: {} });
1729
+ }
1730
+ if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
1731
+ return jsonResponse({
1732
+ success: true,
1733
+ state: {
1734
+ version: 2,
1735
+ enabled: false,
1736
+ phase: 'idle',
1737
+ pullCursor: null,
1738
+ lastPullAt: null,
1739
+ lastPushAt: null,
1740
+ lastSyncedAt: null
1741
+ }
1742
+ });
1743
+ }
1744
+ if (url.includes('/api/taskforce/config')) {
1745
+ return jsonResponse({
1746
+ ...mockConfig,
1747
+ runtimeMode: 'local',
1748
+ authRequiredForApi: false,
1749
+ workspaceId: 'workspace-local-active',
1750
+ setupState: {
1751
+ globalSetupState: 'present',
1752
+ workspaceSetupState: 'present',
1753
+ runtimeMode: 'local',
1754
+ workspaceId: 'workspace-local-active',
1755
+ mode: 'core',
1756
+ forceSetup: false,
1757
+ forceGlobalSetup: false,
1758
+ forceWorkspaceSetup: false
1759
+ }
1760
+ });
1761
+ }
1762
+ if (url.includes('/api/taskforce/tasks')) {
1763
+ return jsonResponse({ tasks: mockTasks });
1764
+ }
1765
+ if (url.includes('/api/taskforce/archive')) {
1766
+ return jsonResponse({ archived: [] });
1767
+ }
1768
+ if (url.includes('/api/taskforce/categories')) {
1769
+ return jsonResponse(mockCategories);
1770
+ }
1771
+ if (url.includes('/api/taskforce/types')) {
1772
+ return jsonResponse({ types: [] });
1773
+ }
1774
+ if (url.includes('/api/taskforce/priorities')) {
1775
+ return jsonResponse({ priorities: [] });
1776
+ }
1777
+ if (url.includes('/api/taskforce/approaches')) {
1778
+ return jsonResponse({ approaches: [] });
1779
+ }
1780
+ if (url.includes('/api/taskforce/specialists')) {
1781
+ return jsonResponse({ specialists: [] });
1782
+ }
1783
+ if (url.includes('/api/taskforce/workflow-templates')) {
1784
+ return jsonResponse({ templates: [] });
1785
+ }
1786
+ if (url.includes('/api/taskforce/workspace/assignee-options')) {
1787
+ return jsonResponse({ assignees: [] });
1788
+ }
1789
+ if (url.includes('/api/taskforce/initiatives')) {
1790
+ return jsonResponse([]);
1791
+ }
1792
+ if (url.includes('/api/taskforce/workstreams')) {
1793
+ return jsonResponse([]);
1794
+ }
1795
+ return jsonResponse({});
1796
+ });
1797
+ vi.stubGlobal('fetch', fetchMock);
1798
+ render(_jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }));
1799
+ await waitFor(() => {
1800
+ expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
1801
+ });
1802
+ await user.click(screen.getByTitle(/Account/i));
1803
+ await waitFor(() => {
1804
+ expect(screen.getByText(accounts.ross.email)).toBeInTheDocument();
1805
+ expect(screen.getByText('Team 5')).toBeInTheDocument();
1806
+ expect(screen.getByRole('menuitem', { name: 'Team Management' })).toBeInTheDocument();
1807
+ });
1808
+ await user.click(screen.getByRole('menuitem', { name: 'Sign Out' }));
1809
+ await waitFor(() => {
1810
+ expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
1811
+ });
1812
+ await user.click(screen.getByTitle(/Account/i));
1813
+ await user.click(await screen.findByRole('menuitem', { name: 'Sign In / Register' }));
1814
+ await waitFor(() => {
1815
+ expect(screen.getByRole('button', { name: 'Register' })).toBeInTheDocument();
1816
+ });
1817
+ await user.click(screen.getByRole('button', { name: 'Register' }));
1818
+ await user.type(await screen.findByPlaceholderText('Email'), accounts.newcomer.email);
1819
+ await user.type(screen.getByPlaceholderText('Display name'), accounts.newcomer.displayName);
1820
+ await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
1821
+ await user.click(screen.getByRole('button', { name: 'Create Account' }));
1822
+ await waitFor(() => {
1823
+ expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
1824
+ });
1825
+ await user.click(screen.getByTitle(/Account/i));
1826
+ await waitFor(() => {
1827
+ expect(screen.getByText(accounts.newcomer.email)).toBeInTheDocument();
1828
+ expect(screen.getByText('Starter')).toBeInTheDocument();
1829
+ });
1830
+ await user.click(screen.getByRole('menuitem', { name: 'Sign Out' }));
1831
+ await waitFor(() => {
1832
+ expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
1833
+ });
1834
+ await user.click(screen.getByTitle(/Account/i));
1835
+ await user.click(await screen.findByRole('menuitem', { name: 'Sign In / Register' }));
1836
+ await waitFor(() => {
1837
+ expect(screen.getByText('Sign in with your account credentials.')).toBeInTheDocument();
1838
+ });
1839
+ await user.type(await screen.findByPlaceholderText('Email'), accounts.ross.email);
1840
+ await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
1841
+ await user.click(screen.getByRole('button', { name: 'Sign In' }));
1842
+ await waitFor(() => {
1843
+ expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
1844
+ });
1845
+ await user.click(screen.getByTitle(/Account/i));
1846
+ await waitFor(() => {
1847
+ expect(screen.getByText(accounts.ross.email)).toBeInTheDocument();
1848
+ expect(screen.getByText('Team 5')).toBeInTheDocument();
1849
+ expect(screen.queryByText('Starter')).not.toBeInTheDocument();
1850
+ expect(screen.getByRole('menuitem', { name: 'Team Management' })).toBeInTheDocument();
1851
+ });
1852
+ await user.click(screen.getByRole('button', { name: /Subscription Team 5/i }));
1853
+ await waitFor(() => {
1854
+ expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
1855
+ });
1856
+ const currentPlanCard = screen.getByTestId('plan-card-team-5');
1857
+ expect(within(currentPlanCard).getByTestId('plan-manage-team-5')).toHaveTextContent('Current Plan');
1858
+ });
1572
1859
  it('allows local runtime to open the shared cloud-backed register route', async () => {
1573
1860
  const fetchMock = vi.fn((url) => {
1574
1861
  if (url.includes('/api/taskforce/auth/session')) {
@@ -1582,44 +1869,995 @@ describe('TaskforceCore Integration', () => {
1582
1869
  });
1583
1870
  }
1584
1871
  if (url.includes('/api/taskforce/config')) {
1585
- return Promise.resolve({
1586
- ok: true,
1587
- json: () => Promise.resolve({
1588
- ...mockConfig,
1589
- runtimeMode: 'local',
1590
- authRequiredForApi: false,
1591
- setupState: {
1592
- globalSetupState: 'present',
1593
- workspaceSetupState: 'present',
1594
- runtimeMode: 'local',
1595
- workspaceId: 'workspace-local-active',
1596
- mode: 'core',
1597
- forceSetup: false,
1598
- forceGlobalSetup: false,
1599
- forceWorkspaceSetup: false
1600
- }
1601
- })
1872
+ return Promise.resolve({
1873
+ ok: true,
1874
+ json: () => Promise.resolve({
1875
+ ...mockConfig,
1876
+ runtimeMode: 'local',
1877
+ authRequiredForApi: false,
1878
+ setupState: {
1879
+ globalSetupState: 'present',
1880
+ workspaceSetupState: 'present',
1881
+ runtimeMode: 'local',
1882
+ workspaceId: 'workspace-local-active',
1883
+ mode: 'core',
1884
+ forceSetup: false,
1885
+ forceGlobalSetup: false,
1886
+ forceWorkspaceSetup: false
1887
+ }
1888
+ })
1889
+ });
1890
+ }
1891
+ if (url.includes('/api/taskforce/workflow-templates')) {
1892
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
1893
+ }
1894
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
1895
+ });
1896
+ vi.stubGlobal('fetch', fetchMock);
1897
+ const { container } = render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
1898
+ await waitFor(() => {
1899
+ expect(screen.getByText('Create your Taskforce account.')).toBeInTheDocument();
1900
+ expect(screen.getByLabelText('Email')).toBeInTheDocument();
1901
+ expect(screen.getByLabelText('Display name')).toBeInTheDocument();
1902
+ expect(screen.getByPlaceholderText('Email')).toBeInTheDocument();
1903
+ expect(screen.getByPlaceholderText('Display name')).toBeInTheDocument();
1904
+ expect(screen.getByRole('button', { name: 'Create Account' })).toBeInTheDocument();
1905
+ expect(screen.getByRole('button', { name: 'Sign In' })).toBeInTheDocument();
1906
+ expect(screen.queryByRole('button', { name: 'Register' })).not.toBeInTheDocument();
1907
+ expect(screen.getByRole('button', { name: 'Close sign in' })).toBeInTheDocument();
1908
+ expect(screen.getByTestId('auth-submit-button')).toBeInTheDocument();
1909
+ expect(document.title).toBe('Create Account | Taskforce');
1910
+ });
1911
+ expect(container.querySelector('[aria-pressed]')).not.toBeInTheDocument();
1912
+ });
1913
+ it('keeps the verification screen focused on verification and lets the user request a fresh email token', async () => {
1914
+ const fetchMock = vi.fn((url) => {
1915
+ if (url.includes('/api/taskforce/auth/session')) {
1916
+ return Promise.resolve({
1917
+ ok: true,
1918
+ json: () => Promise.resolve({
1919
+ authenticated: false,
1920
+ runtimeMode: 'cloud',
1921
+ authRequiredForApi: false
1922
+ })
1923
+ });
1924
+ }
1925
+ if (url.includes('/api/taskforce/config')) {
1926
+ return Promise.resolve({
1927
+ ok: true,
1928
+ json: () => Promise.resolve({
1929
+ ...mockConfig,
1930
+ runtimeMode: 'cloud',
1931
+ authRequiredForApi: false,
1932
+ setupState: {
1933
+ globalSetupState: 'present',
1934
+ workspaceSetupState: 'present',
1935
+ runtimeMode: 'cloud',
1936
+ workspaceId: 'workspace-local-active',
1937
+ mode: 'core',
1938
+ forceSetup: false,
1939
+ forceGlobalSetup: false,
1940
+ forceWorkspaceSetup: false
1941
+ }
1942
+ })
1943
+ });
1944
+ }
1945
+ if (url.includes('/api/taskforce/workflow-templates')) {
1946
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
1947
+ }
1948
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
1949
+ });
1950
+ vi.stubGlobal('fetch', fetchMock);
1951
+ render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=verify'], children: _jsx(TaskforceCore, {}) }));
1952
+ await waitFor(() => {
1953
+ expect(screen.getByText('Enter your verification token.')).toBeInTheDocument();
1954
+ expect(screen.getByPlaceholderText('Email')).toBeInTheDocument();
1955
+ expect(screen.getByRole('button', { name: 'Verify Email' })).toBeInTheDocument();
1956
+ expect(screen.getByRole('button', { name: 'Back to Sign In' })).toBeInTheDocument();
1957
+ expect(screen.queryByRole('button', { name: 'Register' })).not.toBeInTheDocument();
1958
+ expect(screen.getByRole('button', { name: 'Resend Verification' })).toBeDisabled();
1959
+ });
1960
+ });
1961
+ it('allows the verify screen to resend a verification email after the user re-enters their email', async () => {
1962
+ const user = userEvent.setup();
1963
+ const fetchMock = vi.fn((input) => {
1964
+ const url = String(input);
1965
+ if (url.includes('/api/taskforce/auth/session')) {
1966
+ return Promise.resolve({
1967
+ ok: true,
1968
+ json: () => Promise.resolve({
1969
+ authenticated: false,
1970
+ runtimeMode: 'cloud',
1971
+ authRequiredForApi: false
1972
+ })
1973
+ });
1974
+ }
1975
+ if (url.includes('/api/taskforce/config')) {
1976
+ return Promise.resolve({
1977
+ ok: true,
1978
+ json: () => Promise.resolve({
1979
+ ...mockConfig,
1980
+ runtimeMode: 'cloud',
1981
+ authRequiredForApi: false,
1982
+ setupState: {
1983
+ globalSetupState: 'present',
1984
+ workspaceSetupState: 'present',
1985
+ runtimeMode: 'cloud',
1986
+ workspaceId: 'workspace-local-active',
1987
+ mode: 'core',
1988
+ forceSetup: false,
1989
+ forceGlobalSetup: false,
1990
+ forceWorkspaceSetup: false
1991
+ }
1992
+ })
1993
+ });
1994
+ }
1995
+ if (url.includes('/api/taskforce/auth/register')) {
1996
+ return Promise.resolve({
1997
+ ok: true,
1998
+ json: () => Promise.resolve({
1999
+ success: true,
2000
+ verificationRequired: true,
2001
+ workspaceSetupRequired: false,
2002
+ emailSent: true
2003
+ })
2004
+ });
2005
+ }
2006
+ if (url.includes('/api/taskforce/auth/verify-email/request')) {
2007
+ return Promise.resolve({
2008
+ ok: true,
2009
+ json: () => Promise.resolve({
2010
+ success: true,
2011
+ verificationToken: 'resent-token'
2012
+ })
2013
+ });
2014
+ }
2015
+ if (url.includes('/api/taskforce/public/pricing')) {
2016
+ return Promise.resolve({
2017
+ ok: true,
2018
+ json: () => Promise.resolve({
2019
+ plans: []
2020
+ })
2021
+ });
2022
+ }
2023
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
2024
+ });
2025
+ vi.stubGlobal('fetch', fetchMock);
2026
+ render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=verify'], children: _jsx(TaskforceCore, {}) }));
2027
+ await user.type(await screen.findByPlaceholderText('Email'), 'verify@example.com');
2028
+ await user.click(screen.getByRole('button', { name: 'Resend Verification' }));
2029
+ await waitFor(() => {
2030
+ expect(screen.getByRole('button', { name: /Resend Verification/ })).toBeInTheDocument();
2031
+ expect(screen.getByDisplayValue('resent-token')).toBeInTheDocument();
2032
+ expect(screen.getByText('Verification email resent.')).toBeInTheDocument();
2033
+ });
2034
+ expect(screen.getByRole('button', { name: /Resend Verification/ })).toBeDisabled();
2035
+ expect(fetchMock.mock.calls.some(([url]) => String(url).includes('/api/taskforce/auth/verify-email/request'))).toBe(true);
2036
+ });
2037
+ it('does not switch to verify mode after registration when verification is not required', async () => {
2038
+ const user = userEvent.setup();
2039
+ let authenticated = false;
2040
+ const fetchMock = vi.fn((input) => {
2041
+ const url = String(input);
2042
+ if (url.includes('/api/taskforce/auth/runtime-config')) {
2043
+ return Promise.resolve({
2044
+ ok: true,
2045
+ json: () => Promise.resolve({
2046
+ config: {
2047
+ baseUrl: 'https://app.taskforce.example',
2048
+ apiBaseUrl: 'https://app.taskforce.example/api',
2049
+ cloudAuthBaseUrl: 'https://app.taskforce.example',
2050
+ runtimeMode: 'cloud',
2051
+ authSource: 'cloud',
2052
+ workspaceMode: 'multi-cloud',
2053
+ workspaceSwitchingEnabled: true,
2054
+ authRequiredForApi: true
2055
+ }
2056
+ })
2057
+ });
2058
+ }
2059
+ if (url.includes('/api/taskforce/auth/session')) {
2060
+ return Promise.resolve({
2061
+ ok: true,
2062
+ json: () => Promise.resolve({
2063
+ authenticated,
2064
+ runtimeMode: 'cloud',
2065
+ authRequiredForApi: true,
2066
+ userId: authenticated ? 'user-no-verify' : null,
2067
+ workspaceId: null,
2068
+ betaAccess: true
2069
+ })
2070
+ });
2071
+ }
2072
+ if (url.includes('/api/taskforce/config')) {
2073
+ return Promise.resolve({
2074
+ ok: true,
2075
+ json: () => Promise.resolve({
2076
+ ...mockConfig,
2077
+ runtimeMode: 'cloud',
2078
+ authRequiredForApi: true,
2079
+ workspaceSwitchingEnabled: true,
2080
+ setupState: {
2081
+ globalSetupState: 'present',
2082
+ workspaceSetupState: 'missing',
2083
+ runtimeMode: 'cloud',
2084
+ workspaceId: 'default',
2085
+ mode: 'core',
2086
+ forceSetup: true,
2087
+ forceGlobalSetup: false,
2088
+ forceWorkspaceSetup: true
2089
+ }
2090
+ })
2091
+ });
2092
+ }
2093
+ if (url.includes('/api/taskforce/workspaces')) {
2094
+ return Promise.resolve({
2095
+ ok: true,
2096
+ json: () => Promise.resolve({
2097
+ success: true,
2098
+ workspaces: [],
2099
+ currentWorkspaceId: 'default'
2100
+ })
2101
+ });
2102
+ }
2103
+ if (url.includes('/api/taskforce/account/access-status')) {
2104
+ return Promise.resolve({
2105
+ ok: true,
2106
+ json: () => Promise.resolve({
2107
+ gate: 'ok',
2108
+ hasEntitlement: true,
2109
+ canAccessApp: true,
2110
+ canAccessPlans: true,
2111
+ message: null
2112
+ })
2113
+ });
2114
+ }
2115
+ if (url.includes('/api/taskforce/public/pricing')) {
2116
+ return Promise.resolve({
2117
+ ok: true,
2118
+ json: () => Promise.resolve({
2119
+ plans: []
2120
+ })
2121
+ });
2122
+ }
2123
+ if (url.includes('/api/taskforce/auth/register')) {
2124
+ authenticated = true;
2125
+ return Promise.resolve({
2126
+ ok: true,
2127
+ json: () => Promise.resolve({
2128
+ success: true,
2129
+ workspaceSetupRequired: true,
2130
+ verificationRequired: false,
2131
+ commercialState: 'active'
2132
+ })
2133
+ });
2134
+ }
2135
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
2136
+ });
2137
+ vi.stubGlobal('fetch', fetchMock);
2138
+ render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2Fplans'], children: _jsx(TaskforceCore, {}) }));
2139
+ await user.type(await screen.findByPlaceholderText('Email'), 'local@example.com');
2140
+ await user.type(screen.getByPlaceholderText('Display name'), 'Local User');
2141
+ await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
2142
+ await user.click(screen.getByRole('button', { name: 'Create Account' }));
2143
+ await waitFor(() => {
2144
+ expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
2145
+ });
2146
+ expect(screen.queryByPlaceholderText('Verification token')).not.toBeInTheDocument();
2147
+ expect(screen.queryByRole('button', { name: 'Create Account' })).not.toBeInTheDocument();
2148
+ });
2149
+ it('routes successful registration into plan selection when onboarding leaves the account pending plan selection', async () => {
2150
+ const user = userEvent.setup();
2151
+ let authenticated = false;
2152
+ const fetchMock = vi.fn((input) => {
2153
+ const url = String(input);
2154
+ if (url.includes('/api/taskforce/auth/runtime-config')) {
2155
+ return jsonResponse({
2156
+ config: {
2157
+ baseUrl: 'https://app.taskforce.example',
2158
+ apiBaseUrl: 'https://app.taskforce.example/api',
2159
+ cloudAuthBaseUrl: 'https://app.taskforce.example',
2160
+ runtimeMode: 'cloud',
2161
+ authSource: 'cloud',
2162
+ workspaceMode: 'multi-cloud',
2163
+ workspaceSwitchingEnabled: true,
2164
+ authRequiredForApi: true
2165
+ }
2166
+ });
2167
+ }
2168
+ if (url.includes('/api/taskforce/auth/session')) {
2169
+ return jsonResponse({
2170
+ authenticated,
2171
+ runtimeMode: 'cloud',
2172
+ authRequiredForApi: true,
2173
+ userId: authenticated ? 'user-1' : null,
2174
+ workspaceId: authenticated ? 'workspace-1' : null,
2175
+ betaAccess: true
2176
+ });
2177
+ }
2178
+ if (url.includes('/api/taskforce/config')) {
2179
+ return jsonResponse({
2180
+ ...mockConfig,
2181
+ runtimeMode: 'cloud',
2182
+ authRequiredForApi: true,
2183
+ workspaceSwitchingEnabled: true,
2184
+ setupState: {
2185
+ globalSetupState: 'present',
2186
+ workspaceSetupState: 'present',
2187
+ runtimeMode: 'cloud',
2188
+ workspaceId: 'workspace-1',
2189
+ mode: 'core',
2190
+ forceSetup: false,
2191
+ forceGlobalSetup: false,
2192
+ forceWorkspaceSetup: false
2193
+ }
2194
+ });
2195
+ }
2196
+ if (url.includes('/api/taskforce/workspaces')) {
2197
+ return jsonResponse({
2198
+ success: true,
2199
+ workspaces: [{
2200
+ id: 'workspace-1',
2201
+ slug: 'workspace-1',
2202
+ name: 'Workspace 1',
2203
+ description: null,
2204
+ role: 'owner',
2205
+ status: 'active',
2206
+ betaAccess: true
2207
+ }],
2208
+ currentWorkspaceId: 'workspace-1'
2209
+ });
2210
+ }
2211
+ if (url.includes('/api/taskforce/account/access-status')) {
2212
+ return jsonResponse({
2213
+ gate: 'plan_selection_required',
2214
+ hasEntitlement: true,
2215
+ canAccessApp: false,
2216
+ canAccessPlans: true,
2217
+ message: 'Choose a plan to continue.'
2218
+ });
2219
+ }
2220
+ if (url.includes('/api/taskforce/account/profile-summary')) {
2221
+ return jsonResponse({
2222
+ entitlementState: 'pending_plan_selection',
2223
+ gate: 'plan_selection_required',
2224
+ message: 'Choose a plan to continue.',
2225
+ workspaceId: 'workspace-1'
2226
+ });
2227
+ }
2228
+ if (url.includes('/api/taskforce/public/pricing')) {
2229
+ return jsonResponse({
2230
+ plans: [{
2231
+ planId: 'starter',
2232
+ displayName: 'Starter',
2233
+ status: 'active',
2234
+ defaultVersion: {
2235
+ planVersionId: 'starter-v1',
2236
+ versionNumber: 1,
2237
+ seatLimit: null,
2238
+ metadata: {}
2239
+ },
2240
+ features: [],
2241
+ pricing: {
2242
+ month: {
2243
+ planVersionId: 'starter-v1',
2244
+ interval: 'month',
2245
+ pricingType: 'free',
2246
+ stripePriceId: 'free:starter-v1:month',
2247
+ displayAmount: '$0.00',
2248
+ unitAmount: 0,
2249
+ currency: 'usd'
2250
+ },
2251
+ year: null
2252
+ }
2253
+ }]
2254
+ });
2255
+ }
2256
+ if (url.includes('/api/taskforce/auth/register')) {
2257
+ authenticated = true;
2258
+ return jsonResponse({
2259
+ success: true,
2260
+ workspaceSetupRequired: false,
2261
+ verificationRequired: false,
2262
+ planSelectionRequired: true,
2263
+ commercialState: 'pending_plan_selection'
2264
+ });
2265
+ }
2266
+ if (url.includes('/api/taskforce/categories')) {
2267
+ return jsonResponse(mockCategories);
2268
+ }
2269
+ if (url.includes('/api/taskforce/tasks')) {
2270
+ return jsonResponse({ tasks: mockTasks });
2271
+ }
2272
+ if (url.includes('/api/taskforce/archive')) {
2273
+ return jsonResponse({ archived: [] });
2274
+ }
2275
+ if (url.includes('/api/taskforce/data-version')) {
2276
+ return jsonResponse({ dataVersion: 1 });
2277
+ }
2278
+ if (url.includes('/api/taskforce/types')) {
2279
+ return jsonResponse({ types: [] });
2280
+ }
2281
+ if (url.includes('/api/taskforce/priorities')) {
2282
+ return jsonResponse({ priorities: [] });
2283
+ }
2284
+ if (url.includes('/api/taskforce/approaches')) {
2285
+ return jsonResponse({ approaches: [] });
2286
+ }
2287
+ if (url.includes('/api/taskforce/specialists')) {
2288
+ return jsonResponse({ specialists: [] });
2289
+ }
2290
+ if (url.includes('/api/taskforce/workflow-templates')) {
2291
+ return jsonResponse({ templates: [] });
2292
+ }
2293
+ return jsonResponse({});
2294
+ });
2295
+ vi.stubGlobal('fetch', fetchMock);
2296
+ render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
2297
+ await user.type(await screen.findByPlaceholderText('Email'), 'pending@example.com');
2298
+ await user.type(screen.getByPlaceholderText('Display name'), 'Pending User');
2299
+ await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
2300
+ await user.click(screen.getByRole('button', { name: 'Create Account' }));
2301
+ await waitFor(() => {
2302
+ expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
2303
+ expect(screen.getByText('Choose a plan to continue.')).toBeInTheDocument();
2304
+ });
2305
+ expect(fetchMock.mock.calls.some(([url]) => String(url).includes('/api/taskforce/billing/checkout-session'))).toBe(false);
2306
+ });
2307
+ it('keeps registration on plan selection when commercial onboarding is blocked even if the new cloud user has no workspace yet', async () => {
2308
+ const user = userEvent.setup();
2309
+ let authenticated = false;
2310
+ const fetchMock = vi.fn((input) => {
2311
+ const url = String(input);
2312
+ if (url.includes('/api/taskforce/auth/runtime-config')) {
2313
+ return jsonResponse({
2314
+ config: {
2315
+ baseUrl: 'https://app.taskforce.example',
2316
+ apiBaseUrl: 'https://app.taskforce.example/api',
2317
+ cloudAuthBaseUrl: 'https://app.taskforce.example',
2318
+ runtimeMode: 'cloud',
2319
+ authSource: 'cloud',
2320
+ workspaceMode: 'multi-cloud',
2321
+ workspaceSwitchingEnabled: true,
2322
+ authRequiredForApi: true
2323
+ }
2324
+ });
2325
+ }
2326
+ if (url.includes('/api/taskforce/auth/session')) {
2327
+ return jsonResponse({
2328
+ authenticated,
2329
+ runtimeMode: 'cloud',
2330
+ authRequiredForApi: true,
2331
+ userId: authenticated ? 'user-new' : null,
2332
+ workspaceId: null,
2333
+ betaAccess: true
2334
+ });
2335
+ }
2336
+ if (url.includes('/api/taskforce/config')) {
2337
+ return jsonResponse({
2338
+ ...mockConfig,
2339
+ runtimeMode: 'cloud',
2340
+ authRequiredForApi: true,
2341
+ workspaceSwitchingEnabled: true,
2342
+ setupState: {
2343
+ globalSetupState: 'present',
2344
+ workspaceSetupState: 'missing',
2345
+ runtimeMode: 'cloud',
2346
+ workspaceId: 'default',
2347
+ mode: 'core',
2348
+ forceSetup: false,
2349
+ forceGlobalSetup: false,
2350
+ forceWorkspaceSetup: false
2351
+ }
2352
+ });
2353
+ }
2354
+ if (url.includes('/api/taskforce/workspaces')) {
2355
+ return jsonResponse({
2356
+ success: true,
2357
+ workspaces: [],
2358
+ currentWorkspaceId: 'default'
2359
+ });
2360
+ }
2361
+ if (url.includes('/api/taskforce/account/access-status')) {
2362
+ return jsonResponse({
2363
+ gate: 'plan_selection_required',
2364
+ hasEntitlement: true,
2365
+ canAccessApp: false,
2366
+ canAccessPlans: true,
2367
+ message: 'Choose a plan to continue.'
2368
+ });
2369
+ }
2370
+ if (url.includes('/api/taskforce/account/profile-summary')) {
2371
+ return jsonResponse({
2372
+ entitlementState: 'pending_plan_selection',
2373
+ gate: 'plan_selection_required',
2374
+ message: 'Choose a plan to continue.',
2375
+ workspaceId: null
2376
+ });
2377
+ }
2378
+ if (url.includes('/api/taskforce/public/pricing')) {
2379
+ return jsonResponse({ plans: [] });
2380
+ }
2381
+ if (url.includes('/api/taskforce/auth/register')) {
2382
+ authenticated = true;
2383
+ return jsonResponse({
2384
+ success: true,
2385
+ workspaceSetupRequired: true,
2386
+ verificationRequired: false,
2387
+ planSelectionRequired: true,
2388
+ commercialState: 'pending_plan_selection'
2389
+ });
2390
+ }
2391
+ if (url.includes('/api/taskforce/categories'))
2392
+ return jsonResponse(mockCategories);
2393
+ if (url.includes('/api/taskforce/tasks'))
2394
+ return jsonResponse({ tasks: mockTasks });
2395
+ if (url.includes('/api/taskforce/archive'))
2396
+ return jsonResponse({ archived: [] });
2397
+ if (url.includes('/api/taskforce/data-version'))
2398
+ return jsonResponse({ dataVersion: 1 });
2399
+ if (url.includes('/api/taskforce/types'))
2400
+ return jsonResponse({ types: [] });
2401
+ if (url.includes('/api/taskforce/priorities'))
2402
+ return jsonResponse({ priorities: [] });
2403
+ if (url.includes('/api/taskforce/approaches'))
2404
+ return jsonResponse({ approaches: [] });
2405
+ if (url.includes('/api/taskforce/specialists'))
2406
+ return jsonResponse({ specialists: [] });
2407
+ if (url.includes('/api/taskforce/workflow-templates'))
2408
+ return jsonResponse({ templates: [] });
2409
+ return jsonResponse({});
2410
+ });
2411
+ vi.stubGlobal('fetch', fetchMock);
2412
+ render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
2413
+ await user.type(await screen.findByPlaceholderText('Email'), 'brandnew@example.com');
2414
+ await user.type(screen.getByPlaceholderText('Display name'), 'Brand New');
2415
+ await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
2416
+ await user.click(screen.getByRole('button', { name: 'Create Account' }));
2417
+ await waitFor(() => {
2418
+ expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
2419
+ expect(screen.getByText('Choose a plan to continue.')).toBeInTheDocument();
2420
+ });
2421
+ expect(screen.queryByText('Configure your new workspace.')).not.toBeInTheDocument();
2422
+ });
2423
+ it('routes successful registration into checkout when onboarding leaves the account checkout-pending', async () => {
2424
+ const user = userEvent.setup();
2425
+ let authenticated = false;
2426
+ const fetchMock = vi.fn((input) => {
2427
+ const url = String(input);
2428
+ if (url.includes('/api/taskforce/auth/runtime-config')) {
2429
+ return jsonResponse({
2430
+ config: {
2431
+ baseUrl: 'https://app.taskforce.example',
2432
+ apiBaseUrl: 'https://app.taskforce.example/api',
2433
+ cloudAuthBaseUrl: 'https://app.taskforce.example',
2434
+ runtimeMode: 'cloud',
2435
+ authSource: 'cloud',
2436
+ workspaceMode: 'multi-cloud',
2437
+ workspaceSwitchingEnabled: true,
2438
+ authRequiredForApi: true
2439
+ }
2440
+ });
2441
+ }
2442
+ if (url.includes('/api/taskforce/auth/session')) {
2443
+ return jsonResponse({
2444
+ authenticated,
2445
+ runtimeMode: 'cloud',
2446
+ authRequiredForApi: true,
2447
+ userId: authenticated ? 'user-1' : null,
2448
+ workspaceId: authenticated ? 'workspace-1' : null,
2449
+ betaAccess: true
2450
+ });
2451
+ }
2452
+ if (url.includes('/api/taskforce/config')) {
2453
+ return jsonResponse({
2454
+ ...mockConfig,
2455
+ runtimeMode: 'cloud',
2456
+ authRequiredForApi: true,
2457
+ workspaceSwitchingEnabled: true,
2458
+ setupState: {
2459
+ globalSetupState: 'present',
2460
+ workspaceSetupState: 'present',
2461
+ runtimeMode: 'cloud',
2462
+ workspaceId: 'workspace-1',
2463
+ mode: 'core',
2464
+ forceSetup: false,
2465
+ forceGlobalSetup: false,
2466
+ forceWorkspaceSetup: false
2467
+ }
2468
+ });
2469
+ }
2470
+ if (url.includes('/api/taskforce/workspaces')) {
2471
+ return jsonResponse({
2472
+ success: true,
2473
+ workspaces: [{
2474
+ id: 'workspace-1',
2475
+ slug: 'workspace-1',
2476
+ name: 'Workspace 1',
2477
+ description: null,
2478
+ role: 'owner',
2479
+ status: 'active',
2480
+ betaAccess: true
2481
+ }],
2482
+ currentWorkspaceId: 'workspace-1'
2483
+ });
2484
+ }
2485
+ if (url.includes('/api/taskforce/account/access-status')) {
2486
+ return jsonResponse({
2487
+ gate: 'checkout_pending',
2488
+ hasEntitlement: true,
2489
+ canAccessApp: false,
2490
+ canAccessPlans: true,
2491
+ message: 'Complete checkout to continue.'
2492
+ });
2493
+ }
2494
+ if (url.includes('/api/taskforce/account/profile-summary')) {
2495
+ return jsonResponse({
2496
+ planId: 'pro',
2497
+ planName: 'Pro',
2498
+ planVersionId: 'pro-v1',
2499
+ entitlementState: 'checkout_pending',
2500
+ gate: 'checkout_pending',
2501
+ message: 'Complete checkout to continue.',
2502
+ workspaceId: 'workspace-1'
2503
+ });
2504
+ }
2505
+ if (url.includes('/api/taskforce/public/pricing')) {
2506
+ return jsonResponse({
2507
+ plans: [{
2508
+ planId: 'pro',
2509
+ displayName: 'Pro',
2510
+ status: 'active',
2511
+ defaultVersion: {
2512
+ planVersionId: 'pro-v1',
2513
+ versionNumber: 1,
2514
+ seatLimit: null,
2515
+ metadata: {}
2516
+ },
2517
+ features: [],
2518
+ pricing: {
2519
+ month: {
2520
+ planVersionId: 'pro-v1',
2521
+ interval: 'month',
2522
+ pricingType: 'stripe',
2523
+ stripePriceId: 'price_pro_month',
2524
+ displayAmount: '$19.00',
2525
+ unitAmount: 1900,
2526
+ currency: 'usd'
2527
+ },
2528
+ year: {
2529
+ planVersionId: 'pro-v1',
2530
+ interval: 'year',
2531
+ pricingType: 'stripe',
2532
+ stripePriceId: 'price_pro_year',
2533
+ displayAmount: '$190.00',
2534
+ unitAmount: 19000,
2535
+ currency: 'usd'
2536
+ }
2537
+ }
2538
+ }]
2539
+ });
2540
+ }
2541
+ if (url.includes('/api/taskforce/auth/register')) {
2542
+ authenticated = true;
2543
+ return jsonResponse({
2544
+ success: true,
2545
+ workspaceSetupRequired: false,
2546
+ verificationRequired: false,
2547
+ checkoutPending: true,
2548
+ commercialState: 'checkout_pending'
2549
+ });
2550
+ }
2551
+ if (url.includes('/api/taskforce/billing/checkout-session')) {
2552
+ return jsonResponse({
2553
+ checkoutState: 'updated',
2554
+ message: 'Checkout started.'
2555
+ });
2556
+ }
2557
+ if (url.includes('/api/taskforce/categories')) {
2558
+ return jsonResponse(mockCategories);
2559
+ }
2560
+ if (url.includes('/api/taskforce/tasks')) {
2561
+ return jsonResponse({ tasks: mockTasks });
2562
+ }
2563
+ if (url.includes('/api/taskforce/archive')) {
2564
+ return jsonResponse({ archived: [] });
2565
+ }
2566
+ if (url.includes('/api/taskforce/data-version')) {
2567
+ return jsonResponse({ dataVersion: 1 });
2568
+ }
2569
+ if (url.includes('/api/taskforce/types')) {
2570
+ return jsonResponse({ types: [] });
2571
+ }
2572
+ if (url.includes('/api/taskforce/priorities')) {
2573
+ return jsonResponse({ priorities: [] });
2574
+ }
2575
+ if (url.includes('/api/taskforce/approaches')) {
2576
+ return jsonResponse({ approaches: [] });
2577
+ }
2578
+ if (url.includes('/api/taskforce/specialists')) {
2579
+ return jsonResponse({ specialists: [] });
2580
+ }
2581
+ if (url.includes('/api/taskforce/workflow-templates')) {
2582
+ return jsonResponse({ templates: [] });
2583
+ }
2584
+ return jsonResponse({});
2585
+ });
2586
+ vi.stubGlobal('fetch', fetchMock);
2587
+ render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&planId=pro&planVersionId=pro-v1&interval=year&next=%2F'], children: _jsx(TaskforceCore, {}) }));
2588
+ await user.type(await screen.findByPlaceholderText('Email'), 'checkout@example.com');
2589
+ await user.type(screen.getByPlaceholderText('Display name'), 'Checkout User');
2590
+ await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
2591
+ await user.click(screen.getByRole('button', { name: 'Create Account' }));
2592
+ await waitFor(() => {
2593
+ expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
2594
+ });
2595
+ await waitFor(() => {
2596
+ expect(fetchMock.mock.calls.some(([url]) => String(url).includes('/api/taskforce/billing/checkout-session'))).toBe(true);
2597
+ });
2598
+ });
2599
+ it('routes successful registration into workspace setup when onboarding auto-grants free, even if a stale pre-registration session check resolves later', async () => {
2600
+ const user = userEvent.setup();
2601
+ let authenticated = false;
2602
+ let authSessionCallCount = 0;
2603
+ let releaseInitialSession = null;
2604
+ const initialSessionGate = new Promise((resolve) => {
2605
+ releaseInitialSession = resolve;
2606
+ });
2607
+ const fetchMock = vi.fn((input) => {
2608
+ const url = String(input);
2609
+ if (url.includes('/api/taskforce/auth/runtime-config')) {
2610
+ return jsonResponse({
2611
+ config: {
2612
+ baseUrl: 'https://app.taskforce.example',
2613
+ apiBaseUrl: 'https://app.taskforce.example/api',
2614
+ cloudAuthBaseUrl: 'https://app.taskforce.example',
2615
+ runtimeMode: 'cloud',
2616
+ authSource: 'cloud',
2617
+ workspaceMode: 'multi-cloud',
2618
+ workspaceSwitchingEnabled: true,
2619
+ authRequiredForApi: true
2620
+ }
2621
+ });
2622
+ }
2623
+ if (url.includes('/api/taskforce/auth/session')) {
2624
+ authSessionCallCount += 1;
2625
+ const payload = () => ({
2626
+ authenticated,
2627
+ runtimeMode: 'cloud',
2628
+ authRequiredForApi: true,
2629
+ userId: authenticated ? 'user-free-1' : null,
2630
+ workspaceId: null,
2631
+ betaAccess: true
2632
+ });
2633
+ if (authSessionCallCount === 1) {
2634
+ return initialSessionGate.then(() => jsonResponse(payload()));
2635
+ }
2636
+ return jsonResponse(payload());
2637
+ }
2638
+ if (url.includes('/api/taskforce/config')) {
2639
+ return jsonResponse({
2640
+ ...mockConfig,
2641
+ runtimeMode: 'cloud',
2642
+ authRequiredForApi: true,
2643
+ workspaceSwitchingEnabled: true,
2644
+ setupState: {
2645
+ globalSetupState: 'present',
2646
+ workspaceSetupState: 'missing',
2647
+ runtimeMode: 'cloud',
2648
+ workspaceId: 'default',
2649
+ mode: 'core',
2650
+ forceSetup: true,
2651
+ forceGlobalSetup: false,
2652
+ forceWorkspaceSetup: true
2653
+ }
2654
+ });
2655
+ }
2656
+ if (url.includes('/api/taskforce/workspaces')) {
2657
+ return jsonResponse({
2658
+ success: true,
2659
+ workspaces: [],
2660
+ currentWorkspaceId: 'default'
2661
+ });
2662
+ }
2663
+ if (url.includes('/api/taskforce/account/access-status')) {
2664
+ return jsonResponse({
2665
+ gate: 'ok',
2666
+ hasEntitlement: true,
2667
+ canAccessApp: true,
2668
+ canAccessPlans: true,
2669
+ message: null
2670
+ });
2671
+ }
2672
+ if (url.includes('/api/taskforce/public/pricing')) {
2673
+ return jsonResponse({ plans: [] });
2674
+ }
2675
+ if (url.includes('/api/taskforce/auth/register')) {
2676
+ authenticated = true;
2677
+ return jsonResponse({
2678
+ success: true,
2679
+ workspaceSetupRequired: true,
2680
+ verificationRequired: false,
2681
+ commercialState: 'active'
2682
+ });
2683
+ }
2684
+ if (url.includes('/api/taskforce/categories'))
2685
+ return jsonResponse(mockCategories);
2686
+ if (url.includes('/api/taskforce/tasks'))
2687
+ return jsonResponse({ tasks: mockTasks });
2688
+ if (url.includes('/api/taskforce/archive'))
2689
+ return jsonResponse({ archived: [] });
2690
+ if (url.includes('/api/taskforce/data-version'))
2691
+ return jsonResponse({ dataVersion: 1 });
2692
+ if (url.includes('/api/taskforce/types'))
2693
+ return jsonResponse({ types: [] });
2694
+ if (url.includes('/api/taskforce/priorities'))
2695
+ return jsonResponse({ priorities: [] });
2696
+ if (url.includes('/api/taskforce/approaches'))
2697
+ return jsonResponse({ approaches: [] });
2698
+ if (url.includes('/api/taskforce/specialists'))
2699
+ return jsonResponse({ specialists: [] });
2700
+ if (url.includes('/api/taskforce/workflow-templates'))
2701
+ return jsonResponse({ templates: [] });
2702
+ if (url.includes('/api/taskforce/taxonomy-library')) {
2703
+ return jsonResponse({
2704
+ packs: [{
2705
+ id: 'general',
2706
+ label: 'General',
2707
+ description: 'Starter',
2708
+ isDefaultStarter: true,
2709
+ sections: {
2710
+ categories: [{ value: 'general', label: 'General' }],
2711
+ taskTypes: [{ value: 'task', label: 'Task' }],
2712
+ priorities: [{ value: 1, label: 'Low' }]
2713
+ }
2714
+ }]
2715
+ });
2716
+ }
2717
+ return jsonResponse({});
2718
+ });
2719
+ vi.stubGlobal('fetch', fetchMock);
2720
+ render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
2721
+ await user.type(await screen.findByPlaceholderText('Email'), 'free-user@example.com');
2722
+ await user.type(screen.getByPlaceholderText('Display name'), 'Free User');
2723
+ await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
2724
+ await user.click(screen.getByRole('button', { name: 'Create Account' }));
2725
+ await act(async () => {
2726
+ await Promise.resolve();
2727
+ });
2728
+ await waitFor(() => {
2729
+ expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
2730
+ });
2731
+ expect(fetchMock.mock.calls.filter(([url]) => String(url).includes('/api/taskforce/auth/session')).length).toBeGreaterThanOrEqual(2);
2732
+ await act(async () => {
2733
+ releaseInitialSession?.();
2734
+ await Promise.resolve();
2735
+ });
2736
+ await waitFor(() => {
2737
+ expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
2738
+ });
2739
+ expect(screen.queryByText('Sign in with your account credentials.')).not.toBeInTheDocument();
2740
+ });
2741
+ it('routes successful registration directly into the app when onboarding auto-grants free and the new cloud user already has an accessible workspace', async () => {
2742
+ const user = userEvent.setup();
2743
+ let authenticated = false;
2744
+ const fetchMock = vi.fn((input) => {
2745
+ const url = String(input);
2746
+ if (url.includes('/api/taskforce/auth/runtime-config')) {
2747
+ return jsonResponse({
2748
+ config: {
2749
+ baseUrl: 'https://app.taskforce.example',
2750
+ apiBaseUrl: 'https://app.taskforce.example/api',
2751
+ cloudAuthBaseUrl: 'https://app.taskforce.example',
2752
+ runtimeMode: 'cloud',
2753
+ authSource: 'cloud',
2754
+ workspaceMode: 'multi-cloud',
2755
+ workspaceSwitchingEnabled: true,
2756
+ authRequiredForApi: true
2757
+ }
2758
+ });
2759
+ }
2760
+ if (url.includes('/api/taskforce/auth/session')) {
2761
+ return jsonResponse({
2762
+ authenticated,
2763
+ runtimeMode: 'cloud',
2764
+ authRequiredForApi: true,
2765
+ userId: authenticated ? 'user-free-2' : null,
2766
+ workspaceId: authenticated ? 'workspace-1' : null,
2767
+ betaAccess: true
2768
+ });
2769
+ }
2770
+ if (url.includes('/api/taskforce/config')) {
2771
+ return jsonResponse({
2772
+ ...mockConfig,
2773
+ runtimeMode: 'cloud',
2774
+ authRequiredForApi: true,
2775
+ workspaceSwitchingEnabled: true,
2776
+ workspaceId: 'workspace-1',
2777
+ setupState: {
2778
+ globalSetupState: 'present',
2779
+ workspaceSetupState: 'present',
2780
+ runtimeMode: 'cloud',
2781
+ workspaceId: 'workspace-1',
2782
+ mode: 'core',
2783
+ forceSetup: false,
2784
+ forceGlobalSetup: false,
2785
+ forceWorkspaceSetup: false
2786
+ }
1602
2787
  });
1603
2788
  }
1604
- if (url.includes('/api/taskforce/workflow-templates')) {
1605
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
2789
+ if (url.includes('/api/taskforce/workspaces')) {
2790
+ return jsonResponse({
2791
+ success: true,
2792
+ workspaces: [{
2793
+ id: 'workspace-1',
2794
+ slug: 'workspace-1',
2795
+ name: 'Workspace 1',
2796
+ description: null,
2797
+ role: 'owner',
2798
+ status: 'active',
2799
+ betaAccess: true
2800
+ }],
2801
+ currentWorkspaceId: 'workspace-1'
2802
+ });
1606
2803
  }
1607
- return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
2804
+ if (url.includes('/api/taskforce/account/access-status')) {
2805
+ return jsonResponse({
2806
+ gate: 'ok',
2807
+ hasEntitlement: true,
2808
+ canAccessApp: true,
2809
+ canAccessPlans: true,
2810
+ message: null
2811
+ });
2812
+ }
2813
+ if (url.includes('/api/taskforce/public/pricing')) {
2814
+ return jsonResponse({ plans: [] });
2815
+ }
2816
+ if (url.includes('/api/taskforce/auth/register')) {
2817
+ authenticated = true;
2818
+ return jsonResponse({
2819
+ success: true,
2820
+ workspaceSetupRequired: false,
2821
+ verificationRequired: false,
2822
+ commercialState: 'active'
2823
+ });
2824
+ }
2825
+ if (url.includes('/api/taskforce/categories'))
2826
+ return jsonResponse(mockCategories);
2827
+ if (url.includes('/api/taskforce/tasks'))
2828
+ return jsonResponse({ tasks: mockTasks });
2829
+ if (url.includes('/api/taskforce/archive'))
2830
+ return jsonResponse({ archived: [] });
2831
+ if (url.includes('/api/taskforce/data-version'))
2832
+ return jsonResponse({ dataVersion: 1 });
2833
+ if (url.includes('/api/taskforce/types'))
2834
+ return jsonResponse({ types: [] });
2835
+ if (url.includes('/api/taskforce/priorities'))
2836
+ return jsonResponse({ priorities: [] });
2837
+ if (url.includes('/api/taskforce/approaches'))
2838
+ return jsonResponse({ approaches: [] });
2839
+ if (url.includes('/api/taskforce/specialists'))
2840
+ return jsonResponse({ specialists: [] });
2841
+ if (url.includes('/api/taskforce/workflow-templates'))
2842
+ return jsonResponse({ templates: [] });
2843
+ return jsonResponse({});
1608
2844
  });
1609
2845
  vi.stubGlobal('fetch', fetchMock);
1610
- const { container } = render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
2846
+ render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
2847
+ await user.type(await screen.findByPlaceholderText('Email'), 'existing-workspace@example.com');
2848
+ await user.type(screen.getByPlaceholderText('Display name'), 'Existing Workspace User');
2849
+ await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
2850
+ await user.click(screen.getByRole('button', { name: 'Create Account' }));
2851
+ await act(async () => {
2852
+ await Promise.resolve();
2853
+ });
1611
2854
  await waitFor(() => {
1612
- expect(screen.getByText('Create your Taskforce account.')).toBeInTheDocument();
1613
- expect(screen.getByPlaceholderText('Email')).toBeInTheDocument();
1614
- expect(screen.getByPlaceholderText('Display name')).toBeInTheDocument();
1615
- expect(screen.getByRole('button', { name: 'Create Account' })).toBeInTheDocument();
1616
- expect(screen.getByRole('button', { name: 'Sign In' })).toBeInTheDocument();
1617
- expect(screen.queryByRole('button', { name: 'Register' })).not.toBeInTheDocument();
1618
- expect(screen.getByRole('button', { name: 'Close sign in' })).toBeInTheDocument();
2855
+ expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
1619
2856
  });
1620
- expect(container.querySelector('[aria-pressed]')).not.toBeInTheDocument();
2857
+ expect(screen.queryByText('Configure your new workspace.')).not.toBeInTheDocument();
2858
+ expect(screen.queryByRole('heading', { name: 'Subscription Plans' })).not.toBeInTheDocument();
1621
2859
  });
1622
- it('does not switch to verify mode after registration when verification is not required', async () => {
2860
+ it('shows verification before workspace setup when registration still requires email verification', async () => {
1623
2861
  const user = userEvent.setup();
1624
2862
  const fetchMock = vi.fn((input) => {
1625
2863
  const url = String(input);
@@ -1629,7 +2867,7 @@ describe('TaskforceCore Integration', () => {
1629
2867
  json: () => Promise.resolve({
1630
2868
  authenticated: false,
1631
2869
  runtimeMode: 'cloud',
1632
- authRequiredForApi: false
2870
+ authRequiredForApi: true
1633
2871
  })
1634
2872
  });
1635
2873
  }
@@ -1639,12 +2877,12 @@ describe('TaskforceCore Integration', () => {
1639
2877
  json: () => Promise.resolve({
1640
2878
  ...mockConfig,
1641
2879
  runtimeMode: 'cloud',
1642
- authRequiredForApi: false,
2880
+ authRequiredForApi: true,
1643
2881
  setupState: {
1644
2882
  globalSetupState: 'present',
1645
2883
  workspaceSetupState: 'present',
1646
2884
  runtimeMode: 'cloud',
1647
- workspaceId: 'workspace-local-active',
2885
+ workspaceId: 'default',
1648
2886
  mode: 'core',
1649
2887
  forceSetup: false,
1650
2888
  forceGlobalSetup: false,
@@ -1666,8 +2904,9 @@ describe('TaskforceCore Integration', () => {
1666
2904
  ok: true,
1667
2905
  json: () => Promise.resolve({
1668
2906
  success: true,
1669
- workspaceSetupRequired: false,
1670
- verificationRequired: false
2907
+ workspaceSetupRequired: true,
2908
+ verificationRequired: true,
2909
+ emailSent: true
1671
2910
  })
1672
2911
  });
1673
2912
  }
@@ -1680,10 +2919,10 @@ describe('TaskforceCore Integration', () => {
1680
2919
  await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
1681
2920
  await user.click(screen.getByRole('button', { name: 'Create Account' }));
1682
2921
  await waitFor(() => {
1683
- expect(screen.getByText('Subscription Plans')).toBeInTheDocument();
2922
+ expect(screen.getByPlaceholderText('Verification token')).toBeInTheDocument();
1684
2923
  });
1685
- expect(screen.queryByPlaceholderText('Verification token')).not.toBeInTheDocument();
1686
2924
  expect(screen.queryByRole('button', { name: 'Create Account' })).not.toBeInTheDocument();
2925
+ expect(screen.queryByText('Subscription Plans')).not.toBeInTheDocument();
1687
2926
  });
1688
2927
  it('allows returning to sign-in/register from forgot mode in local runtime', async () => {
1689
2928
  const user = userEvent.setup();
@@ -1820,7 +3059,7 @@ describe('TaskforceCore Integration', () => {
1820
3059
  json: () => Promise.resolve({
1821
3060
  success: false,
1822
3061
  code: 'SIGNUP_PLAN_NOT_ENABLED',
1823
- error: 'Configured default signup plan version \"team-10-v1\" belongs to a disabled plan.'
3062
+ error: 'Configured onboarding plan version \"team-10-v1\" belongs to a disabled plan.'
1824
3063
  })
1825
3064
  });
1826
3065
  }
@@ -2072,36 +3311,211 @@ describe('TaskforceCore Integration', () => {
2072
3311
  return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
2073
3312
  }
2074
3313
  if (url.includes('/api/taskforce/workflow-templates')) {
2075
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
3314
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
3315
+ }
3316
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
3317
+ });
3318
+ vi.stubGlobal('fetch', fetchMock);
3319
+ render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, {}) }));
3320
+ await waitFor(() => {
3321
+ expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
3322
+ });
3323
+ const nameInput = screen.getByPlaceholderText('Workspace name');
3324
+ await user.clear(nameInput);
3325
+ await user.type(nameInput, 'Delta Project');
3326
+ await user.type(screen.getByPlaceholderText('Workspace description (optional)'), 'Workspace desc');
3327
+ await user.click(screen.getByRole('button', { name: 'Continue' }));
3328
+ await user.click(screen.getByRole('button', { name: 'Create Workspace' }));
3329
+ await waitFor(() => {
3330
+ expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
3331
+ });
3332
+ expect(fetchMock).toHaveBeenCalledWith('/api/taskforce/workspace-profile', expect.objectContaining({ method: 'POST' }));
3333
+ const workspaceSyncStateCall = fetchMock.mock.calls.find((call) => {
3334
+ if (!String(call[0]).includes('/api/taskforce/ui-state'))
3335
+ return false;
3336
+ const init = call[1];
3337
+ if (String(init?.method || '').toUpperCase() !== 'POST')
3338
+ return false;
3339
+ const body = JSON.parse(String(init?.body || '{}'));
3340
+ return body?.stateKey === 'workspace-sync';
3341
+ });
3342
+ const workspaceSyncStateBody = JSON.parse(String(workspaceSyncStateCall?.[1]?.body || '{}'));
3343
+ expect(workspaceSyncStateBody.stateKey).toBe('workspace-sync');
3344
+ expect(workspaceSyncStateBody.patch?.enabled).toBe(false);
3345
+ });
3346
+ it('creates the first cloud workspace through the workspace creation flow during setup', async () => {
3347
+ const user = userEvent.setup();
3348
+ let workspaceCreated = false;
3349
+ const fetchMock = vi.fn((input, init) => {
3350
+ const url = String(input);
3351
+ if (url.includes('/api/taskforce/auth/runtime-config')) {
3352
+ return jsonResponse({
3353
+ success: true,
3354
+ config: {
3355
+ baseUrl: 'https://app.taskforce.example',
3356
+ apiBaseUrl: 'https://app.taskforce.example/api',
3357
+ cloudAuthBaseUrl: 'https://app.taskforce.example',
3358
+ runtimeMode: 'cloud',
3359
+ authSource: 'cloud',
3360
+ workspaceMode: 'multi-cloud',
3361
+ workspaceSwitchingEnabled: true,
3362
+ cloudAuthViaLocalProxy: false
3363
+ }
3364
+ });
3365
+ }
3366
+ if (url.includes('/api/taskforce/auth/session')) {
3367
+ return jsonResponse({
3368
+ authenticated: true,
3369
+ runtimeMode: 'cloud',
3370
+ authRequiredForApi: true,
3371
+ userId: 'user-cloud-1',
3372
+ workspaceId: workspaceCreated ? 'workspace-cloud-new' : 'default'
3373
+ });
3374
+ }
3375
+ if (url.includes('/api/taskforce/account/access-status')) {
3376
+ return jsonResponse({
3377
+ gate: 'ok',
3378
+ hasEntitlement: true,
3379
+ canAccessApp: true,
3380
+ canAccessPlans: true,
3381
+ message: null
3382
+ });
3383
+ }
3384
+ if (url.includes('/api/taskforce/config')) {
3385
+ return jsonResponse({
3386
+ ...mockConfig,
3387
+ runtimeMode: 'cloud',
3388
+ authRequiredForApi: true,
3389
+ userId: 'user-cloud-1',
3390
+ projectName: workspaceCreated ? 'Cloud Mission' : '',
3391
+ setupState: {
3392
+ globalSetupState: 'present',
3393
+ workspaceSetupState: workspaceCreated ? 'present' : 'missing',
3394
+ runtimeMode: 'cloud',
3395
+ workspaceId: workspaceCreated ? 'workspace-cloud-new' : 'default',
3396
+ mode: 'core',
3397
+ forceSetup: false,
3398
+ forceGlobalSetup: false,
3399
+ forceWorkspaceSetup: false,
3400
+ workspace: workspaceCreated
3401
+ ? { id: 'workspace-cloud-new', slug: 'workspace-cloud-new', name: 'Cloud Mission', description: 'Cloud workspace desc' }
3402
+ : null,
3403
+ suggestedWorkspaceName: ''
3404
+ },
3405
+ runtimeCapabilities: {
3406
+ runtimeMode: 'cloud',
3407
+ supportsCloudAuth: true,
3408
+ supportsAdminManagedSetupOverrides: true,
3409
+ supportsPackageScriptSetupOverrides: false
3410
+ }
3411
+ });
3412
+ }
3413
+ if (url.includes('/api/taskforce/workspaces') && String(init?.method || 'GET').toUpperCase() === 'GET') {
3414
+ return jsonResponse({
3415
+ success: true,
3416
+ workspaces: workspaceCreated
3417
+ ? [{
3418
+ id: 'workspace-cloud-new',
3419
+ slug: 'workspace-cloud-new',
3420
+ name: 'Cloud Mission',
3421
+ description: 'Cloud workspace desc',
3422
+ role: 'owner',
3423
+ status: 'active',
3424
+ betaAccess: true
3425
+ }]
3426
+ : [],
3427
+ currentWorkspaceId: workspaceCreated ? 'workspace-cloud-new' : 'default'
3428
+ });
3429
+ }
3430
+ if (url.includes('/api/taskforce/workspaces') && String(init?.method || '').toUpperCase() === 'POST') {
3431
+ workspaceCreated = true;
3432
+ return jsonResponse({
3433
+ success: true,
3434
+ workspace: {
3435
+ id: 'workspace-cloud-new',
3436
+ slug: 'workspace-cloud-new',
3437
+ name: 'Cloud Mission',
3438
+ description: 'Cloud workspace desc',
3439
+ role: 'owner',
3440
+ status: 'active',
3441
+ betaAccess: true
3442
+ }
3443
+ }, 201);
3444
+ }
3445
+ if (url.includes('/api/taskforce/session/workspace')) {
3446
+ return jsonResponse({
3447
+ success: true,
3448
+ workspaceId: 'workspace-cloud-new',
3449
+ role: 'owner'
3450
+ });
3451
+ }
3452
+ if (url.includes('/api/taskforce/ui-state?key=app')) {
3453
+ return jsonResponse({ success: true, state: {} });
3454
+ }
3455
+ if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
3456
+ return jsonResponse({
3457
+ success: true,
3458
+ state: {
3459
+ version: 2,
3460
+ enabled: false,
3461
+ phase: 'idle',
3462
+ pullCursor: null,
3463
+ lastPullAt: null,
3464
+ lastPushAt: null,
3465
+ lastSyncedAt: null
3466
+ }
3467
+ });
3468
+ }
3469
+ if (url.includes('/api/taskforce/ui-state')) {
3470
+ return jsonResponse({ success: true });
3471
+ }
3472
+ if (url.includes('/api/taskforce/categories')) {
3473
+ return jsonResponse(mockCategories);
3474
+ }
3475
+ if (url.includes('/api/taskforce/tasks')) {
3476
+ return jsonResponse({ tasks: mockTasks });
3477
+ }
3478
+ if (url.includes('/api/taskforce/archive')) {
3479
+ return jsonResponse({ archived: [] });
3480
+ }
3481
+ if (url.includes('/api/taskforce/data-version')) {
3482
+ return jsonResponse({ dataVersion: 1 });
3483
+ }
3484
+ if (url.includes('/api/taskforce/types')) {
3485
+ return jsonResponse({ types: [] });
3486
+ }
3487
+ if (url.includes('/api/taskforce/priorities')) {
3488
+ return jsonResponse({ priorities: [] });
3489
+ }
3490
+ if (url.includes('/api/taskforce/approaches')) {
3491
+ return jsonResponse({ approaches: [] });
3492
+ }
3493
+ if (url.includes('/api/taskforce/specialists')) {
3494
+ return jsonResponse({ specialists: [] });
3495
+ }
3496
+ if (url.includes('/api/taskforce/workflow-templates')) {
3497
+ return jsonResponse({ templates: [] });
2076
3498
  }
2077
- return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
3499
+ return jsonResponse({});
2078
3500
  });
2079
3501
  vi.stubGlobal('fetch', fetchMock);
2080
- render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, {}) }));
3502
+ render(_jsx(MemoryRouter, { initialEntries: ['/setup?step=workspace'], children: _jsx(TaskforceCore, {}) }));
2081
3503
  await waitFor(() => {
2082
- expect(screen.getByText('Workspace Setup Required')).toBeInTheDocument();
3504
+ expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
2083
3505
  });
2084
- const nameInput = screen.getByPlaceholderText('Project name');
3506
+ expect(screen.getByLabelText('Cloud runtime')).toBeInTheDocument();
3507
+ const nameInput = screen.getByPlaceholderText('Workspace name');
2085
3508
  await user.clear(nameInput);
2086
- await user.type(nameInput, 'Delta Project');
2087
- await user.type(screen.getByPlaceholderText('Project description (optional)'), 'Workspace desc');
2088
- await user.click(screen.getByRole('button', { name: 'Save Project Setup' }));
3509
+ await user.type(nameInput, 'Cloud Mission');
3510
+ await user.type(screen.getByPlaceholderText('Workspace description (optional)'), 'Cloud workspace desc');
3511
+ await user.click(screen.getByRole('button', { name: 'Continue' }));
3512
+ await user.click(screen.getByRole('button', { name: 'Create Workspace' }));
2089
3513
  await waitFor(() => {
2090
3514
  expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
2091
3515
  });
2092
- expect(fetchMock).toHaveBeenCalledWith('/api/taskforce/workspace-profile', expect.objectContaining({ method: 'POST' }));
2093
- const workspaceSyncStateCall = fetchMock.mock.calls.find((call) => {
2094
- if (!String(call[0]).includes('/api/taskforce/ui-state'))
2095
- return false;
2096
- const init = call[1];
2097
- if (String(init?.method || '').toUpperCase() !== 'POST')
2098
- return false;
2099
- const body = JSON.parse(String(init?.body || '{}'));
2100
- return body?.stateKey === 'workspace-sync';
2101
- });
2102
- const workspaceSyncStateBody = JSON.parse(String(workspaceSyncStateCall?.[1]?.body || '{}'));
2103
- expect(workspaceSyncStateBody.stateKey).toBe('workspace-sync');
2104
- expect(workspaceSyncStateBody.patch?.enabled).toBe(false);
3516
+ expect(fetchMock.mock.calls.some((call) => (String(call[0]).includes('/api/taskforce/workspaces')
3517
+ && String(call[1]?.method || '').toUpperCase() === 'POST'))).toBe(true);
3518
+ expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('/api/taskforce/workspace-profile'))).toBe(false);
2105
3519
  });
2106
3520
  it('applies the selected starter taxonomy pack during workspace setup', async () => {
2107
3521
  const user = userEvent.setup();
@@ -2209,10 +3623,22 @@ describe('TaskforceCore Integration', () => {
2209
3623
  vi.stubGlobal('fetch', fetchMock);
2210
3624
  render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, {}) }));
2211
3625
  await waitFor(() => {
2212
- expect(screen.getByText('Workspace Setup Required')).toBeInTheDocument();
2213
- });
2214
- await user.click(screen.getByRole('radio', { name: /software development/i }));
2215
- await user.click(screen.getByRole('button', { name: 'Save Project Setup' }));
3626
+ expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
3627
+ });
3628
+ expect(screen.getByLabelText('Local runtime')).toBeInTheDocument();
3629
+ await user.click(screen.getByRole('button', { name: 'Continue' }));
3630
+ expect(screen.queryByRole('radio', { name: /create new local workspace/i })).not.toBeInTheDocument();
3631
+ expect(screen.queryByRole('radio', { name: /sync existing cloud workspace/i })).not.toBeInTheDocument();
3632
+ expect(screen.queryByText(/Includes \d+ categories?/i)).not.toBeInTheDocument();
3633
+ expect(screen.getByRole('option', { name: 'General' })).toBeInTheDocument();
3634
+ expect(screen.getByText('Task')).toBeInTheDocument();
3635
+ expect(screen.getByText('Low')).toBeInTheDocument();
3636
+ await user.selectOptions(screen.getByRole('combobox'), 'software-development');
3637
+ expect(screen.getByText('Engineering')).toBeInTheDocument();
3638
+ expect(screen.getByText('Feature, Bug')).toBeInTheDocument();
3639
+ expect(screen.getByText('Low, Medium, High, Critical')).toBeInTheDocument();
3640
+ await user.click(screen.getByRole('button', { name: 'Continue' }));
3641
+ await user.click(screen.getByRole('button', { name: 'Create Workspace' }));
2216
3642
  await waitFor(() => {
2217
3643
  expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
2218
3644
  });
@@ -2305,9 +3731,9 @@ describe('TaskforceCore Integration', () => {
2305
3731
  expect(configCalls).toBeGreaterThan(1);
2306
3732
  });
2307
3733
  expect(screen.getByRole('button', { name: /add task/i })).toBeInTheDocument();
2308
- expect(screen.queryByText('Workspace Setup Required')).not.toBeInTheDocument();
3734
+ expect(screen.queryByText('Configure your new workspace.')).not.toBeInTheDocument();
2309
3735
  });
2310
- it('allows workspace setup to sync from selected cloud project into matching local workspace id', async () => {
3736
+ it('keeps setup blocking while the selected cloud workspace is performing its first local import', async () => {
2311
3737
  const user = userEvent.setup();
2312
3738
  let workspaceSaved = false;
2313
3739
  const fetchMock = vi.fn((url, init) => {
@@ -2383,9 +3809,45 @@ describe('TaskforceCore Integration', () => {
2383
3809
  if (url.includes('/api/taskforce/session/workspace')) {
2384
3810
  return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
2385
3811
  }
3812
+ if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
3813
+ return Promise.resolve({
3814
+ ok: true,
3815
+ json: () => Promise.resolve({
3816
+ success: true,
3817
+ state: workspaceSaved
3818
+ ? {
3819
+ version: 2,
3820
+ enabled: true,
3821
+ phase: 'attach-cloud',
3822
+ pullCursor: null,
3823
+ lastPullAt: null,
3824
+ lastPushAt: null,
3825
+ lastSyncedAt: null
3826
+ }
3827
+ : {
3828
+ version: 2,
3829
+ enabled: false,
3830
+ phase: 'idle',
3831
+ pullCursor: null,
3832
+ lastPullAt: null,
3833
+ lastPushAt: null,
3834
+ lastSyncedAt: null
3835
+ }
3836
+ })
3837
+ });
3838
+ }
2386
3839
  if (url.includes('/api/taskforce/ui-state')) {
2387
3840
  return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
2388
3841
  }
3842
+ if (url.includes('https://cloud.example.com/api/taskforce/sync/workspace/handshake')) {
3843
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
3844
+ }
3845
+ if (url.includes('https://cloud.example.com/api/taskforce/sync/workspace/pull')) {
3846
+ return new Promise(() => { });
3847
+ }
3848
+ if (url === '/api/taskforce/sync/workspace/apply-local') {
3849
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
3850
+ }
2389
3851
  if (url.includes('/api/taskforce/tasks')) {
2390
3852
  return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
2391
3853
  }
@@ -2397,17 +3859,18 @@ describe('TaskforceCore Integration', () => {
2397
3859
  vi.stubGlobal('fetch', fetchMock);
2398
3860
  render(_jsx(MemoryRouter, { initialEntries: ['/setup?step=workspace&source=cloud'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
2399
3861
  await waitFor(() => {
2400
- expect(screen.getByText('Workspace Setup Required')).toBeInTheDocument();
3862
+ expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
2401
3863
  });
2402
- await user.click(screen.getByRole('radio', { name: /sync existing cloud project/i }));
2403
3864
  await waitFor(() => {
2404
3865
  expect(screen.getByRole('option', { name: /Cloud Project 123/ })).toBeInTheDocument();
2405
3866
  });
2406
3867
  await user.selectOptions(screen.getByRole('combobox'), 'workspace-cloud-123');
2407
- await user.click(screen.getByRole('button', { name: 'Save Project Setup' }));
3868
+ await user.click(screen.getByRole('button', { name: 'Connect Workspace' }));
2408
3869
  await waitFor(() => {
2409
- expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
3870
+ expect(screen.getByText('Importing "Cloud Project 123" from cloud into local.')).toBeInTheDocument();
2410
3871
  });
3872
+ expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
3873
+ expect(screen.getByRole('button', { name: 'Importing Workspace...' })).toBeInTheDocument();
2411
3874
  const workspaceProfileCall = fetchMock.mock.calls.find((call) => String(call[0]).includes('/api/taskforce/workspace-profile'));
2412
3875
  const workspaceProfileBody = JSON.parse(String(workspaceProfileCall?.[1]?.body || '{}'));
2413
3876
  expect(workspaceProfileBody.workspaceId).toBe('workspace-cloud-123');
@@ -2422,8 +3885,258 @@ describe('TaskforceCore Integration', () => {
2422
3885
  });
2423
3886
  const workspaceSyncStateBody = JSON.parse(String(workspaceSyncStateCall?.[1]?.body || '{}'));
2424
3887
  expect(workspaceSyncStateBody.stateKey).toBe('workspace-sync');
2425
- expect(workspaceSyncStateBody.patch?.cloudSyncEnabled).toBe(true);
3888
+ expect(workspaceSyncStateBody.patch?.enabled).toBe(true);
3889
+ expect(workspaceSyncStateBody.patch?.phase).toBe('attach-cloud');
2426
3890
  expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('https://cloud.example.com/api/taskforce/workspaces'))).toBe(true);
3891
+ expect(screen.queryByTitle(/Account/i)).not.toBeInTheDocument();
3892
+ });
3893
+ it('shows sign-in and register actions on signed-out cloud workspace setup and disables connect', async () => {
3894
+ const fetchMock = vi.fn((url) => {
3895
+ if (url.includes('/api/taskforce/auth/session')) {
3896
+ return Promise.resolve({
3897
+ ok: true,
3898
+ json: () => Promise.resolve({
3899
+ authenticated: false,
3900
+ runtimeMode: 'local',
3901
+ authRequiredForApi: false
3902
+ })
3903
+ });
3904
+ }
3905
+ if (url.includes('/api/taskforce/config')) {
3906
+ return Promise.resolve({
3907
+ ok: true,
3908
+ json: () => Promise.resolve({
3909
+ ...mockConfig,
3910
+ runtimeMode: 'local',
3911
+ authRequiredForApi: false,
3912
+ setupState: {
3913
+ globalSetupState: 'present',
3914
+ workspaceSetupState: 'missing',
3915
+ runtimeMode: 'local',
3916
+ workspaceId: 'default',
3917
+ mode: 'core',
3918
+ forceSetup: false,
3919
+ forceGlobalSetup: false,
3920
+ forceWorkspaceSetup: true,
3921
+ workspace: null,
3922
+ suggestedWorkspaceName: 'Suggested Project'
3923
+ },
3924
+ runtimeCapabilities: {
3925
+ runtimeMode: 'local',
3926
+ supportsCloudAuth: true,
3927
+ supportsAdminManagedSetupOverrides: false,
3928
+ supportsPackageScriptSetupOverrides: true
3929
+ }
3930
+ })
3931
+ });
3932
+ }
3933
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
3934
+ });
3935
+ vi.stubGlobal('fetch', fetchMock);
3936
+ render(_jsx(MemoryRouter, { initialEntries: ['/setup?step=workspace&source=cloud&postAuth=select-cloud-workspace'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
3937
+ await waitFor(() => {
3938
+ expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
3939
+ });
3940
+ expect(screen.getByRole('button', { name: 'Sign In' })).toBeInTheDocument();
3941
+ expect(screen.getByRole('button', { name: 'Register' })).toBeInTheDocument();
3942
+ expect(screen.getByRole('button', { name: 'Connect Workspace' })).toBeDisabled();
3943
+ });
3944
+ it('returns signed-in users to the cloud workspace picker after setup sign-in', async () => {
3945
+ const user = userEvent.setup();
3946
+ let authenticated = false;
3947
+ const fetchMock = vi.fn((url, init) => {
3948
+ if (url.includes('/api/taskforce/auth/session')) {
3949
+ return Promise.resolve({
3950
+ ok: true,
3951
+ json: () => Promise.resolve({
3952
+ authenticated,
3953
+ runtimeMode: 'local',
3954
+ authRequiredForApi: false
3955
+ })
3956
+ });
3957
+ }
3958
+ if (url.includes('/api/taskforce/config')) {
3959
+ return Promise.resolve({
3960
+ ok: true,
3961
+ json: () => Promise.resolve({
3962
+ ...mockConfig,
3963
+ runtimeMode: 'local',
3964
+ authRequiredForApi: false,
3965
+ setupState: {
3966
+ globalSetupState: 'present',
3967
+ workspaceSetupState: 'missing',
3968
+ runtimeMode: 'local',
3969
+ workspaceId: 'default',
3970
+ mode: 'core',
3971
+ forceSetup: false,
3972
+ forceGlobalSetup: false,
3973
+ forceWorkspaceSetup: true,
3974
+ workspace: null,
3975
+ suggestedWorkspaceName: 'Suggested Project'
3976
+ },
3977
+ runtimeCapabilities: {
3978
+ runtimeMode: 'local',
3979
+ supportsCloudAuth: true,
3980
+ supportsAdminManagedSetupOverrides: false,
3981
+ supportsPackageScriptSetupOverrides: true
3982
+ }
3983
+ })
3984
+ });
3985
+ }
3986
+ if (url.includes('/api/taskforce/auth/login') && String(init?.method || '').toUpperCase() === 'POST') {
3987
+ authenticated = true;
3988
+ return Promise.resolve({
3989
+ ok: true,
3990
+ json: () => Promise.resolve({
3991
+ success: true,
3992
+ workspaceSetupRequired: true
3993
+ })
3994
+ });
3995
+ }
3996
+ if (url.includes('https://cloud.example.com/api/taskforce/workspaces')) {
3997
+ return Promise.resolve({
3998
+ ok: true,
3999
+ json: () => Promise.resolve({
4000
+ success: true,
4001
+ workspaces: [
4002
+ {
4003
+ id: 'workspace-cloud-123',
4004
+ name: 'Cloud Project 123',
4005
+ description: 'Cloud desc'
4006
+ }
4007
+ ]
4008
+ })
4009
+ });
4010
+ }
4011
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
4012
+ });
4013
+ vi.stubGlobal('fetch', fetchMock);
4014
+ render(_jsx(MemoryRouter, { initialEntries: ['/setup?step=workspace&source=cloud&postAuth=select-cloud-workspace'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
4015
+ await waitFor(() => {
4016
+ expect(screen.getByRole('button', { name: 'Sign In' })).toBeInTheDocument();
4017
+ });
4018
+ await user.click(screen.getByRole('button', { name: 'Sign In' }));
4019
+ await waitFor(() => {
4020
+ expect(screen.getByTestId('auth-email-input')).toBeInTheDocument();
4021
+ });
4022
+ await user.type(screen.getByTestId('auth-email-input'), 'agent@example.com');
4023
+ await user.type(screen.getByTestId('auth-password-input'), 'password123');
4024
+ await user.click(screen.getByTestId('auth-submit-button'));
4025
+ await waitFor(() => {
4026
+ expect(screen.getByRole('option', { name: /Cloud Project 123/ })).toBeInTheDocument();
4027
+ });
4028
+ await waitFor(() => {
4029
+ expect(screen.queryByRole('button', { name: 'Register' })).not.toBeInTheDocument();
4030
+ expect(screen.getByRole('button', { name: 'Connect Workspace' })).toBeEnabled();
4031
+ });
4032
+ });
4033
+ it('loads workspace setup after cloud sign-in when the user has no workspaces', async () => {
4034
+ const user = userEvent.setup();
4035
+ let authenticated = false;
4036
+ const fetchMock = vi.fn((input, init) => {
4037
+ const url = String(input);
4038
+ if (url.includes('/api/taskforce/auth/runtime-config')) {
4039
+ return jsonResponse({
4040
+ success: true,
4041
+ config: {
4042
+ baseUrl: 'https://app.taskforce.example',
4043
+ apiBaseUrl: 'https://app.taskforce.example/api',
4044
+ cloudAuthBaseUrl: 'https://app.taskforce.example',
4045
+ runtimeMode: 'cloud',
4046
+ authSource: 'cloud',
4047
+ workspaceMode: 'multi-cloud',
4048
+ workspaceSwitchingEnabled: true,
4049
+ cloudAuthViaLocalProxy: false
4050
+ }
4051
+ });
4052
+ }
4053
+ if (url.includes('/api/taskforce/auth/session')) {
4054
+ return jsonResponse({
4055
+ authenticated,
4056
+ runtimeMode: 'cloud',
4057
+ authRequiredForApi: true,
4058
+ userId: authenticated ? 'user-cloud-setup' : 'anonymous',
4059
+ workspaceId: 'default'
4060
+ });
4061
+ }
4062
+ if (url.includes('/api/taskforce/auth/login')) {
4063
+ authenticated = true;
4064
+ return jsonResponse({ success: true });
4065
+ }
4066
+ if (url.includes('/api/taskforce/workspaces')) {
4067
+ return jsonResponse({
4068
+ success: true,
4069
+ workspaces: [],
4070
+ currentWorkspaceId: 'default'
4071
+ });
4072
+ }
4073
+ if (url.includes('/api/taskforce/account/access-status')) {
4074
+ return jsonResponse({
4075
+ gate: 'ok',
4076
+ hasEntitlement: true,
4077
+ canAccessApp: true,
4078
+ canAccessPlans: true,
4079
+ message: null
4080
+ });
4081
+ }
4082
+ if (url.includes('/api/taskforce/config')) {
4083
+ return jsonResponse({
4084
+ ...mockConfig,
4085
+ runtimeMode: 'cloud',
4086
+ authRequiredForApi: true,
4087
+ userId: authenticated ? 'user-cloud-setup' : 'anonymous',
4088
+ setupState: {
4089
+ globalSetupState: 'present',
4090
+ workspaceSetupState: authenticated ? 'missing' : 'present',
4091
+ runtimeMode: 'cloud',
4092
+ workspaceId: 'default',
4093
+ mode: 'core',
4094
+ forceSetup: false,
4095
+ forceGlobalSetup: false,
4096
+ forceWorkspaceSetup: false,
4097
+ workspace: null,
4098
+ suggestedWorkspaceName: ''
4099
+ },
4100
+ runtimeCapabilities: {
4101
+ runtimeMode: 'cloud',
4102
+ supportsCloudAuth: true,
4103
+ supportsAdminManagedSetupOverrides: true,
4104
+ supportsPackageScriptSetupOverrides: false
4105
+ }
4106
+ });
4107
+ }
4108
+ if (url.includes('/api/taskforce/ui-state?key=app')) {
4109
+ return jsonResponse({ success: true, state: {} });
4110
+ }
4111
+ if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
4112
+ return jsonResponse({
4113
+ success: true,
4114
+ state: {
4115
+ version: 2,
4116
+ enabled: false,
4117
+ phase: 'idle',
4118
+ pullCursor: null,
4119
+ lastPullAt: null,
4120
+ lastPushAt: null,
4121
+ lastSyncedAt: null
4122
+ }
4123
+ });
4124
+ }
4125
+ if (url.includes('/api/taskforce/workflow-templates')) {
4126
+ return jsonResponse({ templates: [] });
4127
+ }
4128
+ return jsonResponse({});
4129
+ });
4130
+ vi.stubGlobal('fetch', fetchMock);
4131
+ render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=login&next=%2F'], children: _jsx(TaskforceCore, {}) }));
4132
+ await user.type(await screen.findByPlaceholderText('Email'), 'setup@example.com');
4133
+ await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
4134
+ await user.click(screen.getByRole('button', { name: 'Sign In' }));
4135
+ await waitFor(() => {
4136
+ expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
4137
+ });
4138
+ expect(screen.queryByText('Resolving workspace access...')).not.toBeInTheDocument();
4139
+ expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('/api/taskforce/config'))).toBe(true);
2427
4140
  });
2428
4141
  it('enables workspace cloud sync from local setup when signed in and checkbox is selected', async () => {
2429
4142
  const user = userEvent.setup();
@@ -2495,7 +4208,7 @@ describe('TaskforceCore Integration', () => {
2495
4208
  vi.stubGlobal('fetch', fetchMock);
2496
4209
  render(_jsx(MemoryRouter, { initialEntries: ['/setup?step=workspace'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
2497
4210
  await waitFor(() => {
2498
- expect(screen.getByText('Workspace Setup Required')).toBeInTheDocument();
4211
+ expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
2499
4212
  });
2500
4213
  await waitFor(() => {
2501
4214
  expect(screen.getByRole('checkbox', { name: /sync to cloud after setup/i })).toBeInTheDocument();
@@ -2933,7 +4646,7 @@ describe('TaskforceCore Integration', () => {
2933
4646
  vi.stubGlobal('fetch', fetchMock);
2934
4647
  const initialRender = render(_jsx(MemoryRouter, { initialEntries: ['/setup?step=workspace'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
2935
4648
  await waitFor(() => {
2936
- expect(screen.getByText('Workspace Setup Required')).toBeInTheDocument();
4649
+ expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
2937
4650
  });
2938
4651
  await waitFor(() => {
2939
4652
  expect(screen.getByRole('checkbox', { name: /sync to cloud after setup/i })).toBeInTheDocument();
@@ -2949,7 +4662,7 @@ describe('TaskforceCore Integration', () => {
2949
4662
  initialRender.unmount();
2950
4663
  render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
2951
4664
  await waitFor(() => {
2952
- expect(screen.queryByText('Workspace Setup Required')).not.toBeInTheDocument();
4665
+ expect(screen.queryByText('Configure your new workspace.')).not.toBeInTheDocument();
2953
4666
  expect(screen.getByRole('button', { name: /add task/i })).toBeInTheDocument();
2954
4667
  });
2955
4668
  expect(cloudWorkspaceCreated).toBe(false);
@@ -2961,6 +4674,13 @@ describe('TaskforceCore Integration', () => {
2961
4674
  expect(standaloneLayoutSource).toContain('openSharedLoginRoute(\'login\')');
2962
4675
  expect(standaloneLayoutSource).toContain('navigate(`/login?mode=${mode}&next=${encodeURIComponent(target)}`)');
2963
4676
  });
4677
+ it('keeps the account badge neutral until the authenticated identity is fully resolved', () => {
4678
+ const standaloneLayoutPath = path.join(process.cwd(), 'src/components/views/StandaloneLayout.tsx');
4679
+ const standaloneLayoutSource = fs.readFileSync(standaloneLayoutPath, 'utf8');
4680
+ expect(standaloneLayoutSource).toContain("const shouldShowResolvedAccountInitial = isAuthenticated && authSessionResolved && accountBadgeLabel.length > 0;");
4681
+ expect(standaloneLayoutSource).toContain("const shouldShowResolvedAccountAvatar = isAuthenticated && authSessionResolved && accountAvatarUrl.length > 0;");
4682
+ expect(standaloneLayoutSource).toContain('<User size={14} className={accountStyles.avatarIcon} />');
4683
+ });
2964
4684
  it('keeps workspace management cloud-only while allowing cloud-backed team management from local runtime', () => {
2965
4685
  const standaloneLayoutPath = path.join(process.cwd(), 'src/components/views/StandaloneLayout.tsx');
2966
4686
  const standaloneLayoutSource = fs.readFileSync(standaloneLayoutPath, 'utf8');
@@ -2968,17 +4688,25 @@ describe('TaskforceCore Integration', () => {
2968
4688
  const useTaskforceSource = fs.readFileSync(useTaskforcePath, 'utf8');
2969
4689
  expect(standaloneLayoutSource).toContain("const canManageCloudWorkspaces = runtimeMode === 'cloud' && workspaceSwitchingEnabled && isAuthenticated;");
2970
4690
  expect(standaloneLayoutSource).toContain("const canAccessCloudTeamManagement = isAuthenticated && (runtimeMode === 'cloud' || cloudAuthConfigured);");
2971
- expect(standaloneLayoutSource).toContain("const teamManagementWorkspaceId = String(authWorkspaceId || currentWorkspaceId || '').trim();");
2972
- expect(standaloneLayoutSource).toContain("fetchTaskforceApi('/api/taskforce/account/team-management-access'");
4691
+ expect(standaloneLayoutSource).toContain("const teamManagementWorkspaceId = String(accountProfileSummary?.workspaceId || authWorkspaceId || currentWorkspaceId || '').trim();");
4692
+ expect(standaloneLayoutSource).toContain("const accountProfileSummaryUrl = resolveCloudAuthUrl(ACCOUNT_PROFILE_SUMMARY_PATH);");
2973
4693
  expect(standaloneLayoutSource).toContain('computeCanOpenTeamManagement(canAccessCloudTeamManagement, teamWorkspaceRole, teamPlanMode)');
2974
4694
  expect(useTaskforceSource).toContain("const [authWorkspaceId, setAuthWorkspaceId] = useState<string>('');");
2975
4695
  });
2976
4696
  it('routes local plans and billing UI through the connected cloud billing base', () => {
2977
4697
  const standaloneLayoutPath = path.join(process.cwd(), 'src/components/views/StandaloneLayout.tsx');
2978
4698
  const standaloneLayoutSource = fs.readFileSync(standaloneLayoutPath, 'utf8');
2979
- expect(standaloneLayoutSource).toContain("const billingApiBaseUrl = useMemo(() => {");
2980
- expect(standaloneLayoutSource).toContain("loadBillingStatusCached(billingStatusUrl)");
2981
- expect(standaloneLayoutSource).toContain("apiBaseUrl={billingApiBaseUrl}");
4699
+ const plansPagePath = path.join(process.cwd(), 'src/components/views/PlansPage.tsx');
4700
+ const plansPageSource = fs.readFileSync(plansPagePath, 'utf8');
4701
+ const useTaskforcePath = path.join(process.cwd(), 'src/hooks/useTaskforce.ts');
4702
+ const useTaskforceSource = fs.readFileSync(useTaskforcePath, 'utf8');
4703
+ expect(standaloneLayoutSource).toContain("const accountProfileSummaryUrl = resolveCloudAuthUrl(ACCOUNT_PROFILE_SUMMARY_PATH);");
4704
+ expect(standaloneLayoutSource).toContain("resolveCloudAuthUrl('/api/taskforce/billing/portal-session')");
4705
+ expect(standaloneLayoutSource).toContain("resolveCloudAuthUrl={resolveCloudAuthUrl}");
4706
+ expect(plansPageSource).toContain('const buildCloudBillingUrl = useCallback((path: string): string => {');
4707
+ expect(plansPageSource).toContain("const accountProfileSummaryUrl = buildCloudBillingUrl(ACCOUNT_PROFILE_SUMMARY_PATH);");
4708
+ expect(useTaskforceSource).toContain("|| path.startsWith('/api/taskforce/account/')");
4709
+ expect(useTaskforceSource).toContain("|| path.startsWith('/api/taskforce/billing/')");
2982
4710
  });
2983
4711
  it('uses shared /login flow for local sign in instead of a duplicate modal', async () => {
2984
4712
  const standaloneLayoutPath = path.join(process.cwd(), 'src/components/views/StandaloneLayout.tsx');
@@ -3175,18 +4903,31 @@ describe('TaskforceCore Integration', () => {
3175
4903
  vi.stubGlobal('fetch', fetchMock);
3176
4904
  render(_jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }));
3177
4905
  await waitFor(() => {
3178
- expect(screen.getByTitle('Open tasks matching current filters')).toHaveTextContent('1/1');
4906
+ expect(screen.getByTitle(/Open tasks/)).toHaveTextContent(/1/);
3179
4907
  expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
3180
4908
  });
3181
4909
  await user.click(screen.getByTitle(/Account/i));
3182
4910
  await user.click(await screen.findByRole('menuitem', { name: 'Sign Out' }));
3183
4911
  await waitFor(() => {
3184
4912
  expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('https://cloud.example.com/api/taskforce/auth/logout'))).toBe(true);
3185
- expect(screen.getByTitle('Open tasks matching current filters')).toHaveTextContent('1/1');
4913
+ expect(screen.getByTitle(/Open tasks/)).toHaveTextContent(/1/);
3186
4914
  });
3187
4915
  await user.click(screen.getByTitle(/Account/i));
3188
4916
  expect(await screen.findByText('Sign In / Register')).toBeInTheDocument();
3189
4917
  });
4918
+ it('uses an explicit cloud sign-out redirect and broader cloud logout reset path', () => {
4919
+ const standaloneLayoutSource = fs.readFileSync(path.join(__dirname, 'components/views/StandaloneLayout.tsx'), 'utf8');
4920
+ const useTaskforceSource = fs.readFileSync(path.join(__dirname, 'hooks/useTaskforce.ts'), 'utf8');
4921
+ expect(standaloneLayoutSource).toContain("await logout();");
4922
+ expect(standaloneLayoutSource).toContain("if (runtimeMode === 'cloud') {");
4923
+ expect(standaloneLayoutSource).toContain("navigate('/login', { replace: true });");
4924
+ expect(useTaskforceSource).toContain('const resetCloudLogoutClientState = useCallback(() => {');
4925
+ expect(useTaskforceSource).toContain('setArchivedTasks([]);');
4926
+ expect(useTaskforceSource).toContain('setDeletedTasks([]);');
4927
+ expect(useTaskforceSource).toContain('setAssigneeOptions(createDefaultAssigneeOptions());');
4928
+ expect(useTaskforceSource).toContain('setUiStateReady(false);');
4929
+ expect(useTaskforceSource).toContain('resetCloudLogoutClientState();');
4930
+ });
3190
4931
  it('does not pull from cloud when workspace sync toggle is disabled', async () => {
3191
4932
  const fetchMock = vi.fn((url) => {
3192
4933
  if (url.includes('/api/taskforce/auth/session')) {
@@ -4055,9 +5796,10 @@ describe('TaskforceCore Integration', () => {
4055
5796
  it('keeps local workspace context authoritative over cloud auth session workspace id', async () => {
4056
5797
  const useTaskforcePath = path.join(process.cwd(), 'src/hooks/useTaskforce.ts');
4057
5798
  const useTaskforceSource = fs.readFileSync(useTaskforcePath, 'utf8');
4058
- expect(useTaskforceSource).toContain("const keepLocalWorkspaceContext = runtimeMode === 'local' || authOnlyCloudMode;");
4059
- expect(useTaskforceSource).toContain('Local runtime resolves workspace identity from local DB via /api/taskforce/config.');
4060
- expect(useTaskforceSource).toContain('Do not override it from cloud auth session payloads.');
5799
+ expect(useTaskforceSource).toContain("const authOnlyCloudMode = !isCloudHost && Boolean(normalizedCloudAuthBaseUrl) && !normalizedApiBaseUrl;");
5800
+ expect(useTaskforceSource).toContain('Local runtime workspace identity must be sourced from local DB (/api/taskforce/config),');
5801
+ expect(useTaskforceSource).toContain('not from persisted browser context or cloud session payloads.');
5802
+ expect(useTaskforceSource).toContain("if (initialRuntimeMode === 'local') return 'default';");
4061
5803
  });
4062
5804
  it.skip('blocks local cloud-sync pull/apply when workspaceId remains default', async () => {
4063
5805
  const fetchMock = vi.fn((url, init) => {