@taskforcehq/taskforce 0.3.306 → 0.3.308
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -2
- package/dist/Taskforce.module.css +317 -27
- package/dist/TaskforceCore.js +1 -0
- package/dist/TaskforceCore.test.js +178 -0
- package/dist/components/activity/EntityActivityTimeline.d.ts +34 -0
- package/dist/components/activity/EntityActivityTimeline.js +81 -0
- package/dist/components/context/ContextAttachmentManager.js +15 -3
- package/dist/components/features/AgentsModule.d.ts +5 -1
- package/dist/components/features/AgentsModule.js +142 -39
- package/dist/components/features/AgentsModule.test.js +260 -11
- package/dist/components/features/AnnotatedAttachmentWorkspace.d.ts +3 -1
- package/dist/components/features/AnnotatedAttachmentWorkspace.js +118 -4
- package/dist/components/features/AnnotatedAttachmentWorkspace.test.js +52 -0
- package/dist/components/features/DocumentIndex.d.ts +2 -2
- package/dist/components/features/DocumentIndex.js +3 -4
- package/dist/components/features/DocumentViewer.d.ts +3 -1
- package/dist/components/features/DocumentViewer.js +6 -5
- package/dist/components/features/DocumentWorkspace.d.ts +10 -2
- package/dist/components/features/DocumentWorkspace.js +15 -4
- package/dist/components/features/DocumentWorkspace.test.js +20 -3
- package/dist/components/features/TaskSettings.d.ts +2 -0
- package/dist/components/features/TaskSettings.js +83 -78
- package/dist/components/features/TaskSettings.test.js +171 -82
- package/dist/components/task/TaskActionHeader.js +11 -5
- package/dist/components/task/TaskActionHeader.test.js +10 -0
- package/dist/components/task/TaskCard.js +6 -2
- package/dist/components/task/TaskContextUpload.test.js +3 -3
- package/dist/components/task/TaskForm.js +14 -64
- package/dist/components/task/TaskKanban.js +0 -8
- package/dist/components/task/TaskList.js +5 -1
- package/dist/components/task/TaskList.test.js +4 -0
- package/dist/components/ui/HierarchyActionBadge.d.ts +10 -0
- package/dist/components/ui/HierarchyActionBadge.js +5 -0
- package/dist/components/views/AppShellHeader.d.ts +3 -1
- package/dist/components/views/AppShellHeader.js +27 -11
- package/dist/components/views/AppShellHeader.test.js +18 -1
- package/dist/components/views/PlanComparisonPage.d.ts +20 -15
- package/dist/components/views/PlanComparisonPage.js +221 -82
- package/dist/components/views/PlanComparisonPage.test.js +268 -14
- package/dist/components/views/PlansPage.js +16 -10
- package/dist/components/views/PlansPage.test.js +4 -2
- package/dist/components/views/StandaloneLayout.js +256 -69
- package/dist/components/views/WidgetView.js +10 -8
- package/dist/components/views/panels/PlanningDrawer.d.ts +19 -6
- package/dist/components/views/panels/PlanningDrawer.js +259 -51
- package/dist/components/views/panels/PlanningDrawer.test.js +392 -4
- package/dist/config/envSchema.js +1 -0
- package/dist/core/AiProfileService.d.ts +7 -0
- package/dist/core/AiProfileService.js +460 -35
- package/dist/core/AiProfiles.test.js +648 -2
- package/dist/core/AttachmentLinkService.d.ts +3 -0
- package/dist/core/AttachmentLinkService.js +58 -13
- package/dist/core/GlobalSettingsService.js +30 -0
- package/dist/core/GlobalSettingsService.test.d.ts +1 -0
- package/dist/core/GlobalSettingsService.test.js +45 -0
- package/dist/core/McpTokenService.d.ts +12 -0
- package/dist/core/McpTokenService.js +44 -1
- package/dist/core/PlanEntitlementService.d.ts +20 -0
- package/dist/core/PlanEntitlementService.js +215 -19
- package/dist/core/PlanFeatureCatalog.test.js +55 -0
- package/dist/core/PlanningEntities.test.js +366 -4
- package/dist/core/TaskActivityService.d.ts +28 -1
- package/dist/core/TaskActivityService.js +227 -46
- package/dist/core/TaskActivitySync.test.d.ts +1 -0
- package/dist/core/TaskActivitySync.test.js +90 -0
- package/dist/core/TaskAuditTimeline.test.js +96 -0
- package/dist/core/Taskforce.d.ts +204 -26
- package/dist/core/Taskforce.ids.test.js +58 -0
- package/dist/core/Taskforce.js +781 -71
- package/dist/core/Taskforce.mcpAuth.test.js +109 -0
- package/dist/core/types.d.ts +10 -0
- package/dist/hooks/auth/useTaskforceAuthBootstrap.js +2 -1
- package/dist/hooks/bootstrapTimeouts.d.ts +4 -0
- package/dist/hooks/bootstrapTimeouts.js +8 -0
- package/dist/hooks/bootstrapTimeouts.test.d.ts +1 -0
- package/dist/hooks/bootstrapTimeouts.test.js +12 -0
- package/dist/hooks/sync/orchestratorShared.d.ts +3 -3
- package/dist/hooks/sync/orchestratorShared.js +61 -1
- package/dist/hooks/sync/recovery.js +8 -0
- package/dist/hooks/sync/recovery.test.js +5 -5
- package/dist/hooks/sync/transfers.d.ts +2 -1
- package/dist/hooks/sync/transfers.js +36 -9
- package/dist/hooks/sync/transfers.test.js +534 -2
- package/dist/hooks/useFilterSort.js +13 -13
- package/dist/hooks/useFilterSort.test.js +87 -1
- package/dist/hooks/useSyncOrchestrator.js +50 -5
- package/dist/hooks/useTaskforce.d.ts +85 -50
- package/dist/hooks/useTaskforce.js +44 -14
- package/dist/hooks/useTaskforce.runtime-routing.test.js +94 -0
- package/dist/hooks/useTaskforce.sync-behavior.test.js +99 -0
- package/dist/hooks/useWorkspaceSyncController.js +7 -1
- package/dist/hooks/useWorkspaceSyncController.test.js +48 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +14 -7
- package/dist/mcp/canonicalAssetHelpers.d.ts +16 -4
- package/dist/mcp/canonicalAssetHelpers.js +18 -10
- package/dist/mcp/canonicalAssetHelpers.test.d.ts +1 -0
- package/dist/mcp/canonicalAssetHelpers.test.js +67 -0
- package/dist/mcp/clientIntegrations.d.ts +1 -0
- package/dist/mcp/clientIntegrations.js +25 -0
- package/dist/mcp/clientIntegrations.test.js +14 -3
- package/dist/mcp/clientProfileDefaults.js +3 -3
- package/dist/mcp/collaborationRegistrar.js +7 -7
- package/dist/mcp/documentAssetRegistrar.js +113 -7
- package/dist/mcp/runtime.d.ts +1 -0
- package/dist/mcp/runtime.js +969 -122
- package/dist/mcp/runtime.test.js +1083 -11
- package/dist/mcp/taskAttachmentTypes.d.ts +1 -1
- package/dist/mcp/taskPlanningRegistrar.js +48 -0
- package/dist/mcp/toolRegistry.d.ts +56 -0
- package/dist/mcp/toolRegistry.js +8 -0
- package/dist/mcp/toolRegistry.test.js +4 -0
- package/dist/migrations/taskSchemaMigrations.js +58 -15
- package/dist/server/aiProfileAvatarAssets.d.ts +49 -0
- package/dist/server/aiProfileAvatarAssets.js +319 -0
- package/dist/server/aiProfileAvatarAssets.test.d.ts +1 -0
- package/dist/server/aiProfileAvatarAssets.test.js +213 -0
- package/dist/server/annotatedAttachmentsRoutes.test.js +66 -7
- package/dist/server/index.js +4 -0
- package/dist/server/index.test.js +6 -1
- package/dist/server/routes/admin.js +328 -9
- package/dist/server/routes/annotatedAttachments.js +51 -0
- package/dist/server/routes/auth.d.ts +11 -0
- package/dist/server/routes/auth.js +189 -27
- package/dist/server/routes/authSupport.d.ts +5 -0
- package/dist/server/routes/authSupport.js +13 -1
- package/dist/server/routes/billing.d.ts +1 -0
- package/dist/server/routes/billing.js +116 -63
- package/dist/server/routes/billing.test.js +162 -11
- package/dist/server/routes/documents.js +10 -0
- package/dist/server/routes/shared.js +0 -10
- package/dist/server/routes/sync.integration.test.js +221 -0
- package/dist/server/routes/sync.js +13 -3
- package/dist/server/routes/syncSnapshotRoutes.js +2 -0
- package/dist/server/routes/tasks.js +12 -9
- package/dist/server/routes.js +87 -8
- package/dist/server/routes.test.js +972 -23
- package/dist/shared/aiProfileSurfaceType.d.ts +1 -1
- package/dist/shared/aiProfileSurfaceType.js +10 -8
- package/dist/storage/objectStorage.d.ts +1 -0
- package/dist/storage/objectStorage.js +2 -1
- package/dist/storage/objectStorageClient.d.ts +1 -0
- package/dist/storage/objectStorageClient.js +41 -8
- package/dist/storage/objectStorageClient.test.js +26 -1
- package/dist/storage/objectStorageConfig.js +3 -1
- package/dist/storage/objectStorageConfig.test.js +13 -1
- package/dist/sync/collaborationSyncPayload.js +2 -0
- package/dist/sync/collaborationSyncPayload.test.js +1 -0
- package/dist/sync/collaborationSyncState.d.ts +3 -3
- package/dist/sync/collaborationSyncState.js +19 -12
- package/dist/sync/contentSyncPayload.d.ts +5 -1
- package/dist/sync/contentSyncPayload.js +9 -0
- package/dist/sync/contentSyncPayload.test.js +39 -0
- package/dist/sync/contentSyncState.d.ts +5 -1
- package/dist/sync/contentSyncState.js +231 -37
- package/dist/sync/planningSyncPayload.js +5 -0
- package/dist/sync/planningSyncPayload.test.js +24 -0
- package/dist/sync/syncApplyHandlers.d.ts +4 -4
- package/dist/sync/syncApplyHandlers.js +89 -31
- package/dist/sync/syncApplyHandlers.test.js +184 -4
- package/dist/sync/syncResourceAdapters.d.ts +2 -0
- package/dist/sync/syncResourceAdapters.js +2 -0
- package/dist/sync/syncService.d.ts +1 -0
- package/dist/sync/syncService.js +13 -1
- package/dist/sync/syncService.test.js +39 -0
- package/dist/sync/workspacePullFeed.d.ts +4 -0
- package/dist/sync/workspacePullFeed.js +129 -70
- package/dist/sync/workspacePullFeed.test.js +49 -0
- package/dist/sync/workspaceSyncModel.d.ts +28 -6
- package/dist/sync/workspaceSyncModel.js +159 -45
- package/dist/sync/workspaceSyncModel.test.js +269 -3
- package/dist/types.d.ts +33 -2
- package/dist/ui/agent-logos/ChatGPT.png +0 -0
- package/dist/ui/agent-logos/Gemini CLI.jpeg +0 -0
- package/dist/ui/agent-logos/antigravity.jpeg +0 -0
- package/dist/ui/agent-logos/claude-code.jpeg +0 -0
- package/dist/ui/agent-logos/codex.jpeg +0 -0
- package/dist/ui/agent-logos/cursor.png +0 -0
- package/dist/ui/agent-logos/openclaw.jpeg +0 -0
- package/dist/ui/agent-logos/windsurf.png +0 -0
- package/dist/ui/assets/AgentsModule-BS4Pi_nC.js +1 -0
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-BVKew-mH.css +1 -0
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-CCckzWYZ.js +3 -0
- package/dist/ui/assets/ContextAttachmentManager-CfG_ER7C.js +3 -0
- package/dist/ui/assets/DocumentWorkspace-BVaWPA25.js +252 -0
- package/dist/ui/assets/DocumentWorkspace-D4rJKXzd.css +1 -0
- package/dist/ui/assets/EntityActivityTimeline-DSj0ThaK.js +1 -0
- package/dist/ui/assets/EntityActivityTimeline-DyqvvhuF.css +1 -0
- package/dist/ui/assets/{InitiativesModule-9E6ParrY.js → InitiativesModule-AxMkWtxH.js} +1 -1
- package/dist/ui/assets/PlansPage-B_z-D6Nh.css +1 -0
- package/dist/ui/assets/PlansPage-Ndt7mYfo.js +1 -0
- package/dist/ui/assets/TaskContextUpload-Dw4rTF4i.js +1 -0
- package/dist/ui/assets/TaskSettings-f2ga42_V.js +9 -0
- package/dist/ui/assets/{WorkflowsModule-BA7jcEpH.js → WorkflowsModule-E4UL3mov.js} +4 -4
- package/dist/ui/assets/documentReferences-BOUcJm6-.js +1 -0
- package/dist/ui/assets/index-BvAbqx5Q.css +1 -0
- package/dist/ui/assets/index-MAHKvFqp.js +4 -0
- package/dist/ui/assets/{vendor-icons-DuEd_65c.js → vendor-icons-B32hGuF2.js} +1 -1
- package/dist/ui/index.html +3 -3
- package/dist/ui/og-image.png +0 -0
- package/dist/ui/product/agent manager.png +0 -0
- package/dist/ui/product/agent_manager_02.png +0 -0
- package/dist/utils/aiProfileEvents.d.ts +7 -0
- package/dist/utils/aiProfileEvents.js +6 -0
- package/dist/utils/assignees.d.ts +6 -0
- package/dist/utils/assignees.js +8 -1
- package/dist/utils/assignees.test.d.ts +1 -0
- package/dist/utils/assignees.test.js +25 -0
- package/dist/utils/avatarDisplay.d.ts +1 -0
- package/dist/utils/avatarDisplay.js +15 -0
- package/dist/utils/contextFiles.d.ts +3 -1
- package/dist/utils/contextFiles.js +2 -1
- package/dist/utils/taskActivity.d.ts +7 -1
- package/dist/utils/taskActivity.js +63 -1
- package/dist/utils/taskActivity.test.js +23 -1
- package/dist/utils/taskSearch.d.ts +22 -0
- package/dist/utils/taskSearch.js +87 -0
- package/dist/utils/taskSearch.test.d.ts +1 -0
- package/dist/utils/taskSearch.test.js +69 -0
- package/dist/utils/taskUtils.js +10 -3
- package/dist/utils/taskUtils.test.d.ts +1 -0
- package/dist/utils/taskUtils.test.js +19 -0
- package/package.json +1 -1
- package/dist/ui/assets/AgentsModule-DrLawwNl.js +0 -1
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-BaS2VwIr.css +0 -1
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-Z9_whf7F.js +0 -3
- package/dist/ui/assets/DocumentWorkspace-B03MaSkw.js +0 -252
- package/dist/ui/assets/DocumentWorkspace-Dhdso07p.css +0 -1
- package/dist/ui/assets/PlansPage-CvfnMiWq.css +0 -1
- package/dist/ui/assets/PlansPage-DRXscYxH.js +0 -1
- package/dist/ui/assets/TaskSettings-DZX4jk7e.js +0 -9
- package/dist/ui/assets/index-9eT8e0Lu.css +0 -1
- package/dist/ui/assets/index-C6k75jr8.js +0 -6
|
@@ -194,7 +194,7 @@ export function classifyWorkspaceSyncChanges(input) {
|
|
|
194
194
|
model.collaboration.incomingAnnotatedAttachmentSessionDeletes.set(deletedSession.id, deletedSession.deletedAt);
|
|
195
195
|
continue;
|
|
196
196
|
}
|
|
197
|
-
if (op === 'task-event-upsert') {
|
|
197
|
+
if (op === 'task-event-upsert' || op === 'entity-event-upsert') {
|
|
198
198
|
const event = normalizeTaskEventSyncPayload(raw.event, workspaceId, String(raw.updatedAt || raw.watermark || '').trim());
|
|
199
199
|
if (event)
|
|
200
200
|
model.taskActivity.incomingTaskEvents.set(event.id, event);
|
|
@@ -295,6 +295,7 @@ export function applyPlanningSyncChanges(input) {
|
|
|
295
295
|
createdAt: initiative.createdAt,
|
|
296
296
|
updatedAt: initiative.updatedAt,
|
|
297
297
|
order: initiative.order,
|
|
298
|
+
attachments: initiative.attachments,
|
|
298
299
|
isArchived: initiative.isArchived
|
|
299
300
|
}, workspaceId);
|
|
300
301
|
}
|
|
@@ -322,6 +323,7 @@ export function applyPlanningSyncChanges(input) {
|
|
|
322
323
|
createdAt: workstream.createdAt,
|
|
323
324
|
updatedAt: workstream.updatedAt,
|
|
324
325
|
order: workstream.order,
|
|
326
|
+
attachments: workstream.attachments,
|
|
325
327
|
isArchived: workstream.isArchived
|
|
326
328
|
}, workspaceId);
|
|
327
329
|
}
|
|
@@ -384,6 +386,8 @@ export function applyAiProfileAndMemberSyncChanges(input) {
|
|
|
384
386
|
color: profile.color,
|
|
385
387
|
...(Object.prototype.hasOwnProperty.call(profile, 'avatarUrl') ? { avatarUrl: profile.avatarUrl ?? null } : {}),
|
|
386
388
|
...(Object.prototype.hasOwnProperty.call(profile, 'avatarSourceUrl') ? { avatarSourceUrl: profile.avatarSourceUrl ?? null } : {}),
|
|
389
|
+
...(Object.prototype.hasOwnProperty.call(profile, 'avatarRevision') ? { avatarRevision: profile.avatarRevision ?? 0 } : {}),
|
|
390
|
+
avatarUpdatedAt: profile.avatarUpdatedAt ?? null,
|
|
387
391
|
description: profile.description ?? null,
|
|
388
392
|
role: profile.role ?? null,
|
|
389
393
|
provider: profile.provider ?? null,
|
|
@@ -438,6 +442,20 @@ export function applyAiProfileAndMemberSyncChanges(input) {
|
|
|
438
442
|
}
|
|
439
443
|
export async function applyContentSyncChanges(input) {
|
|
440
444
|
const { core, workspaceId, incomingDocuments, incomingDocumentDeletes, incomingAssets, incomingAssetDeletes, basePath, hasObjectStorageClient, hasDocumentSyncMetadataDrift, hasBinaryAssetSyncMetadataDrift, getExistingDocument, writeWorkspaceSyncDocument, deleteWorkspaceSyncDocument, writeWorkspaceSyncBinaryAsset, deleteWorkspaceSyncBinaryAsset, getExistingBinaryAsset, recordSyncDiagnostic, options, } = input;
|
|
445
|
+
const contentApplyFailures = [];
|
|
446
|
+
const recordContentApplyFailure = (failure) => {
|
|
447
|
+
contentApplyFailures.push(failure);
|
|
448
|
+
};
|
|
449
|
+
const throwIfContentApplyFailed = () => {
|
|
450
|
+
if (contentApplyFailures.length === 0)
|
|
451
|
+
return;
|
|
452
|
+
const first = contentApplyFailures[0];
|
|
453
|
+
const error = new Error(`Failed to apply ${contentApplyFailures.length} synced content change${contentApplyFailures.length === 1 ? '' : 's'}; first failure: ${first.kind} ${first.path}: ${first.error}`);
|
|
454
|
+
error.statusCode = 500;
|
|
455
|
+
error.code = 'SYNC_CONTENT_APPLY_FAILED';
|
|
456
|
+
error.details = { failures: contentApplyFailures.slice(0, 10) };
|
|
457
|
+
throw error;
|
|
458
|
+
};
|
|
441
459
|
if (incomingDocuments.length > 0) {
|
|
442
460
|
for (const doc of incomingDocuments) {
|
|
443
461
|
try {
|
|
@@ -448,17 +466,17 @@ export async function applyContentSyncChanges(input) {
|
|
|
448
466
|
const incomingHash = createHash('sha256').update(String(doc.content || '')).digest('hex');
|
|
449
467
|
const existingUpdatedAt = String(existingDoc?.updatedAt || existingDocFallback?.updatedAt || '').trim();
|
|
450
468
|
let existingHash = String(existingDoc?.contentHash || existingDocFallback?.contentHash || '').trim();
|
|
451
|
-
|
|
469
|
+
const metadataDrift = hasDocumentSyncMetadataDrift(workspaceId, doc, options);
|
|
470
|
+
if (!hasObjectStorageClient() && (!existingHash || metadataDrift)) {
|
|
452
471
|
const localDocumentPath = path.resolve(basePath, doc.path);
|
|
453
472
|
if (fs.existsSync(localDocumentPath)) {
|
|
454
473
|
existingHash = createHash('sha256').update(fs.readFileSync(localDocumentPath, 'utf8')).digest('hex');
|
|
455
474
|
}
|
|
456
475
|
}
|
|
457
476
|
const compare = resolveSyncConflictOrdering(doc.updatedAt, existingUpdatedAt, incomingHash, existingHash);
|
|
458
|
-
const metadataDrift = hasDocumentSyncMetadataDrift(workspaceId, doc, options);
|
|
459
477
|
if (compare < 0)
|
|
460
478
|
continue;
|
|
461
|
-
if (compare === 0 && existingHash === incomingHash && !metadataDrift)
|
|
479
|
+
if (compare === 0 && existingHash === incomingHash && !metadataDrift && !hasObjectStorageClient())
|
|
462
480
|
continue;
|
|
463
481
|
await writeWorkspaceSyncDocument(basePath, doc, workspaceId, {
|
|
464
482
|
...options,
|
|
@@ -474,6 +492,11 @@ export async function applyContentSyncChanges(input) {
|
|
|
474
492
|
assetId: typeof doc.assetId === 'string' ? doc.assetId : null,
|
|
475
493
|
error: String(error?.message || error || '')
|
|
476
494
|
});
|
|
495
|
+
recordContentApplyFailure({
|
|
496
|
+
kind: 'document',
|
|
497
|
+
path: doc.path,
|
|
498
|
+
error: String(error?.message || error || '')
|
|
499
|
+
});
|
|
477
500
|
}
|
|
478
501
|
}
|
|
479
502
|
}
|
|
@@ -497,14 +520,14 @@ export async function applyContentSyncChanges(input) {
|
|
|
497
520
|
const localFileMissing = !hasObjectStorageClient() && !fs.existsSync(localAssetPath);
|
|
498
521
|
const existingUpdatedAt = String(existing?.updatedAt || '').trim();
|
|
499
522
|
let existingHash = String(existing?.contentSha256 || '').trim();
|
|
500
|
-
|
|
523
|
+
const metadataDrift = hasBinaryAssetSyncMetadataDrift(workspaceId, asset, options);
|
|
524
|
+
if (!hasObjectStorageClient() && !localFileMissing && (!existingHash || metadataDrift)) {
|
|
501
525
|
existingHash = createHash('sha256').update(fs.readFileSync(localAssetPath)).digest('hex');
|
|
502
526
|
}
|
|
503
527
|
const compare = resolveSyncConflictOrdering(asset.updatedAt, existingUpdatedAt, incomingHash, existingHash);
|
|
504
|
-
const metadataDrift = hasBinaryAssetSyncMetadataDrift(workspaceId, asset, options);
|
|
505
528
|
if (compare < 0)
|
|
506
529
|
continue;
|
|
507
|
-
if (compare === 0 && existingHash === incomingHash && !localFileMissing && !metadataDrift)
|
|
530
|
+
if (compare === 0 && existingHash === incomingHash && !localFileMissing && !metadataDrift && !hasObjectStorageClient())
|
|
508
531
|
continue;
|
|
509
532
|
await writeWorkspaceSyncBinaryAsset(basePath, asset, workspaceId, {
|
|
510
533
|
...options,
|
|
@@ -522,9 +545,15 @@ export async function applyContentSyncChanges(input) {
|
|
|
522
545
|
kind: asset.kind,
|
|
523
546
|
error: String(error?.message || error || '')
|
|
524
547
|
});
|
|
548
|
+
recordContentApplyFailure({
|
|
549
|
+
kind: 'asset',
|
|
550
|
+
path: asset.path,
|
|
551
|
+
error: String(error?.message || error || '')
|
|
552
|
+
});
|
|
525
553
|
}
|
|
526
554
|
}
|
|
527
555
|
}
|
|
556
|
+
throwIfContentApplyFailed();
|
|
528
557
|
if (incomingAssetDeletes.size > 0) {
|
|
529
558
|
for (const [assetPath, deletedAt] of incomingAssetDeletes.entries()) {
|
|
530
559
|
const existing = getExistingBinaryAsset(assetPath, workspaceId);
|
|
@@ -760,22 +789,32 @@ export function applyTaskActivitySyncChanges(input) {
|
|
|
760
789
|
return;
|
|
761
790
|
for (const event of incomingTaskEvents.values()) {
|
|
762
791
|
try {
|
|
763
|
-
const
|
|
764
|
-
|
|
792
|
+
const entityType = event.entityType === 'initiative' || event.entityType === 'workstream' ? event.entityType : 'task';
|
|
793
|
+
const entityId = String(event.entityId || event.taskId || '').trim();
|
|
794
|
+
if (!entityId)
|
|
765
795
|
continue;
|
|
766
|
-
const
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
796
|
+
const entityExists = entityType === 'task'
|
|
797
|
+
? core.taskExistsForSync(entityId, workspaceId)
|
|
798
|
+
: entityType === 'initiative'
|
|
799
|
+
? core.initiativeExistsForSync(entityId, workspaceId)
|
|
800
|
+
: core.workstreamExistsForSync(entityId, workspaceId);
|
|
801
|
+
if (!entityExists) {
|
|
802
|
+
recordSyncDiagnostic(workspaceId, 'apply', 'Skipped entity event during sync apply because the entity does not exist locally.', {
|
|
803
|
+
source: 'apply.entityEventMissingEntity',
|
|
770
804
|
eventId: event.id,
|
|
771
|
-
|
|
805
|
+
entityType,
|
|
806
|
+
entityId,
|
|
772
807
|
});
|
|
773
808
|
continue;
|
|
774
809
|
}
|
|
775
|
-
const existing = core.
|
|
810
|
+
const existing = core.getEntityEventForSync({
|
|
811
|
+
workspaceId,
|
|
812
|
+
eventId: event.id,
|
|
813
|
+
});
|
|
776
814
|
const incomingTieBreak = createHash('sha256')
|
|
777
815
|
.update(JSON.stringify({
|
|
778
|
-
|
|
816
|
+
entityType,
|
|
817
|
+
entityId,
|
|
779
818
|
action: event.action,
|
|
780
819
|
actor: event.actor,
|
|
781
820
|
actorType: event.actorType,
|
|
@@ -785,7 +824,8 @@ export function applyTaskActivitySyncChanges(input) {
|
|
|
785
824
|
const existingTieBreak = existing
|
|
786
825
|
? createHash('sha256')
|
|
787
826
|
.update(JSON.stringify({
|
|
788
|
-
|
|
827
|
+
entityType: existing.entityType,
|
|
828
|
+
entityId: existing.entityId || ('taskId' in existing ? existing.taskId : ''),
|
|
789
829
|
action: existing.action,
|
|
790
830
|
actor: existing.actor,
|
|
791
831
|
actorType: existing.actorType,
|
|
@@ -796,27 +836,45 @@ export function applyTaskActivitySyncChanges(input) {
|
|
|
796
836
|
const compare = resolveSyncConflictOrdering(event.createdAt, String(existing?.createdAt || '').trim(), incomingTieBreak, existingTieBreak);
|
|
797
837
|
if (compare < 0)
|
|
798
838
|
continue;
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
839
|
+
if (entityType === 'task') {
|
|
840
|
+
core.upsertEntityEventForSync({
|
|
841
|
+
id: event.id,
|
|
842
|
+
workspaceId,
|
|
843
|
+
entityType: 'task',
|
|
844
|
+
entityId,
|
|
845
|
+
taskId: String(event.taskId || entityId).trim(),
|
|
846
|
+
action: event.action,
|
|
847
|
+
actor: event.actor,
|
|
848
|
+
actorType: event.actorType,
|
|
849
|
+
details: event.details,
|
|
850
|
+
createdAt: event.createdAt,
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
else {
|
|
854
|
+
core.upsertEntityEventForSync({
|
|
855
|
+
id: event.id,
|
|
856
|
+
workspaceId,
|
|
857
|
+
entityType,
|
|
858
|
+
entityId,
|
|
859
|
+
action: event.action,
|
|
860
|
+
actor: event.actor,
|
|
861
|
+
actorType: event.actorType,
|
|
862
|
+
details: event.details,
|
|
863
|
+
createdAt: event.createdAt,
|
|
864
|
+
});
|
|
865
|
+
}
|
|
809
866
|
}
|
|
810
867
|
catch (error) {
|
|
811
868
|
const details = {
|
|
812
|
-
source: 'apply.
|
|
869
|
+
source: 'apply.entityEventUpsert',
|
|
813
870
|
eventId: event.id,
|
|
814
|
-
|
|
871
|
+
entityType: event.entityType,
|
|
872
|
+
entityId: event.entityId || event.taskId,
|
|
815
873
|
error: String(error?.message || error || ''),
|
|
816
874
|
errorCode: String(error?.code || '').trim() || null
|
|
817
875
|
};
|
|
818
|
-
recordSyncDiagnostic(workspaceId, 'apply', 'Failed to upsert
|
|
819
|
-
throw createSyncRouteError(`Failed to persist
|
|
876
|
+
recordSyncDiagnostic(workspaceId, 'apply', 'Failed to upsert entity event during sync apply.', details);
|
|
877
|
+
throw createSyncRouteError(`Failed to persist entity event ${event.id} during sync apply: ${String(error?.message || error || 'Unknown error')}`, details, { code: 'TASK_EVENT_SYNC_APPLY_FAILED', statusCode: 500 });
|
|
820
878
|
}
|
|
821
879
|
}
|
|
822
880
|
}
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import { describe, expect, it, vi, afterEach } from 'vitest';
|
|
4
|
-
import { applyCollaborationSyncChanges, applyContentSyncChanges, applyPreparedTaskSyncSnapshot, applyTaskSyncDeletes, classifyWorkspaceSyncChanges, prepareTaskSyncSnapshotApply, } from './syncApplyHandlers.js';
|
|
4
|
+
import { applyCollaborationSyncChanges, applyContentSyncChanges, applyPlanningSyncChanges, applyPreparedTaskSyncSnapshot, applyTaskActivitySyncChanges, applyTaskSyncDeletes, classifyWorkspaceSyncChanges, prepareTaskSyncSnapshotApply, } from './syncApplyHandlers.js';
|
|
5
5
|
function createCoreFixture(overrides = {}) {
|
|
6
6
|
return {
|
|
7
7
|
resolveTaskIdentifier: vi.fn().mockReturnValue(null),
|
|
8
8
|
getTask: vi.fn().mockReturnValue(null),
|
|
9
|
+
taskExistsForSync: vi.fn().mockReturnValue(true),
|
|
10
|
+
initiativeExistsForSync: vi.fn().mockReturnValue(true),
|
|
11
|
+
workstreamExistsForSync: vi.fn().mockReturnValue(true),
|
|
12
|
+
getTaskEventForSync: vi.fn().mockReturnValue(null),
|
|
13
|
+
getEntityEventForSync: vi.fn().mockReturnValue(null),
|
|
9
14
|
deleteTaskForSync: vi.fn(),
|
|
10
15
|
applyWorkspaceSyncSnapshot: vi.fn().mockReturnValue({
|
|
11
16
|
total: 1,
|
|
@@ -185,6 +190,84 @@ describe('syncApplyHandlers', () => {
|
|
|
185
190
|
}, workspaceId);
|
|
186
191
|
expect(result).toEqual(expect.objectContaining({ total: 1 }));
|
|
187
192
|
});
|
|
193
|
+
it('preserves planning attachments when applying initiative and workstream sync changes', () => {
|
|
194
|
+
const core = createCoreFixture({
|
|
195
|
+
upsertInitiativeForSync: vi.fn(),
|
|
196
|
+
upsertWorkstreamForSync: vi.fn(),
|
|
197
|
+
updateTaxonomies: vi.fn(),
|
|
198
|
+
});
|
|
199
|
+
applyPlanningSyncChanges({
|
|
200
|
+
core,
|
|
201
|
+
workspaceId: 'workspace-1',
|
|
202
|
+
incomingInitiatives: new Map([
|
|
203
|
+
['initiative-1', {
|
|
204
|
+
id: 'initiative-1',
|
|
205
|
+
referenceNumber: 12,
|
|
206
|
+
referenceLabel: 'I-12',
|
|
207
|
+
title: 'Initiative',
|
|
208
|
+
description: 'Initiative description',
|
|
209
|
+
ownerId: null,
|
|
210
|
+
createdAt: '2026-04-10T09:00:00.000Z',
|
|
211
|
+
updatedAt: '2026-04-10T10:00:00.000Z',
|
|
212
|
+
order: 1,
|
|
213
|
+
attachments: [{
|
|
214
|
+
path: '/api/taskforce/documents/asset-init-doc/content',
|
|
215
|
+
fsPath: 'workspaces/workspace-1/assets/documents/initiative.md',
|
|
216
|
+
assetId: 'asset-init-doc',
|
|
217
|
+
caption: 'Initiative context',
|
|
218
|
+
timestamp: '2026-04-10T10:00:00.000Z'
|
|
219
|
+
}],
|
|
220
|
+
isArchived: false,
|
|
221
|
+
}],
|
|
222
|
+
]),
|
|
223
|
+
incomingWorkstreams: new Map([
|
|
224
|
+
['workstream-1', {
|
|
225
|
+
id: 'workstream-1',
|
|
226
|
+
referenceNumber: 689,
|
|
227
|
+
referenceLabel: 'WS-689',
|
|
228
|
+
initiativeId: 'initiative-1',
|
|
229
|
+
title: 'Security Audit Remediation',
|
|
230
|
+
description: 'Workstream description',
|
|
231
|
+
ownerId: null,
|
|
232
|
+
createdAt: '2026-04-10T09:00:00.000Z',
|
|
233
|
+
updatedAt: '2026-04-10T10:00:00.000Z',
|
|
234
|
+
order: 2,
|
|
235
|
+
attachments: [{
|
|
236
|
+
path: '/api/taskforce/documents/asset-workstream-doc/content',
|
|
237
|
+
fsPath: 'workspaces/workspace-1/assets/documents/security-findings-register.md',
|
|
238
|
+
assetId: 'asset-workstream-doc',
|
|
239
|
+
caption: 'Living security findings register for WS-689',
|
|
240
|
+
timestamp: '2026-04-10T10:00:00.000Z'
|
|
241
|
+
}],
|
|
242
|
+
isArchived: false,
|
|
243
|
+
}],
|
|
244
|
+
]),
|
|
245
|
+
incomingTaxonomies: null,
|
|
246
|
+
incomingTaxonomyState: null,
|
|
247
|
+
incomingTaxonomiesUpdatedAt: '',
|
|
248
|
+
workspaceTaxonomySyncMeta: new Map(),
|
|
249
|
+
recordSyncDiagnostic: vi.fn(),
|
|
250
|
+
});
|
|
251
|
+
expect(core.upsertInitiativeForSync).toHaveBeenCalledWith(expect.objectContaining({
|
|
252
|
+
id: 'initiative-1',
|
|
253
|
+
attachments: [
|
|
254
|
+
expect.objectContaining({
|
|
255
|
+
assetId: 'asset-init-doc',
|
|
256
|
+
caption: 'Initiative context'
|
|
257
|
+
})
|
|
258
|
+
]
|
|
259
|
+
}), 'workspace-1');
|
|
260
|
+
expect(core.upsertWorkstreamForSync).toHaveBeenCalledWith(expect.objectContaining({
|
|
261
|
+
id: 'workstream-1',
|
|
262
|
+
referenceLabel: 'WS-689',
|
|
263
|
+
attachments: [
|
|
264
|
+
expect.objectContaining({
|
|
265
|
+
assetId: 'asset-workstream-doc',
|
|
266
|
+
caption: 'Living security findings register for WS-689'
|
|
267
|
+
})
|
|
268
|
+
]
|
|
269
|
+
}), 'workspace-1');
|
|
270
|
+
});
|
|
188
271
|
it('applies document and asset changes through the content handler helpers', async () => {
|
|
189
272
|
const core = createCoreFixture();
|
|
190
273
|
const writeWorkspaceSyncDocument = vi.fn().mockResolvedValue(undefined);
|
|
@@ -289,7 +372,7 @@ describe('syncApplyHandlers', () => {
|
|
|
289
372
|
expect(writeWorkspaceSyncDocument).toHaveBeenCalledWith('/tmp/taskforce-sync-content', expect.objectContaining({ path: 'docs/spec.md' }), 'workspace-1', { allowReferenceNumberReassignment: true });
|
|
290
373
|
expect(writeWorkspaceSyncBinaryAsset).toHaveBeenCalledWith('/tmp/taskforce-sync-content', expect.objectContaining({ path: 'assets/proof.png' }), 'workspace-1', { allowReferenceNumberReassignment: true });
|
|
291
374
|
});
|
|
292
|
-
it('reuses persisted content hashes instead of re-reading unchanged local files', async () => {
|
|
375
|
+
it('reuses persisted content hashes instead of re-reading unchanged local files while reasserting object storage payloads', async () => {
|
|
293
376
|
const existingDocContent = '# Spec';
|
|
294
377
|
const existingAssetBytes = Buffer.from('png-bytes');
|
|
295
378
|
const core = createCoreFixture({
|
|
@@ -347,8 +430,50 @@ describe('syncApplyHandlers', () => {
|
|
|
347
430
|
recordSyncDiagnostic: vi.fn(),
|
|
348
431
|
});
|
|
349
432
|
expect(readSpy).not.toHaveBeenCalled();
|
|
350
|
-
expect(writeWorkspaceSyncDocument).
|
|
351
|
-
expect(writeWorkspaceSyncBinaryAsset).
|
|
433
|
+
expect(writeWorkspaceSyncDocument).toHaveBeenCalledWith('/tmp/taskforce-sync-content', expect.objectContaining({ path: 'docs/spec.md', assetId: 'asset-doc-1' }), 'workspace-1', { allowReferenceNumberReassignment: false });
|
|
434
|
+
expect(writeWorkspaceSyncBinaryAsset).toHaveBeenCalledWith('/tmp/taskforce-sync-content', expect.objectContaining({ path: 'assets/proof.png', assetId: 'asset-image-1' }), 'workspace-1', { allowReferenceNumberReassignment: false });
|
|
435
|
+
});
|
|
436
|
+
it('rejects synced content applies when a binary asset write fails', async () => {
|
|
437
|
+
const core = createCoreFixture();
|
|
438
|
+
const recordSyncDiagnostic = vi.fn();
|
|
439
|
+
await expect(applyContentSyncChanges({
|
|
440
|
+
core,
|
|
441
|
+
workspaceId: 'workspace-1',
|
|
442
|
+
incomingDocuments: [],
|
|
443
|
+
incomingDocumentDeletes: new Map(),
|
|
444
|
+
incomingAssets: [
|
|
445
|
+
{
|
|
446
|
+
path: 'assets/proof.png',
|
|
447
|
+
updatedAt: '2026-04-10T10:05:00.000Z',
|
|
448
|
+
contentBase64: Buffer.from('png-bytes').toString('base64'),
|
|
449
|
+
assetId: 'asset-image-1',
|
|
450
|
+
kind: 'image',
|
|
451
|
+
mimeType: 'image/png',
|
|
452
|
+
},
|
|
453
|
+
],
|
|
454
|
+
incomingAssetDeletes: new Map(),
|
|
455
|
+
basePath: '/tmp/taskforce-sync-content',
|
|
456
|
+
hasObjectStorageClient: () => true,
|
|
457
|
+
hasDocumentSyncMetadataDrift: () => false,
|
|
458
|
+
hasBinaryAssetSyncMetadataDrift: () => false,
|
|
459
|
+
getExistingDocument: () => null,
|
|
460
|
+
writeWorkspaceSyncDocument: vi.fn(),
|
|
461
|
+
deleteWorkspaceSyncDocument: vi.fn(),
|
|
462
|
+
writeWorkspaceSyncBinaryAsset: vi.fn().mockRejectedValue(new Error('R2 write failed')),
|
|
463
|
+
deleteWorkspaceSyncBinaryAsset: vi.fn(),
|
|
464
|
+
getExistingBinaryAsset: () => null,
|
|
465
|
+
recordSyncDiagnostic,
|
|
466
|
+
})).rejects.toMatchObject({
|
|
467
|
+
code: 'SYNC_CONTENT_APPLY_FAILED',
|
|
468
|
+
statusCode: 500,
|
|
469
|
+
});
|
|
470
|
+
expect(recordSyncDiagnostic).toHaveBeenCalledWith('workspace-1', 'apply', 'Failed to apply synced asset change.', expect.objectContaining({
|
|
471
|
+
source: 'assets.apply.write',
|
|
472
|
+
path: 'assets/proof.png',
|
|
473
|
+
assetId: 'asset-image-1',
|
|
474
|
+
kind: 'image',
|
|
475
|
+
error: 'R2 write failed'
|
|
476
|
+
}));
|
|
352
477
|
});
|
|
353
478
|
it('applies collaboration upserts and deletes through the collaboration handlers', () => {
|
|
354
479
|
const core = createCoreFixture();
|
|
@@ -443,4 +568,59 @@ describe('syncApplyHandlers', () => {
|
|
|
443
568
|
deletedAt: '2026-04-10T11:10:00.000Z',
|
|
444
569
|
});
|
|
445
570
|
});
|
|
571
|
+
it('applies entity events through lean sync lookups instead of hydrated entity activity reads', () => {
|
|
572
|
+
const core = createCoreFixture({
|
|
573
|
+
taskExistsForSync: vi.fn().mockReturnValue(true),
|
|
574
|
+
getEntityEventForSync: vi.fn().mockReturnValue(null),
|
|
575
|
+
listTaskEvents: vi.fn(() => {
|
|
576
|
+
throw new Error('hydrated task activity lookup should not run during sync apply');
|
|
577
|
+
}),
|
|
578
|
+
getTask: vi.fn(() => {
|
|
579
|
+
throw new Error('hydrated task lookup should not run during sync apply');
|
|
580
|
+
}),
|
|
581
|
+
getInitiative: vi.fn(() => {
|
|
582
|
+
throw new Error('hydrated initiative lookup should not run during sync apply');
|
|
583
|
+
}),
|
|
584
|
+
getWorkstream: vi.fn(() => {
|
|
585
|
+
throw new Error('hydrated workstream lookup should not run during sync apply');
|
|
586
|
+
}),
|
|
587
|
+
listEntityEventsForSync: vi.fn(() => {
|
|
588
|
+
throw new Error('hydrated entity event listing should not run during sync apply');
|
|
589
|
+
}),
|
|
590
|
+
upsertEntityEventForSync: vi.fn(),
|
|
591
|
+
});
|
|
592
|
+
applyTaskActivitySyncChanges({
|
|
593
|
+
core,
|
|
594
|
+
workspaceId: 'workspace-1',
|
|
595
|
+
incomingTaskEvents: new Map([
|
|
596
|
+
['task-event-1', {
|
|
597
|
+
id: 'task-event-1',
|
|
598
|
+
taskId: 'task-1',
|
|
599
|
+
entityType: 'task',
|
|
600
|
+
entityId: 'task-1',
|
|
601
|
+
workspaceId: 'workspace-1',
|
|
602
|
+
action: 'task-updated',
|
|
603
|
+
actor: 'user-1',
|
|
604
|
+
actorType: 'human',
|
|
605
|
+
details: { changes: { status: { from: 'task', to: 'review' } } },
|
|
606
|
+
createdAt: '2026-04-10T10:00:00.000Z',
|
|
607
|
+
}],
|
|
608
|
+
]),
|
|
609
|
+
recordSyncDiagnostic: vi.fn(),
|
|
610
|
+
createSyncRouteError: (message, details) => Object.assign(new Error(message), { details }),
|
|
611
|
+
});
|
|
612
|
+
expect(core.taskExistsForSync).toHaveBeenCalledWith('task-1', 'workspace-1');
|
|
613
|
+
expect(core.getEntityEventForSync).toHaveBeenCalledWith({
|
|
614
|
+
workspaceId: 'workspace-1',
|
|
615
|
+
eventId: 'task-event-1',
|
|
616
|
+
});
|
|
617
|
+
expect(core.upsertEntityEventForSync).toHaveBeenCalledWith(expect.objectContaining({
|
|
618
|
+
id: 'task-event-1',
|
|
619
|
+
taskId: 'task-1',
|
|
620
|
+
entityType: 'task',
|
|
621
|
+
entityId: 'task-1',
|
|
622
|
+
workspaceId: 'workspace-1',
|
|
623
|
+
action: 'task-updated',
|
|
624
|
+
}));
|
|
625
|
+
});
|
|
446
626
|
});
|
|
@@ -9,6 +9,8 @@ export interface SyncAiProfileLike {
|
|
|
9
9
|
color?: string | null;
|
|
10
10
|
avatarUrl?: string | null;
|
|
11
11
|
avatarSourceUrl?: string | null;
|
|
12
|
+
avatarRevision?: number | null;
|
|
13
|
+
avatarUpdatedAt?: string | null;
|
|
12
14
|
description?: string | null;
|
|
13
15
|
role?: string | null;
|
|
14
16
|
provider?: string | null;
|
|
@@ -26,6 +26,8 @@ export function getAiProfileSyncChangeKey(profile) {
|
|
|
26
26
|
color: String(source.color || '#8b5cf6').trim() || '#8b5cf6',
|
|
27
27
|
avatarUrl: normalizeSyncSameOriginAssetUrl(source.avatarUrl) || '',
|
|
28
28
|
avatarSourceUrl: normalizeSyncSameOriginAssetUrl(source.avatarSourceUrl) || '',
|
|
29
|
+
avatarRevision: Number.isFinite(Number(source.avatarRevision)) ? Math.max(0, Math.floor(Number(source.avatarRevision))) : 0,
|
|
30
|
+
avatarUpdatedAt: normalizeSyncNullableIsoString(source.avatarUpdatedAt) || '',
|
|
29
31
|
description: normalizeSyncNullableString(source.description) || '',
|
|
30
32
|
role: normalizeSyncNullableString(source.role) || '',
|
|
31
33
|
provider: normalizeSyncNullableString(source.provider) || '',
|
|
@@ -126,6 +126,7 @@ export interface WorkspacePullSyncResult {
|
|
|
126
126
|
pulledActiveUpserts: boolean;
|
|
127
127
|
pulledArchivedUpserts: boolean;
|
|
128
128
|
pulledTaskEventUpserts?: boolean;
|
|
129
|
+
pulledPlanningUpserts?: boolean;
|
|
129
130
|
documentDecryptFailures?: string[];
|
|
130
131
|
emittedEventIds?: string[];
|
|
131
132
|
serverDiagnostics?: Record<string, unknown> | null;
|
package/dist/sync/syncService.js
CHANGED
|
@@ -319,6 +319,7 @@ export async function runWorkspacePullSyncService(input) {
|
|
|
319
319
|
let pulledActiveUpserts = false;
|
|
320
320
|
let pulledArchivedUpserts = false;
|
|
321
321
|
let pulledTaskEventUpserts = false;
|
|
322
|
+
let pulledPlanningUpserts = false;
|
|
322
323
|
const documentDecryptFailures = new Set();
|
|
323
324
|
const emittedEventIds = new Set();
|
|
324
325
|
const pulledAnnotatedAttachmentSessionIds = new Set();
|
|
@@ -363,6 +364,7 @@ export async function runWorkspacePullSyncService(input) {
|
|
|
363
364
|
pulledActiveUpserts,
|
|
364
365
|
pulledArchivedUpserts,
|
|
365
366
|
pulledTaskEventUpserts,
|
|
367
|
+
pulledPlanningUpserts,
|
|
366
368
|
documentDecryptFailures: Array.from(documentDecryptFailures),
|
|
367
369
|
serverDiagnostics: null
|
|
368
370
|
};
|
|
@@ -392,10 +394,14 @@ export async function runWorkspacePullSyncService(input) {
|
|
|
392
394
|
pulledAnnotatedAttachmentSessionIds.add(sessionId);
|
|
393
395
|
continue;
|
|
394
396
|
}
|
|
395
|
-
if (change.op === 'task-event-upsert') {
|
|
397
|
+
if (change.op === 'task-event-upsert' || change.op === 'entity-event-upsert') {
|
|
396
398
|
pulledTaskEventUpserts = true;
|
|
397
399
|
continue;
|
|
398
400
|
}
|
|
401
|
+
if (change.op === 'initiative-upsert' || change.op === 'workstream-upsert') {
|
|
402
|
+
pulledPlanningUpserts = true;
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
399
405
|
if (change.op === 'document-upsert') {
|
|
400
406
|
if (typeof change.content === 'string' && typeof process !== 'undefined' && process.env?.TASKFORCE_SYNC_KEY) {
|
|
401
407
|
try {
|
|
@@ -487,6 +493,7 @@ export async function runWorkspacePullSyncService(input) {
|
|
|
487
493
|
pulledActiveUpserts,
|
|
488
494
|
pulledArchivedUpserts,
|
|
489
495
|
pulledTaskEventUpserts,
|
|
496
|
+
pulledPlanningUpserts,
|
|
490
497
|
documentDecryptFailures: Array.from(documentDecryptFailures),
|
|
491
498
|
emittedEventIds: Array.from(emittedEventIds),
|
|
492
499
|
serverDiagnostics: lastServerDiagnostics
|
|
@@ -509,6 +516,7 @@ export async function runWorkspacePullSyncService(input) {
|
|
|
509
516
|
pulledActiveUpserts,
|
|
510
517
|
pulledArchivedUpserts,
|
|
511
518
|
pulledTaskEventUpserts,
|
|
519
|
+
pulledPlanningUpserts,
|
|
512
520
|
documentDecryptFailures: Array.from(documentDecryptFailures),
|
|
513
521
|
emittedEventIds: Array.from(emittedEventIds),
|
|
514
522
|
serverDiagnostics: lastServerDiagnostics
|
|
@@ -560,6 +568,7 @@ export async function runWorkspacePullSyncService(input) {
|
|
|
560
568
|
pulledActiveUpserts,
|
|
561
569
|
pulledArchivedUpserts,
|
|
562
570
|
pulledTaskEventUpserts,
|
|
571
|
+
pulledPlanningUpserts,
|
|
563
572
|
documentDecryptFailures: Array.from(documentDecryptFailures),
|
|
564
573
|
emittedEventIds: Array.from(emittedEventIds),
|
|
565
574
|
serverDiagnostics: lastServerDiagnostics
|
|
@@ -589,6 +598,7 @@ export async function runWorkspacePullSyncService(input) {
|
|
|
589
598
|
pulledActiveUpserts,
|
|
590
599
|
pulledArchivedUpserts,
|
|
591
600
|
pulledTaskEventUpserts,
|
|
601
|
+
pulledPlanningUpserts,
|
|
592
602
|
documentDecryptFailures: Array.from(documentDecryptFailures),
|
|
593
603
|
emittedEventIds: Array.from(emittedEventIds),
|
|
594
604
|
serverDiagnostics: lastServerDiagnostics
|
|
@@ -608,6 +618,7 @@ export async function runWorkspacePullSyncService(input) {
|
|
|
608
618
|
pulledActiveUpserts,
|
|
609
619
|
pulledArchivedUpserts,
|
|
610
620
|
pulledTaskEventUpserts,
|
|
621
|
+
pulledPlanningUpserts,
|
|
611
622
|
documentDecryptFailures: Array.from(documentDecryptFailures),
|
|
612
623
|
emittedEventIds: Array.from(emittedEventIds),
|
|
613
624
|
serverDiagnostics: lastServerDiagnostics
|
|
@@ -629,6 +640,7 @@ export async function runWorkspacePullSyncService(input) {
|
|
|
629
640
|
pulledActiveUpserts,
|
|
630
641
|
pulledArchivedUpserts,
|
|
631
642
|
pulledTaskEventUpserts,
|
|
643
|
+
pulledPlanningUpserts,
|
|
632
644
|
documentDecryptFailures: Array.from(documentDecryptFailures),
|
|
633
645
|
emittedEventIds: Array.from(emittedEventIds),
|
|
634
646
|
serverDiagnostics: null
|
|
@@ -548,6 +548,45 @@ describe('syncService', () => {
|
|
|
548
548
|
expect(res.pulledActiveUpserts).toBe(false);
|
|
549
549
|
expect(res.pulledArchivedUpserts).toBe(false);
|
|
550
550
|
});
|
|
551
|
+
it('marks planning upserts when the pull contains initiative or workstream changes', async () => {
|
|
552
|
+
vi.mocked(pullWorkspaceChanges).mockResolvedValueOnce({
|
|
553
|
+
ok: true,
|
|
554
|
+
status: 200,
|
|
555
|
+
data: {
|
|
556
|
+
success: true,
|
|
557
|
+
changes: [{
|
|
558
|
+
op: 'workstream-upsert',
|
|
559
|
+
updatedAt: '2026-04-27T22:00:00.000Z',
|
|
560
|
+
workstream: {
|
|
561
|
+
id: 'ws-123',
|
|
562
|
+
title: 'Planning sync workstream',
|
|
563
|
+
description: 'Contains planning attachments',
|
|
564
|
+
createdAt: '2026-04-27T21:00:00.000Z',
|
|
565
|
+
updatedAt: '2026-04-27T22:00:00.000Z',
|
|
566
|
+
attachments: []
|
|
567
|
+
}
|
|
568
|
+
}],
|
|
569
|
+
nextCursor: null,
|
|
570
|
+
hasMore: false
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
vi.mocked(applyLocalWorkspaceChanges).mockResolvedValueOnce({
|
|
574
|
+
ok: true,
|
|
575
|
+
failures: [],
|
|
576
|
+
appliedCount: 1,
|
|
577
|
+
data: { success: true }
|
|
578
|
+
});
|
|
579
|
+
const res = await runWorkspacePullSyncService({
|
|
580
|
+
resolveCloudAuthUrl: mockResolveCloudAuthUrl,
|
|
581
|
+
workspaceId: 'ws-1',
|
|
582
|
+
cursor: null,
|
|
583
|
+
bootstrap: false
|
|
584
|
+
});
|
|
585
|
+
expect(res.success).toBe(true);
|
|
586
|
+
expect(res.pulledPlanningUpserts).toBe(true);
|
|
587
|
+
expect(res.pulledActiveUpserts).toBe(false);
|
|
588
|
+
expect(res.pulledArchivedUpserts).toBe(false);
|
|
589
|
+
});
|
|
551
590
|
it('zero-change bootstrap pull (no changes, hasMore false) returns success true', async () => {
|
|
552
591
|
vi.mocked(pullWorkspaceChanges).mockResolvedValueOnce({
|
|
553
592
|
ok: true,
|