@taskforcehq/taskforce 0.3.303 → 0.3.305
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/README.md +140 -247
- package/dist/Taskforce.module.css +102 -5
- package/dist/TaskforceCore.js +635 -238
- package/dist/TaskforceCore.test.js +2243 -495
- package/dist/annotatedAttachments/service.d.ts +1 -1
- package/dist/annotatedAttachments/types.d.ts +1 -1
- package/dist/compat/workspaceSyncCompat.js +6 -0
- package/dist/components/features/AgentsModule.js +1 -1
- package/dist/components/features/AgentsModule.test.js +1 -1
- package/dist/components/features/DocumentIndex.d.ts +1 -1
- package/dist/components/features/DocumentIndex.js +1 -1
- package/dist/components/features/DocumentViewer.d.ts +1 -1
- package/dist/components/features/DocumentViewer.js +32 -9
- package/dist/components/features/DocumentWorkspace.d.ts +5 -22
- package/dist/components/features/DocumentWorkspace.js +12 -155
- package/dist/components/features/DocumentWorkspace.test.js +193 -6
- package/dist/components/features/TaskSettings.js +87 -41
- package/dist/components/features/TaskSettings.test.js +180 -40
- package/dist/components/features/documentWorkspaceModel.d.ts +24 -0
- package/dist/components/features/documentWorkspaceModel.js +57 -0
- package/dist/components/features/useDocumentWorkspaceController.d.ts +34 -0
- package/dist/components/features/useDocumentWorkspaceController.js +113 -0
- 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 +9 -8
- package/dist/components/task/TaskForm.js +374 -219
- package/dist/components/task/TaskForm.test.js +482 -361
- package/dist/components/task/TaskKanban.d.ts +1 -1
- package/dist/components/task/TaskKanban.js +33 -161
- 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 +9 -10
- package/dist/components/views/PlanComparisonPage.test.js +55 -41
- package/dist/components/views/PlansPage.d.ts +5 -1
- package/dist/components/views/PlansPage.js +150 -81
- package/dist/components/views/PlansPage.test.js +367 -41
- package/dist/components/views/StandaloneLayout.js +135 -54
- package/dist/components/views/WidgetView.js +11 -2
- package/dist/components/views/panels/FilterToolbar.test.js +6 -4
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +2 -14
- package/dist/components/views/standalone/modals/AccountHubModal.js +12 -14
- package/dist/components/views/standalone/modals/AccountHubModal.test.js +24 -1
- package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
- package/dist/config/envSchema.js +1 -1
- package/dist/core/AiProfileService.d.ts +6 -1
- package/dist/core/AiProfileService.js +161 -16
- package/dist/core/AiProfileService.test.js +3 -1
- package/dist/core/AiProfiles.test.js +200 -0
- package/dist/core/AuthTokenService.test.d.ts +1 -0
- package/dist/core/AuthTokenService.test.js +78 -0
- package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
- package/dist/core/EntitlementsPolicy.test.js +75 -13
- package/dist/core/PlanEntitlementService.d.ts +9 -1
- package/dist/core/PlanEntitlementService.js +144 -19
- package/dist/core/PlanVersionPolicy.test.js +19 -26
- package/dist/core/SignupMonetizationSettings.test.js +75 -15
- package/dist/core/SystemAdmin.test.js +212 -56
- 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/TaskTaxonomyValidation.test.js +53 -0
- package/dist/core/Taskforce.d.ts +18 -11
- package/dist/core/Taskforce.js +93 -12
- package/dist/core/WorkspacePlanMode.test.js +73 -8
- package/dist/core/shared.d.ts +2 -0
- package/dist/core/shared.js +11 -0
- 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 +63 -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 +123 -0
- package/dist/hooks/sync/transfers.js +435 -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 +12 -8
- package/dist/hooks/useSyncOrchestrator.js +388 -879
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +641 -6
- package/dist/hooks/useTaskMutations.d.ts +2 -1
- package/dist/hooks/useTaskMutations.js +57 -7
- package/dist/hooks/useTaskMutations.test.js +176 -0
- package/dist/hooks/useTaskforce.d.ts +13 -4
- package/dist/hooks/useTaskforce.js +302 -817
- package/dist/hooks/useTaskforce.sync-behavior.test.js +78 -0
- package/dist/hooks/useWorkspaceSyncController.d.ts +4 -1
- package/dist/hooks/useWorkspaceSyncController.js +33 -11
- package/dist/hooks/useWorkspaceSyncController.test.js +1 -0
- 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/adminWorkspaceRegistrar.d.ts +2 -0
- package/dist/mcp/adminWorkspaceRegistrar.js +154 -0
- 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/collaborationRegistrar.d.ts +2 -0
- package/dist/mcp/collaborationRegistrar.js +71 -0
- package/dist/mcp/documentAssetRegistrar.d.ts +2 -0
- package/dist/mcp/documentAssetRegistrar.js +346 -0
- package/dist/mcp/documentHelpers.d.ts +100 -0
- package/dist/mcp/documentHelpers.js +161 -0
- package/dist/mcp/runtime.js +2455 -3925
- package/dist/mcp/runtime.test.js +240 -8
- 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/mcp/taskPlanningRegistrar.d.ts +2 -0
- package/dist/mcp/taskPlanningRegistrar.js +418 -0
- package/dist/mcp/toolCatalog.d.ts +35 -0
- package/dist/mcp/toolCatalog.js +3 -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.d.ts +1 -0
- package/dist/server/index.js +46 -3
- package/dist/server/index.test.js +12 -1
- package/dist/server/routes/admin.d.ts +1 -5
- package/dist/server/routes/admin.js +151 -80
- package/dist/server/routes/annotatedAttachments.d.ts +1 -1
- package/dist/server/routes/annotatedAttachments.js +1 -1
- package/dist/server/routes/auth.d.ts +1 -4
- package/dist/server/routes/auth.js +59 -64
- package/dist/server/routes/authSupport.d.ts +30 -0
- package/dist/server/routes/authSupport.js +81 -0
- package/dist/server/routes/billing.d.ts +1 -1
- package/dist/server/routes/billing.js +122 -255
- package/dist/server/routes/billing.test.js +257 -86
- package/dist/server/routes/documentReviews.d.ts +1 -1
- package/dist/server/routes/documentReviews.js +1 -1
- package/dist/server/routes/documents.d.ts +4 -2
- package/dist/server/routes/documents.js +73 -7
- package/dist/server/routes/primitives.d.ts +11 -0
- package/dist/server/routes/primitives.js +73 -0
- package/dist/server/routes/resources.d.ts +1 -1
- package/dist/server/routes/resources.js +1 -1
- package/dist/server/routes/shared.d.ts +31 -1
- package/dist/server/routes/shared.js +26 -0
- package/dist/server/routes/sync.d.ts +1 -1
- package/dist/server/routes/sync.integration.test.js +20 -0
- package/dist/server/routes/sync.js +111 -238
- package/dist/server/routes/tasks.d.ts +1 -1
- package/dist/server/routes/tasks.js +50 -8
- package/dist/server/routes/workspaces.d.ts +1 -1
- package/dist/server/routes/workspaces.js +1 -1
- package/dist/server/routes.d.ts +2 -9
- package/dist/server/routes.js +84 -181
- package/dist/server/routes.test.js +912 -32
- package/dist/shared/taskActor.d.ts +10 -0
- package/dist/shared/taskActor.js +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.js +28 -17
- 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 +379 -0
- package/dist/sync/syncService.d.ts +9 -1
- package/dist/sync/syncService.js +44 -2
- package/dist/sync/syncService.test.js +62 -0
- package/dist/sync/workspacePullFeed.d.ts +1 -0
- package/dist/sync/workspacePullFeed.js +66 -59
- package/dist/sync/workspacePullFeed.test.js +72 -4
- package/dist/sync/workspaceSyncModel.d.ts +25 -1
- package/dist/sync/workspaceSyncModel.js +77 -3
- package/dist/sync/workspaceSyncModel.test.js +153 -0
- package/dist/sync/workspaceSyncState.d.ts +2 -0
- package/dist/sync/workspaceSyncState.js +1 -0
- package/dist/types.d.ts +2 -10
- package/dist/ui/assets/AgentsModule-N2MnQBZk.js +1 -0
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-C_TmXZwA.js → AnnotatedAttachmentWorkspace-BG6P_GVW.js} +1 -1
- package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +1 -0
- package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +252 -0
- package/dist/ui/assets/{InitiativesModule-4-Rh2K2n.js → InitiativesModule-Dhm7M8e7.js} +1 -1
- package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
- package/dist/ui/assets/PlansPage-Cq0zXj3I.js +1 -0
- package/dist/ui/assets/TaskSettings-ln0aF7xc.js +9 -0
- package/dist/ui/assets/{WorkflowsModule-CGk9s3NJ.js → WorkflowsModule-BcS4afRn.js} +1 -1
- package/dist/ui/assets/index-C2-OXZV7.css +1 -0
- package/dist/ui/assets/index-DcSI5F86.js +6 -0
- package/dist/ui/assets/{vendor-icons-pWocEipT.js → vendor-icons-BSUaRwF8.js} +1 -1
- package/dist/ui/index.html +4 -4
- package/dist/utils/commercialLifecyclePresentation.d.ts +19 -0
- package/dist/utils/commercialLifecyclePresentation.js +196 -0
- package/dist/utils/taskActivity.js +4 -0
- package/dist/utils/taskActivity.test.js +25 -1
- package/package.json +10 -1
- package/scripts/check-cloud-mcp.mjs +83 -0
- package/scripts/playwright-auth.sh +9 -1
- package/scripts/run-billing-performance-smoke.sh +24 -0
- package/dist/ui/assets/AgentsModule-CpsTmrIW.js +0 -1
- package/dist/ui/assets/DocumentWorkspace-C3GyzrWm.js +0 -250
- package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +0 -1
- package/dist/ui/assets/PlansPage-BP7AYOqr.js +0 -1
- package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +0 -8
- package/dist/ui/assets/index-CLIMgR6g.js +0 -6
- package/dist/ui/assets/index-DneETxcu.css +0 -1
|
@@ -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);
|
|
@@ -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);
|
|
@@ -679,6 +684,22 @@ describe('Server Routes', () => {
|
|
|
679
684
|
res.__getBody = () => Buffer.concat(bodyChunks).toString('utf8');
|
|
680
685
|
return res;
|
|
681
686
|
};
|
|
687
|
+
const withDevAuthTokenExposure = async (value, run) => {
|
|
688
|
+
const previous = process.env.TASKFORCE_AUTH_EXPOSE_DEV_TOKENS;
|
|
689
|
+
if (value === undefined)
|
|
690
|
+
delete process.env.TASKFORCE_AUTH_EXPOSE_DEV_TOKENS;
|
|
691
|
+
else
|
|
692
|
+
process.env.TASKFORCE_AUTH_EXPOSE_DEV_TOKENS = value;
|
|
693
|
+
try {
|
|
694
|
+
await run();
|
|
695
|
+
}
|
|
696
|
+
finally {
|
|
697
|
+
if (previous === undefined)
|
|
698
|
+
delete process.env.TASKFORCE_AUTH_EXPOSE_DEV_TOKENS;
|
|
699
|
+
else
|
|
700
|
+
process.env.TASKFORCE_AUTH_EXPOSE_DEV_TOKENS = previous;
|
|
701
|
+
}
|
|
702
|
+
};
|
|
682
703
|
it('GET /api/taskforce/tasks should return tasks', async () => {
|
|
683
704
|
const mockTasks = { tasks: [], config: {} };
|
|
684
705
|
core.listTasks.mockReturnValue(mockTasks);
|
|
@@ -892,6 +913,8 @@ describe('Server Routes', () => {
|
|
|
892
913
|
auth: expect.objectContaining({
|
|
893
914
|
aiProfileName: 'Automation Codex',
|
|
894
915
|
aiProfileToken: 'tfp_automation_codex',
|
|
916
|
+
aiProfileIcon: 'Code2',
|
|
917
|
+
aiProfileColor: '#10a37f',
|
|
895
918
|
aiProfileDescription: 'OpenAI Codex coding agent connected through MCP.',
|
|
896
919
|
aiProfileRole: 'Coding Agent',
|
|
897
920
|
aiProfileProvider: 'OpenAI',
|
|
@@ -1600,6 +1623,17 @@ describe('Server Routes', () => {
|
|
|
1600
1623
|
});
|
|
1601
1624
|
expect(core.updateUiState).not.toHaveBeenCalledWith('workspace-denied', expect.anything(), expect.anything());
|
|
1602
1625
|
});
|
|
1626
|
+
it('POST /api/taskforce/ui-state should stamp debug response headers', async () => {
|
|
1627
|
+
const match = matchRoute('POST', '/api/taskforce/ui-state', routes);
|
|
1628
|
+
const req = createMockReq('POST', '/api/taskforce/ui-state', {
|
|
1629
|
+
stateKey: 'app',
|
|
1630
|
+
patch: { activeWorkspaceModule: 'tasks' }
|
|
1631
|
+
});
|
|
1632
|
+
const res = createMockRes();
|
|
1633
|
+
await match?.handler(req, res, {});
|
|
1634
|
+
expect(res.setHeader).toHaveBeenCalledWith('x-taskforce-ui-state-route', 'tasks');
|
|
1635
|
+
expect(res.setHeader).toHaveBeenCalledWith('x-taskforce-ui-state-request-id', expect.stringMatching(/^ui-/));
|
|
1636
|
+
});
|
|
1603
1637
|
it('POST /api/taskforce/workspace-profile should persist workspace name and description', async () => {
|
|
1604
1638
|
const match = matchRoute('POST', '/api/taskforce/workspace-profile', routes);
|
|
1605
1639
|
const req = createMockReq('POST', '/api/taskforce/workspace-profile', { workspaceId: 'default', name: 'Acme Project', description: 'Customer onboarding' }, { 'x-taskforce-workspace-id': 'default' });
|
|
@@ -1896,6 +1930,20 @@ describe('Server Routes', () => {
|
|
|
1896
1930
|
});
|
|
1897
1931
|
});
|
|
1898
1932
|
it('POST /api/taskforce/settings/mcp/test-connection should validate the token against workspace and scopes', async () => {
|
|
1933
|
+
const previousCloudEnvironment = process.env.TASKFORCE_CLOUD_ENVIRONMENT;
|
|
1934
|
+
process.env.TASKFORCE_CLOUD_ENVIRONMENT = 'performance';
|
|
1935
|
+
const fetchMock = vi.fn(async () => new Response(JSON.stringify({
|
|
1936
|
+
jsonrpc: '2.0',
|
|
1937
|
+
id: 'mcp-connection-test',
|
|
1938
|
+
result: {
|
|
1939
|
+
protocolVersion: '2025-03-26',
|
|
1940
|
+
serverInfo: { name: 'taskforce-cloud' }
|
|
1941
|
+
}
|
|
1942
|
+
}), {
|
|
1943
|
+
status: 200,
|
|
1944
|
+
headers: { 'Content-Type': 'application/json' }
|
|
1945
|
+
}));
|
|
1946
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
1899
1947
|
core.inspectMcpAccessToken.mockReturnValue({
|
|
1900
1948
|
tokenId: 'mcp-token-1',
|
|
1901
1949
|
tokenPrefix: 'tfmcp_abc123',
|
|
@@ -1922,12 +1970,136 @@ describe('Server Routes', () => {
|
|
|
1922
1970
|
});
|
|
1923
1971
|
const res = createMockRes();
|
|
1924
1972
|
res.req = req;
|
|
1925
|
-
|
|
1973
|
+
try {
|
|
1974
|
+
await match?.handler(req, res, {});
|
|
1975
|
+
}
|
|
1976
|
+
finally {
|
|
1977
|
+
vi.unstubAllGlobals();
|
|
1978
|
+
if (previousCloudEnvironment === undefined)
|
|
1979
|
+
delete process.env.TASKFORCE_CLOUD_ENVIRONMENT;
|
|
1980
|
+
else
|
|
1981
|
+
process.env.TASKFORCE_CLOUD_ENVIRONMENT = previousCloudEnvironment;
|
|
1982
|
+
}
|
|
1926
1983
|
expect(core.inspectMcpAccessToken).toHaveBeenCalledWith('tfmcp_abc123_secret');
|
|
1984
|
+
expect(fetchMock).toHaveBeenCalledWith('https://performance-mcp.taskforcehq.ai/mcp', expect.objectContaining({
|
|
1985
|
+
method: 'POST',
|
|
1986
|
+
headers: expect.objectContaining({
|
|
1987
|
+
Authorization: 'Bearer tfmcp_abc123_secret'
|
|
1988
|
+
})
|
|
1989
|
+
}));
|
|
1927
1990
|
const payload = JSON.parse(res.end.mock.calls[0][0]);
|
|
1928
1991
|
expect(payload.success).toBe(true);
|
|
1929
1992
|
expect(payload.result.workspaceName).toBe('Workspace One');
|
|
1930
1993
|
expect(payload.result.scopes).toEqual(['tasks:read', 'tasks:write']);
|
|
1994
|
+
expect(payload.result.endpoint).toBe('https://performance-mcp.taskforcehq.ai/mcp');
|
|
1995
|
+
expect(payload.result.serverName).toBe('taskforce-cloud');
|
|
1996
|
+
expect(payload.result.protocolVersion).toBe('2025-03-26');
|
|
1997
|
+
});
|
|
1998
|
+
it('POST /api/taskforce/settings/mcp/test-connection should return a clear error when the dedicated MCP host returns a gateway failure', async () => {
|
|
1999
|
+
const previousCloudEnvironment = process.env.TASKFORCE_CLOUD_ENVIRONMENT;
|
|
2000
|
+
process.env.TASKFORCE_CLOUD_ENVIRONMENT = 'performance';
|
|
2001
|
+
const fetchMock = vi.fn(async () => new Response('Bad gateway', {
|
|
2002
|
+
status: 502,
|
|
2003
|
+
headers: { 'Content-Type': 'text/plain' }
|
|
2004
|
+
}));
|
|
2005
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
2006
|
+
core.inspectMcpAccessToken.mockReturnValue({
|
|
2007
|
+
tokenId: 'mcp-token-1',
|
|
2008
|
+
tokenPrefix: 'tfmcp_abc123',
|
|
2009
|
+
workspaceId: 'workspace-1',
|
|
2010
|
+
userId: 'user-1',
|
|
2011
|
+
name: 'Workspace MCP',
|
|
2012
|
+
scopes: ['tasks:read', 'tasks:write'],
|
|
2013
|
+
status: 'active'
|
|
2014
|
+
});
|
|
2015
|
+
const match = matchRoute('POST', '/api/taskforce/settings/mcp/test-connection', routes);
|
|
2016
|
+
const req = createMockReq('POST', '/api/taskforce/settings/mcp/test-connection', {
|
|
2017
|
+
token: 'tfmcp_abc123_secret',
|
|
2018
|
+
requiredScopes: ['tasks:read', 'tasks:write']
|
|
2019
|
+
}, {
|
|
2020
|
+
'x-taskforce-runtime-mode': 'cloud',
|
|
2021
|
+
'x-taskforce-user-id': 'user-1',
|
|
2022
|
+
'x-taskforce-workspace-id': 'workspace-1'
|
|
2023
|
+
});
|
|
2024
|
+
const res = createMockRes();
|
|
2025
|
+
res.req = req;
|
|
2026
|
+
try {
|
|
2027
|
+
await match?.handler(req, res, {});
|
|
2028
|
+
}
|
|
2029
|
+
finally {
|
|
2030
|
+
vi.unstubAllGlobals();
|
|
2031
|
+
if (previousCloudEnvironment === undefined)
|
|
2032
|
+
delete process.env.TASKFORCE_CLOUD_ENVIRONMENT;
|
|
2033
|
+
else
|
|
2034
|
+
process.env.TASKFORCE_CLOUD_ENVIRONMENT = previousCloudEnvironment;
|
|
2035
|
+
}
|
|
2036
|
+
expect(res.writeHead).toHaveBeenCalledWith(502, { 'Content-Type': 'application/json' });
|
|
2037
|
+
const payload = JSON.parse(res.end.mock.calls[0][0]);
|
|
2038
|
+
expect(payload.success).toBe(false);
|
|
2039
|
+
expect(payload.code).toBe('MCP_ENDPOINT_GATEWAY_ERROR');
|
|
2040
|
+
expect(payload.error).toContain('https://performance-mcp.taskforcehq.ai/mcp');
|
|
2041
|
+
expect(payload.details).toEqual(expect.objectContaining({
|
|
2042
|
+
endpoint: 'https://performance-mcp.taskforcehq.ai/mcp',
|
|
2043
|
+
upstreamStatus: 502
|
|
2044
|
+
}));
|
|
2045
|
+
});
|
|
2046
|
+
it('POST /api/taskforce/settings/mcp/test-connection should prefer the dedicated MCP hostname when cloudMcpBaseUrl points at the app host', async () => {
|
|
2047
|
+
const previousCloudEnvironment = process.env.TASKFORCE_CLOUD_ENVIRONMENT;
|
|
2048
|
+
const previousCloudMcpBaseUrl = process.env.VITE_TASKFORCE_CLOUD_MCP_BASE_URL;
|
|
2049
|
+
process.env.TASKFORCE_CLOUD_ENVIRONMENT = 'performance';
|
|
2050
|
+
process.env.VITE_TASKFORCE_CLOUD_MCP_BASE_URL = 'https://performance-app.taskforcehq.ai';
|
|
2051
|
+
const fetchMock = vi.fn(async () => new Response(JSON.stringify({
|
|
2052
|
+
jsonrpc: '2.0',
|
|
2053
|
+
id: 'mcp-connection-test',
|
|
2054
|
+
result: {
|
|
2055
|
+
serverInfo: { name: 'taskforce-cloud' }
|
|
2056
|
+
}
|
|
2057
|
+
}), {
|
|
2058
|
+
status: 200,
|
|
2059
|
+
headers: { 'Content-Type': 'application/json' }
|
|
2060
|
+
}));
|
|
2061
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
2062
|
+
core.inspectMcpAccessToken.mockReturnValue({
|
|
2063
|
+
tokenId: 'mcp-token-1',
|
|
2064
|
+
tokenPrefix: 'tfmcp_abc123',
|
|
2065
|
+
workspaceId: 'workspace-1',
|
|
2066
|
+
userId: 'user-1',
|
|
2067
|
+
name: 'Workspace MCP',
|
|
2068
|
+
scopes: ['tasks:read', 'tasks:write'],
|
|
2069
|
+
status: 'active'
|
|
2070
|
+
});
|
|
2071
|
+
core.getWorkspaceProfile.mockReturnValue({
|
|
2072
|
+
id: 'workspace-1',
|
|
2073
|
+
slug: 'workspace-1',
|
|
2074
|
+
name: 'Workspace One',
|
|
2075
|
+
description: null
|
|
2076
|
+
});
|
|
2077
|
+
const match = matchRoute('POST', '/api/taskforce/settings/mcp/test-connection', routes);
|
|
2078
|
+
const req = createMockReq('POST', '/api/taskforce/settings/mcp/test-connection', {
|
|
2079
|
+
token: 'tfmcp_abc123_secret',
|
|
2080
|
+
requiredScopes: ['tasks:read']
|
|
2081
|
+
}, {
|
|
2082
|
+
'x-taskforce-runtime-mode': 'cloud',
|
|
2083
|
+
'x-taskforce-user-id': 'user-1',
|
|
2084
|
+
'x-taskforce-workspace-id': 'workspace-1'
|
|
2085
|
+
});
|
|
2086
|
+
const res = createMockRes();
|
|
2087
|
+
res.req = req;
|
|
2088
|
+
try {
|
|
2089
|
+
await match?.handler(req, res, {});
|
|
2090
|
+
}
|
|
2091
|
+
finally {
|
|
2092
|
+
vi.unstubAllGlobals();
|
|
2093
|
+
if (previousCloudEnvironment === undefined)
|
|
2094
|
+
delete process.env.TASKFORCE_CLOUD_ENVIRONMENT;
|
|
2095
|
+
else
|
|
2096
|
+
process.env.TASKFORCE_CLOUD_ENVIRONMENT = previousCloudEnvironment;
|
|
2097
|
+
if (previousCloudMcpBaseUrl === undefined)
|
|
2098
|
+
delete process.env.VITE_TASKFORCE_CLOUD_MCP_BASE_URL;
|
|
2099
|
+
else
|
|
2100
|
+
process.env.VITE_TASKFORCE_CLOUD_MCP_BASE_URL = previousCloudMcpBaseUrl;
|
|
2101
|
+
}
|
|
2102
|
+
expect(fetchMock).toHaveBeenCalledWith('https://performance-mcp.taskforcehq.ai/mcp', expect.any(Object));
|
|
1931
2103
|
});
|
|
1932
2104
|
it('POST /api/taskforce/settings/mcp/test-connection should report missing scopes', async () => {
|
|
1933
2105
|
core.inspectMcpAccessToken.mockReturnValue({
|
|
@@ -2212,6 +2384,63 @@ describe('Server Routes', () => {
|
|
|
2212
2384
|
expect(fetchMock).not.toHaveBeenCalled();
|
|
2213
2385
|
vi.unstubAllGlobals();
|
|
2214
2386
|
});
|
|
2387
|
+
it('GET /api/taskforce/documents/:assetId/content should fall back to object storage when local canonical metadata points at a missing file', async () => {
|
|
2388
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
2389
|
+
status: 200,
|
|
2390
|
+
ok: true,
|
|
2391
|
+
headers: new Headers({ 'content-type': 'text/markdown' }),
|
|
2392
|
+
arrayBuffer: async () => Buffer.from('# Recovered canonical doc\n').buffer,
|
|
2393
|
+
});
|
|
2394
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
2395
|
+
const routesWithStorage = createRoutes(core, {
|
|
2396
|
+
objectStorage: {
|
|
2397
|
+
provider: 'r2',
|
|
2398
|
+
r2: {
|
|
2399
|
+
accountId: 'acct-1',
|
|
2400
|
+
bucket: 'taskforce-docs',
|
|
2401
|
+
endpoint: 'https://example.r2.cloudflarestorage.com',
|
|
2402
|
+
accessKeyId: 'access-key-1',
|
|
2403
|
+
secretAccessKey: 'secret-key-1',
|
|
2404
|
+
keyPrefix: 'taskforce',
|
|
2405
|
+
signedUploadTtlSeconds: 600,
|
|
2406
|
+
signedDownloadTtlSeconds: 300
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
});
|
|
2410
|
+
const store = new WorkspaceAssetStore(core.getDatabaseAdapter(), core.getTenantId());
|
|
2411
|
+
store.upsertAsset({
|
|
2412
|
+
assetId: 'asset-doc-content-fallback-1',
|
|
2413
|
+
workspaceId: 'default',
|
|
2414
|
+
kind: 'document',
|
|
2415
|
+
logicalName: 'Recovered canonical doc',
|
|
2416
|
+
originalFilename: 'recovered-canonical-doc.md',
|
|
2417
|
+
mimeType: 'text/markdown',
|
|
2418
|
+
storageProvider: 'local',
|
|
2419
|
+
storageKey: 'workspaces/default/assets/documents/asset-doc-content-fallback-1-recovered-canonical-doc.md',
|
|
2420
|
+
contentSha256: 'sha-canonical-doc-fallback-1',
|
|
2421
|
+
sizeBytes: 25,
|
|
2422
|
+
documentId: 'doc-content-fallback-1',
|
|
2423
|
+
version: 1,
|
|
2424
|
+
isLatest: true,
|
|
2425
|
+
scanStatus: 'clean',
|
|
2426
|
+
scanEngine: null,
|
|
2427
|
+
scanDetails: null,
|
|
2428
|
+
deletedAt: null,
|
|
2429
|
+
purgeAfter: null
|
|
2430
|
+
});
|
|
2431
|
+
const req = createMockReq('GET', '/api/taskforce/documents/asset-doc-content-fallback-1/content');
|
|
2432
|
+
const res = createMockRes();
|
|
2433
|
+
await matchRoute('GET', '/api/taskforce/documents/asset-doc-content-fallback-1/content', routesWithStorage)?.handler(req, res, {
|
|
2434
|
+
assetId: 'asset-doc-content-fallback-1'
|
|
2435
|
+
});
|
|
2436
|
+
expect(res.writeHead).toHaveBeenCalledWith(200, expect.objectContaining({
|
|
2437
|
+
'Content-Type': 'text/markdown',
|
|
2438
|
+
'Content-Disposition': 'inline'
|
|
2439
|
+
}));
|
|
2440
|
+
expect(res.end).toHaveBeenCalledWith(expect.any(Buffer));
|
|
2441
|
+
expect(fetchMock).toHaveBeenCalled();
|
|
2442
|
+
vi.unstubAllGlobals();
|
|
2443
|
+
});
|
|
2215
2444
|
it('GET /api/taskforce/documents/:assetId/content should reject non-document assets', async () => {
|
|
2216
2445
|
const routesWithStorage = createRoutes(core, {
|
|
2217
2446
|
objectStorage: {
|
|
@@ -3467,10 +3696,106 @@ describe('Server Routes', () => {
|
|
|
3467
3696
|
const res = createMockRes();
|
|
3468
3697
|
await match?.handler(req, res, { assetId: 'asset-doc-attached-1' });
|
|
3469
3698
|
expect(res.writeHead).toHaveBeenCalledWith(409, { 'Content-Type': 'application/json' });
|
|
3699
|
+
const payload = JSON.parse(res.end.mock.calls[0][0]);
|
|
3700
|
+
expect(payload).toEqual(expect.objectContaining({
|
|
3701
|
+
error: 'Document is still attached to one or more tasks.',
|
|
3702
|
+
activeAttachmentCount: 1,
|
|
3703
|
+
requiresForceDetach: true
|
|
3704
|
+
}));
|
|
3470
3705
|
expect(store.get('asset-doc-attached-1', 'default')?.deletedAt).toBeNull();
|
|
3471
3706
|
fs.rmSync(dbRoot, { recursive: true, force: true });
|
|
3472
3707
|
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
3473
3708
|
});
|
|
3709
|
+
it('DELETE /api/taskforce/documents/:assetId should unlink attached canonical documents when forceDetach is true', async () => {
|
|
3710
|
+
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'taskforce-documents-delete-force-attached-'));
|
|
3711
|
+
const basePath = path.join(tmpRoot, '.taskforce');
|
|
3712
|
+
const documentPath = path.join(basePath, 'workspaces', 'default', 'assets', 'documents', 'asset-doc-force-delete-1-notes.md');
|
|
3713
|
+
fs.mkdirSync(path.dirname(documentPath), { recursive: true });
|
|
3714
|
+
fs.writeFileSync(documentPath, '# attached\n', 'utf8');
|
|
3715
|
+
core.getPaths.mockReturnValue({
|
|
3716
|
+
projectRoot: tmpRoot,
|
|
3717
|
+
basePath,
|
|
3718
|
+
});
|
|
3719
|
+
const { tmpRoot: dbRoot, db, store } = createWorkspaceAssetTestStore();
|
|
3720
|
+
core.getDatabaseAdapter = vi.fn().mockReturnValue(db);
|
|
3721
|
+
core.getTenantId = vi.fn().mockReturnValue('default');
|
|
3722
|
+
store.upsertAsset({
|
|
3723
|
+
assetId: 'asset-doc-force-delete-1',
|
|
3724
|
+
workspaceId: 'default',
|
|
3725
|
+
kind: 'document',
|
|
3726
|
+
originalFilename: 'notes.md',
|
|
3727
|
+
mimeType: 'text/markdown',
|
|
3728
|
+
storageProvider: 'local',
|
|
3729
|
+
storageKey: 'workspaces/default/assets/documents/asset-doc-force-delete-1-notes.md',
|
|
3730
|
+
contentSha256: 'sha-doc-force-delete-1',
|
|
3731
|
+
sizeBytes: 16,
|
|
3732
|
+
documentId: 'doc-force-delete-1',
|
|
3733
|
+
version: 1,
|
|
3734
|
+
updatedAt: '2026-03-18T14:00:00.000Z'
|
|
3735
|
+
});
|
|
3736
|
+
store.upsertLink({
|
|
3737
|
+
workspaceId: 'default',
|
|
3738
|
+
assetId: 'asset-doc-force-delete-1',
|
|
3739
|
+
taskId: 'task-keep',
|
|
3740
|
+
role: 'attachment'
|
|
3741
|
+
});
|
|
3742
|
+
core.getTask.mockImplementation((id) => id === 'task-keep'
|
|
3743
|
+
? {
|
|
3744
|
+
id: 'task-keep',
|
|
3745
|
+
title: 'Task Keep',
|
|
3746
|
+
attachments: [{
|
|
3747
|
+
path: '/api/taskforce/documents/asset-doc-force-delete-1/content',
|
|
3748
|
+
fsPath: 'workspaces/default/assets/documents/asset-doc-force-delete-1-notes.md',
|
|
3749
|
+
caption: 'notes.md',
|
|
3750
|
+
assetId: 'asset-doc-force-delete-1'
|
|
3751
|
+
}, {
|
|
3752
|
+
path: '/api/taskforce/context/other-file.md',
|
|
3753
|
+
fsPath: 'workspaces/default/assets/files/other-file.md',
|
|
3754
|
+
caption: 'other-file.md'
|
|
3755
|
+
}]
|
|
3756
|
+
}
|
|
3757
|
+
: null);
|
|
3758
|
+
core.updateTask.mockImplementation((_id, patch) => ({
|
|
3759
|
+
id: 'task-keep',
|
|
3760
|
+
attachments: patch.attachments
|
|
3761
|
+
}));
|
|
3762
|
+
const routesWithDb = createRoutes(core);
|
|
3763
|
+
const match = matchRoute('DELETE', '/api/taskforce/documents/asset-doc-force-delete-1', routesWithDb);
|
|
3764
|
+
const req = createMockReq('DELETE', '/api/taskforce/documents/asset-doc-force-delete-1', {
|
|
3765
|
+
forceDetach: true
|
|
3766
|
+
}, {
|
|
3767
|
+
'x-taskforce-workspace-id': 'default'
|
|
3768
|
+
});
|
|
3769
|
+
const res = createMockRes();
|
|
3770
|
+
await match?.handler(req, res, { assetId: 'asset-doc-force-delete-1' });
|
|
3771
|
+
expect(core.updateTask).toHaveBeenCalledWith('task-keep', {
|
|
3772
|
+
attachments: [{
|
|
3773
|
+
path: '/api/taskforce/context/other-file.md',
|
|
3774
|
+
fsPath: 'workspaces/default/assets/files/other-file.md',
|
|
3775
|
+
caption: 'other-file.md'
|
|
3776
|
+
}]
|
|
3777
|
+
}, 'default', expect.objectContaining({ actorRef: 'user', saveSource: 'manual' }));
|
|
3778
|
+
expect(res.writeHead).toHaveBeenCalledWith(200, { 'Content-Type': 'application/json' });
|
|
3779
|
+
const payload = JSON.parse(res.end.mock.calls[0][0]);
|
|
3780
|
+
expect(payload).toEqual(expect.objectContaining({
|
|
3781
|
+
success: true,
|
|
3782
|
+
softDeleted: true,
|
|
3783
|
+
detachedTaskCount: 1
|
|
3784
|
+
}));
|
|
3785
|
+
expect(store.get('asset-doc-force-delete-1', 'default')).toEqual(expect.objectContaining({
|
|
3786
|
+
deletedAt: expect.any(String),
|
|
3787
|
+
purgeAfter: expect.any(String)
|
|
3788
|
+
}));
|
|
3789
|
+
expect(store.listLinksForAsset('asset-doc-force-delete-1', 'default', { includeDeleted: true })).toEqual([
|
|
3790
|
+
expect.objectContaining({
|
|
3791
|
+
taskId: 'task-keep',
|
|
3792
|
+
role: 'attachment',
|
|
3793
|
+
deletedAt: expect.any(String)
|
|
3794
|
+
})
|
|
3795
|
+
]);
|
|
3796
|
+
fs.rmSync(dbRoot, { recursive: true, force: true });
|
|
3797
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
3798
|
+
});
|
|
3474
3799
|
it('PATCH /api/taskforce/documents/:assetId should update the shared display name without changing storage identity', async () => {
|
|
3475
3800
|
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'taskforce-documents-rename-'));
|
|
3476
3801
|
core.getPaths.mockReturnValue({
|
|
@@ -3708,7 +4033,85 @@ describe('Server Routes', () => {
|
|
|
3708
4033
|
}));
|
|
3709
4034
|
expect(res.setHeader).not.toHaveBeenCalledWith('Set-Cookie', expect.any(String));
|
|
3710
4035
|
expect(res.writeHead).toHaveBeenCalledWith(201, expect.any(Object));
|
|
3711
|
-
|
|
4036
|
+
const payload = JSON.parse(String(res.end.mock.calls.at(-1)?.[0] || '{}'));
|
|
4037
|
+
expect(payload).toEqual(expect.objectContaining({
|
|
4038
|
+
success: true,
|
|
4039
|
+
verificationRequired: true
|
|
4040
|
+
}));
|
|
4041
|
+
expect(payload.verificationToken).toBeUndefined();
|
|
4042
|
+
});
|
|
4043
|
+
it('POST /api/taskforce/auth/register should not depend on onboarding policy lookups', async () => {
|
|
4044
|
+
const authConfig = {
|
|
4045
|
+
runtimeMode: 'cloud',
|
|
4046
|
+
enabled: true,
|
|
4047
|
+
requiredForApi: true,
|
|
4048
|
+
excludedPaths: ['/api/taskforce/health', '/api/taskforce/auth'],
|
|
4049
|
+
defaultRole: 'member',
|
|
4050
|
+
defaultWorkspaceId: 'default',
|
|
4051
|
+
sessionCookieName: 'taskforce_session',
|
|
4052
|
+
sessionSecret: 'test-secret',
|
|
4053
|
+
sessionTtlSeconds: 3600
|
|
4054
|
+
};
|
|
4055
|
+
core.getOnboardingPolicy.mockImplementationOnce(() => {
|
|
4056
|
+
throw new Error('for SELECT DISTINCT, ORDER BY expressions must appear in select list');
|
|
4057
|
+
});
|
|
4058
|
+
core.getGlobalSettings.mockReturnValue({
|
|
4059
|
+
auth: {
|
|
4060
|
+
allowSelfRegistration: true,
|
|
4061
|
+
requireVerifiedEmail: false
|
|
4062
|
+
}
|
|
4063
|
+
});
|
|
4064
|
+
routes = createRoutes(core, { authConfig });
|
|
4065
|
+
core.registerPasswordUser.mockReturnValueOnce({
|
|
4066
|
+
userId: 'u-new',
|
|
4067
|
+
workspaceId: 'default',
|
|
4068
|
+
role: 'member',
|
|
4069
|
+
planSelectionRequired: false
|
|
4070
|
+
});
|
|
4071
|
+
const match = matchRoute('POST', '/api/taskforce/auth/register', routes);
|
|
4072
|
+
const req = createMockReq('POST', '/api/taskforce/auth/register', {
|
|
4073
|
+
email: 'newuser@example.com',
|
|
4074
|
+
password: 'password123'
|
|
4075
|
+
});
|
|
4076
|
+
const res = createMockRes();
|
|
4077
|
+
await match?.handler(req, res, {});
|
|
4078
|
+
expect(core.registerPasswordUser).toHaveBeenCalledWith(expect.objectContaining({
|
|
4079
|
+
email: 'newuser@example.com'
|
|
4080
|
+
}));
|
|
4081
|
+
expect(res.writeHead).toHaveBeenCalledWith(201, expect.any(Object));
|
|
4082
|
+
expect(res.end).toHaveBeenCalledWith(expect.stringContaining('"success":true'));
|
|
4083
|
+
});
|
|
4084
|
+
it('POST /api/taskforce/auth/register should only expose verificationToken when explicitly enabled', async () => {
|
|
4085
|
+
const authConfig = {
|
|
4086
|
+
runtimeMode: 'cloud',
|
|
4087
|
+
enabled: true,
|
|
4088
|
+
requiredForApi: true,
|
|
4089
|
+
excludedPaths: ['/api/taskforce/health', '/api/taskforce/auth'],
|
|
4090
|
+
defaultRole: 'member',
|
|
4091
|
+
defaultWorkspaceId: 'default',
|
|
4092
|
+
sessionCookieName: 'taskforce_session',
|
|
4093
|
+
sessionSecret: 'test-secret',
|
|
4094
|
+
sessionTtlSeconds: 3600
|
|
4095
|
+
};
|
|
4096
|
+
await withDevAuthTokenExposure('true', async () => {
|
|
4097
|
+
routes = createRoutes(core, { authConfig });
|
|
4098
|
+
core.registerPasswordUser.mockReturnValueOnce({
|
|
4099
|
+
userId: 'user-opt-in',
|
|
4100
|
+
workspaceId: null,
|
|
4101
|
+
role: 'member',
|
|
4102
|
+
planSelectionRequired: false
|
|
4103
|
+
});
|
|
4104
|
+
core.requestEmailVerification.mockReturnValueOnce({ token: 'verify-token-opt-in' });
|
|
4105
|
+
const match = matchRoute('POST', '/api/taskforce/auth/register', routes);
|
|
4106
|
+
const req = createMockReq('POST', '/api/taskforce/auth/register', {
|
|
4107
|
+
email: 'opt-in@example.com',
|
|
4108
|
+
password: 'password123'
|
|
4109
|
+
});
|
|
4110
|
+
const res = createMockRes();
|
|
4111
|
+
await match?.handler(req, res, {});
|
|
4112
|
+
const payload = JSON.parse(String(res.end.mock.calls.at(-1)?.[0] || '{}'));
|
|
4113
|
+
expect(payload.verificationToken).toBe('verify-token-opt-in');
|
|
4114
|
+
});
|
|
3712
4115
|
});
|
|
3713
4116
|
it('POST /api/taskforce/auth/register should allow first-time registration without workspaceId', async () => {
|
|
3714
4117
|
const authConfig = {
|
|
@@ -3821,7 +4224,51 @@ describe('Server Routes', () => {
|
|
|
3821
4224
|
}));
|
|
3822
4225
|
expect(res.writeHead).toHaveBeenCalledWith(201, expect.any(Object));
|
|
3823
4226
|
});
|
|
3824
|
-
it('POST /api/taskforce/auth/register should
|
|
4227
|
+
it('POST /api/taskforce/auth/register should route duplicate unverified email into verification recovery', async () => {
|
|
4228
|
+
const authConfig = {
|
|
4229
|
+
runtimeMode: 'cloud',
|
|
4230
|
+
enabled: true,
|
|
4231
|
+
requiredForApi: true,
|
|
4232
|
+
excludedPaths: ['/api/taskforce/health', '/api/taskforce/auth'],
|
|
4233
|
+
defaultRole: 'member',
|
|
4234
|
+
defaultWorkspaceId: 'default',
|
|
4235
|
+
sessionCookieName: 'taskforce_session',
|
|
4236
|
+
sessionSecret: 'test-secret',
|
|
4237
|
+
sessionTtlSeconds: 3600
|
|
4238
|
+
};
|
|
4239
|
+
await withDevAuthTokenExposure('true', async () => {
|
|
4240
|
+
routes = createRoutes(core, { authConfig });
|
|
4241
|
+
core.hasWorkspaceAdmin.mockReturnValueOnce(true);
|
|
4242
|
+
core.isEmailVerified.mockReturnValueOnce(false);
|
|
4243
|
+
core.requestEmailVerification.mockReturnValueOnce({ token: 'verify-token-existing' });
|
|
4244
|
+
const error = new Error('Email is already registered');
|
|
4245
|
+
error.statusCode = 409;
|
|
4246
|
+
error.code = 'EMAIL_ALREADY_EXISTS';
|
|
4247
|
+
core.registerPasswordUser.mockImplementationOnce(() => {
|
|
4248
|
+
throw error;
|
|
4249
|
+
});
|
|
4250
|
+
const match = matchRoute('POST', '/api/taskforce/auth/register', routes);
|
|
4251
|
+
const req = createMockReq('POST', '/api/taskforce/auth/register', {
|
|
4252
|
+
email: 'owner@example.com',
|
|
4253
|
+
password: 'password123',
|
|
4254
|
+
workspaceId: 'workspace-1'
|
|
4255
|
+
});
|
|
4256
|
+
const res = createMockRes();
|
|
4257
|
+
await match?.handler(req, res, {});
|
|
4258
|
+
expect(core.isEmailVerified).toHaveBeenCalledWith('owner@example.com');
|
|
4259
|
+
expect(core.requestEmailVerification).toHaveBeenCalledWith({ email: 'owner@example.com' });
|
|
4260
|
+
expect(res.writeHead).toHaveBeenCalledWith(409, expect.any(Object));
|
|
4261
|
+
const responsePayload = JSON.parse(String(res.end.mock.calls.at(-1)?.[0] || '{}'));
|
|
4262
|
+
expect(responsePayload).toEqual(expect.objectContaining({
|
|
4263
|
+
success: false,
|
|
4264
|
+
code: 'EMAIL_ALREADY_EXISTS_UNVERIFIED',
|
|
4265
|
+
verificationRequired: true,
|
|
4266
|
+
emailSent: true,
|
|
4267
|
+
verificationToken: 'verify-token-existing'
|
|
4268
|
+
}));
|
|
4269
|
+
});
|
|
4270
|
+
});
|
|
4271
|
+
it('POST /api/taskforce/auth/register should return conflict when verified email exists', async () => {
|
|
3825
4272
|
const authConfig = {
|
|
3826
4273
|
runtimeMode: 'cloud',
|
|
3827
4274
|
enabled: true,
|
|
@@ -3835,6 +4282,7 @@ describe('Server Routes', () => {
|
|
|
3835
4282
|
};
|
|
3836
4283
|
routes = createRoutes(core, { authConfig });
|
|
3837
4284
|
core.hasWorkspaceAdmin.mockReturnValueOnce(true);
|
|
4285
|
+
core.isEmailVerified.mockReturnValueOnce(true);
|
|
3838
4286
|
const error = new Error('Email is already registered');
|
|
3839
4287
|
error.statusCode = 409;
|
|
3840
4288
|
error.code = 'EMAIL_ALREADY_EXISTS';
|
|
@@ -3849,14 +4297,134 @@ describe('Server Routes', () => {
|
|
|
3849
4297
|
});
|
|
3850
4298
|
const res = createMockRes();
|
|
3851
4299
|
await match?.handler(req, res, {});
|
|
3852
|
-
expect(
|
|
3853
|
-
expect(
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
4300
|
+
expect(core.isEmailVerified).toHaveBeenCalledWith('owner@example.com');
|
|
4301
|
+
expect(core.requestEmailVerification).not.toHaveBeenCalled();
|
|
4302
|
+
expect(res.writeHead).toHaveBeenCalledWith(409, expect.any(Object));
|
|
4303
|
+
expect(res.end).toHaveBeenCalledWith(JSON.stringify({
|
|
4304
|
+
success: false,
|
|
4305
|
+
error: 'Email is already registered',
|
|
4306
|
+
code: 'EMAIL_ALREADY_EXISTS'
|
|
4307
|
+
}));
|
|
4308
|
+
});
|
|
4309
|
+
it('POST /api/taskforce/auth/register should fail when the onboarding policy is misconfigured', async () => {
|
|
4310
|
+
const authConfig = {
|
|
4311
|
+
runtimeMode: 'cloud',
|
|
4312
|
+
enabled: true,
|
|
4313
|
+
requiredForApi: true,
|
|
4314
|
+
excludedPaths: ['/api/taskforce/health', '/api/taskforce/auth'],
|
|
4315
|
+
defaultRole: 'member',
|
|
4316
|
+
defaultWorkspaceId: 'default',
|
|
4317
|
+
sessionCookieName: 'taskforce_session',
|
|
4318
|
+
sessionSecret: 'test-secret',
|
|
4319
|
+
sessionTtlSeconds: 3600
|
|
4320
|
+
};
|
|
4321
|
+
routes = createRoutes(core, { authConfig });
|
|
4322
|
+
const error = new Error('Configured onboarding plan version "missing-v9" was not found.');
|
|
4323
|
+
error.statusCode = 500;
|
|
4324
|
+
error.code = 'SIGNUP_PLAN_VERSION_NOT_FOUND';
|
|
4325
|
+
core.registerPasswordUser.mockImplementationOnce(() => {
|
|
4326
|
+
throw error;
|
|
4327
|
+
});
|
|
4328
|
+
const match = matchRoute('POST', '/api/taskforce/auth/register', routes);
|
|
4329
|
+
const req = createMockReq('POST', '/api/taskforce/auth/register', {
|
|
4330
|
+
email: 'misconfigured@example.com',
|
|
4331
|
+
password: 'password123'
|
|
4332
|
+
});
|
|
4333
|
+
const res = createMockRes();
|
|
4334
|
+
await match?.handler(req, res, {});
|
|
4335
|
+
expect(res.writeHead).toHaveBeenCalledWith(500, expect.any(Object));
|
|
4336
|
+
expect(res.end).toHaveBeenCalledWith(JSON.stringify({
|
|
4337
|
+
success: false,
|
|
4338
|
+
error: 'Configured onboarding plan version "missing-v9" was not found.',
|
|
4339
|
+
code: 'SIGNUP_PLAN_VERSION_NOT_FOUND'
|
|
4340
|
+
}));
|
|
4341
|
+
});
|
|
4342
|
+
it('POST /api/taskforce/auth/verify-email/request should report no-send cases without claiming delivery', async () => {
|
|
4343
|
+
const authConfig = {
|
|
4344
|
+
runtimeMode: 'cloud',
|
|
4345
|
+
enabled: true,
|
|
4346
|
+
requiredForApi: true,
|
|
4347
|
+
excludedPaths: ['/api/taskforce/health', '/api/taskforce/auth'],
|
|
4348
|
+
defaultRole: 'member',
|
|
4349
|
+
defaultWorkspaceId: 'default',
|
|
4350
|
+
sessionCookieName: 'taskforce_session',
|
|
4351
|
+
sessionSecret: 'test-secret',
|
|
4352
|
+
sessionTtlSeconds: 3600
|
|
4353
|
+
};
|
|
4354
|
+
routes = createRoutes(core, { authConfig });
|
|
4355
|
+
core.requestEmailVerification.mockReturnValueOnce({});
|
|
4356
|
+
const match = matchRoute('POST', '/api/taskforce/auth/verify-email/request', routes);
|
|
4357
|
+
const req = createMockReq('POST', '/api/taskforce/auth/verify-email/request', { email: 'nobody@example.com' });
|
|
4358
|
+
const res = createMockRes();
|
|
4359
|
+
await match?.handler(req, res, {});
|
|
4360
|
+
const payload = JSON.parse(String(res.end.mock.calls.at(-1)?.[0] || '{}'));
|
|
4361
|
+
expect(payload).toMatchObject({
|
|
4362
|
+
success: true,
|
|
4363
|
+
emailSent: false,
|
|
4364
|
+
deliveryAttempted: false
|
|
4365
|
+
});
|
|
4366
|
+
expect(payload.emailError).toBeUndefined();
|
|
4367
|
+
});
|
|
4368
|
+
it('POST /api/taskforce/auth/verify-email/request should only expose verificationToken when explicitly enabled', async () => {
|
|
4369
|
+
const authConfig = {
|
|
4370
|
+
runtimeMode: 'cloud',
|
|
4371
|
+
enabled: true,
|
|
4372
|
+
requiredForApi: true,
|
|
4373
|
+
excludedPaths: ['/api/taskforce/health', '/api/taskforce/auth'],
|
|
4374
|
+
defaultRole: 'member',
|
|
4375
|
+
defaultWorkspaceId: 'default',
|
|
4376
|
+
sessionCookieName: 'taskforce_session',
|
|
4377
|
+
sessionSecret: 'test-secret',
|
|
4378
|
+
sessionTtlSeconds: 3600
|
|
4379
|
+
};
|
|
4380
|
+
await withDevAuthTokenExposure(undefined, async () => {
|
|
4381
|
+
routes = createRoutes(core, { authConfig });
|
|
4382
|
+
core.requestEmailVerification.mockReturnValueOnce({ token: 'verify-token-hidden' });
|
|
4383
|
+
const match = matchRoute('POST', '/api/taskforce/auth/verify-email/request', routes);
|
|
4384
|
+
const req = createMockReq('POST', '/api/taskforce/auth/verify-email/request', { email: 'owner@example.com' });
|
|
4385
|
+
const res = createMockRes();
|
|
4386
|
+
await match?.handler(req, res, {});
|
|
4387
|
+
const payload = JSON.parse(String(res.end.mock.calls.at(-1)?.[0] || '{}'));
|
|
4388
|
+
expect(payload.verificationToken).toBeUndefined();
|
|
4389
|
+
});
|
|
4390
|
+
await withDevAuthTokenExposure('true', async () => {
|
|
4391
|
+
routes = createRoutes(core, { authConfig });
|
|
4392
|
+
core.requestEmailVerification.mockReturnValueOnce({ token: 'verify-token-visible' });
|
|
4393
|
+
const match = matchRoute('POST', '/api/taskforce/auth/verify-email/request', routes);
|
|
4394
|
+
const req = createMockReq('POST', '/api/taskforce/auth/verify-email/request', { email: 'owner@example.com' });
|
|
4395
|
+
const res = createMockRes();
|
|
4396
|
+
await match?.handler(req, res, {});
|
|
4397
|
+
const payload = JSON.parse(String(res.end.mock.calls.at(-1)?.[0] || '{}'));
|
|
4398
|
+
expect(payload.verificationToken).toBe('verify-token-visible');
|
|
4399
|
+
});
|
|
4400
|
+
});
|
|
4401
|
+
it('POST /api/taskforce/auth/password-reset/request should report no-send cases without claiming delivery', async () => {
|
|
4402
|
+
const authConfig = {
|
|
4403
|
+
runtimeMode: 'cloud',
|
|
4404
|
+
enabled: true,
|
|
4405
|
+
requiredForApi: true,
|
|
4406
|
+
excludedPaths: ['/api/taskforce/health', '/api/taskforce/auth'],
|
|
4407
|
+
defaultRole: 'member',
|
|
4408
|
+
defaultWorkspaceId: 'default',
|
|
4409
|
+
sessionCookieName: 'taskforce_session',
|
|
4410
|
+
sessionSecret: 'test-secret',
|
|
4411
|
+
sessionTtlSeconds: 3600
|
|
4412
|
+
};
|
|
4413
|
+
routes = createRoutes(core, { authConfig });
|
|
4414
|
+
core.requestPasswordReset.mockReturnValueOnce({});
|
|
4415
|
+
const match = matchRoute('POST', '/api/taskforce/auth/password-reset/request', routes);
|
|
4416
|
+
const req = createMockReq('POST', '/api/taskforce/auth/password-reset/request', { email: 'nobody@example.com' });
|
|
4417
|
+
const res = createMockRes();
|
|
4418
|
+
await match?.handler(req, res, {});
|
|
4419
|
+
const payload = JSON.parse(String(res.end.mock.calls.at(-1)?.[0] || '{}'));
|
|
4420
|
+
expect(payload).toMatchObject({
|
|
4421
|
+
success: true,
|
|
4422
|
+
emailSent: false,
|
|
4423
|
+
deliveryAttempted: false
|
|
4424
|
+
});
|
|
4425
|
+
expect(payload.emailError).toBeUndefined();
|
|
3858
4426
|
});
|
|
3859
|
-
it('POST /api/taskforce/auth/
|
|
4427
|
+
it('POST /api/taskforce/auth/password-reset/request should only expose resetToken when explicitly enabled', async () => {
|
|
3860
4428
|
const authConfig = {
|
|
3861
4429
|
runtimeMode: 'cloud',
|
|
3862
4430
|
enabled: true,
|
|
@@ -3868,26 +4436,26 @@ describe('Server Routes', () => {
|
|
|
3868
4436
|
sessionSecret: 'test-secret',
|
|
3869
4437
|
sessionTtlSeconds: 3600
|
|
3870
4438
|
};
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
4439
|
+
await withDevAuthTokenExposure(undefined, async () => {
|
|
4440
|
+
routes = createRoutes(core, { authConfig });
|
|
4441
|
+
core.requestPasswordReset.mockReturnValueOnce({ token: 'reset-token-hidden' });
|
|
4442
|
+
const match = matchRoute('POST', '/api/taskforce/auth/password-reset/request', routes);
|
|
4443
|
+
const req = createMockReq('POST', '/api/taskforce/auth/password-reset/request', { email: 'owner@example.com' });
|
|
4444
|
+
const res = createMockRes();
|
|
4445
|
+
await match?.handler(req, res, {});
|
|
4446
|
+
const payload = JSON.parse(String(res.end.mock.calls.at(-1)?.[0] || '{}'));
|
|
4447
|
+
expect(payload.resetToken).toBeUndefined();
|
|
3877
4448
|
});
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
4449
|
+
await withDevAuthTokenExposure('true', async () => {
|
|
4450
|
+
routes = createRoutes(core, { authConfig });
|
|
4451
|
+
core.requestPasswordReset.mockReturnValueOnce({ token: 'reset-token-visible' });
|
|
4452
|
+
const match = matchRoute('POST', '/api/taskforce/auth/password-reset/request', routes);
|
|
4453
|
+
const req = createMockReq('POST', '/api/taskforce/auth/password-reset/request', { email: 'owner@example.com' });
|
|
4454
|
+
const res = createMockRes();
|
|
4455
|
+
await match?.handler(req, res, {});
|
|
4456
|
+
const payload = JSON.parse(String(res.end.mock.calls.at(-1)?.[0] || '{}'));
|
|
4457
|
+
expect(payload.resetToken).toBe('reset-token-visible');
|
|
3882
4458
|
});
|
|
3883
|
-
const res = createMockRes();
|
|
3884
|
-
await match?.handler(req, res, {});
|
|
3885
|
-
expect(res.writeHead).toHaveBeenCalledWith(500, expect.any(Object));
|
|
3886
|
-
expect(res.end).toHaveBeenCalledWith(JSON.stringify({
|
|
3887
|
-
success: false,
|
|
3888
|
-
error: 'Configured onboarding plan version "missing-v9" was not found.',
|
|
3889
|
-
code: 'SIGNUP_PLAN_VERSION_NOT_FOUND'
|
|
3890
|
-
}));
|
|
3891
4459
|
});
|
|
3892
4460
|
it('POST /api/taskforce/auth/verify-email/confirm should verify token', async () => {
|
|
3893
4461
|
const authConfig = {
|
|
@@ -4344,6 +4912,40 @@ describe('Server Routes', () => {
|
|
|
4344
4912
|
expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
4345
4913
|
expect(res.end).toHaveBeenCalledWith(expect.stringContaining('"success":true'));
|
|
4346
4914
|
});
|
|
4915
|
+
it('POST /api/taskforce/auth/login should not depend on onboarding policy lookups', async () => {
|
|
4916
|
+
const authConfig = {
|
|
4917
|
+
runtimeMode: 'cloud',
|
|
4918
|
+
enabled: true,
|
|
4919
|
+
requiredForApi: true,
|
|
4920
|
+
excludedPaths: ['/api/taskforce/health', '/api/taskforce/auth'],
|
|
4921
|
+
defaultRole: 'member',
|
|
4922
|
+
defaultWorkspaceId: 'default',
|
|
4923
|
+
sessionCookieName: 'taskforce_session',
|
|
4924
|
+
sessionSecret: 'test-secret',
|
|
4925
|
+
sessionTtlSeconds: 3600
|
|
4926
|
+
};
|
|
4927
|
+
core.getOnboardingPolicy.mockImplementationOnce(() => {
|
|
4928
|
+
throw new Error('for SELECT DISTINCT, ORDER BY expressions must appear in select list');
|
|
4929
|
+
});
|
|
4930
|
+
routes = createRoutes(core, { authConfig });
|
|
4931
|
+
core.authenticatePasswordUserDetailed.mockReturnValueOnce({
|
|
4932
|
+
ok: false,
|
|
4933
|
+
code: 'INVALID_CREDENTIALS'
|
|
4934
|
+
});
|
|
4935
|
+
const match = matchRoute('POST', '/api/taskforce/auth/login', routes);
|
|
4936
|
+
const req = createMockReq('POST', '/api/taskforce/auth/login', {
|
|
4937
|
+
email: 'user@example.com',
|
|
4938
|
+
password: 'wrongpass'
|
|
4939
|
+
});
|
|
4940
|
+
const res = createMockRes();
|
|
4941
|
+
await match?.handler(req, res, {});
|
|
4942
|
+
expect(res.writeHead).toHaveBeenCalledWith(401, expect.any(Object));
|
|
4943
|
+
expect(res.end).toHaveBeenCalledWith(expect.stringContaining('Invalid email/password.'));
|
|
4944
|
+
expect(core.authenticatePasswordUserDetailed).toHaveBeenCalledWith(expect.objectContaining({
|
|
4945
|
+
email: 'user@example.com',
|
|
4946
|
+
requireVerifiedEmail: true
|
|
4947
|
+
}));
|
|
4948
|
+
});
|
|
4347
4949
|
it('GET /api/taskforce/workspaces should list authenticated user workspaces', async () => {
|
|
4348
4950
|
const authConfig = {
|
|
4349
4951
|
runtimeMode: 'cloud',
|
|
@@ -5663,6 +6265,109 @@ describe('Server Routes', () => {
|
|
|
5663
6265
|
]));
|
|
5664
6266
|
fs.rmSync(dbRoot, { recursive: true, force: true });
|
|
5665
6267
|
});
|
|
6268
|
+
it('GET /api/taskforce/sync/workspace/pull should hydrate canonical markdown documents from object storage', async () => {
|
|
6269
|
+
const authConfig = {
|
|
6270
|
+
runtimeMode: 'cloud',
|
|
6271
|
+
enabled: true,
|
|
6272
|
+
requiredForApi: true,
|
|
6273
|
+
excludedPaths: ['/api/taskforce/health', '/api/taskforce/auth'],
|
|
6274
|
+
defaultRole: 'member',
|
|
6275
|
+
defaultWorkspaceId: 'default',
|
|
6276
|
+
sessionCookieName: 'taskforce_session',
|
|
6277
|
+
sessionSecret: 'test-secret',
|
|
6278
|
+
sessionTtlSeconds: 3600
|
|
6279
|
+
};
|
|
6280
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
6281
|
+
status: 200,
|
|
6282
|
+
ok: true,
|
|
6283
|
+
headers: new Headers({ 'content-type': 'text/markdown' }),
|
|
6284
|
+
arrayBuffer: async () => Uint8Array.from(Buffer.from('# Canonical R2 doc\n')).buffer,
|
|
6285
|
+
});
|
|
6286
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
6287
|
+
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'taskforce-sync-pull-doc-r2-'));
|
|
6288
|
+
const basePath = path.join(tmpRoot, '.taskforce');
|
|
6289
|
+
fs.mkdirSync(basePath, { recursive: true });
|
|
6290
|
+
const { tmpRoot: dbRoot, db, store } = createWorkspaceAssetTestStore();
|
|
6291
|
+
store.upsertAsset({
|
|
6292
|
+
assetId: 'asset-doc-r2-1',
|
|
6293
|
+
workspaceId: 'workspace-1',
|
|
6294
|
+
kind: 'document',
|
|
6295
|
+
originalFilename: 'canonical-r2-doc.md',
|
|
6296
|
+
mimeType: 'text/markdown',
|
|
6297
|
+
storageProvider: 'r2',
|
|
6298
|
+
storageKey: 'workspaces/workspace-1/assets/documents/asset-doc-r2-1-canonical-r2-doc.md',
|
|
6299
|
+
contentSha256: 'sha-doc-r2-1',
|
|
6300
|
+
sizeBytes: 19,
|
|
6301
|
+
documentId: 'doc-r2-1',
|
|
6302
|
+
referenceNumber: 201,
|
|
6303
|
+
version: 2,
|
|
6304
|
+
isLatest: true,
|
|
6305
|
+
createdAt: '2026-03-17T09:00:00.000Z',
|
|
6306
|
+
updatedAt: '2026-03-17T10:00:00.000Z'
|
|
6307
|
+
});
|
|
6308
|
+
store.upsertLink({
|
|
6309
|
+
workspaceId: 'workspace-1',
|
|
6310
|
+
assetId: 'asset-doc-r2-1',
|
|
6311
|
+
taskId: 'task-1',
|
|
6312
|
+
role: 'attachment',
|
|
6313
|
+
createdAt: '2026-03-17T10:00:00.000Z',
|
|
6314
|
+
updatedAt: '2026-03-17T10:00:00.000Z'
|
|
6315
|
+
});
|
|
6316
|
+
core.getPaths.mockReturnValue({
|
|
6317
|
+
projectRoot: tmpRoot,
|
|
6318
|
+
basePath,
|
|
6319
|
+
});
|
|
6320
|
+
core.getDatabaseAdapter = vi.fn().mockReturnValue(db);
|
|
6321
|
+
core.getTenantId = vi.fn().mockReturnValue('default');
|
|
6322
|
+
routes = createRoutes(core, {
|
|
6323
|
+
authConfig,
|
|
6324
|
+
objectStorage: {
|
|
6325
|
+
provider: 'r2',
|
|
6326
|
+
r2: {
|
|
6327
|
+
accountId: 'acct-1',
|
|
6328
|
+
bucket: 'taskforce-docs',
|
|
6329
|
+
endpoint: 'https://example.r2.cloudflarestorage.com',
|
|
6330
|
+
accessKeyId: 'access-key-1',
|
|
6331
|
+
secretAccessKey: 'secret-key-1',
|
|
6332
|
+
keyPrefix: 'taskforce',
|
|
6333
|
+
signedUploadTtlSeconds: 600,
|
|
6334
|
+
signedDownloadTtlSeconds: 300
|
|
6335
|
+
}
|
|
6336
|
+
}
|
|
6337
|
+
});
|
|
6338
|
+
core.listUserWorkspaces.mockReturnValueOnce([{ id: 'workspace-1', slug: 'workspace-1', name: 'Workspace 1', description: null, role: 'owner', status: 'active', betaAccess: true }]);
|
|
6339
|
+
const cookie = createSessionTokenCookie('session-sync-pull-doc-r2', authConfig, {
|
|
6340
|
+
role: 'owner',
|
|
6341
|
+
workspaceId: 'workspace-1',
|
|
6342
|
+
userId: 'user-1'
|
|
6343
|
+
});
|
|
6344
|
+
core.listTasks.mockReturnValueOnce({ tasks: [] });
|
|
6345
|
+
core.listArchive.mockReturnValueOnce({ archived: [] });
|
|
6346
|
+
const match = matchRoute('GET', '/api/taskforce/sync/workspace/pull', routes);
|
|
6347
|
+
const req = createMockReq('GET', '/api/taskforce/sync/workspace/pull?workspaceId=workspace-1&limit=20', undefined, { cookie });
|
|
6348
|
+
const res = createMockRes();
|
|
6349
|
+
await match?.handler(req, res, {});
|
|
6350
|
+
const payloadRaw = res.end.mock.calls[0]?.[0];
|
|
6351
|
+
const payload = JSON.parse(String(payloadRaw || '{}'));
|
|
6352
|
+
expect(payload.changes).toEqual(expect.arrayContaining([
|
|
6353
|
+
expect.objectContaining({
|
|
6354
|
+
op: 'document-upsert',
|
|
6355
|
+
path: 'workspaces/workspace-1/assets/documents/asset-doc-r2-1-canonical-r2-doc.md',
|
|
6356
|
+
content: '# Canonical R2 doc\n',
|
|
6357
|
+
assetId: 'asset-doc-r2-1',
|
|
6358
|
+
documentId: 'doc-r2-1',
|
|
6359
|
+
referenceNumber: 201,
|
|
6360
|
+
version: 2,
|
|
6361
|
+
taskId: 'task-1',
|
|
6362
|
+
originalFilename: 'canonical-r2-doc.md',
|
|
6363
|
+
linkRole: 'attachment'
|
|
6364
|
+
})
|
|
6365
|
+
]));
|
|
6366
|
+
expect(fetchMock).toHaveBeenCalled();
|
|
6367
|
+
vi.unstubAllGlobals();
|
|
6368
|
+
fs.rmSync(dbRoot, { recursive: true, force: true });
|
|
6369
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
6370
|
+
});
|
|
5666
6371
|
it('GET /api/taskforce/sync/workspace/pull should include document review session upserts and deletes', async () => {
|
|
5667
6372
|
const authConfig = {
|
|
5668
6373
|
runtimeMode: 'cloud',
|
|
@@ -6531,6 +7236,77 @@ describe('Server Routes', () => {
|
|
|
6531
7236
|
});
|
|
6532
7237
|
expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
6533
7238
|
});
|
|
7239
|
+
it('POST /api/taskforce/sync/workspace/apply-local should apply task event upserts after task snapshots', async () => {
|
|
7240
|
+
core.resolvePreferredWorkspaceId.mockReturnValue('workspace-local');
|
|
7241
|
+
core.getTask.mockImplementation((taskId) => (taskId === 'task-sync-activity-1'
|
|
7242
|
+
? {
|
|
7243
|
+
id: 'task-sync-activity-1',
|
|
7244
|
+
title: 'Synced task',
|
|
7245
|
+
status: 'done',
|
|
7246
|
+
category: 'general',
|
|
7247
|
+
type: 'task',
|
|
7248
|
+
createdAt: '2026-03-24T10:00:00.000Z',
|
|
7249
|
+
updatedAt: '2026-03-24T10:05:00.000Z',
|
|
7250
|
+
taskEvents: []
|
|
7251
|
+
}
|
|
7252
|
+
: null));
|
|
7253
|
+
routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
|
|
7254
|
+
const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
|
|
7255
|
+
const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
|
|
7256
|
+
changes: [
|
|
7257
|
+
{
|
|
7258
|
+
op: 'upsert',
|
|
7259
|
+
archived: false,
|
|
7260
|
+
task: {
|
|
7261
|
+
id: 'task-sync-activity-1',
|
|
7262
|
+
title: 'Synced task',
|
|
7263
|
+
status: 'done',
|
|
7264
|
+
category: 'general',
|
|
7265
|
+
type: 'task',
|
|
7266
|
+
updatedAt: '2026-03-24T10:05:00.000Z'
|
|
7267
|
+
}
|
|
7268
|
+
},
|
|
7269
|
+
{
|
|
7270
|
+
op: 'task-event-upsert',
|
|
7271
|
+
event: {
|
|
7272
|
+
id: 'task-event-sync-1',
|
|
7273
|
+
taskId: 'task-sync-activity-1',
|
|
7274
|
+
workspaceId: 'workspace-local',
|
|
7275
|
+
action: 'task-status-changed',
|
|
7276
|
+
actor: 'ai-profile-1',
|
|
7277
|
+
actorType: 'ai',
|
|
7278
|
+
createdAt: '2026-03-24T10:05:00.000Z',
|
|
7279
|
+
details: {
|
|
7280
|
+
changes: {
|
|
7281
|
+
status: {
|
|
7282
|
+
from: 'in-progress',
|
|
7283
|
+
to: 'done'
|
|
7284
|
+
}
|
|
7285
|
+
}
|
|
7286
|
+
}
|
|
7287
|
+
}
|
|
7288
|
+
}
|
|
7289
|
+
]
|
|
7290
|
+
}, {
|
|
7291
|
+
'x-taskforce-workspace-id': 'workspace-local'
|
|
7292
|
+
});
|
|
7293
|
+
const res = createMockRes();
|
|
7294
|
+
await match?.handler(req, res, {});
|
|
7295
|
+
expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith(expect.objectContaining({
|
|
7296
|
+
tasks: [expect.objectContaining({ id: 'task-sync-activity-1' })],
|
|
7297
|
+
archived: []
|
|
7298
|
+
}), 'workspace-local');
|
|
7299
|
+
expect(core.upsertTaskEventForSync).toHaveBeenCalledWith(expect.objectContaining({
|
|
7300
|
+
id: 'task-event-sync-1',
|
|
7301
|
+
taskId: 'task-sync-activity-1',
|
|
7302
|
+
workspaceId: 'workspace-local',
|
|
7303
|
+
action: 'task-status-changed',
|
|
7304
|
+
actor: 'ai-profile-1',
|
|
7305
|
+
actorType: 'ai',
|
|
7306
|
+
createdAt: '2026-03-24T10:05:00.000Z'
|
|
7307
|
+
}));
|
|
7308
|
+
expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
7309
|
+
});
|
|
6534
7310
|
it('POST /api/taskforce/sync/workspace/apply-local should fail when document review session persistence fails', async () => {
|
|
6535
7311
|
core.resolvePreferredWorkspaceId.mockReturnValue('workspace-local');
|
|
6536
7312
|
const error = new Error('document review session write failed');
|
|
@@ -8771,6 +9547,73 @@ describe('Server Routes', () => {
|
|
|
8771
9547
|
expect(payload.authenticated).toBe(true);
|
|
8772
9548
|
expect(payload.avatarUrl).toBe('/api/taskforce/context/users%2Fu-avatar%2Fprofile%2Favatar%2Fdraft.png');
|
|
8773
9549
|
});
|
|
9550
|
+
it('GET /api/taskforce/auth/session should not wait for avatar cleanup before responding', async () => {
|
|
9551
|
+
const authConfig = {
|
|
9552
|
+
runtimeMode: 'cloud',
|
|
9553
|
+
enabled: true,
|
|
9554
|
+
requiredForApi: true,
|
|
9555
|
+
excludedPaths: ['/api/taskforce/health', '/api/taskforce/auth'],
|
|
9556
|
+
defaultRole: 'member',
|
|
9557
|
+
defaultWorkspaceId: 'default',
|
|
9558
|
+
sessionCookieName: 'taskforce_session',
|
|
9559
|
+
sessionSecret: 'test-secret',
|
|
9560
|
+
sessionTtlSeconds: 3600
|
|
9561
|
+
};
|
|
9562
|
+
const deleteObjectCompletion = { resolve: null };
|
|
9563
|
+
const deleteObjectSpy = vi.spyOn(ObjectStorageClient.prototype, 'deleteObject').mockImplementation(() => new Promise((resolve) => {
|
|
9564
|
+
deleteObjectCompletion.resolve = resolve;
|
|
9565
|
+
}));
|
|
9566
|
+
routes = createRoutes(core, {
|
|
9567
|
+
authConfig,
|
|
9568
|
+
objectStorage: {
|
|
9569
|
+
provider: 'r2',
|
|
9570
|
+
r2: {
|
|
9571
|
+
accountId: 'test-account',
|
|
9572
|
+
bucket: 'taskforce-test',
|
|
9573
|
+
endpoint: 'https://example.com',
|
|
9574
|
+
accessKeyId: 'test-key',
|
|
9575
|
+
secretAccessKey: 'test-secret',
|
|
9576
|
+
keyPrefix: '',
|
|
9577
|
+
signedUploadTtlSeconds: 60,
|
|
9578
|
+
signedDownloadTtlSeconds: 60
|
|
9579
|
+
}
|
|
9580
|
+
}
|
|
9581
|
+
});
|
|
9582
|
+
core.cleanupStaleUserAvatarUploadDrafts.mockReturnValueOnce([{
|
|
9583
|
+
draftId: 'avatar-draft-stale',
|
|
9584
|
+
userId: 'u-avatar',
|
|
9585
|
+
storageKey: 'users/u-avatar/profile/avatar/avatar-draft-stale.png',
|
|
9586
|
+
discardedAt: null,
|
|
9587
|
+
consumedAt: null,
|
|
9588
|
+
finalizedAt: null,
|
|
9589
|
+
createdAt: '2026-04-14T00:00:00.000Z',
|
|
9590
|
+
updatedAt: '2026-04-14T00:00:00.000Z'
|
|
9591
|
+
}]);
|
|
9592
|
+
const cookie = createSessionTokenCookie('session-avatar-cleanup', authConfig, {
|
|
9593
|
+
userId: 'u-avatar',
|
|
9594
|
+
role: 'member',
|
|
9595
|
+
workspaceId: 'workspace-1'
|
|
9596
|
+
});
|
|
9597
|
+
const match = matchRoute('GET', '/api/taskforce/auth/session', routes);
|
|
9598
|
+
const req = createMockReq('GET', '/api/taskforce/auth/session', undefined, { cookie });
|
|
9599
|
+
const res = createMockRes();
|
|
9600
|
+
try {
|
|
9601
|
+
const outcome = await Promise.race([
|
|
9602
|
+
match?.handler(req, res, {}).then(() => 'completed'),
|
|
9603
|
+
new Promise((resolve) => setTimeout(() => resolve('timeout'), 25))
|
|
9604
|
+
]);
|
|
9605
|
+
expect(outcome).toBe('completed');
|
|
9606
|
+
const payload = JSON.parse(res.end.mock.calls[0][0]);
|
|
9607
|
+
expect(payload.authenticated).toBe(true);
|
|
9608
|
+
expect(deleteObjectSpy).toHaveBeenCalledWith('users/u-avatar/profile/avatar/avatar-draft-stale.png');
|
|
9609
|
+
}
|
|
9610
|
+
finally {
|
|
9611
|
+
if (deleteObjectCompletion.resolve) {
|
|
9612
|
+
deleteObjectCompletion.resolve(true);
|
|
9613
|
+
}
|
|
9614
|
+
deleteObjectSpy.mockRestore();
|
|
9615
|
+
}
|
|
9616
|
+
});
|
|
8774
9617
|
it('POST /api/taskforce/auth/profile should save avatar by validated draft id', async () => {
|
|
8775
9618
|
const authConfig = {
|
|
8776
9619
|
runtimeMode: 'cloud',
|
|
@@ -10300,6 +11143,43 @@ describe('Server Routes', () => {
|
|
|
10300
11143
|
}
|
|
10301
11144
|
});
|
|
10302
11145
|
});
|
|
11146
|
+
it('POST /api/taskforce/admin/system-settings should preserve onboarding policy when onboarding fields are omitted', async () => {
|
|
11147
|
+
core.getOnboardingPolicy.mockReturnValue({
|
|
11148
|
+
mode: 'auto_grant_free',
|
|
11149
|
+
planVersionId: 'starter-v1'
|
|
11150
|
+
});
|
|
11151
|
+
const match = matchRoute('POST', '/api/taskforce/admin/system-settings', routes);
|
|
11152
|
+
const req = createMockReq('POST', '/api/taskforce/admin/system-settings', {
|
|
11153
|
+
allowSelfRegistration: false,
|
|
11154
|
+
requireVerifiedEmail: true,
|
|
11155
|
+
defaultNewUserBetaAccess: false,
|
|
11156
|
+
authRateLimitEnabled: true,
|
|
11157
|
+
authRateLimitMaxRequests: 10,
|
|
11158
|
+
authRateLimitWindowMs: 900000
|
|
11159
|
+
}, { 'x-taskforce-role': 'admin' });
|
|
11160
|
+
const res = createMockRes();
|
|
11161
|
+
await match?.handler(req, res, {});
|
|
11162
|
+
expect(core.validateOnboardingPolicy).toHaveBeenCalledWith({
|
|
11163
|
+
mode: 'auto_grant_free',
|
|
11164
|
+
planVersionId: 'starter-v1'
|
|
11165
|
+
});
|
|
11166
|
+
expect(core.updateGlobalSettings).toHaveBeenCalledWith({
|
|
11167
|
+
auth: {
|
|
11168
|
+
allowSelfRegistration: false,
|
|
11169
|
+
requireVerifiedEmail: true,
|
|
11170
|
+
defaultNewUserBetaAccess: false,
|
|
11171
|
+
authRateLimitEnabled: true,
|
|
11172
|
+
authRateLimitMaxRequests: 10,
|
|
11173
|
+
authRateLimitWindowMs: 900000
|
|
11174
|
+
},
|
|
11175
|
+
billing: {
|
|
11176
|
+
onboardingPolicy: {
|
|
11177
|
+
mode: 'auto_grant_free',
|
|
11178
|
+
planVersionId: 'starter-v1'
|
|
11179
|
+
}
|
|
11180
|
+
}
|
|
11181
|
+
});
|
|
11182
|
+
});
|
|
10303
11183
|
it('GET /api/taskforce/admin/site-settings should expose effective website settings', async () => {
|
|
10304
11184
|
core.getGlobalSettings.mockReturnValue({
|
|
10305
11185
|
site: {
|
|
@@ -11571,7 +12451,7 @@ describe('Server Routes', () => {
|
|
|
11571
12451
|
errors: []
|
|
11572
12452
|
}));
|
|
11573
12453
|
});
|
|
11574
|
-
it('PATCH /api/taskforce/task/:id should pass autosave
|
|
12454
|
+
it('PATCH /api/taskforce/task/:id should pass autosave metadata through to core.updateTask', async () => {
|
|
11575
12455
|
const updatedTask = { id: '123', description: 'Draft body' };
|
|
11576
12456
|
core.updateTask.mockReturnValue(updatedTask);
|
|
11577
12457
|
const url = '/api/taskforce/task/123';
|
|
@@ -11581,12 +12461,12 @@ describe('Server Routes', () => {
|
|
|
11581
12461
|
await match?.handler(req, res, { id: '123' });
|
|
11582
12462
|
expect(core.updateTask).toHaveBeenCalledWith('123', { description: 'Draft body' }, 'default', {
|
|
11583
12463
|
actorRef: 'user',
|
|
11584
|
-
|
|
12464
|
+
saveSource: 'autosave'
|
|
11585
12465
|
});
|
|
11586
12466
|
expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
11587
12467
|
expect(res.end).toHaveBeenCalledWith(JSON.stringify(updatedTask));
|
|
11588
12468
|
});
|
|
11589
|
-
it('PATCH /api/taskforce/task/:id should
|
|
12469
|
+
it('PATCH /api/taskforce/task/:id should pass manual save metadata through to core.updateTask', async () => {
|
|
11590
12470
|
const updatedTask = { id: '123', description: 'Manual body' };
|
|
11591
12471
|
core.updateTask.mockReturnValue(updatedTask);
|
|
11592
12472
|
const url = '/api/taskforce/task/123';
|
|
@@ -11596,7 +12476,7 @@ describe('Server Routes', () => {
|
|
|
11596
12476
|
await match?.handler(req, res, { id: '123' });
|
|
11597
12477
|
expect(core.updateTask).toHaveBeenCalledWith('123', { description: 'Manual body' }, 'default', {
|
|
11598
12478
|
actorRef: 'user',
|
|
11599
|
-
|
|
12479
|
+
saveSource: 'manual'
|
|
11600
12480
|
});
|
|
11601
12481
|
expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
11602
12482
|
expect(res.end).toHaveBeenCalledWith(JSON.stringify(updatedTask));
|