@taskforcehq/taskforce 0.3.301 → 0.3.303

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 (171) hide show
  1. package/dist/Taskforce.module.css +37 -2
  2. package/dist/TaskforceCore.filter-persistence.test.js +1 -1
  3. package/dist/TaskforceCore.js +72 -33
  4. package/dist/TaskforceCore.test.js +848 -130
  5. package/dist/components/features/TaskSettings.js +14 -82
  6. package/dist/components/features/TaskSettings.test.js +33 -70
  7. package/dist/components/features/settings/TaxonomyLibraryManager.js +8 -2
  8. package/dist/components/features/settings/TaxonomyLibraryManager.test.js +1 -1
  9. package/dist/components/task/TaskActionHeader.js +6 -3
  10. package/dist/components/task/TaskCard.js +6 -3
  11. package/dist/components/task/TaskForm.js +2 -2
  12. package/dist/components/task/TaskForm.test.js +41 -2
  13. package/dist/components/task/TaskKanban.test.js +17 -0
  14. package/dist/components/ui/ReferenceBadgeButton.js +1 -1
  15. package/dist/components/views/PlansPage.d.ts +3 -1
  16. package/dist/components/views/PlansPage.js +109 -49
  17. package/dist/components/views/PlansPage.test.d.ts +1 -0
  18. package/dist/components/views/PlansPage.test.js +100 -0
  19. package/dist/components/views/StandaloneLayout.d.ts +1 -0
  20. package/dist/components/views/StandaloneLayout.js +473 -256
  21. package/dist/components/views/panels/PlanningDrawer.js +18 -1
  22. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +48 -0
  23. package/dist/components/views/standalone/modals/AccountHubModal.js +36 -0
  24. package/dist/components/views/standalone/modals/AccountHubModal.test.d.ts +1 -0
  25. package/dist/components/views/standalone/modals/AccountHubModal.test.js +26 -0
  26. package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.d.ts +9 -0
  27. package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.js +8 -0
  28. package/dist/components/views/standalone/modals/EditProfileModal.d.ts +26 -0
  29. package/dist/components/views/standalone/modals/EditProfileModal.js +10 -0
  30. package/dist/components/views/standalone/modals/HelpModal.d.ts +9 -0
  31. package/dist/components/views/standalone/modals/HelpModal.js +8 -0
  32. package/dist/components/views/standalone/modals/ScheduleWarningModal.d.ts +13 -0
  33. package/dist/components/views/standalone/modals/ScheduleWarningModal.js +6 -0
  34. package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +61 -0
  35. package/dist/components/views/standalone/modals/SyncStatusModal.js +38 -0
  36. package/dist/components/views/standalone/modals/TeamManagementModal.d.ts +64 -0
  37. package/dist/components/views/standalone/modals/TeamManagementModal.js +10 -0
  38. package/dist/components/views/standalone/modals/types.d.ts +2 -0
  39. package/dist/components/views/standalone/modals/types.js +1 -0
  40. package/dist/components/views/teamManagementAccess.d.ts +1 -1
  41. package/dist/components/views/teamManagementAccess.js +2 -2
  42. package/dist/components/views/teamManagementAccess.test.js +3 -3
  43. package/dist/config/deployment.js +2 -2
  44. package/dist/config/deployment.test.js +5 -3
  45. package/dist/core/AiProfileService.js +7 -2
  46. package/dist/core/AiProfiles.test.js +19 -0
  47. package/dist/core/EntitlementsPolicy.test.js +2 -2
  48. package/dist/core/GlobalSettingsService.js +78 -18
  49. package/dist/core/PlanEntitlementService.d.ts +29 -5
  50. package/dist/core/PlanEntitlementService.js +297 -219
  51. package/dist/core/SignupMonetizationSettings.test.js +173 -49
  52. package/dist/core/SyncReconciliationService.d.ts +2 -0
  53. package/dist/core/SyncReconciliationService.js +3 -0
  54. package/dist/core/SystemAdmin.test.js +186 -96
  55. package/dist/core/TaskAuditTimeline.test.js +40 -0
  56. package/dist/core/Taskforce.d.ts +100 -10
  57. package/dist/core/Taskforce.ids.test.js +133 -0
  58. package/dist/core/Taskforce.js +459 -127
  59. package/dist/core/Taskforce.mcpAuth.test.js +0 -2
  60. package/dist/core/UserProfileAvatarDrafts.test.d.ts +1 -0
  61. package/dist/core/UserProfileAvatarDrafts.test.js +111 -0
  62. package/dist/core/taskReferencePolicy.d.ts +28 -0
  63. package/dist/core/taskReferencePolicy.js +189 -0
  64. package/dist/core/types.d.ts +13 -12
  65. package/dist/hooks/tasks/useTaskFormActions.js +3 -0
  66. package/dist/hooks/useSyncOrchestrator.js +17 -3
  67. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +97 -5
  68. package/dist/hooks/useTaskData.js +3 -2
  69. package/dist/hooks/useTaskData.test.js +66 -0
  70. package/dist/hooks/useTaskforce.d.ts +8 -0
  71. package/dist/hooks/useTaskforce.js +72 -19
  72. package/dist/hooks/useTaskforce.sync-behavior.test.js +185 -6
  73. package/dist/mcp/aiProfileBootstrap.d.ts +29 -0
  74. package/dist/mcp/aiProfileBootstrap.js +80 -0
  75. package/dist/mcp/aiProfileBootstrap.test.d.ts +1 -0
  76. package/dist/mcp/aiProfileBootstrap.test.js +68 -0
  77. package/dist/mcp/clientIntegrations.d.ts +2 -8
  78. package/dist/mcp/clientIntegrations.js +21 -40
  79. package/dist/mcp/clientIntegrations.test.js +28 -2
  80. package/dist/mcp/clientProfileDefaults.d.ts +12 -0
  81. package/dist/mcp/clientProfileDefaults.js +37 -0
  82. package/dist/mcp/runtime.d.ts +10 -7
  83. package/dist/mcp/runtime.js +131 -48
  84. package/dist/mcp/runtime.test.js +321 -83
  85. package/dist/migrations/billingSchemaParity.test.js +3 -0
  86. package/dist/migrations/taskSchemaMigrations.js +68 -1
  87. package/dist/qaOpen.test.d.ts +1 -0
  88. package/dist/qaOpen.test.js +56 -0
  89. package/dist/server/index.cookieProxy.test.js +2 -0
  90. package/dist/server/index.d.ts +1 -0
  91. package/dist/server/index.js +17 -1
  92. package/dist/server/index.test.js +15 -1
  93. package/dist/server/mockStripe.d.ts +1 -0
  94. package/dist/server/mockStripe.js +2 -0
  95. package/dist/server/routes/admin.js +105 -45
  96. package/dist/server/routes/auth.d.ts +6 -0
  97. package/dist/server/routes/auth.js +505 -30
  98. package/dist/server/routes/billing.js +517 -24
  99. package/dist/server/routes/billing.test.js +967 -45
  100. package/dist/server/routes/documents.js +6 -1
  101. package/dist/server/routes/shared.d.ts +1 -1
  102. package/dist/server/routes/shared.js +16 -6
  103. package/dist/server/routes/sync.integration.test.js +290 -0
  104. package/dist/server/routes/sync.js +234 -1922
  105. package/dist/server/routes/tasks.js +18 -4
  106. package/dist/server/routes.js +29 -22
  107. package/dist/server/routes.test.js +639 -82
  108. package/dist/shared/runtimeContract.d.ts +1 -1
  109. package/dist/sync/cloudSyncApi.d.ts +1 -0
  110. package/dist/sync/collaborationSyncPayload.d.ts +17 -0
  111. package/dist/sync/collaborationSyncPayload.js +131 -0
  112. package/dist/sync/collaborationSyncPayload.test.d.ts +1 -0
  113. package/dist/sync/collaborationSyncPayload.test.js +104 -0
  114. package/dist/sync/collaborationSyncState.d.ts +15 -0
  115. package/dist/sync/collaborationSyncState.js +60 -0
  116. package/dist/sync/contentSyncPayload.d.ts +45 -0
  117. package/dist/sync/contentSyncPayload.js +104 -0
  118. package/dist/sync/contentSyncPayload.test.d.ts +1 -0
  119. package/dist/sync/contentSyncPayload.test.js +104 -0
  120. package/dist/sync/contentSyncState.d.ts +98 -0
  121. package/dist/sync/contentSyncState.js +1004 -0
  122. package/dist/sync/planningSyncPayload.d.ts +9 -0
  123. package/dist/sync/planningSyncPayload.js +82 -0
  124. package/dist/sync/planningSyncPayload.test.d.ts +1 -0
  125. package/dist/sync/planningSyncPayload.test.js +68 -0
  126. package/dist/sync/syncApplyHandlers.d.ts +83 -0
  127. package/dist/sync/syncApplyHandlers.js +445 -0
  128. package/dist/sync/syncService.d.ts +1 -0
  129. package/dist/sync/syncService.js +21 -8
  130. package/dist/sync/taskSyncPayload.d.ts +33 -0
  131. package/dist/sync/taskSyncPayload.js +126 -0
  132. package/dist/sync/taskSyncPayload.test.d.ts +1 -0
  133. package/dist/sync/taskSyncPayload.test.js +54 -0
  134. package/dist/sync/workspacePullFeed.d.ts +38 -0
  135. package/dist/sync/workspacePullFeed.js +349 -0
  136. package/dist/sync/workspacePullFeed.test.d.ts +1 -0
  137. package/dist/sync/workspacePullFeed.test.js +278 -0
  138. package/dist/sync/workspaceSyncModel.d.ts +4 -1
  139. package/dist/sync/workspaceSyncModel.js +9 -2
  140. package/dist/sync/workspaceSyncModel.test.js +29 -0
  141. package/dist/test/setup.js +1 -1
  142. package/dist/types.d.ts +1 -0
  143. package/dist/ui/assets/{AgentsModule-Bdq3T1Ts.js → AgentsModule-CpsTmrIW.js} +1 -1
  144. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-jKUVo-PJ.js → AnnotatedAttachmentWorkspace-C_TmXZwA.js} +1 -1
  145. package/dist/ui/assets/{DocumentWorkspace-DuQa-uIC.js → DocumentWorkspace-C3GyzrWm.js} +2 -2
  146. package/dist/ui/assets/{InitiativesModule-C8Pi2VTk.js → InitiativesModule-4-Rh2K2n.js} +1 -1
  147. package/dist/ui/assets/PlansPage-BP7AYOqr.js +1 -0
  148. package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +8 -0
  149. package/dist/ui/assets/{TaskSettings-BwB9NtmJ.css → TaskSettings-CnIBL_Eb.css} +1 -1
  150. package/dist/ui/assets/{WorkflowsModule-b6TLuHwJ.js → WorkflowsModule-CGk9s3NJ.js} +1 -1
  151. package/dist/ui/assets/index-CLIMgR6g.js +6 -0
  152. package/dist/ui/assets/index-DneETxcu.css +1 -0
  153. package/dist/ui/assets/{vendor-icons-QZyhEwgT.js → vendor-icons-pWocEipT.js} +1 -1
  154. package/dist/ui/index.html +3 -3
  155. package/dist/utils/accountProfileSummaryCache.d.ts +31 -0
  156. package/dist/utils/accountProfileSummaryCache.js +97 -0
  157. package/dist/utils/accountProfileSummaryCache.test.d.ts +1 -0
  158. package/dist/utils/accountProfileSummaryCache.test.js +63 -0
  159. package/dist/utils/taskActivity.js +17 -4
  160. package/dist/utils/taskActivity.test.js +27 -0
  161. package/dist/utils/taskNormalization.js +10 -2
  162. package/dist/utils/taskReferences.d.ts +11 -1
  163. package/dist/utils/taskReferences.js +33 -1
  164. package/package.json +4 -3
  165. package/scripts/qa-open-lib.mjs +175 -0
  166. package/scripts/qa-open-lib.mjs.d.ts +49 -0
  167. package/scripts/qa-open.mjs +352 -0
  168. package/dist/ui/assets/PlansPage-B6BDECSi.js +0 -1
  169. package/dist/ui/assets/TaskSettings-DUJgRTPU.js +0 -8
  170. package/dist/ui/assets/index-BiiF1cLQ.css +0 -1
  171. package/dist/ui/assets/index-xBWLPOEb.js +0 -6
@@ -48,9 +48,10 @@ function jsonResponse(body, status = 200) {
48
48
  describe('TaskforceCore Integration', () => {
49
49
  beforeEach(() => {
50
50
  localStorage.clear();
51
+ sessionStorage.clear();
51
52
  vi.stubGlobal('fetch', vi.fn((url) => {
52
53
  if (url.includes('/api/taskforce/auth/runtime-config')) {
53
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'local' } }) });
54
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'cloud' } }) });
54
55
  }
55
56
  if (url.includes('/api/taskforce/ui-state?key=app')) {
56
57
  return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true, state: {} }) });
@@ -168,7 +169,7 @@ describe('TaskforceCore Integration', () => {
168
169
  it('shows the header sync badge as off when local runtime is signed out', async () => {
169
170
  vi.stubGlobal('fetch', vi.fn((url) => {
170
171
  if (url.includes('/api/taskforce/auth/runtime-config')) {
171
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'local' } }) });
172
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'cloud' } }) });
172
173
  }
173
174
  if (url.includes('/api/taskforce/ui-state?key=app')) {
174
175
  return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true, state: {} }) });
@@ -1037,7 +1038,7 @@ describe('TaskforceCore Integration', () => {
1037
1038
  })
1038
1039
  });
1039
1040
  }
1040
- if (url.includes('/api/taskforce/billing/status')) {
1041
+ if (url.includes('/api/taskforce/account/profile-summary')) {
1041
1042
  return Promise.resolve({
1042
1043
  ok: true,
1043
1044
  json: () => Promise.resolve({
@@ -1047,7 +1048,7 @@ describe('TaskforceCore Integration', () => {
1047
1048
  })
1048
1049
  });
1049
1050
  }
1050
- if (url.includes('/api/taskforce/billing/status')) {
1051
+ if (url.includes('/api/taskforce/account/profile-summary')) {
1051
1052
  return Promise.resolve({
1052
1053
  ok: true,
1053
1054
  json: () => Promise.resolve({
@@ -1143,7 +1144,7 @@ describe('TaskforceCore Integration', () => {
1143
1144
  hasEntitlement: false,
1144
1145
  canAccessApp: false,
1145
1146
  canAccessPlans: true,
1146
- message: 'Signup monetization is misconfigured.'
1147
+ message: 'Onboarding policy is misconfigured.'
1147
1148
  })
1148
1149
  });
1149
1150
  }
@@ -1196,7 +1197,7 @@ describe('TaskforceCore Integration', () => {
1196
1197
  render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, {}) }));
1197
1198
  await waitFor(() => {
1198
1199
  expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
1199
- expect(screen.getByText('Signup monetization is misconfigured. Contact support or a system administrator.')).toBeInTheDocument();
1200
+ expect(screen.getByText('Onboarding policy is misconfigured. Contact support or a system administrator.')).toBeInTheDocument();
1200
1201
  });
1201
1202
  });
1202
1203
  it('suppresses stale missing-entitlement gate messaging when billing status is already active', async () => {
@@ -1287,7 +1288,7 @@ describe('TaskforceCore Integration', () => {
1287
1288
  })
1288
1289
  });
1289
1290
  }
1290
- if (url.includes('/api/taskforce/billing/status')) {
1291
+ if (url.includes('/api/taskforce/account/profile-summary')) {
1291
1292
  return Promise.resolve({
1292
1293
  ok: true,
1293
1294
  json: () => Promise.resolve({
@@ -1388,7 +1389,7 @@ describe('TaskforceCore Integration', () => {
1388
1389
  })
1389
1390
  });
1390
1391
  }
1391
- if (url.includes('/api/taskforce/billing/status')) {
1392
+ if (url.includes('/api/taskforce/account/profile-summary')) {
1392
1393
  return Promise.resolve({
1393
1394
  ok: true,
1394
1395
  json: () => Promise.resolve({
@@ -1445,7 +1446,7 @@ describe('TaskforceCore Integration', () => {
1445
1446
  await waitFor(() => {
1446
1447
  expect(screen.queryByText(/Signed in as/i)).not.toBeInTheDocument();
1447
1448
  expect(screen.getByText('member@example.com')).toBeInTheDocument();
1448
- expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('/api/taskforce/billing/status'))).toBe(true);
1449
+ expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('/api/taskforce/account/profile-summary'))).toBe(true);
1449
1450
  });
1450
1451
  await waitFor(() => {
1451
1452
  expect(screen.getByText('Subscription')).toBeInTheDocument();
@@ -1507,7 +1508,7 @@ describe('TaskforceCore Integration', () => {
1507
1508
  })
1508
1509
  });
1509
1510
  }
1510
- if (url.includes('/api/taskforce/billing/status')) {
1511
+ if (url.includes('/api/taskforce/account/profile-summary')) {
1511
1512
  return Promise.resolve({
1512
1513
  ok: true,
1513
1514
  json: () => Promise.resolve({
@@ -1569,7 +1570,293 @@ describe('TaskforceCore Integration', () => {
1569
1570
  expect(screen.getByText('CLOUD.EXAMPLE.COM')).toBeInTheDocument();
1570
1571
  });
1571
1572
  }, 10000);
1572
- it('allows local runtime to open shared /login and register flow', async () => {
1573
+ it('preserves the original user billing and team access after registering and switching back in local runtime', async () => {
1574
+ const user = userEvent.setup();
1575
+ const accounts = {
1576
+ ross: {
1577
+ email: 'ross.burke@gmail.com',
1578
+ userId: 'user-ross',
1579
+ displayName: 'Ross Burke',
1580
+ workspaceId: 'workspace-local-active',
1581
+ planId: 'team-5',
1582
+ planVersionId: 'team-5-v1',
1583
+ planName: 'Team 5',
1584
+ teamManagementAllowed: true,
1585
+ teamPlanMode: 'team',
1586
+ workspaceRole: 'owner'
1587
+ },
1588
+ newcomer: {
1589
+ email: 'new.user@example.com',
1590
+ userId: 'user-new',
1591
+ displayName: 'New User',
1592
+ workspaceId: 'workspace-local-active',
1593
+ planId: 'starter',
1594
+ planVersionId: 'starter-v1',
1595
+ planName: 'Starter',
1596
+ teamManagementAllowed: false,
1597
+ teamPlanMode: 'personal',
1598
+ workspaceRole: 'member'
1599
+ }
1600
+ };
1601
+ let currentAccountKey = 'ross';
1602
+ const currentAccount = () => (currentAccountKey ? accounts[currentAccountKey] : null);
1603
+ const fetchMock = vi.fn((input, init) => {
1604
+ const url = String(input);
1605
+ const account = currentAccount();
1606
+ if (url.includes('/api/taskforce/auth/runtime-config')) {
1607
+ return jsonResponse({
1608
+ success: true,
1609
+ config: {
1610
+ baseUrl: 'https://cloud.example.com',
1611
+ apiBaseUrl: 'https://cloud.example.com',
1612
+ cloudAuthBaseUrl: 'https://cloud.example.com',
1613
+ runtimeMode: 'local',
1614
+ authSource: 'cloud',
1615
+ workspaceMode: 'single-local',
1616
+ workspaceSwitchingEnabled: false,
1617
+ cloudAuthViaLocalProxy: false
1618
+ }
1619
+ });
1620
+ }
1621
+ if (url.includes('/api/taskforce/auth/session')) {
1622
+ return jsonResponse({
1623
+ authenticated: Boolean(account),
1624
+ runtimeMode: 'local',
1625
+ authRequiredForApi: false,
1626
+ email: account?.email || '',
1627
+ displayName: account?.displayName || '',
1628
+ userId: account?.userId || 'anonymous',
1629
+ workspaceId: account?.workspaceId || '',
1630
+ betaAccess: true
1631
+ });
1632
+ }
1633
+ if (url.includes('/api/taskforce/auth/logout')) {
1634
+ currentAccountKey = null;
1635
+ return jsonResponse({ success: true });
1636
+ }
1637
+ if (url.includes('/api/taskforce/auth/register')) {
1638
+ currentAccountKey = 'newcomer';
1639
+ return jsonResponse({
1640
+ success: true,
1641
+ workspaceSetupRequired: false,
1642
+ verificationRequired: false
1643
+ });
1644
+ }
1645
+ if (url.includes('/api/taskforce/auth/login')) {
1646
+ const body = typeof init?.body === 'string' ? JSON.parse(init.body) : {};
1647
+ currentAccountKey = String(body?.email || '').trim().toLowerCase() === accounts.ross.email
1648
+ ? 'ross'
1649
+ : 'newcomer';
1650
+ return jsonResponse({ success: true });
1651
+ }
1652
+ if (url.includes('/api/taskforce/account/profile-summary')) {
1653
+ return jsonResponse(account
1654
+ ? {
1655
+ planId: account.planId,
1656
+ planVersionId: account.planVersionId,
1657
+ planName: account.planName,
1658
+ entitlementState: 'active',
1659
+ workspaceId: account.workspaceId,
1660
+ workspaceRole: account.workspaceRole,
1661
+ canManageWorkspace: account.workspaceRole === 'owner' || account.workspaceRole === 'admin',
1662
+ teamPlanMode: account.teamPlanMode,
1663
+ teamManagementAllowed: account.teamManagementAllowed
1664
+ }
1665
+ : {
1666
+ planId: null,
1667
+ planVersionId: null,
1668
+ planName: null,
1669
+ entitlementState: null,
1670
+ workspaceId: 'workspace-local-active',
1671
+ workspaceRole: null,
1672
+ canManageWorkspace: false,
1673
+ teamPlanMode: 'unknown',
1674
+ teamManagementAllowed: false
1675
+ });
1676
+ }
1677
+ if (url.includes('/api/taskforce/auth/workspace-members')) {
1678
+ return jsonResponse({
1679
+ users: account ? [{
1680
+ userId: account.userId,
1681
+ email: account.email,
1682
+ displayName: account.displayName,
1683
+ role: account.workspaceRole,
1684
+ status: 'active',
1685
+ disabled: false
1686
+ }] : []
1687
+ });
1688
+ }
1689
+ if (url.includes('/api/taskforce/public/pricing')) {
1690
+ return jsonResponse({
1691
+ plans: [
1692
+ {
1693
+ planId: 'starter',
1694
+ displayName: 'Starter',
1695
+ status: 'active',
1696
+ defaultVersion: {
1697
+ planVersionId: 'starter-v1',
1698
+ versionNumber: 1,
1699
+ seatLimit: null,
1700
+ metadata: {}
1701
+ },
1702
+ features: [],
1703
+ pricing: {
1704
+ month: { pricingType: 'free', stripePriceId: null, unitAmount: 0, currency: null, interval: 'month' },
1705
+ year: null
1706
+ }
1707
+ },
1708
+ {
1709
+ planId: 'team-5',
1710
+ displayName: 'Team 5',
1711
+ status: 'active',
1712
+ defaultVersion: {
1713
+ planVersionId: 'team-5-v1',
1714
+ versionNumber: 1,
1715
+ seatLimit: 5,
1716
+ metadata: {}
1717
+ },
1718
+ features: [],
1719
+ pricing: {
1720
+ month: { pricingType: 'stripe', stripePriceId: 'price_team_5_month', unitAmount: 5000, currency: 'usd', interval: 'month' },
1721
+ year: { pricingType: 'stripe', stripePriceId: 'price_team_5_year', unitAmount: 50000, currency: 'usd', interval: 'year' }
1722
+ }
1723
+ }
1724
+ ]
1725
+ });
1726
+ }
1727
+ if (url.includes('/api/taskforce/ui-state?key=app')) {
1728
+ return jsonResponse({ success: true, state: {} });
1729
+ }
1730
+ if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
1731
+ return jsonResponse({
1732
+ success: true,
1733
+ state: {
1734
+ version: 2,
1735
+ enabled: false,
1736
+ phase: 'idle',
1737
+ pullCursor: null,
1738
+ lastPullAt: null,
1739
+ lastPushAt: null,
1740
+ lastSyncedAt: null
1741
+ }
1742
+ });
1743
+ }
1744
+ if (url.includes('/api/taskforce/config')) {
1745
+ return jsonResponse({
1746
+ ...mockConfig,
1747
+ runtimeMode: 'local',
1748
+ authRequiredForApi: false,
1749
+ workspaceId: 'workspace-local-active',
1750
+ setupState: {
1751
+ globalSetupState: 'present',
1752
+ workspaceSetupState: 'present',
1753
+ runtimeMode: 'local',
1754
+ workspaceId: 'workspace-local-active',
1755
+ mode: 'core',
1756
+ forceSetup: false,
1757
+ forceGlobalSetup: false,
1758
+ forceWorkspaceSetup: false
1759
+ }
1760
+ });
1761
+ }
1762
+ if (url.includes('/api/taskforce/tasks')) {
1763
+ return jsonResponse({ tasks: mockTasks });
1764
+ }
1765
+ if (url.includes('/api/taskforce/archive')) {
1766
+ return jsonResponse({ archived: [] });
1767
+ }
1768
+ if (url.includes('/api/taskforce/categories')) {
1769
+ return jsonResponse(mockCategories);
1770
+ }
1771
+ if (url.includes('/api/taskforce/types')) {
1772
+ return jsonResponse({ types: [] });
1773
+ }
1774
+ if (url.includes('/api/taskforce/priorities')) {
1775
+ return jsonResponse({ priorities: [] });
1776
+ }
1777
+ if (url.includes('/api/taskforce/approaches')) {
1778
+ return jsonResponse({ approaches: [] });
1779
+ }
1780
+ if (url.includes('/api/taskforce/specialists')) {
1781
+ return jsonResponse({ specialists: [] });
1782
+ }
1783
+ if (url.includes('/api/taskforce/workflow-templates')) {
1784
+ return jsonResponse({ templates: [] });
1785
+ }
1786
+ if (url.includes('/api/taskforce/workspace/assignee-options')) {
1787
+ return jsonResponse({ assignees: [] });
1788
+ }
1789
+ if (url.includes('/api/taskforce/initiatives')) {
1790
+ return jsonResponse([]);
1791
+ }
1792
+ if (url.includes('/api/taskforce/workstreams')) {
1793
+ return jsonResponse([]);
1794
+ }
1795
+ return jsonResponse({});
1796
+ });
1797
+ vi.stubGlobal('fetch', fetchMock);
1798
+ render(_jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }));
1799
+ await waitFor(() => {
1800
+ expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
1801
+ });
1802
+ await user.click(screen.getByTitle(/Account/i));
1803
+ await waitFor(() => {
1804
+ expect(screen.getByText(accounts.ross.email)).toBeInTheDocument();
1805
+ expect(screen.getByText('Team 5')).toBeInTheDocument();
1806
+ expect(screen.getByRole('menuitem', { name: 'Team Management' })).toBeInTheDocument();
1807
+ });
1808
+ await user.click(screen.getByRole('menuitem', { name: 'Sign Out' }));
1809
+ await waitFor(() => {
1810
+ expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
1811
+ });
1812
+ await user.click(screen.getByTitle(/Account/i));
1813
+ await user.click(await screen.findByRole('menuitem', { name: 'Sign In / Register' }));
1814
+ await waitFor(() => {
1815
+ expect(screen.getByRole('button', { name: 'Register' })).toBeInTheDocument();
1816
+ });
1817
+ await user.click(screen.getByRole('button', { name: 'Register' }));
1818
+ await user.type(await screen.findByPlaceholderText('Email'), accounts.newcomer.email);
1819
+ await user.type(screen.getByPlaceholderText('Display name'), accounts.newcomer.displayName);
1820
+ await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
1821
+ await user.click(screen.getByRole('button', { name: 'Create Account' }));
1822
+ await waitFor(() => {
1823
+ expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
1824
+ });
1825
+ await user.click(screen.getByTitle(/Account/i));
1826
+ await waitFor(() => {
1827
+ expect(screen.getByText(accounts.newcomer.email)).toBeInTheDocument();
1828
+ expect(screen.getByText('Starter')).toBeInTheDocument();
1829
+ });
1830
+ await user.click(screen.getByRole('menuitem', { name: 'Sign Out' }));
1831
+ await waitFor(() => {
1832
+ expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
1833
+ });
1834
+ await user.click(screen.getByTitle(/Account/i));
1835
+ await user.click(await screen.findByRole('menuitem', { name: 'Sign In / Register' }));
1836
+ await waitFor(() => {
1837
+ expect(screen.getByText('Sign in with your account credentials.')).toBeInTheDocument();
1838
+ });
1839
+ await user.type(await screen.findByPlaceholderText('Email'), accounts.ross.email);
1840
+ await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
1841
+ await user.click(screen.getByRole('button', { name: 'Sign In' }));
1842
+ await waitFor(() => {
1843
+ expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
1844
+ });
1845
+ await user.click(screen.getByTitle(/Account/i));
1846
+ await waitFor(() => {
1847
+ expect(screen.getByText(accounts.ross.email)).toBeInTheDocument();
1848
+ expect(screen.getByText('Team 5')).toBeInTheDocument();
1849
+ expect(screen.queryByText('Starter')).not.toBeInTheDocument();
1850
+ expect(screen.getByRole('menuitem', { name: 'Team Management' })).toBeInTheDocument();
1851
+ });
1852
+ await user.click(screen.getByRole('button', { name: /Subscription Team 5/i }));
1853
+ await waitFor(() => {
1854
+ expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
1855
+ });
1856
+ const currentPlanCard = screen.getByTestId('plan-card-team-5');
1857
+ expect(within(currentPlanCard).getByTestId('plan-manage-team-5')).toHaveTextContent('Current Plan');
1858
+ });
1859
+ it('allows local runtime to open the shared cloud-backed register route', async () => {
1573
1860
  const fetchMock = vi.fn((url) => {
1574
1861
  if (url.includes('/api/taskforce/auth/session')) {
1575
1862
  return Promise.resolve({
@@ -1619,14 +1906,16 @@ describe('TaskforceCore Integration', () => {
1619
1906
  });
1620
1907
  expect(container.querySelector('[aria-pressed]')).not.toBeInTheDocument();
1621
1908
  });
1622
- it('shows debug registration autofill only when debug mode is enabled', async () => {
1623
- const fetchMock = vi.fn((url) => {
1909
+ it('does not switch to verify mode after registration when verification is not required', async () => {
1910
+ const user = userEvent.setup();
1911
+ const fetchMock = vi.fn((input) => {
1912
+ const url = String(input);
1624
1913
  if (url.includes('/api/taskforce/auth/session')) {
1625
1914
  return Promise.resolve({
1626
1915
  ok: true,
1627
1916
  json: () => Promise.resolve({
1628
1917
  authenticated: false,
1629
- runtimeMode: 'local',
1918
+ runtimeMode: 'cloud',
1630
1919
  authRequiredForApi: false
1631
1920
  })
1632
1921
  });
@@ -1636,12 +1925,12 @@ describe('TaskforceCore Integration', () => {
1636
1925
  ok: true,
1637
1926
  json: () => Promise.resolve({
1638
1927
  ...mockConfig,
1639
- runtimeMode: 'local',
1928
+ runtimeMode: 'cloud',
1640
1929
  authRequiredForApi: false,
1641
1930
  setupState: {
1642
1931
  globalSetupState: 'present',
1643
1932
  workspaceSetupState: 'present',
1644
- runtimeMode: 'local',
1933
+ runtimeMode: 'cloud',
1645
1934
  workspaceId: 'workspace-local-active',
1646
1935
  mode: 'core',
1647
1936
  forceSetup: false,
@@ -1651,99 +1940,373 @@ describe('TaskforceCore Integration', () => {
1651
1940
  })
1652
1941
  });
1653
1942
  }
1654
- if (url.includes('/api/taskforce/workflow-templates')) {
1655
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
1943
+ if (url.includes('/api/taskforce/public/pricing')) {
1944
+ return Promise.resolve({
1945
+ ok: true,
1946
+ json: () => Promise.resolve({
1947
+ plans: []
1948
+ })
1949
+ });
1950
+ }
1951
+ if (url.includes('/api/taskforce/auth/register')) {
1952
+ return Promise.resolve({
1953
+ ok: true,
1954
+ json: () => Promise.resolve({
1955
+ success: true,
1956
+ workspaceSetupRequired: false,
1957
+ verificationRequired: false
1958
+ })
1959
+ });
1960
+ }
1961
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
1962
+ });
1963
+ vi.stubGlobal('fetch', fetchMock);
1964
+ render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2Fplans'], children: _jsx(TaskforceCore, {}) }));
1965
+ await user.type(await screen.findByPlaceholderText('Email'), 'local@example.com');
1966
+ await user.type(screen.getByPlaceholderText('Display name'), 'Local User');
1967
+ await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
1968
+ await user.click(screen.getByRole('button', { name: 'Create Account' }));
1969
+ await waitFor(() => {
1970
+ expect(screen.getByText('Subscription Plans')).toBeInTheDocument();
1971
+ });
1972
+ expect(screen.queryByPlaceholderText('Verification token')).not.toBeInTheDocument();
1973
+ expect(screen.queryByRole('button', { name: 'Create Account' })).not.toBeInTheDocument();
1974
+ });
1975
+ it('routes successful registration into plan selection when onboarding leaves the account pending plan selection', async () => {
1976
+ const user = userEvent.setup();
1977
+ let authenticated = false;
1978
+ const fetchMock = vi.fn((input) => {
1979
+ const url = String(input);
1980
+ if (url.includes('/api/taskforce/auth/runtime-config')) {
1981
+ return jsonResponse({
1982
+ config: {
1983
+ baseUrl: 'https://app.taskforce.example',
1984
+ apiBaseUrl: 'https://app.taskforce.example/api',
1985
+ cloudAuthBaseUrl: 'https://app.taskforce.example',
1986
+ runtimeMode: 'cloud',
1987
+ authSource: 'cloud',
1988
+ workspaceMode: 'multi-cloud',
1989
+ workspaceSwitchingEnabled: true,
1990
+ authRequiredForApi: true
1991
+ }
1992
+ });
1993
+ }
1994
+ if (url.includes('/api/taskforce/auth/session')) {
1995
+ return jsonResponse({
1996
+ authenticated,
1997
+ runtimeMode: 'cloud',
1998
+ authRequiredForApi: true,
1999
+ userId: authenticated ? 'user-1' : null,
2000
+ workspaceId: authenticated ? 'workspace-1' : null,
2001
+ betaAccess: true
2002
+ });
2003
+ }
2004
+ if (url.includes('/api/taskforce/config')) {
2005
+ return jsonResponse({
2006
+ ...mockConfig,
2007
+ runtimeMode: 'cloud',
2008
+ authRequiredForApi: true,
2009
+ workspaceSwitchingEnabled: true,
2010
+ setupState: {
2011
+ globalSetupState: 'present',
2012
+ workspaceSetupState: 'present',
2013
+ runtimeMode: 'cloud',
2014
+ workspaceId: 'workspace-1',
2015
+ mode: 'core',
2016
+ forceSetup: false,
2017
+ forceGlobalSetup: false,
2018
+ forceWorkspaceSetup: false
2019
+ }
2020
+ });
2021
+ }
2022
+ if (url.includes('/api/taskforce/workspaces')) {
2023
+ return jsonResponse({
2024
+ success: true,
2025
+ workspaces: [{
2026
+ id: 'workspace-1',
2027
+ slug: 'workspace-1',
2028
+ name: 'Workspace 1',
2029
+ description: null,
2030
+ role: 'owner',
2031
+ status: 'active',
2032
+ betaAccess: true
2033
+ }],
2034
+ currentWorkspaceId: 'workspace-1'
2035
+ });
2036
+ }
2037
+ if (url.includes('/api/taskforce/account/access-status')) {
2038
+ return jsonResponse({
2039
+ gate: 'plan_selection_required',
2040
+ hasEntitlement: true,
2041
+ canAccessApp: false,
2042
+ canAccessPlans: true,
2043
+ message: 'Choose a plan to continue.'
2044
+ });
2045
+ }
2046
+ if (url.includes('/api/taskforce/account/profile-summary')) {
2047
+ return jsonResponse({
2048
+ entitlementState: 'pending_plan_selection',
2049
+ gate: 'plan_selection_required',
2050
+ message: 'Choose a plan to continue.',
2051
+ workspaceId: 'workspace-1'
2052
+ });
2053
+ }
2054
+ if (url.includes('/api/taskforce/public/pricing')) {
2055
+ return jsonResponse({
2056
+ plans: [{
2057
+ planId: 'starter',
2058
+ displayName: 'Starter',
2059
+ status: 'active',
2060
+ defaultVersion: {
2061
+ planVersionId: 'starter-v1',
2062
+ versionNumber: 1,
2063
+ seatLimit: null,
2064
+ metadata: {}
2065
+ },
2066
+ features: [],
2067
+ pricing: {
2068
+ month: {
2069
+ planVersionId: 'starter-v1',
2070
+ interval: 'month',
2071
+ pricingType: 'free',
2072
+ stripePriceId: 'free:starter-v1:month',
2073
+ displayAmount: '$0.00',
2074
+ unitAmount: 0,
2075
+ currency: 'usd'
2076
+ },
2077
+ year: null
2078
+ }
2079
+ }]
2080
+ });
2081
+ }
2082
+ if (url.includes('/api/taskforce/auth/register')) {
2083
+ authenticated = true;
2084
+ return jsonResponse({
2085
+ success: true,
2086
+ workspaceSetupRequired: false,
2087
+ verificationRequired: false,
2088
+ planSelectionRequired: true,
2089
+ commercialState: 'pending_plan_selection'
2090
+ });
2091
+ }
2092
+ if (url.includes('/api/taskforce/categories')) {
2093
+ return jsonResponse(mockCategories);
2094
+ }
2095
+ if (url.includes('/api/taskforce/tasks')) {
2096
+ return jsonResponse({ tasks: mockTasks });
2097
+ }
2098
+ if (url.includes('/api/taskforce/archive')) {
2099
+ return jsonResponse({ archived: [] });
2100
+ }
2101
+ if (url.includes('/api/taskforce/data-version')) {
2102
+ return jsonResponse({ dataVersion: 1 });
2103
+ }
2104
+ if (url.includes('/api/taskforce/types')) {
2105
+ return jsonResponse({ types: [] });
2106
+ }
2107
+ if (url.includes('/api/taskforce/priorities')) {
2108
+ return jsonResponse({ priorities: [] });
2109
+ }
2110
+ if (url.includes('/api/taskforce/approaches')) {
2111
+ return jsonResponse({ approaches: [] });
2112
+ }
2113
+ if (url.includes('/api/taskforce/specialists')) {
2114
+ return jsonResponse({ specialists: [] });
2115
+ }
2116
+ if (url.includes('/api/taskforce/workflow-templates')) {
2117
+ return jsonResponse({ templates: [] });
2118
+ }
2119
+ return jsonResponse({});
2120
+ });
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' }));
2127
+ await waitFor(() => {
2128
+ expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
2129
+ expect(screen.getByText('Choose a plan to continue.')).toBeInTheDocument();
2130
+ });
2131
+ expect(fetchMock.mock.calls.some(([url]) => String(url).includes('/api/taskforce/billing/checkout-session'))).toBe(false);
2132
+ });
2133
+ it('routes successful registration into checkout when onboarding leaves the account checkout-pending', async () => {
2134
+ const user = userEvent.setup();
2135
+ let authenticated = false;
2136
+ const fetchMock = vi.fn((input) => {
2137
+ const url = String(input);
2138
+ if (url.includes('/api/taskforce/auth/runtime-config')) {
2139
+ return jsonResponse({
2140
+ config: {
2141
+ baseUrl: 'https://app.taskforce.example',
2142
+ apiBaseUrl: 'https://app.taskforce.example/api',
2143
+ cloudAuthBaseUrl: 'https://app.taskforce.example',
2144
+ runtimeMode: 'cloud',
2145
+ authSource: 'cloud',
2146
+ workspaceMode: 'multi-cloud',
2147
+ workspaceSwitchingEnabled: true,
2148
+ authRequiredForApi: true
2149
+ }
2150
+ });
2151
+ }
2152
+ if (url.includes('/api/taskforce/auth/session')) {
2153
+ return jsonResponse({
2154
+ authenticated,
2155
+ runtimeMode: 'cloud',
2156
+ authRequiredForApi: true,
2157
+ userId: authenticated ? 'user-1' : null,
2158
+ workspaceId: authenticated ? 'workspace-1' : null,
2159
+ betaAccess: true
2160
+ });
2161
+ }
2162
+ if (url.includes('/api/taskforce/config')) {
2163
+ return jsonResponse({
2164
+ ...mockConfig,
2165
+ runtimeMode: 'cloud',
2166
+ authRequiredForApi: true,
2167
+ workspaceSwitchingEnabled: true,
2168
+ setupState: {
2169
+ globalSetupState: 'present',
2170
+ workspaceSetupState: 'present',
2171
+ runtimeMode: 'cloud',
2172
+ workspaceId: 'workspace-1',
2173
+ mode: 'core',
2174
+ forceSetup: false,
2175
+ forceGlobalSetup: false,
2176
+ forceWorkspaceSetup: false
2177
+ }
2178
+ });
2179
+ }
2180
+ if (url.includes('/api/taskforce/workspaces')) {
2181
+ return jsonResponse({
2182
+ success: true,
2183
+ workspaces: [{
2184
+ id: 'workspace-1',
2185
+ slug: 'workspace-1',
2186
+ name: 'Workspace 1',
2187
+ description: null,
2188
+ role: 'owner',
2189
+ status: 'active',
2190
+ betaAccess: true
2191
+ }],
2192
+ currentWorkspaceId: 'workspace-1'
2193
+ });
1656
2194
  }
1657
- return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
1658
- });
1659
- vi.stubGlobal('fetch', fetchMock);
1660
- const firstRender = render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
1661
- await waitFor(() => {
1662
- expect(screen.getByText('Create your Taskforce account.')).toBeInTheDocument();
1663
- });
1664
- expect(screen.queryByRole('button', { name: 'Fill Form' })).not.toBeInTheDocument();
1665
- firstRender.unmount();
1666
- globalThis.__DEBUG_MODE__ = true;
1667
- render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
1668
- expect(await screen.findByRole('button', { name: 'Fill Form' })).toBeInTheDocument();
1669
- });
1670
- it('fills and submits generated debug registration credentials through the normal register flow', async () => {
1671
- globalThis.__DEBUG_MODE__ = true;
1672
- const user = userEvent.setup();
1673
- const nowSpy = vi.spyOn(Date, 'now').mockReturnValue(1735689600000);
1674
- const fetchMock = vi.fn((input, init) => {
1675
- const url = String(input);
1676
- if (url.includes('/api/taskforce/auth/session')) {
1677
- return Promise.resolve({
1678
- ok: true,
1679
- json: () => Promise.resolve({
1680
- authenticated: false,
1681
- runtimeMode: 'local',
1682
- authRequiredForApi: false
1683
- })
2195
+ if (url.includes('/api/taskforce/account/access-status')) {
2196
+ return jsonResponse({
2197
+ gate: 'checkout_pending',
2198
+ hasEntitlement: true,
2199
+ canAccessApp: false,
2200
+ canAccessPlans: true,
2201
+ message: 'Complete checkout to continue.'
1684
2202
  });
1685
2203
  }
1686
- if (url.includes('/api/taskforce/config')) {
1687
- return Promise.resolve({
1688
- ok: true,
1689
- json: () => Promise.resolve({
1690
- ...mockConfig,
1691
- runtimeMode: 'local',
1692
- authRequiredForApi: false,
1693
- setupState: {
1694
- globalSetupState: 'present',
1695
- workspaceSetupState: 'present',
1696
- runtimeMode: 'local',
1697
- workspaceId: 'workspace-local-active',
1698
- mode: 'core',
1699
- forceSetup: false,
1700
- forceGlobalSetup: false,
1701
- forceWorkspaceSetup: false
1702
- }
1703
- })
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'
1704
2213
  });
1705
2214
  }
1706
- if (url.includes('/api/taskforce/workflow-templates')) {
1707
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
2215
+ 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
+ });
1708
2250
  }
1709
2251
  if (url.includes('/api/taskforce/auth/register')) {
1710
- return Promise.resolve({
1711
- ok: true,
1712
- json: () => Promise.resolve({
1713
- success: true,
1714
- workspaceSetupRequired: false,
1715
- verificationRequired: true,
1716
- emailSent: true
1717
- })
2252
+ authenticated = true;
2253
+ return jsonResponse({
2254
+ success: true,
2255
+ workspaceSetupRequired: false,
2256
+ verificationRequired: false,
2257
+ checkoutPending: true,
2258
+ commercialState: 'checkout_pending'
1718
2259
  });
1719
2260
  }
1720
- return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
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')) {
2268
+ return jsonResponse(mockCategories);
2269
+ }
2270
+ if (url.includes('/api/taskforce/tasks')) {
2271
+ return jsonResponse({ tasks: mockTasks });
2272
+ }
2273
+ if (url.includes('/api/taskforce/archive')) {
2274
+ return jsonResponse({ archived: [] });
2275
+ }
2276
+ if (url.includes('/api/taskforce/data-version')) {
2277
+ return jsonResponse({ dataVersion: 1 });
2278
+ }
2279
+ if (url.includes('/api/taskforce/types')) {
2280
+ return jsonResponse({ types: [] });
2281
+ }
2282
+ if (url.includes('/api/taskforce/priorities')) {
2283
+ return jsonResponse({ priorities: [] });
2284
+ }
2285
+ if (url.includes('/api/taskforce/approaches')) {
2286
+ return jsonResponse({ approaches: [] });
2287
+ }
2288
+ if (url.includes('/api/taskforce/specialists')) {
2289
+ return jsonResponse({ specialists: [] });
2290
+ }
2291
+ if (url.includes('/api/taskforce/workflow-templates')) {
2292
+ return jsonResponse({ templates: [] });
2293
+ }
2294
+ return jsonResponse({});
1721
2295
  });
1722
2296
  vi.stubGlobal('fetch', fetchMock);
1723
- render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
1724
- const emailInput = await screen.findByPlaceholderText('Email');
1725
- const displayNameInput = screen.getByPlaceholderText('Display name');
1726
- const passwordInput = screen.getByPlaceholderText('Password');
1727
- await user.click(screen.getByRole('button', { name: 'Fill Form' }));
1728
- expect(String(emailInput.value)).toMatch(/^debug\+[a-z0-9]+@example\.com$/);
1729
- expect(String(displayNameInput.value)).toMatch(/^Debug [A-Z0-9]{4}$/);
1730
- expect(passwordInput).toHaveValue('DebugPass123!');
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');
2300
+ await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
1731
2301
  await user.click(screen.getByRole('button', { name: 'Create Account' }));
1732
2302
  await waitFor(() => {
1733
- const registerCall = fetchMock.mock.calls.find((call) => String(call[0]).includes('/api/taskforce/auth/register'));
1734
- expect(registerCall).toBeTruthy();
1735
- const payload = JSON.parse(String(registerCall?.[1]?.body || '{}'));
1736
- expect(String(registerCall?.[0] || '')).toContain('/api/taskforce/auth/register');
1737
- const init = registerCall?.[1];
1738
- expect(String(init?.method || '')).toBe('POST');
1739
- expect(String(init?.credentials || '')).toBe('include');
1740
- expect(payload.email).toBe(String(emailInput.value));
1741
- expect(payload.displayName).toBe(String(displayNameInput.value));
1742
- expect(payload.password).toBe('DebugPass123!');
1743
- });
1744
- nowSpy.mockRestore();
2303
+ expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
2304
+ });
2305
+ await waitFor(() => {
2306
+ expect(fetchMock.mock.calls.some(([url]) => String(url).includes('/api/taskforce/billing/checkout-session'))).toBe(true);
2307
+ });
1745
2308
  });
1746
- it('does not switch to verify mode after registration when verification is not required', async () => {
2309
+ it('shows verification before workspace setup when registration still requires email verification', async () => {
1747
2310
  const user = userEvent.setup();
1748
2311
  const fetchMock = vi.fn((input) => {
1749
2312
  const url = String(input);
@@ -1752,8 +2315,8 @@ describe('TaskforceCore Integration', () => {
1752
2315
  ok: true,
1753
2316
  json: () => Promise.resolve({
1754
2317
  authenticated: false,
1755
- runtimeMode: 'local',
1756
- authRequiredForApi: false
2318
+ runtimeMode: 'cloud',
2319
+ authRequiredForApi: true
1757
2320
  })
1758
2321
  });
1759
2322
  }
@@ -1762,13 +2325,13 @@ describe('TaskforceCore Integration', () => {
1762
2325
  ok: true,
1763
2326
  json: () => Promise.resolve({
1764
2327
  ...mockConfig,
1765
- runtimeMode: 'local',
1766
- authRequiredForApi: false,
2328
+ runtimeMode: 'cloud',
2329
+ authRequiredForApi: true,
1767
2330
  setupState: {
1768
2331
  globalSetupState: 'present',
1769
2332
  workspaceSetupState: 'present',
1770
- runtimeMode: 'local',
1771
- workspaceId: 'workspace-local-active',
2333
+ runtimeMode: 'cloud',
2334
+ workspaceId: 'default',
1772
2335
  mode: 'core',
1773
2336
  forceSetup: false,
1774
2337
  forceGlobalSetup: false,
@@ -1790,8 +2353,9 @@ describe('TaskforceCore Integration', () => {
1790
2353
  ok: true,
1791
2354
  json: () => Promise.resolve({
1792
2355
  success: true,
1793
- workspaceSetupRequired: false,
1794
- verificationRequired: false
2356
+ workspaceSetupRequired: true,
2357
+ verificationRequired: true,
2358
+ emailSent: true
1795
2359
  })
1796
2360
  });
1797
2361
  }
@@ -1804,12 +2368,12 @@ describe('TaskforceCore Integration', () => {
1804
2368
  await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
1805
2369
  await user.click(screen.getByRole('button', { name: 'Create Account' }));
1806
2370
  await waitFor(() => {
1807
- expect(screen.getByPlaceholderText('Search tasks...')).toBeInTheDocument();
2371
+ expect(screen.getByPlaceholderText('Verification token')).toBeInTheDocument();
1808
2372
  });
1809
- expect(screen.queryByPlaceholderText('Verification token')).not.toBeInTheDocument();
1810
2373
  expect(screen.queryByRole('button', { name: 'Create Account' })).not.toBeInTheDocument();
2374
+ expect(screen.queryByText('Subscription Plans')).not.toBeInTheDocument();
1811
2375
  });
1812
- it('allows returning to sign-in/register from forgot mode', async () => {
2376
+ it('allows returning to sign-in/register from forgot mode in local runtime', async () => {
1813
2377
  const user = userEvent.setup();
1814
2378
  const fetchMock = vi.fn((url) => {
1815
2379
  if (url.includes('/api/taskforce/auth/session')) {
@@ -1872,14 +2436,14 @@ describe('TaskforceCore Integration', () => {
1872
2436
  json: () => Promise.resolve({
1873
2437
  success: true,
1874
2438
  config: {
1875
- baseUrl: 'http://localhost:5174',
1876
- apiBaseUrl: 'http://localhost:5174',
1877
- cloudAuthBaseUrl: 'http://localhost:5174',
1878
- runtimeMode: 'local',
2439
+ baseUrl: 'https://app.taskforce.example',
2440
+ apiBaseUrl: 'https://app.taskforce.example/api',
2441
+ cloudAuthBaseUrl: 'https://app.taskforce.example',
2442
+ runtimeMode: 'cloud',
1879
2443
  authSource: 'cloud',
1880
- workspaceMode: 'single-local',
1881
- workspaceSwitchingEnabled: false,
1882
- cloudAuthViaLocalProxy: true
2444
+ workspaceMode: 'multi-cloud',
2445
+ workspaceSwitchingEnabled: true,
2446
+ cloudAuthViaLocalProxy: false
1883
2447
  }
1884
2448
  })
1885
2449
  });
@@ -1889,8 +2453,8 @@ describe('TaskforceCore Integration', () => {
1889
2453
  ok: true,
1890
2454
  json: () => Promise.resolve({
1891
2455
  authenticated: false,
1892
- runtimeMode: 'local',
1893
- authRequiredForApi: false
2456
+ runtimeMode: 'cloud',
2457
+ authRequiredForApi: true
1894
2458
  })
1895
2459
  });
1896
2460
  }
@@ -1899,12 +2463,12 @@ describe('TaskforceCore Integration', () => {
1899
2463
  ok: true,
1900
2464
  json: () => Promise.resolve({
1901
2465
  ...mockConfig,
1902
- runtimeMode: 'local',
1903
- authRequiredForApi: false,
2466
+ runtimeMode: 'cloud',
2467
+ authRequiredForApi: true,
1904
2468
  setupState: {
1905
2469
  globalSetupState: 'present',
1906
2470
  workspaceSetupState: 'present',
1907
- runtimeMode: 'local',
2471
+ runtimeMode: 'cloud',
1908
2472
  workspaceId: 'workspace-local-active',
1909
2473
  mode: 'core',
1910
2474
  forceSetup: false,
@@ -1944,7 +2508,7 @@ describe('TaskforceCore Integration', () => {
1944
2508
  json: () => Promise.resolve({
1945
2509
  success: false,
1946
2510
  code: 'SIGNUP_PLAN_NOT_ENABLED',
1947
- error: 'Configured default signup plan version \"team-10-v1\" belongs to a disabled plan.'
2511
+ error: 'Configured onboarding plan version \"team-10-v1\" belongs to a disabled plan.'
1948
2512
  })
1949
2513
  });
1950
2514
  }
@@ -3078,13 +3642,20 @@ describe('TaskforceCore Integration', () => {
3078
3642
  });
3079
3643
  expect(cloudWorkspaceCreated).toBe(false);
3080
3644
  });
3081
- it('shows local runtime Sign In / Register action and routes to shared login', async () => {
3645
+ it('shows local runtime Sign In action and routes to shared login', async () => {
3082
3646
  const standaloneLayoutPath = path.join(process.cwd(), 'src/components/views/StandaloneLayout.tsx');
3083
3647
  const standaloneLayoutSource = fs.readFileSync(standaloneLayoutPath, 'utf8');
3084
- expect(standaloneLayoutSource).toContain('Sign In / Register');
3648
+ expect(standaloneLayoutSource).toContain('Sign In');
3085
3649
  expect(standaloneLayoutSource).toContain('openSharedLoginRoute(\'login\')');
3086
3650
  expect(standaloneLayoutSource).toContain('navigate(`/login?mode=${mode}&next=${encodeURIComponent(target)}`)');
3087
3651
  });
3652
+ it('keeps the account badge neutral until the authenticated identity is fully resolved', () => {
3653
+ const standaloneLayoutPath = path.join(process.cwd(), 'src/components/views/StandaloneLayout.tsx');
3654
+ const standaloneLayoutSource = fs.readFileSync(standaloneLayoutPath, 'utf8');
3655
+ expect(standaloneLayoutSource).toContain("const shouldShowResolvedAccountInitial = isAuthenticated && authSessionResolved && accountBadgeLabel.length > 0;");
3656
+ expect(standaloneLayoutSource).toContain("const shouldShowResolvedAccountAvatar = isAuthenticated && authSessionResolved && accountAvatarUrl.length > 0;");
3657
+ expect(standaloneLayoutSource).toContain('<User size={14} className={accountStyles.avatarIcon} />');
3658
+ });
3088
3659
  it('keeps workspace management cloud-only while allowing cloud-backed team management from local runtime', () => {
3089
3660
  const standaloneLayoutPath = path.join(process.cwd(), 'src/components/views/StandaloneLayout.tsx');
3090
3661
  const standaloneLayoutSource = fs.readFileSync(standaloneLayoutPath, 'utf8');
@@ -3092,16 +3663,25 @@ describe('TaskforceCore Integration', () => {
3092
3663
  const useTaskforceSource = fs.readFileSync(useTaskforcePath, 'utf8');
3093
3664
  expect(standaloneLayoutSource).toContain("const canManageCloudWorkspaces = runtimeMode === 'cloud' && workspaceSwitchingEnabled && isAuthenticated;");
3094
3665
  expect(standaloneLayoutSource).toContain("const canAccessCloudTeamManagement = isAuthenticated && (runtimeMode === 'cloud' || cloudAuthConfigured);");
3095
- expect(standaloneLayoutSource).toContain("const teamManagementWorkspaceId = String(authWorkspaceId || currentWorkspaceId || '').trim();");
3096
- expect(standaloneLayoutSource).toContain('computeCanOpenTeamManagement(canAccessCloudTeamManagement, currentWorkspaceRole, teamPlanMode)');
3666
+ expect(standaloneLayoutSource).toContain("const teamManagementWorkspaceId = String(accountProfileSummary?.workspaceId || authWorkspaceId || currentWorkspaceId || '').trim();");
3667
+ expect(standaloneLayoutSource).toContain("const accountProfileSummaryUrl = resolveCloudAuthUrl(ACCOUNT_PROFILE_SUMMARY_PATH);");
3668
+ expect(standaloneLayoutSource).toContain('computeCanOpenTeamManagement(canAccessCloudTeamManagement, teamWorkspaceRole, teamPlanMode)');
3097
3669
  expect(useTaskforceSource).toContain("const [authWorkspaceId, setAuthWorkspaceId] = useState<string>('');");
3098
3670
  });
3099
3671
  it('routes local plans and billing UI through the connected cloud billing base', () => {
3100
3672
  const standaloneLayoutPath = path.join(process.cwd(), 'src/components/views/StandaloneLayout.tsx');
3101
3673
  const standaloneLayoutSource = fs.readFileSync(standaloneLayoutPath, 'utf8');
3102
- expect(standaloneLayoutSource).toContain("const billingApiBaseUrl = useMemo(() => {");
3103
- expect(standaloneLayoutSource).toContain("const res = await fetch(buildBillingApiUrl('/api/taskforce/billing/status'), {");
3104
- expect(standaloneLayoutSource).toContain("apiBaseUrl={billingApiBaseUrl}");
3674
+ const plansPagePath = path.join(process.cwd(), 'src/components/views/PlansPage.tsx');
3675
+ const plansPageSource = fs.readFileSync(plansPagePath, 'utf8');
3676
+ const useTaskforcePath = path.join(process.cwd(), 'src/hooks/useTaskforce.ts');
3677
+ const useTaskforceSource = fs.readFileSync(useTaskforcePath, 'utf8');
3678
+ expect(standaloneLayoutSource).toContain("const accountProfileSummaryUrl = resolveCloudAuthUrl(ACCOUNT_PROFILE_SUMMARY_PATH);");
3679
+ expect(standaloneLayoutSource).toContain("resolveCloudAuthUrl('/api/taskforce/billing/portal-session')");
3680
+ expect(standaloneLayoutSource).toContain("resolveCloudAuthUrl={resolveCloudAuthUrl}");
3681
+ expect(plansPageSource).toContain('const buildCloudBillingUrl = useCallback((path: string): string => {');
3682
+ expect(plansPageSource).toContain("const accountProfileSummaryUrl = buildCloudBillingUrl(ACCOUNT_PROFILE_SUMMARY_PATH);");
3683
+ expect(useTaskforceSource).toContain("|| path.startsWith('/api/taskforce/account/')");
3684
+ expect(useTaskforceSource).toContain("|| path.startsWith('/api/taskforce/billing/')");
3105
3685
  });
3106
3686
  it('uses shared /login flow for local sign in instead of a duplicate modal', async () => {
3107
3687
  const standaloneLayoutPath = path.join(process.cwd(), 'src/components/views/StandaloneLayout.tsx');
@@ -3125,6 +3705,8 @@ describe('TaskforceCore Integration', () => {
3125
3705
  const appMainPath = path.join(process.cwd(), 'apps/app/main.tsx');
3126
3706
  const appMainSource = fs.readFileSync(appMainPath, 'utf8');
3127
3707
  expect(useTaskforceSource).toContain("return 'https://app.taskforcehq.ai';");
3708
+ expect(useTaskforceSource).toContain('const preferredCloudAuthBaseUrl = isLoopbackHost');
3709
+ expect(useTaskforceSource).toContain("? (runtimeResolvedCloudAuthBaseUrl || explicitConfiguredCloudAuthBaseUrl)");
3128
3710
  expect(appMainSource).toContain('resolveClientDeploymentConfig');
3129
3711
  expect(appMainSource).toContain('return undefined;');
3130
3712
  });
@@ -3204,6 +3786,110 @@ describe('TaskforceCore Integration', () => {
3204
3786
  const pullCalls = fetchMock.mock.calls.filter((call) => typeof call[0] === 'string' && String(call[0]).includes('/sync/workspace/pull'));
3205
3787
  expect(pullCalls.length).toBe(0);
3206
3788
  });
3789
+ it('keeps local workspace data loaded after signing out from the account menu', async () => {
3790
+ const user = userEvent.setup();
3791
+ let signedIn = true;
3792
+ const fetchMock = vi.fn((url) => {
3793
+ if (url.includes('/api/taskforce/auth/runtime-config')) {
3794
+ return jsonResponse({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'cloud' } });
3795
+ }
3796
+ if (url.includes('https://cloud.example.com/api/taskforce/auth/session')) {
3797
+ return jsonResponse({
3798
+ authenticated: signedIn,
3799
+ runtimeMode: 'local',
3800
+ authRequiredForApi: false,
3801
+ email: signedIn ? 'member@example.com' : '',
3802
+ userId: signedIn ? 'user-member' : 'anonymous',
3803
+ workspaceId: 'workspace-local-active',
3804
+ betaAccess: true
3805
+ });
3806
+ }
3807
+ if (url.includes('https://cloud.example.com/api/taskforce/auth/logout')) {
3808
+ signedIn = false;
3809
+ return jsonResponse({ success: true });
3810
+ }
3811
+ if (url.includes('/api/taskforce/ui-state?key=app')) {
3812
+ return jsonResponse({ success: true, state: {} });
3813
+ }
3814
+ if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
3815
+ return jsonResponse({
3816
+ success: true,
3817
+ state: {
3818
+ version: 2,
3819
+ enabled: false,
3820
+ phase: 'idle',
3821
+ pullCursor: null,
3822
+ lastPullAt: null,
3823
+ lastPushAt: null,
3824
+ lastSyncedAt: null
3825
+ }
3826
+ });
3827
+ }
3828
+ if (url.includes('/api/taskforce/config')) {
3829
+ return jsonResponse({
3830
+ ...mockConfig,
3831
+ runtimeMode: 'local',
3832
+ authRequiredForApi: false,
3833
+ workspaceId: 'workspace-local-active',
3834
+ setupState: {
3835
+ globalSetupState: 'present',
3836
+ workspaceSetupState: 'present',
3837
+ runtimeMode: 'local',
3838
+ workspaceId: 'workspace-local-active',
3839
+ mode: 'core',
3840
+ forceSetup: false,
3841
+ forceGlobalSetup: false,
3842
+ forceWorkspaceSetup: false
3843
+ }
3844
+ });
3845
+ }
3846
+ if (url.includes('/api/taskforce/tasks')) {
3847
+ return jsonResponse({ tasks: mockTasks });
3848
+ }
3849
+ if (url.includes('/api/taskforce/archive')) {
3850
+ return jsonResponse({ archived: [] });
3851
+ }
3852
+ if (url.includes('/api/taskforce/categories')) {
3853
+ return jsonResponse(mockCategories);
3854
+ }
3855
+ if (url.includes('/api/taskforce/types')) {
3856
+ return jsonResponse({ types: [] });
3857
+ }
3858
+ if (url.includes('/api/taskforce/priorities')) {
3859
+ return jsonResponse({ priorities: [] });
3860
+ }
3861
+ if (url.includes('/api/taskforce/approaches')) {
3862
+ return jsonResponse({ approaches: [] });
3863
+ }
3864
+ if (url.includes('/api/taskforce/specialists')) {
3865
+ return jsonResponse({ specialists: [] });
3866
+ }
3867
+ if (url.includes('/api/taskforce/workflow-templates')) {
3868
+ return jsonResponse({ templates: [] });
3869
+ }
3870
+ if (url.includes('/api/taskforce/initiatives')) {
3871
+ return jsonResponse([]);
3872
+ }
3873
+ if (url.includes('/api/taskforce/workstreams')) {
3874
+ return jsonResponse([]);
3875
+ }
3876
+ return jsonResponse({});
3877
+ });
3878
+ vi.stubGlobal('fetch', fetchMock);
3879
+ render(_jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }));
3880
+ await waitFor(() => {
3881
+ expect(screen.getByTitle('Open tasks matching current filters')).toHaveTextContent('1/1');
3882
+ expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
3883
+ });
3884
+ await user.click(screen.getByTitle(/Account/i));
3885
+ await user.click(await screen.findByRole('menuitem', { name: 'Sign Out' }));
3886
+ await waitFor(() => {
3887
+ 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');
3889
+ });
3890
+ await user.click(screen.getByTitle(/Account/i));
3891
+ expect(await screen.findByText('Sign In / Register')).toBeInTheDocument();
3892
+ });
3207
3893
  it('does not pull from cloud when workspace sync toggle is disabled', async () => {
3208
3894
  const fetchMock = vi.fn((url) => {
3209
3895
  if (url.includes('/api/taskforce/auth/session')) {
@@ -4745,13 +5431,14 @@ describe('TaskforceCore Integration', () => {
4745
5431
  expect(screen.getByTitle('Workspace ID: workspace-cloud-active')).toBeInTheDocument();
4746
5432
  });
4747
5433
  });
4748
- it('opens edit profile from the account menu and saves the updated display name', async () => {
5434
+ it('opens edit profile from the account menu and saves the updated display name and avatar', async () => {
4749
5435
  const user = userEvent.setup();
4750
5436
  let savedDisplayName = 'Original Name';
5437
+ let savedAvatarUrl = '';
4751
5438
  const fetchMock = vi.fn((input, init) => {
4752
5439
  const url = String(input);
4753
5440
  if (url.includes('/api/taskforce/auth/runtime-config')) {
4754
- return jsonResponse({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'local' } });
5441
+ return jsonResponse({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'cloud' } });
4755
5442
  }
4756
5443
  if (url.includes('/api/taskforce/auth/session')) {
4757
5444
  return jsonResponse({
@@ -4761,6 +5448,7 @@ describe('TaskforceCore Integration', () => {
4761
5448
  userId: 'user-1',
4762
5449
  email: 'user@example.com',
4763
5450
  displayName: savedDisplayName,
5451
+ avatarUrl: savedAvatarUrl,
4764
5452
  workspaceId: 'workspace-local-active'
4765
5453
  });
4766
5454
  }
@@ -4817,15 +5505,38 @@ describe('TaskforceCore Integration', () => {
4817
5505
  return jsonResponse({ templates: [] });
4818
5506
  if (url.includes('/api/taskforce/workspace/assignee-options'))
4819
5507
  return jsonResponse({ options: [] });
5508
+ if (url.includes('/api/taskforce/auth/profile/avatar/init')) {
5509
+ return jsonResponse({
5510
+ success: true,
5511
+ draftId: 'avatar-draft-1',
5512
+ uploadUrl: 'https://upload.example.com/avatar-draft-1',
5513
+ relativePath: 'users/user-1/profile/avatar/avatar-draft-1-avatar.png',
5514
+ path: '/api/taskforce/context/users%2Fuser-1%2Fprofile%2Favatar%2Favatar-draft-1-avatar.png'
5515
+ });
5516
+ }
5517
+ if (url.includes('https://upload.example.com/avatar-draft-1')) {
5518
+ return Promise.resolve({ ok: true, status: 200, json: async () => ({}) });
5519
+ }
5520
+ if (url.includes('/api/taskforce/auth/profile/avatar/finalize')) {
5521
+ return jsonResponse({
5522
+ success: true,
5523
+ draftId: 'avatar-draft-1',
5524
+ avatarUrl: '/api/taskforce/context/users%2Fuser-1%2Fprofile%2Favatar%2Favatar-draft-1-avatar.png'
5525
+ });
5526
+ }
4820
5527
  if (url.includes('/api/taskforce/auth/profile')) {
4821
5528
  const body = JSON.parse(String(init?.body || '{}'));
4822
5529
  savedDisplayName = String(body?.displayName || '').trim() || savedDisplayName;
5530
+ if (body?.avatarDraftId === 'avatar-draft-1') {
5531
+ savedAvatarUrl = '/api/taskforce/context/users%2Fuser-1%2Fprofile%2Favatar%2Favatar-draft-1-avatar.png';
5532
+ }
4823
5533
  return jsonResponse({
4824
5534
  success: true,
4825
5535
  profile: {
4826
5536
  userId: 'user-1',
4827
5537
  email: 'user@example.com',
4828
- displayName: savedDisplayName
5538
+ displayName: savedDisplayName,
5539
+ avatarUrl: savedAvatarUrl
4829
5540
  }
4830
5541
  });
4831
5542
  }
@@ -4845,17 +5556,24 @@ describe('TaskforceCore Integration', () => {
4845
5556
  const displayNameInput = within(modal).getByPlaceholderText('Display name');
4846
5557
  await user.clear(displayNameInput);
4847
5558
  await user.type(displayNameInput, 'Updated Name');
5559
+ const avatarInput = within(modal).getByLabelText('Upload profile photo');
5560
+ await user.upload(avatarInput, new File(['avatar'], 'avatar.png', { type: 'image/png' }));
4848
5561
  await user.click(within(modal).getByRole('button', { name: 'Save Profile' }));
4849
5562
  await waitFor(() => {
4850
- const profileCall = fetchMock.mock.calls.find((call) => String(call[0]).includes('/api/taskforce/auth/profile'));
5563
+ const profileCall = fetchMock.mock.calls.find((call) => {
5564
+ const url = String(call[0]);
5565
+ return url.includes('/api/taskforce/auth/profile') && !url.includes('/api/taskforce/auth/profile/avatar/');
5566
+ });
4851
5567
  expect(profileCall).toBeTruthy();
4852
5568
  const payload = JSON.parse(String(profileCall?.[1]?.body || '{}'));
4853
5569
  expect(payload.displayName).toBe('Updated Name');
5570
+ expect(payload.avatarDraftId).toBe('avatar-draft-1');
4854
5571
  });
4855
5572
  await waitFor(() => {
4856
5573
  expect(screen.queryByText('Edit Profile')).not.toBeInTheDocument();
4857
5574
  });
4858
5575
  await user.click(screen.getByTitle(/Account/i));
4859
5576
  expect(await screen.findByText('Updated Name')).toBeInTheDocument();
5577
+ expect(screen.getByTitle(/Account/i).querySelector('img')).toHaveAttribute('src', '/api/taskforce/context/users%2Fuser-1%2Fprofile%2Favatar%2Favatar-draft-1-avatar.png');
4860
5578
  });
4861
5579
  });