@taskforcehq/taskforce 0.3.303 → 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 (165) hide show
  1. package/dist/Taskforce.module.css +102 -5
  2. package/dist/TaskforceCore.js +514 -212
  3. package/dist/TaskforceCore.test.js +1172 -133
  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/TaskStatusActions.d.ts +6 -1
  21. package/dist/components/task/TaskStatusActions.js +8 -3
  22. package/dist/components/task/TaskStatusActions.test.js +9 -0
  23. package/dist/components/task/taskKanbanDropRules.d.ts +58 -0
  24. package/dist/components/task/taskKanbanDropRules.js +187 -0
  25. package/dist/components/task/taskKanbanDropRules.test.d.ts +1 -0
  26. package/dist/components/task/taskKanbanDropRules.test.js +114 -0
  27. package/dist/components/ui/Modal.d.ts +1 -1
  28. package/dist/components/ui/Modal.js +1 -0
  29. package/dist/components/views/AppShellHeader.js +5 -1
  30. package/dist/components/views/AppShellHeader.test.js +4 -0
  31. package/dist/components/views/PlanComparisonPage.d.ts +3 -2
  32. package/dist/components/views/PlanComparisonPage.js +4 -2
  33. package/dist/components/views/PlanComparisonPage.test.js +4 -0
  34. package/dist/components/views/PlansPage.d.ts +5 -1
  35. package/dist/components/views/PlansPage.js +39 -12
  36. package/dist/components/views/PlansPage.test.js +59 -2
  37. package/dist/components/views/StandaloneLayout.js +113 -38
  38. package/dist/components/views/WidgetView.js +11 -2
  39. package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
  40. package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
  41. package/dist/core/PlanEntitlementService.d.ts +3 -1
  42. package/dist/core/PlanEntitlementService.js +61 -11
  43. package/dist/core/SignupMonetizationSettings.test.js +37 -2
  44. package/dist/core/TaskActivityService.d.ts +4 -1
  45. package/dist/core/TaskActivityService.js +45 -0
  46. package/dist/core/TaskAuditTimeline.test.js +52 -1
  47. package/dist/core/Taskforce.d.ts +15 -11
  48. package/dist/core/Taskforce.js +43 -2
  49. package/dist/core/WorkspacePlanMode.test.js +65 -0
  50. package/dist/documentReviews/service.d.ts +1 -1
  51. package/dist/documentReviews/types.d.ts +1 -1
  52. package/dist/hooks/auth/useTaskforceAuthBootstrap.d.ts +48 -0
  53. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +219 -0
  54. package/dist/hooks/sync/auth.d.ts +37 -0
  55. package/dist/hooks/sync/auth.js +61 -0
  56. package/dist/hooks/sync/bootstrap.d.ts +34 -0
  57. package/dist/hooks/sync/bootstrap.js +52 -0
  58. package/dist/hooks/sync/orchestratorShared.d.ts +36 -0
  59. package/dist/hooks/sync/orchestratorShared.js +209 -0
  60. package/dist/hooks/sync/orchestratorShared.test.d.ts +1 -0
  61. package/dist/hooks/sync/orchestratorShared.test.js +49 -0
  62. package/dist/hooks/sync/realtime.d.ts +25 -0
  63. package/dist/hooks/sync/realtime.js +60 -0
  64. package/dist/hooks/sync/recovery.d.ts +69 -0
  65. package/dist/hooks/sync/recovery.js +118 -0
  66. package/dist/hooks/sync/transfers.d.ts +122 -0
  67. package/dist/hooks/sync/transfers.js +431 -0
  68. package/dist/hooks/sync/useDataVersionRefresh.d.ts +16 -0
  69. package/dist/hooks/sync/useDataVersionRefresh.js +73 -0
  70. package/dist/hooks/ui/useResourceExport.d.ts +3 -1
  71. package/dist/hooks/ui/useResourceExport.js +5 -2
  72. package/dist/hooks/ui/useWorkflowTemplates.js +11 -6
  73. package/dist/hooks/useRealtimeSync.js +2 -1
  74. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +8 -0
  75. package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +1 -0
  76. package/dist/hooks/useSyncOrchestrator.context-assets.test.js +228 -0
  77. package/dist/hooks/useSyncOrchestrator.d.ts +5 -4
  78. package/dist/hooks/useSyncOrchestrator.js +314 -873
  79. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +568 -3
  80. package/dist/hooks/useTaskMutations.js +45 -6
  81. package/dist/hooks/useTaskMutations.test.js +147 -0
  82. package/dist/hooks/useTaskforce.d.ts +8 -2
  83. package/dist/hooks/useTaskforce.js +285 -812
  84. package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
  85. package/dist/hooks/useWorkspaceSyncController.js +22 -10
  86. package/dist/hooks/workspace/useTaskforceApiRouting.test.js +17 -0
  87. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +166 -0
  88. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +434 -0
  89. package/dist/hooks/workspace/workspaceLocalState.d.ts +19 -0
  90. package/dist/hooks/workspace/workspaceLocalState.js +100 -0
  91. package/dist/hooks/workspace/workspaceLocalState.test.d.ts +1 -0
  92. package/dist/hooks/workspace/workspaceLocalState.test.js +59 -0
  93. package/dist/localization/locales/en-US.js +2 -2
  94. package/dist/mcp/canonicalAssetHelpers.d.ts +70 -0
  95. package/dist/mcp/canonicalAssetHelpers.js +259 -0
  96. package/dist/mcp/clientIntegrations.d.ts +12 -1
  97. package/dist/mcp/clientIntegrations.js +161 -6
  98. package/dist/mcp/clientIntegrations.test.js +76 -4
  99. package/dist/mcp/documentHelpers.d.ts +100 -0
  100. package/dist/mcp/documentHelpers.js +161 -0
  101. package/dist/mcp/runtime.js +174 -476
  102. package/dist/mcp/runtime.test.js +81 -8
  103. package/dist/mcp/taskAttachmentHelpers.d.ts +41 -0
  104. package/dist/mcp/taskAttachmentHelpers.js +60 -0
  105. package/dist/mcp/taskAttachmentTypes.d.ts +37 -0
  106. package/dist/mcp/taskAttachmentTypes.js +1 -0
  107. package/dist/plugins/vite.js +1 -1
  108. package/dist/runtime/appGateDefinitions.d.ts +1 -1
  109. package/dist/runtime/appGateDefinitions.js +1 -1
  110. package/dist/server/index.d.ts +1 -0
  111. package/dist/server/index.js +46 -3
  112. package/dist/server/index.test.js +12 -1
  113. package/dist/server/routes/admin.js +13 -19
  114. package/dist/server/routes/auth.js +37 -2
  115. package/dist/server/routes/billing.js +18 -67
  116. package/dist/server/routes/billing.test.js +43 -0
  117. package/dist/server/routes/shared.d.ts +30 -0
  118. package/dist/server/routes/shared.js +26 -0
  119. package/dist/server/routes/sync.integration.test.js +20 -0
  120. package/dist/server/routes/sync.js +110 -237
  121. package/dist/server/routes/tasks.js +49 -7
  122. package/dist/server/routes.js +8 -19
  123. package/dist/server/routes.test.js +243 -5
  124. package/dist/shared/taskActor.d.ts +10 -0
  125. package/dist/shared/taskActor.js +1 -0
  126. package/dist/sync/collaborationSyncPayload.js +4 -0
  127. package/dist/sync/collaborationSyncPayload.test.js +8 -0
  128. package/dist/sync/collaborationSyncState.d.ts +3 -1
  129. package/dist/sync/collaborationSyncState.js +19 -0
  130. package/dist/sync/contentSyncState.js +12 -10
  131. package/dist/sync/syncApplyHandlers.d.ts +87 -1
  132. package/dist/sync/syncApplyHandlers.js +370 -8
  133. package/dist/sync/syncApplyHandlers.test.d.ts +1 -0
  134. package/dist/sync/syncApplyHandlers.test.js +377 -0
  135. package/dist/sync/syncService.d.ts +8 -1
  136. package/dist/sync/syncService.js +36 -2
  137. package/dist/sync/syncService.test.js +48 -0
  138. package/dist/sync/workspacePullFeed.d.ts +1 -0
  139. package/dist/sync/workspacePullFeed.js +66 -59
  140. package/dist/sync/workspacePullFeed.test.js +72 -4
  141. package/dist/sync/workspaceSyncModel.d.ts +22 -1
  142. package/dist/sync/workspaceSyncModel.js +65 -0
  143. package/dist/sync/workspaceSyncModel.test.js +119 -0
  144. package/dist/types.d.ts +2 -10
  145. package/dist/ui/assets/AgentsModule-BLWVbuKP.js +1 -0
  146. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-C_TmXZwA.js → AnnotatedAttachmentWorkspace-BlS-cqnl.js} +1 -1
  147. package/dist/ui/assets/{DocumentWorkspace-C3GyzrWm.js → DocumentWorkspace-BH_6DF6x.js} +1 -1
  148. package/dist/ui/assets/{InitiativesModule-4-Rh2K2n.js → InitiativesModule-BjR6iBf9.js} +1 -1
  149. package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
  150. package/dist/ui/assets/PlansPage-CSDQ4sLa.js +1 -0
  151. package/dist/ui/assets/TaskSettings-CEDy34Jo.js +9 -0
  152. package/dist/ui/assets/{WorkflowsModule-CGk9s3NJ.js → WorkflowsModule-DnFqdUME.js} +1 -1
  153. package/dist/ui/assets/index-DXUdtH1B.js +6 -0
  154. package/dist/ui/assets/index-Ii0B0rTO.css +1 -0
  155. package/dist/ui/assets/{vendor-icons-pWocEipT.js → vendor-icons-I7ZwG1z_.js} +1 -1
  156. package/dist/ui/index.html +4 -4
  157. package/dist/utils/taskActivity.js +4 -0
  158. package/dist/utils/taskActivity.test.js +25 -1
  159. package/package.json +3 -1
  160. package/scripts/playwright-auth.sh +4 -0
  161. package/dist/ui/assets/AgentsModule-CpsTmrIW.js +0 -1
  162. package/dist/ui/assets/PlansPage-BP7AYOqr.js +0 -1
  163. package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +0 -8
  164. package/dist/ui/assets/index-CLIMgR6g.js +0 -6
  165. package/dist/ui/assets/index-DneETxcu.css +0 -1
@@ -1897,16 +1897,68 @@ describe('TaskforceCore Integration', () => {
1897
1897
  const { container } = render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
1898
1898
  await waitFor(() => {
1899
1899
  expect(screen.getByText('Create your Taskforce account.')).toBeInTheDocument();
1900
+ expect(screen.getByLabelText('Email')).toBeInTheDocument();
1901
+ expect(screen.getByLabelText('Display name')).toBeInTheDocument();
1900
1902
  expect(screen.getByPlaceholderText('Email')).toBeInTheDocument();
1901
1903
  expect(screen.getByPlaceholderText('Display name')).toBeInTheDocument();
1902
1904
  expect(screen.getByRole('button', { name: 'Create Account' })).toBeInTheDocument();
1903
1905
  expect(screen.getByRole('button', { name: 'Sign In' })).toBeInTheDocument();
1904
1906
  expect(screen.queryByRole('button', { name: 'Register' })).not.toBeInTheDocument();
1905
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');
1906
1910
  });
1907
1911
  expect(container.querySelector('[aria-pressed]')).not.toBeInTheDocument();
1908
1912
  });
1909
- it('does not switch to verify mode after registration when verification is not required', async () => {
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 () => {
1910
1962
  const user = userEvent.setup();
1911
1963
  const fetchMock = vi.fn((input) => {
1912
1964
  const url = String(input);
@@ -1940,6 +1992,126 @@ describe('TaskforceCore Integration', () => {
1940
1992
  })
1941
1993
  });
1942
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
+ }
1943
2115
  if (url.includes('/api/taskforce/public/pricing')) {
1944
2116
  return Promise.resolve({
1945
2117
  ok: true,
@@ -1949,12 +2121,14 @@ describe('TaskforceCore Integration', () => {
1949
2121
  });
1950
2122
  }
1951
2123
  if (url.includes('/api/taskforce/auth/register')) {
2124
+ authenticated = true;
1952
2125
  return Promise.resolve({
1953
2126
  ok: true,
1954
2127
  json: () => Promise.resolve({
1955
2128
  success: true,
1956
- workspaceSetupRequired: false,
1957
- verificationRequired: false
2129
+ workspaceSetupRequired: true,
2130
+ verificationRequired: false,
2131
+ commercialState: 'active'
1958
2132
  })
1959
2133
  });
1960
2134
  }
@@ -1967,7 +2141,7 @@ describe('TaskforceCore Integration', () => {
1967
2141
  await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
1968
2142
  await user.click(screen.getByRole('button', { name: 'Create Account' }));
1969
2143
  await waitFor(() => {
1970
- expect(screen.getByText('Subscription Plans')).toBeInTheDocument();
2144
+ expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
1971
2145
  });
1972
2146
  expect(screen.queryByPlaceholderText('Verification token')).not.toBeInTheDocument();
1973
2147
  expect(screen.queryByRole('button', { name: 'Create Account' })).not.toBeInTheDocument();
@@ -2118,19 +2292,453 @@ describe('TaskforceCore Integration', () => {
2118
2292
  }
2119
2293
  return jsonResponse({});
2120
2294
  });
2121
- vi.stubGlobal('fetch', fetchMock);
2122
- render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
2123
- await user.type(await screen.findByPlaceholderText('Email'), 'pending@example.com');
2124
- await user.type(screen.getByPlaceholderText('Display name'), 'Pending User');
2125
- await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
2126
- await user.click(screen.getByRole('button', { name: 'Create Account' }));
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
+ });
2127
2736
  await waitFor(() => {
2128
- expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
2129
- expect(screen.getByText('Choose a plan to continue.')).toBeInTheDocument();
2737
+ expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
2130
2738
  });
2131
- expect(fetchMock.mock.calls.some(([url]) => String(url).includes('/api/taskforce/billing/checkout-session'))).toBe(false);
2739
+ expect(screen.queryByText('Sign in with your account credentials.')).not.toBeInTheDocument();
2132
2740
  });
2133
- it('routes successful registration into checkout when onboarding leaves the account checkout-pending', async () => {
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 () => {
2134
2742
  const user = userEvent.setup();
2135
2743
  let authenticated = false;
2136
2744
  const fetchMock = vi.fn((input) => {
@@ -2154,7 +2762,7 @@ describe('TaskforceCore Integration', () => {
2154
2762
  authenticated,
2155
2763
  runtimeMode: 'cloud',
2156
2764
  authRequiredForApi: true,
2157
- userId: authenticated ? 'user-1' : null,
2765
+ userId: authenticated ? 'user-free-2' : null,
2158
2766
  workspaceId: authenticated ? 'workspace-1' : null,
2159
2767
  betaAccess: true
2160
2768
  });
@@ -2165,6 +2773,7 @@ describe('TaskforceCore Integration', () => {
2165
2773
  runtimeMode: 'cloud',
2166
2774
  authRequiredForApi: true,
2167
2775
  workspaceSwitchingEnabled: true,
2776
+ workspaceId: 'workspace-1',
2168
2777
  setupState: {
2169
2778
  globalSetupState: 'present',
2170
2779
  workspaceSetupState: 'present',
@@ -2194,59 +2803,15 @@ describe('TaskforceCore Integration', () => {
2194
2803
  }
2195
2804
  if (url.includes('/api/taskforce/account/access-status')) {
2196
2805
  return jsonResponse({
2197
- gate: 'checkout_pending',
2806
+ gate: 'ok',
2198
2807
  hasEntitlement: true,
2199
- canAccessApp: false,
2808
+ canAccessApp: true,
2200
2809
  canAccessPlans: true,
2201
- message: 'Complete checkout to continue.'
2202
- });
2203
- }
2204
- if (url.includes('/api/taskforce/account/profile-summary')) {
2205
- return jsonResponse({
2206
- planId: 'pro',
2207
- planName: 'Pro',
2208
- planVersionId: 'pro-v1',
2209
- entitlementState: 'checkout_pending',
2210
- gate: 'checkout_pending',
2211
- message: 'Complete checkout to continue.',
2212
- workspaceId: 'workspace-1'
2810
+ message: null
2213
2811
  });
2214
2812
  }
2215
2813
  if (url.includes('/api/taskforce/public/pricing')) {
2216
- return jsonResponse({
2217
- plans: [{
2218
- planId: 'pro',
2219
- displayName: 'Pro',
2220
- status: 'active',
2221
- defaultVersion: {
2222
- planVersionId: 'pro-v1',
2223
- versionNumber: 1,
2224
- seatLimit: null,
2225
- metadata: {}
2226
- },
2227
- features: [],
2228
- pricing: {
2229
- month: {
2230
- planVersionId: 'pro-v1',
2231
- interval: 'month',
2232
- pricingType: 'stripe',
2233
- stripePriceId: 'price_pro_month',
2234
- displayAmount: '$19.00',
2235
- unitAmount: 1900,
2236
- currency: 'usd'
2237
- },
2238
- year: {
2239
- planVersionId: 'pro-v1',
2240
- interval: 'year',
2241
- pricingType: 'stripe',
2242
- stripePriceId: 'price_pro_year',
2243
- displayAmount: '$190.00',
2244
- unitAmount: 19000,
2245
- currency: 'usd'
2246
- }
2247
- }
2248
- }]
2249
- });
2814
+ return jsonResponse({ plans: [] });
2250
2815
  }
2251
2816
  if (url.includes('/api/taskforce/auth/register')) {
2252
2817
  authenticated = true;
@@ -2254,57 +2819,43 @@ describe('TaskforceCore Integration', () => {
2254
2819
  success: true,
2255
2820
  workspaceSetupRequired: false,
2256
2821
  verificationRequired: false,
2257
- checkoutPending: true,
2258
- commercialState: 'checkout_pending'
2822
+ commercialState: 'active'
2259
2823
  });
2260
2824
  }
2261
- if (url.includes('/api/taskforce/billing/checkout-session')) {
2262
- return jsonResponse({
2263
- checkoutState: 'updated',
2264
- message: 'Checkout started.'
2265
- });
2266
- }
2267
- if (url.includes('/api/taskforce/categories')) {
2825
+ if (url.includes('/api/taskforce/categories'))
2268
2826
  return jsonResponse(mockCategories);
2269
- }
2270
- if (url.includes('/api/taskforce/tasks')) {
2827
+ if (url.includes('/api/taskforce/tasks'))
2271
2828
  return jsonResponse({ tasks: mockTasks });
2272
- }
2273
- if (url.includes('/api/taskforce/archive')) {
2829
+ if (url.includes('/api/taskforce/archive'))
2274
2830
  return jsonResponse({ archived: [] });
2275
- }
2276
- if (url.includes('/api/taskforce/data-version')) {
2831
+ if (url.includes('/api/taskforce/data-version'))
2277
2832
  return jsonResponse({ dataVersion: 1 });
2278
- }
2279
- if (url.includes('/api/taskforce/types')) {
2833
+ if (url.includes('/api/taskforce/types'))
2280
2834
  return jsonResponse({ types: [] });
2281
- }
2282
- if (url.includes('/api/taskforce/priorities')) {
2835
+ if (url.includes('/api/taskforce/priorities'))
2283
2836
  return jsonResponse({ priorities: [] });
2284
- }
2285
- if (url.includes('/api/taskforce/approaches')) {
2837
+ if (url.includes('/api/taskforce/approaches'))
2286
2838
  return jsonResponse({ approaches: [] });
2287
- }
2288
- if (url.includes('/api/taskforce/specialists')) {
2839
+ if (url.includes('/api/taskforce/specialists'))
2289
2840
  return jsonResponse({ specialists: [] });
2290
- }
2291
- if (url.includes('/api/taskforce/workflow-templates')) {
2841
+ if (url.includes('/api/taskforce/workflow-templates'))
2292
2842
  return jsonResponse({ templates: [] });
2293
- }
2294
2843
  return jsonResponse({});
2295
2844
  });
2296
2845
  vi.stubGlobal('fetch', fetchMock);
2297
- render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&planId=pro&planVersionId=pro-v1&interval=year&next=%2F'], children: _jsx(TaskforceCore, {}) }));
2298
- await user.type(await screen.findByPlaceholderText('Email'), 'checkout@example.com');
2299
- await user.type(screen.getByPlaceholderText('Display name'), 'Checkout User');
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');
2300
2849
  await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
2301
2850
  await user.click(screen.getByRole('button', { name: 'Create Account' }));
2302
- await waitFor(() => {
2303
- expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
2851
+ await act(async () => {
2852
+ await Promise.resolve();
2304
2853
  });
2305
2854
  await waitFor(() => {
2306
- expect(fetchMock.mock.calls.some(([url]) => String(url).includes('/api/taskforce/billing/checkout-session'))).toBe(true);
2855
+ expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
2307
2856
  });
2857
+ expect(screen.queryByText('Configure your new workspace.')).not.toBeInTheDocument();
2858
+ expect(screen.queryByRole('heading', { name: 'Subscription Plans' })).not.toBeInTheDocument();
2308
2859
  });
2309
2860
  it('shows verification before workspace setup when registration still requires email verification', async () => {
2310
2861
  const user = userEvent.setup();
@@ -2765,31 +3316,206 @@ describe('TaskforceCore Integration', () => {
2765
3316
  return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
2766
3317
  });
2767
3318
  vi.stubGlobal('fetch', fetchMock);
2768
- render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, {}) }));
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: [] });
3498
+ }
3499
+ return jsonResponse({});
3500
+ });
3501
+ vi.stubGlobal('fetch', fetchMock);
3502
+ render(_jsx(MemoryRouter, { initialEntries: ['/setup?step=workspace'], children: _jsx(TaskforceCore, {}) }));
2769
3503
  await waitFor(() => {
2770
- expect(screen.getByText('Workspace Setup Required')).toBeInTheDocument();
3504
+ expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
2771
3505
  });
2772
- const nameInput = screen.getByPlaceholderText('Project name');
3506
+ expect(screen.getByLabelText('Cloud runtime')).toBeInTheDocument();
3507
+ const nameInput = screen.getByPlaceholderText('Workspace name');
2773
3508
  await user.clear(nameInput);
2774
- await user.type(nameInput, 'Delta Project');
2775
- await user.type(screen.getByPlaceholderText('Project description (optional)'), 'Workspace desc');
2776
- await user.click(screen.getByRole('button', { name: 'Save Project Setup' }));
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' }));
2777
3513
  await waitFor(() => {
2778
3514
  expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
2779
3515
  });
2780
- expect(fetchMock).toHaveBeenCalledWith('/api/taskforce/workspace-profile', expect.objectContaining({ method: 'POST' }));
2781
- const workspaceSyncStateCall = fetchMock.mock.calls.find((call) => {
2782
- if (!String(call[0]).includes('/api/taskforce/ui-state'))
2783
- return false;
2784
- const init = call[1];
2785
- if (String(init?.method || '').toUpperCase() !== 'POST')
2786
- return false;
2787
- const body = JSON.parse(String(init?.body || '{}'));
2788
- return body?.stateKey === 'workspace-sync';
2789
- });
2790
- const workspaceSyncStateBody = JSON.parse(String(workspaceSyncStateCall?.[1]?.body || '{}'));
2791
- expect(workspaceSyncStateBody.stateKey).toBe('workspace-sync');
2792
- 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);
2793
3519
  });
2794
3520
  it('applies the selected starter taxonomy pack during workspace setup', async () => {
2795
3521
  const user = userEvent.setup();
@@ -2897,10 +3623,22 @@ describe('TaskforceCore Integration', () => {
2897
3623
  vi.stubGlobal('fetch', fetchMock);
2898
3624
  render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, {}) }));
2899
3625
  await waitFor(() => {
2900
- expect(screen.getByText('Workspace Setup Required')).toBeInTheDocument();
2901
- });
2902
- await user.click(screen.getByRole('radio', { name: /software development/i }));
2903
- 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' }));
2904
3642
  await waitFor(() => {
2905
3643
  expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
2906
3644
  });
@@ -2993,9 +3731,9 @@ describe('TaskforceCore Integration', () => {
2993
3731
  expect(configCalls).toBeGreaterThan(1);
2994
3732
  });
2995
3733
  expect(screen.getByRole('button', { name: /add task/i })).toBeInTheDocument();
2996
- expect(screen.queryByText('Workspace Setup Required')).not.toBeInTheDocument();
3734
+ expect(screen.queryByText('Configure your new workspace.')).not.toBeInTheDocument();
2997
3735
  });
2998
- 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 () => {
2999
3737
  const user = userEvent.setup();
3000
3738
  let workspaceSaved = false;
3001
3739
  const fetchMock = vi.fn((url, init) => {
@@ -3071,9 +3809,45 @@ describe('TaskforceCore Integration', () => {
3071
3809
  if (url.includes('/api/taskforce/session/workspace')) {
3072
3810
  return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
3073
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
+ }
3074
3839
  if (url.includes('/api/taskforce/ui-state')) {
3075
3840
  return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
3076
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
+ }
3077
3851
  if (url.includes('/api/taskforce/tasks')) {
3078
3852
  return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
3079
3853
  }
@@ -3085,17 +3859,18 @@ describe('TaskforceCore Integration', () => {
3085
3859
  vi.stubGlobal('fetch', fetchMock);
3086
3860
  render(_jsx(MemoryRouter, { initialEntries: ['/setup?step=workspace&source=cloud'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
3087
3861
  await waitFor(() => {
3088
- expect(screen.getByText('Workspace Setup Required')).toBeInTheDocument();
3862
+ expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
3089
3863
  });
3090
- await user.click(screen.getByRole('radio', { name: /sync existing cloud project/i }));
3091
3864
  await waitFor(() => {
3092
3865
  expect(screen.getByRole('option', { name: /Cloud Project 123/ })).toBeInTheDocument();
3093
3866
  });
3094
3867
  await user.selectOptions(screen.getByRole('combobox'), 'workspace-cloud-123');
3095
- await user.click(screen.getByRole('button', { name: 'Save Project Setup' }));
3868
+ await user.click(screen.getByRole('button', { name: 'Connect Workspace' }));
3096
3869
  await waitFor(() => {
3097
- expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
3870
+ expect(screen.getByText('Importing "Cloud Project 123" from cloud into local.')).toBeInTheDocument();
3098
3871
  });
3872
+ expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
3873
+ expect(screen.getByRole('button', { name: 'Importing Workspace...' })).toBeInTheDocument();
3099
3874
  const workspaceProfileCall = fetchMock.mock.calls.find((call) => String(call[0]).includes('/api/taskforce/workspace-profile'));
3100
3875
  const workspaceProfileBody = JSON.parse(String(workspaceProfileCall?.[1]?.body || '{}'));
3101
3876
  expect(workspaceProfileBody.workspaceId).toBe('workspace-cloud-123');
@@ -3110,8 +3885,258 @@ describe('TaskforceCore Integration', () => {
3110
3885
  });
3111
3886
  const workspaceSyncStateBody = JSON.parse(String(workspaceSyncStateCall?.[1]?.body || '{}'));
3112
3887
  expect(workspaceSyncStateBody.stateKey).toBe('workspace-sync');
3113
- expect(workspaceSyncStateBody.patch?.cloudSyncEnabled).toBe(true);
3888
+ expect(workspaceSyncStateBody.patch?.enabled).toBe(true);
3889
+ expect(workspaceSyncStateBody.patch?.phase).toBe('attach-cloud');
3114
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);
3115
4140
  });
3116
4141
  it('enables workspace cloud sync from local setup when signed in and checkbox is selected', async () => {
3117
4142
  const user = userEvent.setup();
@@ -3183,7 +4208,7 @@ describe('TaskforceCore Integration', () => {
3183
4208
  vi.stubGlobal('fetch', fetchMock);
3184
4209
  render(_jsx(MemoryRouter, { initialEntries: ['/setup?step=workspace'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
3185
4210
  await waitFor(() => {
3186
- expect(screen.getByText('Workspace Setup Required')).toBeInTheDocument();
4211
+ expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
3187
4212
  });
3188
4213
  await waitFor(() => {
3189
4214
  expect(screen.getByRole('checkbox', { name: /sync to cloud after setup/i })).toBeInTheDocument();
@@ -3621,7 +4646,7 @@ describe('TaskforceCore Integration', () => {
3621
4646
  vi.stubGlobal('fetch', fetchMock);
3622
4647
  const initialRender = render(_jsx(MemoryRouter, { initialEntries: ['/setup?step=workspace'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
3623
4648
  await waitFor(() => {
3624
- expect(screen.getByText('Workspace Setup Required')).toBeInTheDocument();
4649
+ expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
3625
4650
  });
3626
4651
  await waitFor(() => {
3627
4652
  expect(screen.getByRole('checkbox', { name: /sync to cloud after setup/i })).toBeInTheDocument();
@@ -3637,7 +4662,7 @@ describe('TaskforceCore Integration', () => {
3637
4662
  initialRender.unmount();
3638
4663
  render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
3639
4664
  await waitFor(() => {
3640
- expect(screen.queryByText('Workspace Setup Required')).not.toBeInTheDocument();
4665
+ expect(screen.queryByText('Configure your new workspace.')).not.toBeInTheDocument();
3641
4666
  expect(screen.getByRole('button', { name: /add task/i })).toBeInTheDocument();
3642
4667
  });
3643
4668
  expect(cloudWorkspaceCreated).toBe(false);
@@ -3878,18 +4903,31 @@ describe('TaskforceCore Integration', () => {
3878
4903
  vi.stubGlobal('fetch', fetchMock);
3879
4904
  render(_jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }));
3880
4905
  await waitFor(() => {
3881
- expect(screen.getByTitle('Open tasks matching current filters')).toHaveTextContent('1/1');
4906
+ expect(screen.getByTitle(/Open tasks/)).toHaveTextContent(/1/);
3882
4907
  expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
3883
4908
  });
3884
4909
  await user.click(screen.getByTitle(/Account/i));
3885
4910
  await user.click(await screen.findByRole('menuitem', { name: 'Sign Out' }));
3886
4911
  await waitFor(() => {
3887
4912
  expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('https://cloud.example.com/api/taskforce/auth/logout'))).toBe(true);
3888
- expect(screen.getByTitle('Open tasks matching current filters')).toHaveTextContent('1/1');
4913
+ expect(screen.getByTitle(/Open tasks/)).toHaveTextContent(/1/);
3889
4914
  });
3890
4915
  await user.click(screen.getByTitle(/Account/i));
3891
4916
  expect(await screen.findByText('Sign In / Register')).toBeInTheDocument();
3892
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
+ });
3893
4931
  it('does not pull from cloud when workspace sync toggle is disabled', async () => {
3894
4932
  const fetchMock = vi.fn((url) => {
3895
4933
  if (url.includes('/api/taskforce/auth/session')) {
@@ -4758,9 +5796,10 @@ describe('TaskforceCore Integration', () => {
4758
5796
  it('keeps local workspace context authoritative over cloud auth session workspace id', async () => {
4759
5797
  const useTaskforcePath = path.join(process.cwd(), 'src/hooks/useTaskforce.ts');
4760
5798
  const useTaskforceSource = fs.readFileSync(useTaskforcePath, 'utf8');
4761
- expect(useTaskforceSource).toContain("const keepLocalWorkspaceContext = runtimeMode === 'local' || authOnlyCloudMode;");
4762
- expect(useTaskforceSource).toContain('Local runtime resolves workspace identity from local DB via /api/taskforce/config.');
4763
- expect(useTaskforceSource).toContain('Do not override it from cloud auth session payloads.');
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';");
4764
5803
  });
4765
5804
  it.skip('blocks local cloud-sync pull/apply when workspaceId remains default', async () => {
4766
5805
  const fetchMock = vi.fn((url, init) => {