@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.
- package/dist/Taskforce.module.css +113 -5
- package/dist/TaskforceCore.js +560 -208
- package/dist/TaskforceCore.test.js +1833 -91
- package/dist/annotatedAttachments/service.d.ts +1 -1
- package/dist/annotatedAttachments/types.d.ts +1 -1
- package/dist/components/features/AgentsModule.js +1 -1
- package/dist/components/features/AgentsModule.test.js +1 -1
- package/dist/components/features/TaskSettings.js +58 -38
- package/dist/components/features/TaskSettings.test.js +139 -40
- package/dist/components/task/TaskActionHeader.d.ts +2 -1
- package/dist/components/task/TaskActionHeader.js +3 -3
- package/dist/components/task/TaskActionHeader.test.js +8 -0
- package/dist/components/task/TaskCard.js +16 -1
- package/dist/components/task/TaskCard.test.js +31 -0
- package/dist/components/task/TaskForm.d.ts +2 -1
- package/dist/components/task/TaskForm.js +22 -4
- package/dist/components/task/TaskForm.test.js +46 -0
- package/dist/components/task/TaskKanban.d.ts +1 -1
- package/dist/components/task/TaskKanban.js +31 -160
- package/dist/components/task/TaskKanban.test.js +17 -0
- package/dist/components/task/TaskStatusActions.d.ts +6 -1
- package/dist/components/task/TaskStatusActions.js +8 -3
- package/dist/components/task/TaskStatusActions.test.js +9 -0
- package/dist/components/task/taskKanbanDropRules.d.ts +58 -0
- package/dist/components/task/taskKanbanDropRules.js +187 -0
- package/dist/components/task/taskKanbanDropRules.test.d.ts +1 -0
- package/dist/components/task/taskKanbanDropRules.test.js +114 -0
- package/dist/components/ui/Modal.d.ts +1 -1
- package/dist/components/ui/Modal.js +1 -0
- package/dist/components/views/AppShellHeader.js +5 -1
- package/dist/components/views/AppShellHeader.test.js +4 -0
- package/dist/components/views/PlanComparisonPage.d.ts +3 -2
- package/dist/components/views/PlanComparisonPage.js +4 -2
- package/dist/components/views/PlanComparisonPage.test.js +4 -0
- package/dist/components/views/PlansPage.d.ts +7 -1
- package/dist/components/views/PlansPage.js +139 -53
- package/dist/components/views/PlansPage.test.d.ts +1 -0
- package/dist/components/views/PlansPage.test.js +157 -0
- package/dist/components/views/StandaloneLayout.d.ts +1 -0
- package/dist/components/views/StandaloneLayout.js +216 -186
- package/dist/components/views/WidgetView.js +11 -2
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +8 -3
- package/dist/components/views/standalone/modals/AccountHubModal.js +16 -2
- 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/SyncStatusModal.js +1 -1
- package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
- package/dist/core/EntitlementsPolicy.test.js +2 -2
- package/dist/core/GlobalSettingsService.js +78 -18
- package/dist/core/PlanEntitlementService.d.ts +31 -5
- package/dist/core/PlanEntitlementService.js +350 -222
- package/dist/core/SignupMonetizationSettings.test.js +208 -49
- package/dist/core/SystemAdmin.test.js +186 -96
- package/dist/core/TaskActivityService.d.ts +4 -1
- package/dist/core/TaskActivityService.js +45 -0
- package/dist/core/TaskAuditTimeline.test.js +52 -1
- package/dist/core/Taskforce.d.ts +65 -20
- package/dist/core/Taskforce.js +261 -51
- package/dist/core/Taskforce.mcpAuth.test.js +0 -2
- package/dist/core/UserProfileAvatarDrafts.test.js +12 -3
- package/dist/core/WorkspacePlanMode.test.js +65 -0
- package/dist/core/types.d.ts +11 -12
- package/dist/documentReviews/service.d.ts +1 -1
- package/dist/documentReviews/types.d.ts +1 -1
- package/dist/hooks/auth/useTaskforceAuthBootstrap.d.ts +48 -0
- package/dist/hooks/auth/useTaskforceAuthBootstrap.js +219 -0
- package/dist/hooks/sync/auth.d.ts +37 -0
- package/dist/hooks/sync/auth.js +61 -0
- package/dist/hooks/sync/bootstrap.d.ts +34 -0
- package/dist/hooks/sync/bootstrap.js +52 -0
- package/dist/hooks/sync/orchestratorShared.d.ts +36 -0
- package/dist/hooks/sync/orchestratorShared.js +209 -0
- package/dist/hooks/sync/orchestratorShared.test.d.ts +1 -0
- package/dist/hooks/sync/orchestratorShared.test.js +49 -0
- package/dist/hooks/sync/realtime.d.ts +25 -0
- package/dist/hooks/sync/realtime.js +60 -0
- package/dist/hooks/sync/recovery.d.ts +69 -0
- package/dist/hooks/sync/recovery.js +118 -0
- package/dist/hooks/sync/transfers.d.ts +122 -0
- package/dist/hooks/sync/transfers.js +431 -0
- package/dist/hooks/sync/useDataVersionRefresh.d.ts +16 -0
- package/dist/hooks/sync/useDataVersionRefresh.js +73 -0
- package/dist/hooks/ui/useResourceExport.d.ts +3 -1
- package/dist/hooks/ui/useResourceExport.js +5 -2
- package/dist/hooks/ui/useWorkflowTemplates.js +11 -6
- package/dist/hooks/useRealtimeSync.js +2 -1
- package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +8 -0
- package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +1 -0
- package/dist/hooks/useSyncOrchestrator.context-assets.test.js +228 -0
- package/dist/hooks/useSyncOrchestrator.d.ts +5 -4
- package/dist/hooks/useSyncOrchestrator.js +315 -860
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +665 -8
- package/dist/hooks/useTaskMutations.js +45 -6
- package/dist/hooks/useTaskMutations.test.js +147 -0
- package/dist/hooks/useTaskforce.d.ts +11 -2
- package/dist/hooks/useTaskforce.js +290 -812
- package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
- package/dist/hooks/useWorkspaceSyncController.js +22 -10
- package/dist/hooks/workspace/useTaskforceApiRouting.test.js +17 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +166 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +434 -0
- package/dist/hooks/workspace/workspaceLocalState.d.ts +19 -0
- package/dist/hooks/workspace/workspaceLocalState.js +100 -0
- package/dist/hooks/workspace/workspaceLocalState.test.d.ts +1 -0
- package/dist/hooks/workspace/workspaceLocalState.test.js +59 -0
- package/dist/localization/locales/en-US.js +2 -2
- package/dist/mcp/canonicalAssetHelpers.d.ts +70 -0
- package/dist/mcp/canonicalAssetHelpers.js +259 -0
- package/dist/mcp/clientIntegrations.d.ts +12 -1
- package/dist/mcp/clientIntegrations.js +161 -6
- package/dist/mcp/clientIntegrations.test.js +76 -4
- package/dist/mcp/documentHelpers.d.ts +100 -0
- package/dist/mcp/documentHelpers.js +161 -0
- package/dist/mcp/runtime.js +264 -505
- package/dist/mcp/runtime.test.js +353 -89
- package/dist/mcp/taskAttachmentHelpers.d.ts +41 -0
- package/dist/mcp/taskAttachmentHelpers.js +60 -0
- package/dist/mcp/taskAttachmentTypes.d.ts +37 -0
- package/dist/mcp/taskAttachmentTypes.js +1 -0
- package/dist/migrations/billingSchemaParity.test.js +3 -0
- package/dist/migrations/taskSchemaMigrations.js +15 -0
- package/dist/plugins/vite.js +1 -1
- package/dist/runtime/appGateDefinitions.d.ts +1 -1
- package/dist/runtime/appGateDefinitions.js +1 -1
- package/dist/server/index.cookieProxy.test.js +1 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +48 -3
- package/dist/server/index.test.js +14 -2
- package/dist/server/mockStripe.d.ts +1 -0
- package/dist/server/mockStripe.js +2 -0
- package/dist/server/routes/admin.js +91 -40
- package/dist/server/routes/auth.d.ts +1 -0
- package/dist/server/routes/auth.js +231 -55
- package/dist/server/routes/billing.js +473 -39
- package/dist/server/routes/billing.test.js +972 -37
- package/dist/server/routes/shared.d.ts +30 -0
- package/dist/server/routes/shared.js +31 -3
- package/dist/server/routes/sync.integration.test.js +180 -0
- package/dist/server/routes/sync.js +125 -242
- package/dist/server/routes/tasks.js +49 -7
- package/dist/server/routes.js +11 -22
- package/dist/server/routes.test.js +530 -81
- package/dist/shared/taskActor.d.ts +10 -0
- package/dist/shared/taskActor.js +1 -0
- package/dist/sync/cloudSyncApi.d.ts +1 -0
- package/dist/sync/collaborationSyncPayload.js +4 -0
- package/dist/sync/collaborationSyncPayload.test.js +8 -0
- package/dist/sync/collaborationSyncState.d.ts +3 -1
- package/dist/sync/collaborationSyncState.js +19 -0
- package/dist/sync/contentSyncState.d.ts +10 -0
- package/dist/sync/contentSyncState.js +277 -14
- package/dist/sync/syncApplyHandlers.d.ts +87 -1
- package/dist/sync/syncApplyHandlers.js +370 -8
- package/dist/sync/syncApplyHandlers.test.d.ts +1 -0
- package/dist/sync/syncApplyHandlers.test.js +377 -0
- package/dist/sync/syncService.d.ts +9 -1
- package/dist/sync/syncService.js +57 -10
- package/dist/sync/syncService.test.js +48 -0
- package/dist/sync/workspacePullFeed.d.ts +11 -0
- package/dist/sync/workspacePullFeed.js +123 -68
- package/dist/sync/workspacePullFeed.test.js +180 -0
- package/dist/sync/workspaceSyncModel.d.ts +22 -1
- package/dist/sync/workspaceSyncModel.js +65 -0
- package/dist/sync/workspaceSyncModel.test.js +119 -0
- package/dist/types.d.ts +2 -10
- package/dist/ui/assets/AgentsModule-BLWVbuKP.js +1 -0
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-Dvi02Y7q.js → AnnotatedAttachmentWorkspace-BlS-cqnl.js} +1 -1
- package/dist/ui/assets/{DocumentWorkspace-iys3zfca.js → DocumentWorkspace-BH_6DF6x.js} +1 -1
- package/dist/ui/assets/{InitiativesModule-DJvLJI3A.js → InitiativesModule-BjR6iBf9.js} +1 -1
- package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
- package/dist/ui/assets/PlansPage-CSDQ4sLa.js +1 -0
- package/dist/ui/assets/TaskSettings-CEDy34Jo.js +9 -0
- package/dist/ui/assets/{WorkflowsModule-BguHeQaV.js → WorkflowsModule-DnFqdUME.js} +1 -1
- package/dist/ui/assets/index-DXUdtH1B.js +6 -0
- package/dist/ui/assets/index-Ii0B0rTO.css +1 -0
- package/dist/ui/assets/{vendor-icons-C8EED3Oh.js → vendor-icons-I7ZwG1z_.js} +1 -1
- package/dist/ui/index.html +4 -4
- 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 +4 -0
- package/dist/utils/taskActivity.test.js +25 -1
- package/package.json +5 -3
- package/scripts/playwright-auth.sh +4 -0
- package/dist/ui/assets/AgentsModule-JG5jc3he.js +0 -1
- package/dist/ui/assets/PlansPage-BMPlpFpw.js +0 -1
- package/dist/ui/assets/TaskSettings-Bqd4cCzW.js +0 -8
- package/dist/ui/assets/index-MXeWoebC.css +0 -1
- package/dist/ui/assets/index-YJ4k5QNa.js +0 -6
- package/dist/utils/billingStatusCache.d.ts +0 -21
- 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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
270
|
+
getOnboardingPolicy = vi.fn().mockReturnValue({
|
|
271
|
+
mode: 'require_plan_selection',
|
|
272
|
+
planVersionId: null
|
|
268
273
|
});
|
|
269
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
10046
|
-
|
|
10446
|
+
authRateLimitWindowMs: 900000
|
|
10447
|
+
},
|
|
10448
|
+
billing: {
|
|
10449
|
+
onboardingPolicy: {
|
|
10450
|
+
mode: 'auto_grant_free',
|
|
10451
|
+
planVersionId: 'starter-v1'
|
|
10452
|
+
}
|
|
10047
10453
|
}
|
|
10048
10454
|
});
|
|
10049
|
-
core.
|
|
10050
|
-
|
|
10051
|
-
|
|
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.
|
|
10059
|
-
expect(payload.status.
|
|
10060
|
-
expect(payload.settings.
|
|
10061
|
-
expect(payload.settings.
|
|
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
|
|
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
|
-
|
|
10073
|
-
|
|
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.
|
|
10078
|
-
|
|
10079
|
-
|
|
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
|
-
|
|
10090
|
-
|
|
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
|
|
10200
|
-
core.
|
|
10646
|
+
it('POST /api/taskforce/admin/system-settings should reject invalid onboarding settings', async () => {
|
|
10647
|
+
core.validateOnboardingPolicy.mockReturnValueOnce({
|
|
10201
10648
|
valid: false,
|
|
10202
|
-
code: '
|
|
10203
|
-
message: '
|
|
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
|
-
|
|
10208
|
-
|
|
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
|
-
|
|
10215
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
11837
|
+
saveSource: 'manual'
|
|
11389
11838
|
});
|
|
11390
11839
|
expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
11391
11840
|
expect(res.end).toHaveBeenCalledWith(JSON.stringify(updatedTask));
|