@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
|
@@ -2,6 +2,7 @@ import type { DatabaseAdapter } from '../storage/databaseAdapter.js';
|
|
|
2
2
|
export type AttachmentLinkInput = string | {
|
|
3
3
|
path: string;
|
|
4
4
|
fsPath?: string;
|
|
5
|
+
assetId?: string | null;
|
|
5
6
|
caption?: string;
|
|
6
7
|
displayName?: string;
|
|
7
8
|
originalFilename?: string;
|
|
@@ -12,4 +13,6 @@ export interface AttachmentLinkServiceDeps {
|
|
|
12
13
|
tenantId: string;
|
|
13
14
|
normalizeWorkspaceId: (workspaceId: string) => string;
|
|
14
15
|
}
|
|
16
|
+
export type AttachmentLinkTargetType = 'task' | 'initiative' | 'workstream';
|
|
17
|
+
export declare function syncCanonicalAttachmentLinksForTargetService(deps: AttachmentLinkServiceDeps, targetType: AttachmentLinkTargetType, targetId: string, workspaceId: string, attachments: AttachmentLinkInput[]): void;
|
|
15
18
|
export declare function syncCanonicalTaskAttachmentLinksService(deps: AttachmentLinkServiceDeps, taskId: string, workspaceId: string, attachments: AttachmentLinkInput[]): void;
|
|
@@ -63,6 +63,25 @@ function normalizeAttachmentStorageKey(pathValue, fsPathValue) {
|
|
|
63
63
|
}
|
|
64
64
|
return null;
|
|
65
65
|
}
|
|
66
|
+
function extractAttachmentAssetId(attachment) {
|
|
67
|
+
const explicitAssetId = typeof attachment === 'string'
|
|
68
|
+
? ''
|
|
69
|
+
: String(attachment.assetId || '').trim();
|
|
70
|
+
if (explicitAssetId)
|
|
71
|
+
return explicitAssetId;
|
|
72
|
+
const pathValue = typeof attachment === 'string'
|
|
73
|
+
? attachment.trim()
|
|
74
|
+
: String(attachment.path || '').trim();
|
|
75
|
+
const match = pathValue.match(/^\/api\/taskforce\/documents\/([^/?#]+)\/content(?:[?#].*)?$/);
|
|
76
|
+
if (!match)
|
|
77
|
+
return null;
|
|
78
|
+
try {
|
|
79
|
+
return decodeURIComponent(match[1]).trim() || null;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return match[1].trim() || null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
66
85
|
function ensureExternalReferenceAsset(deps, storageKeyRaw, workspaceId, attachment) {
|
|
67
86
|
const storageKey = String(storageKeyRaw || '').trim();
|
|
68
87
|
const normalizedWorkspaceId = deps.normalizeWorkspaceId(workspaceId);
|
|
@@ -124,25 +143,31 @@ function ensureExternalReferenceAsset(deps, storageKeyRaw, workspaceId, attachme
|
|
|
124
143
|
`).run(assetId, deps.tenantId, normalizedWorkspaceId, 'file', logicalName, logicalName, mimeType, 'external', storageKey, urlHash, Buffer.byteLength(storageKey, 'utf8'), null, null, 1, 'clean', null, null, null, timestamp, timestamp);
|
|
125
144
|
return assetId;
|
|
126
145
|
}
|
|
127
|
-
export function
|
|
128
|
-
const
|
|
146
|
+
export function syncCanonicalAttachmentLinksForTargetService(deps, targetType, targetId, workspaceId, attachments) {
|
|
147
|
+
const normalizedTargetId = String(targetId || '').trim();
|
|
129
148
|
const normalizedWorkspaceId = deps.normalizeWorkspaceId(workspaceId);
|
|
130
|
-
if (!
|
|
149
|
+
if (!normalizedTargetId)
|
|
131
150
|
return;
|
|
132
151
|
const supportsTargetType = supportsWorkspaceAssetLinkTargetType(deps.db);
|
|
152
|
+
const safeTargetType = targetType === 'initiative' || targetType === 'workstream'
|
|
153
|
+
? targetType
|
|
154
|
+
: 'task';
|
|
155
|
+
const nowIso = new Date().toISOString();
|
|
133
156
|
if (supportsTargetType) {
|
|
134
157
|
deps.db.prepare(`
|
|
135
158
|
UPDATE workspace_asset_links
|
|
136
159
|
SET deleted_at = ?, updated_at = ?
|
|
137
|
-
WHERE tenant_id = ? AND workspace_id = ? AND target_type =
|
|
138
|
-
`).run(
|
|
160
|
+
WHERE tenant_id = ? AND workspace_id = ? AND target_type = ? AND task_id = ? AND deleted_at IS NULL
|
|
161
|
+
`).run(nowIso, nowIso, deps.tenantId, normalizedWorkspaceId, safeTargetType, normalizedTargetId);
|
|
139
162
|
}
|
|
140
163
|
else {
|
|
164
|
+
if (safeTargetType !== 'task')
|
|
165
|
+
return;
|
|
141
166
|
deps.db.prepare(`
|
|
142
167
|
UPDATE workspace_asset_links
|
|
143
168
|
SET deleted_at = ?, updated_at = ?
|
|
144
169
|
WHERE tenant_id = ? AND workspace_id = ? AND task_id = ? AND deleted_at IS NULL
|
|
145
|
-
`).run(
|
|
170
|
+
`).run(nowIso, nowIso, deps.tenantId, normalizedWorkspaceId, normalizedTargetId);
|
|
146
171
|
}
|
|
147
172
|
const selectAsset = deps.db.prepare(`
|
|
148
173
|
SELECT asset_id, kind
|
|
@@ -150,11 +175,17 @@ export function syncCanonicalTaskAttachmentLinksService(deps, taskId, workspaceI
|
|
|
150
175
|
WHERE tenant_id = ? AND workspace_id = ? AND storage_key = ? AND deleted_at IS NULL
|
|
151
176
|
LIMIT 1
|
|
152
177
|
`);
|
|
178
|
+
const selectAssetById = deps.db.prepare(`
|
|
179
|
+
SELECT asset_id, kind
|
|
180
|
+
FROM workspace_assets
|
|
181
|
+
WHERE tenant_id = ? AND workspace_id = ? AND asset_id = ? AND deleted_at IS NULL
|
|
182
|
+
LIMIT 1
|
|
183
|
+
`);
|
|
153
184
|
const upsertLink = supportsTargetType
|
|
154
185
|
? deps.db.prepare(`
|
|
155
186
|
INSERT INTO workspace_asset_links (
|
|
156
187
|
tenant_id, workspace_id, asset_id, target_type, task_id, role, display_name, sort_order, created_at, updated_at, deleted_at
|
|
157
|
-
) VALUES (?, ?, ?,
|
|
188
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)
|
|
158
189
|
ON CONFLICT (tenant_id, workspace_id, asset_id, task_id, role) DO UPDATE SET
|
|
159
190
|
target_type = excluded.target_type,
|
|
160
191
|
display_name = excluded.display_name,
|
|
@@ -174,16 +205,27 @@ export function syncCanonicalTaskAttachmentLinksService(deps, taskId, workspaceI
|
|
|
174
205
|
`);
|
|
175
206
|
attachments.forEach((attachment, index) => {
|
|
176
207
|
const storageKey = normalizeAttachmentStorageKey(typeof attachment === 'string' ? attachment : attachment.path, typeof attachment === 'string' ? null : attachment.fsPath);
|
|
177
|
-
|
|
208
|
+
const attachmentAssetId = extractAttachmentAssetId(attachment);
|
|
209
|
+
if (!storageKey && !attachmentAssetId)
|
|
178
210
|
return;
|
|
179
|
-
const row =
|
|
211
|
+
const row = (storageKey
|
|
212
|
+
? selectAsset.get(deps.tenantId, normalizedWorkspaceId, storageKey)
|
|
213
|
+
: null)
|
|
214
|
+
|| (attachmentAssetId
|
|
215
|
+
? selectAssetById.get(deps.tenantId, normalizedWorkspaceId, attachmentAssetId)
|
|
216
|
+
: null);
|
|
217
|
+
const isExternalStorageKey = typeof storageKey === 'string' && /^https?:\/\//i.test(storageKey);
|
|
218
|
+
const hasExplicitFileStorageKey = typeof storageKey === 'string'
|
|
219
|
+
&& typeof attachment !== 'string'
|
|
220
|
+
&& typeof attachment.fsPath === 'string'
|
|
221
|
+
&& attachment.fsPath.trim().length > 0;
|
|
180
222
|
const assetId = String(row?.asset_id || '').trim()
|
|
181
|
-
|| (
|
|
223
|
+
|| (isExternalStorageKey || hasExplicitFileStorageKey
|
|
182
224
|
? ensureExternalReferenceAsset(deps, storageKey, normalizedWorkspaceId, attachment)
|
|
183
225
|
: '');
|
|
184
226
|
if (!assetId)
|
|
185
227
|
return;
|
|
186
|
-
const role =
|
|
228
|
+
const role = isExternalStorageKey
|
|
187
229
|
? 'reference'
|
|
188
230
|
: String(row?.kind || '').trim() === 'image'
|
|
189
231
|
? 'image'
|
|
@@ -192,8 +234,11 @@ export function syncCanonicalTaskAttachmentLinksService(deps, taskId, workspaceI
|
|
|
192
234
|
? null
|
|
193
235
|
: (typeof attachment === 'string'
|
|
194
236
|
? null
|
|
195
|
-
: buildAttachmentDisplayName(attachment,
|
|
237
|
+
: buildAttachmentDisplayName(attachment, isExternalStorageKey || !storageKey ? null : path.basename(storageKey)));
|
|
196
238
|
const timestamp = typeof attachment === 'string' ? new Date().toISOString() : (attachment.timestamp || new Date().toISOString());
|
|
197
|
-
upsertLink.run(deps.tenantId, normalizedWorkspaceId, assetId,
|
|
239
|
+
upsertLink.run(deps.tenantId, normalizedWorkspaceId, assetId, safeTargetType, normalizedTargetId, role, displayName, index, timestamp, timestamp);
|
|
198
240
|
});
|
|
199
241
|
}
|
|
242
|
+
export function syncCanonicalTaskAttachmentLinksService(deps, taskId, workspaceId, attachments) {
|
|
243
|
+
syncCanonicalAttachmentLinksForTargetService(deps, 'task', taskId, workspaceId, attachments);
|
|
244
|
+
}
|
|
@@ -194,6 +194,12 @@ export function getGlobalSettingsService(deps) {
|
|
|
194
194
|
: {}),
|
|
195
195
|
...(typeof global.site.pricingPageEnabled === 'boolean'
|
|
196
196
|
? { pricingPageEnabled: global.site.pricingPageEnabled }
|
|
197
|
+
: {}),
|
|
198
|
+
...(typeof global.site.animatedBackgroundEnabled === 'boolean'
|
|
199
|
+
? { animatedBackgroundEnabled: global.site.animatedBackgroundEnabled }
|
|
200
|
+
: {}),
|
|
201
|
+
...(global.site.siteTheme === 'light' || global.site.siteTheme === 'dark'
|
|
202
|
+
? { siteTheme: global.site.siteTheme }
|
|
197
203
|
: {})
|
|
198
204
|
}
|
|
199
205
|
}
|
|
@@ -407,6 +413,12 @@ export function updateGlobalSettingsService(deps, settings) {
|
|
|
407
413
|
: {}),
|
|
408
414
|
...(typeof incoming.pricingPageEnabled === 'boolean'
|
|
409
415
|
? { pricingPageEnabled: incoming.pricingPageEnabled }
|
|
416
|
+
: {}),
|
|
417
|
+
...(typeof incoming.animatedBackgroundEnabled === 'boolean'
|
|
418
|
+
? { animatedBackgroundEnabled: incoming.animatedBackgroundEnabled }
|
|
419
|
+
: {}),
|
|
420
|
+
...(incoming.siteTheme === 'light' || incoming.siteTheme === 'dark'
|
|
421
|
+
? { siteTheme: incoming.siteTheme }
|
|
410
422
|
: {})
|
|
411
423
|
};
|
|
412
424
|
}
|
|
@@ -60,6 +60,11 @@ export declare class McpTokenService {
|
|
|
60
60
|
workspaceId: string;
|
|
61
61
|
tokenId: string;
|
|
62
62
|
}): McpAccessTokenRecord;
|
|
63
|
+
deleteMcpAccessToken(input: {
|
|
64
|
+
actorUserId: string;
|
|
65
|
+
workspaceId: string;
|
|
66
|
+
tokenId: string;
|
|
67
|
+
}): boolean;
|
|
63
68
|
regenerateMcpAccessToken(input: {
|
|
64
69
|
actorUserId: string;
|
|
65
70
|
workspaceId: string;
|
|
@@ -111,6 +116,13 @@ export declare class McpTokenService {
|
|
|
111
116
|
grantId: string;
|
|
112
117
|
}): McpOAuthConnectorGrantRecord;
|
|
113
118
|
authenticateMcpOAuthAccessToken(token: string, workspaceId?: string): McpOAuthAccessTokenAuthResult | null;
|
|
119
|
+
bindMcpOAuthConnectorGrantAiProfile(input: {
|
|
120
|
+
grantId: string;
|
|
121
|
+
workspaceId: string;
|
|
122
|
+
profileId: string;
|
|
123
|
+
profileToken: string;
|
|
124
|
+
profileName: string;
|
|
125
|
+
}): boolean;
|
|
114
126
|
recordMcpOAuthConnectorGrantUsage(input: {
|
|
115
127
|
grantId: string;
|
|
116
128
|
workspaceId: string;
|
|
@@ -518,6 +518,23 @@ export class McpTokenService {
|
|
|
518
518
|
}
|
|
519
519
|
return this.rowToMcpAccessTokenRecord(updated);
|
|
520
520
|
}
|
|
521
|
+
deleteMcpAccessToken(input) {
|
|
522
|
+
this.getActiveWorkspaceMemberForMcp(input.actorUserId, input.workspaceId);
|
|
523
|
+
const row = this.getMcpAccessTokenRow(input.tokenId, input.workspaceId);
|
|
524
|
+
if (!row) {
|
|
525
|
+
throw new TaskforceRuleError('MCP token not found', 404, 'MCP_TOKEN_NOT_FOUND');
|
|
526
|
+
}
|
|
527
|
+
this.assertCanManageOwnMcpAccessToken(input.actorUserId, row);
|
|
528
|
+
const status = this.resolveMcpAccessTokenStatus(row);
|
|
529
|
+
if (status === 'active') {
|
|
530
|
+
throw new TaskforceRuleError('Active MCP tokens must be revoked before deletion', 400, 'MCP_TOKEN_DELETE_ACTIVE');
|
|
531
|
+
}
|
|
532
|
+
const result = this.db.prepare(`
|
|
533
|
+
DELETE FROM mcp_access_tokens
|
|
534
|
+
WHERE tenant_id = ? AND workspace_id = ? AND id = ?
|
|
535
|
+
`).run(this.tenantId, input.workspaceId, row.id);
|
|
536
|
+
return Number(result.changes || 0) > 0;
|
|
537
|
+
}
|
|
521
538
|
regenerateMcpAccessToken(input) {
|
|
522
539
|
this.getActiveWorkspaceMemberForMcp(input.actorUserId, input.workspaceId);
|
|
523
540
|
const existing = this.getMcpAccessTokenRow(input.tokenId, input.workspaceId);
|
|
@@ -872,7 +889,8 @@ export class McpTokenService {
|
|
|
872
889
|
if (!parsed)
|
|
873
890
|
return null;
|
|
874
891
|
const row = this.db.prepare(`
|
|
875
|
-
SELECT id, provider, client_id, user_id, workspace_id, scopes_json, token_hash, status, revoked_at
|
|
892
|
+
SELECT id, provider, client_id, user_id, workspace_id, scopes_json, token_hash, status, revoked_at,
|
|
893
|
+
ai_profile_id, ai_profile_token, ai_profile_name
|
|
876
894
|
FROM mcp_oauth_connector_grants
|
|
877
895
|
WHERE tenant_id = ?
|
|
878
896
|
AND token_prefix = ?
|
|
@@ -902,8 +920,33 @@ export class McpTokenService {
|
|
|
902
920
|
provider: this.normalizeMcpOAuthProvider(row.provider),
|
|
903
921
|
scopes,
|
|
904
922
|
status: this.resolveMcpOAuthGrantStatus(row),
|
|
923
|
+
aiProfileId: row?.ai_profile_id ? String(row.ai_profile_id) : null,
|
|
924
|
+
aiProfileToken: row?.ai_profile_token ? String(row.ai_profile_token) : null,
|
|
925
|
+
aiProfileName: row?.ai_profile_name ? String(row.ai_profile_name) : null,
|
|
905
926
|
};
|
|
906
927
|
}
|
|
928
|
+
bindMcpOAuthConnectorGrantAiProfile(input) {
|
|
929
|
+
const grantId = String(input.grantId || '').trim();
|
|
930
|
+
const workspaceId = String(input.workspaceId || '').trim();
|
|
931
|
+
const profileId = String(input.profileId || '').trim();
|
|
932
|
+
const profileToken = String(input.profileToken || '').trim();
|
|
933
|
+
const profileName = String(input.profileName || '').trim();
|
|
934
|
+
if (!grantId || !workspaceId || !profileId || !profileToken || !profileName)
|
|
935
|
+
return false;
|
|
936
|
+
const now = new Date().toISOString();
|
|
937
|
+
const result = this.db.prepare(`
|
|
938
|
+
UPDATE mcp_oauth_connector_grants
|
|
939
|
+
SET ai_profile_id = ?,
|
|
940
|
+
ai_profile_token = ?,
|
|
941
|
+
ai_profile_name = ?,
|
|
942
|
+
updated_at = ?
|
|
943
|
+
WHERE tenant_id = ?
|
|
944
|
+
AND workspace_id = ?
|
|
945
|
+
AND id = ?
|
|
946
|
+
AND status = 'active'
|
|
947
|
+
`).run(profileId, profileToken, profileName, now, this.tenantId, workspaceId, grantId);
|
|
948
|
+
return Number(result?.changes || 0) > 0;
|
|
949
|
+
}
|
|
907
950
|
recordMcpOAuthConnectorGrantUsage(input) {
|
|
908
951
|
const grantId = String(input.grantId || '').trim();
|
|
909
952
|
const workspaceId = String(input.workspaceId || '').trim();
|
|
@@ -22,13 +22,33 @@ describe('TaskforceCore planning entity persistence', () => {
|
|
|
22
22
|
const initiative = core.createInitiative({
|
|
23
23
|
title: 'Launch initiative',
|
|
24
24
|
comments: [{ id: 'comment-1', author: 'user', text: 'Kickoff note', timestamp: '2026-01-01T00:00:00.000Z' }],
|
|
25
|
-
attachments: [{
|
|
25
|
+
attachments: [{
|
|
26
|
+
path: '/tmp/launch-brief.md',
|
|
27
|
+
fsPath: 'workspaces/workspace-1/assets/documents/asset-launch-brief.md',
|
|
28
|
+
caption: 'Launch brief',
|
|
29
|
+
displayName: 'Launch Brief',
|
|
30
|
+
originalFilename: 'launch-brief.md',
|
|
31
|
+
assetId: 'asset-launch-brief',
|
|
32
|
+
referenceNumber: 12,
|
|
33
|
+
referenceLabel: 'D-12',
|
|
34
|
+
timestamp: '2026-01-01T00:00:00.000Z',
|
|
35
|
+
}],
|
|
26
36
|
});
|
|
27
37
|
const workstream = core.createWorkstream({
|
|
28
38
|
title: 'Billing workstream',
|
|
29
39
|
initiativeId: initiative.id,
|
|
30
40
|
comments: [{ id: 'comment-2', author: 'user', text: 'Billing note', timestamp: '2026-01-01T00:00:00.000Z' }],
|
|
31
|
-
attachments: [{
|
|
41
|
+
attachments: [{
|
|
42
|
+
path: '/tmp/billing-brief.md',
|
|
43
|
+
fsPath: 'workspaces/workspace-1/assets/documents/asset-billing-brief.md',
|
|
44
|
+
caption: 'Billing brief',
|
|
45
|
+
displayName: 'Billing Brief',
|
|
46
|
+
originalFilename: 'billing-brief.md',
|
|
47
|
+
assetId: 'asset-billing-brief',
|
|
48
|
+
referenceNumber: 13,
|
|
49
|
+
referenceLabel: 'D-13',
|
|
50
|
+
timestamp: '2026-01-01T00:00:00.000Z',
|
|
51
|
+
}],
|
|
32
52
|
});
|
|
33
53
|
const task = core.createTask({
|
|
34
54
|
title: 'Adjust billing copy',
|
|
@@ -43,11 +63,29 @@ describe('TaskforceCore planning entity persistence', () => {
|
|
|
43
63
|
const persistedTask = core.getTask(task.id);
|
|
44
64
|
expect(persistedInitiative?.referenceLabel).toBe('IN-1');
|
|
45
65
|
expect(persistedInitiative?.comments?.[0]?.text).toBe('Kickoff note');
|
|
46
|
-
expect(persistedInitiative?.attachments?.[0]).toMatchObject({
|
|
66
|
+
expect(persistedInitiative?.attachments?.[0]).toMatchObject({
|
|
67
|
+
path: '/tmp/launch-brief.md',
|
|
68
|
+
fsPath: 'workspaces/workspace-1/assets/documents/asset-launch-brief.md',
|
|
69
|
+
caption: 'Launch brief',
|
|
70
|
+
displayName: 'Launch Brief',
|
|
71
|
+
originalFilename: 'launch-brief.md',
|
|
72
|
+
assetId: 'asset-launch-brief',
|
|
73
|
+
referenceNumber: 12,
|
|
74
|
+
referenceLabel: 'D-12',
|
|
75
|
+
});
|
|
47
76
|
expect(persistedWorkstream?.referenceLabel).toBe('WS-1');
|
|
48
77
|
expect(persistedWorkstream?.initiativeId).toBe(initiative.id);
|
|
49
78
|
expect(persistedWorkstream?.comments?.[0]?.text).toBe('Billing note');
|
|
50
|
-
expect(persistedWorkstream?.attachments?.[0]).toMatchObject({
|
|
79
|
+
expect(persistedWorkstream?.attachments?.[0]).toMatchObject({
|
|
80
|
+
path: '/tmp/billing-brief.md',
|
|
81
|
+
fsPath: 'workspaces/workspace-1/assets/documents/asset-billing-brief.md',
|
|
82
|
+
caption: 'Billing brief',
|
|
83
|
+
displayName: 'Billing Brief',
|
|
84
|
+
originalFilename: 'billing-brief.md',
|
|
85
|
+
assetId: 'asset-billing-brief',
|
|
86
|
+
referenceNumber: 13,
|
|
87
|
+
referenceLabel: 'D-13',
|
|
88
|
+
});
|
|
51
89
|
expect(persistedTask?.workstreamId).toBe(workstream.id);
|
|
52
90
|
expect(persistedTask?.checklistItems?.map((item) => item.title)).toEqual([
|
|
53
91
|
'Update docs',
|
|
@@ -92,6 +130,328 @@ describe('TaskforceCore planning entity persistence', () => {
|
|
|
92
130
|
expect(clearedTask?.workstreamId).toBeNull();
|
|
93
131
|
core.close();
|
|
94
132
|
});
|
|
133
|
+
it('syncs canonical attachment links for initiatives and workstreams like task attachments do', () => {
|
|
134
|
+
const core = createCore();
|
|
135
|
+
const workspaceId = 'default';
|
|
136
|
+
const assetStore = core.getWorkspaceAssetStore();
|
|
137
|
+
assetStore.upsertAsset({
|
|
138
|
+
assetId: 'asset-initiative-doc',
|
|
139
|
+
workspaceId,
|
|
140
|
+
kind: 'document',
|
|
141
|
+
logicalName: 'Initiative context',
|
|
142
|
+
originalFilename: 'initiative-context.md',
|
|
143
|
+
mimeType: 'text/markdown',
|
|
144
|
+
storageProvider: 'local',
|
|
145
|
+
storageKey: 'workspaces/default/assets/documents/initiative-context.md',
|
|
146
|
+
contentSha256: 'sha-init',
|
|
147
|
+
sizeBytes: 120,
|
|
148
|
+
documentId: 'doc-initiative-context',
|
|
149
|
+
version: 1,
|
|
150
|
+
isLatest: 1,
|
|
151
|
+
});
|
|
152
|
+
assetStore.upsertAsset({
|
|
153
|
+
assetId: 'asset-workstream-doc',
|
|
154
|
+
workspaceId,
|
|
155
|
+
kind: 'document',
|
|
156
|
+
logicalName: 'Workstream context',
|
|
157
|
+
originalFilename: 'workstream-context.md',
|
|
158
|
+
mimeType: 'text/markdown',
|
|
159
|
+
storageProvider: 'local',
|
|
160
|
+
storageKey: 'workspaces/default/assets/documents/workstream-context.md',
|
|
161
|
+
contentSha256: 'sha-workstream',
|
|
162
|
+
sizeBytes: 140,
|
|
163
|
+
documentId: 'doc-workstream-context',
|
|
164
|
+
version: 1,
|
|
165
|
+
isLatest: 1,
|
|
166
|
+
});
|
|
167
|
+
const initiative = core.createInitiative({
|
|
168
|
+
title: 'Platform',
|
|
169
|
+
attachments: [{
|
|
170
|
+
path: '/api/taskforce/documents/asset-initiative-doc/content',
|
|
171
|
+
fsPath: 'workspaces/default/assets/documents/initiative-context.md',
|
|
172
|
+
caption: 'Initiative context',
|
|
173
|
+
assetId: 'asset-initiative-doc',
|
|
174
|
+
timestamp: '2026-01-01T00:00:00.000Z',
|
|
175
|
+
}],
|
|
176
|
+
}, workspaceId);
|
|
177
|
+
const workstream = core.createWorkstream({
|
|
178
|
+
title: 'Sync',
|
|
179
|
+
initiativeId: initiative.id,
|
|
180
|
+
attachments: [{
|
|
181
|
+
path: '/api/taskforce/documents/asset-workstream-doc/content',
|
|
182
|
+
fsPath: 'workspaces/default/assets/documents/workstream-context.md',
|
|
183
|
+
caption: 'Workstream context',
|
|
184
|
+
assetId: 'asset-workstream-doc',
|
|
185
|
+
timestamp: '2026-01-01T00:00:00.000Z',
|
|
186
|
+
}],
|
|
187
|
+
}, workspaceId);
|
|
188
|
+
expect(assetStore.listLinksForTarget('initiative', initiative.id, workspaceId)).toEqual([
|
|
189
|
+
expect.objectContaining({
|
|
190
|
+
assetId: 'asset-initiative-doc',
|
|
191
|
+
targetType: 'initiative',
|
|
192
|
+
targetId: initiative.id,
|
|
193
|
+
role: 'attachment',
|
|
194
|
+
})
|
|
195
|
+
]);
|
|
196
|
+
expect(assetStore.listLinksForTarget('workstream', workstream.id, workspaceId)).toEqual([
|
|
197
|
+
expect.objectContaining({
|
|
198
|
+
assetId: 'asset-workstream-doc',
|
|
199
|
+
targetType: 'workstream',
|
|
200
|
+
targetId: workstream.id,
|
|
201
|
+
role: 'attachment',
|
|
202
|
+
})
|
|
203
|
+
]);
|
|
204
|
+
core.updateInitiative(initiative.id, { attachments: [] }, workspaceId);
|
|
205
|
+
core.updateWorkstream(workstream.id, { attachments: [] }, workspaceId);
|
|
206
|
+
expect(assetStore.listLinksForTarget('initiative', initiative.id, workspaceId)).toEqual([]);
|
|
207
|
+
expect(assetStore.listLinksForTarget('initiative', initiative.id, workspaceId, { includeDeleted: true })).toEqual([
|
|
208
|
+
expect.objectContaining({
|
|
209
|
+
assetId: 'asset-initiative-doc',
|
|
210
|
+
deletedAt: expect.any(String),
|
|
211
|
+
})
|
|
212
|
+
]);
|
|
213
|
+
expect(assetStore.listLinksForTarget('workstream', workstream.id, workspaceId)).toEqual([]);
|
|
214
|
+
expect(assetStore.listLinksForTarget('workstream', workstream.id, workspaceId, { includeDeleted: true })).toEqual([
|
|
215
|
+
expect.objectContaining({
|
|
216
|
+
assetId: 'asset-workstream-doc',
|
|
217
|
+
deletedAt: expect.any(String),
|
|
218
|
+
})
|
|
219
|
+
]);
|
|
220
|
+
core.close();
|
|
221
|
+
});
|
|
222
|
+
it('syncs sparse canonical document attachment links by asset id for planning entities', () => {
|
|
223
|
+
const core = createCore();
|
|
224
|
+
const workspaceId = 'default';
|
|
225
|
+
const assetStore = core.getWorkspaceAssetStore();
|
|
226
|
+
assetStore.upsertAsset({
|
|
227
|
+
assetId: 'asset-sparse-initiative-doc',
|
|
228
|
+
workspaceId,
|
|
229
|
+
kind: 'document',
|
|
230
|
+
logicalName: 'Sparse initiative context',
|
|
231
|
+
originalFilename: 'sparse-initiative.md',
|
|
232
|
+
mimeType: 'text/markdown',
|
|
233
|
+
storageProvider: 'local',
|
|
234
|
+
storageKey: 'workspaces/default/assets/documents/sparse-initiative.md',
|
|
235
|
+
contentSha256: 'sha-sparse-initiative',
|
|
236
|
+
sizeBytes: 120,
|
|
237
|
+
documentId: 'doc-sparse-initiative',
|
|
238
|
+
version: 1,
|
|
239
|
+
isLatest: 1,
|
|
240
|
+
});
|
|
241
|
+
assetStore.upsertAsset({
|
|
242
|
+
assetId: 'asset-sparse-workstream-doc',
|
|
243
|
+
workspaceId,
|
|
244
|
+
kind: 'document',
|
|
245
|
+
logicalName: 'Sparse workstream context',
|
|
246
|
+
originalFilename: 'sparse-workstream.md',
|
|
247
|
+
mimeType: 'text/markdown',
|
|
248
|
+
storageProvider: 'local',
|
|
249
|
+
storageKey: 'workspaces/default/assets/documents/sparse-workstream.md',
|
|
250
|
+
contentSha256: 'sha-sparse-workstream',
|
|
251
|
+
sizeBytes: 140,
|
|
252
|
+
documentId: 'doc-sparse-workstream',
|
|
253
|
+
version: 1,
|
|
254
|
+
isLatest: 1,
|
|
255
|
+
});
|
|
256
|
+
const initiative = core.createInitiative({
|
|
257
|
+
title: 'Platform',
|
|
258
|
+
attachments: [{
|
|
259
|
+
path: '/api/taskforce/documents/asset-sparse-initiative-doc/content',
|
|
260
|
+
assetId: 'asset-sparse-initiative-doc',
|
|
261
|
+
caption: 'Sparse initiative context',
|
|
262
|
+
referenceLabel: 'D-101',
|
|
263
|
+
timestamp: '2026-01-01T00:00:00.000Z',
|
|
264
|
+
}],
|
|
265
|
+
}, workspaceId);
|
|
266
|
+
const workstream = core.createWorkstream({
|
|
267
|
+
title: 'Sync',
|
|
268
|
+
initiativeId: initiative.id,
|
|
269
|
+
attachments: [{
|
|
270
|
+
path: '/api/taskforce/documents/asset-sparse-workstream-doc/content',
|
|
271
|
+
assetId: 'asset-sparse-workstream-doc',
|
|
272
|
+
caption: 'Sparse workstream context',
|
|
273
|
+
referenceLabel: 'D-102',
|
|
274
|
+
timestamp: '2026-01-01T00:00:00.000Z',
|
|
275
|
+
}],
|
|
276
|
+
}, workspaceId);
|
|
277
|
+
expect(assetStore.listLinksForTarget('initiative', initiative.id, workspaceId)).toEqual([
|
|
278
|
+
expect.objectContaining({
|
|
279
|
+
assetId: 'asset-sparse-initiative-doc',
|
|
280
|
+
targetType: 'initiative',
|
|
281
|
+
targetId: initiative.id,
|
|
282
|
+
role: 'attachment',
|
|
283
|
+
})
|
|
284
|
+
]);
|
|
285
|
+
expect(assetStore.listLinksForTarget('workstream', workstream.id, workspaceId)).toEqual([
|
|
286
|
+
expect.objectContaining({
|
|
287
|
+
assetId: 'asset-sparse-workstream-doc',
|
|
288
|
+
targetType: 'workstream',
|
|
289
|
+
targetId: workstream.id,
|
|
290
|
+
role: 'attachment',
|
|
291
|
+
})
|
|
292
|
+
]);
|
|
293
|
+
core.close();
|
|
294
|
+
});
|
|
295
|
+
it('preserves planning attachments when sync upserts omit attachment fields', () => {
|
|
296
|
+
const core = createCore();
|
|
297
|
+
const workspaceId = 'default';
|
|
298
|
+
const assetStore = core.getWorkspaceAssetStore();
|
|
299
|
+
assetStore.upsertAsset({
|
|
300
|
+
assetId: 'asset-preserved-initiative-doc',
|
|
301
|
+
workspaceId,
|
|
302
|
+
kind: 'document',
|
|
303
|
+
logicalName: 'Preserved initiative context',
|
|
304
|
+
originalFilename: 'preserved-initiative.md',
|
|
305
|
+
mimeType: 'text/markdown',
|
|
306
|
+
storageProvider: 'local',
|
|
307
|
+
storageKey: 'workspaces/default/assets/documents/preserved-initiative.md',
|
|
308
|
+
contentSha256: 'sha-preserved-initiative',
|
|
309
|
+
sizeBytes: 120,
|
|
310
|
+
documentId: 'doc-preserved-initiative',
|
|
311
|
+
version: 1,
|
|
312
|
+
isLatest: 1,
|
|
313
|
+
});
|
|
314
|
+
assetStore.upsertAsset({
|
|
315
|
+
assetId: 'asset-preserved-workstream-doc',
|
|
316
|
+
workspaceId,
|
|
317
|
+
kind: 'document',
|
|
318
|
+
logicalName: 'Preserved workstream context',
|
|
319
|
+
originalFilename: 'preserved-workstream.md',
|
|
320
|
+
mimeType: 'text/markdown',
|
|
321
|
+
storageProvider: 'local',
|
|
322
|
+
storageKey: 'workspaces/default/assets/documents/preserved-workstream.md',
|
|
323
|
+
contentSha256: 'sha-preserved-workstream',
|
|
324
|
+
sizeBytes: 140,
|
|
325
|
+
documentId: 'doc-preserved-workstream',
|
|
326
|
+
version: 1,
|
|
327
|
+
isLatest: 1,
|
|
328
|
+
});
|
|
329
|
+
const initiative = core.createInitiative({
|
|
330
|
+
title: 'Platform',
|
|
331
|
+
attachments: [{
|
|
332
|
+
path: '/api/taskforce/documents/asset-preserved-initiative-doc/content',
|
|
333
|
+
assetId: 'asset-preserved-initiative-doc',
|
|
334
|
+
caption: 'Preserved initiative context',
|
|
335
|
+
timestamp: '2026-01-01T00:00:00.000Z',
|
|
336
|
+
}],
|
|
337
|
+
}, workspaceId);
|
|
338
|
+
const workstream = core.createWorkstream({
|
|
339
|
+
title: 'Sync',
|
|
340
|
+
initiativeId: initiative.id,
|
|
341
|
+
attachments: [{
|
|
342
|
+
path: '/api/taskforce/documents/asset-preserved-workstream-doc/content',
|
|
343
|
+
assetId: 'asset-preserved-workstream-doc',
|
|
344
|
+
caption: 'Preserved workstream context',
|
|
345
|
+
timestamp: '2026-01-01T00:00:00.000Z',
|
|
346
|
+
}],
|
|
347
|
+
}, workspaceId);
|
|
348
|
+
core.upsertInitiativeForSync({
|
|
349
|
+
id: initiative.id,
|
|
350
|
+
referenceNumber: initiative.referenceNumber,
|
|
351
|
+
title: 'Platform updated elsewhere',
|
|
352
|
+
description: initiative.description,
|
|
353
|
+
ownerId: initiative.ownerId,
|
|
354
|
+
createdAt: initiative.createdAt,
|
|
355
|
+
updatedAt: '2026-01-01T00:05:00.000Z',
|
|
356
|
+
order: initiative.order,
|
|
357
|
+
isArchived: false,
|
|
358
|
+
}, workspaceId);
|
|
359
|
+
core.upsertWorkstreamForSync({
|
|
360
|
+
id: workstream.id,
|
|
361
|
+
referenceNumber: workstream.referenceNumber,
|
|
362
|
+
initiativeId: initiative.id,
|
|
363
|
+
title: 'Sync updated elsewhere',
|
|
364
|
+
description: workstream.description,
|
|
365
|
+
ownerId: workstream.ownerId,
|
|
366
|
+
createdAt: workstream.createdAt,
|
|
367
|
+
updatedAt: '2026-01-01T00:05:00.000Z',
|
|
368
|
+
order: workstream.order,
|
|
369
|
+
isArchived: false,
|
|
370
|
+
}, workspaceId);
|
|
371
|
+
expect(core.getInitiative(initiative.id, workspaceId)?.attachments).toEqual([
|
|
372
|
+
expect.objectContaining({
|
|
373
|
+
assetId: 'asset-preserved-initiative-doc',
|
|
374
|
+
caption: 'Preserved initiative context',
|
|
375
|
+
})
|
|
376
|
+
]);
|
|
377
|
+
expect(core.getWorkstream(workstream.id, workspaceId)?.attachments).toEqual([
|
|
378
|
+
expect.objectContaining({
|
|
379
|
+
assetId: 'asset-preserved-workstream-doc',
|
|
380
|
+
caption: 'Preserved workstream context',
|
|
381
|
+
})
|
|
382
|
+
]);
|
|
383
|
+
expect(assetStore.listLinksForTarget('initiative', initiative.id, workspaceId)).toEqual([
|
|
384
|
+
expect.objectContaining({
|
|
385
|
+
assetId: 'asset-preserved-initiative-doc',
|
|
386
|
+
deletedAt: null,
|
|
387
|
+
})
|
|
388
|
+
]);
|
|
389
|
+
expect(assetStore.listLinksForTarget('workstream', workstream.id, workspaceId)).toEqual([
|
|
390
|
+
expect.objectContaining({
|
|
391
|
+
assetId: 'asset-preserved-workstream-doc',
|
|
392
|
+
deletedAt: null,
|
|
393
|
+
})
|
|
394
|
+
]);
|
|
395
|
+
core.close();
|
|
396
|
+
});
|
|
397
|
+
it('rejects stale planning sync upserts so older empty snapshots cannot clear newer attachments', () => {
|
|
398
|
+
const core = createCore();
|
|
399
|
+
const workspaceId = 'default';
|
|
400
|
+
const initiative = core.createInitiative({
|
|
401
|
+
title: 'Cloud initiative',
|
|
402
|
+
updatedAt: '2026-01-01T00:10:00.000Z',
|
|
403
|
+
attachments: [{
|
|
404
|
+
path: '/api/taskforce/documents/asset-newer-initiative-doc/content',
|
|
405
|
+
assetId: 'asset-newer-initiative-doc',
|
|
406
|
+
caption: 'Newer initiative doc',
|
|
407
|
+
timestamp: '2026-01-01T00:10:00.000Z',
|
|
408
|
+
}],
|
|
409
|
+
}, workspaceId);
|
|
410
|
+
const workstream = core.createWorkstream({
|
|
411
|
+
title: 'Cloud workstream',
|
|
412
|
+
initiativeId: initiative.id,
|
|
413
|
+
updatedAt: '2026-01-01T00:10:00.000Z',
|
|
414
|
+
attachments: [{
|
|
415
|
+
path: '/api/taskforce/documents/asset-newer-workstream-doc/content',
|
|
416
|
+
assetId: 'asset-newer-workstream-doc',
|
|
417
|
+
caption: 'Newer workstream doc',
|
|
418
|
+
timestamp: '2026-01-01T00:10:00.000Z',
|
|
419
|
+
}],
|
|
420
|
+
}, workspaceId);
|
|
421
|
+
core.upsertInitiativeForSync({
|
|
422
|
+
...initiative,
|
|
423
|
+
title: 'Stale initiative snapshot',
|
|
424
|
+
updatedAt: '2026-01-01T00:05:00.000Z',
|
|
425
|
+
attachments: [],
|
|
426
|
+
}, workspaceId);
|
|
427
|
+
core.upsertWorkstreamForSync({
|
|
428
|
+
...workstream,
|
|
429
|
+
title: 'Stale workstream snapshot',
|
|
430
|
+
updatedAt: '2026-01-01T00:05:00.000Z',
|
|
431
|
+
attachments: [],
|
|
432
|
+
}, workspaceId);
|
|
433
|
+
expect(core.getInitiative(initiative.id, workspaceId)).toEqual(expect.objectContaining({
|
|
434
|
+
title: 'Cloud initiative',
|
|
435
|
+
updatedAt: '2026-01-01T00:10:00.000Z',
|
|
436
|
+
attachments: [
|
|
437
|
+
expect.objectContaining({
|
|
438
|
+
assetId: 'asset-newer-initiative-doc',
|
|
439
|
+
caption: 'Newer initiative doc',
|
|
440
|
+
})
|
|
441
|
+
],
|
|
442
|
+
}));
|
|
443
|
+
expect(core.getWorkstream(workstream.id, workspaceId)).toEqual(expect.objectContaining({
|
|
444
|
+
title: 'Cloud workstream',
|
|
445
|
+
updatedAt: '2026-01-01T00:10:00.000Z',
|
|
446
|
+
attachments: [
|
|
447
|
+
expect.objectContaining({
|
|
448
|
+
assetId: 'asset-newer-workstream-doc',
|
|
449
|
+
caption: 'Newer workstream doc',
|
|
450
|
+
})
|
|
451
|
+
],
|
|
452
|
+
}));
|
|
453
|
+
core.close();
|
|
454
|
+
});
|
|
95
455
|
it('builds lightweight planning bootstrap summaries without hydrating full task details', () => {
|
|
96
456
|
const core = createCore();
|
|
97
457
|
const initiative = core.createInitiative({ title: 'Launch initiative' });
|
|
@@ -127,6 +487,8 @@ describe('TaskforceCore planning entity persistence', () => {
|
|
|
127
487
|
const billingSummary = bootstrap.workstreamTaskSummaries.find((entry) => entry.workstreamId === billingWorkstream.id);
|
|
128
488
|
expect(bootstrap.initiatives.map((entry) => entry.id)).toEqual([initiative.id]);
|
|
129
489
|
expect(bootstrap.workstreams.map((entry) => entry.id)).toEqual([deliveryWorkstream.id, billingWorkstream.id]);
|
|
490
|
+
expect(bootstrap.initiatives.every((entry) => entry.activity === undefined && entry.entityEvents === undefined)).toBe(true);
|
|
491
|
+
expect(bootstrap.workstreams.every((entry) => entry.activity === undefined && entry.entityEvents === undefined)).toBe(true);
|
|
130
492
|
expect(deliverySummary).toMatchObject({
|
|
131
493
|
workstreamId: deliveryWorkstream.id,
|
|
132
494
|
taskCount: 2,
|