@taskforcehq/taskforce 0.3.301 → 0.3.302

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 (147) hide show
  1. package/dist/Taskforce.module.css +26 -2
  2. package/dist/TaskforceCore.filter-persistence.test.js +1 -1
  3. package/dist/TaskforceCore.js +13 -24
  4. package/dist/TaskforceCore.test.js +167 -152
  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/ui/ReferenceBadgeButton.js +1 -1
  14. package/dist/components/views/PlansPage.js +9 -8
  15. package/dist/components/views/StandaloneLayout.js +466 -204
  16. package/dist/components/views/panels/PlanningDrawer.js +18 -1
  17. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +43 -0
  18. package/dist/components/views/standalone/modals/AccountHubModal.js +22 -0
  19. package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.d.ts +9 -0
  20. package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.js +8 -0
  21. package/dist/components/views/standalone/modals/EditProfileModal.d.ts +26 -0
  22. package/dist/components/views/standalone/modals/EditProfileModal.js +10 -0
  23. package/dist/components/views/standalone/modals/HelpModal.d.ts +9 -0
  24. package/dist/components/views/standalone/modals/HelpModal.js +8 -0
  25. package/dist/components/views/standalone/modals/ScheduleWarningModal.d.ts +13 -0
  26. package/dist/components/views/standalone/modals/ScheduleWarningModal.js +6 -0
  27. package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +61 -0
  28. package/dist/components/views/standalone/modals/SyncStatusModal.js +38 -0
  29. package/dist/components/views/standalone/modals/TeamManagementModal.d.ts +64 -0
  30. package/dist/components/views/standalone/modals/TeamManagementModal.js +10 -0
  31. package/dist/components/views/standalone/modals/types.d.ts +2 -0
  32. package/dist/components/views/standalone/modals/types.js +1 -0
  33. package/dist/components/views/teamManagementAccess.d.ts +1 -1
  34. package/dist/components/views/teamManagementAccess.js +2 -2
  35. package/dist/components/views/teamManagementAccess.test.js +3 -3
  36. package/dist/config/deployment.js +2 -2
  37. package/dist/config/deployment.test.js +5 -3
  38. package/dist/core/AiProfileService.js +7 -2
  39. package/dist/core/AiProfiles.test.js +19 -0
  40. package/dist/core/SyncReconciliationService.d.ts +2 -0
  41. package/dist/core/SyncReconciliationService.js +3 -0
  42. package/dist/core/TaskAuditTimeline.test.js +40 -0
  43. package/dist/core/Taskforce.d.ts +49 -0
  44. package/dist/core/Taskforce.ids.test.js +133 -0
  45. package/dist/core/Taskforce.js +241 -78
  46. package/dist/core/UserProfileAvatarDrafts.test.d.ts +1 -0
  47. package/dist/core/UserProfileAvatarDrafts.test.js +102 -0
  48. package/dist/core/taskReferencePolicy.d.ts +28 -0
  49. package/dist/core/taskReferencePolicy.js +189 -0
  50. package/dist/core/types.d.ts +2 -0
  51. package/dist/hooks/tasks/useTaskFormActions.js +3 -0
  52. package/dist/hooks/useTaskData.js +3 -2
  53. package/dist/hooks/useTaskData.test.js +66 -0
  54. package/dist/hooks/useTaskforce.d.ts +5 -0
  55. package/dist/hooks/useTaskforce.js +63 -15
  56. package/dist/hooks/useTaskforce.sync-behavior.test.js +185 -6
  57. package/dist/mcp/aiProfileBootstrap.d.ts +29 -0
  58. package/dist/mcp/aiProfileBootstrap.js +80 -0
  59. package/dist/mcp/aiProfileBootstrap.test.d.ts +1 -0
  60. package/dist/mcp/aiProfileBootstrap.test.js +68 -0
  61. package/dist/mcp/clientIntegrations.d.ts +2 -8
  62. package/dist/mcp/clientIntegrations.js +21 -40
  63. package/dist/mcp/clientIntegrations.test.js +28 -2
  64. package/dist/mcp/clientProfileDefaults.d.ts +12 -0
  65. package/dist/mcp/clientProfileDefaults.js +37 -0
  66. package/dist/mcp/runtime.d.ts +10 -7
  67. package/dist/mcp/runtime.js +41 -19
  68. package/dist/mcp/runtime.test.js +49 -2
  69. package/dist/migrations/taskSchemaMigrations.js +53 -1
  70. package/dist/qaOpen.test.d.ts +1 -0
  71. package/dist/qaOpen.test.js +56 -0
  72. package/dist/server/index.cookieProxy.test.js +1 -0
  73. package/dist/server/index.d.ts +1 -0
  74. package/dist/server/index.js +15 -1
  75. package/dist/server/index.test.js +14 -1
  76. package/dist/server/routes/admin.js +25 -22
  77. package/dist/server/routes/auth.d.ts +5 -0
  78. package/dist/server/routes/auth.js +350 -16
  79. package/dist/server/routes/billing.js +11 -1
  80. package/dist/server/routes/billing.test.js +32 -2
  81. package/dist/server/routes/documents.js +6 -1
  82. package/dist/server/routes/shared.d.ts +1 -1
  83. package/dist/server/routes/shared.js +11 -3
  84. package/dist/server/routes/sync.integration.test.js +130 -0
  85. package/dist/server/routes/sync.js +226 -1924
  86. package/dist/server/routes/tasks.js +18 -4
  87. package/dist/server/routes.js +26 -19
  88. package/dist/server/routes.test.js +352 -6
  89. package/dist/shared/runtimeContract.d.ts +1 -1
  90. package/dist/sync/collaborationSyncPayload.d.ts +17 -0
  91. package/dist/sync/collaborationSyncPayload.js +131 -0
  92. package/dist/sync/collaborationSyncPayload.test.d.ts +1 -0
  93. package/dist/sync/collaborationSyncPayload.test.js +104 -0
  94. package/dist/sync/collaborationSyncState.d.ts +15 -0
  95. package/dist/sync/collaborationSyncState.js +60 -0
  96. package/dist/sync/contentSyncPayload.d.ts +45 -0
  97. package/dist/sync/contentSyncPayload.js +104 -0
  98. package/dist/sync/contentSyncPayload.test.d.ts +1 -0
  99. package/dist/sync/contentSyncPayload.test.js +104 -0
  100. package/dist/sync/contentSyncState.d.ts +88 -0
  101. package/dist/sync/contentSyncState.js +743 -0
  102. package/dist/sync/planningSyncPayload.d.ts +9 -0
  103. package/dist/sync/planningSyncPayload.js +82 -0
  104. package/dist/sync/planningSyncPayload.test.d.ts +1 -0
  105. package/dist/sync/planningSyncPayload.test.js +68 -0
  106. package/dist/sync/syncApplyHandlers.d.ts +83 -0
  107. package/dist/sync/syncApplyHandlers.js +445 -0
  108. package/dist/sync/taskSyncPayload.d.ts +33 -0
  109. package/dist/sync/taskSyncPayload.js +126 -0
  110. package/dist/sync/taskSyncPayload.test.d.ts +1 -0
  111. package/dist/sync/taskSyncPayload.test.js +54 -0
  112. package/dist/sync/workspacePullFeed.d.ts +28 -0
  113. package/dist/sync/workspacePullFeed.js +301 -0
  114. package/dist/sync/workspacePullFeed.test.d.ts +1 -0
  115. package/dist/sync/workspacePullFeed.test.js +166 -0
  116. package/dist/sync/workspaceSyncModel.d.ts +4 -1
  117. package/dist/sync/workspaceSyncModel.js +9 -2
  118. package/dist/sync/workspaceSyncModel.test.js +29 -0
  119. package/dist/test/setup.js +1 -1
  120. package/dist/types.d.ts +1 -0
  121. package/dist/ui/assets/{AgentsModule-Bdq3T1Ts.js → AgentsModule-JG5jc3he.js} +1 -1
  122. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-jKUVo-PJ.js → AnnotatedAttachmentWorkspace-Dvi02Y7q.js} +1 -1
  123. package/dist/ui/assets/{DocumentWorkspace-DuQa-uIC.js → DocumentWorkspace-iys3zfca.js} +2 -2
  124. package/dist/ui/assets/{InitiativesModule-C8Pi2VTk.js → InitiativesModule-DJvLJI3A.js} +1 -1
  125. package/dist/ui/assets/PlansPage-BMPlpFpw.js +1 -0
  126. package/dist/ui/assets/TaskSettings-Bqd4cCzW.js +8 -0
  127. package/dist/ui/assets/{TaskSettings-BwB9NtmJ.css → TaskSettings-CnIBL_Eb.css} +1 -1
  128. package/dist/ui/assets/{WorkflowsModule-b6TLuHwJ.js → WorkflowsModule-BguHeQaV.js} +1 -1
  129. package/dist/ui/assets/index-MXeWoebC.css +1 -0
  130. package/dist/ui/assets/index-YJ4k5QNa.js +6 -0
  131. package/dist/ui/assets/{vendor-icons-QZyhEwgT.js → vendor-icons-C8EED3Oh.js} +1 -1
  132. package/dist/ui/index.html +3 -3
  133. package/dist/utils/billingStatusCache.d.ts +21 -0
  134. package/dist/utils/billingStatusCache.js +70 -0
  135. package/dist/utils/taskActivity.js +17 -4
  136. package/dist/utils/taskActivity.test.js +27 -0
  137. package/dist/utils/taskNormalization.js +10 -2
  138. package/dist/utils/taskReferences.d.ts +11 -1
  139. package/dist/utils/taskReferences.js +33 -1
  140. package/package.json +2 -1
  141. package/scripts/qa-open-lib.mjs +175 -0
  142. package/scripts/qa-open-lib.mjs.d.ts +49 -0
  143. package/scripts/qa-open.mjs +352 -0
  144. package/dist/ui/assets/PlansPage-B6BDECSi.js +0 -1
  145. package/dist/ui/assets/TaskSettings-DUJgRTPU.js +0 -8
  146. package/dist/ui/assets/index-BiiF1cLQ.css +0 -1
  147. package/dist/ui/assets/index-xBWLPOEb.js +0 -6
@@ -50,7 +50,7 @@ describe('TaskforceCore Integration', () => {
50
50
  localStorage.clear();
51
51
  vi.stubGlobal('fetch', vi.fn((url) => {
52
52
  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' } }) });
53
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'cloud' } }) });
54
54
  }
55
55
  if (url.includes('/api/taskforce/ui-state?key=app')) {
56
56
  return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true, state: {} }) });
@@ -168,7 +168,7 @@ describe('TaskforceCore Integration', () => {
168
168
  it('shows the header sync badge as off when local runtime is signed out', async () => {
169
169
  vi.stubGlobal('fetch', vi.fn((url) => {
170
170
  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' } }) });
171
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'cloud' } }) });
172
172
  }
173
173
  if (url.includes('/api/taskforce/ui-state?key=app')) {
174
174
  return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true, state: {} }) });
@@ -1569,7 +1569,7 @@ describe('TaskforceCore Integration', () => {
1569
1569
  expect(screen.getByText('CLOUD.EXAMPLE.COM')).toBeInTheDocument();
1570
1570
  });
1571
1571
  }, 10000);
1572
- it('allows local runtime to open shared /login and register flow', async () => {
1572
+ it('allows local runtime to open the shared cloud-backed register route', async () => {
1573
1573
  const fetchMock = vi.fn((url) => {
1574
1574
  if (url.includes('/api/taskforce/auth/session')) {
1575
1575
  return Promise.resolve({
@@ -1619,130 +1619,6 @@ describe('TaskforceCore Integration', () => {
1619
1619
  });
1620
1620
  expect(container.querySelector('[aria-pressed]')).not.toBeInTheDocument();
1621
1621
  });
1622
- it('shows debug registration autofill only when debug mode is enabled', async () => {
1623
- const fetchMock = vi.fn((url) => {
1624
- if (url.includes('/api/taskforce/auth/session')) {
1625
- return Promise.resolve({
1626
- ok: true,
1627
- json: () => Promise.resolve({
1628
- authenticated: false,
1629
- runtimeMode: 'local',
1630
- authRequiredForApi: false
1631
- })
1632
- });
1633
- }
1634
- if (url.includes('/api/taskforce/config')) {
1635
- return Promise.resolve({
1636
- ok: true,
1637
- json: () => Promise.resolve({
1638
- ...mockConfig,
1639
- runtimeMode: 'local',
1640
- authRequiredForApi: false,
1641
- setupState: {
1642
- globalSetupState: 'present',
1643
- workspaceSetupState: 'present',
1644
- runtimeMode: 'local',
1645
- workspaceId: 'workspace-local-active',
1646
- mode: 'core',
1647
- forceSetup: false,
1648
- forceGlobalSetup: false,
1649
- forceWorkspaceSetup: false
1650
- }
1651
- })
1652
- });
1653
- }
1654
- if (url.includes('/api/taskforce/workflow-templates')) {
1655
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
1656
- }
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
- })
1684
- });
1685
- }
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
- })
1704
- });
1705
- }
1706
- if (url.includes('/api/taskforce/workflow-templates')) {
1707
- return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
1708
- }
1709
- 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
- })
1718
- });
1719
- }
1720
- return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
1721
- });
1722
- 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!');
1731
- await user.click(screen.getByRole('button', { name: 'Create Account' }));
1732
- 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();
1745
- });
1746
1622
  it('does not switch to verify mode after registration when verification is not required', async () => {
1747
1623
  const user = userEvent.setup();
1748
1624
  const fetchMock = vi.fn((input) => {
@@ -1752,7 +1628,7 @@ describe('TaskforceCore Integration', () => {
1752
1628
  ok: true,
1753
1629
  json: () => Promise.resolve({
1754
1630
  authenticated: false,
1755
- runtimeMode: 'local',
1631
+ runtimeMode: 'cloud',
1756
1632
  authRequiredForApi: false
1757
1633
  })
1758
1634
  });
@@ -1762,12 +1638,12 @@ describe('TaskforceCore Integration', () => {
1762
1638
  ok: true,
1763
1639
  json: () => Promise.resolve({
1764
1640
  ...mockConfig,
1765
- runtimeMode: 'local',
1641
+ runtimeMode: 'cloud',
1766
1642
  authRequiredForApi: false,
1767
1643
  setupState: {
1768
1644
  globalSetupState: 'present',
1769
1645
  workspaceSetupState: 'present',
1770
- runtimeMode: 'local',
1646
+ runtimeMode: 'cloud',
1771
1647
  workspaceId: 'workspace-local-active',
1772
1648
  mode: 'core',
1773
1649
  forceSetup: false,
@@ -1804,12 +1680,12 @@ describe('TaskforceCore Integration', () => {
1804
1680
  await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
1805
1681
  await user.click(screen.getByRole('button', { name: 'Create Account' }));
1806
1682
  await waitFor(() => {
1807
- expect(screen.getByPlaceholderText('Search tasks...')).toBeInTheDocument();
1683
+ expect(screen.getByText('Subscription Plans')).toBeInTheDocument();
1808
1684
  });
1809
1685
  expect(screen.queryByPlaceholderText('Verification token')).not.toBeInTheDocument();
1810
1686
  expect(screen.queryByRole('button', { name: 'Create Account' })).not.toBeInTheDocument();
1811
1687
  });
1812
- it('allows returning to sign-in/register from forgot mode', async () => {
1688
+ it('allows returning to sign-in/register from forgot mode in local runtime', async () => {
1813
1689
  const user = userEvent.setup();
1814
1690
  const fetchMock = vi.fn((url) => {
1815
1691
  if (url.includes('/api/taskforce/auth/session')) {
@@ -1872,14 +1748,14 @@ describe('TaskforceCore Integration', () => {
1872
1748
  json: () => Promise.resolve({
1873
1749
  success: true,
1874
1750
  config: {
1875
- baseUrl: 'http://localhost:5174',
1876
- apiBaseUrl: 'http://localhost:5174',
1877
- cloudAuthBaseUrl: 'http://localhost:5174',
1878
- runtimeMode: 'local',
1751
+ baseUrl: 'https://app.taskforce.example',
1752
+ apiBaseUrl: 'https://app.taskforce.example/api',
1753
+ cloudAuthBaseUrl: 'https://app.taskforce.example',
1754
+ runtimeMode: 'cloud',
1879
1755
  authSource: 'cloud',
1880
- workspaceMode: 'single-local',
1881
- workspaceSwitchingEnabled: false,
1882
- cloudAuthViaLocalProxy: true
1756
+ workspaceMode: 'multi-cloud',
1757
+ workspaceSwitchingEnabled: true,
1758
+ cloudAuthViaLocalProxy: false
1883
1759
  }
1884
1760
  })
1885
1761
  });
@@ -1889,8 +1765,8 @@ describe('TaskforceCore Integration', () => {
1889
1765
  ok: true,
1890
1766
  json: () => Promise.resolve({
1891
1767
  authenticated: false,
1892
- runtimeMode: 'local',
1893
- authRequiredForApi: false
1768
+ runtimeMode: 'cloud',
1769
+ authRequiredForApi: true
1894
1770
  })
1895
1771
  });
1896
1772
  }
@@ -1899,12 +1775,12 @@ describe('TaskforceCore Integration', () => {
1899
1775
  ok: true,
1900
1776
  json: () => Promise.resolve({
1901
1777
  ...mockConfig,
1902
- runtimeMode: 'local',
1903
- authRequiredForApi: false,
1778
+ runtimeMode: 'cloud',
1779
+ authRequiredForApi: true,
1904
1780
  setupState: {
1905
1781
  globalSetupState: 'present',
1906
1782
  workspaceSetupState: 'present',
1907
- runtimeMode: 'local',
1783
+ runtimeMode: 'cloud',
1908
1784
  workspaceId: 'workspace-local-active',
1909
1785
  mode: 'core',
1910
1786
  forceSetup: false,
@@ -3078,10 +2954,10 @@ describe('TaskforceCore Integration', () => {
3078
2954
  });
3079
2955
  expect(cloudWorkspaceCreated).toBe(false);
3080
2956
  });
3081
- it('shows local runtime Sign In / Register action and routes to shared login', async () => {
2957
+ it('shows local runtime Sign In action and routes to shared login', async () => {
3082
2958
  const standaloneLayoutPath = path.join(process.cwd(), 'src/components/views/StandaloneLayout.tsx');
3083
2959
  const standaloneLayoutSource = fs.readFileSync(standaloneLayoutPath, 'utf8');
3084
- expect(standaloneLayoutSource).toContain('Sign In / Register');
2960
+ expect(standaloneLayoutSource).toContain('Sign In');
3085
2961
  expect(standaloneLayoutSource).toContain('openSharedLoginRoute(\'login\')');
3086
2962
  expect(standaloneLayoutSource).toContain('navigate(`/login?mode=${mode}&next=${encodeURIComponent(target)}`)');
3087
2963
  });
@@ -3093,14 +2969,15 @@ describe('TaskforceCore Integration', () => {
3093
2969
  expect(standaloneLayoutSource).toContain("const canManageCloudWorkspaces = runtimeMode === 'cloud' && workspaceSwitchingEnabled && isAuthenticated;");
3094
2970
  expect(standaloneLayoutSource).toContain("const canAccessCloudTeamManagement = isAuthenticated && (runtimeMode === 'cloud' || cloudAuthConfigured);");
3095
2971
  expect(standaloneLayoutSource).toContain("const teamManagementWorkspaceId = String(authWorkspaceId || currentWorkspaceId || '').trim();");
3096
- expect(standaloneLayoutSource).toContain('computeCanOpenTeamManagement(canAccessCloudTeamManagement, currentWorkspaceRole, teamPlanMode)');
2972
+ expect(standaloneLayoutSource).toContain("fetchTaskforceApi('/api/taskforce/account/team-management-access'");
2973
+ expect(standaloneLayoutSource).toContain('computeCanOpenTeamManagement(canAccessCloudTeamManagement, teamWorkspaceRole, teamPlanMode)');
3097
2974
  expect(useTaskforceSource).toContain("const [authWorkspaceId, setAuthWorkspaceId] = useState<string>('');");
3098
2975
  });
3099
2976
  it('routes local plans and billing UI through the connected cloud billing base', () => {
3100
2977
  const standaloneLayoutPath = path.join(process.cwd(), 'src/components/views/StandaloneLayout.tsx');
3101
2978
  const standaloneLayoutSource = fs.readFileSync(standaloneLayoutPath, 'utf8');
3102
2979
  expect(standaloneLayoutSource).toContain("const billingApiBaseUrl = useMemo(() => {");
3103
- expect(standaloneLayoutSource).toContain("const res = await fetch(buildBillingApiUrl('/api/taskforce/billing/status'), {");
2980
+ expect(standaloneLayoutSource).toContain("loadBillingStatusCached(billingStatusUrl)");
3104
2981
  expect(standaloneLayoutSource).toContain("apiBaseUrl={billingApiBaseUrl}");
3105
2982
  });
3106
2983
  it('uses shared /login flow for local sign in instead of a duplicate modal', async () => {
@@ -3125,6 +3002,8 @@ describe('TaskforceCore Integration', () => {
3125
3002
  const appMainPath = path.join(process.cwd(), 'apps/app/main.tsx');
3126
3003
  const appMainSource = fs.readFileSync(appMainPath, 'utf8');
3127
3004
  expect(useTaskforceSource).toContain("return 'https://app.taskforcehq.ai';");
3005
+ expect(useTaskforceSource).toContain('const preferredCloudAuthBaseUrl = isLoopbackHost');
3006
+ expect(useTaskforceSource).toContain("? (runtimeResolvedCloudAuthBaseUrl || explicitConfiguredCloudAuthBaseUrl)");
3128
3007
  expect(appMainSource).toContain('resolveClientDeploymentConfig');
3129
3008
  expect(appMainSource).toContain('return undefined;');
3130
3009
  });
@@ -3204,6 +3083,110 @@ describe('TaskforceCore Integration', () => {
3204
3083
  const pullCalls = fetchMock.mock.calls.filter((call) => typeof call[0] === 'string' && String(call[0]).includes('/sync/workspace/pull'));
3205
3084
  expect(pullCalls.length).toBe(0);
3206
3085
  });
3086
+ it('keeps local workspace data loaded after signing out from the account menu', async () => {
3087
+ const user = userEvent.setup();
3088
+ let signedIn = true;
3089
+ const fetchMock = vi.fn((url) => {
3090
+ if (url.includes('/api/taskforce/auth/runtime-config')) {
3091
+ return jsonResponse({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'cloud' } });
3092
+ }
3093
+ if (url.includes('https://cloud.example.com/api/taskforce/auth/session')) {
3094
+ return jsonResponse({
3095
+ authenticated: signedIn,
3096
+ runtimeMode: 'local',
3097
+ authRequiredForApi: false,
3098
+ email: signedIn ? 'member@example.com' : '',
3099
+ userId: signedIn ? 'user-member' : 'anonymous',
3100
+ workspaceId: 'workspace-local-active',
3101
+ betaAccess: true
3102
+ });
3103
+ }
3104
+ if (url.includes('https://cloud.example.com/api/taskforce/auth/logout')) {
3105
+ signedIn = false;
3106
+ return jsonResponse({ success: true });
3107
+ }
3108
+ if (url.includes('/api/taskforce/ui-state?key=app')) {
3109
+ return jsonResponse({ success: true, state: {} });
3110
+ }
3111
+ if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
3112
+ return jsonResponse({
3113
+ success: true,
3114
+ state: {
3115
+ version: 2,
3116
+ enabled: false,
3117
+ phase: 'idle',
3118
+ pullCursor: null,
3119
+ lastPullAt: null,
3120
+ lastPushAt: null,
3121
+ lastSyncedAt: null
3122
+ }
3123
+ });
3124
+ }
3125
+ if (url.includes('/api/taskforce/config')) {
3126
+ return jsonResponse({
3127
+ ...mockConfig,
3128
+ runtimeMode: 'local',
3129
+ authRequiredForApi: false,
3130
+ workspaceId: 'workspace-local-active',
3131
+ setupState: {
3132
+ globalSetupState: 'present',
3133
+ workspaceSetupState: 'present',
3134
+ runtimeMode: 'local',
3135
+ workspaceId: 'workspace-local-active',
3136
+ mode: 'core',
3137
+ forceSetup: false,
3138
+ forceGlobalSetup: false,
3139
+ forceWorkspaceSetup: false
3140
+ }
3141
+ });
3142
+ }
3143
+ if (url.includes('/api/taskforce/tasks')) {
3144
+ return jsonResponse({ tasks: mockTasks });
3145
+ }
3146
+ if (url.includes('/api/taskforce/archive')) {
3147
+ return jsonResponse({ archived: [] });
3148
+ }
3149
+ if (url.includes('/api/taskforce/categories')) {
3150
+ return jsonResponse(mockCategories);
3151
+ }
3152
+ if (url.includes('/api/taskforce/types')) {
3153
+ return jsonResponse({ types: [] });
3154
+ }
3155
+ if (url.includes('/api/taskforce/priorities')) {
3156
+ return jsonResponse({ priorities: [] });
3157
+ }
3158
+ if (url.includes('/api/taskforce/approaches')) {
3159
+ return jsonResponse({ approaches: [] });
3160
+ }
3161
+ if (url.includes('/api/taskforce/specialists')) {
3162
+ return jsonResponse({ specialists: [] });
3163
+ }
3164
+ if (url.includes('/api/taskforce/workflow-templates')) {
3165
+ return jsonResponse({ templates: [] });
3166
+ }
3167
+ if (url.includes('/api/taskforce/initiatives')) {
3168
+ return jsonResponse([]);
3169
+ }
3170
+ if (url.includes('/api/taskforce/workstreams')) {
3171
+ return jsonResponse([]);
3172
+ }
3173
+ return jsonResponse({});
3174
+ });
3175
+ vi.stubGlobal('fetch', fetchMock);
3176
+ render(_jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }));
3177
+ await waitFor(() => {
3178
+ expect(screen.getByTitle('Open tasks matching current filters')).toHaveTextContent('1/1');
3179
+ expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
3180
+ });
3181
+ await user.click(screen.getByTitle(/Account/i));
3182
+ await user.click(await screen.findByRole('menuitem', { name: 'Sign Out' }));
3183
+ await waitFor(() => {
3184
+ expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('https://cloud.example.com/api/taskforce/auth/logout'))).toBe(true);
3185
+ expect(screen.getByTitle('Open tasks matching current filters')).toHaveTextContent('1/1');
3186
+ });
3187
+ await user.click(screen.getByTitle(/Account/i));
3188
+ expect(await screen.findByText('Sign In / Register')).toBeInTheDocument();
3189
+ });
3207
3190
  it('does not pull from cloud when workspace sync toggle is disabled', async () => {
3208
3191
  const fetchMock = vi.fn((url) => {
3209
3192
  if (url.includes('/api/taskforce/auth/session')) {
@@ -4745,13 +4728,14 @@ describe('TaskforceCore Integration', () => {
4745
4728
  expect(screen.getByTitle('Workspace ID: workspace-cloud-active')).toBeInTheDocument();
4746
4729
  });
4747
4730
  });
4748
- it('opens edit profile from the account menu and saves the updated display name', async () => {
4731
+ it('opens edit profile from the account menu and saves the updated display name and avatar', async () => {
4749
4732
  const user = userEvent.setup();
4750
4733
  let savedDisplayName = 'Original Name';
4734
+ let savedAvatarUrl = '';
4751
4735
  const fetchMock = vi.fn((input, init) => {
4752
4736
  const url = String(input);
4753
4737
  if (url.includes('/api/taskforce/auth/runtime-config')) {
4754
- return jsonResponse({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'local' } });
4738
+ return jsonResponse({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'cloud' } });
4755
4739
  }
4756
4740
  if (url.includes('/api/taskforce/auth/session')) {
4757
4741
  return jsonResponse({
@@ -4761,6 +4745,7 @@ describe('TaskforceCore Integration', () => {
4761
4745
  userId: 'user-1',
4762
4746
  email: 'user@example.com',
4763
4747
  displayName: savedDisplayName,
4748
+ avatarUrl: savedAvatarUrl,
4764
4749
  workspaceId: 'workspace-local-active'
4765
4750
  });
4766
4751
  }
@@ -4817,15 +4802,38 @@ describe('TaskforceCore Integration', () => {
4817
4802
  return jsonResponse({ templates: [] });
4818
4803
  if (url.includes('/api/taskforce/workspace/assignee-options'))
4819
4804
  return jsonResponse({ options: [] });
4805
+ if (url.includes('/api/taskforce/auth/profile/avatar/init')) {
4806
+ return jsonResponse({
4807
+ success: true,
4808
+ draftId: 'avatar-draft-1',
4809
+ uploadUrl: 'https://upload.example.com/avatar-draft-1',
4810
+ relativePath: 'users/user-1/profile/avatar/avatar-draft-1-avatar.png',
4811
+ path: '/api/taskforce/context/users%2Fuser-1%2Fprofile%2Favatar%2Favatar-draft-1-avatar.png'
4812
+ });
4813
+ }
4814
+ if (url.includes('https://upload.example.com/avatar-draft-1')) {
4815
+ return Promise.resolve({ ok: true, status: 200, json: async () => ({}) });
4816
+ }
4817
+ if (url.includes('/api/taskforce/auth/profile/avatar/finalize')) {
4818
+ return jsonResponse({
4819
+ success: true,
4820
+ draftId: 'avatar-draft-1',
4821
+ avatarUrl: '/api/taskforce/context/users%2Fuser-1%2Fprofile%2Favatar%2Favatar-draft-1-avatar.png'
4822
+ });
4823
+ }
4820
4824
  if (url.includes('/api/taskforce/auth/profile')) {
4821
4825
  const body = JSON.parse(String(init?.body || '{}'));
4822
4826
  savedDisplayName = String(body?.displayName || '').trim() || savedDisplayName;
4827
+ if (body?.avatarDraftId === 'avatar-draft-1') {
4828
+ savedAvatarUrl = '/api/taskforce/context/users%2Fuser-1%2Fprofile%2Favatar%2Favatar-draft-1-avatar.png';
4829
+ }
4823
4830
  return jsonResponse({
4824
4831
  success: true,
4825
4832
  profile: {
4826
4833
  userId: 'user-1',
4827
4834
  email: 'user@example.com',
4828
- displayName: savedDisplayName
4835
+ displayName: savedDisplayName,
4836
+ avatarUrl: savedAvatarUrl
4829
4837
  }
4830
4838
  });
4831
4839
  }
@@ -4845,17 +4853,24 @@ describe('TaskforceCore Integration', () => {
4845
4853
  const displayNameInput = within(modal).getByPlaceholderText('Display name');
4846
4854
  await user.clear(displayNameInput);
4847
4855
  await user.type(displayNameInput, 'Updated Name');
4856
+ const avatarInput = within(modal).getByLabelText('Upload profile photo');
4857
+ await user.upload(avatarInput, new File(['avatar'], 'avatar.png', { type: 'image/png' }));
4848
4858
  await user.click(within(modal).getByRole('button', { name: 'Save Profile' }));
4849
4859
  await waitFor(() => {
4850
- const profileCall = fetchMock.mock.calls.find((call) => String(call[0]).includes('/api/taskforce/auth/profile'));
4860
+ const profileCall = fetchMock.mock.calls.find((call) => {
4861
+ const url = String(call[0]);
4862
+ return url.includes('/api/taskforce/auth/profile') && !url.includes('/api/taskforce/auth/profile/avatar/');
4863
+ });
4851
4864
  expect(profileCall).toBeTruthy();
4852
4865
  const payload = JSON.parse(String(profileCall?.[1]?.body || '{}'));
4853
4866
  expect(payload.displayName).toBe('Updated Name');
4867
+ expect(payload.avatarDraftId).toBe('avatar-draft-1');
4854
4868
  });
4855
4869
  await waitFor(() => {
4856
4870
  expect(screen.queryByText('Edit Profile')).not.toBeInTheDocument();
4857
4871
  });
4858
4872
  await user.click(screen.getByTitle(/Account/i));
4859
4873
  expect(await screen.findByText('Updated Name')).toBeInTheDocument();
4874
+ expect(screen.getByTitle(/Account/i).querySelector('img')).toHaveAttribute('src', '/api/taskforce/context/users%2Fuser-1%2Fprofile%2Favatar%2Favatar-draft-1-avatar.png');
4860
4875
  });
4861
4876
  });