@taskforcehq/taskforce 0.3.306 → 0.3.307
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.js +9 -1
- package/dist/components/views/PlanComparisonPage.test.js +19 -0
- package/dist/components/views/PlansPage.js +1 -1
- 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 +12 -0
- package/dist/core/McpTokenService.d.ts +12 -0
- package/dist/core/McpTokenService.js +44 -1
- 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 +190 -26
- package/dist/core/Taskforce.ids.test.js +58 -0
- package/dist/core/Taskforce.js +775 -71
- package/dist/core/Taskforce.mcpAuth.test.js +109 -0
- package/dist/core/types.d.ts +7 -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 +21 -0
- 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 +187 -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 +4 -0
- package/dist/server/routes/authSupport.js +12 -1
- package/dist/server/routes/billing.js +11 -0
- package/dist/server/routes/billing.test.js +3 -0
- 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 +890 -22
- 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/assets/AgentsModule-2y82_3DO.js +1 -0
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-BTLZZCiQ.js +3 -0
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-BVKew-mH.css +1 -0
- package/dist/ui/assets/ContextAttachmentManager-D2epuNnG.js +3 -0
- package/dist/ui/assets/DocumentWorkspace-D4rJKXzd.css +1 -0
- package/dist/ui/assets/DocumentWorkspace-sVa-3Do6.js +252 -0
- package/dist/ui/assets/EntityActivityTimeline-DyqvvhuF.css +1 -0
- package/dist/ui/assets/EntityActivityTimeline-Zods27y_.js +1 -0
- package/dist/ui/assets/{InitiativesModule-9E6ParrY.js → InitiativesModule-D4XseTwW.js} +1 -1
- package/dist/ui/assets/PlansPage-D5AcbO0L.js +1 -0
- package/dist/ui/assets/PlansPage-Dvqsz5zc.css +1 -0
- package/dist/ui/assets/TaskContextUpload-uC5qx4Bv.js +1 -0
- package/dist/ui/assets/TaskSettings-CqvPAsTv.js +9 -0
- package/dist/ui/assets/{WorkflowsModule-BA7jcEpH.js → WorkflowsModule-oOA9bcdK.js} +4 -4
- package/dist/ui/assets/documentReferences-DPZuTgHh.js +1 -0
- package/dist/ui/assets/index-607WPo_X.js +4 -0
- package/dist/ui/assets/index-CYyodXsg.css +1 -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/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');
|
|
@@ -11953,6 +12416,8 @@ describe('Server Routes', () => {
|
|
|
11953
12416
|
expect(matchRoute('PATCH', '/api/taskforce/admin/users/u1/role', routes)).not.toBeNull();
|
|
11954
12417
|
expect(matchRoute('GET', '/api/taskforce/admin/workspace-audit-logs', routes)).not.toBeNull();
|
|
11955
12418
|
expect(matchRoute('POST', '/api/taskforce/workspace/ai-profiles/avatar', routes)).not.toBeNull();
|
|
12419
|
+
expect(matchRoute('POST', '/api/taskforce/workspace/ai-profiles/avatar/upload', routes)).not.toBeNull();
|
|
12420
|
+
expect(matchRoute('POST', '/api/taskforce/workspace/ai-profiles/metadata', routes)).toBeNull();
|
|
11956
12421
|
});
|
|
11957
12422
|
it('POST /api/taskforce/admin/users/invite should authorize against the target workspace', async () => {
|
|
11958
12423
|
const authConfig = createCloudAuthConfig();
|
|
@@ -12382,6 +12847,8 @@ describe('Server Routes', () => {
|
|
|
12382
12847
|
color: '#ef6c00',
|
|
12383
12848
|
avatarUrl: null,
|
|
12384
12849
|
avatarSourceUrl: null,
|
|
12850
|
+
avatarRevision: 0,
|
|
12851
|
+
avatarUpdatedAt: null,
|
|
12385
12852
|
kind: 'agent',
|
|
12386
12853
|
description: 'Builds product code',
|
|
12387
12854
|
role: 'Implementer',
|
|
@@ -12396,7 +12863,86 @@ describe('Server Routes', () => {
|
|
|
12396
12863
|
}
|
|
12397
12864
|
]);
|
|
12398
12865
|
});
|
|
12399
|
-
it('GET /api/taskforce/workspace/assignee-options should
|
|
12866
|
+
it('GET /api/taskforce/workspace/assignee-options should support agent-only roster loads', async () => {
|
|
12867
|
+
const authConfig = {
|
|
12868
|
+
runtimeMode: 'cloud',
|
|
12869
|
+
enabled: true,
|
|
12870
|
+
requiredForApi: true,
|
|
12871
|
+
excludedPaths: ['/api/taskforce/health', '/api/taskforce/auth'],
|
|
12872
|
+
defaultRole: 'member',
|
|
12873
|
+
defaultWorkspaceId: 'default',
|
|
12874
|
+
sessionCookieName: 'taskforce_session',
|
|
12875
|
+
sessionSecret: 'test-secret',
|
|
12876
|
+
sessionTtlSeconds: 3600
|
|
12877
|
+
};
|
|
12878
|
+
routes = createRoutes(core, { authConfig });
|
|
12879
|
+
core.listWorkspaceUsers.mockReturnValue([
|
|
12880
|
+
{
|
|
12881
|
+
userId: 'member-1',
|
|
12882
|
+
email: 'member1@example.com',
|
|
12883
|
+
displayName: 'Member One',
|
|
12884
|
+
avatarUrl: null,
|
|
12885
|
+
role: 'member',
|
|
12886
|
+
permissionMode: 'read-write',
|
|
12887
|
+
betaAccess: true,
|
|
12888
|
+
status: 'active',
|
|
12889
|
+
disabled: false
|
|
12890
|
+
}
|
|
12891
|
+
]);
|
|
12892
|
+
core.listAiProfiles.mockReturnValue([
|
|
12893
|
+
{
|
|
12894
|
+
id: 'ai-profile-1',
|
|
12895
|
+
workspaceId: 'workspace-1',
|
|
12896
|
+
profileToken: 'tfp_codex_sync',
|
|
12897
|
+
name: 'Codex',
|
|
12898
|
+
username: 'codex',
|
|
12899
|
+
icon: 'Bot',
|
|
12900
|
+
color: '#ef6c00',
|
|
12901
|
+
description: 'Builds product code',
|
|
12902
|
+
role: 'Implementer',
|
|
12903
|
+
provider: 'OpenAI',
|
|
12904
|
+
model: 'gpt-5.4',
|
|
12905
|
+
surfaceType: 'coding_tool',
|
|
12906
|
+
seatScope: 'cloud_metered',
|
|
12907
|
+
archivedAt: null,
|
|
12908
|
+
createdAt: '2026-03-01T00:00:00.000Z',
|
|
12909
|
+
updatedAt: '2026-03-01T00:00:00.000Z',
|
|
12910
|
+
lastActiveAt: null
|
|
12911
|
+
}
|
|
12912
|
+
]);
|
|
12913
|
+
core.countAiProfileUsage.mockReturnValue({ used: 1, limit: 3, remaining: 2 });
|
|
12914
|
+
const match = matchRoute('GET', '/api/taskforce/workspace/assignee-options', routes);
|
|
12915
|
+
const cookie = createSessionTokenCookie('session-member-agent-roster', authConfig, {
|
|
12916
|
+
userId: 'member-user',
|
|
12917
|
+
role: 'member',
|
|
12918
|
+
workspaceId: 'workspace-1'
|
|
12919
|
+
});
|
|
12920
|
+
const req = createMockReq('GET', '/api/taskforce/workspace/assignee-options?kind=agent', undefined, {
|
|
12921
|
+
cookie,
|
|
12922
|
+
'x-taskforce-runtime-mode': 'cloud',
|
|
12923
|
+
'x-taskforce-role': 'member',
|
|
12924
|
+
'x-taskforce-user-id': 'member-user',
|
|
12925
|
+
'x-taskforce-workspace-id': 'workspace-1'
|
|
12926
|
+
});
|
|
12927
|
+
const res = createMockRes();
|
|
12928
|
+
await match?.handler(req, res, {});
|
|
12929
|
+
expect(core.listWorkspaceUsers).not.toHaveBeenCalled();
|
|
12930
|
+
expect(core.listAiProfiles).toHaveBeenCalledWith('workspace-1', expect.objectContaining({
|
|
12931
|
+
diagnostics: expect.any(Object)
|
|
12932
|
+
}));
|
|
12933
|
+
expect(core.countAiProfileUsage).toHaveBeenCalledWith('workspace-1');
|
|
12934
|
+
expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
12935
|
+
const payload = JSON.parse(res.end.mock.calls[0][0]);
|
|
12936
|
+
expect(payload.aiProfileSeatUsage).toEqual({ used: 1, limit: 3, remaining: 2 });
|
|
12937
|
+
expect(payload.assignees).toEqual([
|
|
12938
|
+
expect.objectContaining({
|
|
12939
|
+
value: 'ai-profile-1',
|
|
12940
|
+
label: 'Codex',
|
|
12941
|
+
kind: 'agent'
|
|
12942
|
+
})
|
|
12943
|
+
]);
|
|
12944
|
+
});
|
|
12945
|
+
it('GET /api/taskforce/workspace/assignee-options should not include ai profile tokens for workspace admins', async () => {
|
|
12400
12946
|
const authConfig = {
|
|
12401
12947
|
runtimeMode: 'cloud',
|
|
12402
12948
|
enabled: true,
|
|
@@ -12450,9 +12996,187 @@ describe('Server Routes', () => {
|
|
|
12450
12996
|
expect(payload.assignees).toEqual([
|
|
12451
12997
|
expect.objectContaining({
|
|
12452
12998
|
value: 'ai-profile-1',
|
|
12453
|
-
|
|
12999
|
+
label: 'Codex'
|
|
12454
13000
|
})
|
|
12455
13001
|
]);
|
|
13002
|
+
expect(payload.assignees[0]).not.toHaveProperty('profileToken');
|
|
13003
|
+
});
|
|
13004
|
+
it('POST /api/taskforce/workspace/ai-profiles/avatar should reject direct avatar URL writes', async () => {
|
|
13005
|
+
const authConfig = {
|
|
13006
|
+
runtimeMode: 'cloud',
|
|
13007
|
+
enabled: true,
|
|
13008
|
+
requiredForApi: true,
|
|
13009
|
+
excludedPaths: ['/api/taskforce/health', '/api/taskforce/auth'],
|
|
13010
|
+
defaultRole: 'member',
|
|
13011
|
+
defaultWorkspaceId: 'default',
|
|
13012
|
+
sessionCookieName: 'taskforce_session',
|
|
13013
|
+
sessionSecret: 'test-secret',
|
|
13014
|
+
sessionTtlSeconds: 3600
|
|
13015
|
+
};
|
|
13016
|
+
routes = createRoutes(core, { authConfig });
|
|
13017
|
+
const match = matchRoute('POST', '/api/taskforce/workspace/ai-profiles/avatar', routes);
|
|
13018
|
+
const cookie = createSessionTokenCookie('session-admin-avatar-ai-profile-reject', authConfig, {
|
|
13019
|
+
userId: 'admin-user',
|
|
13020
|
+
role: 'admin',
|
|
13021
|
+
workspaceId: 'workspace-1'
|
|
13022
|
+
});
|
|
13023
|
+
const req = createMockReq('POST', '/api/taskforce/workspace/ai-profiles/avatar', {
|
|
13024
|
+
profileId: 'ai-profile-1',
|
|
13025
|
+
avatarUrl: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fai-profiles%2Favatar.webp'
|
|
13026
|
+
}, {
|
|
13027
|
+
cookie,
|
|
13028
|
+
'x-taskforce-runtime-mode': 'cloud',
|
|
13029
|
+
'x-taskforce-role': 'admin',
|
|
13030
|
+
'x-taskforce-user-id': 'admin-user',
|
|
13031
|
+
'x-taskforce-workspace-id': 'workspace-1'
|
|
13032
|
+
});
|
|
13033
|
+
const res = createMockRes();
|
|
13034
|
+
await match?.handler(req, res, {});
|
|
13035
|
+
expect(core.updateAiProfileAvatar).not.toHaveBeenCalled();
|
|
13036
|
+
expect(res.writeHead).toHaveBeenCalledWith(400, expect.any(Object));
|
|
13037
|
+
const payload = JSON.parse(res.end.mock.calls[0][0]);
|
|
13038
|
+
expect(payload).toEqual({
|
|
13039
|
+
error: 'AI profile avatar updates must use the stable avatar upload endpoint.',
|
|
13040
|
+
code: 'AI_PROFILE_AVATAR_STABLE_UPLOAD_REQUIRED'
|
|
13041
|
+
});
|
|
13042
|
+
});
|
|
13043
|
+
it('POST /api/taskforce/workspace/ai-profiles/avatar should allow clear-only updates and redact profile tokens', async () => {
|
|
13044
|
+
const authConfig = {
|
|
13045
|
+
runtimeMode: 'cloud',
|
|
13046
|
+
enabled: true,
|
|
13047
|
+
requiredForApi: true,
|
|
13048
|
+
excludedPaths: ['/api/taskforce/health', '/api/taskforce/auth'],
|
|
13049
|
+
defaultRole: 'member',
|
|
13050
|
+
defaultWorkspaceId: 'default',
|
|
13051
|
+
sessionCookieName: 'taskforce_session',
|
|
13052
|
+
sessionSecret: 'test-secret',
|
|
13053
|
+
sessionTtlSeconds: 3600
|
|
13054
|
+
};
|
|
13055
|
+
routes = createRoutes(core, { authConfig });
|
|
13056
|
+
core.listAiProfiles.mockReturnValue([
|
|
13057
|
+
{
|
|
13058
|
+
id: 'ai-profile-1',
|
|
13059
|
+
workspaceId: 'workspace-1',
|
|
13060
|
+
avatarUrl: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fai-profiles%2Fai-profile-1%2Favatar%2Fdisplay.webp',
|
|
13061
|
+
avatarSourceUrl: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fai-profiles%2Fai-profile-1%2Favatar%2Fsource.webp'
|
|
13062
|
+
}
|
|
13063
|
+
]);
|
|
13064
|
+
core.updateAiProfileAvatar.mockReturnValue({
|
|
13065
|
+
id: 'ai-profile-1',
|
|
13066
|
+
workspaceId: 'workspace-1',
|
|
13067
|
+
profileToken: 'tfp_codex_sync',
|
|
13068
|
+
name: 'Codex',
|
|
13069
|
+
username: 'codex',
|
|
13070
|
+
icon: 'Bot',
|
|
13071
|
+
color: '#ef6c00',
|
|
13072
|
+
avatarUrl: null,
|
|
13073
|
+
avatarSourceUrl: null,
|
|
13074
|
+
description: 'Builds product code',
|
|
13075
|
+
role: 'Implementer',
|
|
13076
|
+
provider: 'OpenAI',
|
|
13077
|
+
model: 'gpt-5.4',
|
|
13078
|
+
surfaceType: 'coding_tool',
|
|
13079
|
+
archivedAt: null,
|
|
13080
|
+
createdAt: '2026-03-01T00:00:00.000Z',
|
|
13081
|
+
updatedAt: '2026-04-19T17:00:00.000Z',
|
|
13082
|
+
lastActiveAt: null
|
|
13083
|
+
});
|
|
13084
|
+
const match = matchRoute('POST', '/api/taskforce/workspace/ai-profiles/avatar', routes);
|
|
13085
|
+
const cookie = createSessionTokenCookie('session-admin-avatar-ai-profile-clear', authConfig, {
|
|
13086
|
+
userId: 'admin-user',
|
|
13087
|
+
role: 'admin',
|
|
13088
|
+
workspaceId: 'workspace-1'
|
|
13089
|
+
});
|
|
13090
|
+
const req = createMockReq('POST', '/api/taskforce/workspace/ai-profiles/avatar', {
|
|
13091
|
+
profileId: 'ai-profile-1',
|
|
13092
|
+
avatarUrl: null,
|
|
13093
|
+
avatarSourceUrl: null
|
|
13094
|
+
}, {
|
|
13095
|
+
cookie,
|
|
13096
|
+
'x-taskforce-runtime-mode': 'cloud',
|
|
13097
|
+
'x-taskforce-role': 'admin',
|
|
13098
|
+
'x-taskforce-user-id': 'admin-user',
|
|
13099
|
+
'x-taskforce-workspace-id': 'workspace-1'
|
|
13100
|
+
});
|
|
13101
|
+
const res = createMockRes();
|
|
13102
|
+
await match?.handler(req, res, {});
|
|
13103
|
+
expect(core.updateAiProfileAvatar).toHaveBeenCalledWith({
|
|
13104
|
+
workspaceId: 'workspace-1',
|
|
13105
|
+
profileId: 'ai-profile-1',
|
|
13106
|
+
avatarUrl: null,
|
|
13107
|
+
avatarSourceUrl: null
|
|
13108
|
+
});
|
|
13109
|
+
expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
13110
|
+
const payload = JSON.parse(res.end.mock.calls[0][0]);
|
|
13111
|
+
expect(payload.success).toBe(true);
|
|
13112
|
+
expect(payload.profile).toEqual(expect.objectContaining({
|
|
13113
|
+
id: 'ai-profile-1',
|
|
13114
|
+
avatarUrl: null,
|
|
13115
|
+
avatarSourceUrl: null
|
|
13116
|
+
}));
|
|
13117
|
+
expect(payload.profile).not.toHaveProperty('profileToken');
|
|
13118
|
+
});
|
|
13119
|
+
it('POST /api/taskforce/workspace/ai-profiles/merge should redact profile tokens from admin responses', async () => {
|
|
13120
|
+
const authConfig = {
|
|
13121
|
+
runtimeMode: 'cloud',
|
|
13122
|
+
enabled: true,
|
|
13123
|
+
requiredForApi: true,
|
|
13124
|
+
excludedPaths: ['/api/taskforce/health', '/api/taskforce/auth'],
|
|
13125
|
+
defaultRole: 'member',
|
|
13126
|
+
defaultWorkspaceId: 'default',
|
|
13127
|
+
sessionCookieName: 'taskforce_session',
|
|
13128
|
+
sessionSecret: 'test-secret',
|
|
13129
|
+
sessionTtlSeconds: 3600
|
|
13130
|
+
};
|
|
13131
|
+
routes = createRoutes(core, { authConfig });
|
|
13132
|
+
core.mergeAiProfiles.mockReturnValue({
|
|
13133
|
+
id: 'ai-profile-keep',
|
|
13134
|
+
workspaceId: 'workspace-1',
|
|
13135
|
+
profileToken: 'tfp_codex_sync',
|
|
13136
|
+
name: 'Codex',
|
|
13137
|
+
username: 'codex',
|
|
13138
|
+
icon: 'Bot',
|
|
13139
|
+
color: '#ef6c00',
|
|
13140
|
+
description: 'Builds product code',
|
|
13141
|
+
role: 'Implementer',
|
|
13142
|
+
provider: 'OpenAI',
|
|
13143
|
+
model: 'gpt-5.4',
|
|
13144
|
+
surfaceType: 'coding_tool',
|
|
13145
|
+
archivedAt: null,
|
|
13146
|
+
createdAt: '2026-03-01T00:00:00.000Z',
|
|
13147
|
+
updatedAt: '2026-04-19T17:00:00.000Z',
|
|
13148
|
+
lastActiveAt: null
|
|
13149
|
+
});
|
|
13150
|
+
const match = matchRoute('POST', '/api/taskforce/workspace/ai-profiles/merge', routes);
|
|
13151
|
+
const cookie = createSessionTokenCookie('session-admin-merge-ai-profile', authConfig, {
|
|
13152
|
+
userId: 'admin-user',
|
|
13153
|
+
role: 'admin',
|
|
13154
|
+
workspaceId: 'workspace-1'
|
|
13155
|
+
});
|
|
13156
|
+
const req = createMockReq('POST', '/api/taskforce/workspace/ai-profiles/merge', {
|
|
13157
|
+
keepId: 'ai-profile-keep',
|
|
13158
|
+
mergeId: 'ai-profile-merge'
|
|
13159
|
+
}, {
|
|
13160
|
+
cookie,
|
|
13161
|
+
'x-taskforce-runtime-mode': 'cloud',
|
|
13162
|
+
'x-taskforce-role': 'admin',
|
|
13163
|
+
'x-taskforce-user-id': 'admin-user',
|
|
13164
|
+
'x-taskforce-workspace-id': 'workspace-1'
|
|
13165
|
+
});
|
|
13166
|
+
const res = createMockRes();
|
|
13167
|
+
await match?.handler(req, res, {});
|
|
13168
|
+
expect(core.mergeAiProfiles).toHaveBeenCalledWith({
|
|
13169
|
+
workspaceId: 'workspace-1',
|
|
13170
|
+
keepId: 'ai-profile-keep',
|
|
13171
|
+
mergeId: 'ai-profile-merge'
|
|
13172
|
+
});
|
|
13173
|
+
expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
|
|
13174
|
+
const payload = JSON.parse(res.end.mock.calls[0][0]);
|
|
13175
|
+
expect(payload.profile).toEqual(expect.objectContaining({
|
|
13176
|
+
id: 'ai-profile-keep',
|
|
13177
|
+
name: 'Codex'
|
|
13178
|
+
}));
|
|
13179
|
+
expect(payload.profile).not.toHaveProperty('profileToken');
|
|
12456
13180
|
});
|
|
12457
13181
|
it('POST /api/taskforce/workspace/ai-profiles/archive should archive active profiles for workspace admins', async () => {
|
|
12458
13182
|
const authConfig = {
|
|
@@ -12518,6 +13242,7 @@ describe('Server Routes', () => {
|
|
|
12518
13242
|
id: 'ai-profile-1',
|
|
12519
13243
|
archivedReason: 'manual_archive'
|
|
12520
13244
|
}));
|
|
13245
|
+
expect(payload.profile).not.toHaveProperty('profileToken');
|
|
12521
13246
|
});
|
|
12522
13247
|
it('POST /api/taskforce/workspace/ai-profiles/archive should honor actual owner membership even when the request role is stale', async () => {
|
|
12523
13248
|
const authConfig = {
|
|
@@ -12837,7 +13562,11 @@ describe('Server Routes', () => {
|
|
|
12837
13562
|
core.getGlobalSettings.mockReturnValue({
|
|
12838
13563
|
site: {
|
|
12839
13564
|
showRunTaskforceLocally: false,
|
|
12840
|
-
pricingPageEnabled: false
|
|
13565
|
+
pricingPageEnabled: false,
|
|
13566
|
+
animatedBackgroundEnabled: true,
|
|
13567
|
+
siteTheme: 'light',
|
|
13568
|
+
pricingPageTitle: 'Choose Your Plan',
|
|
13569
|
+
pricingPageDescription: 'Compare plans and pick the right one.'
|
|
12841
13570
|
}
|
|
12842
13571
|
});
|
|
12843
13572
|
const match = matchRoute('GET', '/api/taskforce/admin/site-settings', routes);
|
|
@@ -12847,14 +13576,26 @@ describe('Server Routes', () => {
|
|
|
12847
13576
|
const payload = JSON.parse(res.end.mock.calls[0][0]);
|
|
12848
13577
|
expect(payload.status.showRunTaskforceLocally).toBe(false);
|
|
12849
13578
|
expect(payload.status.pricingPageEnabled).toBe(false);
|
|
13579
|
+
expect(payload.status.animatedBackgroundEnabled).toBe(true);
|
|
13580
|
+
expect(payload.status.siteTheme).toBe('light');
|
|
13581
|
+
expect(payload.status.pricingPageTitle).toBe('Choose Your Plan');
|
|
13582
|
+
expect(payload.status.pricingPageDescription).toBe('Compare plans and pick the right one.');
|
|
12850
13583
|
expect(payload.settings.showRunTaskforceLocally).toBe(false);
|
|
12851
13584
|
expect(payload.settings.pricingPageEnabled).toBe(false);
|
|
13585
|
+
expect(payload.settings.animatedBackgroundEnabled).toBe(true);
|
|
13586
|
+
expect(payload.settings.siteTheme).toBe('light');
|
|
13587
|
+
expect(payload.settings.pricingPageTitle).toBe('Choose Your Plan');
|
|
13588
|
+
expect(payload.settings.pricingPageDescription).toBe('Compare plans and pick the right one.');
|
|
12852
13589
|
});
|
|
12853
13590
|
it('GET /api/taskforce/admin/site_settings should expose effective website settings for legacy admin bundles', async () => {
|
|
12854
13591
|
core.getGlobalSettings.mockReturnValue({
|
|
12855
13592
|
site: {
|
|
12856
13593
|
showRunTaskforceLocally: false,
|
|
12857
|
-
pricingPageEnabled: false
|
|
13594
|
+
pricingPageEnabled: false,
|
|
13595
|
+
animatedBackgroundEnabled: true,
|
|
13596
|
+
siteTheme: 'light',
|
|
13597
|
+
pricingPageTitle: 'Choose Your Plan',
|
|
13598
|
+
pricingPageDescription: 'Compare plans and pick the right one.'
|
|
12858
13599
|
}
|
|
12859
13600
|
});
|
|
12860
13601
|
const match = matchRoute('GET', '/api/taskforce/admin/site_settings', routes);
|
|
@@ -12864,21 +13605,37 @@ describe('Server Routes', () => {
|
|
|
12864
13605
|
const payload = JSON.parse(res.end.mock.calls[0][0]);
|
|
12865
13606
|
expect(payload.status.showRunTaskforceLocally).toBe(false);
|
|
12866
13607
|
expect(payload.status.pricingPageEnabled).toBe(false);
|
|
13608
|
+
expect(payload.status.animatedBackgroundEnabled).toBe(true);
|
|
13609
|
+
expect(payload.status.siteTheme).toBe('light');
|
|
13610
|
+
expect(payload.status.pricingPageTitle).toBe('Choose Your Plan');
|
|
13611
|
+
expect(payload.status.pricingPageDescription).toBe('Compare plans and pick the right one.');
|
|
12867
13612
|
expect(payload.settings.showRunTaskforceLocally).toBe(false);
|
|
12868
13613
|
expect(payload.settings.pricingPageEnabled).toBe(false);
|
|
13614
|
+
expect(payload.settings.animatedBackgroundEnabled).toBe(true);
|
|
13615
|
+
expect(payload.settings.siteTheme).toBe('light');
|
|
13616
|
+
expect(payload.settings.pricingPageTitle).toBe('Choose Your Plan');
|
|
13617
|
+
expect(payload.settings.pricingPageDescription).toBe('Compare plans and pick the right one.');
|
|
12869
13618
|
});
|
|
12870
13619
|
it('POST /api/taskforce/admin/site-settings should persist website settings', async () => {
|
|
12871
13620
|
const match = matchRoute('POST', '/api/taskforce/admin/site-settings', routes);
|
|
12872
13621
|
const req = createMockReq('POST', '/api/taskforce/admin/site-settings', {
|
|
12873
13622
|
showRunTaskforceLocally: false,
|
|
12874
|
-
pricingPageEnabled: false
|
|
13623
|
+
pricingPageEnabled: false,
|
|
13624
|
+
animatedBackgroundEnabled: true,
|
|
13625
|
+
siteTheme: 'light',
|
|
13626
|
+
pricingPageTitle: 'Choose Your Plan',
|
|
13627
|
+
pricingPageDescription: 'Compare plans and pick the right one.'
|
|
12875
13628
|
}, { 'x-taskforce-role': 'admin' });
|
|
12876
13629
|
const res = createMockRes();
|
|
12877
13630
|
await match?.handler(req, res, {});
|
|
12878
13631
|
expect(core.updateGlobalSettings).toHaveBeenCalledWith({
|
|
12879
13632
|
site: {
|
|
12880
13633
|
showRunTaskforceLocally: false,
|
|
12881
|
-
pricingPageEnabled: false
|
|
13634
|
+
pricingPageEnabled: false,
|
|
13635
|
+
animatedBackgroundEnabled: true,
|
|
13636
|
+
siteTheme: 'light',
|
|
13637
|
+
pricingPageTitle: 'Choose Your Plan',
|
|
13638
|
+
pricingPageDescription: 'Compare plans and pick the right one.'
|
|
12882
13639
|
}
|
|
12883
13640
|
});
|
|
12884
13641
|
});
|
|
@@ -12886,14 +13643,22 @@ describe('Server Routes', () => {
|
|
|
12886
13643
|
const match = matchRoute('POST', '/api/taskforce/admin/site_settings', routes);
|
|
12887
13644
|
const req = createMockReq('POST', '/api/taskforce/admin/site_settings', {
|
|
12888
13645
|
showRunTaskforceLocally: false,
|
|
12889
|
-
pricingPageEnabled: false
|
|
13646
|
+
pricingPageEnabled: false,
|
|
13647
|
+
animatedBackgroundEnabled: true,
|
|
13648
|
+
siteTheme: 'light',
|
|
13649
|
+
pricingPageTitle: 'Choose Your Plan',
|
|
13650
|
+
pricingPageDescription: 'Compare plans and pick the right one.'
|
|
12890
13651
|
}, { 'x-taskforce-role': 'admin' });
|
|
12891
13652
|
const res = createMockRes();
|
|
12892
13653
|
await match?.handler(req, res, {});
|
|
12893
13654
|
expect(core.updateGlobalSettings).toHaveBeenCalledWith({
|
|
12894
13655
|
site: {
|
|
12895
13656
|
showRunTaskforceLocally: false,
|
|
12896
|
-
pricingPageEnabled: false
|
|
13657
|
+
pricingPageEnabled: false,
|
|
13658
|
+
animatedBackgroundEnabled: true,
|
|
13659
|
+
siteTheme: 'light',
|
|
13660
|
+
pricingPageTitle: 'Choose Your Plan',
|
|
13661
|
+
pricingPageDescription: 'Compare plans and pick the right one.'
|
|
12897
13662
|
}
|
|
12898
13663
|
});
|
|
12899
13664
|
});
|
|
@@ -12901,7 +13666,11 @@ describe('Server Routes', () => {
|
|
|
12901
13666
|
core.getGlobalSettings.mockReturnValue({
|
|
12902
13667
|
site: {
|
|
12903
13668
|
showRunTaskforceLocally: false,
|
|
12904
|
-
pricingPageEnabled: false
|
|
13669
|
+
pricingPageEnabled: false,
|
|
13670
|
+
animatedBackgroundEnabled: true,
|
|
13671
|
+
siteTheme: 'light',
|
|
13672
|
+
pricingPageTitle: 'Choose Your Plan',
|
|
13673
|
+
pricingPageDescription: 'Compare plans and pick the right one.'
|
|
12905
13674
|
}
|
|
12906
13675
|
});
|
|
12907
13676
|
const match = matchRoute('GET', '/api/taskforce/public/site-config', routes);
|
|
@@ -12916,7 +13685,11 @@ describe('Server Routes', () => {
|
|
|
12916
13685
|
success: true,
|
|
12917
13686
|
config: {
|
|
12918
13687
|
showRunTaskforceLocally: false,
|
|
12919
|
-
pricingPageEnabled: false
|
|
13688
|
+
pricingPageEnabled: false,
|
|
13689
|
+
animatedBackgroundEnabled: true,
|
|
13690
|
+
siteTheme: 'light',
|
|
13691
|
+
pricingPageTitle: 'Choose Your Plan',
|
|
13692
|
+
pricingPageDescription: 'Compare plans and pick the right one.'
|
|
12920
13693
|
}
|
|
12921
13694
|
});
|
|
12922
13695
|
});
|
|
@@ -12924,7 +13697,11 @@ describe('Server Routes', () => {
|
|
|
12924
13697
|
core.getGlobalSettings.mockReturnValue({
|
|
12925
13698
|
site: {
|
|
12926
13699
|
showRunTaskforceLocally: false,
|
|
12927
|
-
pricingPageEnabled: false
|
|
13700
|
+
pricingPageEnabled: false,
|
|
13701
|
+
animatedBackgroundEnabled: true,
|
|
13702
|
+
siteTheme: 'light',
|
|
13703
|
+
pricingPageTitle: 'Choose Your Plan',
|
|
13704
|
+
pricingPageDescription: 'Compare plans and pick the right one.'
|
|
12928
13705
|
}
|
|
12929
13706
|
});
|
|
12930
13707
|
const match = matchRoute('GET', '/api/taskforce/public/site-config-script', routes);
|
|
@@ -12937,6 +13714,9 @@ describe('Server Routes', () => {
|
|
|
12937
13714
|
}));
|
|
12938
13715
|
expect(res.end.mock.calls[0][0]).toContain('window.__TASKFORCE_SITE_CONFIG__ = ');
|
|
12939
13716
|
expect(res.end.mock.calls[0][0]).toContain('"pricingPageEnabled":false');
|
|
13717
|
+
expect(res.end.mock.calls[0][0]).toContain('"animatedBackgroundEnabled":true');
|
|
13718
|
+
expect(res.end.mock.calls[0][0]).toContain('"siteTheme":"light"');
|
|
13719
|
+
expect(res.end.mock.calls[0][0]).toContain('"pricingPageTitle":"Choose Your Plan"');
|
|
12940
13720
|
});
|
|
12941
13721
|
it('POST /api/taskforce/admin/system-settings should reject invalid onboarding settings', async () => {
|
|
12942
13722
|
core.validateOnboardingPolicy.mockReturnValueOnce({
|
|
@@ -13113,6 +13893,94 @@ describe('Server Routes', () => {
|
|
|
13113
13893
|
]);
|
|
13114
13894
|
fs.rmSync(dbRoot, { recursive: true, force: true });
|
|
13115
13895
|
});
|
|
13896
|
+
it('GET /api/taskforce/admin/orphaned-assets should not list AI profile avatar assets still referenced by profiles', async () => {
|
|
13897
|
+
const { tmpRoot: dbRoot, db, store } = createWorkspaceAssetTestStore();
|
|
13898
|
+
const avatarStorageKey = 'workspaces/workspace-1/assets/images/ai-profiles/ai-profile-codex/avatar/asset-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-cropped.webp';
|
|
13899
|
+
core.getDatabaseAdapter = vi.fn().mockReturnValue(db);
|
|
13900
|
+
core.getTenantId = vi.fn().mockReturnValue('default');
|
|
13901
|
+
core.listAiProfiles.mockReturnValueOnce([
|
|
13902
|
+
{
|
|
13903
|
+
id: 'ai-profile-codex',
|
|
13904
|
+
workspaceId: 'workspace-1',
|
|
13905
|
+
name: 'Codex',
|
|
13906
|
+
avatarUrl: `/api/taskforce/context/${encodeURIComponent(avatarStorageKey)}`,
|
|
13907
|
+
avatarSourceUrl: null,
|
|
13908
|
+
createdAt: '2026-03-18T17:00:00.000Z',
|
|
13909
|
+
updatedAt: '2026-03-18T17:05:00.000Z'
|
|
13910
|
+
}
|
|
13911
|
+
]);
|
|
13912
|
+
store.upsertAsset({
|
|
13913
|
+
assetId: 'asset-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
|
13914
|
+
workspaceId: 'workspace-1',
|
|
13915
|
+
kind: 'image',
|
|
13916
|
+
originalFilename: 'avatar.webp',
|
|
13917
|
+
mimeType: 'image/webp',
|
|
13918
|
+
storageProvider: 'r2',
|
|
13919
|
+
storageKey: avatarStorageKey,
|
|
13920
|
+
contentSha256: 'sha-avatar',
|
|
13921
|
+
sizeBytes: 42,
|
|
13922
|
+
createdAt: '2026-03-18T17:00:00.000Z',
|
|
13923
|
+
updatedAt: '2026-03-18T17:05:00.000Z'
|
|
13924
|
+
});
|
|
13925
|
+
routes = createRoutes(core);
|
|
13926
|
+
const match = matchRoute('GET', '/api/taskforce/admin/orphaned-assets', routes);
|
|
13927
|
+
const req = createMockReq('GET', '/api/taskforce/admin/orphaned-assets?workspaceId=workspace-1', undefined, {
|
|
13928
|
+
'x-taskforce-role': 'admin',
|
|
13929
|
+
'x-taskforce-workspace-id': 'workspace-1'
|
|
13930
|
+
});
|
|
13931
|
+
const res = createMockRes();
|
|
13932
|
+
await match?.handler(req, res, {});
|
|
13933
|
+
const payload = JSON.parse(res.end.mock.calls[0][0]);
|
|
13934
|
+
expect(payload.success).toBe(true);
|
|
13935
|
+
expect(payload.assets).toEqual([]);
|
|
13936
|
+
fs.rmSync(dbRoot, { recursive: true, force: true });
|
|
13937
|
+
});
|
|
13938
|
+
it('POST /api/taskforce/admin/orphaned-assets/delete should reject AI profile avatar assets still referenced by profiles', async () => {
|
|
13939
|
+
const { tmpRoot: dbRoot, db, store } = createWorkspaceAssetTestStore();
|
|
13940
|
+
const avatarStorageKey = 'workspaces/workspace-1/assets/images/ai-profiles/ai-profile-codex/avatar/asset-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-cropped.webp';
|
|
13941
|
+
core.getDatabaseAdapter = vi.fn().mockReturnValue(db);
|
|
13942
|
+
core.getTenantId = vi.fn().mockReturnValue('default');
|
|
13943
|
+
core.listAiProfiles.mockReturnValueOnce([
|
|
13944
|
+
{
|
|
13945
|
+
id: 'ai-profile-codex',
|
|
13946
|
+
workspaceId: 'workspace-1',
|
|
13947
|
+
name: 'Codex',
|
|
13948
|
+
avatarUrl: `/api/taskforce/context/${encodeURIComponent(avatarStorageKey)}`,
|
|
13949
|
+
avatarSourceUrl: null,
|
|
13950
|
+
createdAt: '2026-03-18T17:00:00.000Z',
|
|
13951
|
+
updatedAt: '2026-03-18T17:05:00.000Z'
|
|
13952
|
+
}
|
|
13953
|
+
]);
|
|
13954
|
+
store.upsertAsset({
|
|
13955
|
+
assetId: 'asset-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
|
|
13956
|
+
workspaceId: 'workspace-1',
|
|
13957
|
+
kind: 'image',
|
|
13958
|
+
originalFilename: 'avatar.webp',
|
|
13959
|
+
mimeType: 'image/webp',
|
|
13960
|
+
storageProvider: 'r2',
|
|
13961
|
+
storageKey: avatarStorageKey,
|
|
13962
|
+
contentSha256: 'sha-avatar',
|
|
13963
|
+
sizeBytes: 42,
|
|
13964
|
+
createdAt: '2026-03-18T17:00:00.000Z',
|
|
13965
|
+
updatedAt: '2026-03-18T17:05:00.000Z'
|
|
13966
|
+
});
|
|
13967
|
+
routes = createRoutes(core);
|
|
13968
|
+
const match = matchRoute('POST', '/api/taskforce/admin/orphaned-assets/delete', routes);
|
|
13969
|
+
const req = createMockReq('POST', '/api/taskforce/admin/orphaned-assets/delete', {
|
|
13970
|
+
assetId: 'asset-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
|
|
13971
|
+
workspaceId: 'workspace-1'
|
|
13972
|
+
}, {
|
|
13973
|
+
'x-taskforce-role': 'admin',
|
|
13974
|
+
'x-taskforce-workspace-id': 'workspace-1'
|
|
13975
|
+
});
|
|
13976
|
+
const res = createMockRes();
|
|
13977
|
+
await match?.handler(req, res, {});
|
|
13978
|
+
expect(res.writeHead).toHaveBeenCalledWith(409, expect.any(Object));
|
|
13979
|
+
const payload = JSON.parse(res.end.mock.calls[0][0]);
|
|
13980
|
+
expect(payload.error).toMatch(/referenced by an AI profile avatar/i);
|
|
13981
|
+
expect(store.get('asset-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', 'workspace-1')?.deletedAt).toBeNull();
|
|
13982
|
+
fs.rmSync(dbRoot, { recursive: true, force: true });
|
|
13983
|
+
});
|
|
13116
13984
|
it('POST /api/taskforce/admin/object-storage-settings should persist sanitized values', async () => {
|
|
13117
13985
|
const match = matchRoute('POST', '/api/taskforce/admin/object-storage-settings', routes);
|
|
13118
13986
|
const req = createMockReq('POST', '/api/taskforce/admin/object-storage-settings', {
|