@taskforcehq/taskforce 0.3.306 → 0.3.308
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 +0 -2
- package/dist/Taskforce.module.css +317 -27
- package/dist/TaskforceCore.js +1 -0
- package/dist/TaskforceCore.test.js +178 -0
- package/dist/components/activity/EntityActivityTimeline.d.ts +34 -0
- package/dist/components/activity/EntityActivityTimeline.js +81 -0
- package/dist/components/context/ContextAttachmentManager.js +15 -3
- package/dist/components/features/AgentsModule.d.ts +5 -1
- package/dist/components/features/AgentsModule.js +142 -39
- package/dist/components/features/AgentsModule.test.js +260 -11
- package/dist/components/features/AnnotatedAttachmentWorkspace.d.ts +3 -1
- package/dist/components/features/AnnotatedAttachmentWorkspace.js +118 -4
- package/dist/components/features/AnnotatedAttachmentWorkspace.test.js +52 -0
- package/dist/components/features/DocumentIndex.d.ts +2 -2
- package/dist/components/features/DocumentIndex.js +3 -4
- package/dist/components/features/DocumentViewer.d.ts +3 -1
- package/dist/components/features/DocumentViewer.js +6 -5
- package/dist/components/features/DocumentWorkspace.d.ts +10 -2
- package/dist/components/features/DocumentWorkspace.js +15 -4
- package/dist/components/features/DocumentWorkspace.test.js +20 -3
- package/dist/components/features/TaskSettings.d.ts +2 -0
- package/dist/components/features/TaskSettings.js +83 -78
- package/dist/components/features/TaskSettings.test.js +171 -82
- package/dist/components/task/TaskActionHeader.js +11 -5
- package/dist/components/task/TaskActionHeader.test.js +10 -0
- package/dist/components/task/TaskCard.js +6 -2
- package/dist/components/task/TaskContextUpload.test.js +3 -3
- package/dist/components/task/TaskForm.js +14 -64
- package/dist/components/task/TaskKanban.js +0 -8
- package/dist/components/task/TaskList.js +5 -1
- package/dist/components/task/TaskList.test.js +4 -0
- package/dist/components/ui/HierarchyActionBadge.d.ts +10 -0
- package/dist/components/ui/HierarchyActionBadge.js +5 -0
- package/dist/components/views/AppShellHeader.d.ts +3 -1
- package/dist/components/views/AppShellHeader.js +27 -11
- package/dist/components/views/AppShellHeader.test.js +18 -1
- package/dist/components/views/PlanComparisonPage.d.ts +20 -15
- package/dist/components/views/PlanComparisonPage.js +221 -82
- package/dist/components/views/PlanComparisonPage.test.js +268 -14
- package/dist/components/views/PlansPage.js +16 -10
- package/dist/components/views/PlansPage.test.js +4 -2
- package/dist/components/views/StandaloneLayout.js +256 -69
- package/dist/components/views/WidgetView.js +10 -8
- package/dist/components/views/panels/PlanningDrawer.d.ts +19 -6
- package/dist/components/views/panels/PlanningDrawer.js +259 -51
- package/dist/components/views/panels/PlanningDrawer.test.js +392 -4
- package/dist/config/envSchema.js +1 -0
- package/dist/core/AiProfileService.d.ts +7 -0
- package/dist/core/AiProfileService.js +460 -35
- package/dist/core/AiProfiles.test.js +648 -2
- package/dist/core/AttachmentLinkService.d.ts +3 -0
- package/dist/core/AttachmentLinkService.js +58 -13
- package/dist/core/GlobalSettingsService.js +30 -0
- package/dist/core/GlobalSettingsService.test.d.ts +1 -0
- package/dist/core/GlobalSettingsService.test.js +45 -0
- package/dist/core/McpTokenService.d.ts +12 -0
- package/dist/core/McpTokenService.js +44 -1
- package/dist/core/PlanEntitlementService.d.ts +20 -0
- package/dist/core/PlanEntitlementService.js +215 -19
- package/dist/core/PlanFeatureCatalog.test.js +55 -0
- package/dist/core/PlanningEntities.test.js +366 -4
- package/dist/core/TaskActivityService.d.ts +28 -1
- package/dist/core/TaskActivityService.js +227 -46
- package/dist/core/TaskActivitySync.test.d.ts +1 -0
- package/dist/core/TaskActivitySync.test.js +90 -0
- package/dist/core/TaskAuditTimeline.test.js +96 -0
- package/dist/core/Taskforce.d.ts +204 -26
- package/dist/core/Taskforce.ids.test.js +58 -0
- package/dist/core/Taskforce.js +781 -71
- package/dist/core/Taskforce.mcpAuth.test.js +109 -0
- package/dist/core/types.d.ts +10 -0
- package/dist/hooks/auth/useTaskforceAuthBootstrap.js +2 -1
- package/dist/hooks/bootstrapTimeouts.d.ts +4 -0
- package/dist/hooks/bootstrapTimeouts.js +8 -0
- package/dist/hooks/bootstrapTimeouts.test.d.ts +1 -0
- package/dist/hooks/bootstrapTimeouts.test.js +12 -0
- package/dist/hooks/sync/orchestratorShared.d.ts +3 -3
- package/dist/hooks/sync/orchestratorShared.js +61 -1
- package/dist/hooks/sync/recovery.js +8 -0
- package/dist/hooks/sync/recovery.test.js +5 -5
- package/dist/hooks/sync/transfers.d.ts +2 -1
- package/dist/hooks/sync/transfers.js +36 -9
- package/dist/hooks/sync/transfers.test.js +534 -2
- package/dist/hooks/useFilterSort.js +13 -13
- package/dist/hooks/useFilterSort.test.js +87 -1
- package/dist/hooks/useSyncOrchestrator.js +50 -5
- package/dist/hooks/useTaskforce.d.ts +85 -50
- package/dist/hooks/useTaskforce.js +44 -14
- package/dist/hooks/useTaskforce.runtime-routing.test.js +94 -0
- package/dist/hooks/useTaskforce.sync-behavior.test.js +99 -0
- package/dist/hooks/useWorkspaceSyncController.js +7 -1
- package/dist/hooks/useWorkspaceSyncController.test.js +48 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +14 -7
- package/dist/mcp/canonicalAssetHelpers.d.ts +16 -4
- package/dist/mcp/canonicalAssetHelpers.js +18 -10
- package/dist/mcp/canonicalAssetHelpers.test.d.ts +1 -0
- package/dist/mcp/canonicalAssetHelpers.test.js +67 -0
- package/dist/mcp/clientIntegrations.d.ts +1 -0
- package/dist/mcp/clientIntegrations.js +25 -0
- package/dist/mcp/clientIntegrations.test.js +14 -3
- package/dist/mcp/clientProfileDefaults.js +3 -3
- package/dist/mcp/collaborationRegistrar.js +7 -7
- package/dist/mcp/documentAssetRegistrar.js +113 -7
- package/dist/mcp/runtime.d.ts +1 -0
- package/dist/mcp/runtime.js +969 -122
- package/dist/mcp/runtime.test.js +1083 -11
- package/dist/mcp/taskAttachmentTypes.d.ts +1 -1
- package/dist/mcp/taskPlanningRegistrar.js +48 -0
- package/dist/mcp/toolRegistry.d.ts +56 -0
- package/dist/mcp/toolRegistry.js +8 -0
- package/dist/mcp/toolRegistry.test.js +4 -0
- package/dist/migrations/taskSchemaMigrations.js +58 -15
- package/dist/server/aiProfileAvatarAssets.d.ts +49 -0
- package/dist/server/aiProfileAvatarAssets.js +319 -0
- package/dist/server/aiProfileAvatarAssets.test.d.ts +1 -0
- package/dist/server/aiProfileAvatarAssets.test.js +213 -0
- package/dist/server/annotatedAttachmentsRoutes.test.js +66 -7
- package/dist/server/index.js +4 -0
- package/dist/server/index.test.js +6 -1
- package/dist/server/routes/admin.js +328 -9
- package/dist/server/routes/annotatedAttachments.js +51 -0
- package/dist/server/routes/auth.d.ts +11 -0
- package/dist/server/routes/auth.js +189 -27
- package/dist/server/routes/authSupport.d.ts +5 -0
- package/dist/server/routes/authSupport.js +13 -1
- package/dist/server/routes/billing.d.ts +1 -0
- package/dist/server/routes/billing.js +116 -63
- package/dist/server/routes/billing.test.js +162 -11
- package/dist/server/routes/documents.js +10 -0
- package/dist/server/routes/shared.js +0 -10
- package/dist/server/routes/sync.integration.test.js +221 -0
- package/dist/server/routes/sync.js +13 -3
- package/dist/server/routes/syncSnapshotRoutes.js +2 -0
- package/dist/server/routes/tasks.js +12 -9
- package/dist/server/routes.js +87 -8
- package/dist/server/routes.test.js +972 -23
- package/dist/shared/aiProfileSurfaceType.d.ts +1 -1
- package/dist/shared/aiProfileSurfaceType.js +10 -8
- package/dist/storage/objectStorage.d.ts +1 -0
- package/dist/storage/objectStorage.js +2 -1
- package/dist/storage/objectStorageClient.d.ts +1 -0
- package/dist/storage/objectStorageClient.js +41 -8
- package/dist/storage/objectStorageClient.test.js +26 -1
- package/dist/storage/objectStorageConfig.js +3 -1
- package/dist/storage/objectStorageConfig.test.js +13 -1
- package/dist/sync/collaborationSyncPayload.js +2 -0
- package/dist/sync/collaborationSyncPayload.test.js +1 -0
- package/dist/sync/collaborationSyncState.d.ts +3 -3
- package/dist/sync/collaborationSyncState.js +19 -12
- package/dist/sync/contentSyncPayload.d.ts +5 -1
- package/dist/sync/contentSyncPayload.js +9 -0
- package/dist/sync/contentSyncPayload.test.js +39 -0
- package/dist/sync/contentSyncState.d.ts +5 -1
- package/dist/sync/contentSyncState.js +231 -37
- package/dist/sync/planningSyncPayload.js +5 -0
- package/dist/sync/planningSyncPayload.test.js +24 -0
- package/dist/sync/syncApplyHandlers.d.ts +4 -4
- package/dist/sync/syncApplyHandlers.js +89 -31
- package/dist/sync/syncApplyHandlers.test.js +184 -4
- package/dist/sync/syncResourceAdapters.d.ts +2 -0
- package/dist/sync/syncResourceAdapters.js +2 -0
- package/dist/sync/syncService.d.ts +1 -0
- package/dist/sync/syncService.js +13 -1
- package/dist/sync/syncService.test.js +39 -0
- package/dist/sync/workspacePullFeed.d.ts +4 -0
- package/dist/sync/workspacePullFeed.js +129 -70
- package/dist/sync/workspacePullFeed.test.js +49 -0
- package/dist/sync/workspaceSyncModel.d.ts +28 -6
- package/dist/sync/workspaceSyncModel.js +159 -45
- package/dist/sync/workspaceSyncModel.test.js +269 -3
- package/dist/types.d.ts +33 -2
- package/dist/ui/agent-logos/ChatGPT.png +0 -0
- package/dist/ui/agent-logos/Gemini CLI.jpeg +0 -0
- package/dist/ui/agent-logos/antigravity.jpeg +0 -0
- package/dist/ui/agent-logos/claude-code.jpeg +0 -0
- package/dist/ui/agent-logos/codex.jpeg +0 -0
- package/dist/ui/agent-logos/cursor.png +0 -0
- package/dist/ui/agent-logos/openclaw.jpeg +0 -0
- package/dist/ui/agent-logos/windsurf.png +0 -0
- package/dist/ui/assets/AgentsModule-BS4Pi_nC.js +1 -0
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-BVKew-mH.css +1 -0
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-CCckzWYZ.js +3 -0
- package/dist/ui/assets/ContextAttachmentManager-CfG_ER7C.js +3 -0
- package/dist/ui/assets/DocumentWorkspace-BVaWPA25.js +252 -0
- package/dist/ui/assets/DocumentWorkspace-D4rJKXzd.css +1 -0
- package/dist/ui/assets/EntityActivityTimeline-DSj0ThaK.js +1 -0
- package/dist/ui/assets/EntityActivityTimeline-DyqvvhuF.css +1 -0
- package/dist/ui/assets/{InitiativesModule-9E6ParrY.js → InitiativesModule-AxMkWtxH.js} +1 -1
- package/dist/ui/assets/PlansPage-B_z-D6Nh.css +1 -0
- package/dist/ui/assets/PlansPage-Ndt7mYfo.js +1 -0
- package/dist/ui/assets/TaskContextUpload-Dw4rTF4i.js +1 -0
- package/dist/ui/assets/TaskSettings-f2ga42_V.js +9 -0
- package/dist/ui/assets/{WorkflowsModule-BA7jcEpH.js → WorkflowsModule-E4UL3mov.js} +4 -4
- package/dist/ui/assets/documentReferences-BOUcJm6-.js +1 -0
- package/dist/ui/assets/index-BvAbqx5Q.css +1 -0
- package/dist/ui/assets/index-MAHKvFqp.js +4 -0
- package/dist/ui/assets/{vendor-icons-DuEd_65c.js → vendor-icons-B32hGuF2.js} +1 -1
- package/dist/ui/index.html +3 -3
- package/dist/ui/og-image.png +0 -0
- package/dist/ui/product/agent manager.png +0 -0
- package/dist/ui/product/agent_manager_02.png +0 -0
- package/dist/utils/aiProfileEvents.d.ts +7 -0
- package/dist/utils/aiProfileEvents.js +6 -0
- package/dist/utils/assignees.d.ts +6 -0
- package/dist/utils/assignees.js +8 -1
- package/dist/utils/assignees.test.d.ts +1 -0
- package/dist/utils/assignees.test.js +25 -0
- package/dist/utils/avatarDisplay.d.ts +1 -0
- package/dist/utils/avatarDisplay.js +15 -0
- package/dist/utils/contextFiles.d.ts +3 -1
- package/dist/utils/contextFiles.js +2 -1
- package/dist/utils/taskActivity.d.ts +7 -1
- package/dist/utils/taskActivity.js +63 -1
- package/dist/utils/taskActivity.test.js +23 -1
- package/dist/utils/taskSearch.d.ts +22 -0
- package/dist/utils/taskSearch.js +87 -0
- package/dist/utils/taskSearch.test.d.ts +1 -0
- package/dist/utils/taskSearch.test.js +69 -0
- package/dist/utils/taskUtils.js +10 -3
- package/dist/utils/taskUtils.test.d.ts +1 -0
- package/dist/utils/taskUtils.test.js +19 -0
- package/package.json +1 -1
- package/dist/ui/assets/AgentsModule-DrLawwNl.js +0 -1
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-BaS2VwIr.css +0 -1
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-Z9_whf7F.js +0 -3
- package/dist/ui/assets/DocumentWorkspace-B03MaSkw.js +0 -252
- package/dist/ui/assets/DocumentWorkspace-Dhdso07p.css +0 -1
- package/dist/ui/assets/PlansPage-CvfnMiWq.css +0 -1
- package/dist/ui/assets/PlansPage-DRXscYxH.js +0 -1
- package/dist/ui/assets/TaskSettings-DZX4jk7e.js +0 -9
- package/dist/ui/assets/index-9eT8e0Lu.css +0 -1
- package/dist/ui/assets/index-C6k75jr8.js +0 -6
|
@@ -138,6 +138,7 @@ vi.mock('../core/Taskforce', () => {
|
|
|
138
138
|
listMcpAccessTokens = vi.fn().mockReturnValue({ items: [], total: 0, page: 1, pageSize: 20 });
|
|
139
139
|
createMcpAccessToken = vi.fn();
|
|
140
140
|
revokeMcpAccessToken = vi.fn();
|
|
141
|
+
deleteMcpAccessToken = vi.fn();
|
|
141
142
|
regenerateMcpAccessToken = vi.fn();
|
|
142
143
|
inspectMcpAccessToken = vi.fn();
|
|
143
144
|
authenticateMcpAccessToken = vi.fn();
|
|
@@ -164,12 +165,14 @@ vi.mock('../core/Taskforce', () => {
|
|
|
164
165
|
countAiProfileUsage = vi.fn().mockReturnValue({ used: 0, limit: null, remaining: null });
|
|
165
166
|
mergeAiProfiles = vi.fn();
|
|
166
167
|
archiveAiProfile = vi.fn();
|
|
168
|
+
updateAiProfileAvatar = vi.fn();
|
|
167
169
|
replaceWorkspaceUsersForSync = vi.fn().mockReturnValue([]);
|
|
168
170
|
listDocumentReviewSessionsForSync = vi.fn().mockReturnValue([]);
|
|
169
171
|
listDocumentReviewCommentsForSync = vi.fn().mockReturnValue([]);
|
|
170
172
|
listAnnotatedAttachmentSessionsForSync = vi.fn().mockReturnValue([]);
|
|
171
173
|
listTaskEventsForSync = vi.fn().mockReturnValue([]);
|
|
172
174
|
listTaskEvents = vi.fn().mockReturnValue([]);
|
|
175
|
+
recordTaskEvent = vi.fn();
|
|
173
176
|
getDocumentReviewSession = vi.fn().mockReturnValue(null);
|
|
174
177
|
upsertAiProfileForSync = vi.fn();
|
|
175
178
|
upsertDocumentReviewSessionForSync = vi.fn();
|
|
@@ -829,13 +832,15 @@ describe('Server Routes', () => {
|
|
|
829
832
|
}));
|
|
830
833
|
expect(res.end).toHaveBeenCalledWith();
|
|
831
834
|
});
|
|
832
|
-
it('POST /mcp should accept authenticated initialize requests
|
|
835
|
+
it('POST /mcp should accept authenticated initialize requests, record token usage, and close the request runtime', async () => {
|
|
836
|
+
const serverClose = vi.fn().mockResolvedValue(undefined);
|
|
833
837
|
const createServerSpy = vi.spyOn(mcpRuntime, 'createTaskforceMcpServer').mockReturnValue({
|
|
834
838
|
server: {
|
|
835
839
|
connect: vi.fn().mockResolvedValue(undefined),
|
|
836
|
-
close:
|
|
840
|
+
close: serverClose
|
|
837
841
|
}
|
|
838
842
|
});
|
|
843
|
+
const closeTransportSpy = vi.spyOn(StreamableHTTPServerTransport.prototype, 'close').mockResolvedValue(undefined);
|
|
839
844
|
const handleRequestSpy = vi.spyOn(StreamableHTTPServerTransport.prototype, 'handleRequest').mockImplementation(async (_req, res, parsedBody) => {
|
|
840
845
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
841
846
|
res.end(JSON.stringify({
|
|
@@ -878,6 +883,8 @@ describe('Server Routes', () => {
|
|
|
878
883
|
expect(match).not.toBeNull();
|
|
879
884
|
await match.handler(req, res, match.params);
|
|
880
885
|
expect(core.authenticateMcpAccessToken).toHaveBeenCalledWith('tfmcp_test_token');
|
|
886
|
+
const runtimeOptions = createServerSpy.mock.calls[0]?.[0];
|
|
887
|
+
expect(runtimeOptions?.core).toBe(core);
|
|
881
888
|
expect(createServerSpy).toHaveBeenCalledWith(expect.objectContaining({
|
|
882
889
|
workspaceId: 'workspace-1',
|
|
883
890
|
runtimeMode: 'cloud',
|
|
@@ -907,6 +914,62 @@ describe('Server Routes', () => {
|
|
|
907
914
|
})
|
|
908
915
|
})
|
|
909
916
|
}));
|
|
917
|
+
expect(closeTransportSpy).not.toHaveBeenCalled();
|
|
918
|
+
expect(serverClose).toHaveBeenCalledTimes(1);
|
|
919
|
+
closeTransportSpy.mockRestore();
|
|
920
|
+
handleRequestSpy.mockRestore();
|
|
921
|
+
createServerSpy.mockRestore();
|
|
922
|
+
});
|
|
923
|
+
it('POST /mcp should distinguish transport failures from tool backend failures', async () => {
|
|
924
|
+
const serverClose = vi.fn().mockResolvedValue(undefined);
|
|
925
|
+
const createServerSpy = vi.spyOn(mcpRuntime, 'createTaskforceMcpServer').mockReturnValue({
|
|
926
|
+
server: {
|
|
927
|
+
connect: vi.fn().mockResolvedValue(undefined),
|
|
928
|
+
close: serverClose
|
|
929
|
+
}
|
|
930
|
+
});
|
|
931
|
+
const handleRequestSpy = vi.spyOn(StreamableHTTPServerTransport.prototype, 'handleRequest').mockRejectedValue(new Error('transport broke'));
|
|
932
|
+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
933
|
+
core.authenticateMcpAccessToken.mockReturnValue({
|
|
934
|
+
tokenId: 'token-transport-1',
|
|
935
|
+
userId: 'user-1',
|
|
936
|
+
workspaceId: 'workspace-1',
|
|
937
|
+
scopes: ['tasks:read', 'workspace:read'],
|
|
938
|
+
status: 'active'
|
|
939
|
+
});
|
|
940
|
+
const match = matchRoute('POST', '/mcp', routes);
|
|
941
|
+
const req = createMockReq('POST', '/mcp', {
|
|
942
|
+
jsonrpc: '2.0',
|
|
943
|
+
id: 2,
|
|
944
|
+
method: 'tools/call',
|
|
945
|
+
params: {
|
|
946
|
+
name: 'get_workstream',
|
|
947
|
+
arguments: { id: 'WS-1' }
|
|
948
|
+
}
|
|
949
|
+
}, {
|
|
950
|
+
'x-taskforce-runtime-mode': 'cloud',
|
|
951
|
+
authorization: 'Bearer tfmcp_transport_token',
|
|
952
|
+
accept: 'application/json, text/event-stream',
|
|
953
|
+
'content-type': 'application/json',
|
|
954
|
+
'x-request-id': 'req-mcp-transport'
|
|
955
|
+
});
|
|
956
|
+
const res = createMockRes();
|
|
957
|
+
expect(match).not.toBeNull();
|
|
958
|
+
await match.handler(req, res, match.params);
|
|
959
|
+
expect(res.statusCode).toBe(500);
|
|
960
|
+
expect(JSON.parse(res.__getBody())).toEqual(expect.objectContaining({
|
|
961
|
+
jsonrpc: '2.0',
|
|
962
|
+
error: expect.objectContaining({
|
|
963
|
+
message: 'MCP transport failure.',
|
|
964
|
+
data: expect.objectContaining({
|
|
965
|
+
kind: 'tool_transport_failure',
|
|
966
|
+
retryable: true,
|
|
967
|
+
requestId: 'req-mcp-transport'
|
|
968
|
+
})
|
|
969
|
+
})
|
|
970
|
+
}));
|
|
971
|
+
expect(serverClose).toHaveBeenCalledTimes(1);
|
|
972
|
+
consoleErrorSpy.mockRestore();
|
|
910
973
|
handleRequestSpy.mockRestore();
|
|
911
974
|
createServerSpy.mockRestore();
|
|
912
975
|
});
|
|
@@ -1090,7 +1153,10 @@ describe('Server Routes', () => {
|
|
|
1090
1153
|
clientId: 'client-1',
|
|
1091
1154
|
provider: 'chatgpt',
|
|
1092
1155
|
scopes: ['tasks:read'],
|
|
1093
|
-
status: 'active'
|
|
1156
|
+
status: 'active',
|
|
1157
|
+
aiProfileId: 'ai-profile-1',
|
|
1158
|
+
aiProfileToken: 'tfp_connector_codex',
|
|
1159
|
+
aiProfileName: 'Codex Connector'
|
|
1094
1160
|
});
|
|
1095
1161
|
const match = matchRoute('POST', '/mcp', routes);
|
|
1096
1162
|
const req = createMockReq('POST', '/mcp', {
|
|
@@ -1118,7 +1184,9 @@ describe('Server Routes', () => {
|
|
|
1118
1184
|
scopes: ['tasks:read'],
|
|
1119
1185
|
actorType: 'oauth',
|
|
1120
1186
|
tokenId: 'grant-1',
|
|
1121
|
-
userId: 'user-oauth'
|
|
1187
|
+
userId: 'user-oauth',
|
|
1188
|
+
aiProfileToken: 'tfp_connector_codex',
|
|
1189
|
+
aiProfileName: 'Codex Connector'
|
|
1122
1190
|
})
|
|
1123
1191
|
}));
|
|
1124
1192
|
handleRequestSpy.mockRestore();
|
|
@@ -1572,6 +1640,43 @@ describe('Server Routes', () => {
|
|
|
1572
1640
|
items: expect.any(Array)
|
|
1573
1641
|
}));
|
|
1574
1642
|
});
|
|
1643
|
+
it('DELETE /api/taskforce/settings/mcp/tokens/:id should delete an inactive token for the signed-in user', async () => {
|
|
1644
|
+
const authConfig = {
|
|
1645
|
+
runtimeMode: 'cloud',
|
|
1646
|
+
enabled: true,
|
|
1647
|
+
requiredForApi: true,
|
|
1648
|
+
excludedPaths: ['/api/taskforce/health', '/api/taskforce/auth'],
|
|
1649
|
+
defaultRole: 'member',
|
|
1650
|
+
defaultWorkspaceId: 'workspace-1',
|
|
1651
|
+
sessionCookieName: 'taskforce_session',
|
|
1652
|
+
sessionSecret: 'test-secret',
|
|
1653
|
+
sessionTtlSeconds: 3600
|
|
1654
|
+
};
|
|
1655
|
+
routes = createRoutes(core, { authConfig });
|
|
1656
|
+
core.deleteMcpAccessToken.mockReturnValue(true);
|
|
1657
|
+
const cookie = createSessionTokenCookie('session-mcp-token-delete', authConfig, {
|
|
1658
|
+
role: 'member',
|
|
1659
|
+
workspaceId: 'workspace-1',
|
|
1660
|
+
userId: 'user-1'
|
|
1661
|
+
});
|
|
1662
|
+
const match = matchRoute('DELETE', '/api/taskforce/settings/mcp/tokens/token-1', routes);
|
|
1663
|
+
const req = createMockReq('DELETE', '/api/taskforce/settings/mcp/tokens/token-1', undefined, {
|
|
1664
|
+
cookie,
|
|
1665
|
+
'x-taskforce-runtime-mode': 'cloud'
|
|
1666
|
+
});
|
|
1667
|
+
const res = createMockRes();
|
|
1668
|
+
expect(match).not.toBeNull();
|
|
1669
|
+
await match.handler(req, res, { id: 'token-1' });
|
|
1670
|
+
expect(core.deleteMcpAccessToken).toHaveBeenCalledWith({
|
|
1671
|
+
actorUserId: 'user-1',
|
|
1672
|
+
workspaceId: 'workspace-1',
|
|
1673
|
+
tokenId: 'token-1'
|
|
1674
|
+
});
|
|
1675
|
+
expect(JSON.parse(res.__getBody())).toEqual({
|
|
1676
|
+
success: true,
|
|
1677
|
+
deleted: true
|
|
1678
|
+
});
|
|
1679
|
+
});
|
|
1575
1680
|
it('POST /api/taskforce/global-settings should update global settings', async () => {
|
|
1576
1681
|
const payload = { theme: 'light', shortcut: 'Ctrl+Shift+L' };
|
|
1577
1682
|
const match = matchRoute('POST', '/api/taskforce/global-settings', routes);
|
|
@@ -1639,6 +1744,49 @@ describe('Server Routes', () => {
|
|
|
1639
1744
|
});
|
|
1640
1745
|
expect(getAfterRes.end).toHaveBeenCalledWith(expect.stringContaining('"sourceOfTruth":"cloud"'));
|
|
1641
1746
|
});
|
|
1747
|
+
it('POST /api/taskforce/ui-state should accept large workspace-sync baselines', async () => {
|
|
1748
|
+
const match = matchRoute('POST', '/api/taskforce/ui-state', routes);
|
|
1749
|
+
const largeWatermark = 'x'.repeat(1024 * 1024 + 1);
|
|
1750
|
+
const req = createMockReq('POST', '/api/taskforce/ui-state', {
|
|
1751
|
+
stateKey: 'workspace-sync',
|
|
1752
|
+
workspaceId: 'workspace-large-baseline',
|
|
1753
|
+
patch: {
|
|
1754
|
+
version: 2,
|
|
1755
|
+
enabled: true,
|
|
1756
|
+
phase: 'active',
|
|
1757
|
+
pushBaseline: {
|
|
1758
|
+
taskIds: ['task-1'],
|
|
1759
|
+
taskWatermarks: [['task-1', largeWatermark]],
|
|
1760
|
+
taskChangeKeys: [],
|
|
1761
|
+
initiativeWatermarks: [],
|
|
1762
|
+
workstreamWatermarks: [],
|
|
1763
|
+
aiProfileWatermarks: [],
|
|
1764
|
+
aiProfileChangeKeys: [],
|
|
1765
|
+
documentWatermarks: [],
|
|
1766
|
+
assetWatermarks: [],
|
|
1767
|
+
documentReviewSessionWatermarks: [],
|
|
1768
|
+
documentReviewCommentWatermarks: [],
|
|
1769
|
+
annotatedAttachmentSessionWatermarks: [],
|
|
1770
|
+
taskEventWatermarks: [],
|
|
1771
|
+
taxonomyStateFingerprint: ''
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
}, {
|
|
1775
|
+
'x-taskforce-runtime-mode': 'local',
|
|
1776
|
+
'x-taskforce-workspace-id': 'workspace-current'
|
|
1777
|
+
});
|
|
1778
|
+
const res = createMockRes();
|
|
1779
|
+
await match?.handler(req, res, {});
|
|
1780
|
+
expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
1781
|
+
expect(core.updateUiState).toHaveBeenCalledWith('workspace-large-baseline', 'anonymous', {
|
|
1782
|
+
stateKey: 'workspace-sync',
|
|
1783
|
+
patch: expect.objectContaining({
|
|
1784
|
+
pushBaseline: expect.objectContaining({
|
|
1785
|
+
taskWatermarks: [['task-1', largeWatermark]]
|
|
1786
|
+
})
|
|
1787
|
+
})
|
|
1788
|
+
});
|
|
1789
|
+
});
|
|
1642
1790
|
it('GET /api/taskforce/ui-state should ignore query workspace overrides in cloud mode', async () => {
|
|
1643
1791
|
core.getUiState.mockReturnValue({ activeWorkspaceModule: 'tasks' });
|
|
1644
1792
|
const match = matchRoute('GET', '/api/taskforce/ui-state', routes);
|
|
@@ -2242,6 +2390,50 @@ describe('Server Routes', () => {
|
|
|
2242
2390
|
expect(String(fetchMock.mock.calls[0][0])).toContain('https://example.r2.cloudflarestorage.com/taskforce-docs/taskforce/users/user-1/profile/avatar/avatar-draft-1-headshot.webp');
|
|
2243
2391
|
vi.unstubAllGlobals();
|
|
2244
2392
|
});
|
|
2393
|
+
it('GET /api/taskforce/context/:filename should serve synced local AI avatars when R2 object is missing', async () => {
|
|
2394
|
+
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'taskforce-ai-avatar-r2-fallback-'));
|
|
2395
|
+
const projectRoot = fs.realpathSync(tmpRoot);
|
|
2396
|
+
const basePath = path.join(projectRoot, '.taskforce');
|
|
2397
|
+
const relativePath = 'workspaces/workspace-1/assets/images/ai-profiles/ai-profile-codex/avatar/asset-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-cropped.webp';
|
|
2398
|
+
const absolutePath = path.join(basePath, relativePath);
|
|
2399
|
+
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
2400
|
+
fs.writeFileSync(absolutePath, Buffer.from('synced-ai-avatar'));
|
|
2401
|
+
core.getPaths.mockReturnValue({ projectRoot, basePath });
|
|
2402
|
+
const fetchMock = vi.fn().mockResolvedValue(new Response('', { status: 404 }));
|
|
2403
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
2404
|
+
try {
|
|
2405
|
+
const routesWithStorage = createRoutes(core, {
|
|
2406
|
+
objectStorage: {
|
|
2407
|
+
provider: 'r2',
|
|
2408
|
+
r2: {
|
|
2409
|
+
accountId: 'acct-1',
|
|
2410
|
+
bucket: 'taskforce-docs',
|
|
2411
|
+
endpoint: 'https://example.r2.cloudflarestorage.com',
|
|
2412
|
+
accessKeyId: 'access-key-1',
|
|
2413
|
+
secretAccessKey: 'secret-key-1',
|
|
2414
|
+
keyPrefix: 'taskforce',
|
|
2415
|
+
signedUploadTtlSeconds: 600,
|
|
2416
|
+
signedDownloadTtlSeconds: 300
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
});
|
|
2420
|
+
const encoded = encodeURIComponent(relativePath);
|
|
2421
|
+
const req = createMockReq('GET', `/api/taskforce/context/${encoded}`);
|
|
2422
|
+
const res = createMockRes();
|
|
2423
|
+
await matchRoute('GET', `/api/taskforce/context/${encoded}`, routesWithStorage)?.handler(req, res, { filename: encoded });
|
|
2424
|
+
expect(fetchMock).toHaveBeenCalled();
|
|
2425
|
+
expect(res.writeHead).toHaveBeenCalledWith(200, expect.objectContaining({
|
|
2426
|
+
'Content-Type': 'image/webp',
|
|
2427
|
+
'Content-Disposition': 'inline'
|
|
2428
|
+
}));
|
|
2429
|
+
expect(String(res.getHeader('location') || '')).toBe('');
|
|
2430
|
+
expect(res.__getBody()).toBe('synced-ai-avatar');
|
|
2431
|
+
}
|
|
2432
|
+
finally {
|
|
2433
|
+
vi.unstubAllGlobals();
|
|
2434
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
2435
|
+
}
|
|
2436
|
+
});
|
|
2245
2437
|
it('GET /api/taskforce/context/:filename should proxy missing cloud-owned user avatars in local runtime', async () => {
|
|
2246
2438
|
const previousCloudProxyBaseUrl = process.env.TASKFORCE_CLOUD_PROXY_BASE_URL;
|
|
2247
2439
|
const previousClientId = process.env.TASKFORCE_CF_ACCESS_CLIENT_ID;
|
|
@@ -3215,6 +3407,53 @@ describe('Server Routes', () => {
|
|
|
3215
3407
|
vi.unstubAllGlobals();
|
|
3216
3408
|
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
3217
3409
|
});
|
|
3410
|
+
it('POST /api/taskforce/context-upload/finalize should record direct AI avatar uploads instead of fast-finalizing', async () => {
|
|
3411
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
3412
|
+
ok: true,
|
|
3413
|
+
status: 200,
|
|
3414
|
+
statusText: 'OK',
|
|
3415
|
+
headers: { get: () => 'image/webp' },
|
|
3416
|
+
arrayBuffer: async () => new TextEncoder().encode('avatar-bytes').buffer
|
|
3417
|
+
});
|
|
3418
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
3419
|
+
const routesWithStorage = createRoutes(core, {
|
|
3420
|
+
objectStorage: {
|
|
3421
|
+
provider: 'r2',
|
|
3422
|
+
r2: {
|
|
3423
|
+
accountId: 'acct-1',
|
|
3424
|
+
bucket: 'taskforce-docs',
|
|
3425
|
+
endpoint: 'https://example.r2.cloudflarestorage.com',
|
|
3426
|
+
accessKeyId: 'access-key-1',
|
|
3427
|
+
secretAccessKey: 'secret-key-1',
|
|
3428
|
+
keyPrefix: 'taskforce',
|
|
3429
|
+
signedUploadTtlSeconds: 600,
|
|
3430
|
+
signedDownloadTtlSeconds: 300
|
|
3431
|
+
}
|
|
3432
|
+
}
|
|
3433
|
+
});
|
|
3434
|
+
const relativePath = 'workspaces/workspace-1/assets/images/ai-profiles/ai-profile-codex/avatar/asset-avatar.webp';
|
|
3435
|
+
const match = matchRoute('POST', '/api/taskforce/context-upload/finalize', routesWithStorage);
|
|
3436
|
+
const req = createMockReq('POST', '/api/taskforce/context-upload/finalize', {
|
|
3437
|
+
relativePath,
|
|
3438
|
+
mimeType: 'image/webp',
|
|
3439
|
+
originalName: 'avatar.webp',
|
|
3440
|
+
fastFinalize: true
|
|
3441
|
+
}, {
|
|
3442
|
+
'x-taskforce-workspace-id': 'workspace-1'
|
|
3443
|
+
});
|
|
3444
|
+
const res = createMockRes();
|
|
3445
|
+
await match?.handler(req, res, {});
|
|
3446
|
+
expect(fetchMock).toHaveBeenCalled();
|
|
3447
|
+
expect(res.writeHead).toHaveBeenCalledWith(200, { 'Content-Type': 'application/json' });
|
|
3448
|
+
const payload = JSON.parse(res.end.mock.calls[0][0]);
|
|
3449
|
+
expect(payload).toEqual(expect.objectContaining({
|
|
3450
|
+
success: true,
|
|
3451
|
+
fsPath: relativePath,
|
|
3452
|
+
path: `/api/taskforce/context/${encodeURIComponent(relativePath)}`
|
|
3453
|
+
}));
|
|
3454
|
+
expect(payload.mode).not.toBe('direct-fast');
|
|
3455
|
+
vi.unstubAllGlobals();
|
|
3456
|
+
});
|
|
3218
3457
|
it('POST /api/taskforce/context-upload/finalize should mirror upload metadata into workspace_assets', async () => {
|
|
3219
3458
|
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'taskforce-context-finalize-canonical-'));
|
|
3220
3459
|
core.getPaths.mockReturnValue({
|
|
@@ -3248,8 +3487,21 @@ describe('Server Routes', () => {
|
|
|
3248
3487
|
}
|
|
3249
3488
|
});
|
|
3250
3489
|
const match = matchRoute('POST', '/api/taskforce/context-upload/finalize', routesWithStorage);
|
|
3490
|
+
const attachmentPath = '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Ffiles%2Fasset-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-spec.pdf';
|
|
3491
|
+
const attachmentFsPath = 'workspaces/workspace-1/assets/files/asset-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-spec.pdf';
|
|
3492
|
+
core.getTask
|
|
3493
|
+
.mockImplementationOnce(() => ({ id: 'task-1', attachments: [] }))
|
|
3494
|
+
.mockImplementationOnce(() => ({
|
|
3495
|
+
id: 'task-1',
|
|
3496
|
+
attachments: [{
|
|
3497
|
+
path: attachmentPath,
|
|
3498
|
+
fsPath: attachmentFsPath,
|
|
3499
|
+
caption: 'spec.pdf',
|
|
3500
|
+
assetId: 'asset-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
|
3501
|
+
}]
|
|
3502
|
+
}));
|
|
3251
3503
|
const req = createMockReq('POST', '/api/taskforce/context-upload/finalize', {
|
|
3252
|
-
relativePath:
|
|
3504
|
+
relativePath: attachmentFsPath,
|
|
3253
3505
|
mimeType: 'application/pdf',
|
|
3254
3506
|
taskId: 'task-1',
|
|
3255
3507
|
originalName: 'spec.pdf'
|
|
@@ -3261,8 +3513,8 @@ describe('Server Routes', () => {
|
|
|
3261
3513
|
const payload = JSON.parse(res.end.mock.calls[0][0]);
|
|
3262
3514
|
expect(payload).toEqual(expect.objectContaining({
|
|
3263
3515
|
success: true,
|
|
3264
|
-
path:
|
|
3265
|
-
fsPath:
|
|
3516
|
+
path: attachmentPath,
|
|
3517
|
+
fsPath: attachmentFsPath,
|
|
3266
3518
|
taskId: 'task-1'
|
|
3267
3519
|
}));
|
|
3268
3520
|
const assets = store.listByWorkspace('workspace-1', { includeDeleted: true });
|
|
@@ -3276,6 +3528,25 @@ describe('Server Routes', () => {
|
|
|
3276
3528
|
expect(store.listLinksForTask('task-1', 'workspace-1')).toEqual([
|
|
3277
3529
|
expect.objectContaining({ role: 'attachment' })
|
|
3278
3530
|
]);
|
|
3531
|
+
expect(core.recordTaskEvent).toHaveBeenCalledWith(expect.objectContaining({
|
|
3532
|
+
taskId: 'task-1',
|
|
3533
|
+
workspaceId: 'workspace-1',
|
|
3534
|
+
action: 'task-attachments-changed',
|
|
3535
|
+
actor: 'user',
|
|
3536
|
+
createdAt: expect.any(String),
|
|
3537
|
+
details: {
|
|
3538
|
+
changes: {
|
|
3539
|
+
attachments: {
|
|
3540
|
+
from: [],
|
|
3541
|
+
to: [expect.objectContaining({
|
|
3542
|
+
path: attachmentPath,
|
|
3543
|
+
fsPath: attachmentFsPath,
|
|
3544
|
+
assetId: 'asset-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
|
3545
|
+
})],
|
|
3546
|
+
},
|
|
3547
|
+
},
|
|
3548
|
+
},
|
|
3549
|
+
}));
|
|
3279
3550
|
vi.unstubAllGlobals();
|
|
3280
3551
|
fs.rmSync(dbRoot, { recursive: true, force: true });
|
|
3281
3552
|
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
@@ -3782,6 +4053,8 @@ describe('Server Routes', () => {
|
|
|
3782
4053
|
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
3783
4054
|
});
|
|
3784
4055
|
it('POST /api/taskforce/documents/:assetId/attach should attach a canonical document to a task target', async () => {
|
|
4056
|
+
vi.useFakeTimers();
|
|
4057
|
+
vi.setSystemTime(new Date('2026-03-18T14:45:00.000Z'));
|
|
3785
4058
|
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'taskforce-documents-attach-task-'));
|
|
3786
4059
|
core.getPaths.mockReturnValue({
|
|
3787
4060
|
projectRoot: tmpRoot,
|
|
@@ -3817,7 +4090,12 @@ describe('Server Routes', () => {
|
|
|
3817
4090
|
'x-taskforce-workspace-id': 'default'
|
|
3818
4091
|
});
|
|
3819
4092
|
const res = createMockRes();
|
|
3820
|
-
|
|
4093
|
+
try {
|
|
4094
|
+
await match?.handler(req, res, { assetId: 'asset-doc-attach-route-1' });
|
|
4095
|
+
}
|
|
4096
|
+
finally {
|
|
4097
|
+
vi.useRealTimers();
|
|
4098
|
+
}
|
|
3821
4099
|
expect(res.writeHead).toHaveBeenCalledWith(200, { 'Content-Type': 'application/json' });
|
|
3822
4100
|
expect(core.updateTask).toHaveBeenCalledWith('task-attach-1', expect.objectContaining({
|
|
3823
4101
|
attachments: [
|
|
@@ -3825,6 +4103,7 @@ describe('Server Routes', () => {
|
|
|
3825
4103
|
assetId: 'asset-doc-attach-route-1',
|
|
3826
4104
|
taskId: 'task-attach-1',
|
|
3827
4105
|
path: '/api/taskforce/documents/asset-doc-attach-route-1/content',
|
|
4106
|
+
timestamp: '2026-03-18T14:45:00.000Z',
|
|
3828
4107
|
})
|
|
3829
4108
|
]
|
|
3830
4109
|
}), 'default', expect.objectContaining({
|
|
@@ -6443,6 +6722,58 @@ describe('Server Routes', () => {
|
|
|
6443
6722
|
]));
|
|
6444
6723
|
fs.rmSync(dbRoot, { recursive: true, force: true });
|
|
6445
6724
|
});
|
|
6725
|
+
it('GET /api/taskforce/sync/workspace/assets should include AI profile avatar files referenced without canonical metadata', async () => {
|
|
6726
|
+
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'taskforce-sync-ai-avatar-assets-'));
|
|
6727
|
+
const projectRoot = fs.realpathSync(tmpRoot);
|
|
6728
|
+
const basePath = path.join(tmpRoot, '.taskforce');
|
|
6729
|
+
const avatarStorageKey = 'workspaces/workspace-local/assets/images/ai-profiles/ai-profile-codex/avatar/asset-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-cropped.webp';
|
|
6730
|
+
const avatarDir = path.join(basePath, 'workspaces/workspace-local/assets/images/ai-profiles/ai-profile-codex/avatar');
|
|
6731
|
+
fs.mkdirSync(avatarDir, { recursive: true });
|
|
6732
|
+
const fileBuffer = Buffer.from('ai-avatar-bytes');
|
|
6733
|
+
fs.writeFileSync(path.join(avatarDir, 'asset-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-cropped.webp'), fileBuffer);
|
|
6734
|
+
const { tmpRoot: dbRoot, db } = createWorkspaceAssetTestStore();
|
|
6735
|
+
core.getPaths.mockReturnValue({
|
|
6736
|
+
projectRoot,
|
|
6737
|
+
basePath,
|
|
6738
|
+
});
|
|
6739
|
+
core.resolvePreferredWorkspaceId.mockReturnValue('workspace-local');
|
|
6740
|
+
core.getDatabaseAdapter = vi.fn().mockReturnValue(db);
|
|
6741
|
+
core.getTenantId = vi.fn().mockReturnValue('default');
|
|
6742
|
+
core.listAiProfiles.mockReturnValue([
|
|
6743
|
+
{
|
|
6744
|
+
id: 'ai-profile-codex',
|
|
6745
|
+
workspaceId: 'workspace-local',
|
|
6746
|
+
name: 'Codex',
|
|
6747
|
+
username: 'codex',
|
|
6748
|
+
avatarUrl: `/api/taskforce/context/${encodeURIComponent(avatarStorageKey)}`,
|
|
6749
|
+
avatarSourceUrl: null,
|
|
6750
|
+
createdAt: '2026-03-17T09:00:00.000Z',
|
|
6751
|
+
updatedAt: '2026-03-17T10:00:00.000Z'
|
|
6752
|
+
}
|
|
6753
|
+
]);
|
|
6754
|
+
routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
|
|
6755
|
+
const match = matchRoute('GET', '/api/taskforce/sync/workspace/assets', routes);
|
|
6756
|
+
const req = createMockReq('GET', '/api/taskforce/sync/workspace/assets?workspaceId=workspace-local', undefined, {
|
|
6757
|
+
'x-taskforce-workspace-id': 'workspace-local'
|
|
6758
|
+
});
|
|
6759
|
+
const res = createMockRes();
|
|
6760
|
+
await match?.handler(req, res, {});
|
|
6761
|
+
const payloadRaw = res.end.mock.calls[0]?.[0];
|
|
6762
|
+
const payload = JSON.parse(String(payloadRaw || '{}'));
|
|
6763
|
+
expect(payload.knownPaths).toContain(avatarStorageKey);
|
|
6764
|
+
expect(payload.assets).toEqual(expect.arrayContaining([
|
|
6765
|
+
expect.objectContaining({
|
|
6766
|
+
path: avatarStorageKey,
|
|
6767
|
+
assetId: 'asset-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
|
6768
|
+
kind: 'image',
|
|
6769
|
+
mimeType: 'image/webp',
|
|
6770
|
+
originalFilename: 'asset-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-cropped.webp',
|
|
6771
|
+
contentBase64: fileBuffer.toString('base64')
|
|
6772
|
+
})
|
|
6773
|
+
]));
|
|
6774
|
+
fs.rmSync(dbRoot, { recursive: true, force: true });
|
|
6775
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
6776
|
+
});
|
|
6446
6777
|
it('GET /api/taskforce/sync/workspace/assets should ignore malformed external workspace asset rows', async () => {
|
|
6447
6778
|
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'taskforce-sync-assets-external-'));
|
|
6448
6779
|
const projectRoot = fs.realpathSync(tmpRoot);
|
|
@@ -7109,6 +7440,82 @@ describe('Server Routes', () => {
|
|
|
7109
7440
|
]));
|
|
7110
7441
|
fs.rmSync(dbRoot, { recursive: true, force: true });
|
|
7111
7442
|
});
|
|
7443
|
+
it('GET /api/taskforce/sync/workspace/pull should include referenced AI profile avatar assets without canonical metadata', async () => {
|
|
7444
|
+
const authConfig = {
|
|
7445
|
+
runtimeMode: 'cloud',
|
|
7446
|
+
enabled: true,
|
|
7447
|
+
requiredForApi: true,
|
|
7448
|
+
excludedPaths: ['/api/taskforce/health', '/api/taskforce/auth'],
|
|
7449
|
+
defaultRole: 'member',
|
|
7450
|
+
defaultWorkspaceId: 'default',
|
|
7451
|
+
sessionCookieName: 'taskforce_session',
|
|
7452
|
+
sessionSecret: 'test-secret',
|
|
7453
|
+
sessionTtlSeconds: 3600
|
|
7454
|
+
};
|
|
7455
|
+
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'taskforce-sync-pull-ai-avatar-'));
|
|
7456
|
+
const basePath = path.join(tmpRoot, '.taskforce');
|
|
7457
|
+
const avatarStorageKey = 'workspaces/workspace-1/assets/images/ai-profiles/ai-profile-modesty/avatar/asset-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-cropped.webp';
|
|
7458
|
+
const avatarPath = path.join(basePath, avatarStorageKey);
|
|
7459
|
+
const avatarBuffer = Buffer.from('ai-avatar-bytes');
|
|
7460
|
+
fs.mkdirSync(path.dirname(avatarPath), { recursive: true });
|
|
7461
|
+
fs.writeFileSync(avatarPath, avatarBuffer);
|
|
7462
|
+
const { tmpRoot: dbRoot, db } = createWorkspaceAssetTestStore();
|
|
7463
|
+
core.getPaths.mockReturnValue({
|
|
7464
|
+
projectRoot: tmpRoot,
|
|
7465
|
+
basePath,
|
|
7466
|
+
});
|
|
7467
|
+
core.getDatabaseAdapter = vi.fn().mockReturnValue(db);
|
|
7468
|
+
core.getTenantId = vi.fn().mockReturnValue('default');
|
|
7469
|
+
core.listAiProfiles.mockReturnValue([
|
|
7470
|
+
{
|
|
7471
|
+
id: 'ai-profile-modesty',
|
|
7472
|
+
workspaceId: 'workspace-1',
|
|
7473
|
+
name: 'Modesty',
|
|
7474
|
+
username: 'modesty',
|
|
7475
|
+
icon: 'Bot',
|
|
7476
|
+
color: '#0ea5e9',
|
|
7477
|
+
avatarUrl: `/api/taskforce/context/${encodeURIComponent(avatarStorageKey)}`,
|
|
7478
|
+
avatarSourceUrl: null,
|
|
7479
|
+
createdAt: '2026-03-17T09:00:00.000Z',
|
|
7480
|
+
updatedAt: '2026-03-17T10:00:00.000Z'
|
|
7481
|
+
}
|
|
7482
|
+
]);
|
|
7483
|
+
routes = createRoutes(core, { authConfig });
|
|
7484
|
+
core.listUserWorkspaces.mockReturnValueOnce([{ id: 'workspace-1', slug: 'workspace-1', name: 'Workspace 1', description: null, role: 'owner', status: 'active', betaAccess: true }]);
|
|
7485
|
+
const cookie = createSessionTokenCookie('session-sync-pull-ai-avatar', authConfig, {
|
|
7486
|
+
role: 'owner',
|
|
7487
|
+
workspaceId: 'workspace-1',
|
|
7488
|
+
userId: 'user-1'
|
|
7489
|
+
});
|
|
7490
|
+
core.listTasks.mockReturnValueOnce({ tasks: [] });
|
|
7491
|
+
core.listArchive.mockReturnValueOnce({ archived: [] });
|
|
7492
|
+
const match = matchRoute('GET', '/api/taskforce/sync/workspace/pull', routes);
|
|
7493
|
+
const req = createMockReq('GET', '/api/taskforce/sync/workspace/pull?workspaceId=workspace-1&limit=20', undefined, { cookie });
|
|
7494
|
+
const res = createMockRes();
|
|
7495
|
+
await match?.handler(req, res, {});
|
|
7496
|
+
const payload = JSON.parse(String(res.end.mock.calls[0]?.[0] || '{}'));
|
|
7497
|
+
expect(payload.changes).toEqual(expect.arrayContaining([
|
|
7498
|
+
expect.objectContaining({
|
|
7499
|
+
op: 'ai-profile-upsert',
|
|
7500
|
+
profile: expect.objectContaining({
|
|
7501
|
+
id: 'ai-profile-modesty',
|
|
7502
|
+
avatarUrl: `/api/taskforce/context/${encodeURIComponent(avatarStorageKey)}`
|
|
7503
|
+
})
|
|
7504
|
+
}),
|
|
7505
|
+
expect.objectContaining({
|
|
7506
|
+
op: 'asset-upsert',
|
|
7507
|
+
path: avatarStorageKey,
|
|
7508
|
+
assetId: 'asset-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
|
7509
|
+
kind: 'image',
|
|
7510
|
+
mimeType: 'image/webp',
|
|
7511
|
+
originalFilename: 'asset-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-cropped.webp',
|
|
7512
|
+
linkRole: 'image',
|
|
7513
|
+
contentBase64: avatarBuffer.toString('base64')
|
|
7514
|
+
})
|
|
7515
|
+
]));
|
|
7516
|
+
fs.rmSync(dbRoot, { recursive: true, force: true });
|
|
7517
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
7518
|
+
});
|
|
7112
7519
|
it('GET /api/taskforce/sync/workspace/pull should include mixed document and binary attachment upserts for the same task', async () => {
|
|
7113
7520
|
const authConfig = {
|
|
7114
7521
|
runtimeMode: 'cloud',
|
|
@@ -7570,7 +7977,7 @@ describe('Server Routes', () => {
|
|
|
7570
7977
|
expect(res.end).toHaveBeenCalledTimes(1);
|
|
7571
7978
|
expect(res.end).toHaveBeenCalledWith(expect.stringContaining('"success":true'));
|
|
7572
7979
|
});
|
|
7573
|
-
it('GET /api/taskforce/sync/workspace/pull should
|
|
7980
|
+
it('GET /api/taskforce/sync/workspace/pull should degrade optional snapshot failures into diagnostics instead of 500', async () => {
|
|
7574
7981
|
const authConfig = {
|
|
7575
7982
|
runtimeMode: 'cloud',
|
|
7576
7983
|
enabled: true,
|
|
@@ -7583,9 +7990,63 @@ describe('Server Routes', () => {
|
|
|
7583
7990
|
sessionTtlSeconds: 3600
|
|
7584
7991
|
};
|
|
7585
7992
|
routes = createRoutes(core, { authConfig });
|
|
7586
|
-
|
|
7993
|
+
core.listUserWorkspaces.mockReturnValueOnce([{ id: 'workspace-1', slug: 'workspace-1', name: 'Workspace 1', description: null, role: 'owner', status: 'active', betaAccess: true }]);
|
|
7994
|
+
core.listTasks.mockReturnValueOnce({
|
|
7995
|
+
tasks: [
|
|
7996
|
+
{
|
|
7997
|
+
id: 'task-survives',
|
|
7998
|
+
title: 'Survives partial feed failure',
|
|
7999
|
+
status: 'task',
|
|
8000
|
+
category: 'general',
|
|
8001
|
+
type: 'task',
|
|
8002
|
+
createdAt: '2026-04-10T10:00:00.000Z'
|
|
8003
|
+
}
|
|
8004
|
+
]
|
|
8005
|
+
});
|
|
8006
|
+
core.listDocumentReviewSessionsForSync.mockImplementationOnce(() => {
|
|
8007
|
+
throw new Error('document review session snapshot exploded');
|
|
8008
|
+
});
|
|
8009
|
+
const cookie = createSessionTokenCookie('session-sync-pull-partial', authConfig, {
|
|
7587
8010
|
role: 'owner',
|
|
7588
|
-
workspaceId: 'workspace-
|
|
8011
|
+
workspaceId: 'workspace-1',
|
|
8012
|
+
userId: 'user-1'
|
|
8013
|
+
});
|
|
8014
|
+
const match = matchRoute('GET', '/api/taskforce/sync/workspace/pull', routes);
|
|
8015
|
+
const req = createMockReq('GET', '/api/taskforce/sync/workspace/pull?workspaceId=workspace-1&limit=10', undefined, { cookie });
|
|
8016
|
+
const res = createMockRes();
|
|
8017
|
+
await match?.handler(req, res, {});
|
|
8018
|
+
expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
8019
|
+
const body = JSON.parse(String(res.end.mock.calls[0]?.[0] || '{}'));
|
|
8020
|
+
expect(body.success).toBe(true);
|
|
8021
|
+
expect(body.changes).toEqual(expect.arrayContaining([
|
|
8022
|
+
expect.objectContaining({
|
|
8023
|
+
op: 'upsert',
|
|
8024
|
+
task: expect.objectContaining({ id: 'task-survives' })
|
|
8025
|
+
})
|
|
8026
|
+
]));
|
|
8027
|
+
expect(body.diagnostics?.snapshotErrors).toEqual(expect.arrayContaining([
|
|
8028
|
+
expect.objectContaining({
|
|
8029
|
+
domain: 'document-review-sessions',
|
|
8030
|
+
error: 'document review session snapshot exploded'
|
|
8031
|
+
})
|
|
8032
|
+
]));
|
|
8033
|
+
});
|
|
8034
|
+
it('GET /api/taskforce/sync/workspace/pull should reject unauthorized workspace override', async () => {
|
|
8035
|
+
const authConfig = {
|
|
8036
|
+
runtimeMode: 'cloud',
|
|
8037
|
+
enabled: true,
|
|
8038
|
+
requiredForApi: true,
|
|
8039
|
+
excludedPaths: ['/api/taskforce/health', '/api/taskforce/auth'],
|
|
8040
|
+
defaultRole: 'member',
|
|
8041
|
+
defaultWorkspaceId: 'default',
|
|
8042
|
+
sessionCookieName: 'taskforce_session',
|
|
8043
|
+
sessionSecret: 'test-secret',
|
|
8044
|
+
sessionTtlSeconds: 3600
|
|
8045
|
+
};
|
|
8046
|
+
routes = createRoutes(core, { authConfig });
|
|
8047
|
+
const cookie = createSessionTokenCookie('session-sync-pull-forbidden', authConfig, {
|
|
8048
|
+
role: 'owner',
|
|
8049
|
+
workspaceId: 'workspace-cloud-selected',
|
|
7589
8050
|
userId: 'user-1'
|
|
7590
8051
|
});
|
|
7591
8052
|
core.listUserWorkspaces.mockReturnValueOnce([
|
|
@@ -10362,6 +10823,8 @@ describe('Server Routes', () => {
|
|
|
10362
10823
|
await match?.handler(req, res, {});
|
|
10363
10824
|
const payload = JSON.parse(res.end.mock.calls[0][0]);
|
|
10364
10825
|
expect(payload.authenticated).toBe(true);
|
|
10826
|
+
expect(payload.authRequiredForApi).toBe(true);
|
|
10827
|
+
expect(payload.runtimeMode).toBe('cloud');
|
|
10365
10828
|
expect(payload.betaAccess).toBe(false);
|
|
10366
10829
|
expect(res.setHeader).toHaveBeenCalledWith('Server-Timing', expect.stringContaining('auth-session'));
|
|
10367
10830
|
expect(core.getUserBetaAccess).toHaveBeenCalledWith('u-no-workspace');
|
|
@@ -11343,7 +11806,8 @@ describe('Server Routes', () => {
|
|
|
11343
11806
|
label: null,
|
|
11344
11807
|
description: null,
|
|
11345
11808
|
publicLabel: null,
|
|
11346
|
-
publicDescription: null
|
|
11809
|
+
publicDescription: null,
|
|
11810
|
+
publicDescriptionVisible: false
|
|
11347
11811
|
}
|
|
11348
11812
|
]);
|
|
11349
11813
|
}
|
|
@@ -11497,6 +11961,7 @@ describe('Server Routes', () => {
|
|
|
11497
11961
|
description: 'Allow registered AI collaborator profiles with an optional per-workspace limit.',
|
|
11498
11962
|
publicLabel: 'AI teammates',
|
|
11499
11963
|
publicDescription: 'Add reusable AI teammates to your workspace.',
|
|
11964
|
+
publicDescriptionVisible: false,
|
|
11500
11965
|
publicDisplayOrder: 20,
|
|
11501
11966
|
allowedAccessModes: ['enabled', 'limited', 'disabled'],
|
|
11502
11967
|
configTemplate: { maxAiProfiles: 1 }
|
|
@@ -11505,6 +11970,7 @@ describe('Server Routes', () => {
|
|
|
11505
11970
|
const req = createMockReq('PATCH', '/api/platform/features/collaboration.ai_profiles', {
|
|
11506
11971
|
publicLabel: 'AI teammates',
|
|
11507
11972
|
publicDescription: 'Add reusable AI teammates to your workspace.',
|
|
11973
|
+
publicDescriptionVisible: false,
|
|
11508
11974
|
publicDisplayOrder: 20
|
|
11509
11975
|
}, {
|
|
11510
11976
|
'x-taskforce-runtime-mode': 'cloud',
|
|
@@ -11514,13 +11980,65 @@ describe('Server Routes', () => {
|
|
|
11514
11980
|
await match?.handler(req, res, { featureKey: 'collaboration.ai_profiles' });
|
|
11515
11981
|
expect(core.updatePlanFeatureCatalogMarketingCopy).toHaveBeenCalledWith({
|
|
11516
11982
|
featureKey: 'collaboration.ai_profiles',
|
|
11983
|
+
label: undefined,
|
|
11984
|
+
description: undefined,
|
|
11517
11985
|
publicLabel: 'AI teammates',
|
|
11518
11986
|
publicDescription: 'Add reusable AI teammates to your workspace.',
|
|
11987
|
+
publicDescriptionVisible: false,
|
|
11519
11988
|
publicDisplayOrder: 20
|
|
11520
11989
|
});
|
|
11521
11990
|
expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
11522
11991
|
expect(res.end).toHaveBeenCalledWith(expect.stringContaining('"publicLabel":"AI teammates"'));
|
|
11523
11992
|
});
|
|
11993
|
+
it('POST /api/platform/features should create a custom feature catalog entry', async () => {
|
|
11994
|
+
core.createCustomPlanFeatureCatalogEntry = vi.fn().mockReturnValue({
|
|
11995
|
+
featureKey: 'marketing.unlimited_local_workspaces',
|
|
11996
|
+
label: 'Unlimited Local Workspaces',
|
|
11997
|
+
description: 'Run unlimited local workspaces on your device.',
|
|
11998
|
+
isCustom: true,
|
|
11999
|
+
publicLabel: null,
|
|
12000
|
+
publicDescription: null,
|
|
12001
|
+
publicDescriptionVisible: true,
|
|
12002
|
+
publicDisplayOrder: 12,
|
|
12003
|
+
allowedAccessModes: ['enabled', 'disabled'],
|
|
12004
|
+
configTemplate: {}
|
|
12005
|
+
});
|
|
12006
|
+
const match = matchRoute('POST', '/api/platform/features', routes);
|
|
12007
|
+
const req = createMockReq('POST', '/api/platform/features', {
|
|
12008
|
+
label: 'Unlimited Local Workspaces',
|
|
12009
|
+
description: 'Run unlimited local workspaces on your device.',
|
|
12010
|
+
publicDescriptionVisible: true,
|
|
12011
|
+
publicDisplayOrder: 12
|
|
12012
|
+
}, {
|
|
12013
|
+
'x-taskforce-runtime-mode': 'cloud',
|
|
12014
|
+
'x-taskforce-system-role': 'system_admin'
|
|
12015
|
+
});
|
|
12016
|
+
const res = createMockRes();
|
|
12017
|
+
await match?.handler(req, res, {});
|
|
12018
|
+
expect(core.createCustomPlanFeatureCatalogEntry).toHaveBeenCalledWith({
|
|
12019
|
+
label: 'Unlimited Local Workspaces',
|
|
12020
|
+
description: 'Run unlimited local workspaces on your device.',
|
|
12021
|
+
publicLabel: undefined,
|
|
12022
|
+
publicDescription: undefined,
|
|
12023
|
+
publicDescriptionVisible: true,
|
|
12024
|
+
publicDisplayOrder: 12
|
|
12025
|
+
});
|
|
12026
|
+
expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
12027
|
+
expect(res.end).toHaveBeenCalledWith(expect.stringContaining('"featureKey":"marketing.unlimited_local_workspaces"'));
|
|
12028
|
+
});
|
|
12029
|
+
it('DELETE /api/platform/features/:featureKey should remove a custom feature catalog entry', async () => {
|
|
12030
|
+
core.deleteCustomPlanFeatureCatalogEntry = vi.fn();
|
|
12031
|
+
const match = matchRoute('DELETE', '/api/platform/features/marketing.unlimited_local_workspaces', routes);
|
|
12032
|
+
const req = createMockReq('DELETE', '/api/platform/features/marketing.unlimited_local_workspaces', undefined, {
|
|
12033
|
+
'x-taskforce-runtime-mode': 'cloud',
|
|
12034
|
+
'x-taskforce-system-role': 'system_admin'
|
|
12035
|
+
});
|
|
12036
|
+
const res = createMockRes();
|
|
12037
|
+
await match?.handler(req, res, { featureKey: 'marketing.unlimited_local_workspaces' });
|
|
12038
|
+
expect(core.deleteCustomPlanFeatureCatalogEntry).toHaveBeenCalledWith('marketing.unlimited_local_workspaces');
|
|
12039
|
+
expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
12040
|
+
expect(res.end).toHaveBeenCalledWith(expect.stringContaining('"success":true'));
|
|
12041
|
+
});
|
|
11524
12042
|
it('PATCH /api/platform/plans/:planId/versions/:versionId/features should return normalized dependency state', async () => {
|
|
11525
12043
|
core.updatePlanVersionFeatures.mockReturnValueOnce({
|
|
11526
12044
|
planVersionId: 'collab-v1',
|
|
@@ -11953,6 +12471,8 @@ describe('Server Routes', () => {
|
|
|
11953
12471
|
expect(matchRoute('PATCH', '/api/taskforce/admin/users/u1/role', routes)).not.toBeNull();
|
|
11954
12472
|
expect(matchRoute('GET', '/api/taskforce/admin/workspace-audit-logs', routes)).not.toBeNull();
|
|
11955
12473
|
expect(matchRoute('POST', '/api/taskforce/workspace/ai-profiles/avatar', routes)).not.toBeNull();
|
|
12474
|
+
expect(matchRoute('POST', '/api/taskforce/workspace/ai-profiles/avatar/upload', routes)).not.toBeNull();
|
|
12475
|
+
expect(matchRoute('POST', '/api/taskforce/workspace/ai-profiles/metadata', routes)).toBeNull();
|
|
11956
12476
|
});
|
|
11957
12477
|
it('POST /api/taskforce/admin/users/invite should authorize against the target workspace', async () => {
|
|
11958
12478
|
const authConfig = createCloudAuthConfig();
|
|
@@ -12382,6 +12902,8 @@ describe('Server Routes', () => {
|
|
|
12382
12902
|
color: '#ef6c00',
|
|
12383
12903
|
avatarUrl: null,
|
|
12384
12904
|
avatarSourceUrl: null,
|
|
12905
|
+
avatarRevision: 0,
|
|
12906
|
+
avatarUpdatedAt: null,
|
|
12385
12907
|
kind: 'agent',
|
|
12386
12908
|
description: 'Builds product code',
|
|
12387
12909
|
role: 'Implementer',
|
|
@@ -12396,7 +12918,86 @@ describe('Server Routes', () => {
|
|
|
12396
12918
|
}
|
|
12397
12919
|
]);
|
|
12398
12920
|
});
|
|
12399
|
-
it('GET /api/taskforce/workspace/assignee-options should
|
|
12921
|
+
it('GET /api/taskforce/workspace/assignee-options should support agent-only roster loads', async () => {
|
|
12922
|
+
const authConfig = {
|
|
12923
|
+
runtimeMode: 'cloud',
|
|
12924
|
+
enabled: true,
|
|
12925
|
+
requiredForApi: true,
|
|
12926
|
+
excludedPaths: ['/api/taskforce/health', '/api/taskforce/auth'],
|
|
12927
|
+
defaultRole: 'member',
|
|
12928
|
+
defaultWorkspaceId: 'default',
|
|
12929
|
+
sessionCookieName: 'taskforce_session',
|
|
12930
|
+
sessionSecret: 'test-secret',
|
|
12931
|
+
sessionTtlSeconds: 3600
|
|
12932
|
+
};
|
|
12933
|
+
routes = createRoutes(core, { authConfig });
|
|
12934
|
+
core.listWorkspaceUsers.mockReturnValue([
|
|
12935
|
+
{
|
|
12936
|
+
userId: 'member-1',
|
|
12937
|
+
email: 'member1@example.com',
|
|
12938
|
+
displayName: 'Member One',
|
|
12939
|
+
avatarUrl: null,
|
|
12940
|
+
role: 'member',
|
|
12941
|
+
permissionMode: 'read-write',
|
|
12942
|
+
betaAccess: true,
|
|
12943
|
+
status: 'active',
|
|
12944
|
+
disabled: false
|
|
12945
|
+
}
|
|
12946
|
+
]);
|
|
12947
|
+
core.listAiProfiles.mockReturnValue([
|
|
12948
|
+
{
|
|
12949
|
+
id: 'ai-profile-1',
|
|
12950
|
+
workspaceId: 'workspace-1',
|
|
12951
|
+
profileToken: 'tfp_codex_sync',
|
|
12952
|
+
name: 'Codex',
|
|
12953
|
+
username: 'codex',
|
|
12954
|
+
icon: 'Bot',
|
|
12955
|
+
color: '#ef6c00',
|
|
12956
|
+
description: 'Builds product code',
|
|
12957
|
+
role: 'Implementer',
|
|
12958
|
+
provider: 'OpenAI',
|
|
12959
|
+
model: 'gpt-5.4',
|
|
12960
|
+
surfaceType: 'coding_tool',
|
|
12961
|
+
seatScope: 'cloud_metered',
|
|
12962
|
+
archivedAt: null,
|
|
12963
|
+
createdAt: '2026-03-01T00:00:00.000Z',
|
|
12964
|
+
updatedAt: '2026-03-01T00:00:00.000Z',
|
|
12965
|
+
lastActiveAt: null
|
|
12966
|
+
}
|
|
12967
|
+
]);
|
|
12968
|
+
core.countAiProfileUsage.mockReturnValue({ used: 1, limit: 3, remaining: 2 });
|
|
12969
|
+
const match = matchRoute('GET', '/api/taskforce/workspace/assignee-options', routes);
|
|
12970
|
+
const cookie = createSessionTokenCookie('session-member-agent-roster', authConfig, {
|
|
12971
|
+
userId: 'member-user',
|
|
12972
|
+
role: 'member',
|
|
12973
|
+
workspaceId: 'workspace-1'
|
|
12974
|
+
});
|
|
12975
|
+
const req = createMockReq('GET', '/api/taskforce/workspace/assignee-options?kind=agent', undefined, {
|
|
12976
|
+
cookie,
|
|
12977
|
+
'x-taskforce-runtime-mode': 'cloud',
|
|
12978
|
+
'x-taskforce-role': 'member',
|
|
12979
|
+
'x-taskforce-user-id': 'member-user',
|
|
12980
|
+
'x-taskforce-workspace-id': 'workspace-1'
|
|
12981
|
+
});
|
|
12982
|
+
const res = createMockRes();
|
|
12983
|
+
await match?.handler(req, res, {});
|
|
12984
|
+
expect(core.listWorkspaceUsers).not.toHaveBeenCalled();
|
|
12985
|
+
expect(core.listAiProfiles).toHaveBeenCalledWith('workspace-1', expect.objectContaining({
|
|
12986
|
+
diagnostics: expect.any(Object)
|
|
12987
|
+
}));
|
|
12988
|
+
expect(core.countAiProfileUsage).toHaveBeenCalledWith('workspace-1');
|
|
12989
|
+
expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
12990
|
+
const payload = JSON.parse(res.end.mock.calls[0][0]);
|
|
12991
|
+
expect(payload.aiProfileSeatUsage).toEqual({ used: 1, limit: 3, remaining: 2 });
|
|
12992
|
+
expect(payload.assignees).toEqual([
|
|
12993
|
+
expect.objectContaining({
|
|
12994
|
+
value: 'ai-profile-1',
|
|
12995
|
+
label: 'Codex',
|
|
12996
|
+
kind: 'agent'
|
|
12997
|
+
})
|
|
12998
|
+
]);
|
|
12999
|
+
});
|
|
13000
|
+
it('GET /api/taskforce/workspace/assignee-options should not include ai profile tokens for workspace admins', async () => {
|
|
12400
13001
|
const authConfig = {
|
|
12401
13002
|
runtimeMode: 'cloud',
|
|
12402
13003
|
enabled: true,
|
|
@@ -12450,9 +13051,187 @@ describe('Server Routes', () => {
|
|
|
12450
13051
|
expect(payload.assignees).toEqual([
|
|
12451
13052
|
expect.objectContaining({
|
|
12452
13053
|
value: 'ai-profile-1',
|
|
12453
|
-
|
|
13054
|
+
label: 'Codex'
|
|
12454
13055
|
})
|
|
12455
13056
|
]);
|
|
13057
|
+
expect(payload.assignees[0]).not.toHaveProperty('profileToken');
|
|
13058
|
+
});
|
|
13059
|
+
it('POST /api/taskforce/workspace/ai-profiles/avatar should reject direct avatar URL writes', async () => {
|
|
13060
|
+
const authConfig = {
|
|
13061
|
+
runtimeMode: 'cloud',
|
|
13062
|
+
enabled: true,
|
|
13063
|
+
requiredForApi: true,
|
|
13064
|
+
excludedPaths: ['/api/taskforce/health', '/api/taskforce/auth'],
|
|
13065
|
+
defaultRole: 'member',
|
|
13066
|
+
defaultWorkspaceId: 'default',
|
|
13067
|
+
sessionCookieName: 'taskforce_session',
|
|
13068
|
+
sessionSecret: 'test-secret',
|
|
13069
|
+
sessionTtlSeconds: 3600
|
|
13070
|
+
};
|
|
13071
|
+
routes = createRoutes(core, { authConfig });
|
|
13072
|
+
const match = matchRoute('POST', '/api/taskforce/workspace/ai-profiles/avatar', routes);
|
|
13073
|
+
const cookie = createSessionTokenCookie('session-admin-avatar-ai-profile-reject', authConfig, {
|
|
13074
|
+
userId: 'admin-user',
|
|
13075
|
+
role: 'admin',
|
|
13076
|
+
workspaceId: 'workspace-1'
|
|
13077
|
+
});
|
|
13078
|
+
const req = createMockReq('POST', '/api/taskforce/workspace/ai-profiles/avatar', {
|
|
13079
|
+
profileId: 'ai-profile-1',
|
|
13080
|
+
avatarUrl: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fai-profiles%2Favatar.webp'
|
|
13081
|
+
}, {
|
|
13082
|
+
cookie,
|
|
13083
|
+
'x-taskforce-runtime-mode': 'cloud',
|
|
13084
|
+
'x-taskforce-role': 'admin',
|
|
13085
|
+
'x-taskforce-user-id': 'admin-user',
|
|
13086
|
+
'x-taskforce-workspace-id': 'workspace-1'
|
|
13087
|
+
});
|
|
13088
|
+
const res = createMockRes();
|
|
13089
|
+
await match?.handler(req, res, {});
|
|
13090
|
+
expect(core.updateAiProfileAvatar).not.toHaveBeenCalled();
|
|
13091
|
+
expect(res.writeHead).toHaveBeenCalledWith(400, expect.any(Object));
|
|
13092
|
+
const payload = JSON.parse(res.end.mock.calls[0][0]);
|
|
13093
|
+
expect(payload).toEqual({
|
|
13094
|
+
error: 'AI profile avatar updates must use the stable avatar upload endpoint.',
|
|
13095
|
+
code: 'AI_PROFILE_AVATAR_STABLE_UPLOAD_REQUIRED'
|
|
13096
|
+
});
|
|
13097
|
+
});
|
|
13098
|
+
it('POST /api/taskforce/workspace/ai-profiles/avatar should allow clear-only updates and redact profile tokens', async () => {
|
|
13099
|
+
const authConfig = {
|
|
13100
|
+
runtimeMode: 'cloud',
|
|
13101
|
+
enabled: true,
|
|
13102
|
+
requiredForApi: true,
|
|
13103
|
+
excludedPaths: ['/api/taskforce/health', '/api/taskforce/auth'],
|
|
13104
|
+
defaultRole: 'member',
|
|
13105
|
+
defaultWorkspaceId: 'default',
|
|
13106
|
+
sessionCookieName: 'taskforce_session',
|
|
13107
|
+
sessionSecret: 'test-secret',
|
|
13108
|
+
sessionTtlSeconds: 3600
|
|
13109
|
+
};
|
|
13110
|
+
routes = createRoutes(core, { authConfig });
|
|
13111
|
+
core.listAiProfiles.mockReturnValue([
|
|
13112
|
+
{
|
|
13113
|
+
id: 'ai-profile-1',
|
|
13114
|
+
workspaceId: 'workspace-1',
|
|
13115
|
+
avatarUrl: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fai-profiles%2Fai-profile-1%2Favatar%2Fdisplay.webp',
|
|
13116
|
+
avatarSourceUrl: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fai-profiles%2Fai-profile-1%2Favatar%2Fsource.webp'
|
|
13117
|
+
}
|
|
13118
|
+
]);
|
|
13119
|
+
core.updateAiProfileAvatar.mockReturnValue({
|
|
13120
|
+
id: 'ai-profile-1',
|
|
13121
|
+
workspaceId: 'workspace-1',
|
|
13122
|
+
profileToken: 'tfp_codex_sync',
|
|
13123
|
+
name: 'Codex',
|
|
13124
|
+
username: 'codex',
|
|
13125
|
+
icon: 'Bot',
|
|
13126
|
+
color: '#ef6c00',
|
|
13127
|
+
avatarUrl: null,
|
|
13128
|
+
avatarSourceUrl: null,
|
|
13129
|
+
description: 'Builds product code',
|
|
13130
|
+
role: 'Implementer',
|
|
13131
|
+
provider: 'OpenAI',
|
|
13132
|
+
model: 'gpt-5.4',
|
|
13133
|
+
surfaceType: 'coding_tool',
|
|
13134
|
+
archivedAt: null,
|
|
13135
|
+
createdAt: '2026-03-01T00:00:00.000Z',
|
|
13136
|
+
updatedAt: '2026-04-19T17:00:00.000Z',
|
|
13137
|
+
lastActiveAt: null
|
|
13138
|
+
});
|
|
13139
|
+
const match = matchRoute('POST', '/api/taskforce/workspace/ai-profiles/avatar', routes);
|
|
13140
|
+
const cookie = createSessionTokenCookie('session-admin-avatar-ai-profile-clear', authConfig, {
|
|
13141
|
+
userId: 'admin-user',
|
|
13142
|
+
role: 'admin',
|
|
13143
|
+
workspaceId: 'workspace-1'
|
|
13144
|
+
});
|
|
13145
|
+
const req = createMockReq('POST', '/api/taskforce/workspace/ai-profiles/avatar', {
|
|
13146
|
+
profileId: 'ai-profile-1',
|
|
13147
|
+
avatarUrl: null,
|
|
13148
|
+
avatarSourceUrl: null
|
|
13149
|
+
}, {
|
|
13150
|
+
cookie,
|
|
13151
|
+
'x-taskforce-runtime-mode': 'cloud',
|
|
13152
|
+
'x-taskforce-role': 'admin',
|
|
13153
|
+
'x-taskforce-user-id': 'admin-user',
|
|
13154
|
+
'x-taskforce-workspace-id': 'workspace-1'
|
|
13155
|
+
});
|
|
13156
|
+
const res = createMockRes();
|
|
13157
|
+
await match?.handler(req, res, {});
|
|
13158
|
+
expect(core.updateAiProfileAvatar).toHaveBeenCalledWith({
|
|
13159
|
+
workspaceId: 'workspace-1',
|
|
13160
|
+
profileId: 'ai-profile-1',
|
|
13161
|
+
avatarUrl: null,
|
|
13162
|
+
avatarSourceUrl: null
|
|
13163
|
+
});
|
|
13164
|
+
expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
13165
|
+
const payload = JSON.parse(res.end.mock.calls[0][0]);
|
|
13166
|
+
expect(payload.success).toBe(true);
|
|
13167
|
+
expect(payload.profile).toEqual(expect.objectContaining({
|
|
13168
|
+
id: 'ai-profile-1',
|
|
13169
|
+
avatarUrl: null,
|
|
13170
|
+
avatarSourceUrl: null
|
|
13171
|
+
}));
|
|
13172
|
+
expect(payload.profile).not.toHaveProperty('profileToken');
|
|
13173
|
+
});
|
|
13174
|
+
it('POST /api/taskforce/workspace/ai-profiles/merge should redact profile tokens from admin responses', async () => {
|
|
13175
|
+
const authConfig = {
|
|
13176
|
+
runtimeMode: 'cloud',
|
|
13177
|
+
enabled: true,
|
|
13178
|
+
requiredForApi: true,
|
|
13179
|
+
excludedPaths: ['/api/taskforce/health', '/api/taskforce/auth'],
|
|
13180
|
+
defaultRole: 'member',
|
|
13181
|
+
defaultWorkspaceId: 'default',
|
|
13182
|
+
sessionCookieName: 'taskforce_session',
|
|
13183
|
+
sessionSecret: 'test-secret',
|
|
13184
|
+
sessionTtlSeconds: 3600
|
|
13185
|
+
};
|
|
13186
|
+
routes = createRoutes(core, { authConfig });
|
|
13187
|
+
core.mergeAiProfiles.mockReturnValue({
|
|
13188
|
+
id: 'ai-profile-keep',
|
|
13189
|
+
workspaceId: 'workspace-1',
|
|
13190
|
+
profileToken: 'tfp_codex_sync',
|
|
13191
|
+
name: 'Codex',
|
|
13192
|
+
username: 'codex',
|
|
13193
|
+
icon: 'Bot',
|
|
13194
|
+
color: '#ef6c00',
|
|
13195
|
+
description: 'Builds product code',
|
|
13196
|
+
role: 'Implementer',
|
|
13197
|
+
provider: 'OpenAI',
|
|
13198
|
+
model: 'gpt-5.4',
|
|
13199
|
+
surfaceType: 'coding_tool',
|
|
13200
|
+
archivedAt: null,
|
|
13201
|
+
createdAt: '2026-03-01T00:00:00.000Z',
|
|
13202
|
+
updatedAt: '2026-04-19T17:00:00.000Z',
|
|
13203
|
+
lastActiveAt: null
|
|
13204
|
+
});
|
|
13205
|
+
const match = matchRoute('POST', '/api/taskforce/workspace/ai-profiles/merge', routes);
|
|
13206
|
+
const cookie = createSessionTokenCookie('session-admin-merge-ai-profile', authConfig, {
|
|
13207
|
+
userId: 'admin-user',
|
|
13208
|
+
role: 'admin',
|
|
13209
|
+
workspaceId: 'workspace-1'
|
|
13210
|
+
});
|
|
13211
|
+
const req = createMockReq('POST', '/api/taskforce/workspace/ai-profiles/merge', {
|
|
13212
|
+
keepId: 'ai-profile-keep',
|
|
13213
|
+
mergeId: 'ai-profile-merge'
|
|
13214
|
+
}, {
|
|
13215
|
+
cookie,
|
|
13216
|
+
'x-taskforce-runtime-mode': 'cloud',
|
|
13217
|
+
'x-taskforce-role': 'admin',
|
|
13218
|
+
'x-taskforce-user-id': 'admin-user',
|
|
13219
|
+
'x-taskforce-workspace-id': 'workspace-1'
|
|
13220
|
+
});
|
|
13221
|
+
const res = createMockRes();
|
|
13222
|
+
await match?.handler(req, res, {});
|
|
13223
|
+
expect(core.mergeAiProfiles).toHaveBeenCalledWith({
|
|
13224
|
+
workspaceId: 'workspace-1',
|
|
13225
|
+
keepId: 'ai-profile-keep',
|
|
13226
|
+
mergeId: 'ai-profile-merge'
|
|
13227
|
+
});
|
|
13228
|
+
expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
13229
|
+
const payload = JSON.parse(res.end.mock.calls[0][0]);
|
|
13230
|
+
expect(payload.profile).toEqual(expect.objectContaining({
|
|
13231
|
+
id: 'ai-profile-keep',
|
|
13232
|
+
name: 'Codex'
|
|
13233
|
+
}));
|
|
13234
|
+
expect(payload.profile).not.toHaveProperty('profileToken');
|
|
12456
13235
|
});
|
|
12457
13236
|
it('POST /api/taskforce/workspace/ai-profiles/archive should archive active profiles for workspace admins', async () => {
|
|
12458
13237
|
const authConfig = {
|
|
@@ -12518,6 +13297,7 @@ describe('Server Routes', () => {
|
|
|
12518
13297
|
id: 'ai-profile-1',
|
|
12519
13298
|
archivedReason: 'manual_archive'
|
|
12520
13299
|
}));
|
|
13300
|
+
expect(payload.profile).not.toHaveProperty('profileToken');
|
|
12521
13301
|
});
|
|
12522
13302
|
it('POST /api/taskforce/workspace/ai-profiles/archive should honor actual owner membership even when the request role is stale', async () => {
|
|
12523
13303
|
const authConfig = {
|
|
@@ -12837,7 +13617,12 @@ describe('Server Routes', () => {
|
|
|
12837
13617
|
core.getGlobalSettings.mockReturnValue({
|
|
12838
13618
|
site: {
|
|
12839
13619
|
showRunTaskforceLocally: false,
|
|
12840
|
-
pricingPageEnabled: false
|
|
13620
|
+
pricingPageEnabled: false,
|
|
13621
|
+
animatedBackgroundEnabled: true,
|
|
13622
|
+
siteTheme: 'light',
|
|
13623
|
+
pricingPageTitle: 'Choose Your Plan',
|
|
13624
|
+
pricingPageDescription: 'Compare plans and pick the right one.',
|
|
13625
|
+
pricingPageShowPublicDescriptions: false
|
|
12841
13626
|
}
|
|
12842
13627
|
});
|
|
12843
13628
|
const match = matchRoute('GET', '/api/taskforce/admin/site-settings', routes);
|
|
@@ -12847,14 +13632,29 @@ describe('Server Routes', () => {
|
|
|
12847
13632
|
const payload = JSON.parse(res.end.mock.calls[0][0]);
|
|
12848
13633
|
expect(payload.status.showRunTaskforceLocally).toBe(false);
|
|
12849
13634
|
expect(payload.status.pricingPageEnabled).toBe(false);
|
|
13635
|
+
expect(payload.status.animatedBackgroundEnabled).toBe(true);
|
|
13636
|
+
expect(payload.status.siteTheme).toBe('light');
|
|
13637
|
+
expect(payload.status.pricingPageTitle).toBe('Choose Your Plan');
|
|
13638
|
+
expect(payload.status.pricingPageDescription).toBe('Compare plans and pick the right one.');
|
|
13639
|
+
expect(payload.status.pricingPageShowPublicDescriptions).toBe(false);
|
|
12850
13640
|
expect(payload.settings.showRunTaskforceLocally).toBe(false);
|
|
12851
13641
|
expect(payload.settings.pricingPageEnabled).toBe(false);
|
|
13642
|
+
expect(payload.settings.animatedBackgroundEnabled).toBe(true);
|
|
13643
|
+
expect(payload.settings.siteTheme).toBe('light');
|
|
13644
|
+
expect(payload.settings.pricingPageTitle).toBe('Choose Your Plan');
|
|
13645
|
+
expect(payload.settings.pricingPageDescription).toBe('Compare plans and pick the right one.');
|
|
13646
|
+
expect(payload.settings.pricingPageShowPublicDescriptions).toBe(false);
|
|
12852
13647
|
});
|
|
12853
13648
|
it('GET /api/taskforce/admin/site_settings should expose effective website settings for legacy admin bundles', async () => {
|
|
12854
13649
|
core.getGlobalSettings.mockReturnValue({
|
|
12855
13650
|
site: {
|
|
12856
13651
|
showRunTaskforceLocally: false,
|
|
12857
|
-
pricingPageEnabled: false
|
|
13652
|
+
pricingPageEnabled: false,
|
|
13653
|
+
animatedBackgroundEnabled: true,
|
|
13654
|
+
siteTheme: 'light',
|
|
13655
|
+
pricingPageTitle: 'Choose Your Plan',
|
|
13656
|
+
pricingPageDescription: 'Compare plans and pick the right one.',
|
|
13657
|
+
pricingPageShowPublicDescriptions: false
|
|
12858
13658
|
}
|
|
12859
13659
|
});
|
|
12860
13660
|
const match = matchRoute('GET', '/api/taskforce/admin/site_settings', routes);
|
|
@@ -12864,36 +13664,78 @@ describe('Server Routes', () => {
|
|
|
12864
13664
|
const payload = JSON.parse(res.end.mock.calls[0][0]);
|
|
12865
13665
|
expect(payload.status.showRunTaskforceLocally).toBe(false);
|
|
12866
13666
|
expect(payload.status.pricingPageEnabled).toBe(false);
|
|
13667
|
+
expect(payload.status.animatedBackgroundEnabled).toBe(true);
|
|
13668
|
+
expect(payload.status.siteTheme).toBe('light');
|
|
13669
|
+
expect(payload.status.pricingPageTitle).toBe('Choose Your Plan');
|
|
13670
|
+
expect(payload.status.pricingPageDescription).toBe('Compare plans and pick the right one.');
|
|
13671
|
+
expect(payload.status.pricingPageShowPublicDescriptions).toBe(false);
|
|
12867
13672
|
expect(payload.settings.showRunTaskforceLocally).toBe(false);
|
|
12868
13673
|
expect(payload.settings.pricingPageEnabled).toBe(false);
|
|
13674
|
+
expect(payload.settings.animatedBackgroundEnabled).toBe(true);
|
|
13675
|
+
expect(payload.settings.siteTheme).toBe('light');
|
|
13676
|
+
expect(payload.settings.pricingPageTitle).toBe('Choose Your Plan');
|
|
13677
|
+
expect(payload.settings.pricingPageDescription).toBe('Compare plans and pick the right one.');
|
|
13678
|
+
expect(payload.settings.pricingPageShowPublicDescriptions).toBe(false);
|
|
12869
13679
|
});
|
|
12870
13680
|
it('POST /api/taskforce/admin/site-settings should persist website settings', async () => {
|
|
12871
13681
|
const match = matchRoute('POST', '/api/taskforce/admin/site-settings', routes);
|
|
13682
|
+
const pricingPageDescription = `<p>${'Compare plans and pick the right one. '.repeat(12)}</p>`;
|
|
12872
13683
|
const req = createMockReq('POST', '/api/taskforce/admin/site-settings', {
|
|
12873
13684
|
showRunTaskforceLocally: false,
|
|
12874
|
-
pricingPageEnabled: false
|
|
13685
|
+
pricingPageEnabled: false,
|
|
13686
|
+
animatedBackgroundEnabled: true,
|
|
13687
|
+
siteTheme: 'light',
|
|
13688
|
+
pricingPageTitle: 'Choose Your Plan',
|
|
13689
|
+
pricingPageDescription,
|
|
13690
|
+
pricingPageShowPublicDescriptions: false
|
|
12875
13691
|
}, { 'x-taskforce-role': 'admin' });
|
|
12876
13692
|
const res = createMockRes();
|
|
12877
13693
|
await match?.handler(req, res, {});
|
|
12878
13694
|
expect(core.updateGlobalSettings).toHaveBeenCalledWith({
|
|
12879
13695
|
site: {
|
|
12880
13696
|
showRunTaskforceLocally: false,
|
|
12881
|
-
pricingPageEnabled: false
|
|
13697
|
+
pricingPageEnabled: false,
|
|
13698
|
+
animatedBackgroundEnabled: true,
|
|
13699
|
+
siteTheme: 'light',
|
|
13700
|
+
pricingPageTitle: 'Choose Your Plan',
|
|
13701
|
+
pricingPageDescription,
|
|
13702
|
+
pricingPageShowPublicDescriptions: false
|
|
12882
13703
|
}
|
|
12883
13704
|
});
|
|
12884
13705
|
});
|
|
13706
|
+
it('POST /api/taskforce/admin/site-settings should reject pricing page descriptions over 2000 characters', async () => {
|
|
13707
|
+
const match = matchRoute('POST', '/api/taskforce/admin/site-settings', routes);
|
|
13708
|
+
const req = createMockReq('POST', '/api/taskforce/admin/site-settings', {
|
|
13709
|
+
pricingPageDescription: 'x'.repeat(2001)
|
|
13710
|
+
}, { 'x-taskforce-role': 'admin' });
|
|
13711
|
+
const res = createMockRes();
|
|
13712
|
+
await match?.handler(req, res, {});
|
|
13713
|
+
expect(res.writeHead).toHaveBeenCalledWith(400, { 'Content-Type': 'application/json' });
|
|
13714
|
+
expect(res.end.mock.calls[0][0]).toContain('PRICING_PAGE_DESCRIPTION_TOO_LONG');
|
|
13715
|
+
expect(core.updateGlobalSettings).not.toHaveBeenCalled();
|
|
13716
|
+
});
|
|
12885
13717
|
it('POST /api/taskforce/admin/site_settings should persist website settings for legacy admin bundles', async () => {
|
|
12886
13718
|
const match = matchRoute('POST', '/api/taskforce/admin/site_settings', routes);
|
|
12887
13719
|
const req = createMockReq('POST', '/api/taskforce/admin/site_settings', {
|
|
12888
13720
|
showRunTaskforceLocally: false,
|
|
12889
|
-
pricingPageEnabled: false
|
|
13721
|
+
pricingPageEnabled: false,
|
|
13722
|
+
animatedBackgroundEnabled: true,
|
|
13723
|
+
siteTheme: 'light',
|
|
13724
|
+
pricingPageTitle: 'Choose Your Plan',
|
|
13725
|
+
pricingPageDescription: 'Compare plans and pick the right one.',
|
|
13726
|
+
pricingPageShowPublicDescriptions: false
|
|
12890
13727
|
}, { 'x-taskforce-role': 'admin' });
|
|
12891
13728
|
const res = createMockRes();
|
|
12892
13729
|
await match?.handler(req, res, {});
|
|
12893
13730
|
expect(core.updateGlobalSettings).toHaveBeenCalledWith({
|
|
12894
13731
|
site: {
|
|
12895
13732
|
showRunTaskforceLocally: false,
|
|
12896
|
-
pricingPageEnabled: false
|
|
13733
|
+
pricingPageEnabled: false,
|
|
13734
|
+
animatedBackgroundEnabled: true,
|
|
13735
|
+
siteTheme: 'light',
|
|
13736
|
+
pricingPageTitle: 'Choose Your Plan',
|
|
13737
|
+
pricingPageDescription: 'Compare plans and pick the right one.',
|
|
13738
|
+
pricingPageShowPublicDescriptions: false
|
|
12897
13739
|
}
|
|
12898
13740
|
});
|
|
12899
13741
|
});
|
|
@@ -12901,7 +13743,12 @@ describe('Server Routes', () => {
|
|
|
12901
13743
|
core.getGlobalSettings.mockReturnValue({
|
|
12902
13744
|
site: {
|
|
12903
13745
|
showRunTaskforceLocally: false,
|
|
12904
|
-
pricingPageEnabled: false
|
|
13746
|
+
pricingPageEnabled: false,
|
|
13747
|
+
animatedBackgroundEnabled: true,
|
|
13748
|
+
siteTheme: 'light',
|
|
13749
|
+
pricingPageTitle: 'Choose Your Plan',
|
|
13750
|
+
pricingPageDescription: 'Compare plans and pick the right one.',
|
|
13751
|
+
pricingPageShowPublicDescriptions: false
|
|
12905
13752
|
}
|
|
12906
13753
|
});
|
|
12907
13754
|
const match = matchRoute('GET', '/api/taskforce/public/site-config', routes);
|
|
@@ -12916,7 +13763,12 @@ describe('Server Routes', () => {
|
|
|
12916
13763
|
success: true,
|
|
12917
13764
|
config: {
|
|
12918
13765
|
showRunTaskforceLocally: false,
|
|
12919
|
-
pricingPageEnabled: false
|
|
13766
|
+
pricingPageEnabled: false,
|
|
13767
|
+
animatedBackgroundEnabled: true,
|
|
13768
|
+
siteTheme: 'light',
|
|
13769
|
+
pricingPageTitle: 'Choose Your Plan',
|
|
13770
|
+
pricingPageDescription: 'Compare plans and pick the right one.',
|
|
13771
|
+
pricingPageShowPublicDescriptions: false
|
|
12920
13772
|
}
|
|
12921
13773
|
});
|
|
12922
13774
|
});
|
|
@@ -12924,7 +13776,12 @@ describe('Server Routes', () => {
|
|
|
12924
13776
|
core.getGlobalSettings.mockReturnValue({
|
|
12925
13777
|
site: {
|
|
12926
13778
|
showRunTaskforceLocally: false,
|
|
12927
|
-
pricingPageEnabled: false
|
|
13779
|
+
pricingPageEnabled: false,
|
|
13780
|
+
animatedBackgroundEnabled: true,
|
|
13781
|
+
siteTheme: 'light',
|
|
13782
|
+
pricingPageTitle: 'Choose Your Plan',
|
|
13783
|
+
pricingPageDescription: 'Compare plans and pick the right one.',
|
|
13784
|
+
pricingPageShowPublicDescriptions: false
|
|
12928
13785
|
}
|
|
12929
13786
|
});
|
|
12930
13787
|
const match = matchRoute('GET', '/api/taskforce/public/site-config-script', routes);
|
|
@@ -12937,6 +13794,10 @@ describe('Server Routes', () => {
|
|
|
12937
13794
|
}));
|
|
12938
13795
|
expect(res.end.mock.calls[0][0]).toContain('window.__TASKFORCE_SITE_CONFIG__ = ');
|
|
12939
13796
|
expect(res.end.mock.calls[0][0]).toContain('"pricingPageEnabled":false');
|
|
13797
|
+
expect(res.end.mock.calls[0][0]).toContain('"animatedBackgroundEnabled":true');
|
|
13798
|
+
expect(res.end.mock.calls[0][0]).toContain('"siteTheme":"light"');
|
|
13799
|
+
expect(res.end.mock.calls[0][0]).toContain('"pricingPageTitle":"Choose Your Plan"');
|
|
13800
|
+
expect(res.end.mock.calls[0][0]).toContain('"pricingPageShowPublicDescriptions":false');
|
|
12940
13801
|
});
|
|
12941
13802
|
it('POST /api/taskforce/admin/system-settings should reject invalid onboarding settings', async () => {
|
|
12942
13803
|
core.validateOnboardingPolicy.mockReturnValueOnce({
|
|
@@ -13113,6 +13974,94 @@ describe('Server Routes', () => {
|
|
|
13113
13974
|
]);
|
|
13114
13975
|
fs.rmSync(dbRoot, { recursive: true, force: true });
|
|
13115
13976
|
});
|
|
13977
|
+
it('GET /api/taskforce/admin/orphaned-assets should not list AI profile avatar assets still referenced by profiles', async () => {
|
|
13978
|
+
const { tmpRoot: dbRoot, db, store } = createWorkspaceAssetTestStore();
|
|
13979
|
+
const avatarStorageKey = 'workspaces/workspace-1/assets/images/ai-profiles/ai-profile-codex/avatar/asset-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-cropped.webp';
|
|
13980
|
+
core.getDatabaseAdapter = vi.fn().mockReturnValue(db);
|
|
13981
|
+
core.getTenantId = vi.fn().mockReturnValue('default');
|
|
13982
|
+
core.listAiProfiles.mockReturnValueOnce([
|
|
13983
|
+
{
|
|
13984
|
+
id: 'ai-profile-codex',
|
|
13985
|
+
workspaceId: 'workspace-1',
|
|
13986
|
+
name: 'Codex',
|
|
13987
|
+
avatarUrl: `/api/taskforce/context/${encodeURIComponent(avatarStorageKey)}`,
|
|
13988
|
+
avatarSourceUrl: null,
|
|
13989
|
+
createdAt: '2026-03-18T17:00:00.000Z',
|
|
13990
|
+
updatedAt: '2026-03-18T17:05:00.000Z'
|
|
13991
|
+
}
|
|
13992
|
+
]);
|
|
13993
|
+
store.upsertAsset({
|
|
13994
|
+
assetId: 'asset-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
|
13995
|
+
workspaceId: 'workspace-1',
|
|
13996
|
+
kind: 'image',
|
|
13997
|
+
originalFilename: 'avatar.webp',
|
|
13998
|
+
mimeType: 'image/webp',
|
|
13999
|
+
storageProvider: 'r2',
|
|
14000
|
+
storageKey: avatarStorageKey,
|
|
14001
|
+
contentSha256: 'sha-avatar',
|
|
14002
|
+
sizeBytes: 42,
|
|
14003
|
+
createdAt: '2026-03-18T17:00:00.000Z',
|
|
14004
|
+
updatedAt: '2026-03-18T17:05:00.000Z'
|
|
14005
|
+
});
|
|
14006
|
+
routes = createRoutes(core);
|
|
14007
|
+
const match = matchRoute('GET', '/api/taskforce/admin/orphaned-assets', routes);
|
|
14008
|
+
const req = createMockReq('GET', '/api/taskforce/admin/orphaned-assets?workspaceId=workspace-1', undefined, {
|
|
14009
|
+
'x-taskforce-role': 'admin',
|
|
14010
|
+
'x-taskforce-workspace-id': 'workspace-1'
|
|
14011
|
+
});
|
|
14012
|
+
const res = createMockRes();
|
|
14013
|
+
await match?.handler(req, res, {});
|
|
14014
|
+
const payload = JSON.parse(res.end.mock.calls[0][0]);
|
|
14015
|
+
expect(payload.success).toBe(true);
|
|
14016
|
+
expect(payload.assets).toEqual([]);
|
|
14017
|
+
fs.rmSync(dbRoot, { recursive: true, force: true });
|
|
14018
|
+
});
|
|
14019
|
+
it('POST /api/taskforce/admin/orphaned-assets/delete should reject AI profile avatar assets still referenced by profiles', async () => {
|
|
14020
|
+
const { tmpRoot: dbRoot, db, store } = createWorkspaceAssetTestStore();
|
|
14021
|
+
const avatarStorageKey = 'workspaces/workspace-1/assets/images/ai-profiles/ai-profile-codex/avatar/asset-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-cropped.webp';
|
|
14022
|
+
core.getDatabaseAdapter = vi.fn().mockReturnValue(db);
|
|
14023
|
+
core.getTenantId = vi.fn().mockReturnValue('default');
|
|
14024
|
+
core.listAiProfiles.mockReturnValueOnce([
|
|
14025
|
+
{
|
|
14026
|
+
id: 'ai-profile-codex',
|
|
14027
|
+
workspaceId: 'workspace-1',
|
|
14028
|
+
name: 'Codex',
|
|
14029
|
+
avatarUrl: `/api/taskforce/context/${encodeURIComponent(avatarStorageKey)}`,
|
|
14030
|
+
avatarSourceUrl: null,
|
|
14031
|
+
createdAt: '2026-03-18T17:00:00.000Z',
|
|
14032
|
+
updatedAt: '2026-03-18T17:05:00.000Z'
|
|
14033
|
+
}
|
|
14034
|
+
]);
|
|
14035
|
+
store.upsertAsset({
|
|
14036
|
+
assetId: 'asset-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
|
|
14037
|
+
workspaceId: 'workspace-1',
|
|
14038
|
+
kind: 'image',
|
|
14039
|
+
originalFilename: 'avatar.webp',
|
|
14040
|
+
mimeType: 'image/webp',
|
|
14041
|
+
storageProvider: 'r2',
|
|
14042
|
+
storageKey: avatarStorageKey,
|
|
14043
|
+
contentSha256: 'sha-avatar',
|
|
14044
|
+
sizeBytes: 42,
|
|
14045
|
+
createdAt: '2026-03-18T17:00:00.000Z',
|
|
14046
|
+
updatedAt: '2026-03-18T17:05:00.000Z'
|
|
14047
|
+
});
|
|
14048
|
+
routes = createRoutes(core);
|
|
14049
|
+
const match = matchRoute('POST', '/api/taskforce/admin/orphaned-assets/delete', routes);
|
|
14050
|
+
const req = createMockReq('POST', '/api/taskforce/admin/orphaned-assets/delete', {
|
|
14051
|
+
assetId: 'asset-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
|
|
14052
|
+
workspaceId: 'workspace-1'
|
|
14053
|
+
}, {
|
|
14054
|
+
'x-taskforce-role': 'admin',
|
|
14055
|
+
'x-taskforce-workspace-id': 'workspace-1'
|
|
14056
|
+
});
|
|
14057
|
+
const res = createMockRes();
|
|
14058
|
+
await match?.handler(req, res, {});
|
|
14059
|
+
expect(res.writeHead).toHaveBeenCalledWith(409, expect.any(Object));
|
|
14060
|
+
const payload = JSON.parse(res.end.mock.calls[0][0]);
|
|
14061
|
+
expect(payload.error).toMatch(/referenced by an AI profile avatar/i);
|
|
14062
|
+
expect(store.get('asset-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', 'workspace-1')?.deletedAt).toBeNull();
|
|
14063
|
+
fs.rmSync(dbRoot, { recursive: true, force: true });
|
|
14064
|
+
});
|
|
13116
14065
|
it('POST /api/taskforce/admin/object-storage-settings should persist sanitized values', async () => {
|
|
13117
14066
|
const match = matchRoute('POST', '/api/taskforce/admin/object-storage-settings', routes);
|
|
13118
14067
|
const req = createMockReq('POST', '/api/taskforce/admin/object-storage-settings', {
|