@taskforcehq/taskforce 0.3.304 → 0.3.305

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/README.md +140 -247
  2. package/dist/TaskforceCore.js +165 -70
  3. package/dist/TaskforceCore.test.js +799 -90
  4. package/dist/compat/workspaceSyncCompat.js +6 -0
  5. package/dist/components/features/DocumentIndex.d.ts +1 -1
  6. package/dist/components/features/DocumentIndex.js +1 -1
  7. package/dist/components/features/DocumentViewer.d.ts +1 -1
  8. package/dist/components/features/DocumentViewer.js +32 -9
  9. package/dist/components/features/DocumentWorkspace.d.ts +5 -22
  10. package/dist/components/features/DocumentWorkspace.js +12 -155
  11. package/dist/components/features/DocumentWorkspace.test.js +193 -6
  12. package/dist/components/features/TaskSettings.js +29 -3
  13. package/dist/components/features/TaskSettings.test.js +42 -1
  14. package/dist/components/features/documentWorkspaceModel.d.ts +24 -0
  15. package/dist/components/features/documentWorkspaceModel.js +57 -0
  16. package/dist/components/features/useDocumentWorkspaceController.d.ts +34 -0
  17. package/dist/components/features/useDocumentWorkspaceController.js +113 -0
  18. package/dist/components/task/TaskForm.d.ts +8 -8
  19. package/dist/components/task/TaskForm.js +358 -221
  20. package/dist/components/task/TaskForm.test.js +456 -381
  21. package/dist/components/task/TaskKanban.js +2 -1
  22. package/dist/components/views/PlanComparisonPage.js +5 -8
  23. package/dist/components/views/PlanComparisonPage.test.js +51 -41
  24. package/dist/components/views/PlansPage.js +126 -84
  25. package/dist/components/views/PlansPage.test.js +308 -39
  26. package/dist/components/views/StandaloneLayout.js +28 -22
  27. package/dist/components/views/panels/FilterToolbar.test.js +6 -4
  28. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +2 -14
  29. package/dist/components/views/standalone/modals/AccountHubModal.js +12 -14
  30. package/dist/components/views/standalone/modals/AccountHubModal.test.js +24 -1
  31. package/dist/config/envSchema.js +1 -1
  32. package/dist/core/AiProfileService.d.ts +6 -1
  33. package/dist/core/AiProfileService.js +161 -16
  34. package/dist/core/AiProfileService.test.js +3 -1
  35. package/dist/core/AiProfiles.test.js +200 -0
  36. package/dist/core/AuthTokenService.test.d.ts +1 -0
  37. package/dist/core/AuthTokenService.test.js +78 -0
  38. package/dist/core/EntitlementsPolicy.test.js +75 -13
  39. package/dist/core/PlanEntitlementService.d.ts +7 -1
  40. package/dist/core/PlanEntitlementService.js +117 -42
  41. package/dist/core/PlanVersionPolicy.test.js +19 -26
  42. package/dist/core/SignupMonetizationSettings.test.js +46 -21
  43. package/dist/core/SystemAdmin.test.js +212 -56
  44. package/dist/core/TaskTaxonomyValidation.test.js +53 -0
  45. package/dist/core/Taskforce.d.ts +3 -0
  46. package/dist/core/Taskforce.js +50 -10
  47. package/dist/core/WorkspacePlanMode.test.js +12 -12
  48. package/dist/core/shared.d.ts +2 -0
  49. package/dist/core/shared.js +11 -0
  50. package/dist/hooks/sync/auth.js +3 -1
  51. package/dist/hooks/sync/transfers.d.ts +2 -1
  52. package/dist/hooks/sync/transfers.js +8 -4
  53. package/dist/hooks/useSyncOrchestrator.d.ts +7 -4
  54. package/dist/hooks/useSyncOrchestrator.js +75 -7
  55. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +80 -10
  56. package/dist/hooks/useTaskMutations.d.ts +2 -1
  57. package/dist/hooks/useTaskMutations.js +12 -1
  58. package/dist/hooks/useTaskMutations.test.js +30 -1
  59. package/dist/hooks/useTaskforce.d.ts +5 -2
  60. package/dist/hooks/useTaskforce.js +17 -5
  61. package/dist/hooks/useTaskforce.sync-behavior.test.js +78 -0
  62. package/dist/hooks/useWorkspaceSyncController.d.ts +3 -1
  63. package/dist/hooks/useWorkspaceSyncController.js +11 -1
  64. package/dist/hooks/useWorkspaceSyncController.test.js +1 -0
  65. package/dist/mcp/adminWorkspaceRegistrar.d.ts +2 -0
  66. package/dist/mcp/adminWorkspaceRegistrar.js +154 -0
  67. package/dist/mcp/collaborationRegistrar.d.ts +2 -0
  68. package/dist/mcp/collaborationRegistrar.js +71 -0
  69. package/dist/mcp/documentAssetRegistrar.d.ts +2 -0
  70. package/dist/mcp/documentAssetRegistrar.js +346 -0
  71. package/dist/mcp/runtime.js +2362 -3530
  72. package/dist/mcp/runtime.test.js +159 -0
  73. package/dist/mcp/taskPlanningRegistrar.d.ts +2 -0
  74. package/dist/mcp/taskPlanningRegistrar.js +418 -0
  75. package/dist/mcp/toolCatalog.d.ts +35 -0
  76. package/dist/mcp/toolCatalog.js +3 -0
  77. package/dist/server/routes/admin.d.ts +1 -5
  78. package/dist/server/routes/admin.js +138 -61
  79. package/dist/server/routes/annotatedAttachments.d.ts +1 -1
  80. package/dist/server/routes/annotatedAttachments.js +1 -1
  81. package/dist/server/routes/auth.d.ts +1 -4
  82. package/dist/server/routes/auth.js +23 -63
  83. package/dist/server/routes/authSupport.d.ts +30 -0
  84. package/dist/server/routes/authSupport.js +81 -0
  85. package/dist/server/routes/billing.d.ts +1 -1
  86. package/dist/server/routes/billing.js +104 -188
  87. package/dist/server/routes/billing.test.js +214 -86
  88. package/dist/server/routes/documentReviews.d.ts +1 -1
  89. package/dist/server/routes/documentReviews.js +1 -1
  90. package/dist/server/routes/documents.d.ts +4 -2
  91. package/dist/server/routes/documents.js +73 -7
  92. package/dist/server/routes/primitives.d.ts +11 -0
  93. package/dist/server/routes/primitives.js +73 -0
  94. package/dist/server/routes/resources.d.ts +1 -1
  95. package/dist/server/routes/resources.js +1 -1
  96. package/dist/server/routes/shared.d.ts +1 -1
  97. package/dist/server/routes/sync.d.ts +1 -1
  98. package/dist/server/routes/sync.js +1 -1
  99. package/dist/server/routes/tasks.d.ts +1 -1
  100. package/dist/server/routes/tasks.js +1 -1
  101. package/dist/server/routes/workspaces.d.ts +1 -1
  102. package/dist/server/routes/workspaces.js +1 -1
  103. package/dist/server/routes.d.ts +2 -9
  104. package/dist/server/routes.js +76 -162
  105. package/dist/server/routes.test.js +672 -30
  106. package/dist/sync/contentSyncState.js +16 -7
  107. package/dist/sync/syncApplyHandlers.test.js +2 -0
  108. package/dist/sync/syncService.d.ts +1 -0
  109. package/dist/sync/syncService.js +8 -0
  110. package/dist/sync/syncService.test.js +14 -0
  111. package/dist/sync/workspaceSyncModel.d.ts +3 -0
  112. package/dist/sync/workspaceSyncModel.js +12 -3
  113. package/dist/sync/workspaceSyncModel.test.js +34 -0
  114. package/dist/sync/workspaceSyncState.d.ts +2 -0
  115. package/dist/sync/workspaceSyncState.js +1 -0
  116. package/dist/ui/assets/{AgentsModule-BLWVbuKP.js → AgentsModule-N2MnQBZk.js} +1 -1
  117. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BlS-cqnl.js → AnnotatedAttachmentWorkspace-BG6P_GVW.js} +1 -1
  118. package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +1 -0
  119. package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +252 -0
  120. package/dist/ui/assets/{InitiativesModule-BjR6iBf9.js → InitiativesModule-Dhm7M8e7.js} +1 -1
  121. package/dist/ui/assets/PlansPage-Cq0zXj3I.js +1 -0
  122. package/dist/ui/assets/TaskSettings-ln0aF7xc.js +9 -0
  123. package/dist/ui/assets/{WorkflowsModule-DnFqdUME.js → WorkflowsModule-BcS4afRn.js} +1 -1
  124. package/dist/ui/assets/{index-Ii0B0rTO.css → index-C2-OXZV7.css} +1 -1
  125. package/dist/ui/assets/index-DcSI5F86.js +6 -0
  126. package/dist/ui/assets/{vendor-icons-I7ZwG1z_.js → vendor-icons-BSUaRwF8.js} +1 -1
  127. package/dist/ui/index.html +3 -3
  128. package/dist/utils/commercialLifecyclePresentation.d.ts +19 -0
  129. package/dist/utils/commercialLifecyclePresentation.js +196 -0
  130. package/package.json +8 -1
  131. package/scripts/check-cloud-mcp.mjs +83 -0
  132. package/scripts/playwright-auth.sh +5 -1
  133. package/scripts/run-billing-performance-smoke.sh +24 -0
  134. package/dist/ui/assets/DocumentWorkspace-BH_6DF6x.js +0 -250
  135. package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +0 -1
  136. package/dist/ui/assets/PlansPage-CSDQ4sLa.js +0 -1
  137. package/dist/ui/assets/TaskSettings-CEDy34Jo.js +0 -9
  138. package/dist/ui/assets/index-DXUdtH1B.js +0 -6
@@ -830,6 +830,9 @@ describe('TaskforceCore Integration', () => {
830
830
  });
831
831
  it('shows full-page login when cloud auth is required and user is unauthenticated', async () => {
832
832
  const fetchMock = vi.fn((url) => {
833
+ if (url.includes('/api/taskforce/auth/runtime-config')) {
834
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'cloud' } }) });
835
+ }
833
836
  if (url.includes('/api/taskforce/auth/session')) {
834
837
  return Promise.resolve({
835
838
  ok: true,
@@ -929,8 +932,8 @@ describe('TaskforceCore Integration', () => {
929
932
  json: () => Promise.resolve({
930
933
  plans: [
931
934
  {
932
- planId: 'pro',
933
- displayName: 'Pro Plan test mode',
935
+ planId: 'basic',
936
+ displayName: 'Basic Plan test mode',
934
937
  status: 'active',
935
938
  features: [],
936
939
  pricing: { month: null, year: null },
@@ -946,7 +949,7 @@ describe('TaskforceCore Integration', () => {
946
949
  render(_jsx(MemoryRouter, { initialEntries: ['/pricing'], children: _jsx(TaskforceCore, {}) }));
947
950
  await waitFor(() => {
948
951
  expect(screen.queryByPlaceholderText('Email')).not.toBeInTheDocument();
949
- expect(screen.getByText('Pro Plan test mode')).toBeInTheDocument();
952
+ expect(screen.getByText('Basic Plan test mode')).toBeInTheDocument();
950
953
  expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
951
954
  });
952
955
  });
@@ -1019,8 +1022,8 @@ describe('TaskforceCore Integration', () => {
1019
1022
  json: () => Promise.resolve({
1020
1023
  plans: [
1021
1024
  {
1022
- planId: 'starter',
1023
- displayName: 'Starter',
1025
+ planId: 'free',
1026
+ displayName: 'Free',
1024
1027
  status: 'active',
1025
1028
  features: [],
1026
1029
  pricing: {
@@ -1028,7 +1031,7 @@ describe('TaskforceCore Integration', () => {
1028
1031
  year: null
1029
1032
  },
1030
1033
  defaultVersion: {
1031
- planVersionId: 'starter-v1',
1034
+ planVersionId: 'free-v1',
1032
1035
  versionNumber: 1,
1033
1036
  seatLimit: null,
1034
1037
  metadata: {}
@@ -1042,8 +1045,8 @@ describe('TaskforceCore Integration', () => {
1042
1045
  return Promise.resolve({
1043
1046
  ok: true,
1044
1047
  json: () => Promise.resolve({
1045
- planId: 'starter',
1046
- planName: 'Starter',
1048
+ planId: 'free',
1049
+ planName: 'Free',
1047
1050
  entitlementState: 'active'
1048
1051
  })
1049
1052
  });
@@ -1052,8 +1055,8 @@ describe('TaskforceCore Integration', () => {
1052
1055
  return Promise.resolve({
1053
1056
  ok: true,
1054
1057
  json: () => Promise.resolve({
1055
- planId: 'starter',
1056
- planName: 'Starter',
1058
+ planId: 'free',
1059
+ planName: 'Free',
1057
1060
  entitlementState: 'active'
1058
1061
  })
1059
1062
  });
@@ -1154,8 +1157,8 @@ describe('TaskforceCore Integration', () => {
1154
1157
  json: () => Promise.resolve({
1155
1158
  plans: [
1156
1159
  {
1157
- planId: 'starter',
1158
- displayName: 'Starter',
1160
+ planId: 'free',
1161
+ displayName: 'Free',
1159
1162
  status: 'active',
1160
1163
  features: [],
1161
1164
  pricing: {
@@ -1163,7 +1166,7 @@ describe('TaskforceCore Integration', () => {
1163
1166
  year: null
1164
1167
  },
1165
1168
  defaultVersion: {
1166
- planVersionId: 'starter-v1',
1169
+ planVersionId: 'free-v1',
1167
1170
  versionNumber: 1,
1168
1171
  seatLimit: null,
1169
1172
  metadata: {}
@@ -1269,8 +1272,8 @@ describe('TaskforceCore Integration', () => {
1269
1272
  json: () => Promise.resolve({
1270
1273
  plans: [
1271
1274
  {
1272
- planId: 'starter',
1273
- displayName: 'Starter',
1275
+ planId: 'free',
1276
+ displayName: 'Free',
1274
1277
  status: 'active',
1275
1278
  features: [],
1276
1279
  pricing: {
@@ -1278,7 +1281,7 @@ describe('TaskforceCore Integration', () => {
1278
1281
  year: null
1279
1282
  },
1280
1283
  defaultVersion: {
1281
- planVersionId: 'starter-v1',
1284
+ planVersionId: 'free-v1',
1282
1285
  versionNumber: 1,
1283
1286
  seatLimit: null,
1284
1287
  metadata: {}
@@ -1292,9 +1295,9 @@ describe('TaskforceCore Integration', () => {
1292
1295
  return Promise.resolve({
1293
1296
  ok: true,
1294
1297
  json: () => Promise.resolve({
1295
- planId: 'starter',
1296
- planName: 'Starter',
1297
- planVersionId: 'starter-v1',
1298
+ planId: 'free',
1299
+ planName: 'Free',
1300
+ planVersionId: 'free-v1',
1298
1301
  entitlementState: 'active'
1299
1302
  })
1300
1303
  });
@@ -1323,7 +1326,7 @@ describe('TaskforceCore Integration', () => {
1323
1326
  render(_jsx(MemoryRouter, { initialEntries: ['/?screen=plans&gate=missing_entitlement'], children: _jsx(TaskforceCore, {}) }));
1324
1327
  await waitFor(() => {
1325
1328
  expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
1326
- expect(screen.getByText('Starter')).toBeInTheDocument();
1329
+ expect(screen.getByText('Free')).toBeInTheDocument();
1327
1330
  });
1328
1331
  await waitFor(() => {
1329
1332
  expect(screen.queryByText('No entitlement is linked to this account.')).not.toBeInTheDocument();
@@ -1393,8 +1396,8 @@ describe('TaskforceCore Integration', () => {
1393
1396
  return Promise.resolve({
1394
1397
  ok: true,
1395
1398
  json: () => Promise.resolve({
1396
- planId: 'starter',
1397
- planName: 'Starter',
1399
+ planId: 'free',
1400
+ planName: 'Free',
1398
1401
  entitlementState: 'active'
1399
1402
  })
1400
1403
  });
@@ -1450,14 +1453,14 @@ describe('TaskforceCore Integration', () => {
1450
1453
  });
1451
1454
  await waitFor(() => {
1452
1455
  expect(screen.getByText('Subscription')).toBeInTheDocument();
1453
- expect(screen.getByText('Starter')).toBeInTheDocument();
1456
+ expect(screen.getByText('Free')).toBeInTheDocument();
1454
1457
  expect(screen.getByText('Runtime')).toBeInTheDocument();
1455
1458
  expect(screen.getByText('LOCAL')).toBeInTheDocument();
1456
1459
  expect(screen.getByText('Environment')).toBeInTheDocument();
1457
1460
  expect(screen.getByText('CLOUD.EXAMPLE.COM')).toBeInTheDocument();
1458
1461
  expect(screen.queryByText(/Current Plan:/i)).not.toBeInTheDocument();
1459
1462
  });
1460
- await user.click(screen.getByRole('button', { name: /Subscription Starter/i }));
1463
+ await user.click(screen.getByRole('button', { name: /Subscription Free/i }));
1461
1464
  await waitFor(() => {
1462
1465
  expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
1463
1466
  });
@@ -1512,8 +1515,8 @@ describe('TaskforceCore Integration', () => {
1512
1515
  return Promise.resolve({
1513
1516
  ok: true,
1514
1517
  json: () => Promise.resolve({
1515
- planId: 'starter',
1516
- planName: 'Starter',
1518
+ planId: 'free',
1519
+ planName: 'Free',
1517
1520
  entitlementState: 'active'
1518
1521
  })
1519
1522
  });
@@ -1563,7 +1566,7 @@ describe('TaskforceCore Integration', () => {
1563
1566
  }
1564
1567
  await waitFor(() => {
1565
1568
  expect(screen.getByText('Subscription')).toBeInTheDocument();
1566
- expect(screen.getByText('Starter')).toBeInTheDocument();
1569
+ expect(screen.getByText('Free')).toBeInTheDocument();
1567
1570
  expect(screen.getByText('Runtime')).toBeInTheDocument();
1568
1571
  expect(screen.getByText('LOCAL')).toBeInTheDocument();
1569
1572
  expect(screen.getByText('Environment')).toBeInTheDocument();
@@ -1590,9 +1593,9 @@ describe('TaskforceCore Integration', () => {
1590
1593
  userId: 'user-new',
1591
1594
  displayName: 'New User',
1592
1595
  workspaceId: 'workspace-local-active',
1593
- planId: 'starter',
1594
- planVersionId: 'starter-v1',
1595
- planName: 'Starter',
1596
+ planId: 'free',
1597
+ planVersionId: 'free-v1',
1598
+ planName: 'Free',
1596
1599
  teamManagementAllowed: false,
1597
1600
  teamPlanMode: 'personal',
1598
1601
  workspaceRole: 'member'
@@ -1690,11 +1693,11 @@ describe('TaskforceCore Integration', () => {
1690
1693
  return jsonResponse({
1691
1694
  plans: [
1692
1695
  {
1693
- planId: 'starter',
1694
- displayName: 'Starter',
1696
+ planId: 'free',
1697
+ displayName: 'Free',
1695
1698
  status: 'active',
1696
1699
  defaultVersion: {
1697
- planVersionId: 'starter-v1',
1700
+ planVersionId: 'free-v1',
1698
1701
  versionNumber: 1,
1699
1702
  seatLimit: null,
1700
1703
  metadata: {}
@@ -1825,7 +1828,7 @@ describe('TaskforceCore Integration', () => {
1825
1828
  await user.click(screen.getByTitle(/Account/i));
1826
1829
  await waitFor(() => {
1827
1830
  expect(screen.getByText(accounts.newcomer.email)).toBeInTheDocument();
1828
- expect(screen.getByText('Starter')).toBeInTheDocument();
1831
+ expect(screen.getByText('Free')).toBeInTheDocument();
1829
1832
  });
1830
1833
  await user.click(screen.getByRole('menuitem', { name: 'Sign Out' }));
1831
1834
  await waitFor(() => {
@@ -1846,7 +1849,7 @@ describe('TaskforceCore Integration', () => {
1846
1849
  await waitFor(() => {
1847
1850
  expect(screen.getByText(accounts.ross.email)).toBeInTheDocument();
1848
1851
  expect(screen.getByText('Team 5')).toBeInTheDocument();
1849
- expect(screen.queryByText('Starter')).not.toBeInTheDocument();
1852
+ expect(screen.queryByText('Free')).not.toBeInTheDocument();
1850
1853
  expect(screen.getByRole('menuitem', { name: 'Team Management' })).toBeInTheDocument();
1851
1854
  });
1852
1855
  await user.click(screen.getByRole('button', { name: /Subscription Team 5/i }));
@@ -2034,6 +2037,69 @@ describe('TaskforceCore Integration', () => {
2034
2037
  expect(screen.getByRole('button', { name: /Resend Verification/ })).toBeDisabled();
2035
2038
  expect(fetchMock.mock.calls.some(([url]) => String(url).includes('/api/taskforce/auth/verify-email/request'))).toBe(true);
2036
2039
  });
2040
+ it('shows neutral verification copy when resend succeeds without attempting delivery', async () => {
2041
+ const user = userEvent.setup();
2042
+ const fetchMock = vi.fn((input) => {
2043
+ const url = String(input);
2044
+ if (url.includes('/api/taskforce/auth/session')) {
2045
+ return Promise.resolve({
2046
+ ok: true,
2047
+ json: () => Promise.resolve({
2048
+ authenticated: false,
2049
+ runtimeMode: 'cloud',
2050
+ authRequiredForApi: false
2051
+ })
2052
+ });
2053
+ }
2054
+ if (url.includes('/api/taskforce/config')) {
2055
+ return Promise.resolve({
2056
+ ok: true,
2057
+ json: () => Promise.resolve({
2058
+ ...mockConfig,
2059
+ runtimeMode: 'cloud',
2060
+ authRequiredForApi: false,
2061
+ setupState: {
2062
+ globalSetupState: 'present',
2063
+ workspaceSetupState: 'present',
2064
+ runtimeMode: 'cloud',
2065
+ workspaceId: 'workspace-local-active',
2066
+ mode: 'core',
2067
+ forceSetup: false,
2068
+ forceGlobalSetup: false,
2069
+ forceWorkspaceSetup: false
2070
+ }
2071
+ })
2072
+ });
2073
+ }
2074
+ if (url.includes('/api/taskforce/auth/verify-email/request')) {
2075
+ return Promise.resolve({
2076
+ ok: true,
2077
+ json: () => Promise.resolve({
2078
+ success: true,
2079
+ emailSent: false,
2080
+ deliveryAttempted: false,
2081
+ verificationToken: null
2082
+ })
2083
+ });
2084
+ }
2085
+ if (url.includes('/api/taskforce/public/pricing')) {
2086
+ return Promise.resolve({
2087
+ ok: true,
2088
+ json: () => Promise.resolve({
2089
+ plans: []
2090
+ })
2091
+ });
2092
+ }
2093
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
2094
+ });
2095
+ vi.stubGlobal('fetch', fetchMock);
2096
+ render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=verify'], children: _jsx(TaskforceCore, {}) }));
2097
+ await user.type(await screen.findByPlaceholderText('Email'), 'verify@example.com');
2098
+ await user.click(screen.getByRole('button', { name: 'Resend Verification' }));
2099
+ await waitFor(() => {
2100
+ expect(screen.getByText('If that account exists and still needs verification, we sent an email.')).toBeInTheDocument();
2101
+ });
2102
+ });
2037
2103
  it('does not switch to verify mode after registration when verification is not required', async () => {
2038
2104
  const user = userEvent.setup();
2039
2105
  let authenticated = false;
@@ -2228,11 +2294,11 @@ describe('TaskforceCore Integration', () => {
2228
2294
  if (url.includes('/api/taskforce/public/pricing')) {
2229
2295
  return jsonResponse({
2230
2296
  plans: [{
2231
- planId: 'starter',
2232
- displayName: 'Starter',
2297
+ planId: 'free',
2298
+ displayName: 'Free',
2233
2299
  status: 'active',
2234
2300
  defaultVersion: {
2235
- planVersionId: 'starter-v1',
2301
+ planVersionId: 'free-v1',
2236
2302
  versionNumber: 1,
2237
2303
  seatLimit: null,
2238
2304
  metadata: {}
@@ -2240,10 +2306,10 @@ describe('TaskforceCore Integration', () => {
2240
2306
  features: [],
2241
2307
  pricing: {
2242
2308
  month: {
2243
- planVersionId: 'starter-v1',
2309
+ planVersionId: 'free-v1',
2244
2310
  interval: 'month',
2245
2311
  pricingType: 'free',
2246
- stripePriceId: 'free:starter-v1:month',
2312
+ stripePriceId: 'free:free-v1:month',
2247
2313
  displayAmount: '$0.00',
2248
2314
  unitAmount: 0,
2249
2315
  currency: 'usd'
@@ -2493,9 +2559,9 @@ describe('TaskforceCore Integration', () => {
2493
2559
  }
2494
2560
  if (url.includes('/api/taskforce/account/profile-summary')) {
2495
2561
  return jsonResponse({
2496
- planId: 'pro',
2497
- planName: 'Pro',
2498
- planVersionId: 'pro-v1',
2562
+ planId: 'basic',
2563
+ planName: 'Basic',
2564
+ planVersionId: 'basic-v1',
2499
2565
  entitlementState: 'checkout_pending',
2500
2566
  gate: 'checkout_pending',
2501
2567
  message: 'Complete checkout to continue.',
@@ -2505,11 +2571,11 @@ describe('TaskforceCore Integration', () => {
2505
2571
  if (url.includes('/api/taskforce/public/pricing')) {
2506
2572
  return jsonResponse({
2507
2573
  plans: [{
2508
- planId: 'pro',
2509
- displayName: 'Pro',
2574
+ planId: 'basic',
2575
+ displayName: 'Basic',
2510
2576
  status: 'active',
2511
2577
  defaultVersion: {
2512
- planVersionId: 'pro-v1',
2578
+ planVersionId: 'basic-v1',
2513
2579
  versionNumber: 1,
2514
2580
  seatLimit: null,
2515
2581
  metadata: {}
@@ -2517,19 +2583,19 @@ describe('TaskforceCore Integration', () => {
2517
2583
  features: [],
2518
2584
  pricing: {
2519
2585
  month: {
2520
- planVersionId: 'pro-v1',
2586
+ planVersionId: 'basic-v1',
2521
2587
  interval: 'month',
2522
2588
  pricingType: 'stripe',
2523
- stripePriceId: 'price_pro_month',
2589
+ stripePriceId: 'price_basic_month',
2524
2590
  displayAmount: '$19.00',
2525
2591
  unitAmount: 1900,
2526
2592
  currency: 'usd'
2527
2593
  },
2528
2594
  year: {
2529
- planVersionId: 'pro-v1',
2595
+ planVersionId: 'basic-v1',
2530
2596
  interval: 'year',
2531
2597
  pricingType: 'stripe',
2532
- stripePriceId: 'price_pro_year',
2598
+ stripePriceId: 'price_basic_year',
2533
2599
  displayAmount: '$190.00',
2534
2600
  unitAmount: 19000,
2535
2601
  currency: 'usd'
@@ -2584,7 +2650,7 @@ describe('TaskforceCore Integration', () => {
2584
2650
  return jsonResponse({});
2585
2651
  });
2586
2652
  vi.stubGlobal('fetch', fetchMock);
2587
- render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&planId=pro&planVersionId=pro-v1&interval=year&next=%2F'], children: _jsx(TaskforceCore, {}) }));
2653
+ render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&planId=basic&planVersionId=basic-v1&interval=year&next=%2F'], children: _jsx(TaskforceCore, {}) }));
2588
2654
  await user.type(await screen.findByPlaceholderText('Email'), 'checkout@example.com');
2589
2655
  await user.type(screen.getByPlaceholderText('Display name'), 'Checkout User');
2590
2656
  await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
@@ -2977,6 +3043,162 @@ describe('TaskforceCore Integration', () => {
2977
3043
  expect(screen.getByRole('button', { name: 'Register' })).toBeInTheDocument();
2978
3044
  });
2979
3045
  });
3046
+ it('shows neutral password reset copy when reset delivery was not attempted', async () => {
3047
+ const user = userEvent.setup();
3048
+ const fetchMock = vi.fn((input) => {
3049
+ const url = String(input);
3050
+ if (url.includes('/api/taskforce/auth/runtime-config')) {
3051
+ return Promise.resolve({
3052
+ ok: true,
3053
+ json: () => Promise.resolve({
3054
+ success: true,
3055
+ config: {
3056
+ baseUrl: 'https://app.taskforce.example',
3057
+ apiBaseUrl: 'https://app.taskforce.example/api',
3058
+ cloudAuthBaseUrl: 'https://app.taskforce.example',
3059
+ runtimeMode: 'cloud',
3060
+ authSource: 'cloud',
3061
+ workspaceMode: 'multi-cloud',
3062
+ workspaceSwitchingEnabled: true,
3063
+ cloudAuthViaLocalProxy: false
3064
+ }
3065
+ })
3066
+ });
3067
+ }
3068
+ if (url.includes('/api/taskforce/auth/session')) {
3069
+ return Promise.resolve({
3070
+ ok: true,
3071
+ json: () => Promise.resolve({
3072
+ authenticated: false,
3073
+ runtimeMode: 'cloud',
3074
+ authRequiredForApi: false
3075
+ })
3076
+ });
3077
+ }
3078
+ if (url.includes('/api/taskforce/config')) {
3079
+ return Promise.resolve({
3080
+ ok: true,
3081
+ json: () => Promise.resolve({
3082
+ ...mockConfig,
3083
+ runtimeMode: 'cloud',
3084
+ authRequiredForApi: false,
3085
+ setupState: {
3086
+ globalSetupState: 'present',
3087
+ workspaceSetupState: 'present',
3088
+ runtimeMode: 'cloud',
3089
+ workspaceId: 'workspace-local-active',
3090
+ mode: 'core',
3091
+ forceSetup: false,
3092
+ forceGlobalSetup: false,
3093
+ forceWorkspaceSetup: false
3094
+ }
3095
+ })
3096
+ });
3097
+ }
3098
+ if (url.includes('/api/taskforce/auth/password-reset/request')) {
3099
+ return Promise.resolve({
3100
+ ok: true,
3101
+ json: () => Promise.resolve({
3102
+ success: true,
3103
+ emailSent: false,
3104
+ deliveryAttempted: false,
3105
+ resetToken: null
3106
+ })
3107
+ });
3108
+ }
3109
+ if (url.includes('/api/taskforce/workflow-templates')) {
3110
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
3111
+ }
3112
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
3113
+ });
3114
+ vi.stubGlobal('fetch', fetchMock);
3115
+ render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=forgot&next=%2F'], children: _jsx(TaskforceCore, {}) }));
3116
+ await user.type(await screen.findByPlaceholderText('Email'), 'nobody@example.com');
3117
+ await user.click(screen.getByRole('button', { name: 'Send Reset Link' }));
3118
+ await waitFor(() => {
3119
+ expect(screen.getByText('If that account exists, we sent password reset instructions.')).toBeInTheDocument();
3120
+ });
3121
+ });
3122
+ it('keeps forgot-password users on the request form when reset delivery fails', async () => {
3123
+ const user = userEvent.setup();
3124
+ const fetchMock = vi.fn((input) => {
3125
+ const url = String(input);
3126
+ if (url.includes('/api/taskforce/auth/runtime-config')) {
3127
+ return Promise.resolve({
3128
+ ok: true,
3129
+ json: () => Promise.resolve({
3130
+ success: true,
3131
+ config: {
3132
+ baseUrl: 'https://app.taskforce.example',
3133
+ apiBaseUrl: 'https://app.taskforce.example/api',
3134
+ cloudAuthBaseUrl: 'https://app.taskforce.example',
3135
+ runtimeMode: 'cloud',
3136
+ authSource: 'cloud',
3137
+ workspaceMode: 'multi-cloud',
3138
+ workspaceSwitchingEnabled: true,
3139
+ cloudAuthViaLocalProxy: false
3140
+ }
3141
+ })
3142
+ });
3143
+ }
3144
+ if (url.includes('/api/taskforce/auth/session')) {
3145
+ return Promise.resolve({
3146
+ ok: true,
3147
+ json: () => Promise.resolve({
3148
+ authenticated: false,
3149
+ runtimeMode: 'cloud',
3150
+ authRequiredForApi: false
3151
+ })
3152
+ });
3153
+ }
3154
+ if (url.includes('/api/taskforce/config')) {
3155
+ return Promise.resolve({
3156
+ ok: true,
3157
+ json: () => Promise.resolve({
3158
+ ...mockConfig,
3159
+ runtimeMode: 'cloud',
3160
+ authRequiredForApi: false,
3161
+ setupState: {
3162
+ globalSetupState: 'present',
3163
+ workspaceSetupState: 'present',
3164
+ runtimeMode: 'cloud',
3165
+ workspaceId: 'workspace-local-active',
3166
+ mode: 'core',
3167
+ forceSetup: false,
3168
+ forceGlobalSetup: false,
3169
+ forceWorkspaceSetup: false
3170
+ }
3171
+ })
3172
+ });
3173
+ }
3174
+ if (url.includes('/api/taskforce/auth/password-reset/request')) {
3175
+ return Promise.resolve({
3176
+ ok: true,
3177
+ json: () => Promise.resolve({
3178
+ success: true,
3179
+ emailSent: false,
3180
+ deliveryAttempted: true,
3181
+ emailError: 'SMTP temporary failure',
3182
+ resetToken: null
3183
+ })
3184
+ });
3185
+ }
3186
+ if (url.includes('/api/taskforce/workflow-templates')) {
3187
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
3188
+ }
3189
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
3190
+ });
3191
+ vi.stubGlobal('fetch', fetchMock);
3192
+ render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=forgot&next=%2F'], children: _jsx(TaskforceCore, {}) }));
3193
+ await user.type(await screen.findByPlaceholderText('Email'), 'owner@example.com');
3194
+ await user.click(screen.getByRole('button', { name: 'Send Reset Link' }));
3195
+ await waitFor(() => {
3196
+ expect(screen.getByText('Password reset email failed to send. SMTP temporary failure')).toBeInTheDocument();
3197
+ });
3198
+ expect(screen.getByText('Request a password reset link.')).toBeInTheDocument();
3199
+ expect(screen.queryByText('Enter the password reset token and choose a new password.')).not.toBeInTheDocument();
3200
+ expect(screen.getByRole('button', { name: 'Send Reset Link' })).toBeInTheDocument();
3201
+ });
2980
3202
  it('redirects register users to plans when signup plan configuration blocks account creation', async () => {
2981
3203
  const user = userEvent.setup();
2982
3204
  const fetchMock = vi.fn((input) => {
@@ -3135,15 +3357,20 @@ describe('TaskforceCore Integration', () => {
3135
3357
  expect(screen.getByRole('button', { name: 'Sign In to Continue' })).toBeInTheDocument();
3136
3358
  });
3137
3359
  });
3138
- it('shows password creation for new-user invites', async () => {
3360
+ it('uses invite join after invite-mode sign-in even if invite reinspection has not refreshed yet', async () => {
3361
+ let authenticated = false;
3362
+ let inviteInspectCalls = 0;
3139
3363
  const fetchMock = vi.fn((url) => {
3140
3364
  if (url.includes('/api/taskforce/auth/session')) {
3141
3365
  return Promise.resolve({
3142
3366
  ok: true,
3143
3367
  json: () => Promise.resolve({
3144
- authenticated: false,
3368
+ authenticated,
3145
3369
  runtimeMode: 'cloud',
3146
- authRequiredForApi: true
3370
+ authRequiredForApi: true,
3371
+ userId: authenticated ? 'user-existing' : 'anonymous',
3372
+ workspaceId: authenticated ? 'workspace-home' : 'default',
3373
+ email: authenticated ? 'existing@example.com' : null
3147
3374
  })
3148
3375
  });
3149
3376
  }
@@ -3158,7 +3385,7 @@ describe('TaskforceCore Integration', () => {
3158
3385
  globalSetupState: 'present',
3159
3386
  workspaceSetupState: 'present',
3160
3387
  runtimeMode: 'cloud',
3161
- workspaceId: 'workspace-1',
3388
+ workspaceId: authenticated ? 'workspace-home' : 'default',
3162
3389
  mode: 'core',
3163
3390
  forceSetup: false,
3164
3391
  forceGlobalSetup: false,
@@ -3168,54 +3395,314 @@ describe('TaskforceCore Integration', () => {
3168
3395
  });
3169
3396
  }
3170
3397
  if (url.includes('/api/taskforce/auth/invite/inspect')) {
3398
+ inviteInspectCalls += 1;
3399
+ if (authenticated && inviteInspectCalls > 1) {
3400
+ return new Promise(() => undefined);
3401
+ }
3171
3402
  return Promise.resolve({
3172
3403
  ok: true,
3173
3404
  json: () => Promise.resolve({
3174
3405
  success: true,
3175
3406
  state: 'valid',
3176
- email: 'new@example.com',
3407
+ email: 'existing@example.com',
3177
3408
  workspaceId: 'workspace-1',
3178
- inviteeKind: 'new_user',
3179
- passwordRequired: true
3409
+ inviteeKind: 'existing_user',
3410
+ passwordRequired: false
3180
3411
  })
3181
3412
  });
3182
3413
  }
3183
- if (url.includes('/api/taskforce/workflow-templates')) {
3184
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
3414
+ if (url.includes('/api/taskforce/auth/login')) {
3415
+ authenticated = true;
3416
+ return Promise.resolve({
3417
+ ok: true,
3418
+ json: () => Promise.resolve({ success: true })
3419
+ });
3185
3420
  }
3186
- return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
3187
- });
3188
- vi.stubGlobal('fetch', fetchMock);
3189
- render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=invite&token=invite-token&next=%2F'], children: _jsx(TaskforceCore, {}) }));
3190
- await waitFor(() => {
3191
- expect(screen.getByText('Create your account to join this workspace.')).toBeInTheDocument();
3192
- expect(screen.getByPlaceholderText('Create password')).toBeInTheDocument();
3193
- expect(screen.getByRole('button', { name: 'Create Account and Join' })).toBeInTheDocument();
3194
- });
3195
- });
3196
- it('shows global setup route when forced and persists selected mode', async () => {
3197
- const user = userEvent.setup();
3198
- let selectedMode = null;
3199
- const fetchMock = vi.fn((url, init) => {
3200
- if (url.includes('/api/taskforce/auth/session')) {
3421
+ if (url.includes('/api/taskforce/auth/invite/join')) {
3422
+ return Promise.resolve({
3423
+ ok: true,
3424
+ json: () => Promise.resolve({ success: true, workspaceId: 'workspace-1' })
3425
+ });
3426
+ }
3427
+ if (url.includes('/api/taskforce/workspaces')) {
3201
3428
  return Promise.resolve({
3202
3429
  ok: true,
3203
3430
  json: () => Promise.resolve({
3204
- authenticated: true,
3205
- runtimeMode: 'local',
3206
- authRequiredForApi: false
3431
+ currentWorkspaceId: authenticated ? 'workspace-home' : null,
3432
+ workspaces: authenticated
3433
+ ? [{ id: 'workspace-home', name: 'Home workspace', role: 'owner' }]
3434
+ : []
3207
3435
  })
3208
3436
  });
3209
3437
  }
3210
- if (url.includes('/api/taskforce/config')) {
3211
- const mode = selectedMode;
3438
+ if (url.includes('/api/taskforce/tasks')) {
3439
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: [], config: {} }) });
3440
+ }
3441
+ if (url.includes('/api/taskforce/account/access-status')) {
3212
3442
  return Promise.resolve({
3213
3443
  ok: true,
3214
3444
  json: () => Promise.resolve({
3215
- ...mockConfig,
3216
- runtimeMode: 'local',
3217
- authRequiredForApi: false,
3218
- setupState: {
3445
+ success: true,
3446
+ authenticated,
3447
+ commercialState: 'active',
3448
+ planSelectionRequired: false,
3449
+ checkoutPending: false
3450
+ })
3451
+ });
3452
+ }
3453
+ if (url.includes('/api/taskforce/account/profile-summary')) {
3454
+ return Promise.resolve({
3455
+ ok: true,
3456
+ json: () => Promise.resolve({
3457
+ success: true,
3458
+ currentWorkspaceId: authenticated ? 'workspace-home' : null,
3459
+ workspaces: authenticated ? [{ id: 'workspace-home', name: 'Home workspace' }] : []
3460
+ })
3461
+ });
3462
+ }
3463
+ if (url.includes('/api/taskforce/session/workspace')) {
3464
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true, workspaceId: 'workspace-1' }) });
3465
+ }
3466
+ if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
3467
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true, state: null }) });
3468
+ }
3469
+ if (url.includes('/api/taskforce/workflow-templates')) {
3470
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
3471
+ }
3472
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
3473
+ });
3474
+ vi.stubGlobal('fetch', fetchMock);
3475
+ render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=invite&token=invite-token&next=%2F'], children: _jsx(TaskforceCore, {}) }));
3476
+ await waitFor(() => {
3477
+ expect(screen.getByRole('button', { name: 'Sign In to Continue' })).toBeInTheDocument();
3478
+ });
3479
+ await userEvent.type(screen.getByPlaceholderText('Password'), 'ExistingUserPass123!');
3480
+ await userEvent.click(screen.getByTestId('auth-submit-button'));
3481
+ await waitFor(() => {
3482
+ expect(screen.getByText('Signed in. Review the invite details to continue.')).toBeInTheDocument();
3483
+ expect(screen.getByRole('button', { name: 'Join Workspace' })).toBeInTheDocument();
3484
+ });
3485
+ await userEvent.click(screen.getByTestId('auth-submit-button'));
3486
+ await waitFor(() => {
3487
+ expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('/api/taskforce/auth/invite/join'))).toBe(true);
3488
+ });
3489
+ expect(fetchMock.mock.calls.filter((call) => String(call[0]).includes('/api/taskforce/auth/login'))).toHaveLength(1);
3490
+ });
3491
+ it('keeps existing-user invites recoverable when the wrong account signs in first', async () => {
3492
+ let authenticated = false;
3493
+ let sessionEmail = null;
3494
+ const fetchMock = vi.fn((url, init) => {
3495
+ if (url.includes('/api/taskforce/auth/session')) {
3496
+ return Promise.resolve({
3497
+ ok: true,
3498
+ json: () => Promise.resolve({
3499
+ authenticated,
3500
+ runtimeMode: 'cloud',
3501
+ authRequiredForApi: true,
3502
+ userId: authenticated ? 'user-other' : 'anonymous',
3503
+ workspaceId: authenticated ? 'workspace-home' : 'default',
3504
+ email: sessionEmail
3505
+ })
3506
+ });
3507
+ }
3508
+ if (url.includes('/api/taskforce/config')) {
3509
+ return Promise.resolve({
3510
+ ok: true,
3511
+ json: () => Promise.resolve({
3512
+ ...mockConfig,
3513
+ runtimeMode: 'cloud',
3514
+ authRequiredForApi: true,
3515
+ setupState: {
3516
+ globalSetupState: 'present',
3517
+ workspaceSetupState: 'present',
3518
+ runtimeMode: 'cloud',
3519
+ workspaceId: authenticated ? 'workspace-home' : 'default',
3520
+ mode: 'core',
3521
+ forceSetup: false,
3522
+ forceGlobalSetup: false,
3523
+ forceWorkspaceSetup: false
3524
+ }
3525
+ })
3526
+ });
3527
+ }
3528
+ if (url.includes('/api/taskforce/auth/invite/inspect')) {
3529
+ return Promise.resolve({
3530
+ ok: true,
3531
+ json: () => Promise.resolve({
3532
+ success: true,
3533
+ state: 'valid',
3534
+ email: 'existing@example.com',
3535
+ workspaceId: 'workspace-1',
3536
+ inviteeKind: 'existing_user',
3537
+ passwordRequired: false
3538
+ })
3539
+ });
3540
+ }
3541
+ if (url.includes('/api/taskforce/auth/login')) {
3542
+ const body = typeof init?.body === 'string' ? JSON.parse(init.body) : {};
3543
+ authenticated = true;
3544
+ sessionEmail = String(body?.email || '').trim().toLowerCase() || null;
3545
+ return Promise.resolve({
3546
+ ok: true,
3547
+ json: () => Promise.resolve({ success: true })
3548
+ });
3549
+ }
3550
+ if (url.includes('/api/taskforce/auth/invite/join')) {
3551
+ return Promise.resolve({
3552
+ ok: false,
3553
+ json: () => Promise.resolve({
3554
+ success: false,
3555
+ code: 'INVITE_ACCOUNT_MISMATCH',
3556
+ error: 'Sign in with the invited account to join this workspace.'
3557
+ })
3558
+ });
3559
+ }
3560
+ if (url.includes('/api/taskforce/workspaces')) {
3561
+ return Promise.resolve({
3562
+ ok: true,
3563
+ json: () => Promise.resolve({
3564
+ currentWorkspaceId: authenticated ? 'workspace-home' : null,
3565
+ workspaces: authenticated
3566
+ ? [{ id: 'workspace-home', name: 'Home workspace', role: 'owner' }]
3567
+ : []
3568
+ })
3569
+ });
3570
+ }
3571
+ if (url.includes('/api/taskforce/tasks')) {
3572
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: [], config: {} }) });
3573
+ }
3574
+ if (url.includes('/api/taskforce/account/access-status')) {
3575
+ return Promise.resolve({
3576
+ ok: true,
3577
+ json: () => Promise.resolve({
3578
+ success: true,
3579
+ authenticated,
3580
+ commercialState: 'active',
3581
+ planSelectionRequired: false,
3582
+ checkoutPending: false
3583
+ })
3584
+ });
3585
+ }
3586
+ if (url.includes('/api/taskforce/account/profile-summary')) {
3587
+ return Promise.resolve({
3588
+ ok: true,
3589
+ json: () => Promise.resolve({
3590
+ success: true,
3591
+ currentWorkspaceId: authenticated ? 'workspace-home' : null,
3592
+ workspaces: authenticated ? [{ id: 'workspace-home', name: 'Home workspace' }] : []
3593
+ })
3594
+ });
3595
+ }
3596
+ if (url.includes('/api/taskforce/session/workspace')) {
3597
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true, workspaceId: 'workspace-home' }) });
3598
+ }
3599
+ if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
3600
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true, state: null }) });
3601
+ }
3602
+ if (url.includes('/api/taskforce/workflow-templates')) {
3603
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
3604
+ }
3605
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
3606
+ });
3607
+ vi.stubGlobal('fetch', fetchMock);
3608
+ render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=invite&token=invite-token&next=%2F'], children: _jsx(TaskforceCore, {}) }));
3609
+ await waitFor(() => {
3610
+ expect(screen.getByRole('button', { name: 'Sign In to Continue' })).toBeInTheDocument();
3611
+ });
3612
+ await userEvent.clear(screen.getByPlaceholderText('Email'));
3613
+ await userEvent.type(screen.getByPlaceholderText('Email'), 'other@example.com');
3614
+ await userEvent.type(screen.getByPlaceholderText('Password'), 'OtherUserPass123!');
3615
+ await userEvent.click(screen.getByTestId('auth-submit-button'));
3616
+ await waitFor(() => {
3617
+ expect(screen.getByText('Sign in with the invited account to join this workspace.')).toBeInTheDocument();
3618
+ expect(screen.getByDisplayValue('existing@example.com')).toBeInTheDocument();
3619
+ expect(screen.getByPlaceholderText('Password')).toBeInTheDocument();
3620
+ expect(screen.queryByRole('button', { name: 'Join Workspace' })).not.toBeInTheDocument();
3621
+ });
3622
+ expect(fetchMock.mock.calls.filter((call) => String(call[0]).includes('/api/taskforce/auth/login'))).toHaveLength(1);
3623
+ expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('/api/taskforce/auth/invite/join'))).toBe(false);
3624
+ });
3625
+ it('shows password creation for new-user invites', async () => {
3626
+ const fetchMock = vi.fn((url) => {
3627
+ if (url.includes('/api/taskforce/auth/session')) {
3628
+ return Promise.resolve({
3629
+ ok: true,
3630
+ json: () => Promise.resolve({
3631
+ authenticated: false,
3632
+ runtimeMode: 'cloud',
3633
+ authRequiredForApi: true
3634
+ })
3635
+ });
3636
+ }
3637
+ if (url.includes('/api/taskforce/config')) {
3638
+ return Promise.resolve({
3639
+ ok: true,
3640
+ json: () => Promise.resolve({
3641
+ ...mockConfig,
3642
+ runtimeMode: 'cloud',
3643
+ authRequiredForApi: true,
3644
+ setupState: {
3645
+ globalSetupState: 'present',
3646
+ workspaceSetupState: 'present',
3647
+ runtimeMode: 'cloud',
3648
+ workspaceId: 'workspace-1',
3649
+ mode: 'core',
3650
+ forceSetup: false,
3651
+ forceGlobalSetup: false,
3652
+ forceWorkspaceSetup: false
3653
+ }
3654
+ })
3655
+ });
3656
+ }
3657
+ if (url.includes('/api/taskforce/auth/invite/inspect')) {
3658
+ return Promise.resolve({
3659
+ ok: true,
3660
+ json: () => Promise.resolve({
3661
+ success: true,
3662
+ state: 'valid',
3663
+ email: 'new@example.com',
3664
+ workspaceId: 'workspace-1',
3665
+ inviteeKind: 'new_user',
3666
+ passwordRequired: true
3667
+ })
3668
+ });
3669
+ }
3670
+ if (url.includes('/api/taskforce/workflow-templates')) {
3671
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
3672
+ }
3673
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
3674
+ });
3675
+ vi.stubGlobal('fetch', fetchMock);
3676
+ render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=invite&token=invite-token&next=%2F'], children: _jsx(TaskforceCore, {}) }));
3677
+ await waitFor(() => {
3678
+ expect(screen.getByText('Create your account to join this workspace.')).toBeInTheDocument();
3679
+ expect(screen.getByPlaceholderText('Create password')).toBeInTheDocument();
3680
+ expect(screen.getByRole('button', { name: 'Create Account and Join' })).toBeInTheDocument();
3681
+ });
3682
+ });
3683
+ it('shows global setup route when forced and persists selected mode', async () => {
3684
+ const user = userEvent.setup();
3685
+ let selectedMode = null;
3686
+ const fetchMock = vi.fn((url, init) => {
3687
+ if (url.includes('/api/taskforce/auth/session')) {
3688
+ return Promise.resolve({
3689
+ ok: true,
3690
+ json: () => Promise.resolve({
3691
+ authenticated: true,
3692
+ runtimeMode: 'local',
3693
+ authRequiredForApi: false
3694
+ })
3695
+ });
3696
+ }
3697
+ if (url.includes('/api/taskforce/config')) {
3698
+ const mode = selectedMode;
3699
+ return Promise.resolve({
3700
+ ok: true,
3701
+ json: () => Promise.resolve({
3702
+ ...mockConfig,
3703
+ runtimeMode: 'local',
3704
+ authRequiredForApi: false,
3705
+ setupState: {
3219
3706
  globalSetupState: mode ? 'present' : 'missing',
3220
3707
  workspaceSetupState: 'present',
3221
3708
  runtimeMode: 'local',
@@ -3887,9 +4374,229 @@ describe('TaskforceCore Integration', () => {
3887
4374
  expect(workspaceSyncStateBody.stateKey).toBe('workspace-sync');
3888
4375
  expect(workspaceSyncStateBody.patch?.enabled).toBe(true);
3889
4376
  expect(workspaceSyncStateBody.patch?.phase).toBe('attach-cloud');
4377
+ expect(workspaceSyncStateBody.patch?.setupIntent).toBe('attach-cloud-import');
3890
4378
  expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('https://cloud.example.com/api/taskforce/workspaces'))).toBe(true);
3891
4379
  expect(screen.queryByTitle(/Account/i)).not.toBeInTheDocument();
3892
4380
  });
4381
+ it('keeps attach-cloud import blocking after refresh when setup intent is persisted', async () => {
4382
+ const fetchMock = vi.fn((url) => {
4383
+ if (url.includes('/api/taskforce/auth/runtime-config')) {
4384
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'cloud' } }) });
4385
+ }
4386
+ if (url.includes('/api/taskforce/auth/session')) {
4387
+ return Promise.resolve({
4388
+ ok: true,
4389
+ json: () => Promise.resolve({
4390
+ authenticated: true,
4391
+ runtimeMode: 'local',
4392
+ authRequiredForApi: false
4393
+ })
4394
+ });
4395
+ }
4396
+ if (url.includes('/api/taskforce/config')) {
4397
+ return Promise.resolve({
4398
+ ok: true,
4399
+ json: () => Promise.resolve({
4400
+ ...mockConfig,
4401
+ runtimeMode: 'local',
4402
+ authRequiredForApi: false,
4403
+ workspaceId: 'workspace-cloud-123',
4404
+ setupState: {
4405
+ globalSetupState: 'present',
4406
+ workspaceSetupState: 'missing',
4407
+ runtimeMode: 'local',
4408
+ workspaceId: 'workspace-cloud-123',
4409
+ mode: 'core',
4410
+ forceSetup: false,
4411
+ forceGlobalSetup: false,
4412
+ forceWorkspaceSetup: true,
4413
+ workspace: null,
4414
+ suggestedWorkspaceName: 'Cloud Project 123'
4415
+ },
4416
+ runtimeCapabilities: {
4417
+ runtimeMode: 'local',
4418
+ supportsCloudAuth: true,
4419
+ supportsAdminManagedSetupOverrides: false,
4420
+ supportsPackageScriptSetupOverrides: true
4421
+ }
4422
+ })
4423
+ });
4424
+ }
4425
+ if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
4426
+ return Promise.resolve({
4427
+ ok: true,
4428
+ json: () => Promise.resolve({
4429
+ success: true,
4430
+ state: {
4431
+ version: 2,
4432
+ enabled: true,
4433
+ phase: 'attach-cloud',
4434
+ setupIntent: 'attach-cloud-import',
4435
+ pullCursor: null,
4436
+ lastPullAt: null,
4437
+ lastPushAt: null,
4438
+ lastSyncedAt: null
4439
+ }
4440
+ })
4441
+ });
4442
+ }
4443
+ if (url.includes('https://cloud.example.com/api/taskforce/workspaces')) {
4444
+ return Promise.resolve({
4445
+ ok: true,
4446
+ json: () => Promise.resolve({
4447
+ success: true,
4448
+ workspaces: [
4449
+ {
4450
+ id: 'workspace-cloud-123',
4451
+ name: 'Cloud Project 123',
4452
+ description: 'Cloud desc'
4453
+ }
4454
+ ]
4455
+ })
4456
+ });
4457
+ }
4458
+ if (url.includes('/api/taskforce/ui-state?key=app')) {
4459
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true, state: {} }) });
4460
+ }
4461
+ if (url.includes('/api/taskforce/tasks')) {
4462
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
4463
+ }
4464
+ if (url.includes('/api/taskforce/archive')) {
4465
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ archived: [] }) });
4466
+ }
4467
+ if (url.includes('/api/taskforce/categories')) {
4468
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(mockCategories) });
4469
+ }
4470
+ if (url.includes('/api/taskforce/global-settings')) {
4471
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
4472
+ }
4473
+ if (url.includes('/api/taskforce/data-version')) {
4474
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ dataVersion: 1 }) });
4475
+ }
4476
+ if (url.includes('/api/taskforce/types'))
4477
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ types: [] }) });
4478
+ if (url.includes('/api/taskforce/priorities'))
4479
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ priorities: [] }) });
4480
+ if (url.includes('/api/taskforce/approaches'))
4481
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ approaches: [] }) });
4482
+ if (url.includes('/api/taskforce/specialists'))
4483
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
4484
+ if (url.includes('/api/taskforce/workflow-templates'))
4485
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
4486
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
4487
+ });
4488
+ vi.stubGlobal('fetch', fetchMock);
4489
+ render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
4490
+ await waitFor(() => {
4491
+ expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
4492
+ });
4493
+ await waitFor(() => {
4494
+ expect(screen.getByText('Importing "Cloud Project 123" from cloud into local.')).toBeInTheDocument();
4495
+ });
4496
+ });
4497
+ it('clears persisted attach-cloud import intent after the first successful import', async () => {
4498
+ const uiStatePosts = [];
4499
+ const fetchMock = vi.fn((url, init) => {
4500
+ if (url.includes('/api/taskforce/auth/runtime-config')) {
4501
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'cloud' } }) });
4502
+ }
4503
+ if (url.includes('/api/taskforce/auth/session')) {
4504
+ return Promise.resolve({
4505
+ ok: true,
4506
+ json: () => Promise.resolve({
4507
+ authenticated: true,
4508
+ runtimeMode: 'local',
4509
+ authRequiredForApi: false
4510
+ })
4511
+ });
4512
+ }
4513
+ if (url.includes('/api/taskforce/config')) {
4514
+ return Promise.resolve({
4515
+ ok: true,
4516
+ json: () => Promise.resolve({
4517
+ ...mockConfig,
4518
+ runtimeMode: 'local',
4519
+ authRequiredForApi: false,
4520
+ workspaceId: 'workspace-cloud-123'
4521
+ })
4522
+ });
4523
+ }
4524
+ if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
4525
+ return Promise.resolve({
4526
+ ok: true,
4527
+ json: () => Promise.resolve({
4528
+ success: true,
4529
+ state: {
4530
+ version: 2,
4531
+ enabled: true,
4532
+ phase: 'active',
4533
+ setupIntent: 'attach-cloud-import',
4534
+ pullCursor: 'cursor-1',
4535
+ lastPullAt: '2026-04-15T12:00:00.000Z',
4536
+ lastPushAt: null,
4537
+ lastSyncedAt: '2026-04-15T12:00:00.000Z'
4538
+ }
4539
+ })
4540
+ });
4541
+ }
4542
+ if (url.includes('/api/taskforce/ui-state') && String(init?.method || '').toUpperCase() === 'POST') {
4543
+ uiStatePosts.push(JSON.parse(String(init?.body || '{}')));
4544
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
4545
+ }
4546
+ if (url.includes('https://cloud.example.com/api/taskforce/workspaces')) {
4547
+ return Promise.resolve({
4548
+ ok: true,
4549
+ json: () => Promise.resolve({
4550
+ success: true,
4551
+ workspaces: [
4552
+ {
4553
+ id: 'workspace-cloud-123',
4554
+ name: 'Cloud Project 123',
4555
+ description: 'Cloud desc'
4556
+ }
4557
+ ]
4558
+ })
4559
+ });
4560
+ }
4561
+ if (url.includes('/api/taskforce/ui-state?key=app')) {
4562
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true, state: {} }) });
4563
+ }
4564
+ if (url.includes('/api/taskforce/tasks')) {
4565
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
4566
+ }
4567
+ if (url.includes('/api/taskforce/archive')) {
4568
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ archived: [] }) });
4569
+ }
4570
+ if (url.includes('/api/taskforce/categories')) {
4571
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(mockCategories) });
4572
+ }
4573
+ if (url.includes('/api/taskforce/global-settings')) {
4574
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
4575
+ }
4576
+ if (url.includes('/api/taskforce/data-version')) {
4577
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ dataVersion: 1 }) });
4578
+ }
4579
+ if (url.includes('/api/taskforce/types'))
4580
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ types: [] }) });
4581
+ if (url.includes('/api/taskforce/priorities'))
4582
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ priorities: [] }) });
4583
+ if (url.includes('/api/taskforce/approaches'))
4584
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ approaches: [] }) });
4585
+ if (url.includes('/api/taskforce/specialists'))
4586
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
4587
+ if (url.includes('/api/taskforce/workflow-templates'))
4588
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
4589
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
4590
+ });
4591
+ vi.stubGlobal('fetch', fetchMock);
4592
+ render(_jsx(MemoryRouter, { initialEntries: ['/setup?step=workspace&source=cloud'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
4593
+ await waitFor(() => {
4594
+ expect(screen.getByText('Test Project')).toBeInTheDocument();
4595
+ });
4596
+ await waitFor(() => {
4597
+ expect(uiStatePosts.some((entry) => entry?.stateKey === 'workspace-sync' && entry?.patch?.setupIntent === null)).toBe(true);
4598
+ });
4599
+ });
3893
4600
  it('shows sign-in and register actions on signed-out cloud workspace setup and disables connect', async () => {
3894
4601
  const fetchMock = vi.fn((url) => {
3895
4602
  if (url.includes('/api/taskforce/auth/session')) {
@@ -4214,10 +4921,11 @@ describe('TaskforceCore Integration', () => {
4214
4921
  expect(screen.getByRole('checkbox', { name: /sync to cloud after setup/i })).toBeInTheDocument();
4215
4922
  });
4216
4923
  await user.click(screen.getByRole('checkbox', { name: /sync to cloud after setup/i }));
4217
- const nameInput = screen.getByPlaceholderText('Project name');
4924
+ const nameInput = screen.getByPlaceholderText('Workspace name');
4218
4925
  await user.clear(nameInput);
4219
4926
  await user.type(nameInput, 'Local Project');
4220
- await user.click(screen.getByRole('button', { name: 'Save Project Setup' }));
4927
+ await user.click(screen.getByRole('button', { name: 'Continue' }));
4928
+ await user.click(screen.getByRole('button', { name: 'Create Workspace' }));
4221
4929
  await waitFor(() => {
4222
4930
  expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
4223
4931
  });
@@ -4231,10 +4939,11 @@ describe('TaskforceCore Integration', () => {
4231
4939
  return body?.stateKey === 'workspace-sync';
4232
4940
  });
4233
4941
  const workspaceSyncStateBody = JSON.parse(String(workspaceSyncStateCall?.[1]?.body || '{}'));
4234
- expect(workspaceSyncStateBody.patch?.cloudSyncEnabled).toBe(true);
4235
- expect(workspaceSyncStateBody.patch?.sourceOfTruth).toBe('local');
4236
- // I6: onboarding now starts as false; the orchestrator marks it true after first pull.
4237
- expect(workspaceSyncStateBody.patch?.onboardingCompleted).toBe(false);
4942
+ expect(workspaceSyncStateBody.patch?.version).toBe(2);
4943
+ expect(workspaceSyncStateBody.patch?.enabled).toBe(true);
4944
+ expect(workspaceSyncStateBody.patch?.phase).toBe('provision-local');
4945
+ expect(workspaceSyncStateBody.patch?.setupIntent).toBeNull();
4946
+ expect(workspaceSyncStateBody.patch?.pullCursor).toBeNull();
4238
4947
  });
4239
4948
  it('creates a new cloud workspace only after guided setup submit', async () => {
4240
4949
  const user = userEvent.setup();