@taskforcehq/taskforce 0.3.301 → 0.3.303
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Taskforce.module.css +37 -2
- package/dist/TaskforceCore.filter-persistence.test.js +1 -1
- package/dist/TaskforceCore.js +72 -33
- package/dist/TaskforceCore.test.js +848 -130
- package/dist/components/features/TaskSettings.js +14 -82
- package/dist/components/features/TaskSettings.test.js +33 -70
- package/dist/components/features/settings/TaxonomyLibraryManager.js +8 -2
- package/dist/components/features/settings/TaxonomyLibraryManager.test.js +1 -1
- package/dist/components/task/TaskActionHeader.js +6 -3
- package/dist/components/task/TaskCard.js +6 -3
- package/dist/components/task/TaskForm.js +2 -2
- package/dist/components/task/TaskForm.test.js +41 -2
- package/dist/components/task/TaskKanban.test.js +17 -0
- package/dist/components/ui/ReferenceBadgeButton.js +1 -1
- package/dist/components/views/PlansPage.d.ts +3 -1
- package/dist/components/views/PlansPage.js +109 -49
- package/dist/components/views/PlansPage.test.d.ts +1 -0
- package/dist/components/views/PlansPage.test.js +100 -0
- package/dist/components/views/StandaloneLayout.d.ts +1 -0
- package/dist/components/views/StandaloneLayout.js +473 -256
- package/dist/components/views/panels/PlanningDrawer.js +18 -1
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +48 -0
- package/dist/components/views/standalone/modals/AccountHubModal.js +36 -0
- package/dist/components/views/standalone/modals/AccountHubModal.test.d.ts +1 -0
- package/dist/components/views/standalone/modals/AccountHubModal.test.js +26 -0
- package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.d.ts +9 -0
- package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.js +8 -0
- package/dist/components/views/standalone/modals/EditProfileModal.d.ts +26 -0
- package/dist/components/views/standalone/modals/EditProfileModal.js +10 -0
- package/dist/components/views/standalone/modals/HelpModal.d.ts +9 -0
- package/dist/components/views/standalone/modals/HelpModal.js +8 -0
- package/dist/components/views/standalone/modals/ScheduleWarningModal.d.ts +13 -0
- package/dist/components/views/standalone/modals/ScheduleWarningModal.js +6 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +61 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.js +38 -0
- package/dist/components/views/standalone/modals/TeamManagementModal.d.ts +64 -0
- package/dist/components/views/standalone/modals/TeamManagementModal.js +10 -0
- package/dist/components/views/standalone/modals/types.d.ts +2 -0
- package/dist/components/views/standalone/modals/types.js +1 -0
- package/dist/components/views/teamManagementAccess.d.ts +1 -1
- package/dist/components/views/teamManagementAccess.js +2 -2
- package/dist/components/views/teamManagementAccess.test.js +3 -3
- package/dist/config/deployment.js +2 -2
- package/dist/config/deployment.test.js +5 -3
- package/dist/core/AiProfileService.js +7 -2
- package/dist/core/AiProfiles.test.js +19 -0
- package/dist/core/EntitlementsPolicy.test.js +2 -2
- package/dist/core/GlobalSettingsService.js +78 -18
- package/dist/core/PlanEntitlementService.d.ts +29 -5
- package/dist/core/PlanEntitlementService.js +297 -219
- package/dist/core/SignupMonetizationSettings.test.js +173 -49
- package/dist/core/SyncReconciliationService.d.ts +2 -0
- package/dist/core/SyncReconciliationService.js +3 -0
- package/dist/core/SystemAdmin.test.js +186 -96
- package/dist/core/TaskAuditTimeline.test.js +40 -0
- package/dist/core/Taskforce.d.ts +100 -10
- package/dist/core/Taskforce.ids.test.js +133 -0
- package/dist/core/Taskforce.js +459 -127
- package/dist/core/Taskforce.mcpAuth.test.js +0 -2
- package/dist/core/UserProfileAvatarDrafts.test.d.ts +1 -0
- package/dist/core/UserProfileAvatarDrafts.test.js +111 -0
- package/dist/core/taskReferencePolicy.d.ts +28 -0
- package/dist/core/taskReferencePolicy.js +189 -0
- package/dist/core/types.d.ts +13 -12
- package/dist/hooks/tasks/useTaskFormActions.js +3 -0
- package/dist/hooks/useSyncOrchestrator.js +17 -3
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +97 -5
- package/dist/hooks/useTaskData.js +3 -2
- package/dist/hooks/useTaskData.test.js +66 -0
- package/dist/hooks/useTaskforce.d.ts +8 -0
- package/dist/hooks/useTaskforce.js +72 -19
- package/dist/hooks/useTaskforce.sync-behavior.test.js +185 -6
- package/dist/mcp/aiProfileBootstrap.d.ts +29 -0
- package/dist/mcp/aiProfileBootstrap.js +80 -0
- package/dist/mcp/aiProfileBootstrap.test.d.ts +1 -0
- package/dist/mcp/aiProfileBootstrap.test.js +68 -0
- package/dist/mcp/clientIntegrations.d.ts +2 -8
- package/dist/mcp/clientIntegrations.js +21 -40
- package/dist/mcp/clientIntegrations.test.js +28 -2
- package/dist/mcp/clientProfileDefaults.d.ts +12 -0
- package/dist/mcp/clientProfileDefaults.js +37 -0
- package/dist/mcp/runtime.d.ts +10 -7
- package/dist/mcp/runtime.js +131 -48
- package/dist/mcp/runtime.test.js +321 -83
- package/dist/migrations/billingSchemaParity.test.js +3 -0
- package/dist/migrations/taskSchemaMigrations.js +68 -1
- package/dist/qaOpen.test.d.ts +1 -0
- package/dist/qaOpen.test.js +56 -0
- package/dist/server/index.cookieProxy.test.js +2 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +17 -1
- package/dist/server/index.test.js +15 -1
- package/dist/server/mockStripe.d.ts +1 -0
- package/dist/server/mockStripe.js +2 -0
- package/dist/server/routes/admin.js +105 -45
- package/dist/server/routes/auth.d.ts +6 -0
- package/dist/server/routes/auth.js +505 -30
- package/dist/server/routes/billing.js +517 -24
- package/dist/server/routes/billing.test.js +967 -45
- package/dist/server/routes/documents.js +6 -1
- package/dist/server/routes/shared.d.ts +1 -1
- package/dist/server/routes/shared.js +16 -6
- package/dist/server/routes/sync.integration.test.js +290 -0
- package/dist/server/routes/sync.js +234 -1922
- package/dist/server/routes/tasks.js +18 -4
- package/dist/server/routes.js +29 -22
- package/dist/server/routes.test.js +639 -82
- package/dist/shared/runtimeContract.d.ts +1 -1
- package/dist/sync/cloudSyncApi.d.ts +1 -0
- package/dist/sync/collaborationSyncPayload.d.ts +17 -0
- package/dist/sync/collaborationSyncPayload.js +131 -0
- package/dist/sync/collaborationSyncPayload.test.d.ts +1 -0
- package/dist/sync/collaborationSyncPayload.test.js +104 -0
- package/dist/sync/collaborationSyncState.d.ts +15 -0
- package/dist/sync/collaborationSyncState.js +60 -0
- package/dist/sync/contentSyncPayload.d.ts +45 -0
- package/dist/sync/contentSyncPayload.js +104 -0
- package/dist/sync/contentSyncPayload.test.d.ts +1 -0
- package/dist/sync/contentSyncPayload.test.js +104 -0
- package/dist/sync/contentSyncState.d.ts +98 -0
- package/dist/sync/contentSyncState.js +1004 -0
- package/dist/sync/planningSyncPayload.d.ts +9 -0
- package/dist/sync/planningSyncPayload.js +82 -0
- package/dist/sync/planningSyncPayload.test.d.ts +1 -0
- package/dist/sync/planningSyncPayload.test.js +68 -0
- package/dist/sync/syncApplyHandlers.d.ts +83 -0
- package/dist/sync/syncApplyHandlers.js +445 -0
- package/dist/sync/syncService.d.ts +1 -0
- package/dist/sync/syncService.js +21 -8
- package/dist/sync/taskSyncPayload.d.ts +33 -0
- package/dist/sync/taskSyncPayload.js +126 -0
- package/dist/sync/taskSyncPayload.test.d.ts +1 -0
- package/dist/sync/taskSyncPayload.test.js +54 -0
- package/dist/sync/workspacePullFeed.d.ts +38 -0
- package/dist/sync/workspacePullFeed.js +349 -0
- package/dist/sync/workspacePullFeed.test.d.ts +1 -0
- package/dist/sync/workspacePullFeed.test.js +278 -0
- package/dist/sync/workspaceSyncModel.d.ts +4 -1
- package/dist/sync/workspaceSyncModel.js +9 -2
- package/dist/sync/workspaceSyncModel.test.js +29 -0
- package/dist/test/setup.js +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/ui/assets/{AgentsModule-Bdq3T1Ts.js → AgentsModule-CpsTmrIW.js} +1 -1
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-jKUVo-PJ.js → AnnotatedAttachmentWorkspace-C_TmXZwA.js} +1 -1
- package/dist/ui/assets/{DocumentWorkspace-DuQa-uIC.js → DocumentWorkspace-C3GyzrWm.js} +2 -2
- package/dist/ui/assets/{InitiativesModule-C8Pi2VTk.js → InitiativesModule-4-Rh2K2n.js} +1 -1
- package/dist/ui/assets/PlansPage-BP7AYOqr.js +1 -0
- package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +8 -0
- package/dist/ui/assets/{TaskSettings-BwB9NtmJ.css → TaskSettings-CnIBL_Eb.css} +1 -1
- package/dist/ui/assets/{WorkflowsModule-b6TLuHwJ.js → WorkflowsModule-CGk9s3NJ.js} +1 -1
- package/dist/ui/assets/index-CLIMgR6g.js +6 -0
- package/dist/ui/assets/index-DneETxcu.css +1 -0
- package/dist/ui/assets/{vendor-icons-QZyhEwgT.js → vendor-icons-pWocEipT.js} +1 -1
- package/dist/ui/index.html +3 -3
- package/dist/utils/accountProfileSummaryCache.d.ts +31 -0
- package/dist/utils/accountProfileSummaryCache.js +97 -0
- package/dist/utils/accountProfileSummaryCache.test.d.ts +1 -0
- package/dist/utils/accountProfileSummaryCache.test.js +63 -0
- package/dist/utils/taskActivity.js +17 -4
- package/dist/utils/taskActivity.test.js +27 -0
- package/dist/utils/taskNormalization.js +10 -2
- package/dist/utils/taskReferences.d.ts +11 -1
- package/dist/utils/taskReferences.js +33 -1
- package/package.json +4 -3
- package/scripts/qa-open-lib.mjs +175 -0
- package/scripts/qa-open-lib.mjs.d.ts +49 -0
- package/scripts/qa-open.mjs +352 -0
- package/dist/ui/assets/PlansPage-B6BDECSi.js +0 -1
- package/dist/ui/assets/TaskSettings-DUJgRTPU.js +0 -8
- package/dist/ui/assets/index-BiiF1cLQ.css +0 -1
- package/dist/ui/assets/index-xBWLPOEb.js +0 -6
|
@@ -236,6 +236,7 @@ vi.mock('../core/Taskforce', () => {
|
|
|
236
236
|
setSystemRole = vi.fn().mockReturnValue(true);
|
|
237
237
|
setUserDisabledGlobal = vi.fn().mockReturnValue(true);
|
|
238
238
|
setUserBetaAccessGlobal = vi.fn().mockReturnValue(true);
|
|
239
|
+
purgeSystemUser = vi.fn();
|
|
239
240
|
updateWorkspaceUserRole = vi.fn();
|
|
240
241
|
inviteWorkspaceUser = vi.fn();
|
|
241
242
|
updateWorkspaceProfile = vi.fn().mockImplementation((workspaceId, input) => ({
|
|
@@ -262,17 +263,16 @@ vi.mock('../core/Taskforce', () => {
|
|
|
262
263
|
revokeInviteAcceptanceForUser = vi.fn().mockReturnValue({ revoked: 1 });
|
|
263
264
|
getWorkspacePlanMode = vi.fn().mockReturnValue('team');
|
|
264
265
|
setWorkspacePlanMode = vi.fn().mockImplementation((_workspaceId, mode) => mode);
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
266
|
+
getOnboardingPolicy = vi.fn().mockReturnValue({
|
|
267
|
+
mode: 'require_plan_selection',
|
|
268
|
+
planVersionId: null
|
|
268
269
|
});
|
|
269
|
-
|
|
270
|
+
validateOnboardingPolicy = vi.fn().mockReturnValue({ valid: true });
|
|
270
271
|
getAccountAccessStatus = vi.fn().mockReturnValue({
|
|
271
272
|
gate: 'ok',
|
|
272
273
|
hasEntitlement: true,
|
|
273
274
|
canAccessApp: true,
|
|
274
275
|
canAccessPlans: true,
|
|
275
|
-
signupMonetizationStrategy: 'auto_assign_default',
|
|
276
276
|
planSelectionRequired: false,
|
|
277
277
|
message: null,
|
|
278
278
|
entitlement: {
|
|
@@ -462,10 +462,22 @@ vi.mock('../core/Taskforce', () => {
|
|
|
462
462
|
}));
|
|
463
463
|
resolveUserIdentity = vi.fn().mockImplementation((userId) => {
|
|
464
464
|
if (userId === 'admin-user') {
|
|
465
|
-
return { userId, email: 'admin@example.com' };
|
|
465
|
+
return { userId, email: 'admin@example.com', avatarUrl: null };
|
|
466
466
|
}
|
|
467
467
|
return null;
|
|
468
468
|
});
|
|
469
|
+
updateUserProfile = vi.fn().mockImplementation((userId, input) => ({
|
|
470
|
+
userId,
|
|
471
|
+
email: `${userId}@example.com`,
|
|
472
|
+
displayName: input.displayName,
|
|
473
|
+
avatarUrl: input.avatarUrl === undefined ? null : input.avatarUrl
|
|
474
|
+
}));
|
|
475
|
+
createUserAvatarUploadDraft = vi.fn();
|
|
476
|
+
getUserAvatarUploadDraft = vi.fn().mockReturnValue(null);
|
|
477
|
+
markUserAvatarUploadDraftFinalized = vi.fn().mockReturnValue(null);
|
|
478
|
+
markUserAvatarUploadDraftConsumed = vi.fn().mockReturnValue(null);
|
|
479
|
+
discardUserAvatarUploadDraft = vi.fn().mockReturnValue(null);
|
|
480
|
+
cleanupStaleUserAvatarUploadDrafts = vi.fn().mockReturnValue([]);
|
|
469
481
|
resolvePreferredWorkspaceId = vi.fn().mockImplementation((options) => (options?.allowDefault ? 'default' : null));
|
|
470
482
|
resolvePreferredUserWorkspaceId = vi.fn().mockImplementation((_userId, options) => {
|
|
471
483
|
const preferred = String(options?.preferredWorkspaceId || '').trim();
|
|
@@ -863,6 +875,8 @@ describe('Server Routes', () => {
|
|
|
863
875
|
authorization: 'Bearer tfmcp_ai_header_token',
|
|
864
876
|
'x-taskforce-ai-name': 'Automation Codex',
|
|
865
877
|
'x-taskforce-ai-profile-token': 'tfp_automation_codex',
|
|
878
|
+
'x-taskforce-ai-icon': 'Code2',
|
|
879
|
+
'x-taskforce-ai-color': '#10a37f',
|
|
866
880
|
'x-taskforce-ai-description': 'OpenAI Codex coding agent connected through MCP.',
|
|
867
881
|
'x-taskforce-ai-role': 'Coding Agent',
|
|
868
882
|
'x-taskforce-ai-provider': 'OpenAI',
|
|
@@ -3511,7 +3525,11 @@ describe('Server Routes', () => {
|
|
|
3511
3525
|
const req = createMockReq('POST', '/api/taskforce/task', newTask);
|
|
3512
3526
|
const res = createMockRes();
|
|
3513
3527
|
await match?.handler(req, res, {});
|
|
3514
|
-
expect(core.createTask).toHaveBeenCalledWith(newTask, {
|
|
3528
|
+
expect(core.createTask).toHaveBeenCalledWith(newTask, {
|
|
3529
|
+
workspaceId: 'default',
|
|
3530
|
+
actorRef: 'user',
|
|
3531
|
+
provisionalLocalReference: true
|
|
3532
|
+
});
|
|
3515
3533
|
expect(res.writeHead).toHaveBeenCalledWith(201, expect.any(Object));
|
|
3516
3534
|
expect(res.end).toHaveBeenCalledWith(JSON.stringify(createdTask));
|
|
3517
3535
|
});
|
|
@@ -3591,7 +3609,7 @@ describe('Server Routes', () => {
|
|
|
3591
3609
|
const payload = JSON.parse(res.end.mock.calls[0][0]);
|
|
3592
3610
|
expect(payload?.config?.baseUrl).toBe('http://127.0.0.1:4271');
|
|
3593
3611
|
expect(payload?.config?.apiBaseUrl).toBe('http://127.0.0.1:4271');
|
|
3594
|
-
expect(payload?.config?.cloudAuthBaseUrl).toBe('
|
|
3612
|
+
expect(payload?.config?.cloudAuthBaseUrl).toBe('https://app.taskforcehq.ai');
|
|
3595
3613
|
expect(payload?.config?.wsBaseUrl).toBe('http://127.0.0.1:4271');
|
|
3596
3614
|
expect(payload?.config?.cloudMcpBaseUrl).toBe('https://mcp.taskforcehq.ai');
|
|
3597
3615
|
}
|
|
@@ -3655,7 +3673,7 @@ describe('Server Routes', () => {
|
|
|
3655
3673
|
process.env.VITE_TASKFORCE_API_BASE_URL = previousApiBaseUrl;
|
|
3656
3674
|
}
|
|
3657
3675
|
});
|
|
3658
|
-
it('POST /api/taskforce/auth/register should create account
|
|
3676
|
+
it('POST /api/taskforce/auth/register should create account without a session cookie when verification is required', async () => {
|
|
3659
3677
|
const authConfig = {
|
|
3660
3678
|
runtimeMode: 'cloud',
|
|
3661
3679
|
enabled: true,
|
|
@@ -3688,6 +3706,7 @@ describe('Server Routes', () => {
|
|
|
3688
3706
|
email: 'owner@example.com',
|
|
3689
3707
|
role: 'owner'
|
|
3690
3708
|
}));
|
|
3709
|
+
expect(res.setHeader).not.toHaveBeenCalledWith('Set-Cookie', expect.any(String));
|
|
3691
3710
|
expect(res.writeHead).toHaveBeenCalledWith(201, expect.any(Object));
|
|
3692
3711
|
expect(res.end).toHaveBeenCalledWith(expect.stringContaining('"verificationRequired":true'));
|
|
3693
3712
|
});
|
|
@@ -3723,7 +3742,7 @@ describe('Server Routes', () => {
|
|
|
3723
3742
|
workspaceId: undefined,
|
|
3724
3743
|
role: 'member'
|
|
3725
3744
|
}));
|
|
3726
|
-
expect(res.setHeader).toHaveBeenCalledWith('Set-Cookie', expect.
|
|
3745
|
+
expect(res.setHeader).not.toHaveBeenCalledWith('Set-Cookie', expect.any(String));
|
|
3727
3746
|
expect(res.writeHead).toHaveBeenCalledWith(201, expect.any(Object));
|
|
3728
3747
|
expect(res.end).toHaveBeenCalledWith(expect.stringContaining('"workspaceSetupRequired":true'));
|
|
3729
3748
|
});
|
|
@@ -3762,6 +3781,7 @@ describe('Server Routes', () => {
|
|
|
3762
3781
|
const res = createMockRes();
|
|
3763
3782
|
await match?.handler(req, res, {});
|
|
3764
3783
|
expect(core.requestEmailVerification).not.toHaveBeenCalled();
|
|
3784
|
+
expect(res.setHeader).toHaveBeenCalledWith('Set-Cookie', expect.stringContaining('taskforce_session='));
|
|
3765
3785
|
expect(res.writeHead).toHaveBeenCalledWith(201, expect.any(Object));
|
|
3766
3786
|
expect(res.end).toHaveBeenCalledWith(expect.stringContaining('"verificationRequired":false'));
|
|
3767
3787
|
});
|
|
@@ -3836,7 +3856,7 @@ describe('Server Routes', () => {
|
|
|
3836
3856
|
code: 'EMAIL_ALREADY_EXISTS'
|
|
3837
3857
|
}));
|
|
3838
3858
|
});
|
|
3839
|
-
it('POST /api/taskforce/auth/register should fail when
|
|
3859
|
+
it('POST /api/taskforce/auth/register should fail when the onboarding policy is misconfigured', async () => {
|
|
3840
3860
|
const authConfig = {
|
|
3841
3861
|
runtimeMode: 'cloud',
|
|
3842
3862
|
enabled: true,
|
|
@@ -3849,7 +3869,7 @@ describe('Server Routes', () => {
|
|
|
3849
3869
|
sessionTtlSeconds: 3600
|
|
3850
3870
|
};
|
|
3851
3871
|
routes = createRoutes(core, { authConfig });
|
|
3852
|
-
const error = new Error('Configured
|
|
3872
|
+
const error = new Error('Configured onboarding plan version "missing-v9" was not found.');
|
|
3853
3873
|
error.statusCode = 500;
|
|
3854
3874
|
error.code = 'SIGNUP_PLAN_VERSION_NOT_FOUND';
|
|
3855
3875
|
core.registerPasswordUser.mockImplementationOnce(() => {
|
|
@@ -3865,7 +3885,7 @@ describe('Server Routes', () => {
|
|
|
3865
3885
|
expect(res.writeHead).toHaveBeenCalledWith(500, expect.any(Object));
|
|
3866
3886
|
expect(res.end).toHaveBeenCalledWith(JSON.stringify({
|
|
3867
3887
|
success: false,
|
|
3868
|
-
error: 'Configured
|
|
3888
|
+
error: 'Configured onboarding plan version "missing-v9" was not found.',
|
|
3869
3889
|
code: 'SIGNUP_PLAN_VERSION_NOT_FOUND'
|
|
3870
3890
|
}));
|
|
3871
3891
|
});
|
|
@@ -3882,12 +3902,19 @@ describe('Server Routes', () => {
|
|
|
3882
3902
|
sessionTtlSeconds: 3600
|
|
3883
3903
|
};
|
|
3884
3904
|
routes = createRoutes(core, { authConfig });
|
|
3905
|
+
core.inspectOneTimeAuthToken = vi.fn().mockReturnValueOnce({
|
|
3906
|
+
state: 'valid',
|
|
3907
|
+
userId: 'u-verify',
|
|
3908
|
+
workspaceId: 'workspace-1'
|
|
3909
|
+
});
|
|
3885
3910
|
core.verifyEmailWithToken.mockReturnValueOnce(true);
|
|
3886
3911
|
const match = matchRoute('POST', '/api/taskforce/auth/verify-email/confirm', routes);
|
|
3887
3912
|
const req = createMockReq('POST', '/api/taskforce/auth/verify-email/confirm', { token: 'tok1' });
|
|
3888
3913
|
const res = createMockRes();
|
|
3889
3914
|
await match?.handler(req, res, {});
|
|
3915
|
+
expect(core.inspectOneTimeAuthToken).toHaveBeenCalledWith('tok1', 'email_verification');
|
|
3890
3916
|
expect(core.verifyEmailWithToken).toHaveBeenCalledWith('tok1');
|
|
3917
|
+
expect(res.setHeader).toHaveBeenCalledWith('Set-Cookie', expect.stringContaining('taskforce_session='));
|
|
3891
3918
|
expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
3892
3919
|
});
|
|
3893
3920
|
it('POST /api/taskforce/auth/password-reset/confirm should set new password', async () => {
|
|
@@ -4172,6 +4199,44 @@ describe('Server Routes', () => {
|
|
|
4172
4199
|
expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
4173
4200
|
expect(res.end).toHaveBeenCalledWith(expect.stringContaining('"success":true'));
|
|
4174
4201
|
});
|
|
4202
|
+
it('POST /api/taskforce/auth/login should treat string false as disabled verified-email enforcement', async () => {
|
|
4203
|
+
const authConfig = {
|
|
4204
|
+
runtimeMode: 'cloud',
|
|
4205
|
+
enabled: true,
|
|
4206
|
+
requiredForApi: true,
|
|
4207
|
+
excludedPaths: ['/api/taskforce/health', '/api/taskforce/auth'],
|
|
4208
|
+
defaultRole: 'member',
|
|
4209
|
+
defaultWorkspaceId: 'default',
|
|
4210
|
+
sessionCookieName: 'taskforce_session',
|
|
4211
|
+
sessionSecret: 'test-secret',
|
|
4212
|
+
sessionTtlSeconds: 3600
|
|
4213
|
+
};
|
|
4214
|
+
core.getGlobalSettings.mockReturnValue({
|
|
4215
|
+
auth: {
|
|
4216
|
+
allowSelfRegistration: true,
|
|
4217
|
+
requireVerifiedEmail: 'false'
|
|
4218
|
+
}
|
|
4219
|
+
});
|
|
4220
|
+
routes = createRoutes(core, { authConfig });
|
|
4221
|
+
core.resolvePreferredUserWorkspaceId.mockReturnValueOnce(null);
|
|
4222
|
+
core.authenticatePasswordUserDetailed.mockReturnValueOnce({
|
|
4223
|
+
ok: true,
|
|
4224
|
+
access: { userId: 'u2', workspaceId: 'default', role: 'member' }
|
|
4225
|
+
});
|
|
4226
|
+
const match = matchRoute('POST', '/api/taskforce/auth/login', routes);
|
|
4227
|
+
const req = createMockReq('POST', '/api/taskforce/auth/login', {
|
|
4228
|
+
email: 'user@example.com',
|
|
4229
|
+
password: 'password123'
|
|
4230
|
+
});
|
|
4231
|
+
const res = createMockRes();
|
|
4232
|
+
res.setHeader = vi.fn();
|
|
4233
|
+
await match?.handler(req, res, {});
|
|
4234
|
+
expect(core.authenticatePasswordUserDetailed).toHaveBeenCalledWith(expect.objectContaining({
|
|
4235
|
+
email: 'user@example.com',
|
|
4236
|
+
requireVerifiedEmail: false
|
|
4237
|
+
}));
|
|
4238
|
+
expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
4239
|
+
});
|
|
4175
4240
|
it('POST /api/taskforce/auth/login should re-scope default session workspace to accessible membership', async () => {
|
|
4176
4241
|
const authConfig = {
|
|
4177
4242
|
runtimeMode: 'cloud',
|
|
@@ -4723,10 +4788,10 @@ describe('Server Routes', () => {
|
|
|
4723
4788
|
}, { cookie });
|
|
4724
4789
|
const res = createMockRes();
|
|
4725
4790
|
await match?.handler(req, res, {});
|
|
4726
|
-
expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith({
|
|
4727
|
-
tasks: [{ id: 'task-1', title: 'Task 1' }],
|
|
4728
|
-
archived: [{ id: 'task-2', title: 'Task 2', isArchived: true }]
|
|
4729
|
-
}, 'workspace-1');
|
|
4791
|
+
expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith(expect.objectContaining({
|
|
4792
|
+
tasks: [expect.objectContaining({ id: 'task-1', title: 'Task 1' })],
|
|
4793
|
+
archived: [expect.objectContaining({ id: 'task-2', title: 'Task 2', isArchived: true })]
|
|
4794
|
+
}), 'workspace-1');
|
|
4730
4795
|
expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
4731
4796
|
expect(res.end).toHaveBeenCalledWith(expect.stringContaining('"success":true'));
|
|
4732
4797
|
expect(core.addAdminAuditLog).toHaveBeenCalledWith(expect.objectContaining({
|
|
@@ -6371,10 +6436,10 @@ describe('Server Routes', () => {
|
|
|
6371
6436
|
const res = createMockRes();
|
|
6372
6437
|
await match?.handler(req, res, {});
|
|
6373
6438
|
expect(core.deleteTaskForSync).toHaveBeenCalledWith('task-2', 'workspace-local', { persistBackup: false });
|
|
6374
|
-
expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith({
|
|
6375
|
-
tasks: [{ id: 'task-1', title: 'Task 1' }],
|
|
6439
|
+
expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith(expect.objectContaining({
|
|
6440
|
+
tasks: [expect.objectContaining({ id: 'task-1', title: 'Task 1' })],
|
|
6376
6441
|
archived: []
|
|
6377
|
-
}, 'workspace-local');
|
|
6442
|
+
}), 'workspace-local');
|
|
6378
6443
|
expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
6379
6444
|
});
|
|
6380
6445
|
it('POST /api/taskforce/sync/workspace/apply-local should apply document review session upserts and deletes', async () => {
|
|
@@ -6812,10 +6877,10 @@ describe('Server Routes', () => {
|
|
|
6812
6877
|
});
|
|
6813
6878
|
const res = createMockRes();
|
|
6814
6879
|
await match?.handler(req, res, {});
|
|
6815
|
-
expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith({
|
|
6816
|
-
tasks: [{ id: 'task-shared-1', title: 'Shared task' }],
|
|
6880
|
+
expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith(expect.objectContaining({
|
|
6881
|
+
tasks: [expect.objectContaining({ id: 'task-shared-1', title: 'Shared task' })],
|
|
6817
6882
|
archived: []
|
|
6818
|
-
}, 'workspace-shared-local');
|
|
6883
|
+
}), 'workspace-shared-local');
|
|
6819
6884
|
expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
6820
6885
|
});
|
|
6821
6886
|
it('POST /api/taskforce/sync/workspace/apply-local should route archived upserts and preserve canceledReason', async () => {
|
|
@@ -6842,17 +6907,17 @@ describe('Server Routes', () => {
|
|
|
6842
6907
|
});
|
|
6843
6908
|
const res = createMockRes();
|
|
6844
6909
|
await match?.handler(req, res, {});
|
|
6845
|
-
expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith({
|
|
6910
|
+
expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith(expect.objectContaining({
|
|
6846
6911
|
tasks: [],
|
|
6847
|
-
archived: [{
|
|
6912
|
+
archived: [expect.objectContaining({
|
|
6848
6913
|
id: 'task-archived-cancelled',
|
|
6849
6914
|
title: 'Cancelled + Archived',
|
|
6850
6915
|
status: 'cancelled',
|
|
6851
6916
|
canceledReason: 'Scope changed',
|
|
6852
6917
|
workstreamId: 'workstream-archived-1',
|
|
6853
6918
|
isArchived: true
|
|
6854
|
-
}]
|
|
6855
|
-
}, 'workspace-local');
|
|
6919
|
+
})]
|
|
6920
|
+
}), 'workspace-local');
|
|
6856
6921
|
expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
6857
6922
|
});
|
|
6858
6923
|
it('POST /api/taskforce/sync/workspace/apply-local should prioritize delete over same-task upsert', async () => {
|
|
@@ -7519,10 +7584,10 @@ describe('Server Routes', () => {
|
|
|
7519
7584
|
}, { cookie });
|
|
7520
7585
|
const res1 = createMockRes();
|
|
7521
7586
|
await match?.handler(req1, res1, {});
|
|
7522
|
-
expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith({
|
|
7523
|
-
tasks: [{ id: 'task-1', title: 'Task 1' }],
|
|
7587
|
+
expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith(expect.objectContaining({
|
|
7588
|
+
tasks: [expect.objectContaining({ id: 'task-1', title: 'Task 1' })],
|
|
7524
7589
|
archived: []
|
|
7525
|
-
}, 'workspace-1');
|
|
7590
|
+
}), 'workspace-1');
|
|
7526
7591
|
expect(res1.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
7527
7592
|
const firstPayload = JSON.parse(String(res1.end.mock.calls.at(-1)?.[0] || '{}'));
|
|
7528
7593
|
expect(Array.isArray(firstPayload.emittedEventIds)).toBe(true);
|
|
@@ -7724,10 +7789,10 @@ describe('Server Routes', () => {
|
|
|
7724
7789
|
}, { cookie });
|
|
7725
7790
|
const res = createMockRes();
|
|
7726
7791
|
await match?.handler(req, res, {});
|
|
7727
|
-
expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith({
|
|
7728
|
-
tasks: [{ id: 'task-1', title: 'Task 1' }],
|
|
7792
|
+
expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith(expect.objectContaining({
|
|
7793
|
+
tasks: [expect.objectContaining({ id: 'task-1', title: 'Task 1' })],
|
|
7729
7794
|
archived: []
|
|
7730
|
-
}, 'workspace-local-active');
|
|
7795
|
+
}), 'workspace-local-active');
|
|
7731
7796
|
expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
7732
7797
|
expect(core.listUserWorkspaces).toHaveBeenCalledWith('user-1');
|
|
7733
7798
|
});
|
|
@@ -8004,15 +8069,15 @@ describe('Server Routes', () => {
|
|
|
8004
8069
|
}, { cookie });
|
|
8005
8070
|
const res = createMockRes();
|
|
8006
8071
|
await match?.handler(req, res, {});
|
|
8007
|
-
expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith({
|
|
8008
|
-
tasks: [{
|
|
8072
|
+
expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith(expect.objectContaining({
|
|
8073
|
+
tasks: [expect.objectContaining({
|
|
8009
8074
|
id: 'task-cancelled-1',
|
|
8010
8075
|
title: 'Cancelled Task',
|
|
8011
8076
|
status: 'cancelled',
|
|
8012
8077
|
canceledReason: 'No longer needed'
|
|
8013
|
-
}],
|
|
8078
|
+
})],
|
|
8014
8079
|
archived: []
|
|
8015
|
-
}, 'workspace-1');
|
|
8080
|
+
}), 'workspace-1');
|
|
8016
8081
|
expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
8017
8082
|
});
|
|
8018
8083
|
it('POST /api/taskforce/sync/workspace/push should preserve workstreamId in upsert payload', async () => {
|
|
@@ -8053,15 +8118,15 @@ describe('Server Routes', () => {
|
|
|
8053
8118
|
}, { cookie });
|
|
8054
8119
|
const res = createMockRes();
|
|
8055
8120
|
await match?.handler(req, res, {});
|
|
8056
|
-
expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith({
|
|
8057
|
-
tasks: [{
|
|
8121
|
+
expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith(expect.objectContaining({
|
|
8122
|
+
tasks: [expect.objectContaining({
|
|
8058
8123
|
id: 'task-workstream-1',
|
|
8059
8124
|
title: 'Linked task',
|
|
8060
8125
|
status: 'task',
|
|
8061
8126
|
workstreamId: 'workstream-1'
|
|
8062
|
-
}],
|
|
8127
|
+
})],
|
|
8063
8128
|
archived: []
|
|
8064
|
-
}, 'workspace-1');
|
|
8129
|
+
}), 'workspace-1');
|
|
8065
8130
|
expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
8066
8131
|
});
|
|
8067
8132
|
it('POST /api/taskforce/sync/workspace/push should route archived upserts into archived snapshot list', async () => {
|
|
@@ -8102,15 +8167,15 @@ describe('Server Routes', () => {
|
|
|
8102
8167
|
}, { cookie });
|
|
8103
8168
|
const res = createMockRes();
|
|
8104
8169
|
await match?.handler(req, res, {});
|
|
8105
|
-
expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith({
|
|
8170
|
+
expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith(expect.objectContaining({
|
|
8106
8171
|
tasks: [],
|
|
8107
|
-
archived: [{
|
|
8172
|
+
archived: [expect.objectContaining({
|
|
8108
8173
|
id: 'task-archived-1',
|
|
8109
8174
|
title: 'Archived Task',
|
|
8110
8175
|
workstreamId: 'workstream-archived-1',
|
|
8111
8176
|
isArchived: true
|
|
8112
|
-
}]
|
|
8113
|
-
}, 'workspace-1');
|
|
8177
|
+
})]
|
|
8178
|
+
}), 'workspace-1');
|
|
8114
8179
|
expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
8115
8180
|
});
|
|
8116
8181
|
it('GET /api/taskforce/sync/workspace/pull should include canceledReason and archived marker in response payload', async () => {
|
|
@@ -8236,7 +8301,7 @@ describe('Server Routes', () => {
|
|
|
8236
8301
|
expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
8237
8302
|
const response = JSON.parse(String(res.end.mock.calls[0]?.[0] || '{}'));
|
|
8238
8303
|
const changes = Array.isArray(response?.changes) ? response.changes : [];
|
|
8239
|
-
expect(changes).toContainEqual({
|
|
8304
|
+
expect(changes).toContainEqual(expect.objectContaining({
|
|
8240
8305
|
op: 'taxonomy-upsert',
|
|
8241
8306
|
taxonomyState: {
|
|
8242
8307
|
categories: [{ value: 'default', label: 'General' }],
|
|
@@ -8272,8 +8337,9 @@ describe('Server Routes', () => {
|
|
|
8272
8337
|
filterEnabled: true,
|
|
8273
8338
|
sortEnabled: true
|
|
8274
8339
|
}],
|
|
8340
|
+
updatedAt: '2026-02-22T10:00:00.000Z',
|
|
8275
8341
|
watermark: '2026-02-22T10:00:00.000Z'
|
|
8276
|
-
});
|
|
8342
|
+
}));
|
|
8277
8343
|
});
|
|
8278
8344
|
it('POST /api/taskforce/workspaces should create workspace for authenticated user', async () => {
|
|
8279
8345
|
const authConfig = {
|
|
@@ -8673,6 +8739,215 @@ describe('Server Routes', () => {
|
|
|
8673
8739
|
expect(payload.betaAccess).toBe(false);
|
|
8674
8740
|
expect(core.getUserBetaAccess).toHaveBeenCalledWith('u-no-workspace');
|
|
8675
8741
|
});
|
|
8742
|
+
it('GET /api/taskforce/auth/session should expose avatarUrl from the resolved profile', async () => {
|
|
8743
|
+
const authConfig = {
|
|
8744
|
+
runtimeMode: 'cloud',
|
|
8745
|
+
enabled: true,
|
|
8746
|
+
requiredForApi: true,
|
|
8747
|
+
excludedPaths: ['/api/taskforce/health', '/api/taskforce/auth'],
|
|
8748
|
+
defaultRole: 'member',
|
|
8749
|
+
defaultWorkspaceId: 'default',
|
|
8750
|
+
sessionCookieName: 'taskforce_session',
|
|
8751
|
+
sessionSecret: 'test-secret',
|
|
8752
|
+
sessionTtlSeconds: 3600
|
|
8753
|
+
};
|
|
8754
|
+
routes = createRoutes(core, { authConfig });
|
|
8755
|
+
core.resolveUserIdentity.mockReturnValueOnce({
|
|
8756
|
+
userId: 'u-avatar',
|
|
8757
|
+
email: 'avatar@example.com',
|
|
8758
|
+
displayName: 'Avatar User',
|
|
8759
|
+
avatarUrl: '/api/taskforce/context/users%2Fu-avatar%2Fprofile%2Favatar%2Fdraft.png'
|
|
8760
|
+
});
|
|
8761
|
+
const cookie = createSessionTokenCookie('session-avatar-auth', authConfig, {
|
|
8762
|
+
userId: 'u-avatar',
|
|
8763
|
+
role: 'member',
|
|
8764
|
+
workspaceId: 'workspace-1'
|
|
8765
|
+
});
|
|
8766
|
+
const match = matchRoute('GET', '/api/taskforce/auth/session', routes);
|
|
8767
|
+
const req = createMockReq('GET', '/api/taskforce/auth/session', undefined, { cookie });
|
|
8768
|
+
const res = createMockRes();
|
|
8769
|
+
await match?.handler(req, res, {});
|
|
8770
|
+
const payload = JSON.parse(res.end.mock.calls[0][0]);
|
|
8771
|
+
expect(payload.authenticated).toBe(true);
|
|
8772
|
+
expect(payload.avatarUrl).toBe('/api/taskforce/context/users%2Fu-avatar%2Fprofile%2Favatar%2Fdraft.png');
|
|
8773
|
+
});
|
|
8774
|
+
it('POST /api/taskforce/auth/profile should save avatar by validated draft id', async () => {
|
|
8775
|
+
const authConfig = {
|
|
8776
|
+
runtimeMode: 'cloud',
|
|
8777
|
+
enabled: true,
|
|
8778
|
+
requiredForApi: true,
|
|
8779
|
+
excludedPaths: ['/api/taskforce/health', '/api/taskforce/auth'],
|
|
8780
|
+
defaultRole: 'member',
|
|
8781
|
+
defaultWorkspaceId: 'default',
|
|
8782
|
+
sessionCookieName: 'taskforce_session',
|
|
8783
|
+
sessionSecret: 'test-secret',
|
|
8784
|
+
sessionTtlSeconds: 3600
|
|
8785
|
+
};
|
|
8786
|
+
routes = createRoutes(core, { authConfig });
|
|
8787
|
+
core.getUserAvatarUploadDraft.mockReturnValueOnce({
|
|
8788
|
+
draftId: 'avatar-draft-1',
|
|
8789
|
+
userId: 'u1',
|
|
8790
|
+
storageKey: 'users/u1/profile/avatar/avatar-draft-1-headshot.png',
|
|
8791
|
+
finalizedAt: '2026-04-09T00:00:00.000Z',
|
|
8792
|
+
discardedAt: null
|
|
8793
|
+
});
|
|
8794
|
+
core.resolveUserIdentity.mockReturnValueOnce({
|
|
8795
|
+
userId: 'u1',
|
|
8796
|
+
email: 'u1@example.com',
|
|
8797
|
+
displayName: 'Before',
|
|
8798
|
+
avatarUrl: null
|
|
8799
|
+
});
|
|
8800
|
+
core.updateUserProfile.mockReturnValueOnce({
|
|
8801
|
+
userId: 'u1',
|
|
8802
|
+
email: 'u1@example.com',
|
|
8803
|
+
displayName: 'Updated Name',
|
|
8804
|
+
avatarUrl: '/api/taskforce/context/users%2Fu1%2Fprofile%2Favatar%2Favatar-draft-1-headshot.png'
|
|
8805
|
+
});
|
|
8806
|
+
const cookie = createSessionTokenCookie('session-profile-auth', authConfig, {
|
|
8807
|
+
userId: 'u1',
|
|
8808
|
+
role: 'member',
|
|
8809
|
+
workspaceId: 'workspace-1'
|
|
8810
|
+
});
|
|
8811
|
+
const match = matchRoute('POST', '/api/taskforce/auth/profile', routes);
|
|
8812
|
+
const req = createMockReq('POST', '/api/taskforce/auth/profile', {
|
|
8813
|
+
displayName: 'Updated Name',
|
|
8814
|
+
avatarDraftId: 'avatar-draft-1'
|
|
8815
|
+
}, { cookie });
|
|
8816
|
+
const res = createMockRes();
|
|
8817
|
+
await match?.handler(req, res, {});
|
|
8818
|
+
expect(core.updateUserProfile).toHaveBeenCalledWith('u1', {
|
|
8819
|
+
displayName: 'Updated Name',
|
|
8820
|
+
avatarUrl: '/api/taskforce/context/users%2Fu1%2Fprofile%2Favatar%2Favatar-draft-1-headshot.png'
|
|
8821
|
+
});
|
|
8822
|
+
expect(core.addAdminAuditLog).toHaveBeenCalledWith(expect.objectContaining({
|
|
8823
|
+
action: 'auth-profile-updated',
|
|
8824
|
+
actorUserId: 'u1',
|
|
8825
|
+
actorRole: 'member'
|
|
8826
|
+
}));
|
|
8827
|
+
const payload = JSON.parse(res.end.mock.calls[0][0]);
|
|
8828
|
+
expect(payload.profile.avatarUrl).toBe('/api/taskforce/context/users%2Fu1%2Fprofile%2Favatar%2Favatar-draft-1-headshot.png');
|
|
8829
|
+
});
|
|
8830
|
+
it('POST /api/taskforce/auth/profile/avatar/init should create a signed avatar upload draft', async () => {
|
|
8831
|
+
const authConfig = {
|
|
8832
|
+
runtimeMode: 'cloud',
|
|
8833
|
+
enabled: true,
|
|
8834
|
+
requiredForApi: true,
|
|
8835
|
+
excludedPaths: ['/api/taskforce/health', '/api/taskforce/auth'],
|
|
8836
|
+
defaultRole: 'member',
|
|
8837
|
+
defaultWorkspaceId: 'default',
|
|
8838
|
+
sessionCookieName: 'taskforce_session',
|
|
8839
|
+
sessionSecret: 'test-secret',
|
|
8840
|
+
sessionTtlSeconds: 3600
|
|
8841
|
+
};
|
|
8842
|
+
const routesWithStorage = createRoutes(core, {
|
|
8843
|
+
authConfig,
|
|
8844
|
+
objectStorage: {
|
|
8845
|
+
provider: 'r2',
|
|
8846
|
+
r2: {
|
|
8847
|
+
accountId: 'acct-1',
|
|
8848
|
+
bucket: 'taskforce-docs',
|
|
8849
|
+
endpoint: 'https://example.r2.cloudflarestorage.com',
|
|
8850
|
+
accessKeyId: 'access-key-1',
|
|
8851
|
+
secretAccessKey: 'secret-key-1',
|
|
8852
|
+
keyPrefix: 'taskforce',
|
|
8853
|
+
signedUploadTtlSeconds: 600,
|
|
8854
|
+
signedDownloadTtlSeconds: 300
|
|
8855
|
+
}
|
|
8856
|
+
}
|
|
8857
|
+
});
|
|
8858
|
+
const cookie = createSessionTokenCookie('session-avatar-init', authConfig, {
|
|
8859
|
+
userId: 'u1',
|
|
8860
|
+
role: 'member',
|
|
8861
|
+
workspaceId: 'workspace-1'
|
|
8862
|
+
});
|
|
8863
|
+
const match = matchRoute('POST', '/api/taskforce/auth/profile/avatar/init', routesWithStorage);
|
|
8864
|
+
const req = createMockReq('POST', '/api/taskforce/auth/profile/avatar/init', {
|
|
8865
|
+
originalName: 'headshot.png',
|
|
8866
|
+
mimeType: 'image/png',
|
|
8867
|
+
size: 1024
|
|
8868
|
+
}, { cookie });
|
|
8869
|
+
const res = createMockRes();
|
|
8870
|
+
await match?.handler(req, res, {});
|
|
8871
|
+
const payload = JSON.parse(res.end.mock.calls[0][0]);
|
|
8872
|
+
expect(payload.success).toBe(true);
|
|
8873
|
+
expect(payload.draftId).toMatch(/^avatar-draft-/);
|
|
8874
|
+
expect(payload.relativePath).toMatch(/^users\/u1\/profile\/avatar\/avatar-draft-[a-f0-9]{32}-headshot\.png$/);
|
|
8875
|
+
expect(payload.uploadUrl).toContain('X-Amz-Signature=');
|
|
8876
|
+
expect(core.createUserAvatarUploadDraft).toHaveBeenCalledWith(expect.objectContaining({
|
|
8877
|
+
userId: 'u1',
|
|
8878
|
+
draftId: payload.draftId,
|
|
8879
|
+
storageKey: payload.relativePath,
|
|
8880
|
+
mimeType: 'image/png'
|
|
8881
|
+
}));
|
|
8882
|
+
});
|
|
8883
|
+
it('POST /api/taskforce/auth/profile/avatar/finalize should validate and finalize the uploaded draft', async () => {
|
|
8884
|
+
const authConfig = {
|
|
8885
|
+
runtimeMode: 'cloud',
|
|
8886
|
+
enabled: true,
|
|
8887
|
+
requiredForApi: true,
|
|
8888
|
+
excludedPaths: ['/api/taskforce/health', '/api/taskforce/auth'],
|
|
8889
|
+
defaultRole: 'member',
|
|
8890
|
+
defaultWorkspaceId: 'default',
|
|
8891
|
+
sessionCookieName: 'taskforce_session',
|
|
8892
|
+
sessionSecret: 'test-secret',
|
|
8893
|
+
sessionTtlSeconds: 3600
|
|
8894
|
+
};
|
|
8895
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
8896
|
+
ok: true,
|
|
8897
|
+
status: 200,
|
|
8898
|
+
statusText: 'OK',
|
|
8899
|
+
headers: { get: () => 'image/png' },
|
|
8900
|
+
arrayBuffer: async () => new Uint8Array([1, 2, 3]).buffer
|
|
8901
|
+
});
|
|
8902
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
8903
|
+
const routesWithStorage = createRoutes(core, {
|
|
8904
|
+
authConfig,
|
|
8905
|
+
objectStorage: {
|
|
8906
|
+
provider: 'r2',
|
|
8907
|
+
r2: {
|
|
8908
|
+
accountId: 'acct-1',
|
|
8909
|
+
bucket: 'taskforce-docs',
|
|
8910
|
+
endpoint: 'https://example.r2.cloudflarestorage.com',
|
|
8911
|
+
accessKeyId: 'access-key-1',
|
|
8912
|
+
secretAccessKey: 'secret-key-1',
|
|
8913
|
+
keyPrefix: 'taskforce',
|
|
8914
|
+
signedUploadTtlSeconds: 600,
|
|
8915
|
+
signedDownloadTtlSeconds: 300
|
|
8916
|
+
}
|
|
8917
|
+
}
|
|
8918
|
+
});
|
|
8919
|
+
core.getUserAvatarUploadDraft.mockReturnValueOnce({
|
|
8920
|
+
draftId: 'avatar-draft-1',
|
|
8921
|
+
userId: 'u1',
|
|
8922
|
+
storageKey: 'users/u1/profile/avatar/avatar-draft-1-headshot.png',
|
|
8923
|
+
finalizedAt: null,
|
|
8924
|
+
discardedAt: null
|
|
8925
|
+
});
|
|
8926
|
+
core.markUserAvatarUploadDraftFinalized.mockReturnValueOnce({
|
|
8927
|
+
draftId: 'avatar-draft-1',
|
|
8928
|
+
userId: 'u1',
|
|
8929
|
+
storageKey: 'users/u1/profile/avatar/avatar-draft-1-headshot.png',
|
|
8930
|
+
finalizedAt: '2026-04-09T00:00:00.000Z',
|
|
8931
|
+
discardedAt: null
|
|
8932
|
+
});
|
|
8933
|
+
const cookie = createSessionTokenCookie('session-avatar-finalize', authConfig, {
|
|
8934
|
+
userId: 'u1',
|
|
8935
|
+
role: 'member',
|
|
8936
|
+
workspaceId: 'workspace-1'
|
|
8937
|
+
});
|
|
8938
|
+
const match = matchRoute('POST', '/api/taskforce/auth/profile/avatar/finalize', routesWithStorage);
|
|
8939
|
+
const req = createMockReq('POST', '/api/taskforce/auth/profile/avatar/finalize', {
|
|
8940
|
+
draftId: 'avatar-draft-1',
|
|
8941
|
+
relativePath: 'users/u1/profile/avatar/avatar-draft-1-headshot.png'
|
|
8942
|
+
}, { cookie });
|
|
8943
|
+
const res = createMockRes();
|
|
8944
|
+
await match?.handler(req, res, {});
|
|
8945
|
+
const payload = JSON.parse(res.end.mock.calls[0][0]);
|
|
8946
|
+
expect(payload.success).toBe(true);
|
|
8947
|
+
expect(payload.avatarUrl).toBe('/api/taskforce/context/users%2Fu1%2Fprofile%2Favatar%2Favatar-draft-1-headshot.png');
|
|
8948
|
+
expect(core.markUserAvatarUploadDraftFinalized).toHaveBeenCalledWith('u1', 'avatar-draft-1');
|
|
8949
|
+
vi.unstubAllGlobals();
|
|
8950
|
+
});
|
|
8676
8951
|
it('GET /api/taskforce/admin/system-users should require system admin role', async () => {
|
|
8677
8952
|
const match = matchRoute('GET', '/api/taskforce/admin/system-users', routes);
|
|
8678
8953
|
const req = createMockReq('GET', '/api/taskforce/admin/system-users', undefined, { 'x-taskforce-system-role': 'user' });
|
|
@@ -8701,13 +8976,116 @@ describe('Server Routes', () => {
|
|
|
8701
8976
|
expect(payload.planId).toBe('collab');
|
|
8702
8977
|
expect(payload.features['collaboration.invites'].allowed).toBe(true);
|
|
8703
8978
|
});
|
|
8979
|
+
it('GET /api/taskforce/account/team-management-access should return unified role and team plan access', async () => {
|
|
8980
|
+
core.resolveUserAccess.mockReturnValueOnce({
|
|
8981
|
+
userId: 'u1',
|
|
8982
|
+
workspaceId: 'default',
|
|
8983
|
+
role: 'owner',
|
|
8984
|
+
disabled: false,
|
|
8985
|
+
betaAccess: true
|
|
8986
|
+
});
|
|
8987
|
+
const match = matchRoute('GET', '/api/taskforce/account/team-management-access', routes);
|
|
8988
|
+
const req = createMockReq('GET', '/api/taskforce/account/team-management-access', undefined, {
|
|
8989
|
+
'x-taskforce-user-id': 'u1',
|
|
8990
|
+
'x-taskforce-workspace-id': 'workspace-1',
|
|
8991
|
+
'x-taskforce-role': 'owner'
|
|
8992
|
+
});
|
|
8993
|
+
const res = createMockRes();
|
|
8994
|
+
await match?.handler(req, res, {});
|
|
8995
|
+
expect(core.resolveUserAccess).toHaveBeenCalledWith('u1', 'default', 'member');
|
|
8996
|
+
expect(core.getAccountEntitlementsPayload).toHaveBeenCalledWith({ userId: 'u1', workspaceId: 'default' });
|
|
8997
|
+
expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
8998
|
+
const payload = JSON.parse(res.end.mock.calls[0][0]);
|
|
8999
|
+
expect(payload).toMatchObject({
|
|
9000
|
+
workspaceId: 'default',
|
|
9001
|
+
role: 'owner',
|
|
9002
|
+
canManageWorkspace: true,
|
|
9003
|
+
teamPlanMode: 'team',
|
|
9004
|
+
allowed: true
|
|
9005
|
+
});
|
|
9006
|
+
});
|
|
9007
|
+
it('GET /api/taskforce/account/team-management-access should fall back to the hinted workspace role when local access cannot resolve the cloud user id', async () => {
|
|
9008
|
+
core.resolveUserAccess.mockReturnValueOnce(null);
|
|
9009
|
+
const match = matchRoute('GET', '/api/taskforce/account/team-management-access', routes);
|
|
9010
|
+
const req = createMockReq('GET', '/api/taskforce/account/team-management-access?workspaceId=workspace-1', undefined, {
|
|
9011
|
+
'x-taskforce-user-id': 'u-cloud',
|
|
9012
|
+
'x-taskforce-workspace-role': 'owner'
|
|
9013
|
+
});
|
|
9014
|
+
const res = createMockRes();
|
|
9015
|
+
await match?.handler(req, res, {});
|
|
9016
|
+
expect(core.resolveUserAccess).toHaveBeenCalledWith('u-cloud', 'workspace-1', 'member');
|
|
9017
|
+
expect(core.getAccountEntitlementsPayload).toHaveBeenCalledWith({ userId: 'u-cloud', workspaceId: 'workspace-1' });
|
|
9018
|
+
const payload = JSON.parse(res.end.mock.calls[0][0]);
|
|
9019
|
+
expect(payload).toMatchObject({
|
|
9020
|
+
workspaceId: 'workspace-1',
|
|
9021
|
+
role: 'owner',
|
|
9022
|
+
canManageWorkspace: true,
|
|
9023
|
+
teamPlanMode: 'team',
|
|
9024
|
+
allowed: true
|
|
9025
|
+
});
|
|
9026
|
+
});
|
|
9027
|
+
it('GET /api/taskforce/account/team-management-access should still resolve access when billing tables are unavailable', async () => {
|
|
9028
|
+
core.resolveUserAccess.mockReturnValueOnce({
|
|
9029
|
+
userId: 'u1',
|
|
9030
|
+
workspaceId: 'workspace-1',
|
|
9031
|
+
role: 'owner',
|
|
9032
|
+
disabled: false,
|
|
9033
|
+
betaAccess: true
|
|
9034
|
+
});
|
|
9035
|
+
core.getAccountAccessStatus.mockReturnValueOnce({
|
|
9036
|
+
gate: 'ok',
|
|
9037
|
+
hasEntitlement: true,
|
|
9038
|
+
canAccessApp: true,
|
|
9039
|
+
canAccessPlans: true,
|
|
9040
|
+
planSelectionRequired: false,
|
|
9041
|
+
message: null,
|
|
9042
|
+
entitlement: {
|
|
9043
|
+
state: 'active',
|
|
9044
|
+
planId: 'collab',
|
|
9045
|
+
planVersionId: 'collab-v1'
|
|
9046
|
+
}
|
|
9047
|
+
});
|
|
9048
|
+
core.getAccountEntitlementsPayload.mockReturnValueOnce({
|
|
9049
|
+
state: 'active',
|
|
9050
|
+
planId: 'collab',
|
|
9051
|
+
features: {
|
|
9052
|
+
'collaboration.team_management': { allowed: true }
|
|
9053
|
+
}
|
|
9054
|
+
});
|
|
9055
|
+
const baseDb = core.getDatabaseAdapter();
|
|
9056
|
+
core.getDatabaseAdapter = vi.fn().mockReturnValue({
|
|
9057
|
+
...baseDb,
|
|
9058
|
+
prepare: vi.fn((sql) => {
|
|
9059
|
+
if (sql.includes('FROM user_billing') || sql.includes('FROM plan_catalog')) {
|
|
9060
|
+
throw new Error('no such table');
|
|
9061
|
+
}
|
|
9062
|
+
return baseDb.prepare(sql);
|
|
9063
|
+
})
|
|
9064
|
+
});
|
|
9065
|
+
const match = matchRoute('GET', '/api/taskforce/account/team-management-access', routes);
|
|
9066
|
+
const req = createMockReq('GET', '/api/taskforce/account/team-management-access?workspaceId=workspace-1', undefined, {
|
|
9067
|
+
'x-taskforce-user-id': 'u1'
|
|
9068
|
+
});
|
|
9069
|
+
const res = createMockRes();
|
|
9070
|
+
await match?.handler(req, res, {});
|
|
9071
|
+
expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
9072
|
+
const payload = JSON.parse(res.end.mock.calls[0][0]);
|
|
9073
|
+
expect(payload).toMatchObject({
|
|
9074
|
+
workspaceId: 'workspace-1',
|
|
9075
|
+
role: 'owner',
|
|
9076
|
+
canManageWorkspace: true,
|
|
9077
|
+
teamPlanMode: 'team',
|
|
9078
|
+
allowed: true,
|
|
9079
|
+
gate: 'ok',
|
|
9080
|
+
planSelectionRequired: false
|
|
9081
|
+
});
|
|
9082
|
+
});
|
|
8704
9083
|
it('GET /api/taskforce/account/access-status should return pending plan-selection gate', async () => {
|
|
8705
9084
|
core.getAccountAccessStatus.mockReturnValueOnce({
|
|
8706
9085
|
gate: 'plan_selection_required',
|
|
8707
9086
|
hasEntitlement: false,
|
|
8708
9087
|
canAccessApp: false,
|
|
8709
9088
|
canAccessPlans: true,
|
|
8710
|
-
signupMonetizationStrategy: 'plan_selection_required',
|
|
8711
9089
|
planSelectionRequired: true,
|
|
8712
9090
|
message: 'Choose a plan to continue.',
|
|
8713
9091
|
entitlement: null
|
|
@@ -8733,7 +9111,6 @@ describe('Server Routes', () => {
|
|
|
8733
9111
|
hasEntitlement: false,
|
|
8734
9112
|
canAccessApp: false,
|
|
8735
9113
|
canAccessPlans: true,
|
|
8736
|
-
signupMonetizationStrategy: 'plan_selection_required',
|
|
8737
9114
|
planSelectionRequired: true,
|
|
8738
9115
|
message: 'Choose a plan to continue.',
|
|
8739
9116
|
entitlement: null
|
|
@@ -8748,6 +9125,87 @@ describe('Server Routes', () => {
|
|
|
8748
9125
|
expect(res.writeHead).toHaveBeenCalledWith(403, expect.any(Object));
|
|
8749
9126
|
expect(res.end).toHaveBeenCalledWith(expect.stringContaining('"code":"PLAN_SELECTION_REQUIRED"'));
|
|
8750
9127
|
});
|
|
9128
|
+
it('GET /api/taskforce/account/profile-summary should return checkout-pending recovery context', async () => {
|
|
9129
|
+
core.resolveUserAccess.mockReturnValueOnce({
|
|
9130
|
+
userId: 'u1',
|
|
9131
|
+
workspaceId: 'workspace-1',
|
|
9132
|
+
role: 'owner',
|
|
9133
|
+
disabled: false,
|
|
9134
|
+
betaAccess: true
|
|
9135
|
+
});
|
|
9136
|
+
core.getAccountAccessStatus.mockReturnValueOnce({
|
|
9137
|
+
gate: 'checkout_pending',
|
|
9138
|
+
hasEntitlement: true,
|
|
9139
|
+
canAccessApp: false,
|
|
9140
|
+
canAccessPlans: true,
|
|
9141
|
+
planSelectionRequired: false,
|
|
9142
|
+
message: 'Complete checkout to continue.',
|
|
9143
|
+
entitlement: {
|
|
9144
|
+
accountId: 'u1',
|
|
9145
|
+
planId: 'pro',
|
|
9146
|
+
planVersionId: 'pro-v1',
|
|
9147
|
+
state: 'checkout_pending'
|
|
9148
|
+
}
|
|
9149
|
+
});
|
|
9150
|
+
core.getAccountEntitlementsPayload.mockImplementationOnce(() => {
|
|
9151
|
+
const error = new Error('Entitlement not found');
|
|
9152
|
+
error.code = 'ENTITLEMENT_NOT_FOUND';
|
|
9153
|
+
throw error;
|
|
9154
|
+
});
|
|
9155
|
+
const match = matchRoute('GET', '/api/taskforce/account/profile-summary', routes);
|
|
9156
|
+
const req = createMockReq('GET', '/api/taskforce/account/profile-summary?workspaceId=workspace-1', undefined, {
|
|
9157
|
+
'x-taskforce-user-id': 'u1'
|
|
9158
|
+
});
|
|
9159
|
+
const res = createMockRes();
|
|
9160
|
+
await match?.handler(req, res, {});
|
|
9161
|
+
expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
9162
|
+
const payload = JSON.parse(res.end.mock.calls[0][0]);
|
|
9163
|
+
expect(payload).toMatchObject({
|
|
9164
|
+
planId: 'pro',
|
|
9165
|
+
planVersionId: 'pro-v1',
|
|
9166
|
+
entitlementState: 'checkout_pending',
|
|
9167
|
+
gate: 'checkout_pending',
|
|
9168
|
+
message: 'Complete checkout to continue.',
|
|
9169
|
+
billingConflictCode: null,
|
|
9170
|
+
planSelectionRequired: false,
|
|
9171
|
+
workspaceId: 'workspace-1',
|
|
9172
|
+
workspaceRole: 'owner',
|
|
9173
|
+
canManageWorkspace: true,
|
|
9174
|
+
teamPlanMode: 'personal',
|
|
9175
|
+
teamManagementAllowed: false
|
|
9176
|
+
});
|
|
9177
|
+
});
|
|
9178
|
+
it('GET /api/taskforce/account/entitlements should surface checkout-pending gate when billing is not complete', async () => {
|
|
9179
|
+
core.getAccountEntitlementsPayload.mockImplementationOnce(() => {
|
|
9180
|
+
const error = new Error('Entitlement not found');
|
|
9181
|
+
error.code = 'ENTITLEMENT_NOT_FOUND';
|
|
9182
|
+
throw error;
|
|
9183
|
+
});
|
|
9184
|
+
core.getAccountAccessStatus.mockReturnValueOnce({
|
|
9185
|
+
gate: 'checkout_pending',
|
|
9186
|
+
hasEntitlement: true,
|
|
9187
|
+
canAccessApp: false,
|
|
9188
|
+
canAccessPlans: true,
|
|
9189
|
+
planSelectionRequired: false,
|
|
9190
|
+
message: 'Complete checkout to continue.',
|
|
9191
|
+
entitlement: {
|
|
9192
|
+
accountId: 'u1',
|
|
9193
|
+
planId: 'pro',
|
|
9194
|
+
planVersionId: 'pro-v1',
|
|
9195
|
+
state: 'checkout_pending'
|
|
9196
|
+
}
|
|
9197
|
+
});
|
|
9198
|
+
const match = matchRoute('GET', '/api/taskforce/account/entitlements', routes);
|
|
9199
|
+
const req = createMockReq('GET', '/api/taskforce/account/entitlements', undefined, {
|
|
9200
|
+
'x-taskforce-user-id': 'u1',
|
|
9201
|
+
'x-taskforce-workspace-id': 'workspace-1'
|
|
9202
|
+
});
|
|
9203
|
+
const res = createMockRes();
|
|
9204
|
+
await match?.handler(req, res, {});
|
|
9205
|
+
expect(res.writeHead).toHaveBeenCalledWith(403, expect.any(Object));
|
|
9206
|
+
expect(res.end).toHaveBeenCalledWith(expect.stringContaining('"code":"CHECKOUT_PENDING"'));
|
|
9207
|
+
expect(res.end).toHaveBeenCalledWith(expect.stringContaining('"gate":"checkout_pending"'));
|
|
9208
|
+
});
|
|
8751
9209
|
it('GET /api/taskforce/config should block authenticated users who must select a plan first', async () => {
|
|
8752
9210
|
const authConfig = {
|
|
8753
9211
|
runtimeMode: 'cloud',
|
|
@@ -8771,7 +9229,6 @@ describe('Server Routes', () => {
|
|
|
8771
9229
|
hasEntitlement: false,
|
|
8772
9230
|
canAccessApp: false,
|
|
8773
9231
|
canAccessPlans: true,
|
|
8774
|
-
signupMonetizationStrategy: 'plan_selection_required',
|
|
8775
9232
|
planSelectionRequired: true,
|
|
8776
9233
|
message: 'Choose a plan to continue.',
|
|
8777
9234
|
entitlement: null
|
|
@@ -8810,7 +9267,6 @@ describe('Server Routes', () => {
|
|
|
8810
9267
|
hasEntitlement: false,
|
|
8811
9268
|
canAccessApp: false,
|
|
8812
9269
|
canAccessPlans: true,
|
|
8813
|
-
signupMonetizationStrategy: 'auto_assign_default',
|
|
8814
9270
|
planSelectionRequired: false,
|
|
8815
9271
|
message: 'No entitlement is linked to this account.',
|
|
8816
9272
|
entitlement: null
|
|
@@ -9403,6 +9859,64 @@ describe('Server Routes', () => {
|
|
|
9403
9859
|
}));
|
|
9404
9860
|
expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
9405
9861
|
});
|
|
9862
|
+
it('DELETE /api/taskforce/admin/system-users/:id should purge a user in allowed cloud environments', async () => {
|
|
9863
|
+
const authConfig = { runtimeMode: 'cloud', enabled: false };
|
|
9864
|
+
routes = createRoutes(core, { authConfig });
|
|
9865
|
+
core.purgeSystemUser.mockReturnValue({
|
|
9866
|
+
targetUserId: 'u1',
|
|
9867
|
+
email: 'u1@example.com',
|
|
9868
|
+
unassignedTaskCount: 2,
|
|
9869
|
+
clearedInitiativeOwnerCount: 0,
|
|
9870
|
+
clearedWorkstreamOwnerCount: 0,
|
|
9871
|
+
deletedWorkspaceMembershipCount: 1,
|
|
9872
|
+
deletedAuthTokenCount: 1,
|
|
9873
|
+
deletedAvatarDraftCount: 0,
|
|
9874
|
+
deletedMcpAccessTokenCount: 1,
|
|
9875
|
+
deletedMcpOauthAuthCodeCount: 0,
|
|
9876
|
+
deletedMcpOauthConnectorGrantCount: 0,
|
|
9877
|
+
deletedUiStateCount: 1,
|
|
9878
|
+
deletedUserPreferenceCount: 1,
|
|
9879
|
+
deletedEntitlementCount: 1,
|
|
9880
|
+
deletedUserBillingCount: 1,
|
|
9881
|
+
deletedUserCount: 1
|
|
9882
|
+
});
|
|
9883
|
+
const match = matchRoute('DELETE', '/api/taskforce/admin/system-users/u1', routes);
|
|
9884
|
+
const req = createMockReq('DELETE', '/api/taskforce/admin/system-users/u1', { emailConfirmation: 'u1@example.com' }, {
|
|
9885
|
+
'x-taskforce-system-role': 'system_admin',
|
|
9886
|
+
'x-taskforce-user-id': 'admin-user',
|
|
9887
|
+
host: 'performance-app.taskforcehq.ai',
|
|
9888
|
+
'x-forwarded-proto': 'https'
|
|
9889
|
+
});
|
|
9890
|
+
const res = createMockRes();
|
|
9891
|
+
await match?.handler(req, res, { id: 'u1' });
|
|
9892
|
+
expect(core.purgeSystemUser).toHaveBeenCalledWith({
|
|
9893
|
+
targetUserId: 'u1',
|
|
9894
|
+
actorUserId: 'admin-user',
|
|
9895
|
+
emailConfirmation: 'u1@example.com'
|
|
9896
|
+
});
|
|
9897
|
+
expect(core.addAdminAuditLog).toHaveBeenCalledWith(expect.objectContaining({
|
|
9898
|
+
action: 'purge-system-user',
|
|
9899
|
+
actorRole: 'system_admin',
|
|
9900
|
+
workspaceId: 'system'
|
|
9901
|
+
}));
|
|
9902
|
+
expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
9903
|
+
});
|
|
9904
|
+
it('DELETE /api/taskforce/admin/system-users/:id should reject production environment purges', async () => {
|
|
9905
|
+
const authConfig = { runtimeMode: 'cloud', enabled: false };
|
|
9906
|
+
routes = createRoutes(core, { authConfig });
|
|
9907
|
+
const match = matchRoute('DELETE', '/api/taskforce/admin/system-users/u1', routes);
|
|
9908
|
+
const req = createMockReq('DELETE', '/api/taskforce/admin/system-users/u1', { emailConfirmation: 'u1@example.com' }, {
|
|
9909
|
+
'x-taskforce-system-role': 'system_admin',
|
|
9910
|
+
'x-taskforce-user-id': 'admin-user',
|
|
9911
|
+
host: 'app.taskforcehq.ai',
|
|
9912
|
+
'x-forwarded-proto': 'https'
|
|
9913
|
+
});
|
|
9914
|
+
const res = createMockRes();
|
|
9915
|
+
await match?.handler(req, res, { id: 'u1' });
|
|
9916
|
+
expect(core.purgeSystemUser).not.toHaveBeenCalled();
|
|
9917
|
+
expect(res.writeHead).toHaveBeenCalledWith(403, expect.any(Object));
|
|
9918
|
+
expect(res.end).toHaveBeenCalledWith(expect.stringContaining('"code":"SYSTEM_USER_PURGE_ENVIRONMENT_FORBIDDEN"'));
|
|
9919
|
+
});
|
|
9406
9920
|
it('removed workspace-based plan route should not match', () => {
|
|
9407
9921
|
expect(matchRoute('PATCH', '/api/taskforce/admin/workspace/plan', routes)).toBeNull();
|
|
9408
9922
|
expect(matchRoute('GET', '/api/taskforce/workspace/assignee-options', routes)).not.toBeNull();
|
|
@@ -9720,7 +10234,7 @@ describe('Server Routes', () => {
|
|
|
9720
10234
|
await match?.handler(req, res, {});
|
|
9721
10235
|
expect(res.writeHead).toHaveBeenCalledWith(403, expect.any(Object));
|
|
9722
10236
|
});
|
|
9723
|
-
it('GET /api/taskforce/admin/system-settings should include
|
|
10237
|
+
it('GET /api/taskforce/admin/system-settings should include onboarding policy fields', async () => {
|
|
9724
10238
|
core.getGlobalSettings.mockReturnValue({
|
|
9725
10239
|
auth: {
|
|
9726
10240
|
allowSelfRegistration: true,
|
|
@@ -9728,26 +10242,30 @@ describe('Server Routes', () => {
|
|
|
9728
10242
|
defaultNewUserBetaAccess: false,
|
|
9729
10243
|
authRateLimitEnabled: true,
|
|
9730
10244
|
authRateLimitMaxRequests: 10,
|
|
9731
|
-
authRateLimitWindowMs: 900000
|
|
9732
|
-
|
|
9733
|
-
|
|
10245
|
+
authRateLimitWindowMs: 900000
|
|
10246
|
+
},
|
|
10247
|
+
billing: {
|
|
10248
|
+
onboardingPolicy: {
|
|
10249
|
+
mode: 'auto_grant_free',
|
|
10250
|
+
planVersionId: 'starter-v1'
|
|
10251
|
+
}
|
|
9734
10252
|
}
|
|
9735
10253
|
});
|
|
9736
|
-
core.
|
|
9737
|
-
|
|
9738
|
-
|
|
10254
|
+
core.getOnboardingPolicy.mockReturnValue({
|
|
10255
|
+
mode: 'auto_grant_free',
|
|
10256
|
+
planVersionId: 'starter-v1'
|
|
9739
10257
|
});
|
|
9740
10258
|
const match = matchRoute('GET', '/api/taskforce/admin/system-settings', routes);
|
|
9741
10259
|
const req = createMockReq('GET', '/api/taskforce/admin/system-settings', undefined, { 'x-taskforce-role': 'admin' });
|
|
9742
10260
|
const res = createMockRes();
|
|
9743
10261
|
await match?.handler(req, res, {});
|
|
9744
10262
|
const payload = JSON.parse(res.end.mock.calls[0][0]);
|
|
9745
|
-
expect(payload.status.
|
|
9746
|
-
expect(payload.status.
|
|
9747
|
-
expect(payload.settings.
|
|
9748
|
-
expect(payload.settings.
|
|
10263
|
+
expect(payload.status.onboardingPolicyMode).toBe('auto_grant_free');
|
|
10264
|
+
expect(payload.status.onboardingPlanVersionId).toBe('starter-v1');
|
|
10265
|
+
expect(payload.settings.onboardingPolicyMode).toBe('auto_grant_free');
|
|
10266
|
+
expect(payload.settings.onboardingPlanVersionId).toBe('starter-v1');
|
|
9749
10267
|
});
|
|
9750
|
-
it('POST /api/taskforce/admin/system-settings should persist
|
|
10268
|
+
it('POST /api/taskforce/admin/system-settings should persist onboarding policy fields', async () => {
|
|
9751
10269
|
const match = matchRoute('POST', '/api/taskforce/admin/system-settings', routes);
|
|
9752
10270
|
const req = createMockReq('POST', '/api/taskforce/admin/system-settings', {
|
|
9753
10271
|
allowSelfRegistration: true,
|
|
@@ -9756,14 +10274,14 @@ describe('Server Routes', () => {
|
|
|
9756
10274
|
authRateLimitEnabled: true,
|
|
9757
10275
|
authRateLimitMaxRequests: 10,
|
|
9758
10276
|
authRateLimitWindowMs: 900000,
|
|
9759
|
-
|
|
9760
|
-
|
|
10277
|
+
onboardingPolicyMode: 'auto_grant_free',
|
|
10278
|
+
onboardingPlanVersionId: 'starter-v1'
|
|
9761
10279
|
}, { 'x-taskforce-role': 'admin' });
|
|
9762
10280
|
const res = createMockRes();
|
|
9763
10281
|
await match?.handler(req, res, {});
|
|
9764
|
-
expect(core.
|
|
9765
|
-
|
|
9766
|
-
|
|
10282
|
+
expect(core.validateOnboardingPolicy).toHaveBeenCalledWith({
|
|
10283
|
+
mode: 'auto_grant_free',
|
|
10284
|
+
planVersionId: 'starter-v1'
|
|
9767
10285
|
});
|
|
9768
10286
|
expect(core.updateGlobalSettings).toHaveBeenCalledWith({
|
|
9769
10287
|
auth: {
|
|
@@ -9772,9 +10290,13 @@ describe('Server Routes', () => {
|
|
|
9772
10290
|
defaultNewUserBetaAccess: false,
|
|
9773
10291
|
authRateLimitEnabled: true,
|
|
9774
10292
|
authRateLimitMaxRequests: 10,
|
|
9775
|
-
authRateLimitWindowMs: 900000
|
|
9776
|
-
|
|
9777
|
-
|
|
10293
|
+
authRateLimitWindowMs: 900000
|
|
10294
|
+
},
|
|
10295
|
+
billing: {
|
|
10296
|
+
onboardingPolicy: {
|
|
10297
|
+
mode: 'auto_grant_free',
|
|
10298
|
+
planVersionId: 'starter-v1'
|
|
10299
|
+
}
|
|
9778
10300
|
}
|
|
9779
10301
|
});
|
|
9780
10302
|
});
|
|
@@ -9883,23 +10405,25 @@ describe('Server Routes', () => {
|
|
|
9883
10405
|
expect(res.end.mock.calls[0][0]).toContain('window.__TASKFORCE_SITE_CONFIG__ = ');
|
|
9884
10406
|
expect(res.end.mock.calls[0][0]).toContain('"pricingPageEnabled":false');
|
|
9885
10407
|
});
|
|
9886
|
-
it('POST /api/taskforce/admin/system-settings should reject invalid
|
|
9887
|
-
core.
|
|
10408
|
+
it('POST /api/taskforce/admin/system-settings should reject invalid onboarding settings', async () => {
|
|
10409
|
+
core.validateOnboardingPolicy.mockReturnValueOnce({
|
|
9888
10410
|
valid: false,
|
|
9889
|
-
code: '
|
|
9890
|
-
message: '
|
|
10411
|
+
code: 'ONBOARDING_PLAN_VERSION_REQUIRED',
|
|
10412
|
+
message: 'An onboarding plan version is required for the selected onboarding mode.'
|
|
9891
10413
|
});
|
|
9892
10414
|
const match = matchRoute('POST', '/api/taskforce/admin/system-settings', routes);
|
|
9893
10415
|
const req = createMockReq('POST', '/api/taskforce/admin/system-settings', {
|
|
9894
|
-
|
|
9895
|
-
|
|
10416
|
+
onboardingPolicyMode: 'auto_grant_free',
|
|
10417
|
+
onboardingPlanVersionId: null
|
|
9896
10418
|
}, { 'x-taskforce-role': 'admin' });
|
|
9897
10419
|
const res = createMockRes();
|
|
9898
10420
|
await match?.handler(req, res, {});
|
|
9899
10421
|
expect(res.writeHead).toHaveBeenCalledWith(400, expect.any(Object));
|
|
9900
10422
|
expect(core.updateGlobalSettings).not.toHaveBeenCalledWith(expect.objectContaining({
|
|
9901
|
-
|
|
9902
|
-
|
|
10423
|
+
billing: expect.objectContaining({
|
|
10424
|
+
onboardingPolicy: expect.objectContaining({
|
|
10425
|
+
mode: 'auto_grant_free'
|
|
10426
|
+
})
|
|
9903
10427
|
})
|
|
9904
10428
|
}));
|
|
9905
10429
|
});
|
|
@@ -10894,7 +11418,8 @@ describe('Server Routes', () => {
|
|
|
10894
11418
|
expect(core.bulkCreateTasks).toHaveBeenCalledWith([newTask], {
|
|
10895
11419
|
atomic: true,
|
|
10896
11420
|
validateOnly: true,
|
|
10897
|
-
workspaceId: 'default'
|
|
11421
|
+
workspaceId: 'default',
|
|
11422
|
+
provisionalLocalReference: true
|
|
10898
11423
|
});
|
|
10899
11424
|
expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
10900
11425
|
expect(res.end).toHaveBeenCalledWith(JSON.stringify({
|
|
@@ -10949,7 +11474,8 @@ describe('Server Routes', () => {
|
|
|
10949
11474
|
expect(core.bulkCreateTasks).toHaveBeenCalledWith(inputs, {
|
|
10950
11475
|
atomic: true,
|
|
10951
11476
|
validateOnly: false,
|
|
10952
|
-
workspaceId: 'default'
|
|
11477
|
+
workspaceId: 'default',
|
|
11478
|
+
provisionalLocalReference: true
|
|
10953
11479
|
});
|
|
10954
11480
|
expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
10955
11481
|
expect(res.end).toHaveBeenCalledWith(JSON.stringify({
|
|
@@ -11023,7 +11549,8 @@ describe('Server Routes', () => {
|
|
|
11023
11549
|
expect(core.bulkCreateTasks).toHaveBeenCalledWith(inputs, {
|
|
11024
11550
|
atomic: true,
|
|
11025
11551
|
validateOnly: true,
|
|
11026
|
-
workspaceId: 'default'
|
|
11552
|
+
workspaceId: 'default',
|
|
11553
|
+
provisionalLocalReference: true
|
|
11027
11554
|
});
|
|
11028
11555
|
expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
11029
11556
|
expect(res.end).toHaveBeenCalledWith(JSON.stringify({
|
|
@@ -11044,6 +11571,36 @@ describe('Server Routes', () => {
|
|
|
11044
11571
|
errors: []
|
|
11045
11572
|
}));
|
|
11046
11573
|
});
|
|
11574
|
+
it('PATCH /api/taskforce/task/:id should pass autosave updates through as non-audited route metadata', async () => {
|
|
11575
|
+
const updatedTask = { id: '123', description: 'Draft body' };
|
|
11576
|
+
core.updateTask.mockReturnValue(updatedTask);
|
|
11577
|
+
const url = '/api/taskforce/task/123';
|
|
11578
|
+
const match = matchRoute('PATCH', url, routes);
|
|
11579
|
+
const req = createMockReq('PATCH', url, { description: 'Draft body', saveSource: 'autosave' });
|
|
11580
|
+
const res = createMockRes();
|
|
11581
|
+
await match?.handler(req, res, { id: '123' });
|
|
11582
|
+
expect(core.updateTask).toHaveBeenCalledWith('123', { description: 'Draft body' }, 'default', {
|
|
11583
|
+
actorRef: 'user',
|
|
11584
|
+
skipTaskEvents: true
|
|
11585
|
+
});
|
|
11586
|
+
expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
11587
|
+
expect(res.end).toHaveBeenCalledWith(JSON.stringify(updatedTask));
|
|
11588
|
+
});
|
|
11589
|
+
it('PATCH /api/taskforce/task/:id should keep manual updates on the audited path', async () => {
|
|
11590
|
+
const updatedTask = { id: '123', description: 'Manual body' };
|
|
11591
|
+
core.updateTask.mockReturnValue(updatedTask);
|
|
11592
|
+
const url = '/api/taskforce/task/123';
|
|
11593
|
+
const match = matchRoute('PATCH', url, routes);
|
|
11594
|
+
const req = createMockReq('PATCH', url, { description: 'Manual body', saveSource: 'manual' });
|
|
11595
|
+
const res = createMockRes();
|
|
11596
|
+
await match?.handler(req, res, { id: '123' });
|
|
11597
|
+
expect(core.updateTask).toHaveBeenCalledWith('123', { description: 'Manual body' }, 'default', {
|
|
11598
|
+
actorRef: 'user',
|
|
11599
|
+
skipTaskEvents: false
|
|
11600
|
+
});
|
|
11601
|
+
expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
11602
|
+
expect(res.end).toHaveBeenCalledWith(JSON.stringify(updatedTask));
|
|
11603
|
+
});
|
|
11047
11604
|
it('PATCH /api/taskforce/task/:id should return rule-error status/message', async () => {
|
|
11048
11605
|
core.updateTask.mockImplementation(() => {
|
|
11049
11606
|
const err = new Error('Task update blocked by rule check');
|