@taskforcehq/taskforce 0.3.302 → 0.3.304

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/dist/Taskforce.module.css +113 -5
  2. package/dist/TaskforceCore.js +560 -208
  3. package/dist/TaskforceCore.test.js +1833 -91
  4. package/dist/annotatedAttachments/service.d.ts +1 -1
  5. package/dist/annotatedAttachments/types.d.ts +1 -1
  6. package/dist/components/features/AgentsModule.js +1 -1
  7. package/dist/components/features/AgentsModule.test.js +1 -1
  8. package/dist/components/features/TaskSettings.js +58 -38
  9. package/dist/components/features/TaskSettings.test.js +139 -40
  10. package/dist/components/task/TaskActionHeader.d.ts +2 -1
  11. package/dist/components/task/TaskActionHeader.js +3 -3
  12. package/dist/components/task/TaskActionHeader.test.js +8 -0
  13. package/dist/components/task/TaskCard.js +16 -1
  14. package/dist/components/task/TaskCard.test.js +31 -0
  15. package/dist/components/task/TaskForm.d.ts +2 -1
  16. package/dist/components/task/TaskForm.js +22 -4
  17. package/dist/components/task/TaskForm.test.js +46 -0
  18. package/dist/components/task/TaskKanban.d.ts +1 -1
  19. package/dist/components/task/TaskKanban.js +31 -160
  20. package/dist/components/task/TaskKanban.test.js +17 -0
  21. package/dist/components/task/TaskStatusActions.d.ts +6 -1
  22. package/dist/components/task/TaskStatusActions.js +8 -3
  23. package/dist/components/task/TaskStatusActions.test.js +9 -0
  24. package/dist/components/task/taskKanbanDropRules.d.ts +58 -0
  25. package/dist/components/task/taskKanbanDropRules.js +187 -0
  26. package/dist/components/task/taskKanbanDropRules.test.d.ts +1 -0
  27. package/dist/components/task/taskKanbanDropRules.test.js +114 -0
  28. package/dist/components/ui/Modal.d.ts +1 -1
  29. package/dist/components/ui/Modal.js +1 -0
  30. package/dist/components/views/AppShellHeader.js +5 -1
  31. package/dist/components/views/AppShellHeader.test.js +4 -0
  32. package/dist/components/views/PlanComparisonPage.d.ts +3 -2
  33. package/dist/components/views/PlanComparisonPage.js +4 -2
  34. package/dist/components/views/PlanComparisonPage.test.js +4 -0
  35. package/dist/components/views/PlansPage.d.ts +7 -1
  36. package/dist/components/views/PlansPage.js +139 -53
  37. package/dist/components/views/PlansPage.test.d.ts +1 -0
  38. package/dist/components/views/PlansPage.test.js +157 -0
  39. package/dist/components/views/StandaloneLayout.d.ts +1 -0
  40. package/dist/components/views/StandaloneLayout.js +216 -186
  41. package/dist/components/views/WidgetView.js +11 -2
  42. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +8 -3
  43. package/dist/components/views/standalone/modals/AccountHubModal.js +16 -2
  44. package/dist/components/views/standalone/modals/AccountHubModal.test.d.ts +1 -0
  45. package/dist/components/views/standalone/modals/AccountHubModal.test.js +26 -0
  46. package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
  47. package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
  48. package/dist/core/EntitlementsPolicy.test.js +2 -2
  49. package/dist/core/GlobalSettingsService.js +78 -18
  50. package/dist/core/PlanEntitlementService.d.ts +31 -5
  51. package/dist/core/PlanEntitlementService.js +350 -222
  52. package/dist/core/SignupMonetizationSettings.test.js +208 -49
  53. package/dist/core/SystemAdmin.test.js +186 -96
  54. package/dist/core/TaskActivityService.d.ts +4 -1
  55. package/dist/core/TaskActivityService.js +45 -0
  56. package/dist/core/TaskAuditTimeline.test.js +52 -1
  57. package/dist/core/Taskforce.d.ts +65 -20
  58. package/dist/core/Taskforce.js +261 -51
  59. package/dist/core/Taskforce.mcpAuth.test.js +0 -2
  60. package/dist/core/UserProfileAvatarDrafts.test.js +12 -3
  61. package/dist/core/WorkspacePlanMode.test.js +65 -0
  62. package/dist/core/types.d.ts +11 -12
  63. package/dist/documentReviews/service.d.ts +1 -1
  64. package/dist/documentReviews/types.d.ts +1 -1
  65. package/dist/hooks/auth/useTaskforceAuthBootstrap.d.ts +48 -0
  66. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +219 -0
  67. package/dist/hooks/sync/auth.d.ts +37 -0
  68. package/dist/hooks/sync/auth.js +61 -0
  69. package/dist/hooks/sync/bootstrap.d.ts +34 -0
  70. package/dist/hooks/sync/bootstrap.js +52 -0
  71. package/dist/hooks/sync/orchestratorShared.d.ts +36 -0
  72. package/dist/hooks/sync/orchestratorShared.js +209 -0
  73. package/dist/hooks/sync/orchestratorShared.test.d.ts +1 -0
  74. package/dist/hooks/sync/orchestratorShared.test.js +49 -0
  75. package/dist/hooks/sync/realtime.d.ts +25 -0
  76. package/dist/hooks/sync/realtime.js +60 -0
  77. package/dist/hooks/sync/recovery.d.ts +69 -0
  78. package/dist/hooks/sync/recovery.js +118 -0
  79. package/dist/hooks/sync/transfers.d.ts +122 -0
  80. package/dist/hooks/sync/transfers.js +431 -0
  81. package/dist/hooks/sync/useDataVersionRefresh.d.ts +16 -0
  82. package/dist/hooks/sync/useDataVersionRefresh.js +73 -0
  83. package/dist/hooks/ui/useResourceExport.d.ts +3 -1
  84. package/dist/hooks/ui/useResourceExport.js +5 -2
  85. package/dist/hooks/ui/useWorkflowTemplates.js +11 -6
  86. package/dist/hooks/useRealtimeSync.js +2 -1
  87. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +8 -0
  88. package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +1 -0
  89. package/dist/hooks/useSyncOrchestrator.context-assets.test.js +228 -0
  90. package/dist/hooks/useSyncOrchestrator.d.ts +5 -4
  91. package/dist/hooks/useSyncOrchestrator.js +315 -860
  92. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +665 -8
  93. package/dist/hooks/useTaskMutations.js +45 -6
  94. package/dist/hooks/useTaskMutations.test.js +147 -0
  95. package/dist/hooks/useTaskforce.d.ts +11 -2
  96. package/dist/hooks/useTaskforce.js +290 -812
  97. package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
  98. package/dist/hooks/useWorkspaceSyncController.js +22 -10
  99. package/dist/hooks/workspace/useTaskforceApiRouting.test.js +17 -0
  100. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +166 -0
  101. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +434 -0
  102. package/dist/hooks/workspace/workspaceLocalState.d.ts +19 -0
  103. package/dist/hooks/workspace/workspaceLocalState.js +100 -0
  104. package/dist/hooks/workspace/workspaceLocalState.test.d.ts +1 -0
  105. package/dist/hooks/workspace/workspaceLocalState.test.js +59 -0
  106. package/dist/localization/locales/en-US.js +2 -2
  107. package/dist/mcp/canonicalAssetHelpers.d.ts +70 -0
  108. package/dist/mcp/canonicalAssetHelpers.js +259 -0
  109. package/dist/mcp/clientIntegrations.d.ts +12 -1
  110. package/dist/mcp/clientIntegrations.js +161 -6
  111. package/dist/mcp/clientIntegrations.test.js +76 -4
  112. package/dist/mcp/documentHelpers.d.ts +100 -0
  113. package/dist/mcp/documentHelpers.js +161 -0
  114. package/dist/mcp/runtime.js +264 -505
  115. package/dist/mcp/runtime.test.js +353 -89
  116. package/dist/mcp/taskAttachmentHelpers.d.ts +41 -0
  117. package/dist/mcp/taskAttachmentHelpers.js +60 -0
  118. package/dist/mcp/taskAttachmentTypes.d.ts +37 -0
  119. package/dist/mcp/taskAttachmentTypes.js +1 -0
  120. package/dist/migrations/billingSchemaParity.test.js +3 -0
  121. package/dist/migrations/taskSchemaMigrations.js +15 -0
  122. package/dist/plugins/vite.js +1 -1
  123. package/dist/runtime/appGateDefinitions.d.ts +1 -1
  124. package/dist/runtime/appGateDefinitions.js +1 -1
  125. package/dist/server/index.cookieProxy.test.js +1 -0
  126. package/dist/server/index.d.ts +1 -0
  127. package/dist/server/index.js +48 -3
  128. package/dist/server/index.test.js +14 -2
  129. package/dist/server/mockStripe.d.ts +1 -0
  130. package/dist/server/mockStripe.js +2 -0
  131. package/dist/server/routes/admin.js +91 -40
  132. package/dist/server/routes/auth.d.ts +1 -0
  133. package/dist/server/routes/auth.js +231 -55
  134. package/dist/server/routes/billing.js +473 -39
  135. package/dist/server/routes/billing.test.js +972 -37
  136. package/dist/server/routes/shared.d.ts +30 -0
  137. package/dist/server/routes/shared.js +31 -3
  138. package/dist/server/routes/sync.integration.test.js +180 -0
  139. package/dist/server/routes/sync.js +125 -242
  140. package/dist/server/routes/tasks.js +49 -7
  141. package/dist/server/routes.js +11 -22
  142. package/dist/server/routes.test.js +530 -81
  143. package/dist/shared/taskActor.d.ts +10 -0
  144. package/dist/shared/taskActor.js +1 -0
  145. package/dist/sync/cloudSyncApi.d.ts +1 -0
  146. package/dist/sync/collaborationSyncPayload.js +4 -0
  147. package/dist/sync/collaborationSyncPayload.test.js +8 -0
  148. package/dist/sync/collaborationSyncState.d.ts +3 -1
  149. package/dist/sync/collaborationSyncState.js +19 -0
  150. package/dist/sync/contentSyncState.d.ts +10 -0
  151. package/dist/sync/contentSyncState.js +277 -14
  152. package/dist/sync/syncApplyHandlers.d.ts +87 -1
  153. package/dist/sync/syncApplyHandlers.js +370 -8
  154. package/dist/sync/syncApplyHandlers.test.d.ts +1 -0
  155. package/dist/sync/syncApplyHandlers.test.js +377 -0
  156. package/dist/sync/syncService.d.ts +9 -1
  157. package/dist/sync/syncService.js +57 -10
  158. package/dist/sync/syncService.test.js +48 -0
  159. package/dist/sync/workspacePullFeed.d.ts +11 -0
  160. package/dist/sync/workspacePullFeed.js +123 -68
  161. package/dist/sync/workspacePullFeed.test.js +180 -0
  162. package/dist/sync/workspaceSyncModel.d.ts +22 -1
  163. package/dist/sync/workspaceSyncModel.js +65 -0
  164. package/dist/sync/workspaceSyncModel.test.js +119 -0
  165. package/dist/types.d.ts +2 -10
  166. package/dist/ui/assets/AgentsModule-BLWVbuKP.js +1 -0
  167. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-Dvi02Y7q.js → AnnotatedAttachmentWorkspace-BlS-cqnl.js} +1 -1
  168. package/dist/ui/assets/{DocumentWorkspace-iys3zfca.js → DocumentWorkspace-BH_6DF6x.js} +1 -1
  169. package/dist/ui/assets/{InitiativesModule-DJvLJI3A.js → InitiativesModule-BjR6iBf9.js} +1 -1
  170. package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
  171. package/dist/ui/assets/PlansPage-CSDQ4sLa.js +1 -0
  172. package/dist/ui/assets/TaskSettings-CEDy34Jo.js +9 -0
  173. package/dist/ui/assets/{WorkflowsModule-BguHeQaV.js → WorkflowsModule-DnFqdUME.js} +1 -1
  174. package/dist/ui/assets/index-DXUdtH1B.js +6 -0
  175. package/dist/ui/assets/index-Ii0B0rTO.css +1 -0
  176. package/dist/ui/assets/{vendor-icons-C8EED3Oh.js → vendor-icons-I7ZwG1z_.js} +1 -1
  177. package/dist/ui/index.html +4 -4
  178. package/dist/utils/accountProfileSummaryCache.d.ts +31 -0
  179. package/dist/utils/accountProfileSummaryCache.js +97 -0
  180. package/dist/utils/accountProfileSummaryCache.test.d.ts +1 -0
  181. package/dist/utils/accountProfileSummaryCache.test.js +63 -0
  182. package/dist/utils/taskActivity.js +4 -0
  183. package/dist/utils/taskActivity.test.js +25 -1
  184. package/package.json +5 -3
  185. package/scripts/playwright-auth.sh +4 -0
  186. package/dist/ui/assets/AgentsModule-JG5jc3he.js +0 -1
  187. package/dist/ui/assets/PlansPage-BMPlpFpw.js +0 -1
  188. package/dist/ui/assets/TaskSettings-Bqd4cCzW.js +0 -8
  189. package/dist/ui/assets/index-MXeWoebC.css +0 -1
  190. package/dist/ui/assets/index-YJ4k5QNa.js +0 -6
  191. package/dist/utils/billingStatusCache.d.ts +0 -21
  192. package/dist/utils/billingStatusCache.js +0 -70
@@ -10,6 +10,7 @@ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/
10
10
  import * as mcpRuntime from '../mcp/runtime';
11
11
  import { createDatabaseAdapter } from '../storage/createDatabaseAdapter.js';
12
12
  import { ensureDocumentReferenceSchema, ensureTaskAssetsSchema, ensureWorkspaceAssetsSchema } from '../migrations/taskSchemaMigrations.js';
13
+ import { ObjectStorageClient } from '../storage/objectStorageClient.js';
13
14
  import { WorkspaceAssetStore } from '../storage/workspaceAssetStore.js';
14
15
  // Mock TaskforceCore
15
16
  vi.mock('../core/Taskforce', () => {
@@ -159,10 +160,13 @@ vi.mock('../core/Taskforce', () => {
159
160
  listDocumentReviewSessionsForSync = vi.fn().mockReturnValue([]);
160
161
  listDocumentReviewCommentsForSync = vi.fn().mockReturnValue([]);
161
162
  listAnnotatedAttachmentSessionsForSync = vi.fn().mockReturnValue([]);
163
+ listTaskEventsForSync = vi.fn().mockReturnValue([]);
164
+ listTaskEvents = vi.fn().mockReturnValue([]);
162
165
  getDocumentReviewSession = vi.fn().mockReturnValue(null);
163
166
  upsertAiProfileForSync = vi.fn();
164
167
  upsertDocumentReviewSessionForSync = vi.fn();
165
168
  upsertDocumentReviewCommentForSync = vi.fn();
169
+ upsertTaskEventForSync = vi.fn();
166
170
  deleteDocumentReviewSessionForSync = vi.fn().mockReturnValue(true);
167
171
  deleteDocumentReviewCommentForSync = vi.fn().mockReturnValue(true);
168
172
  deleteAnnotatedAttachmentSessionForSync = vi.fn().mockReturnValue(true);
@@ -236,6 +240,7 @@ vi.mock('../core/Taskforce', () => {
236
240
  setSystemRole = vi.fn().mockReturnValue(true);
237
241
  setUserDisabledGlobal = vi.fn().mockReturnValue(true);
238
242
  setUserBetaAccessGlobal = vi.fn().mockReturnValue(true);
243
+ purgeSystemUser = vi.fn();
239
244
  updateWorkspaceUserRole = vi.fn();
240
245
  inviteWorkspaceUser = vi.fn();
241
246
  updateWorkspaceProfile = vi.fn().mockImplementation((workspaceId, input) => ({
@@ -262,17 +267,16 @@ vi.mock('../core/Taskforce', () => {
262
267
  revokeInviteAcceptanceForUser = vi.fn().mockReturnValue({ revoked: 1 });
263
268
  getWorkspacePlanMode = vi.fn().mockReturnValue('team');
264
269
  setWorkspacePlanMode = vi.fn().mockImplementation((_workspaceId, mode) => mode);
265
- getSignupMonetizationSettings = vi.fn().mockReturnValue({
266
- strategy: 'auto_assign_default',
267
- defaultSignupPlanVersionId: null
270
+ getOnboardingPolicy = vi.fn().mockReturnValue({
271
+ mode: 'require_plan_selection',
272
+ planVersionId: null
268
273
  });
269
- validateSignupMonetizationSettings = vi.fn().mockReturnValue({ valid: true });
274
+ validateOnboardingPolicy = vi.fn().mockReturnValue({ valid: true });
270
275
  getAccountAccessStatus = vi.fn().mockReturnValue({
271
276
  gate: 'ok',
272
277
  hasEntitlement: true,
273
278
  canAccessApp: true,
274
279
  canAccessPlans: true,
275
- signupMonetizationStrategy: 'auto_assign_default',
276
280
  planSelectionRequired: false,
277
281
  message: null,
278
282
  entitlement: {
@@ -434,6 +438,7 @@ vi.mock('../core/Taskforce', () => {
434
438
  ]);
435
439
  hasWorkspaceAdmin = vi.fn().mockReturnValue(true);
436
440
  registerPasswordUser = vi.fn();
441
+ isEmailVerified = vi.fn().mockReturnValue(false);
437
442
  requestEmailVerification = vi.fn().mockReturnValue({});
438
443
  verifyEmailWithToken = vi.fn().mockReturnValue(false);
439
444
  markEmailVerified = vi.fn().mockReturnValue(true);
@@ -892,6 +897,8 @@ describe('Server Routes', () => {
892
897
  auth: expect.objectContaining({
893
898
  aiProfileName: 'Automation Codex',
894
899
  aiProfileToken: 'tfp_automation_codex',
900
+ aiProfileIcon: 'Code2',
901
+ aiProfileColor: '#10a37f',
895
902
  aiProfileDescription: 'OpenAI Codex coding agent connected through MCP.',
896
903
  aiProfileRole: 'Coding Agent',
897
904
  aiProfileProvider: 'OpenAI',
@@ -1600,6 +1607,17 @@ describe('Server Routes', () => {
1600
1607
  });
1601
1608
  expect(core.updateUiState).not.toHaveBeenCalledWith('workspace-denied', expect.anything(), expect.anything());
1602
1609
  });
1610
+ it('POST /api/taskforce/ui-state should stamp debug response headers', async () => {
1611
+ const match = matchRoute('POST', '/api/taskforce/ui-state', routes);
1612
+ const req = createMockReq('POST', '/api/taskforce/ui-state', {
1613
+ stateKey: 'app',
1614
+ patch: { activeWorkspaceModule: 'tasks' }
1615
+ });
1616
+ const res = createMockRes();
1617
+ await match?.handler(req, res, {});
1618
+ expect(res.setHeader).toHaveBeenCalledWith('x-taskforce-ui-state-route', 'tasks');
1619
+ expect(res.setHeader).toHaveBeenCalledWith('x-taskforce-ui-state-request-id', expect.stringMatching(/^ui-/));
1620
+ });
1603
1621
  it('POST /api/taskforce/workspace-profile should persist workspace name and description', async () => {
1604
1622
  const match = matchRoute('POST', '/api/taskforce/workspace-profile', routes);
1605
1623
  const req = createMockReq('POST', '/api/taskforce/workspace-profile', { workspaceId: 'default', name: 'Acme Project', description: 'Customer onboarding' }, { 'x-taskforce-workspace-id': 'default' });
@@ -3673,7 +3691,7 @@ describe('Server Routes', () => {
3673
3691
  process.env.VITE_TASKFORCE_API_BASE_URL = previousApiBaseUrl;
3674
3692
  }
3675
3693
  });
3676
- it('POST /api/taskforce/auth/register should create account and set session cookie', async () => {
3694
+ it('POST /api/taskforce/auth/register should create account without a session cookie when verification is required', async () => {
3677
3695
  const authConfig = {
3678
3696
  runtimeMode: 'cloud',
3679
3697
  enabled: true,
@@ -3706,6 +3724,7 @@ describe('Server Routes', () => {
3706
3724
  email: 'owner@example.com',
3707
3725
  role: 'owner'
3708
3726
  }));
3727
+ expect(res.setHeader).not.toHaveBeenCalledWith('Set-Cookie', expect.any(String));
3709
3728
  expect(res.writeHead).toHaveBeenCalledWith(201, expect.any(Object));
3710
3729
  expect(res.end).toHaveBeenCalledWith(expect.stringContaining('"verificationRequired":true'));
3711
3730
  });
@@ -3741,7 +3760,7 @@ describe('Server Routes', () => {
3741
3760
  workspaceId: undefined,
3742
3761
  role: 'member'
3743
3762
  }));
3744
- expect(res.setHeader).toHaveBeenCalledWith('Set-Cookie', expect.stringContaining('taskforce_session='));
3763
+ expect(res.setHeader).not.toHaveBeenCalledWith('Set-Cookie', expect.any(String));
3745
3764
  expect(res.writeHead).toHaveBeenCalledWith(201, expect.any(Object));
3746
3765
  expect(res.end).toHaveBeenCalledWith(expect.stringContaining('"workspaceSetupRequired":true'));
3747
3766
  });
@@ -3780,6 +3799,7 @@ describe('Server Routes', () => {
3780
3799
  const res = createMockRes();
3781
3800
  await match?.handler(req, res, {});
3782
3801
  expect(core.requestEmailVerification).not.toHaveBeenCalled();
3802
+ expect(res.setHeader).toHaveBeenCalledWith('Set-Cookie', expect.stringContaining('taskforce_session='));
3783
3803
  expect(res.writeHead).toHaveBeenCalledWith(201, expect.any(Object));
3784
3804
  expect(res.end).toHaveBeenCalledWith(expect.stringContaining('"verificationRequired":false'));
3785
3805
  });
@@ -3819,7 +3839,49 @@ describe('Server Routes', () => {
3819
3839
  }));
3820
3840
  expect(res.writeHead).toHaveBeenCalledWith(201, expect.any(Object));
3821
3841
  });
3822
- it('POST /api/taskforce/auth/register should return conflict when email exists', async () => {
3842
+ it('POST /api/taskforce/auth/register should route duplicate unverified email into verification recovery', async () => {
3843
+ const authConfig = {
3844
+ runtimeMode: 'cloud',
3845
+ enabled: true,
3846
+ requiredForApi: true,
3847
+ excludedPaths: ['/api/taskforce/health', '/api/taskforce/auth'],
3848
+ defaultRole: 'member',
3849
+ defaultWorkspaceId: 'default',
3850
+ sessionCookieName: 'taskforce_session',
3851
+ sessionSecret: 'test-secret',
3852
+ sessionTtlSeconds: 3600
3853
+ };
3854
+ routes = createRoutes(core, { authConfig });
3855
+ core.hasWorkspaceAdmin.mockReturnValueOnce(true);
3856
+ core.isEmailVerified.mockReturnValueOnce(false);
3857
+ core.requestEmailVerification.mockReturnValueOnce({ token: 'verify-token-existing' });
3858
+ const error = new Error('Email is already registered');
3859
+ error.statusCode = 409;
3860
+ error.code = 'EMAIL_ALREADY_EXISTS';
3861
+ core.registerPasswordUser.mockImplementationOnce(() => {
3862
+ throw error;
3863
+ });
3864
+ const match = matchRoute('POST', '/api/taskforce/auth/register', routes);
3865
+ const req = createMockReq('POST', '/api/taskforce/auth/register', {
3866
+ email: 'owner@example.com',
3867
+ password: 'password123',
3868
+ workspaceId: 'workspace-1'
3869
+ });
3870
+ const res = createMockRes();
3871
+ await match?.handler(req, res, {});
3872
+ expect(core.isEmailVerified).toHaveBeenCalledWith('owner@example.com');
3873
+ expect(core.requestEmailVerification).toHaveBeenCalledWith({ email: 'owner@example.com' });
3874
+ expect(res.writeHead).toHaveBeenCalledWith(409, expect.any(Object));
3875
+ const responsePayload = JSON.parse(String(res.end.mock.calls.at(-1)?.[0] || '{}'));
3876
+ expect(responsePayload).toEqual(expect.objectContaining({
3877
+ success: false,
3878
+ code: 'EMAIL_ALREADY_EXISTS_UNVERIFIED',
3879
+ verificationRequired: true,
3880
+ emailSent: true,
3881
+ verificationToken: 'verify-token-existing'
3882
+ }));
3883
+ });
3884
+ it('POST /api/taskforce/auth/register should return conflict when verified email exists', async () => {
3823
3885
  const authConfig = {
3824
3886
  runtimeMode: 'cloud',
3825
3887
  enabled: true,
@@ -3833,6 +3895,7 @@ describe('Server Routes', () => {
3833
3895
  };
3834
3896
  routes = createRoutes(core, { authConfig });
3835
3897
  core.hasWorkspaceAdmin.mockReturnValueOnce(true);
3898
+ core.isEmailVerified.mockReturnValueOnce(true);
3836
3899
  const error = new Error('Email is already registered');
3837
3900
  error.statusCode = 409;
3838
3901
  error.code = 'EMAIL_ALREADY_EXISTS';
@@ -3847,6 +3910,8 @@ describe('Server Routes', () => {
3847
3910
  });
3848
3911
  const res = createMockRes();
3849
3912
  await match?.handler(req, res, {});
3913
+ expect(core.isEmailVerified).toHaveBeenCalledWith('owner@example.com');
3914
+ expect(core.requestEmailVerification).not.toHaveBeenCalled();
3850
3915
  expect(res.writeHead).toHaveBeenCalledWith(409, expect.any(Object));
3851
3916
  expect(res.end).toHaveBeenCalledWith(JSON.stringify({
3852
3917
  success: false,
@@ -3854,7 +3919,7 @@ describe('Server Routes', () => {
3854
3919
  code: 'EMAIL_ALREADY_EXISTS'
3855
3920
  }));
3856
3921
  });
3857
- it('POST /api/taskforce/auth/register should fail when signup monetization is misconfigured', async () => {
3922
+ it('POST /api/taskforce/auth/register should fail when the onboarding policy is misconfigured', async () => {
3858
3923
  const authConfig = {
3859
3924
  runtimeMode: 'cloud',
3860
3925
  enabled: true,
@@ -3867,7 +3932,7 @@ describe('Server Routes', () => {
3867
3932
  sessionTtlSeconds: 3600
3868
3933
  };
3869
3934
  routes = createRoutes(core, { authConfig });
3870
- const error = new Error('Configured default signup plan version "missing-v9" was not found.');
3935
+ const error = new Error('Configured onboarding plan version "missing-v9" was not found.');
3871
3936
  error.statusCode = 500;
3872
3937
  error.code = 'SIGNUP_PLAN_VERSION_NOT_FOUND';
3873
3938
  core.registerPasswordUser.mockImplementationOnce(() => {
@@ -3883,7 +3948,7 @@ describe('Server Routes', () => {
3883
3948
  expect(res.writeHead).toHaveBeenCalledWith(500, expect.any(Object));
3884
3949
  expect(res.end).toHaveBeenCalledWith(JSON.stringify({
3885
3950
  success: false,
3886
- error: 'Configured default signup plan version "missing-v9" was not found.',
3951
+ error: 'Configured onboarding plan version "missing-v9" was not found.',
3887
3952
  code: 'SIGNUP_PLAN_VERSION_NOT_FOUND'
3888
3953
  }));
3889
3954
  });
@@ -3900,12 +3965,19 @@ describe('Server Routes', () => {
3900
3965
  sessionTtlSeconds: 3600
3901
3966
  };
3902
3967
  routes = createRoutes(core, { authConfig });
3968
+ core.inspectOneTimeAuthToken = vi.fn().mockReturnValueOnce({
3969
+ state: 'valid',
3970
+ userId: 'u-verify',
3971
+ workspaceId: 'workspace-1'
3972
+ });
3903
3973
  core.verifyEmailWithToken.mockReturnValueOnce(true);
3904
3974
  const match = matchRoute('POST', '/api/taskforce/auth/verify-email/confirm', routes);
3905
3975
  const req = createMockReq('POST', '/api/taskforce/auth/verify-email/confirm', { token: 'tok1' });
3906
3976
  const res = createMockRes();
3907
3977
  await match?.handler(req, res, {});
3978
+ expect(core.inspectOneTimeAuthToken).toHaveBeenCalledWith('tok1', 'email_verification');
3908
3979
  expect(core.verifyEmailWithToken).toHaveBeenCalledWith('tok1');
3980
+ expect(res.setHeader).toHaveBeenCalledWith('Set-Cookie', expect.stringContaining('taskforce_session='));
3909
3981
  expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
3910
3982
  });
3911
3983
  it('POST /api/taskforce/auth/password-reset/confirm should set new password', async () => {
@@ -4779,10 +4851,10 @@ describe('Server Routes', () => {
4779
4851
  }, { cookie });
4780
4852
  const res = createMockRes();
4781
4853
  await match?.handler(req, res, {});
4782
- expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith({
4783
- tasks: [{ id: 'task-1', title: 'Task 1' }],
4784
- archived: [{ id: 'task-2', title: 'Task 2', isArchived: true }]
4785
- }, 'workspace-1');
4854
+ expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith(expect.objectContaining({
4855
+ tasks: [expect.objectContaining({ id: 'task-1', title: 'Task 1' })],
4856
+ archived: [expect.objectContaining({ id: 'task-2', title: 'Task 2', isArchived: true })]
4857
+ }), 'workspace-1');
4786
4858
  expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
4787
4859
  expect(res.end).toHaveBeenCalledWith(expect.stringContaining('"success":true'));
4788
4860
  expect(core.addAdminAuditLog).toHaveBeenCalledWith(expect.objectContaining({
@@ -6427,10 +6499,10 @@ describe('Server Routes', () => {
6427
6499
  const res = createMockRes();
6428
6500
  await match?.handler(req, res, {});
6429
6501
  expect(core.deleteTaskForSync).toHaveBeenCalledWith('task-2', 'workspace-local', { persistBackup: false });
6430
- expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith({
6431
- tasks: [{ id: 'task-1', title: 'Task 1' }],
6502
+ expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith(expect.objectContaining({
6503
+ tasks: [expect.objectContaining({ id: 'task-1', title: 'Task 1' })],
6432
6504
  archived: []
6433
- }, 'workspace-local');
6505
+ }), 'workspace-local');
6434
6506
  expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
6435
6507
  });
6436
6508
  it('POST /api/taskforce/sync/workspace/apply-local should apply document review session upserts and deletes', async () => {
@@ -6522,6 +6594,77 @@ describe('Server Routes', () => {
6522
6594
  });
6523
6595
  expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
6524
6596
  });
6597
+ it('POST /api/taskforce/sync/workspace/apply-local should apply task event upserts after task snapshots', async () => {
6598
+ core.resolvePreferredWorkspaceId.mockReturnValue('workspace-local');
6599
+ core.getTask.mockImplementation((taskId) => (taskId === 'task-sync-activity-1'
6600
+ ? {
6601
+ id: 'task-sync-activity-1',
6602
+ title: 'Synced task',
6603
+ status: 'done',
6604
+ category: 'general',
6605
+ type: 'task',
6606
+ createdAt: '2026-03-24T10:00:00.000Z',
6607
+ updatedAt: '2026-03-24T10:05:00.000Z',
6608
+ taskEvents: []
6609
+ }
6610
+ : null));
6611
+ routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
6612
+ const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
6613
+ const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
6614
+ changes: [
6615
+ {
6616
+ op: 'upsert',
6617
+ archived: false,
6618
+ task: {
6619
+ id: 'task-sync-activity-1',
6620
+ title: 'Synced task',
6621
+ status: 'done',
6622
+ category: 'general',
6623
+ type: 'task',
6624
+ updatedAt: '2026-03-24T10:05:00.000Z'
6625
+ }
6626
+ },
6627
+ {
6628
+ op: 'task-event-upsert',
6629
+ event: {
6630
+ id: 'task-event-sync-1',
6631
+ taskId: 'task-sync-activity-1',
6632
+ workspaceId: 'workspace-local',
6633
+ action: 'task-status-changed',
6634
+ actor: 'ai-profile-1',
6635
+ actorType: 'ai',
6636
+ createdAt: '2026-03-24T10:05:00.000Z',
6637
+ details: {
6638
+ changes: {
6639
+ status: {
6640
+ from: 'in-progress',
6641
+ to: 'done'
6642
+ }
6643
+ }
6644
+ }
6645
+ }
6646
+ }
6647
+ ]
6648
+ }, {
6649
+ 'x-taskforce-workspace-id': 'workspace-local'
6650
+ });
6651
+ const res = createMockRes();
6652
+ await match?.handler(req, res, {});
6653
+ expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith(expect.objectContaining({
6654
+ tasks: [expect.objectContaining({ id: 'task-sync-activity-1' })],
6655
+ archived: []
6656
+ }), 'workspace-local');
6657
+ expect(core.upsertTaskEventForSync).toHaveBeenCalledWith(expect.objectContaining({
6658
+ id: 'task-event-sync-1',
6659
+ taskId: 'task-sync-activity-1',
6660
+ workspaceId: 'workspace-local',
6661
+ action: 'task-status-changed',
6662
+ actor: 'ai-profile-1',
6663
+ actorType: 'ai',
6664
+ createdAt: '2026-03-24T10:05:00.000Z'
6665
+ }));
6666
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
6667
+ });
6525
6668
  it('POST /api/taskforce/sync/workspace/apply-local should fail when document review session persistence fails', async () => {
6526
6669
  core.resolvePreferredWorkspaceId.mockReturnValue('workspace-local');
6527
6670
  const error = new Error('document review session write failed');
@@ -6868,10 +7011,10 @@ describe('Server Routes', () => {
6868
7011
  });
6869
7012
  const res = createMockRes();
6870
7013
  await match?.handler(req, res, {});
6871
- expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith({
6872
- tasks: [{ id: 'task-shared-1', title: 'Shared task' }],
7014
+ expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith(expect.objectContaining({
7015
+ tasks: [expect.objectContaining({ id: 'task-shared-1', title: 'Shared task' })],
6873
7016
  archived: []
6874
- }, 'workspace-shared-local');
7017
+ }), 'workspace-shared-local');
6875
7018
  expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
6876
7019
  });
6877
7020
  it('POST /api/taskforce/sync/workspace/apply-local should route archived upserts and preserve canceledReason', async () => {
@@ -6898,17 +7041,17 @@ describe('Server Routes', () => {
6898
7041
  });
6899
7042
  const res = createMockRes();
6900
7043
  await match?.handler(req, res, {});
6901
- expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith({
7044
+ expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith(expect.objectContaining({
6902
7045
  tasks: [],
6903
- archived: [{
7046
+ archived: [expect.objectContaining({
6904
7047
  id: 'task-archived-cancelled',
6905
7048
  title: 'Cancelled + Archived',
6906
7049
  status: 'cancelled',
6907
7050
  canceledReason: 'Scope changed',
6908
7051
  workstreamId: 'workstream-archived-1',
6909
7052
  isArchived: true
6910
- }]
6911
- }, 'workspace-local');
7053
+ })]
7054
+ }), 'workspace-local');
6912
7055
  expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
6913
7056
  });
6914
7057
  it('POST /api/taskforce/sync/workspace/apply-local should prioritize delete over same-task upsert', async () => {
@@ -7575,10 +7718,10 @@ describe('Server Routes', () => {
7575
7718
  }, { cookie });
7576
7719
  const res1 = createMockRes();
7577
7720
  await match?.handler(req1, res1, {});
7578
- expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith({
7579
- tasks: [{ id: 'task-1', title: 'Task 1' }],
7721
+ expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith(expect.objectContaining({
7722
+ tasks: [expect.objectContaining({ id: 'task-1', title: 'Task 1' })],
7580
7723
  archived: []
7581
- }, 'workspace-1');
7724
+ }), 'workspace-1');
7582
7725
  expect(res1.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
7583
7726
  const firstPayload = JSON.parse(String(res1.end.mock.calls.at(-1)?.[0] || '{}'));
7584
7727
  expect(Array.isArray(firstPayload.emittedEventIds)).toBe(true);
@@ -7780,10 +7923,10 @@ describe('Server Routes', () => {
7780
7923
  }, { cookie });
7781
7924
  const res = createMockRes();
7782
7925
  await match?.handler(req, res, {});
7783
- expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith({
7784
- tasks: [{ id: 'task-1', title: 'Task 1' }],
7926
+ expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith(expect.objectContaining({
7927
+ tasks: [expect.objectContaining({ id: 'task-1', title: 'Task 1' })],
7785
7928
  archived: []
7786
- }, 'workspace-local-active');
7929
+ }), 'workspace-local-active');
7787
7930
  expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
7788
7931
  expect(core.listUserWorkspaces).toHaveBeenCalledWith('user-1');
7789
7932
  });
@@ -8060,15 +8203,15 @@ describe('Server Routes', () => {
8060
8203
  }, { cookie });
8061
8204
  const res = createMockRes();
8062
8205
  await match?.handler(req, res, {});
8063
- expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith({
8064
- tasks: [{
8206
+ expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith(expect.objectContaining({
8207
+ tasks: [expect.objectContaining({
8065
8208
  id: 'task-cancelled-1',
8066
8209
  title: 'Cancelled Task',
8067
8210
  status: 'cancelled',
8068
8211
  canceledReason: 'No longer needed'
8069
- }],
8212
+ })],
8070
8213
  archived: []
8071
- }, 'workspace-1');
8214
+ }), 'workspace-1');
8072
8215
  expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
8073
8216
  });
8074
8217
  it('POST /api/taskforce/sync/workspace/push should preserve workstreamId in upsert payload', async () => {
@@ -8109,15 +8252,15 @@ describe('Server Routes', () => {
8109
8252
  }, { cookie });
8110
8253
  const res = createMockRes();
8111
8254
  await match?.handler(req, res, {});
8112
- expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith({
8113
- tasks: [{
8255
+ expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith(expect.objectContaining({
8256
+ tasks: [expect.objectContaining({
8114
8257
  id: 'task-workstream-1',
8115
8258
  title: 'Linked task',
8116
8259
  status: 'task',
8117
8260
  workstreamId: 'workstream-1'
8118
- }],
8261
+ })],
8119
8262
  archived: []
8120
- }, 'workspace-1');
8263
+ }), 'workspace-1');
8121
8264
  expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
8122
8265
  });
8123
8266
  it('POST /api/taskforce/sync/workspace/push should route archived upserts into archived snapshot list', async () => {
@@ -8158,15 +8301,15 @@ describe('Server Routes', () => {
8158
8301
  }, { cookie });
8159
8302
  const res = createMockRes();
8160
8303
  await match?.handler(req, res, {});
8161
- expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith({
8304
+ expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith(expect.objectContaining({
8162
8305
  tasks: [],
8163
- archived: [{
8306
+ archived: [expect.objectContaining({
8164
8307
  id: 'task-archived-1',
8165
8308
  title: 'Archived Task',
8166
8309
  workstreamId: 'workstream-archived-1',
8167
8310
  isArchived: true
8168
- }]
8169
- }, 'workspace-1');
8311
+ })]
8312
+ }), 'workspace-1');
8170
8313
  expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
8171
8314
  });
8172
8315
  it('GET /api/taskforce/sync/workspace/pull should include canceledReason and archived marker in response payload', async () => {
@@ -8292,7 +8435,7 @@ describe('Server Routes', () => {
8292
8435
  expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
8293
8436
  const response = JSON.parse(String(res.end.mock.calls[0]?.[0] || '{}'));
8294
8437
  const changes = Array.isArray(response?.changes) ? response.changes : [];
8295
- expect(changes).toContainEqual({
8438
+ expect(changes).toContainEqual(expect.objectContaining({
8296
8439
  op: 'taxonomy-upsert',
8297
8440
  taxonomyState: {
8298
8441
  categories: [{ value: 'default', label: 'General' }],
@@ -8328,8 +8471,9 @@ describe('Server Routes', () => {
8328
8471
  filterEnabled: true,
8329
8472
  sortEnabled: true
8330
8473
  }],
8474
+ updatedAt: '2026-02-22T10:00:00.000Z',
8331
8475
  watermark: '2026-02-22T10:00:00.000Z'
8332
- });
8476
+ }));
8333
8477
  });
8334
8478
  it('POST /api/taskforce/workspaces should create workspace for authenticated user', async () => {
8335
8479
  const authConfig = {
@@ -8761,6 +8905,73 @@ describe('Server Routes', () => {
8761
8905
  expect(payload.authenticated).toBe(true);
8762
8906
  expect(payload.avatarUrl).toBe('/api/taskforce/context/users%2Fu-avatar%2Fprofile%2Favatar%2Fdraft.png');
8763
8907
  });
8908
+ it('GET /api/taskforce/auth/session should not wait for avatar cleanup before responding', async () => {
8909
+ const authConfig = {
8910
+ runtimeMode: 'cloud',
8911
+ enabled: true,
8912
+ requiredForApi: true,
8913
+ excludedPaths: ['/api/taskforce/health', '/api/taskforce/auth'],
8914
+ defaultRole: 'member',
8915
+ defaultWorkspaceId: 'default',
8916
+ sessionCookieName: 'taskforce_session',
8917
+ sessionSecret: 'test-secret',
8918
+ sessionTtlSeconds: 3600
8919
+ };
8920
+ const deleteObjectCompletion = { resolve: null };
8921
+ const deleteObjectSpy = vi.spyOn(ObjectStorageClient.prototype, 'deleteObject').mockImplementation(() => new Promise((resolve) => {
8922
+ deleteObjectCompletion.resolve = resolve;
8923
+ }));
8924
+ routes = createRoutes(core, {
8925
+ authConfig,
8926
+ objectStorage: {
8927
+ provider: 'r2',
8928
+ r2: {
8929
+ accountId: 'test-account',
8930
+ bucket: 'taskforce-test',
8931
+ endpoint: 'https://example.com',
8932
+ accessKeyId: 'test-key',
8933
+ secretAccessKey: 'test-secret',
8934
+ keyPrefix: '',
8935
+ signedUploadTtlSeconds: 60,
8936
+ signedDownloadTtlSeconds: 60
8937
+ }
8938
+ }
8939
+ });
8940
+ core.cleanupStaleUserAvatarUploadDrafts.mockReturnValueOnce([{
8941
+ draftId: 'avatar-draft-stale',
8942
+ userId: 'u-avatar',
8943
+ storageKey: 'users/u-avatar/profile/avatar/avatar-draft-stale.png',
8944
+ discardedAt: null,
8945
+ consumedAt: null,
8946
+ finalizedAt: null,
8947
+ createdAt: '2026-04-14T00:00:00.000Z',
8948
+ updatedAt: '2026-04-14T00:00:00.000Z'
8949
+ }]);
8950
+ const cookie = createSessionTokenCookie('session-avatar-cleanup', authConfig, {
8951
+ userId: 'u-avatar',
8952
+ role: 'member',
8953
+ workspaceId: 'workspace-1'
8954
+ });
8955
+ const match = matchRoute('GET', '/api/taskforce/auth/session', routes);
8956
+ const req = createMockReq('GET', '/api/taskforce/auth/session', undefined, { cookie });
8957
+ const res = createMockRes();
8958
+ try {
8959
+ const outcome = await Promise.race([
8960
+ match?.handler(req, res, {}).then(() => 'completed'),
8961
+ new Promise((resolve) => setTimeout(() => resolve('timeout'), 25))
8962
+ ]);
8963
+ expect(outcome).toBe('completed');
8964
+ const payload = JSON.parse(res.end.mock.calls[0][0]);
8965
+ expect(payload.authenticated).toBe(true);
8966
+ expect(deleteObjectSpy).toHaveBeenCalledWith('users/u-avatar/profile/avatar/avatar-draft-stale.png');
8967
+ }
8968
+ finally {
8969
+ if (deleteObjectCompletion.resolve) {
8970
+ deleteObjectCompletion.resolve(true);
8971
+ }
8972
+ deleteObjectSpy.mockRestore();
8973
+ }
8974
+ });
8764
8975
  it('POST /api/taskforce/auth/profile should save avatar by validated draft id', async () => {
8765
8976
  const authConfig = {
8766
8977
  runtimeMode: 'cloud',
@@ -9014,13 +9225,68 @@ describe('Server Routes', () => {
9014
9225
  allowed: true
9015
9226
  });
9016
9227
  });
9228
+ it('GET /api/taskforce/account/team-management-access should still resolve access when billing tables are unavailable', async () => {
9229
+ core.resolveUserAccess.mockReturnValueOnce({
9230
+ userId: 'u1',
9231
+ workspaceId: 'workspace-1',
9232
+ role: 'owner',
9233
+ disabled: false,
9234
+ betaAccess: true
9235
+ });
9236
+ core.getAccountAccessStatus.mockReturnValueOnce({
9237
+ gate: 'ok',
9238
+ hasEntitlement: true,
9239
+ canAccessApp: true,
9240
+ canAccessPlans: true,
9241
+ planSelectionRequired: false,
9242
+ message: null,
9243
+ entitlement: {
9244
+ state: 'active',
9245
+ planId: 'collab',
9246
+ planVersionId: 'collab-v1'
9247
+ }
9248
+ });
9249
+ core.getAccountEntitlementsPayload.mockReturnValueOnce({
9250
+ state: 'active',
9251
+ planId: 'collab',
9252
+ features: {
9253
+ 'collaboration.team_management': { allowed: true }
9254
+ }
9255
+ });
9256
+ const baseDb = core.getDatabaseAdapter();
9257
+ core.getDatabaseAdapter = vi.fn().mockReturnValue({
9258
+ ...baseDb,
9259
+ prepare: vi.fn((sql) => {
9260
+ if (sql.includes('FROM user_billing') || sql.includes('FROM plan_catalog')) {
9261
+ throw new Error('no such table');
9262
+ }
9263
+ return baseDb.prepare(sql);
9264
+ })
9265
+ });
9266
+ const match = matchRoute('GET', '/api/taskforce/account/team-management-access', routes);
9267
+ const req = createMockReq('GET', '/api/taskforce/account/team-management-access?workspaceId=workspace-1', undefined, {
9268
+ 'x-taskforce-user-id': 'u1'
9269
+ });
9270
+ const res = createMockRes();
9271
+ await match?.handler(req, res, {});
9272
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
9273
+ const payload = JSON.parse(res.end.mock.calls[0][0]);
9274
+ expect(payload).toMatchObject({
9275
+ workspaceId: 'workspace-1',
9276
+ role: 'owner',
9277
+ canManageWorkspace: true,
9278
+ teamPlanMode: 'team',
9279
+ allowed: true,
9280
+ gate: 'ok',
9281
+ planSelectionRequired: false
9282
+ });
9283
+ });
9017
9284
  it('GET /api/taskforce/account/access-status should return pending plan-selection gate', async () => {
9018
9285
  core.getAccountAccessStatus.mockReturnValueOnce({
9019
9286
  gate: 'plan_selection_required',
9020
9287
  hasEntitlement: false,
9021
9288
  canAccessApp: false,
9022
9289
  canAccessPlans: true,
9023
- signupMonetizationStrategy: 'plan_selection_required',
9024
9290
  planSelectionRequired: true,
9025
9291
  message: 'Choose a plan to continue.',
9026
9292
  entitlement: null
@@ -9046,7 +9312,6 @@ describe('Server Routes', () => {
9046
9312
  hasEntitlement: false,
9047
9313
  canAccessApp: false,
9048
9314
  canAccessPlans: true,
9049
- signupMonetizationStrategy: 'plan_selection_required',
9050
9315
  planSelectionRequired: true,
9051
9316
  message: 'Choose a plan to continue.',
9052
9317
  entitlement: null
@@ -9061,6 +9326,87 @@ describe('Server Routes', () => {
9061
9326
  expect(res.writeHead).toHaveBeenCalledWith(403, expect.any(Object));
9062
9327
  expect(res.end).toHaveBeenCalledWith(expect.stringContaining('"code":"PLAN_SELECTION_REQUIRED"'));
9063
9328
  });
9329
+ it('GET /api/taskforce/account/profile-summary should return checkout-pending recovery context', async () => {
9330
+ core.resolveUserAccess.mockReturnValueOnce({
9331
+ userId: 'u1',
9332
+ workspaceId: 'workspace-1',
9333
+ role: 'owner',
9334
+ disabled: false,
9335
+ betaAccess: true
9336
+ });
9337
+ core.getAccountAccessStatus.mockReturnValueOnce({
9338
+ gate: 'checkout_pending',
9339
+ hasEntitlement: true,
9340
+ canAccessApp: false,
9341
+ canAccessPlans: true,
9342
+ planSelectionRequired: false,
9343
+ message: 'Complete checkout to continue.',
9344
+ entitlement: {
9345
+ accountId: 'u1',
9346
+ planId: 'pro',
9347
+ planVersionId: 'pro-v1',
9348
+ state: 'checkout_pending'
9349
+ }
9350
+ });
9351
+ core.getAccountEntitlementsPayload.mockImplementationOnce(() => {
9352
+ const error = new Error('Entitlement not found');
9353
+ error.code = 'ENTITLEMENT_NOT_FOUND';
9354
+ throw error;
9355
+ });
9356
+ const match = matchRoute('GET', '/api/taskforce/account/profile-summary', routes);
9357
+ const req = createMockReq('GET', '/api/taskforce/account/profile-summary?workspaceId=workspace-1', undefined, {
9358
+ 'x-taskforce-user-id': 'u1'
9359
+ });
9360
+ const res = createMockRes();
9361
+ await match?.handler(req, res, {});
9362
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
9363
+ const payload = JSON.parse(res.end.mock.calls[0][0]);
9364
+ expect(payload).toMatchObject({
9365
+ planId: 'pro',
9366
+ planVersionId: 'pro-v1',
9367
+ entitlementState: 'checkout_pending',
9368
+ gate: 'checkout_pending',
9369
+ message: 'Complete checkout to continue.',
9370
+ billingConflictCode: null,
9371
+ planSelectionRequired: false,
9372
+ workspaceId: 'workspace-1',
9373
+ workspaceRole: 'owner',
9374
+ canManageWorkspace: true,
9375
+ teamPlanMode: 'personal',
9376
+ teamManagementAllowed: false
9377
+ });
9378
+ });
9379
+ it('GET /api/taskforce/account/entitlements should surface checkout-pending gate when billing is not complete', async () => {
9380
+ core.getAccountEntitlementsPayload.mockImplementationOnce(() => {
9381
+ const error = new Error('Entitlement not found');
9382
+ error.code = 'ENTITLEMENT_NOT_FOUND';
9383
+ throw error;
9384
+ });
9385
+ core.getAccountAccessStatus.mockReturnValueOnce({
9386
+ gate: 'checkout_pending',
9387
+ hasEntitlement: true,
9388
+ canAccessApp: false,
9389
+ canAccessPlans: true,
9390
+ planSelectionRequired: false,
9391
+ message: 'Complete checkout to continue.',
9392
+ entitlement: {
9393
+ accountId: 'u1',
9394
+ planId: 'pro',
9395
+ planVersionId: 'pro-v1',
9396
+ state: 'checkout_pending'
9397
+ }
9398
+ });
9399
+ const match = matchRoute('GET', '/api/taskforce/account/entitlements', routes);
9400
+ const req = createMockReq('GET', '/api/taskforce/account/entitlements', undefined, {
9401
+ 'x-taskforce-user-id': 'u1',
9402
+ 'x-taskforce-workspace-id': 'workspace-1'
9403
+ });
9404
+ const res = createMockRes();
9405
+ await match?.handler(req, res, {});
9406
+ expect(res.writeHead).toHaveBeenCalledWith(403, expect.any(Object));
9407
+ expect(res.end).toHaveBeenCalledWith(expect.stringContaining('"code":"CHECKOUT_PENDING"'));
9408
+ expect(res.end).toHaveBeenCalledWith(expect.stringContaining('"gate":"checkout_pending"'));
9409
+ });
9064
9410
  it('GET /api/taskforce/config should block authenticated users who must select a plan first', async () => {
9065
9411
  const authConfig = {
9066
9412
  runtimeMode: 'cloud',
@@ -9084,7 +9430,6 @@ describe('Server Routes', () => {
9084
9430
  hasEntitlement: false,
9085
9431
  canAccessApp: false,
9086
9432
  canAccessPlans: true,
9087
- signupMonetizationStrategy: 'plan_selection_required',
9088
9433
  planSelectionRequired: true,
9089
9434
  message: 'Choose a plan to continue.',
9090
9435
  entitlement: null
@@ -9123,7 +9468,6 @@ describe('Server Routes', () => {
9123
9468
  hasEntitlement: false,
9124
9469
  canAccessApp: false,
9125
9470
  canAccessPlans: true,
9126
- signupMonetizationStrategy: 'auto_assign_default',
9127
9471
  planSelectionRequired: false,
9128
9472
  message: 'No entitlement is linked to this account.',
9129
9473
  entitlement: null
@@ -9716,6 +10060,64 @@ describe('Server Routes', () => {
9716
10060
  }));
9717
10061
  expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
9718
10062
  });
10063
+ it('DELETE /api/taskforce/admin/system-users/:id should purge a user in allowed cloud environments', async () => {
10064
+ const authConfig = { runtimeMode: 'cloud', enabled: false };
10065
+ routes = createRoutes(core, { authConfig });
10066
+ core.purgeSystemUser.mockReturnValue({
10067
+ targetUserId: 'u1',
10068
+ email: 'u1@example.com',
10069
+ unassignedTaskCount: 2,
10070
+ clearedInitiativeOwnerCount: 0,
10071
+ clearedWorkstreamOwnerCount: 0,
10072
+ deletedWorkspaceMembershipCount: 1,
10073
+ deletedAuthTokenCount: 1,
10074
+ deletedAvatarDraftCount: 0,
10075
+ deletedMcpAccessTokenCount: 1,
10076
+ deletedMcpOauthAuthCodeCount: 0,
10077
+ deletedMcpOauthConnectorGrantCount: 0,
10078
+ deletedUiStateCount: 1,
10079
+ deletedUserPreferenceCount: 1,
10080
+ deletedEntitlementCount: 1,
10081
+ deletedUserBillingCount: 1,
10082
+ deletedUserCount: 1
10083
+ });
10084
+ const match = matchRoute('DELETE', '/api/taskforce/admin/system-users/u1', routes);
10085
+ const req = createMockReq('DELETE', '/api/taskforce/admin/system-users/u1', { emailConfirmation: 'u1@example.com' }, {
10086
+ 'x-taskforce-system-role': 'system_admin',
10087
+ 'x-taskforce-user-id': 'admin-user',
10088
+ host: 'performance-app.taskforcehq.ai',
10089
+ 'x-forwarded-proto': 'https'
10090
+ });
10091
+ const res = createMockRes();
10092
+ await match?.handler(req, res, { id: 'u1' });
10093
+ expect(core.purgeSystemUser).toHaveBeenCalledWith({
10094
+ targetUserId: 'u1',
10095
+ actorUserId: 'admin-user',
10096
+ emailConfirmation: 'u1@example.com'
10097
+ });
10098
+ expect(core.addAdminAuditLog).toHaveBeenCalledWith(expect.objectContaining({
10099
+ action: 'purge-system-user',
10100
+ actorRole: 'system_admin',
10101
+ workspaceId: 'system'
10102
+ }));
10103
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
10104
+ });
10105
+ it('DELETE /api/taskforce/admin/system-users/:id should reject production environment purges', async () => {
10106
+ const authConfig = { runtimeMode: 'cloud', enabled: false };
10107
+ routes = createRoutes(core, { authConfig });
10108
+ const match = matchRoute('DELETE', '/api/taskforce/admin/system-users/u1', routes);
10109
+ const req = createMockReq('DELETE', '/api/taskforce/admin/system-users/u1', { emailConfirmation: 'u1@example.com' }, {
10110
+ 'x-taskforce-system-role': 'system_admin',
10111
+ 'x-taskforce-user-id': 'admin-user',
10112
+ host: 'app.taskforcehq.ai',
10113
+ 'x-forwarded-proto': 'https'
10114
+ });
10115
+ const res = createMockRes();
10116
+ await match?.handler(req, res, { id: 'u1' });
10117
+ expect(core.purgeSystemUser).not.toHaveBeenCalled();
10118
+ expect(res.writeHead).toHaveBeenCalledWith(403, expect.any(Object));
10119
+ expect(res.end).toHaveBeenCalledWith(expect.stringContaining('"code":"SYSTEM_USER_PURGE_ENVIRONMENT_FORBIDDEN"'));
10120
+ });
9719
10121
  it('removed workspace-based plan route should not match', () => {
9720
10122
  expect(matchRoute('PATCH', '/api/taskforce/admin/workspace/plan', routes)).toBeNull();
9721
10123
  expect(matchRoute('GET', '/api/taskforce/workspace/assignee-options', routes)).not.toBeNull();
@@ -10033,7 +10435,7 @@ describe('Server Routes', () => {
10033
10435
  await match?.handler(req, res, {});
10034
10436
  expect(res.writeHead).toHaveBeenCalledWith(403, expect.any(Object));
10035
10437
  });
10036
- it('GET /api/taskforce/admin/system-settings should include signup monetization fields', async () => {
10438
+ it('GET /api/taskforce/admin/system-settings should include onboarding policy fields', async () => {
10037
10439
  core.getGlobalSettings.mockReturnValue({
10038
10440
  auth: {
10039
10441
  allowSelfRegistration: true,
@@ -10041,26 +10443,30 @@ describe('Server Routes', () => {
10041
10443
  defaultNewUserBetaAccess: false,
10042
10444
  authRateLimitEnabled: true,
10043
10445
  authRateLimitMaxRequests: 10,
10044
- authRateLimitWindowMs: 900000,
10045
- signupMonetizationStrategy: 'auto_assign_default',
10046
- defaultSignupPlanVersionId: 'starter-v1'
10446
+ authRateLimitWindowMs: 900000
10447
+ },
10448
+ billing: {
10449
+ onboardingPolicy: {
10450
+ mode: 'auto_grant_free',
10451
+ planVersionId: 'starter-v1'
10452
+ }
10047
10453
  }
10048
10454
  });
10049
- core.getSignupMonetizationSettings.mockReturnValue({
10050
- strategy: 'auto_assign_default',
10051
- defaultSignupPlanVersionId: 'starter-v1'
10455
+ core.getOnboardingPolicy.mockReturnValue({
10456
+ mode: 'auto_grant_free',
10457
+ planVersionId: 'starter-v1'
10052
10458
  });
10053
10459
  const match = matchRoute('GET', '/api/taskforce/admin/system-settings', routes);
10054
10460
  const req = createMockReq('GET', '/api/taskforce/admin/system-settings', undefined, { 'x-taskforce-role': 'admin' });
10055
10461
  const res = createMockRes();
10056
10462
  await match?.handler(req, res, {});
10057
10463
  const payload = JSON.parse(res.end.mock.calls[0][0]);
10058
- expect(payload.status.signupMonetizationStrategy).toBe('auto_assign_default');
10059
- expect(payload.status.defaultSignupPlanVersionId).toBe('starter-v1');
10060
- expect(payload.settings.signupMonetizationStrategy).toBe('auto_assign_default');
10061
- expect(payload.settings.defaultSignupPlanVersionId).toBe('starter-v1');
10464
+ expect(payload.status.onboardingPolicyMode).toBe('auto_grant_free');
10465
+ expect(payload.status.onboardingPlanVersionId).toBe('starter-v1');
10466
+ expect(payload.settings.onboardingPolicyMode).toBe('auto_grant_free');
10467
+ expect(payload.settings.onboardingPlanVersionId).toBe('starter-v1');
10062
10468
  });
10063
- it('POST /api/taskforce/admin/system-settings should persist signup monetization fields', async () => {
10469
+ it('POST /api/taskforce/admin/system-settings should persist onboarding policy fields', async () => {
10064
10470
  const match = matchRoute('POST', '/api/taskforce/admin/system-settings', routes);
10065
10471
  const req = createMockReq('POST', '/api/taskforce/admin/system-settings', {
10066
10472
  allowSelfRegistration: true,
@@ -10069,14 +10475,14 @@ describe('Server Routes', () => {
10069
10475
  authRateLimitEnabled: true,
10070
10476
  authRateLimitMaxRequests: 10,
10071
10477
  authRateLimitWindowMs: 900000,
10072
- signupMonetizationStrategy: 'auto_assign_default',
10073
- defaultSignupPlanVersionId: 'starter-v1'
10478
+ onboardingPolicyMode: 'auto_grant_free',
10479
+ onboardingPlanVersionId: 'starter-v1'
10074
10480
  }, { 'x-taskforce-role': 'admin' });
10075
10481
  const res = createMockRes();
10076
10482
  await match?.handler(req, res, {});
10077
- expect(core.validateSignupMonetizationSettings).toHaveBeenCalledWith({
10078
- strategy: 'auto_assign_default',
10079
- defaultSignupPlanVersionId: 'starter-v1'
10483
+ expect(core.validateOnboardingPolicy).toHaveBeenCalledWith({
10484
+ mode: 'auto_grant_free',
10485
+ planVersionId: 'starter-v1'
10080
10486
  });
10081
10487
  expect(core.updateGlobalSettings).toHaveBeenCalledWith({
10082
10488
  auth: {
@@ -10085,9 +10491,50 @@ describe('Server Routes', () => {
10085
10491
  defaultNewUserBetaAccess: false,
10086
10492
  authRateLimitEnabled: true,
10087
10493
  authRateLimitMaxRequests: 10,
10088
- authRateLimitWindowMs: 900000,
10089
- signupMonetizationStrategy: 'auto_assign_default',
10090
- defaultSignupPlanVersionId: 'starter-v1'
10494
+ authRateLimitWindowMs: 900000
10495
+ },
10496
+ billing: {
10497
+ onboardingPolicy: {
10498
+ mode: 'auto_grant_free',
10499
+ planVersionId: 'starter-v1'
10500
+ }
10501
+ }
10502
+ });
10503
+ });
10504
+ it('POST /api/taskforce/admin/system-settings should preserve onboarding policy when onboarding fields are omitted', async () => {
10505
+ core.getOnboardingPolicy.mockReturnValue({
10506
+ mode: 'auto_grant_free',
10507
+ planVersionId: 'starter-v1'
10508
+ });
10509
+ const match = matchRoute('POST', '/api/taskforce/admin/system-settings', routes);
10510
+ const req = createMockReq('POST', '/api/taskforce/admin/system-settings', {
10511
+ allowSelfRegistration: false,
10512
+ requireVerifiedEmail: true,
10513
+ defaultNewUserBetaAccess: false,
10514
+ authRateLimitEnabled: true,
10515
+ authRateLimitMaxRequests: 10,
10516
+ authRateLimitWindowMs: 900000
10517
+ }, { 'x-taskforce-role': 'admin' });
10518
+ const res = createMockRes();
10519
+ await match?.handler(req, res, {});
10520
+ expect(core.validateOnboardingPolicy).toHaveBeenCalledWith({
10521
+ mode: 'auto_grant_free',
10522
+ planVersionId: 'starter-v1'
10523
+ });
10524
+ expect(core.updateGlobalSettings).toHaveBeenCalledWith({
10525
+ auth: {
10526
+ allowSelfRegistration: false,
10527
+ requireVerifiedEmail: true,
10528
+ defaultNewUserBetaAccess: false,
10529
+ authRateLimitEnabled: true,
10530
+ authRateLimitMaxRequests: 10,
10531
+ authRateLimitWindowMs: 900000
10532
+ },
10533
+ billing: {
10534
+ onboardingPolicy: {
10535
+ mode: 'auto_grant_free',
10536
+ planVersionId: 'starter-v1'
10537
+ }
10091
10538
  }
10092
10539
  });
10093
10540
  });
@@ -10196,23 +10643,25 @@ describe('Server Routes', () => {
10196
10643
  expect(res.end.mock.calls[0][0]).toContain('window.__TASKFORCE_SITE_CONFIG__ = ');
10197
10644
  expect(res.end.mock.calls[0][0]).toContain('"pricingPageEnabled":false');
10198
10645
  });
10199
- it('POST /api/taskforce/admin/system-settings should reject invalid signup monetization settings', async () => {
10200
- core.validateSignupMonetizationSettings.mockReturnValueOnce({
10646
+ it('POST /api/taskforce/admin/system-settings should reject invalid onboarding settings', async () => {
10647
+ core.validateOnboardingPolicy.mockReturnValueOnce({
10201
10648
  valid: false,
10202
- code: 'SIGNUP_PLAN_VERSION_REQUIRED',
10203
- message: 'Default signup plan version is required when signup strategy is auto-assign default.'
10649
+ code: 'ONBOARDING_PLAN_VERSION_REQUIRED',
10650
+ message: 'An onboarding plan version is required for the selected onboarding mode.'
10204
10651
  });
10205
10652
  const match = matchRoute('POST', '/api/taskforce/admin/system-settings', routes);
10206
10653
  const req = createMockReq('POST', '/api/taskforce/admin/system-settings', {
10207
- signupMonetizationStrategy: 'auto_assign_default',
10208
- defaultSignupPlanVersionId: null
10654
+ onboardingPolicyMode: 'auto_grant_free',
10655
+ onboardingPlanVersionId: null
10209
10656
  }, { 'x-taskforce-role': 'admin' });
10210
10657
  const res = createMockRes();
10211
10658
  await match?.handler(req, res, {});
10212
10659
  expect(res.writeHead).toHaveBeenCalledWith(400, expect.any(Object));
10213
10660
  expect(core.updateGlobalSettings).not.toHaveBeenCalledWith(expect.objectContaining({
10214
- auth: expect.objectContaining({
10215
- signupMonetizationStrategy: 'auto_assign_default'
10661
+ billing: expect.objectContaining({
10662
+ onboardingPolicy: expect.objectContaining({
10663
+ mode: 'auto_grant_free'
10664
+ })
10216
10665
  })
10217
10666
  }));
10218
10667
  });
@@ -11360,7 +11809,7 @@ describe('Server Routes', () => {
11360
11809
  errors: []
11361
11810
  }));
11362
11811
  });
11363
- it('PATCH /api/taskforce/task/:id should pass autosave updates through as non-audited route metadata', async () => {
11812
+ it('PATCH /api/taskforce/task/:id should pass autosave metadata through to core.updateTask', async () => {
11364
11813
  const updatedTask = { id: '123', description: 'Draft body' };
11365
11814
  core.updateTask.mockReturnValue(updatedTask);
11366
11815
  const url = '/api/taskforce/task/123';
@@ -11370,12 +11819,12 @@ describe('Server Routes', () => {
11370
11819
  await match?.handler(req, res, { id: '123' });
11371
11820
  expect(core.updateTask).toHaveBeenCalledWith('123', { description: 'Draft body' }, 'default', {
11372
11821
  actorRef: 'user',
11373
- skipTaskEvents: true
11822
+ saveSource: 'autosave'
11374
11823
  });
11375
11824
  expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
11376
11825
  expect(res.end).toHaveBeenCalledWith(JSON.stringify(updatedTask));
11377
11826
  });
11378
- it('PATCH /api/taskforce/task/:id should keep manual updates on the audited path', async () => {
11827
+ it('PATCH /api/taskforce/task/:id should pass manual save metadata through to core.updateTask', async () => {
11379
11828
  const updatedTask = { id: '123', description: 'Manual body' };
11380
11829
  core.updateTask.mockReturnValue(updatedTask);
11381
11830
  const url = '/api/taskforce/task/123';
@@ -11385,7 +11834,7 @@ describe('Server Routes', () => {
11385
11834
  await match?.handler(req, res, { id: '123' });
11386
11835
  expect(core.updateTask).toHaveBeenCalledWith('123', { description: 'Manual body' }, 'default', {
11387
11836
  actorRef: 'user',
11388
- skipTaskEvents: false
11837
+ saveSource: 'manual'
11389
11838
  });
11390
11839
  expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
11391
11840
  expect(res.end).toHaveBeenCalledWith(JSON.stringify(updatedTask));