@taskforcehq/taskforce 0.3.305 → 0.3.306
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/dist/Taskforce.module.css +295 -89
- package/dist/TaskforceCore.js +215 -43
- package/dist/TaskforceCore.test.js +433 -28
- package/dist/assets/fonts/CourierPrime-Bold.woff2 +0 -0
- package/dist/assets/fonts/CourierPrime-BoldItalic.woff2 +0 -0
- package/dist/assets/fonts/CourierPrime-Italic.woff2 +0 -0
- package/dist/assets/fonts/CourierPrime-Regular.woff2 +0 -0
- package/dist/assets/fonts/Noir_medium.woff2 +0 -0
- package/dist/assets/fonts/Noir_regular.woff2 +0 -0
- package/dist/assets/fonts/RussoOne-Regular.woff2 +0 -0
- package/dist/assets/fonts/SpecialElite-Regular.woff2 +0 -0
- package/dist/compat/workspaceSyncCompat.js +46 -2
- package/dist/compat/workspaceSyncCompat.test.js +38 -1
- package/dist/components/context/ContextAttachmentManager.d.ts +14 -0
- package/dist/components/context/ContextAttachmentManager.js +549 -0
- package/dist/components/features/AgentsModule.d.ts +10 -1
- package/dist/components/features/AgentsModule.js +260 -15
- package/dist/components/features/AgentsModule.test.js +255 -5
- package/dist/components/features/DocumentViewer.d.ts +5 -1
- package/dist/components/features/DocumentViewer.js +60 -3
- package/dist/components/features/DocumentWorkspace.js +1 -1
- package/dist/components/features/DocumentWorkspace.test.js +33 -0
- package/dist/components/features/documentWorkspaceModel.d.ts +1 -1
- package/dist/components/task/TaskCard.js +19 -28
- package/dist/components/task/TaskCard.test.js +38 -3
- package/dist/components/task/TaskContextUpload.js +4 -526
- package/dist/components/task/TaskForm.js +85 -90
- package/dist/components/task/TaskForm.test.js +110 -8
- package/dist/components/task/TaskKanban.d.ts +2 -1
- package/dist/components/task/TaskKanban.js +94 -13
- package/dist/components/task/TaskKanban.test.js +36 -0
- package/dist/components/ui/EditableAvatarButton.d.ts +18 -0
- package/dist/components/ui/EditableAvatarButton.js +18 -0
- package/dist/components/views/PlanComparisonPage.d.ts +9 -1
- package/dist/components/views/PlanComparisonPage.js +70 -14
- package/dist/components/views/PlanComparisonPage.test.js +238 -0
- package/dist/components/views/PlansPage.js +127 -36
- package/dist/components/views/PlansPage.test.js +294 -12
- package/dist/components/views/StandaloneLayout.js +283 -533
- package/dist/components/views/panels/PlanningDrawer.d.ts +8 -1
- package/dist/components/views/panels/PlanningDrawer.js +18 -11
- package/dist/components/views/panels/PlanningDrawer.test.d.ts +1 -0
- package/dist/components/views/panels/PlanningDrawer.test.js +141 -0
- package/dist/components/views/planningScope.d.ts +20 -0
- package/dist/components/views/planningScope.js +25 -0
- package/dist/components/views/planningScope.test.d.ts +1 -0
- package/dist/components/views/planningScope.test.js +60 -0
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +26 -1
- package/dist/components/views/standalone/modals/AccountHubModal.js +76 -2
- package/dist/components/views/standalone/modals/AccountHubModal.test.js +20 -3
- package/dist/components/views/standalone/modals/AvatarImageManagerModal.d.ts +20 -0
- package/dist/components/views/standalone/modals/AvatarImageManagerModal.js +127 -0
- package/dist/components/views/standalone/modals/EditProfileModal.d.ts +2 -9
- package/dist/components/views/standalone/modals/EditProfileModal.js +3 -2
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.d.ts +9 -0
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.js +6 -0
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.d.ts +1 -0
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.js +24 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +2 -1
- package/dist/components/views/standalone/modals/SyncStatusModal.js +2 -2
- package/dist/core/AiProfileService.d.ts +39 -2
- package/dist/core/AiProfileService.js +639 -73
- package/dist/core/AiProfileService.test.js +19 -1
- package/dist/core/AiProfiles.test.js +566 -7
- package/dist/core/AttachmentLinkService.js +57 -15
- package/dist/core/AuthIdentityService.d.ts +107 -0
- package/dist/core/AuthIdentityService.js +284 -0
- package/dist/core/AuthTokenService.d.ts +4 -0
- package/dist/core/AuthTokenService.js +34 -6
- package/dist/core/AuthTokenService.test.js +3 -3
- package/dist/core/EntitlementsPolicy.test.js +2 -2
- package/dist/core/GlobalSettingsService.js +115 -6
- package/dist/core/PlanEntitlementService.d.ts +23 -2
- package/dist/core/PlanEntitlementService.js +343 -57
- package/dist/core/PlanFeatureCatalog.test.d.ts +1 -0
- package/dist/core/PlanFeatureCatalog.test.js +84 -0
- package/dist/core/PlanVersionPolicy.test.js +35 -2
- package/dist/core/PlanningEntities.test.js +52 -0
- package/dist/core/SignupMonetizationSettings.test.js +199 -27
- package/dist/core/SyncReconciliationService.js +2 -65
- package/dist/core/SystemAdmin.test.js +42 -42
- package/dist/core/TaskAttachmentsCanonical.test.js +51 -1
- package/dist/core/Taskforce.d.ts +225 -14
- package/dist/core/Taskforce.js +765 -192
- package/dist/core/Taskforce.listTasksSlim.test.d.ts +1 -0
- package/dist/core/Taskforce.listTasksSlim.test.js +91 -0
- package/dist/core/UserProfileAvatarDrafts.test.js +30 -3
- package/dist/core/WorkspaceLifecycleService.js +0 -5
- package/dist/core/WorkspacePermissions.test.js +25 -1
- package/dist/core/WorkspacePlanMode.test.js +117 -16
- package/dist/core/shared.d.ts +1 -1
- package/dist/core/shared.js +5 -1
- package/dist/core/types.d.ts +68 -2
- package/dist/hooks/auth/useTaskforceAuthBootstrap.js +26 -1
- package/dist/hooks/sync/bootstrap.js +2 -0
- package/dist/hooks/sync/controlPlane.d.ts +37 -0
- package/dist/hooks/sync/controlPlane.js +78 -0
- package/dist/hooks/sync/controlPlane.test.d.ts +1 -0
- package/dist/hooks/sync/controlPlane.test.js +121 -0
- package/dist/hooks/sync/lifecyclePolicy.d.ts +45 -0
- package/dist/hooks/sync/lifecyclePolicy.js +93 -0
- package/dist/hooks/sync/lifecyclePolicy.test.d.ts +1 -0
- package/dist/hooks/sync/lifecyclePolicy.test.js +124 -0
- package/dist/hooks/sync/orchestratorShared.d.ts +10 -1
- package/dist/hooks/sync/orchestratorShared.js +68 -47
- package/dist/hooks/sync/orchestratorShared.test.js +50 -1
- package/dist/hooks/sync/pullLifecycle.d.ts +31 -0
- package/dist/hooks/sync/pullLifecycle.js +24 -0
- package/dist/hooks/sync/pullLifecycle.test.d.ts +1 -0
- package/dist/hooks/sync/pullLifecycle.test.js +80 -0
- package/dist/hooks/sync/recovery.d.ts +16 -2
- package/dist/hooks/sync/recovery.js +171 -53
- package/dist/hooks/sync/recovery.test.d.ts +1 -0
- package/dist/hooks/sync/recovery.test.js +292 -0
- package/dist/hooks/sync/transfers.d.ts +15 -3
- package/dist/hooks/sync/transfers.js +179 -62
- package/dist/hooks/sync/transfers.test.d.ts +1 -0
- package/dist/hooks/sync/transfers.test.js +396 -0
- package/dist/hooks/sync/useRecentSyncEvents.d.ts +21 -0
- package/dist/hooks/sync/useRecentSyncEvents.js +92 -0
- package/dist/hooks/sync/useRecentSyncEvents.test.d.ts +1 -0
- package/dist/hooks/sync/useRecentSyncEvents.test.js +124 -0
- package/dist/hooks/sync/useSyncStatusActions.d.ts +30 -0
- package/dist/hooks/sync/useSyncStatusActions.js +88 -0
- package/dist/hooks/sync/useSyncStatusActions.test.d.ts +1 -0
- package/dist/hooks/sync/useSyncStatusActions.test.js +133 -0
- package/dist/hooks/sync/useSyncStatusControls.d.ts +25 -0
- package/dist/hooks/sync/useSyncStatusControls.js +60 -0
- package/dist/hooks/sync/useSyncStatusControls.test.d.ts +1 -0
- package/dist/hooks/sync/useSyncStatusControls.test.js +88 -0
- package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +12 -0
- package/dist/hooks/useSyncOrchestrator.d.ts +9 -2
- package/dist/hooks/useSyncOrchestrator.js +359 -178
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +407 -0
- package/dist/hooks/useTaskData.js +8 -3
- package/dist/hooks/useTaskData.test.js +45 -0
- package/dist/hooks/useTaskforce.d.ts +60 -2
- package/dist/hooks/useTaskforce.js +549 -240
- package/dist/hooks/useTaskforce.runtime-routing.test.d.ts +1 -0
- package/dist/hooks/useTaskforce.runtime-routing.test.js +152 -0
- package/dist/hooks/useTaskforce.sync-behavior.test.js +2380 -212
- package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
- package/dist/hooks/useWorkspaceSyncController.js +6 -1
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +16 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +316 -35
- package/dist/localization/locales/en-US.d.ts +0 -1
- package/dist/localization/locales/en-US.js +0 -1
- package/dist/localization/locales/es-419.js +0 -1
- package/dist/localization/locales/pt-BR.js +0 -1
- package/dist/mcp/documentAssetRegistrar.js +3 -3
- package/dist/mcp/runtime.d.ts +2 -0
- package/dist/mcp/runtime.js +131 -64
- package/dist/mcp/runtime.test.js +291 -12
- package/dist/migrations/taskSchemaMigrations.d.ts +1 -1
- package/dist/migrations/taskSchemaMigrations.js +171 -61
- package/dist/migrations/taskSchemaMigrations.test.js +15 -0
- package/dist/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
- package/dist/resources/templates/workflows/collaborate.yaml +1 -1
- package/dist/resources/templates/workflows/evaluate.yaml +2 -2
- package/dist/resources/templates/workflows/plan.yaml +1 -1
- package/dist/resources/templates/workflows/review.yaml +2 -2
- package/dist/server/annotatedAttachmentsRoutes.test.js +3 -3
- package/dist/server/auth/providers/apple.d.ts +33 -0
- package/dist/server/auth/providers/apple.js +82 -0
- package/dist/server/auth/providers/appleClientSecret.d.ts +37 -0
- package/dist/server/auth/providers/appleClientSecret.js +65 -0
- package/dist/server/auth/providers/github.d.ts +26 -0
- package/dist/server/auth/providers/github.js +86 -0
- package/dist/server/auth/providers/google.d.ts +21 -0
- package/dist/server/auth/providers/google.js +56 -0
- package/dist/server/auth/providers/types.d.ts +21 -0
- package/dist/server/auth/providers/types.js +1 -0
- package/dist/server/auth.d.ts +2 -0
- package/dist/server/auth.js +6 -3
- package/dist/server/documentReviewRoutes.test.js +36 -3
- package/dist/server/index.cookieProxy.test.js +1 -0
- package/dist/server/index.d.ts +12 -0
- package/dist/server/index.js +120 -9
- package/dist/server/index.rateLimit.test.js +3 -0
- package/dist/server/index.test.js +106 -3
- package/dist/server/routes/admin.d.ts +3 -0
- package/dist/server/routes/admin.js +272 -19
- package/dist/server/routes/auth.d.ts +15 -2
- package/dist/server/routes/auth.js +994 -57
- package/dist/server/routes/authSupport.d.ts +1 -1
- package/dist/server/routes/authSupport.js +1 -1
- package/dist/server/routes/billing.js +361 -35
- package/dist/server/routes/billing.test.js +592 -12
- package/dist/server/routes/documents.d.ts +1 -0
- package/dist/server/routes/documents.js +172 -70
- package/dist/server/routes/shared.d.ts +28 -2
- package/dist/server/routes/shared.js +69 -4
- package/dist/server/routes/sync.integration.test.js +437 -39
- package/dist/server/routes/sync.js +61 -605
- package/dist/server/routes/syncAuxRoutes.d.ts +15 -0
- package/dist/server/routes/syncAuxRoutes.js +91 -0
- package/dist/server/routes/syncAuxRoutes.test.d.ts +1 -0
- package/dist/server/routes/syncAuxRoutes.test.js +158 -0
- package/dist/server/routes/syncPullApplyRoutes.d.ts +36 -0
- package/dist/server/routes/syncPullApplyRoutes.js +204 -0
- package/dist/server/routes/syncPushRoutes.d.ts +24 -0
- package/dist/server/routes/syncPushRoutes.js +212 -0
- package/dist/server/routes/syncRouteGuards.d.ts +36 -0
- package/dist/server/routes/syncRouteGuards.js +67 -0
- package/dist/server/routes/syncRouteGuards.test.d.ts +1 -0
- package/dist/server/routes/syncRouteGuards.test.js +94 -0
- package/dist/server/routes/syncRouteTypes.d.ts +13 -0
- package/dist/server/routes/syncRouteTypes.js +1 -0
- package/dist/server/routes/syncSnapshotRoutes.d.ts +66 -0
- package/dist/server/routes/syncSnapshotRoutes.js +180 -0
- package/dist/server/routes/tasks.js +43 -1
- package/dist/server/routes/workspaces.js +36 -5
- package/dist/server/routes.js +60 -3
- package/dist/server/routes.test.js +1824 -171
- package/dist/services/workflowExportSnapshots.test.js +2 -0
- package/dist/shared/aiProfileSeatScope.d.ts +5 -0
- package/dist/shared/aiProfileSeatScope.js +21 -0
- package/dist/shared/passwordPolicy.d.ts +13 -0
- package/dist/shared/passwordPolicy.js +84 -0
- package/dist/shared/passwordPolicy.test.d.ts +1 -0
- package/dist/shared/passwordPolicy.test.js +34 -0
- package/dist/storage/workspaceAssetStore.d.ts +15 -3
- package/dist/storage/workspaceAssetStore.js +132 -44
- package/dist/storage/workspaceAssetStore.test.js +93 -6
- package/dist/styles/fonts.css +70 -0
- package/dist/sync/collaborationSyncPayload.d.ts +6 -1
- package/dist/sync/collaborationSyncPayload.js +17 -11
- package/dist/sync/collaborationSyncPayload.test.js +13 -1
- package/dist/sync/contentSyncState.d.ts +4 -8
- package/dist/sync/contentSyncState.js +17 -70
- package/dist/sync/syncApplyHandlers.d.ts +7 -0
- package/dist/sync/syncApplyHandlers.js +28 -13
- package/dist/sync/syncApplyHandlers.test.js +70 -3
- package/dist/sync/syncFieldSemantics.d.ts +12 -0
- package/dist/sync/syncFieldSemantics.js +55 -0
- package/dist/sync/syncResourceAdapters.d.ts +44 -0
- package/dist/sync/syncResourceAdapters.js +77 -0
- package/dist/sync/syncService.d.ts +5 -0
- package/dist/sync/syncService.js +52 -0
- package/dist/sync/syncService.test.js +57 -3
- package/dist/sync/taskSyncChangeKey.d.ts +2 -0
- package/dist/sync/taskSyncChangeKey.js +143 -0
- package/dist/sync/taskSyncPayload.js +3 -8
- package/dist/sync/workspacePullFeed.js +4 -3
- package/dist/sync/workspacePullFeed.test.js +43 -0
- package/dist/sync/workspaceRepair.js +5 -2
- package/dist/sync/workspaceSyncModel.d.ts +45 -0
- package/dist/sync/workspaceSyncModel.js +183 -28
- package/dist/sync/workspaceSyncModel.test.js +291 -12
- package/dist/sync/workspaceSyncState.d.ts +17 -0
- package/dist/sync/workspaceSyncState.js +2 -1
- package/dist/sync/workspaceSyncSurface.js +3 -8
- package/dist/types.d.ts +17 -0
- package/dist/ui/assets/AgentsModule-DrLawwNl.js +1 -0
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BG6P_GVW.js → AnnotatedAttachmentWorkspace-Z9_whf7F.js} +1 -1
- package/dist/ui/assets/CourierPrime-Bold-BuLj_dpw.woff2 +0 -0
- package/dist/ui/assets/CourierPrime-BoldItalic-BrK2pUkZ.woff2 +0 -0
- package/dist/ui/assets/CourierPrime-Italic-DFUZK5Ey.woff2 +0 -0
- package/dist/ui/assets/CourierPrime-Regular-BsKphzVf.woff2 +0 -0
- package/dist/ui/assets/DocumentWorkspace-B03MaSkw.js +252 -0
- package/dist/ui/assets/DocumentWorkspace-Dhdso07p.css +1 -0
- package/dist/ui/assets/{InitiativesModule-Dhm7M8e7.js → InitiativesModule-9E6ParrY.js} +1 -1
- package/dist/ui/assets/Noir_medium-bxQwKGzB.woff2 +0 -0
- package/dist/ui/assets/Noir_regular-ojf2kxlG.woff2 +0 -0
- package/dist/ui/assets/PlansPage-CvfnMiWq.css +1 -0
- package/dist/ui/assets/PlansPage-DRXscYxH.js +1 -0
- package/dist/ui/assets/RussoOne-Regular-Cu_qq_qC.woff2 +0 -0
- package/dist/ui/assets/SpecialElite-Regular-Di6gSvAS.woff2 +0 -0
- package/dist/ui/assets/{TaskSettings-ln0aF7xc.js → TaskSettings-DZX4jk7e.js} +1 -1
- package/dist/ui/assets/{WorkflowsModule-BcS4afRn.js → WorkflowsModule-BA7jcEpH.js} +1 -1
- package/dist/ui/assets/index-9eT8e0Lu.css +1 -0
- package/dist/ui/assets/index-C6k75jr8.js +6 -0
- package/dist/ui/assets/{vendor-icons-BSUaRwF8.js → vendor-icons-DuEd_65c.js} +1 -1
- package/dist/ui/fonts/CourierPrime-Bold.ttf +0 -0
- package/dist/ui/fonts/CourierPrime-Bold.woff2 +0 -0
- package/dist/ui/fonts/CourierPrime-BoldItalic.ttf +0 -0
- package/dist/ui/fonts/CourierPrime-BoldItalic.woff2 +0 -0
- package/dist/ui/fonts/CourierPrime-Italic.ttf +0 -0
- package/dist/ui/fonts/CourierPrime-Italic.woff2 +0 -0
- package/dist/ui/fonts/CourierPrime-Regular.ttf +0 -0
- package/dist/ui/fonts/CourierPrime-Regular.woff2 +0 -0
- package/dist/ui/fonts/Noir_medium.otf +0 -0
- package/dist/ui/fonts/Noir_medium.woff2 +0 -0
- package/dist/ui/fonts/Noir_regular.otf +0 -0
- package/dist/ui/fonts/Noir_regular.woff2 +0 -0
- package/dist/ui/fonts/RussoOne-Regular.woff2 +0 -0
- package/dist/ui/fonts/SpecialElite-Regular.ttf +0 -0
- package/dist/ui/fonts/SpecialElite-Regular.woff2 +0 -0
- package/dist/ui/index.html +3 -3
- package/dist/utils/accountProfileSummaryCache.d.ts +5 -0
- package/dist/utils/assignees.d.ts +2 -0
- package/dist/utils/assignees.js +2 -0
- package/dist/utils/avatarUpload.d.ts +27 -0
- package/dist/utils/avatarUpload.js +132 -0
- package/dist/utils/avatarUpload.test.d.ts +1 -0
- package/dist/utils/avatarUpload.test.js +40 -0
- package/dist/utils/bootstrapTrace.d.ts +7 -0
- package/dist/utils/bootstrapTrace.js +29 -0
- package/dist/utils/commercialLifecyclePresentation.js +3 -0
- package/dist/utils/contextAssetEvents.d.ts +2 -0
- package/dist/utils/syncEventDetails.d.ts +3 -0
- package/dist/utils/syncEventDetails.js +64 -0
- package/dist/utils/syncEventDetails.test.d.ts +1 -0
- package/dist/utils/syncEventDetails.test.js +23 -0
- package/dist/utils/syncEventPresentation.d.ts +15 -0
- package/dist/utils/syncEventPresentation.js +129 -0
- package/dist/utils/syncEventPresentation.test.d.ts +1 -0
- package/dist/utils/syncEventPresentation.test.js +64 -0
- package/dist/utils/syncStatusPresentation.d.ts +72 -0
- package/dist/utils/syncStatusPresentation.js +217 -0
- package/dist/utils/syncStatusPresentation.test.d.ts +1 -0
- package/dist/utils/syncStatusPresentation.test.js +164 -0
- package/dist/utils/taskActivity.d.ts +2 -0
- package/dist/utils/taskActivity.js +228 -22
- package/dist/utils/taskActivity.test.js +119 -8
- package/dist/utils/workspaceSyncPresentation.d.ts +4 -0
- package/dist/utils/workspaceSyncPresentation.js +68 -2
- package/dist/utils/workspaceSyncPresentation.test.js +36 -1
- package/package.json +3 -2
- package/scripts/export-sqlite-to-postgres.mjs +7 -9
- package/scripts/run-e2e-realtime.sh +17 -3
- package/scripts/run-smoke.sh +17 -5
- package/scripts/run-soak.sh +17 -5
- package/src/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
- package/src/resources/templates/workflows/collaborate.yaml +1 -1
- package/src/resources/templates/workflows/evaluate.yaml +2 -2
- package/src/resources/templates/workflows/plan.yaml +1 -1
- package/src/resources/templates/workflows/review.yaml +2 -2
- package/dist/ui/assets/AgentsModule-N2MnQBZk.js +0 -1
- package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +0 -1
- package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +0 -252
- package/dist/ui/assets/PlansPage-C2dd2n1X.css +0 -1
- package/dist/ui/assets/PlansPage-Cq0zXj3I.js +0 -1
- package/dist/ui/assets/RussoOne-Regular-C3BxZIj7.ttf +0 -0
- package/dist/ui/assets/index-C2-OXZV7.css +0 -1
- package/dist/ui/assets/index-DcSI5F86.js +0 -6
- package/scripts/migrate-planning-model.ts +0 -233
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export function hasOwnSyncField(source, field) {
|
|
2
|
+
return Boolean(source) && typeof source === 'object' && Object.prototype.hasOwnProperty.call(source, field);
|
|
3
|
+
}
|
|
4
|
+
export function normalizeSyncOptionalString(value) {
|
|
5
|
+
if (value === undefined || value === null)
|
|
6
|
+
return undefined;
|
|
7
|
+
const normalized = String(value).trim();
|
|
8
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
9
|
+
}
|
|
10
|
+
export function normalizeSyncNullableString(value) {
|
|
11
|
+
if (value === undefined || value === null)
|
|
12
|
+
return null;
|
|
13
|
+
const normalized = String(value).trim();
|
|
14
|
+
return normalized.length > 0 ? normalized : null;
|
|
15
|
+
}
|
|
16
|
+
export function normalizeSyncOptionalId(value) {
|
|
17
|
+
return normalizeSyncOptionalString(value);
|
|
18
|
+
}
|
|
19
|
+
export function normalizeSyncNullableId(value) {
|
|
20
|
+
return normalizeSyncNullableString(value);
|
|
21
|
+
}
|
|
22
|
+
export function normalizeSyncNullableIsoString(value) {
|
|
23
|
+
return normalizeSyncNullableString(value);
|
|
24
|
+
}
|
|
25
|
+
export function normalizeSyncSameOriginAssetUrl(value) {
|
|
26
|
+
const normalized = normalizeSyncNullableString(value);
|
|
27
|
+
if (!normalized)
|
|
28
|
+
return null;
|
|
29
|
+
return normalized.startsWith('/api/taskforce/context/') ? normalized : null;
|
|
30
|
+
}
|
|
31
|
+
export function readSyncOptionalStringField(source, field) {
|
|
32
|
+
if (!hasOwnSyncField(source, field))
|
|
33
|
+
return undefined;
|
|
34
|
+
return normalizeSyncOptionalString(source[field]);
|
|
35
|
+
}
|
|
36
|
+
export function readSyncNullableStringField(source, field) {
|
|
37
|
+
if (!hasOwnSyncField(source, field))
|
|
38
|
+
return undefined;
|
|
39
|
+
return normalizeSyncNullableString(source[field]);
|
|
40
|
+
}
|
|
41
|
+
export function readSyncNullableIdField(source, field) {
|
|
42
|
+
if (!hasOwnSyncField(source, field))
|
|
43
|
+
return undefined;
|
|
44
|
+
return normalizeSyncNullableId(source[field]);
|
|
45
|
+
}
|
|
46
|
+
export function readSyncNullableIsoStringField(source, field) {
|
|
47
|
+
if (!hasOwnSyncField(source, field))
|
|
48
|
+
return undefined;
|
|
49
|
+
return normalizeSyncNullableIsoString(source[field]);
|
|
50
|
+
}
|
|
51
|
+
export function readSyncSameOriginAssetUrlField(source, field) {
|
|
52
|
+
if (!hasOwnSyncField(source, field))
|
|
53
|
+
return undefined;
|
|
54
|
+
return normalizeSyncSameOriginAssetUrl(source[field]);
|
|
55
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { TaskItem } from '../types.js';
|
|
2
|
+
export interface SyncAiProfileLike {
|
|
3
|
+
id?: string | null;
|
|
4
|
+
workspaceId?: string | null;
|
|
5
|
+
profileToken?: string | null;
|
|
6
|
+
name?: string | null;
|
|
7
|
+
username?: string | null;
|
|
8
|
+
icon?: string | null;
|
|
9
|
+
color?: string | null;
|
|
10
|
+
avatarUrl?: string | null;
|
|
11
|
+
avatarSourceUrl?: string | null;
|
|
12
|
+
description?: string | null;
|
|
13
|
+
role?: string | null;
|
|
14
|
+
provider?: string | null;
|
|
15
|
+
model?: string | null;
|
|
16
|
+
surfaceType?: string | null;
|
|
17
|
+
seatScope?: string | null;
|
|
18
|
+
providerMetadata?: Record<string, unknown> | null;
|
|
19
|
+
archivedAt?: string | null;
|
|
20
|
+
archivedBy?: string | null;
|
|
21
|
+
archivedReason?: string | null;
|
|
22
|
+
mergedIntoProfileId?: string | null;
|
|
23
|
+
rosterStateUpdatedAt?: string | null;
|
|
24
|
+
createdAt?: string | null;
|
|
25
|
+
updatedAt?: string | null;
|
|
26
|
+
lastActiveAt?: string | null;
|
|
27
|
+
}
|
|
28
|
+
export interface SyncResourceAdapter<T> {
|
|
29
|
+
kind: string;
|
|
30
|
+
getId: (resource: T) => string;
|
|
31
|
+
getWatermark: (resource: T) => string;
|
|
32
|
+
getChangeKey: (resource: T) => string;
|
|
33
|
+
}
|
|
34
|
+
export interface SyncResourceBaselineState {
|
|
35
|
+
watermarks?: Map<string, string> | null;
|
|
36
|
+
changeKeys?: Map<string, string> | null;
|
|
37
|
+
}
|
|
38
|
+
export declare function getAiProfileSyncWatermark(profile: Partial<SyncAiProfileLike> | null | undefined): string;
|
|
39
|
+
export declare function getAiProfileSyncChangeKey(profile: Partial<SyncAiProfileLike> | null | undefined): string;
|
|
40
|
+
export declare const taskSyncResourceAdapter: SyncResourceAdapter<Partial<TaskItem> & {
|
|
41
|
+
isArchived?: boolean | null;
|
|
42
|
+
}>;
|
|
43
|
+
export declare const aiProfileSyncResourceAdapter: SyncResourceAdapter<Partial<SyncAiProfileLike>>;
|
|
44
|
+
export declare function shouldSyncResource<T>(adapter: SyncResourceAdapter<T>, resource: T, baseline?: SyncResourceBaselineState | null): boolean;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { buildTaskSyncChangeKey } from './taskSyncChangeKey.js';
|
|
2
|
+
import { normalizeSyncNullableIsoString, normalizeSyncNullableString, normalizeSyncOptionalId, normalizeSyncSameOriginAssetUrl, } from './syncFieldSemantics.js';
|
|
3
|
+
function normalizeSyncTimestampMax(...values) {
|
|
4
|
+
return values.reduce((latest, value) => {
|
|
5
|
+
const normalized = normalizeSyncNullableIsoString(value) || '';
|
|
6
|
+
if (!latest)
|
|
7
|
+
return normalized;
|
|
8
|
+
if (!normalized)
|
|
9
|
+
return latest;
|
|
10
|
+
return normalized > latest ? normalized : latest;
|
|
11
|
+
}, '');
|
|
12
|
+
}
|
|
13
|
+
export function getAiProfileSyncWatermark(profile) {
|
|
14
|
+
const source = profile || {};
|
|
15
|
+
return normalizeSyncTimestampMax(source.createdAt, source.updatedAt, source.lastActiveAt, source.archivedAt, source.rosterStateUpdatedAt);
|
|
16
|
+
}
|
|
17
|
+
export function getAiProfileSyncChangeKey(profile) {
|
|
18
|
+
const source = profile || {};
|
|
19
|
+
return JSON.stringify({
|
|
20
|
+
id: String(source.id || '').trim(),
|
|
21
|
+
workspaceId: String(source.workspaceId || '').trim(),
|
|
22
|
+
profileToken: String(source.profileToken || '').trim(),
|
|
23
|
+
name: String(source.name || '').trim(),
|
|
24
|
+
username: String(source.username || '').trim(),
|
|
25
|
+
icon: String(source.icon || 'Bot').trim() || 'Bot',
|
|
26
|
+
color: String(source.color || '#8b5cf6').trim() || '#8b5cf6',
|
|
27
|
+
avatarUrl: normalizeSyncSameOriginAssetUrl(source.avatarUrl) || '',
|
|
28
|
+
avatarSourceUrl: normalizeSyncSameOriginAssetUrl(source.avatarSourceUrl) || '',
|
|
29
|
+
description: normalizeSyncNullableString(source.description) || '',
|
|
30
|
+
role: normalizeSyncNullableString(source.role) || '',
|
|
31
|
+
provider: normalizeSyncNullableString(source.provider) || '',
|
|
32
|
+
model: normalizeSyncNullableString(source.model) || '',
|
|
33
|
+
surfaceType: normalizeSyncNullableString(source.surfaceType) || '',
|
|
34
|
+
seatScope: normalizeSyncNullableString(source.seatScope) || '',
|
|
35
|
+
providerMetadata: source.providerMetadata && typeof source.providerMetadata === 'object' && !Array.isArray(source.providerMetadata)
|
|
36
|
+
? source.providerMetadata
|
|
37
|
+
: null,
|
|
38
|
+
archivedAt: normalizeSyncNullableIsoString(source.archivedAt) || '',
|
|
39
|
+
archivedBy: normalizeSyncOptionalId(source.archivedBy) || '',
|
|
40
|
+
archivedReason: normalizeSyncNullableString(source.archivedReason) || '',
|
|
41
|
+
mergedIntoProfileId: normalizeSyncOptionalId(source.mergedIntoProfileId) || '',
|
|
42
|
+
rosterStateUpdatedAt: normalizeSyncNullableIsoString(source.rosterStateUpdatedAt) || '',
|
|
43
|
+
createdAt: normalizeSyncNullableIsoString(source.createdAt) || '',
|
|
44
|
+
updatedAt: normalizeSyncNullableIsoString(source.updatedAt) || '',
|
|
45
|
+
lastActiveAt: normalizeSyncNullableIsoString(source.lastActiveAt) || '',
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
export const taskSyncResourceAdapter = {
|
|
49
|
+
kind: 'task',
|
|
50
|
+
getId: (task) => String(task?.id || '').trim(),
|
|
51
|
+
getWatermark: (task) => String(task?.updatedAt || task?.createdAt || '').trim(),
|
|
52
|
+
getChangeKey: (task) => buildTaskSyncChangeKey(task, Boolean(task?.isArchived))
|
|
53
|
+
};
|
|
54
|
+
export const aiProfileSyncResourceAdapter = {
|
|
55
|
+
kind: 'ai-profile',
|
|
56
|
+
getId: (profile) => String(profile?.id || '').trim(),
|
|
57
|
+
getWatermark: (profile) => getAiProfileSyncWatermark(profile),
|
|
58
|
+
getChangeKey: (profile) => getAiProfileSyncChangeKey(profile)
|
|
59
|
+
};
|
|
60
|
+
export function shouldSyncResource(adapter, resource, baseline) {
|
|
61
|
+
const id = adapter.getId(resource);
|
|
62
|
+
if (!id)
|
|
63
|
+
return false;
|
|
64
|
+
const currentChangeKey = adapter.getChangeKey(resource);
|
|
65
|
+
const previousWatermarks = baseline?.watermarks;
|
|
66
|
+
if (!previousWatermarks || previousWatermarks.size === 0)
|
|
67
|
+
return true;
|
|
68
|
+
if (!previousWatermarks.has(id))
|
|
69
|
+
return true;
|
|
70
|
+
const previousChangeKeys = baseline?.changeKeys;
|
|
71
|
+
const previousChangeKey = previousChangeKeys?.get(id);
|
|
72
|
+
if (typeof previousChangeKey === 'string') {
|
|
73
|
+
return previousChangeKey !== currentChangeKey;
|
|
74
|
+
}
|
|
75
|
+
const previousWatermark = previousWatermarks.get(id) || '';
|
|
76
|
+
return previousWatermark !== currentChangeKey;
|
|
77
|
+
}
|
|
@@ -56,6 +56,8 @@ export interface WorkspacePushSyncResult {
|
|
|
56
56
|
changeCount: number;
|
|
57
57
|
deleteCount: number;
|
|
58
58
|
currentTaskIds: Set<string>;
|
|
59
|
+
currentTaskWatermarks?: Map<string, string>;
|
|
60
|
+
currentTaskChangeKeys?: Map<string, string>;
|
|
59
61
|
deleteTaskIds: Set<string>;
|
|
60
62
|
currentInitiativeIds: Set<string>;
|
|
61
63
|
currentInitiativeWatermarks: Map<string, string>;
|
|
@@ -63,6 +65,7 @@ export interface WorkspacePushSyncResult {
|
|
|
63
65
|
currentWorkstreamWatermarks: Map<string, string>;
|
|
64
66
|
currentAiProfileIds: Set<string>;
|
|
65
67
|
currentAiProfileWatermarks: Map<string, string>;
|
|
68
|
+
currentAiProfileChangeKeys?: Map<string, string>;
|
|
66
69
|
currentDocumentPaths: Set<string>;
|
|
67
70
|
deleteDocumentPaths: Set<string>;
|
|
68
71
|
currentDocumentWatermarks: Map<string, string>;
|
|
@@ -81,6 +84,7 @@ export interface WorkspacePushSyncResult {
|
|
|
81
84
|
currentTaskEventWatermarks: Map<string, string>;
|
|
82
85
|
/** Watermarks for every task upsert that was successfully pushed. */
|
|
83
86
|
pushedWatermarks: Map<string, string>;
|
|
87
|
+
pushedTaskChangeKeys?: Map<string, string>;
|
|
84
88
|
/** Server-emitted realtime mutation event ids for this push apply. */
|
|
85
89
|
emittedEventIds: string[];
|
|
86
90
|
/** Authoritative task snapshots returned immediately by the push route for incoming task upserts. */
|
|
@@ -106,6 +110,7 @@ export interface WorkspacePullSyncResult {
|
|
|
106
110
|
success: boolean;
|
|
107
111
|
kind?: WorkspacePullFailureKind;
|
|
108
112
|
status?: number;
|
|
113
|
+
errorCode?: string;
|
|
109
114
|
retryAfterMs?: number;
|
|
110
115
|
transient?: boolean;
|
|
111
116
|
failures?: string[];
|
package/dist/sync/syncService.js
CHANGED
|
@@ -15,6 +15,14 @@ function resolveWorkspaceAccessConflictMessage(data, fallbackStatus) {
|
|
|
15
15
|
}
|
|
16
16
|
return rawError || `Workspace provisioning failed (${fallbackStatus})`;
|
|
17
17
|
}
|
|
18
|
+
function isMissingCloudWorkspaceSelection(data, status) {
|
|
19
|
+
if (status !== 400)
|
|
20
|
+
return false;
|
|
21
|
+
const code = String(data.code || '').trim().toUpperCase();
|
|
22
|
+
const rawError = String(data.error || '').trim().toLowerCase();
|
|
23
|
+
return code === 'WORKSPACE_ID_REQUIRED'
|
|
24
|
+
&& rawError.includes('no cloud workspace is selected');
|
|
25
|
+
}
|
|
18
26
|
export async function ensureCloudWorkspaceReadyForSyncService(input) {
|
|
19
27
|
if (!input.cloudAuthConfigured)
|
|
20
28
|
return { success: false, error: 'Cloud auth endpoint is not configured.' };
|
|
@@ -44,6 +52,39 @@ export async function ensureCloudWorkspaceReadyForSyncService(input) {
|
|
|
44
52
|
error: String(data.error || 'Workspace sync rate limited. Please retry later.')
|
|
45
53
|
};
|
|
46
54
|
}
|
|
55
|
+
if (isMissingCloudWorkspaceSelection(data, handshakeResult.status)) {
|
|
56
|
+
const provisionResult = await provisionWorkspace(input.resolveCloudAuthUrl, {
|
|
57
|
+
workspaceId: input.workspaceId,
|
|
58
|
+
name: input.workspaceName || input.workspaceId
|
|
59
|
+
});
|
|
60
|
+
if (!provisionResult.ok) {
|
|
61
|
+
const provisionError = asObjectRecord(provisionResult.data);
|
|
62
|
+
return {
|
|
63
|
+
success: false,
|
|
64
|
+
statusCode: provisionResult.status,
|
|
65
|
+
transient: isTransientWorkspaceSyncStatus(provisionResult.status),
|
|
66
|
+
retryAfterMs: provisionResult.retryAfterMs,
|
|
67
|
+
error: resolveWorkspaceAccessConflictMessage(provisionError, provisionResult.status)
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
handshakeResult = await handshakeWorkspace(input.resolveCloudAuthUrl, input.workspaceId);
|
|
71
|
+
if (!handshakeResult.ok) {
|
|
72
|
+
const retryData = asObjectRecord(handshakeResult.data);
|
|
73
|
+
if (handshakeResult.status === 401) {
|
|
74
|
+
return { success: false, statusCode: 401, transient: false, error: String(retryData.error || 'Authentication required for workspace sync.') };
|
|
75
|
+
}
|
|
76
|
+
if (handshakeResult.status === 429) {
|
|
77
|
+
return { success: false, statusCode: 429, retryAfterMs: handshakeResult.retryAfterMs, transient: true, error: String(retryData.error || 'Workspace sync rate limited. Please retry later.') };
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
success: false,
|
|
81
|
+
statusCode: handshakeResult.status,
|
|
82
|
+
transient: isTransientWorkspaceSyncStatus(handshakeResult.status),
|
|
83
|
+
error: String(retryData.error || `Workspace sync handshake failed (${handshakeResult.status})`)
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return { success: true, provisioned: true };
|
|
87
|
+
}
|
|
47
88
|
if (handshakeResult.status === 409 && data.code === 'WORKSPACE_ID_MISMATCH') {
|
|
48
89
|
if (input.allowProvisionOnMismatch === false) {
|
|
49
90
|
return {
|
|
@@ -173,6 +214,8 @@ export async function runWorkspacePushSyncService(input) {
|
|
|
173
214
|
changeCount,
|
|
174
215
|
deleteCount,
|
|
175
216
|
currentTaskIds: input.payload.currentTaskIds,
|
|
217
|
+
currentTaskWatermarks: input.payload.currentTaskWatermarks ?? new Map(),
|
|
218
|
+
currentTaskChangeKeys: input.payload.currentTaskChangeKeys ?? new Map(),
|
|
176
219
|
deleteTaskIds: input.payload.deleteTaskIds,
|
|
177
220
|
currentInitiativeIds: input.payload.currentInitiativeIds,
|
|
178
221
|
currentInitiativeWatermarks: input.payload.currentInitiativeWatermarks,
|
|
@@ -180,6 +223,7 @@ export async function runWorkspacePushSyncService(input) {
|
|
|
180
223
|
currentWorkstreamWatermarks: input.payload.currentWorkstreamWatermarks,
|
|
181
224
|
currentAiProfileIds: input.payload.currentAiProfileIds,
|
|
182
225
|
currentAiProfileWatermarks: input.payload.currentAiProfileWatermarks,
|
|
226
|
+
currentAiProfileChangeKeys: input.payload.currentAiProfileChangeKeys ?? new Map(),
|
|
183
227
|
currentDocumentPaths: input.payload.currentDocumentPaths,
|
|
184
228
|
deleteDocumentPaths: input.payload.deleteDocumentPaths,
|
|
185
229
|
currentDocumentWatermarks: input.payload.currentDocumentWatermarks,
|
|
@@ -197,6 +241,7 @@ export async function runWorkspacePushSyncService(input) {
|
|
|
197
241
|
currentAnnotatedAttachmentSessionWatermarks: input.payload.currentAnnotatedAttachmentSessionWatermarks,
|
|
198
242
|
currentTaskEventWatermarks: input.payload.currentTaskEventWatermarks,
|
|
199
243
|
pushedWatermarks: new Map(),
|
|
244
|
+
pushedTaskChangeKeys: new Map(),
|
|
200
245
|
emittedEventIds: [],
|
|
201
246
|
appliedTaskUpserts: []
|
|
202
247
|
};
|
|
@@ -231,6 +276,8 @@ export async function runWorkspacePushSyncService(input) {
|
|
|
231
276
|
changeCount,
|
|
232
277
|
deleteCount,
|
|
233
278
|
currentTaskIds: input.payload.currentTaskIds,
|
|
279
|
+
currentTaskWatermarks: input.payload.currentTaskWatermarks ?? new Map(),
|
|
280
|
+
currentTaskChangeKeys: input.payload.currentTaskChangeKeys ?? new Map(),
|
|
234
281
|
deleteTaskIds: input.payload.deleteTaskIds,
|
|
235
282
|
currentInitiativeIds: input.payload.currentInitiativeIds,
|
|
236
283
|
currentInitiativeWatermarks: input.payload.currentInitiativeWatermarks,
|
|
@@ -238,6 +285,7 @@ export async function runWorkspacePushSyncService(input) {
|
|
|
238
285
|
currentWorkstreamWatermarks: input.payload.currentWorkstreamWatermarks,
|
|
239
286
|
currentAiProfileIds: input.payload.currentAiProfileIds,
|
|
240
287
|
currentAiProfileWatermarks: input.payload.currentAiProfileWatermarks,
|
|
288
|
+
currentAiProfileChangeKeys: input.payload.currentAiProfileChangeKeys ?? new Map(),
|
|
241
289
|
currentDocumentPaths: input.payload.currentDocumentPaths,
|
|
242
290
|
deleteDocumentPaths: input.payload.deleteDocumentPaths,
|
|
243
291
|
currentDocumentWatermarks: input.payload.currentDocumentWatermarks,
|
|
@@ -255,6 +303,7 @@ export async function runWorkspacePushSyncService(input) {
|
|
|
255
303
|
currentAnnotatedAttachmentSessionWatermarks: input.payload.currentAnnotatedAttachmentSessionWatermarks,
|
|
256
304
|
currentTaskEventWatermarks: input.payload.currentTaskEventWatermarks,
|
|
257
305
|
pushedWatermarks: input.payload.pushedWatermarks,
|
|
306
|
+
pushedTaskChangeKeys: input.payload.pushedTaskChangeKeys ?? new Map(),
|
|
258
307
|
emittedEventIds,
|
|
259
308
|
appliedTaskUpserts
|
|
260
309
|
};
|
|
@@ -296,10 +345,13 @@ export async function runWorkspacePullSyncService(input) {
|
|
|
296
345
|
}
|
|
297
346
|
}
|
|
298
347
|
if (!pullRes.ok) {
|
|
348
|
+
const pullErrorData = asObjectRecord(pullRes.data);
|
|
299
349
|
return {
|
|
300
350
|
success: false,
|
|
301
351
|
kind: 'pull_http',
|
|
302
352
|
status: pullRes.status,
|
|
353
|
+
errorCode: typeof pullErrorData.code === 'string' ? pullErrorData.code : undefined,
|
|
354
|
+
error: typeof pullErrorData.error === 'string' ? pullErrorData.error : undefined,
|
|
303
355
|
retryAfterMs: pullRes.retryAfterMs,
|
|
304
356
|
transient: isTransientWorkspaceSyncStatus(pullRes.status),
|
|
305
357
|
cursor,
|
|
@@ -443,7 +443,7 @@ describe('syncService', () => {
|
|
|
443
443
|
status: 200,
|
|
444
444
|
data: {
|
|
445
445
|
success: true,
|
|
446
|
-
changes: [{ op: 'upsert', task: { id: '
|
|
446
|
+
changes: [{ op: 'upsert', task: { id: 'task-apply-error', title: 'Apply error', status: 'done', updatedAt: '2026-03-15T20:27:08.000Z' } }],
|
|
447
447
|
nextCursor: null,
|
|
448
448
|
hasMore: false
|
|
449
449
|
}
|
|
@@ -452,7 +452,7 @@ describe('syncService', () => {
|
|
|
452
452
|
ok: false,
|
|
453
453
|
status: 400,
|
|
454
454
|
failures: ['/api/taskforce/sync/workspace/apply-local:400'],
|
|
455
|
-
data: { error: '
|
|
455
|
+
data: { error: 'Task apply rejected by local validation' }
|
|
456
456
|
});
|
|
457
457
|
const res = await runWorkspacePullSyncService({
|
|
458
458
|
resolveCloudAuthUrl: mockResolveCloudAuthUrl,
|
|
@@ -463,7 +463,7 @@ describe('syncService', () => {
|
|
|
463
463
|
expect(res.success).toBe(false);
|
|
464
464
|
expect(res.kind).toBe('apply_all_candidates');
|
|
465
465
|
expect(res.status).toBe(400);
|
|
466
|
-
expect(res.error).toBe('
|
|
466
|
+
expect(res.error).toBe('Task apply rejected by local validation');
|
|
467
467
|
});
|
|
468
468
|
it('surfaces document decryption failures without failing pull', async () => {
|
|
469
469
|
const previousKey = process.env.TASKFORCE_SYNC_KEY;
|
|
@@ -702,6 +702,60 @@ describe('syncService', () => {
|
|
|
702
702
|
expect(res.statusCode).toBe(0);
|
|
703
703
|
expect(res.transient).toBe(true);
|
|
704
704
|
});
|
|
705
|
+
it('recovers from missing cloud workspace selection by provisioning and retrying handshake', async () => {
|
|
706
|
+
vi.mocked(handshakeWorkspace)
|
|
707
|
+
.mockResolvedValueOnce({
|
|
708
|
+
ok: false,
|
|
709
|
+
status: 400,
|
|
710
|
+
data: {
|
|
711
|
+
code: 'WORKSPACE_ID_REQUIRED',
|
|
712
|
+
error: 'No cloud workspace is selected. Complete workspace setup first.'
|
|
713
|
+
}
|
|
714
|
+
})
|
|
715
|
+
.mockResolvedValueOnce({
|
|
716
|
+
ok: true,
|
|
717
|
+
status: 200,
|
|
718
|
+
data: {}
|
|
719
|
+
});
|
|
720
|
+
vi.mocked(provisionWorkspace).mockResolvedValueOnce({
|
|
721
|
+
ok: true,
|
|
722
|
+
status: 200,
|
|
723
|
+
data: {
|
|
724
|
+
success: true,
|
|
725
|
+
existing: true
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
const res = await ensureCloudWorkspaceReadyForSyncService(baseHandshakeInput);
|
|
729
|
+
expect(res.success).toBe(true);
|
|
730
|
+
expect(res.provisioned).toBe(true);
|
|
731
|
+
expect(provisionWorkspace).toHaveBeenCalledWith(mockResolveCloudAuthUrl, {
|
|
732
|
+
workspaceId: 'ws-test',
|
|
733
|
+
name: 'Test Workspace'
|
|
734
|
+
});
|
|
735
|
+
expect(handshakeWorkspace).toHaveBeenCalledTimes(2);
|
|
736
|
+
});
|
|
737
|
+
it('surfaces provisioning failure when recovering missing cloud workspace selection', async () => {
|
|
738
|
+
vi.mocked(handshakeWorkspace).mockResolvedValueOnce({
|
|
739
|
+
ok: false,
|
|
740
|
+
status: 400,
|
|
741
|
+
data: {
|
|
742
|
+
code: 'WORKSPACE_ID_REQUIRED',
|
|
743
|
+
error: 'No cloud workspace is selected. Complete workspace setup first.'
|
|
744
|
+
}
|
|
745
|
+
});
|
|
746
|
+
vi.mocked(provisionWorkspace).mockResolvedValueOnce({
|
|
747
|
+
ok: false,
|
|
748
|
+
status: 409,
|
|
749
|
+
data: {
|
|
750
|
+
code: 'WORKSPACE_ID_ALREADY_EXISTS',
|
|
751
|
+
error: 'Workspace ID already exists'
|
|
752
|
+
}
|
|
753
|
+
});
|
|
754
|
+
const res = await ensureCloudWorkspaceReadyForSyncService(baseHandshakeInput);
|
|
755
|
+
expect(res.success).toBe(false);
|
|
756
|
+
expect(res.statusCode).toBe(409);
|
|
757
|
+
expect(res.error).toContain('already linked to another cloud account');
|
|
758
|
+
});
|
|
705
759
|
it('returns failure when cloudAuthConfigured is false', async () => {
|
|
706
760
|
const res = await ensureCloudWorkspaceReadyForSyncService({
|
|
707
761
|
...baseHandshakeInput,
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { serializeTaskForSync } from './taskSyncPayload.js';
|
|
2
|
+
import { normalizeSyncNullableId, normalizeSyncNullableIsoString, normalizeSyncNullableString, normalizeSyncOptionalId, normalizeSyncOptionalString, } from './syncFieldSemantics.js';
|
|
3
|
+
function normalizeConflictTaxonomies(value) {
|
|
4
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
5
|
+
return {};
|
|
6
|
+
const entries = Object.entries(value)
|
|
7
|
+
.filter(([key]) => String(key || '').trim().length > 0)
|
|
8
|
+
.sort(([left], [right]) => left.localeCompare(right));
|
|
9
|
+
const normalized = {};
|
|
10
|
+
for (const [key, raw] of entries) {
|
|
11
|
+
if (Array.isArray(raw)) {
|
|
12
|
+
normalized[key] = raw.map((entry) => String(entry ?? '')).sort((left, right) => left.localeCompare(right));
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
if (raw === undefined || raw === null)
|
|
16
|
+
continue;
|
|
17
|
+
normalized[key] = String(raw);
|
|
18
|
+
}
|
|
19
|
+
return normalized;
|
|
20
|
+
}
|
|
21
|
+
function normalizeConflictChecklistItems(value) {
|
|
22
|
+
if (!Array.isArray(value))
|
|
23
|
+
return [];
|
|
24
|
+
return value
|
|
25
|
+
.map((item) => {
|
|
26
|
+
const entry = item && typeof item === 'object' ? item : {};
|
|
27
|
+
return {
|
|
28
|
+
id: String(entry.id || '').trim(),
|
|
29
|
+
title: String(entry.title || '').trim(),
|
|
30
|
+
isCompleted: Boolean(entry.isCompleted),
|
|
31
|
+
order: entry.order === undefined || entry.order === null ? '' : String(entry.order),
|
|
32
|
+
};
|
|
33
|
+
})
|
|
34
|
+
.sort((left, right) => {
|
|
35
|
+
const leftOrder = Number.parseInt(left.order, 10);
|
|
36
|
+
const rightOrder = Number.parseInt(right.order, 10);
|
|
37
|
+
if (Number.isFinite(leftOrder) && Number.isFinite(rightOrder) && leftOrder !== rightOrder) {
|
|
38
|
+
return leftOrder - rightOrder;
|
|
39
|
+
}
|
|
40
|
+
if (left.title !== right.title)
|
|
41
|
+
return left.title.localeCompare(right.title);
|
|
42
|
+
return left.id.localeCompare(right.id);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function normalizeSyncComments(value) {
|
|
46
|
+
if (!Array.isArray(value))
|
|
47
|
+
return [];
|
|
48
|
+
return value
|
|
49
|
+
.map((entry) => {
|
|
50
|
+
const comment = entry && typeof entry === 'object' ? entry : null;
|
|
51
|
+
return {
|
|
52
|
+
id: String(comment?.id || '').trim(),
|
|
53
|
+
author: String(comment?.author || '').trim(),
|
|
54
|
+
submittedAuthor: normalizeSyncOptionalString(comment?.submittedAuthor) || '',
|
|
55
|
+
text: String(comment?.text || ''),
|
|
56
|
+
timestamp: normalizeSyncNullableIsoString(comment?.timestamp) || '',
|
|
57
|
+
actor: comment?.actor ?? null,
|
|
58
|
+
};
|
|
59
|
+
})
|
|
60
|
+
.sort((left, right) => {
|
|
61
|
+
if (left.timestamp !== right.timestamp)
|
|
62
|
+
return left.timestamp.localeCompare(right.timestamp);
|
|
63
|
+
if (left.id !== right.id)
|
|
64
|
+
return left.id.localeCompare(right.id);
|
|
65
|
+
if (left.author !== right.author)
|
|
66
|
+
return left.author.localeCompare(right.author);
|
|
67
|
+
return left.text.localeCompare(right.text);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
function normalizeSyncAttachments(value) {
|
|
71
|
+
if (!Array.isArray(value))
|
|
72
|
+
return [];
|
|
73
|
+
return value
|
|
74
|
+
.map((entry) => {
|
|
75
|
+
if (typeof entry === 'string') {
|
|
76
|
+
const path = entry.trim();
|
|
77
|
+
return {
|
|
78
|
+
path,
|
|
79
|
+
fsPath: '',
|
|
80
|
+
caption: '',
|
|
81
|
+
displayName: '',
|
|
82
|
+
originalFilename: '',
|
|
83
|
+
assetId: '',
|
|
84
|
+
referenceNumber: '',
|
|
85
|
+
taskId: '',
|
|
86
|
+
timestamp: '',
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const attachment = entry && typeof entry === 'object' ? entry : null;
|
|
90
|
+
return {
|
|
91
|
+
path: String(attachment?.path || '').trim(),
|
|
92
|
+
fsPath: normalizeSyncOptionalString(attachment?.fsPath) || '',
|
|
93
|
+
caption: normalizeSyncOptionalString(attachment?.caption) || '',
|
|
94
|
+
displayName: normalizeSyncOptionalString(attachment?.displayName) || '',
|
|
95
|
+
originalFilename: normalizeSyncOptionalString(attachment?.originalFilename) || '',
|
|
96
|
+
assetId: normalizeSyncOptionalId(attachment?.assetId) || '',
|
|
97
|
+
referenceNumber: attachment?.referenceNumber === undefined || attachment.referenceNumber === null
|
|
98
|
+
? ''
|
|
99
|
+
: String(attachment.referenceNumber),
|
|
100
|
+
taskId: normalizeSyncNullableId(attachment?.taskId) || '',
|
|
101
|
+
timestamp: normalizeSyncNullableIsoString(attachment?.timestamp) || '',
|
|
102
|
+
};
|
|
103
|
+
})
|
|
104
|
+
.sort((left, right) => {
|
|
105
|
+
if (left.path !== right.path)
|
|
106
|
+
return left.path.localeCompare(right.path);
|
|
107
|
+
if (left.assetId !== right.assetId)
|
|
108
|
+
return left.assetId.localeCompare(right.assetId);
|
|
109
|
+
if (left.timestamp !== right.timestamp)
|
|
110
|
+
return left.timestamp.localeCompare(right.timestamp);
|
|
111
|
+
return JSON.stringify(left).localeCompare(JSON.stringify(right));
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
export function buildTaskSyncChangeKey(task, archivedValue) {
|
|
115
|
+
const source = serializeTaskForSync(task);
|
|
116
|
+
return JSON.stringify({
|
|
117
|
+
id: source.id,
|
|
118
|
+
title: source.title,
|
|
119
|
+
description: normalizeSyncNullableString(source.description) || '',
|
|
120
|
+
category: source.category,
|
|
121
|
+
type: source.type,
|
|
122
|
+
priority: source.priority === undefined || source.priority === null ? '' : String(source.priority),
|
|
123
|
+
complexity: source.complexity === undefined || source.complexity === null ? '' : String(source.complexity),
|
|
124
|
+
status: source.status,
|
|
125
|
+
approach: normalizeSyncOptionalString(source.approach) || '',
|
|
126
|
+
canceledReason: normalizeSyncNullableString(source.canceledReason) || '',
|
|
127
|
+
createdAt: source.createdAt,
|
|
128
|
+
completedAt: normalizeSyncNullableIsoString(source.completedAt) || '',
|
|
129
|
+
taxonomies: normalizeConflictTaxonomies(source.taxonomies),
|
|
130
|
+
createdBy: normalizeSyncOptionalId(source.createdBy) || '',
|
|
131
|
+
assignee: normalizeSyncOptionalId(source.assignee) || '',
|
|
132
|
+
scheduledDate: normalizeSyncNullableString(source.scheduledDate) || '',
|
|
133
|
+
dueDate: normalizeSyncNullableString(source.dueDate) || '',
|
|
134
|
+
scheduledWeekKey: normalizeSyncNullableString(source.scheduledWeekKey) || '',
|
|
135
|
+
orderInDay: source.orderInDay === undefined || source.orderInDay === null ? '' : String(source.orderInDay),
|
|
136
|
+
workstreamId: normalizeSyncNullableId(source.workstreamId) || '',
|
|
137
|
+
referenceNumber: source.referenceNumber === undefined || source.referenceNumber === null ? '' : String(source.referenceNumber),
|
|
138
|
+
comments: normalizeSyncComments(source.comments),
|
|
139
|
+
attachments: normalizeSyncAttachments(source.attachments),
|
|
140
|
+
checklistItems: normalizeConflictChecklistItems(source.checklistItems),
|
|
141
|
+
isArchived: archivedValue ? '1' : '0'
|
|
142
|
+
});
|
|
143
|
+
}
|
|
@@ -1,16 +1,11 @@
|
|
|
1
|
+
import { normalizeSyncNullableString, normalizeSyncOptionalString, } from './syncFieldSemantics.js';
|
|
1
2
|
function toOptionalString(value) {
|
|
2
|
-
|
|
3
|
-
return undefined;
|
|
4
|
-
const normalized = String(value).trim();
|
|
5
|
-
return normalized.length > 0 ? normalized : undefined;
|
|
3
|
+
return normalizeSyncOptionalString(value);
|
|
6
4
|
}
|
|
7
5
|
function toNullableString(value) {
|
|
8
6
|
if (value === undefined)
|
|
9
7
|
return undefined;
|
|
10
|
-
|
|
11
|
-
return null;
|
|
12
|
-
const normalized = String(value).trim();
|
|
13
|
-
return normalized.length > 0 ? normalized : null;
|
|
8
|
+
return normalizeSyncNullableString(value);
|
|
14
9
|
}
|
|
15
10
|
function toOptionalBoolean(value) {
|
|
16
11
|
return typeof value === 'boolean' ? value : undefined;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { serializeTaskForSync } from './taskSyncPayload.js';
|
|
2
|
+
import { aiProfileSyncResourceAdapter, taskSyncResourceAdapter } from './syncResourceAdapters.js';
|
|
2
3
|
export async function buildWorkspacePullFeed(input) {
|
|
3
4
|
const buildStartedAtMs = Date.now();
|
|
4
5
|
const { core, workspaceId, cursor, pageSize, encodeWorkspaceSyncCursor, contentSyncState, collaborationSyncState } = input;
|
|
@@ -7,7 +8,7 @@ export async function buildWorkspacePullFeed(input) {
|
|
|
7
8
|
const taskRows = core.listTasksForSync(workspaceId, syncCursor, dbFetchLimit);
|
|
8
9
|
const upsertEntries = taskRows
|
|
9
10
|
.map((entry) => {
|
|
10
|
-
const watermark =
|
|
11
|
+
const watermark = taskSyncResourceAdapter.getWatermark(entry.task);
|
|
11
12
|
const taskId = String(entry.task?.id || '').trim();
|
|
12
13
|
return {
|
|
13
14
|
kind: 'upsert',
|
|
@@ -55,10 +56,10 @@ export async function buildWorkspacePullFeed(input) {
|
|
|
55
56
|
workstream
|
|
56
57
|
}))
|
|
57
58
|
.filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.workstream.id.length > 0);
|
|
58
|
-
const aiProfileEntries = core.listAiProfiles(workspaceId)
|
|
59
|
+
const aiProfileEntries = core.listAiProfiles(workspaceId, { includeArchived: true })
|
|
59
60
|
.map((profile) => ({
|
|
60
61
|
kind: 'ai-profile',
|
|
61
|
-
watermark:
|
|
62
|
+
watermark: aiProfileSyncResourceAdapter.getWatermark(profile),
|
|
62
63
|
sortId: `ai-profile:${String(profile.id || '').trim()}`,
|
|
63
64
|
profile
|
|
64
65
|
}))
|
|
@@ -126,6 +126,7 @@ describe('workspacePullFeed', () => {
|
|
|
126
126
|
userId: 'user-1',
|
|
127
127
|
email: 'person@example.com',
|
|
128
128
|
displayName: 'Person',
|
|
129
|
+
avatarUrl: '/api/taskforce/context/users%2Fuser-1%2Fprofile%2Favatar%2Fheadshot.webp',
|
|
129
130
|
role: 'owner',
|
|
130
131
|
permissionMode: 'read-write',
|
|
131
132
|
betaAccess: true,
|
|
@@ -167,6 +168,48 @@ describe('workspacePullFeed', () => {
|
|
|
167
168
|
displayName: 'Person',
|
|
168
169
|
})
|
|
169
170
|
]);
|
|
171
|
+
expect(result.workspaceMembers[0]).not.toHaveProperty('avatarUrl');
|
|
172
|
+
});
|
|
173
|
+
it('includes ai-profile upserts with avatar fields in the pull feed', async () => {
|
|
174
|
+
const core = createCoreMock();
|
|
175
|
+
core.listAiProfiles = () => [
|
|
176
|
+
{
|
|
177
|
+
id: 'ai-profile-1',
|
|
178
|
+
workspaceId: 'workspace-1',
|
|
179
|
+
profileToken: 'tfp_test',
|
|
180
|
+
name: 'Codex',
|
|
181
|
+
username: 'codex',
|
|
182
|
+
icon: 'Bot',
|
|
183
|
+
color: '#0ea5e9',
|
|
184
|
+
avatarUrl: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fai-profiles%2Fai-profile-1%2Favatar%2Favatar.webp',
|
|
185
|
+
avatarSourceUrl: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fai-profiles%2Fai-profile-1%2Favatar%2Fsource.webp',
|
|
186
|
+
createdAt: '2026-04-10T10:00:00.000Z',
|
|
187
|
+
updatedAt: '2026-04-10T10:05:00.000Z',
|
|
188
|
+
lastActiveAt: '2026-04-10T10:05:00.000Z'
|
|
189
|
+
}
|
|
190
|
+
];
|
|
191
|
+
const result = await buildWorkspacePullFeed({
|
|
192
|
+
core,
|
|
193
|
+
workspaceId: 'workspace-1',
|
|
194
|
+
cursor: null,
|
|
195
|
+
pageSize: 10,
|
|
196
|
+
encodeWorkspaceSyncCursor: encodeCursor,
|
|
197
|
+
contentSyncState: createContentSyncStateMock(),
|
|
198
|
+
collaborationSyncState: createCollaborationSyncStateMock(),
|
|
199
|
+
});
|
|
200
|
+
expect(result.changes).toEqual([
|
|
201
|
+
expect.objectContaining({
|
|
202
|
+
op: 'ai-profile-upsert',
|
|
203
|
+
watermark: '2026-04-10T10:05:00.000Z',
|
|
204
|
+
profile: expect.objectContaining({
|
|
205
|
+
id: 'ai-profile-1',
|
|
206
|
+
avatarUrl: expect.stringContaining('/api/taskforce/context/'),
|
|
207
|
+
avatarSourceUrl: expect.stringContaining('/api/taskforce/context/')
|
|
208
|
+
})
|
|
209
|
+
})
|
|
210
|
+
]);
|
|
211
|
+
expect(result.nextCursor).toBe('2026-04-10T10:05:00.000Z|ai-profile:ai-profile-1');
|
|
212
|
+
expect(result.hasMore).toBe(false);
|
|
170
213
|
});
|
|
171
214
|
it('hydrates document and asset payloads only for entries included in the current page', async () => {
|
|
172
215
|
const core = createCoreMock();
|