@taskforcehq/taskforce 0.3.304 → 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/README.md +140 -247
- package/dist/Taskforce.module.css +295 -89
- package/dist/TaskforceCore.js +378 -111
- package/dist/TaskforceCore.test.js +1221 -107
- 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 +52 -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/DocumentIndex.d.ts +1 -1
- package/dist/components/features/DocumentIndex.js +1 -1
- package/dist/components/features/DocumentViewer.d.ts +6 -2
- package/dist/components/features/DocumentViewer.js +90 -10
- package/dist/components/features/DocumentWorkspace.d.ts +5 -22
- package/dist/components/features/DocumentWorkspace.js +12 -155
- package/dist/components/features/DocumentWorkspace.test.js +226 -6
- package/dist/components/features/TaskSettings.js +29 -3
- package/dist/components/features/TaskSettings.test.js +42 -1
- package/dist/components/features/documentWorkspaceModel.d.ts +24 -0
- package/dist/components/features/documentWorkspaceModel.js +57 -0
- package/dist/components/features/useDocumentWorkspaceController.d.ts +34 -0
- package/dist/components/features/useDocumentWorkspaceController.js +113 -0
- 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.d.ts +8 -8
- package/dist/components/task/TaskForm.js +362 -230
- package/dist/components/task/TaskForm.test.js +558 -381
- package/dist/components/task/TaskKanban.d.ts +2 -1
- package/dist/components/task/TaskKanban.js +96 -14
- 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 -17
- package/dist/components/views/PlanComparisonPage.test.js +289 -41
- package/dist/components/views/PlansPage.js +239 -106
- package/dist/components/views/PlansPage.test.js +593 -42
- package/dist/components/views/StandaloneLayout.js +310 -554
- package/dist/components/views/panels/FilterToolbar.test.js +6 -4
- 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 +27 -14
- package/dist/components/views/standalone/modals/AccountHubModal.js +87 -15
- package/dist/components/views/standalone/modals/AccountHubModal.test.js +42 -2
- 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/config/envSchema.js +1 -1
- package/dist/core/AiProfileService.d.ts +45 -3
- package/dist/core/AiProfileService.js +774 -63
- package/dist/core/AiProfileService.test.js +22 -2
- package/dist/core/AiProfiles.test.js +761 -2
- 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.d.ts +1 -0
- package/dist/core/AuthTokenService.test.js +78 -0
- package/dist/core/EntitlementsPolicy.test.js +77 -15
- package/dist/core/GlobalSettingsService.js +115 -6
- package/dist/core/PlanEntitlementService.d.ts +30 -3
- package/dist/core/PlanEntitlementService.js +454 -93
- package/dist/core/PlanFeatureCatalog.test.d.ts +1 -0
- package/dist/core/PlanFeatureCatalog.test.js +84 -0
- package/dist/core/PlanVersionPolicy.test.js +54 -28
- package/dist/core/PlanningEntities.test.js +52 -0
- package/dist/core/SignupMonetizationSettings.test.js +241 -44
- package/dist/core/SyncReconciliationService.js +2 -65
- package/dist/core/SystemAdmin.test.js +244 -88
- package/dist/core/TaskAttachmentsCanonical.test.js +51 -1
- package/dist/core/TaskTaxonomyValidation.test.js +53 -0
- package/dist/core/Taskforce.d.ts +228 -14
- package/dist/core/Taskforce.js +814 -201
- 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 +128 -27
- package/dist/core/shared.d.ts +3 -1
- package/dist/core/shared.js +16 -1
- package/dist/core/types.d.ts +68 -2
- package/dist/hooks/auth/useTaskforceAuthBootstrap.js +26 -1
- package/dist/hooks/sync/auth.js +3 -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 +16 -3
- package/dist/hooks/sync/transfers.js +186 -65
- 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 +16 -6
- package/dist/hooks/useSyncOrchestrator.js +424 -175
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +487 -10
- package/dist/hooks/useTaskData.js +8 -3
- package/dist/hooks/useTaskData.test.js +45 -0
- package/dist/hooks/useTaskMutations.d.ts +2 -1
- package/dist/hooks/useTaskMutations.js +12 -1
- package/dist/hooks/useTaskMutations.test.js +30 -1
- package/dist/hooks/useTaskforce.d.ts +65 -4
- package/dist/hooks/useTaskforce.js +564 -243
- 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 +2454 -208
- package/dist/hooks/useWorkspaceSyncController.d.ts +4 -1
- package/dist/hooks/useWorkspaceSyncController.js +17 -2
- package/dist/hooks/useWorkspaceSyncController.test.js +1 -0
- 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/adminWorkspaceRegistrar.d.ts +2 -0
- package/dist/mcp/adminWorkspaceRegistrar.js +154 -0
- package/dist/mcp/collaborationRegistrar.d.ts +2 -0
- package/dist/mcp/collaborationRegistrar.js +71 -0
- package/dist/mcp/documentAssetRegistrar.d.ts +2 -0
- package/dist/mcp/documentAssetRegistrar.js +346 -0
- package/dist/mcp/runtime.d.ts +2 -0
- package/dist/mcp/runtime.js +2433 -3534
- package/dist/mcp/runtime.test.js +440 -2
- package/dist/mcp/taskPlanningRegistrar.d.ts +2 -0
- package/dist/mcp/taskPlanningRegistrar.js +418 -0
- package/dist/mcp/toolCatalog.d.ts +35 -0
- package/dist/mcp/toolCatalog.js +3 -0
- 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 +4 -5
- package/dist/server/routes/admin.js +410 -80
- package/dist/server/routes/annotatedAttachments.d.ts +1 -1
- package/dist/server/routes/annotatedAttachments.js +1 -1
- package/dist/server/routes/auth.d.ts +16 -6
- package/dist/server/routes/auth.js +1015 -118
- package/dist/server/routes/authSupport.d.ts +30 -0
- package/dist/server/routes/authSupport.js +81 -0
- package/dist/server/routes/billing.d.ts +1 -1
- package/dist/server/routes/billing.js +276 -34
- package/dist/server/routes/billing.test.js +760 -52
- package/dist/server/routes/documentReviews.d.ts +1 -1
- package/dist/server/routes/documentReviews.js +1 -1
- package/dist/server/routes/documents.d.ts +5 -2
- package/dist/server/routes/documents.js +242 -74
- package/dist/server/routes/primitives.d.ts +11 -0
- package/dist/server/routes/primitives.js +73 -0
- package/dist/server/routes/resources.d.ts +1 -1
- package/dist/server/routes/resources.js +1 -1
- package/dist/server/routes/shared.d.ts +29 -3
- package/dist/server/routes/shared.js +69 -4
- package/dist/server/routes/sync.d.ts +1 -1
- package/dist/server/routes/sync.integration.test.js +437 -39
- package/dist/server/routes/sync.js +62 -606
- 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.d.ts +1 -1
- package/dist/server/routes/tasks.js +44 -2
- package/dist/server/routes/workspaces.d.ts +1 -1
- package/dist/server/routes/workspaces.js +37 -6
- package/dist/server/routes.d.ts +2 -9
- package/dist/server/routes.js +136 -165
- package/dist/server/routes.test.js +2457 -162
- 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 +33 -77
- package/dist/sync/syncApplyHandlers.d.ts +7 -0
- package/dist/sync/syncApplyHandlers.js +28 -13
- package/dist/sync/syncApplyHandlers.test.js +72 -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 +6 -0
- package/dist/sync/syncService.js +60 -0
- package/dist/sync/syncService.test.js +71 -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 +48 -0
- package/dist/sync/workspaceSyncModel.js +195 -31
- package/dist/sync/workspaceSyncModel.test.js +325 -12
- package/dist/sync/workspaceSyncState.d.ts +19 -0
- package/dist/sync/workspaceSyncState.js +3 -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-BlS-cqnl.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-BjR6iBf9.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-DZX4jk7e.js +9 -0
- package/dist/ui/assets/{WorkflowsModule-DnFqdUME.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-I7ZwG1z_.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.d.ts +19 -0
- package/dist/utils/commercialLifecyclePresentation.js +199 -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 +10 -2
- package/scripts/check-cloud-mcp.mjs +83 -0
- package/scripts/export-sqlite-to-postgres.mjs +7 -9
- package/scripts/playwright-auth.sh +5 -1
- package/scripts/run-billing-performance-smoke.sh +24 -0
- 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-BLWVbuKP.js +0 -1
- package/dist/ui/assets/DocumentWorkspace-BH_6DF6x.js +0 -250
- package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +0 -1
- package/dist/ui/assets/PlansPage-C2dd2n1X.css +0 -1
- package/dist/ui/assets/PlansPage-CSDQ4sLa.js +0 -1
- package/dist/ui/assets/RussoOne-Regular-C3BxZIj7.ttf +0 -0
- package/dist/ui/assets/TaskSettings-CEDy34Jo.js +0 -9
- package/dist/ui/assets/index-DXUdtH1B.js +0 -6
- package/dist/ui/assets/index-Ii0B0rTO.css +0 -1
- package/scripts/migrate-planning-model.ts +0 -233
|
@@ -10,6 +10,28 @@ function createProjectRoot(prefix = 'taskforce-ai-profiles-') {
|
|
|
10
10
|
return root;
|
|
11
11
|
}
|
|
12
12
|
const createdRoots = [];
|
|
13
|
+
function enableAiProfileSeatLimit(core, workspaceId, userId, limit) {
|
|
14
|
+
core.upsertPlanCatalog({ planId: 'free', displayName: 'Free', status: 'active' });
|
|
15
|
+
core.ensureBootstrapUserAccess({
|
|
16
|
+
userId,
|
|
17
|
+
workspaceId,
|
|
18
|
+
role: 'owner',
|
|
19
|
+
email: `${userId}@example.com`
|
|
20
|
+
});
|
|
21
|
+
core.updatePlanVersionFeatures({
|
|
22
|
+
planVersionId: 'free-v1',
|
|
23
|
+
features: [
|
|
24
|
+
{ featureKey: 'collaboration.ai_profiles', access: 'limited', config: { maxAiProfiles: limit } }
|
|
25
|
+
]
|
|
26
|
+
});
|
|
27
|
+
core.updateAccountEntitlement({
|
|
28
|
+
accountId: userId,
|
|
29
|
+
planVersionId: 'free-v1',
|
|
30
|
+
entitlementState: 'active',
|
|
31
|
+
actorUserId: 'system-admin',
|
|
32
|
+
actorRole: 'system_admin'
|
|
33
|
+
});
|
|
34
|
+
}
|
|
13
35
|
afterEach(() => {
|
|
14
36
|
while (createdRoots.length > 0) {
|
|
15
37
|
const root = createdRoots.pop();
|
|
@@ -76,6 +98,128 @@ describe('TaskforceCore ai profiles', () => {
|
|
|
76
98
|
expect(second.profile.icon).toBe('Sparkles');
|
|
77
99
|
expect(second.profile.color).toBe('#0ea5e9');
|
|
78
100
|
});
|
|
101
|
+
it('updates ai profile avatar urls without changing the registered icon fallback', () => {
|
|
102
|
+
const projectRoot = createProjectRoot();
|
|
103
|
+
createdRoots.push(projectRoot);
|
|
104
|
+
const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
105
|
+
core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
|
|
106
|
+
const resolved = core.resolveAiProfile({
|
|
107
|
+
workspaceId: 'workspace-1',
|
|
108
|
+
name: 'Codex',
|
|
109
|
+
icon: 'Bot',
|
|
110
|
+
color: '#0ea5e9'
|
|
111
|
+
});
|
|
112
|
+
const avatarUrl = '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fai-profiles%2Fprofile%2Favatar%2Favatar.webp';
|
|
113
|
+
const avatarSourceUrl = '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fai-profiles%2Fprofile%2Favatar%2Fsource.webp';
|
|
114
|
+
const updated = core.updateAiProfileAvatar({
|
|
115
|
+
workspaceId: 'workspace-1',
|
|
116
|
+
profileId: resolved.profile.id,
|
|
117
|
+
avatarUrl,
|
|
118
|
+
avatarSourceUrl
|
|
119
|
+
});
|
|
120
|
+
expect(updated.avatarUrl).toBe(avatarUrl);
|
|
121
|
+
expect(updated.avatarSourceUrl).toBe(avatarSourceUrl);
|
|
122
|
+
expect(updated.icon).toBe('Bot');
|
|
123
|
+
expect(core.listAiProfiles('workspace-1')[0]?.avatarUrl).toBe(avatarUrl);
|
|
124
|
+
expect(core.listAiProfiles('workspace-1')[0]?.avatarSourceUrl).toBe(avatarSourceUrl);
|
|
125
|
+
const cleared = core.updateAiProfileAvatar({
|
|
126
|
+
workspaceId: 'workspace-1',
|
|
127
|
+
profileId: resolved.profile.id,
|
|
128
|
+
avatarUrl: null,
|
|
129
|
+
avatarSourceUrl: null
|
|
130
|
+
});
|
|
131
|
+
expect(cleared.avatarUrl).toBeNull();
|
|
132
|
+
expect(cleared.avatarSourceUrl).toBeNull();
|
|
133
|
+
});
|
|
134
|
+
it('preserves synced ai profile avatars when metadata-only sync payloads omit avatar fields', () => {
|
|
135
|
+
const projectRoot = createProjectRoot();
|
|
136
|
+
createdRoots.push(projectRoot);
|
|
137
|
+
const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
138
|
+
core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
|
|
139
|
+
const resolved = core.resolveAiProfile({
|
|
140
|
+
workspaceId: 'workspace-1',
|
|
141
|
+
name: 'Codex',
|
|
142
|
+
icon: 'Bot',
|
|
143
|
+
color: '#0ea5e9'
|
|
144
|
+
});
|
|
145
|
+
const avatarUrl = '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fai-profiles%2Fprofile%2Favatar%2Favatar.webp';
|
|
146
|
+
const avatarSourceUrl = '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fai-profiles%2Fprofile%2Favatar%2Fsource.webp';
|
|
147
|
+
const updated = core.updateAiProfileAvatar({
|
|
148
|
+
workspaceId: 'workspace-1',
|
|
149
|
+
profileId: resolved.profile.id,
|
|
150
|
+
avatarUrl,
|
|
151
|
+
avatarSourceUrl
|
|
152
|
+
});
|
|
153
|
+
const { avatarUrl: _avatarUrl, avatarSourceUrl: _avatarSourceUrl, ...metadataOnlyProfile } = updated;
|
|
154
|
+
const metadataUpdatedAt = new Date(Date.parse(updated.updatedAt) + 1000).toISOString();
|
|
155
|
+
const replayed = core.upsertAiProfileForSync({
|
|
156
|
+
...metadataOnlyProfile,
|
|
157
|
+
name: 'Codex Metadata Refresh',
|
|
158
|
+
updatedAt: metadataUpdatedAt,
|
|
159
|
+
lastActiveAt: metadataUpdatedAt
|
|
160
|
+
});
|
|
161
|
+
expect(replayed.name).toBe('Codex Metadata Refresh');
|
|
162
|
+
expect(replayed.avatarUrl).toBe(avatarUrl);
|
|
163
|
+
expect(replayed.avatarSourceUrl).toBe(avatarSourceUrl);
|
|
164
|
+
});
|
|
165
|
+
it('clears synced ai profile avatars when sync payloads explicitly send null avatar fields', () => {
|
|
166
|
+
const projectRoot = createProjectRoot();
|
|
167
|
+
createdRoots.push(projectRoot);
|
|
168
|
+
const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
169
|
+
core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
|
|
170
|
+
const resolved = core.resolveAiProfile({
|
|
171
|
+
workspaceId: 'workspace-1',
|
|
172
|
+
name: 'Codex',
|
|
173
|
+
icon: 'Bot',
|
|
174
|
+
color: '#0ea5e9'
|
|
175
|
+
});
|
|
176
|
+
const updated = core.updateAiProfileAvatar({
|
|
177
|
+
workspaceId: 'workspace-1',
|
|
178
|
+
profileId: resolved.profile.id,
|
|
179
|
+
avatarUrl: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fai-profiles%2Fprofile%2Favatar%2Favatar.webp',
|
|
180
|
+
avatarSourceUrl: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fai-profiles%2Fprofile%2Favatar%2Fsource.webp'
|
|
181
|
+
});
|
|
182
|
+
const clearedAt = new Date(Date.parse(updated.updatedAt) + 1000).toISOString();
|
|
183
|
+
const cleared = core.upsertAiProfileForSync({
|
|
184
|
+
...updated,
|
|
185
|
+
avatarUrl: null,
|
|
186
|
+
avatarSourceUrl: null,
|
|
187
|
+
updatedAt: clearedAt,
|
|
188
|
+
lastActiveAt: clearedAt
|
|
189
|
+
});
|
|
190
|
+
expect(cleared.avatarUrl).toBeNull();
|
|
191
|
+
expect(cleared.avatarSourceUrl).toBeNull();
|
|
192
|
+
});
|
|
193
|
+
it('repairs missing synced ai profile avatars when timestamps match', () => {
|
|
194
|
+
const projectRoot = createProjectRoot();
|
|
195
|
+
createdRoots.push(projectRoot);
|
|
196
|
+
const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
197
|
+
core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
|
|
198
|
+
const resolved = core.resolveAiProfile({
|
|
199
|
+
workspaceId: 'workspace-1',
|
|
200
|
+
name: 'Codex',
|
|
201
|
+
icon: 'Bot',
|
|
202
|
+
color: '#0ea5e9'
|
|
203
|
+
});
|
|
204
|
+
const avatarUrl = '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fai-profiles%2Fprofile%2Favatar%2Favatar.webp';
|
|
205
|
+
const avatarSourceUrl = '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fai-profiles%2Fprofile%2Favatar%2Fsource.webp';
|
|
206
|
+
const updated = core.updateAiProfileAvatar({
|
|
207
|
+
workspaceId: 'workspace-1',
|
|
208
|
+
profileId: resolved.profile.id,
|
|
209
|
+
avatarUrl,
|
|
210
|
+
avatarSourceUrl
|
|
211
|
+
});
|
|
212
|
+
core.db.prepare(`
|
|
213
|
+
UPDATE ai_profiles
|
|
214
|
+
SET avatar_url = NULL,
|
|
215
|
+
avatar_source_url = NULL
|
|
216
|
+
WHERE id = ?
|
|
217
|
+
`).run(updated.id);
|
|
218
|
+
const repaired = core.upsertAiProfileForSync(updated);
|
|
219
|
+
expect(repaired.updatedAt).toBe(updated.updatedAt);
|
|
220
|
+
expect(repaired.avatarUrl).toBe(avatarUrl);
|
|
221
|
+
expect(repaired.avatarSourceUrl).toBe(avatarSourceUrl);
|
|
222
|
+
});
|
|
79
223
|
it('stores expanded ai profile fields and preserves them when omitted on re-resolve', () => {
|
|
80
224
|
const projectRoot = createProjectRoot();
|
|
81
225
|
createdRoots.push(projectRoot);
|
|
@@ -103,6 +247,125 @@ describe('TaskforceCore ai profiles', () => {
|
|
|
103
247
|
expect(second.profile.surfaceType).toBe('coding_tool');
|
|
104
248
|
expect(second.profile.color).toBe('#0ea5e9');
|
|
105
249
|
});
|
|
250
|
+
it('applies default metadata for named agent profiles when optional fields are omitted', () => {
|
|
251
|
+
const projectRoot = createProjectRoot();
|
|
252
|
+
createdRoots.push(projectRoot);
|
|
253
|
+
const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
254
|
+
core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
|
|
255
|
+
const resolved = core.resolveAiProfile({
|
|
256
|
+
workspaceId: 'workspace-1',
|
|
257
|
+
name: 'John Drake',
|
|
258
|
+
surfaceType: 'agent'
|
|
259
|
+
});
|
|
260
|
+
expect(resolved.profile.description).toBe('John Drake AI collaborator registered in Taskforce.');
|
|
261
|
+
expect(resolved.profile.role).toBe('AI Collaborator');
|
|
262
|
+
expect(resolved.profile.provider).toBeNull();
|
|
263
|
+
expect(resolved.profile.model).toBeNull();
|
|
264
|
+
expect(resolved.profile.surfaceType).toBe('agent');
|
|
265
|
+
});
|
|
266
|
+
it('self-heals sparse named agent profiles on re-resolve', () => {
|
|
267
|
+
const projectRoot = createProjectRoot();
|
|
268
|
+
createdRoots.push(projectRoot);
|
|
269
|
+
const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
270
|
+
core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
|
|
271
|
+
const first = core.resolveAiProfile({
|
|
272
|
+
workspaceId: 'workspace-1',
|
|
273
|
+
name: 'John Drake'
|
|
274
|
+
});
|
|
275
|
+
expect(first.profile.description).toBeNull();
|
|
276
|
+
expect(first.profile.role).toBeNull();
|
|
277
|
+
const second = core.resolveAiProfile({
|
|
278
|
+
workspaceId: 'workspace-1',
|
|
279
|
+
name: 'John Drake',
|
|
280
|
+
surfaceType: 'agent',
|
|
281
|
+
profileToken: first.profile.profileToken
|
|
282
|
+
});
|
|
283
|
+
expect(second.profile.id).toBe(first.profile.id);
|
|
284
|
+
expect(second.profile.description).toBe('John Drake AI collaborator registered in Taskforce.');
|
|
285
|
+
expect(second.profile.role).toBe('AI Collaborator');
|
|
286
|
+
expect(second.profile.provider).toBeNull();
|
|
287
|
+
expect(second.profile.model).toBeNull();
|
|
288
|
+
expect(second.profile.surfaceType).toBe('agent');
|
|
289
|
+
});
|
|
290
|
+
it('durably repairs sparse named agent profiles when listing profiles', () => {
|
|
291
|
+
const projectRoot = createProjectRoot();
|
|
292
|
+
createdRoots.push(projectRoot);
|
|
293
|
+
const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
294
|
+
core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
|
|
295
|
+
const first = core.resolveAiProfile({
|
|
296
|
+
workspaceId: 'workspace-1',
|
|
297
|
+
name: 'John Drake'
|
|
298
|
+
});
|
|
299
|
+
expect(first.profile.description).toBeNull();
|
|
300
|
+
expect(first.profile.role).toBeNull();
|
|
301
|
+
core.resolveAiProfile({
|
|
302
|
+
workspaceId: 'workspace-1',
|
|
303
|
+
name: 'John Drake',
|
|
304
|
+
surfaceType: 'agent',
|
|
305
|
+
profileToken: first.profile.profileToken
|
|
306
|
+
});
|
|
307
|
+
core.db.prepare(`
|
|
308
|
+
UPDATE ai_profiles
|
|
309
|
+
SET description = NULL, role = NULL, provider = NULL, model = NULL
|
|
310
|
+
WHERE tenant_id = ? AND workspace_id = ? AND id = ?
|
|
311
|
+
`).run('default', 'workspace-1', first.profile.id);
|
|
312
|
+
const listed = core.listAiProfiles('workspace-1')[0];
|
|
313
|
+
expect(listed.description).toBe('John Drake AI collaborator registered in Taskforce.');
|
|
314
|
+
expect(listed.role).toBe('AI Collaborator');
|
|
315
|
+
expect(listed.provider).toBeNull();
|
|
316
|
+
expect(listed.model).toBeNull();
|
|
317
|
+
const repaired = core.db.prepare(`
|
|
318
|
+
SELECT description, role, provider, model
|
|
319
|
+
FROM ai_profiles
|
|
320
|
+
WHERE tenant_id = ? AND workspace_id = ? AND id = ?
|
|
321
|
+
LIMIT 1
|
|
322
|
+
`).get('default', 'workspace-1', first.profile.id);
|
|
323
|
+
expect(repaired).toEqual({
|
|
324
|
+
description: 'John Drake AI collaborator registered in Taskforce.',
|
|
325
|
+
role: 'AI Collaborator',
|
|
326
|
+
provider: null,
|
|
327
|
+
model: null
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
it('durably repairs sparse coding-tool profiles when resolving by token lookup', () => {
|
|
331
|
+
const projectRoot = createProjectRoot();
|
|
332
|
+
createdRoots.push(projectRoot);
|
|
333
|
+
const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
334
|
+
core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
|
|
335
|
+
const resolved = core.resolveAiProfile({
|
|
336
|
+
workspaceId: 'workspace-1',
|
|
337
|
+
name: 'Codex',
|
|
338
|
+
surfaceType: 'coding_tool'
|
|
339
|
+
});
|
|
340
|
+
core.db.prepare(`
|
|
341
|
+
UPDATE ai_profiles
|
|
342
|
+
SET description = NULL, role = NULL, provider = NULL, model = NULL
|
|
343
|
+
WHERE tenant_id = ? AND workspace_id = ? AND id = ?
|
|
344
|
+
`).run('default', 'workspace-1', resolved.profile.id);
|
|
345
|
+
const fetched = core.getAiProfileByToken({
|
|
346
|
+
workspaceId: 'workspace-1',
|
|
347
|
+
profileToken: resolved.profile.profileToken
|
|
348
|
+
});
|
|
349
|
+
expect(fetched).toEqual(expect.objectContaining({
|
|
350
|
+
description: 'Codex coding agent connected through MCP.',
|
|
351
|
+
role: 'Coding Agent',
|
|
352
|
+
provider: 'OpenAI',
|
|
353
|
+
model: 'Codex',
|
|
354
|
+
surfaceType: 'coding_tool'
|
|
355
|
+
}));
|
|
356
|
+
const repaired = core.db.prepare(`
|
|
357
|
+
SELECT description, role, provider, model
|
|
358
|
+
FROM ai_profiles
|
|
359
|
+
WHERE tenant_id = ? AND workspace_id = ? AND id = ?
|
|
360
|
+
LIMIT 1
|
|
361
|
+
`).get('default', 'workspace-1', resolved.profile.id);
|
|
362
|
+
expect(repaired).toEqual({
|
|
363
|
+
description: 'Codex coding agent connected through MCP.',
|
|
364
|
+
role: 'Coding Agent',
|
|
365
|
+
provider: 'OpenAI',
|
|
366
|
+
model: 'Codex'
|
|
367
|
+
});
|
|
368
|
+
});
|
|
106
369
|
it('ignores empty-string optional ai profile updates instead of clearing stored values', () => {
|
|
107
370
|
const projectRoot = createProjectRoot();
|
|
108
371
|
createdRoots.push(projectRoot);
|
|
@@ -368,6 +631,216 @@ describe('TaskforceCore ai profiles', () => {
|
|
|
368
631
|
expect(core.listAiProfiles('workspace-1')).toHaveLength(1);
|
|
369
632
|
expect(core.listAiProfiles('workspace-2')).toHaveLength(1);
|
|
370
633
|
});
|
|
634
|
+
it('allows token-based re-resolve even when the ai profile limit is full', () => {
|
|
635
|
+
const projectRoot = createProjectRoot();
|
|
636
|
+
createdRoots.push(projectRoot);
|
|
637
|
+
const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
638
|
+
core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
|
|
639
|
+
enableAiProfileSeatLimit(core, 'workspace-1', 'owner-u1', 1);
|
|
640
|
+
const first = core.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Codex' });
|
|
641
|
+
const second = core.resolveAiProfile({
|
|
642
|
+
workspaceId: 'workspace-1',
|
|
643
|
+
name: 'Codex Prime',
|
|
644
|
+
icon: 'Sparkles',
|
|
645
|
+
profileToken: first.profile.profileToken
|
|
646
|
+
});
|
|
647
|
+
expect(second.created).toBe(false);
|
|
648
|
+
expect(second.profile.id).toBe(first.profile.id);
|
|
649
|
+
expect(second.profile.name).toBe('Codex Prime');
|
|
650
|
+
expect(core.listAiProfiles('workspace-1')).toHaveLength(1);
|
|
651
|
+
});
|
|
652
|
+
it('reuses an exact-match ai profile even when the ai profile limit is full', () => {
|
|
653
|
+
const projectRoot = createProjectRoot();
|
|
654
|
+
createdRoots.push(projectRoot);
|
|
655
|
+
const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
656
|
+
core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
|
|
657
|
+
enableAiProfileSeatLimit(core, 'workspace-1', 'owner-u1', 1);
|
|
658
|
+
const first = core.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Automation Codex' }, { trustedSeatScope: 'cloud_metered' });
|
|
659
|
+
const second = core.resolveAiProfile({
|
|
660
|
+
workspaceId: 'workspace-1',
|
|
661
|
+
name: 'Automation Codex',
|
|
662
|
+
role: 'Assistant',
|
|
663
|
+
}, { trustedSeatScope: 'cloud_metered' });
|
|
664
|
+
expect(second.created).toBe(false);
|
|
665
|
+
expect(second.profile.id).toBe(first.profile.id);
|
|
666
|
+
expect(second.profile.role).toBe('Assistant');
|
|
667
|
+
expect(core.listAiProfiles('workspace-1')).toHaveLength(1);
|
|
668
|
+
});
|
|
669
|
+
it('blocks net-new ai profile creation when the ai profile limit is full', () => {
|
|
670
|
+
const projectRoot = createProjectRoot();
|
|
671
|
+
createdRoots.push(projectRoot);
|
|
672
|
+
const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
673
|
+
core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
|
|
674
|
+
enableAiProfileSeatLimit(core, 'workspace-1', 'owner-u1', 1);
|
|
675
|
+
const first = core.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Codex' }, { trustedSeatScope: 'cloud_metered' });
|
|
676
|
+
expect(first.created).toBe(true);
|
|
677
|
+
try {
|
|
678
|
+
core.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Claude' }, { trustedSeatScope: 'cloud_metered' });
|
|
679
|
+
throw new Error('Expected AI profile creation to be blocked');
|
|
680
|
+
}
|
|
681
|
+
catch (error) {
|
|
682
|
+
expect(error.message).toContain('No AI seats are available for this workspace');
|
|
683
|
+
expect(error.code).toBe('AI_PROFILE_LIMIT_REACHED');
|
|
684
|
+
expect(error.statusCode).toBe(403);
|
|
685
|
+
expect(error.details).toEqual({
|
|
686
|
+
workspaceId: 'workspace-1',
|
|
687
|
+
limit: 1,
|
|
688
|
+
used: 1,
|
|
689
|
+
remaining: 0
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
it('counts only cloud-metered ai profiles toward the seat limit', () => {
|
|
694
|
+
const projectRoot = createProjectRoot();
|
|
695
|
+
createdRoots.push(projectRoot);
|
|
696
|
+
const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
697
|
+
core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
|
|
698
|
+
enableAiProfileSeatLimit(core, 'workspace-1', 'owner-u1', 1);
|
|
699
|
+
const local = core.resolveAiProfile({
|
|
700
|
+
workspaceId: 'workspace-1',
|
|
701
|
+
name: 'Local Codex',
|
|
702
|
+
}, { trustedSeatScope: 'local_unmetered' });
|
|
703
|
+
const cloud = core.resolveAiProfile({
|
|
704
|
+
workspaceId: 'workspace-1',
|
|
705
|
+
name: 'Cloud Codex',
|
|
706
|
+
}, { trustedSeatScope: 'cloud_metered' });
|
|
707
|
+
const extraLocal = core.resolveAiProfile({
|
|
708
|
+
workspaceId: 'workspace-1',
|
|
709
|
+
name: 'Another Local Agent',
|
|
710
|
+
}, { trustedSeatScope: 'local_unmetered' });
|
|
711
|
+
expect(local.profile.seatScope).toBe('local_unmetered');
|
|
712
|
+
expect(cloud.profile.seatScope).toBe('cloud_metered');
|
|
713
|
+
expect(extraLocal.profile.seatScope).toBe('local_unmetered');
|
|
714
|
+
expect(core.countAiProfileUsage('workspace-1')).toEqual({
|
|
715
|
+
used: 1,
|
|
716
|
+
limit: 1,
|
|
717
|
+
remaining: 0
|
|
718
|
+
});
|
|
719
|
+
expect(core.listAiProfiles('workspace-1')).toHaveLength(3);
|
|
720
|
+
});
|
|
721
|
+
it('honors configured ai profile limits even when legacy plan data stores the feature as enabled', () => {
|
|
722
|
+
const projectRoot = createProjectRoot();
|
|
723
|
+
createdRoots.push(projectRoot);
|
|
724
|
+
const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
725
|
+
core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
|
|
726
|
+
core.upsertPlanCatalog({ planId: 'free', displayName: 'Free', status: 'active' });
|
|
727
|
+
core.ensureBootstrapUserAccess({
|
|
728
|
+
userId: 'owner-u1',
|
|
729
|
+
workspaceId: 'workspace-1',
|
|
730
|
+
role: 'owner',
|
|
731
|
+
email: 'owner-u1@example.com'
|
|
732
|
+
});
|
|
733
|
+
core.updatePlanVersionFeatures({
|
|
734
|
+
planVersionId: 'free-v1',
|
|
735
|
+
features: [
|
|
736
|
+
{ featureKey: 'collaboration.ai_profiles', access: 'enabled', config: { maxAiProfiles: 20 } }
|
|
737
|
+
]
|
|
738
|
+
});
|
|
739
|
+
core.db.prepare(`
|
|
740
|
+
UPDATE plan_feature_matrix
|
|
741
|
+
SET access = 'enabled',
|
|
742
|
+
config_json = ?
|
|
743
|
+
WHERE tenant_id = ? AND plan_version_id = ? AND feature_key = ?
|
|
744
|
+
`).run(JSON.stringify({ maxAiProfiles: 20 }), 'default', 'free-v1', 'collaboration.ai_profiles');
|
|
745
|
+
core.updateAccountEntitlement({
|
|
746
|
+
accountId: 'owner-u1',
|
|
747
|
+
planVersionId: 'free-v1',
|
|
748
|
+
entitlementState: 'active',
|
|
749
|
+
actorUserId: 'system-admin',
|
|
750
|
+
actorRole: 'system_admin'
|
|
751
|
+
});
|
|
752
|
+
core.resolveAiProfile({
|
|
753
|
+
workspaceId: 'workspace-1',
|
|
754
|
+
name: 'Cloud Codex',
|
|
755
|
+
}, { trustedSeatScope: 'cloud_metered' });
|
|
756
|
+
expect(core.countAiProfileUsage('workspace-1')).toEqual({
|
|
757
|
+
used: 1,
|
|
758
|
+
limit: 20,
|
|
759
|
+
remaining: 19
|
|
760
|
+
});
|
|
761
|
+
});
|
|
762
|
+
it('blocks cloud promotion when a local ai profile would exceed the seat limit', () => {
|
|
763
|
+
const projectRoot = createProjectRoot();
|
|
764
|
+
createdRoots.push(projectRoot);
|
|
765
|
+
const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
766
|
+
core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
|
|
767
|
+
enableAiProfileSeatLimit(core, 'workspace-1', 'owner-u1', 1);
|
|
768
|
+
core.resolveAiProfile({
|
|
769
|
+
workspaceId: 'workspace-1',
|
|
770
|
+
name: 'Cloud Bot',
|
|
771
|
+
}, { trustedSeatScope: 'cloud_metered' });
|
|
772
|
+
const local = core.resolveAiProfile({
|
|
773
|
+
workspaceId: 'workspace-1',
|
|
774
|
+
name: 'Hybrid Bot',
|
|
775
|
+
}, { trustedSeatScope: 'local_unmetered' });
|
|
776
|
+
try {
|
|
777
|
+
core.resolveAiProfile({
|
|
778
|
+
workspaceId: 'workspace-1',
|
|
779
|
+
name: 'Hybrid Bot',
|
|
780
|
+
}, { trustedSeatScope: 'cloud_metered' });
|
|
781
|
+
throw new Error('Expected cloud promotion to be blocked');
|
|
782
|
+
}
|
|
783
|
+
catch (error) {
|
|
784
|
+
expect(error.message).toContain('No AI seats are available for this workspace');
|
|
785
|
+
expect(error.code).toBe('AI_PROFILE_LIMIT_REACHED');
|
|
786
|
+
}
|
|
787
|
+
const refreshed = core.getAiProfileByToken({
|
|
788
|
+
workspaceId: 'workspace-1',
|
|
789
|
+
profileToken: local.profile.profileToken
|
|
790
|
+
});
|
|
791
|
+
expect(refreshed?.seatScope).toBe('local_unmetered');
|
|
792
|
+
});
|
|
793
|
+
it('keeps local and cloud MCP profiles separate when they share the same name', () => {
|
|
794
|
+
const projectRoot = createProjectRoot();
|
|
795
|
+
createdRoots.push(projectRoot);
|
|
796
|
+
const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
797
|
+
core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
|
|
798
|
+
const cloud = core.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Codex', surfaceType: 'coding_tool' }, { trustedSeatScope: 'cloud_metered' });
|
|
799
|
+
const local = core.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Codex', surfaceType: 'coding_tool' }, { trustedSeatScope: 'local_unmetered' });
|
|
800
|
+
expect(local.created).toBe(true);
|
|
801
|
+
expect(local.profile.id).not.toBe(cloud.profile.id);
|
|
802
|
+
expect(local.profile.seatScope).toBe('local_unmetered');
|
|
803
|
+
expect(cloud.profile.seatScope).toBe('cloud_metered');
|
|
804
|
+
expect(core.countAiProfileUsage('workspace-1')).toEqual({
|
|
805
|
+
used: 1,
|
|
806
|
+
limit: null,
|
|
807
|
+
remaining: null
|
|
808
|
+
});
|
|
809
|
+
expect(core.listAiProfiles('workspace-1')).toHaveLength(2);
|
|
810
|
+
});
|
|
811
|
+
it('claims a legacy unscoped profile for the trusted runtime instead of creating a duplicate', () => {
|
|
812
|
+
const projectRoot = createProjectRoot();
|
|
813
|
+
createdRoots.push(projectRoot);
|
|
814
|
+
const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
815
|
+
core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
|
|
816
|
+
const legacy = core.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Legacy Codex' });
|
|
817
|
+
expect(legacy.profile.seatScope ?? null).toBeNull();
|
|
818
|
+
const claimed = core.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Legacy Codex', surfaceType: 'coding_tool' }, { trustedSeatScope: 'local_unmetered' });
|
|
819
|
+
expect(claimed.created).toBe(false);
|
|
820
|
+
expect(claimed.profile.id).toBe(legacy.profile.id);
|
|
821
|
+
expect(claimed.profile.seatScope).toBe('local_unmetered');
|
|
822
|
+
expect(core.listAiProfiles('workspace-1')).toHaveLength(1);
|
|
823
|
+
});
|
|
824
|
+
it('reactivates a retired legacy unscoped profile for the trusted runtime instead of recreating it', () => {
|
|
825
|
+
const projectRoot = createProjectRoot();
|
|
826
|
+
createdRoots.push(projectRoot);
|
|
827
|
+
const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
828
|
+
core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
|
|
829
|
+
const legacy = core.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Retired Legacy Codex' });
|
|
830
|
+
expect(legacy.profile.seatScope ?? null).toBeNull();
|
|
831
|
+
core.archiveAiProfile({
|
|
832
|
+
workspaceId: 'workspace-1',
|
|
833
|
+
profileId: legacy.profile.id,
|
|
834
|
+
archivedBy: 'owner-1',
|
|
835
|
+
reason: 'manual_archive'
|
|
836
|
+
});
|
|
837
|
+
const reactivated = core.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Retired Legacy Codex', surfaceType: 'coding_tool' }, { trustedSeatScope: 'cloud_metered' });
|
|
838
|
+
expect(reactivated.created).toBe(false);
|
|
839
|
+
expect(reactivated.profile.id).toBe(legacy.profile.id);
|
|
840
|
+
expect(reactivated.profile.archivedAt).toBeNull();
|
|
841
|
+
expect(reactivated.profile.seatScope).toBe('cloud_metered');
|
|
842
|
+
expect(core.listAiProfiles('workspace-1')).toHaveLength(1);
|
|
843
|
+
});
|
|
371
844
|
it('merges two duplicate profiles, reassigning task assignees and comment authors', () => {
|
|
372
845
|
const projectRoot = createProjectRoot();
|
|
373
846
|
createdRoots.push(projectRoot);
|
|
@@ -393,16 +866,302 @@ describe('TaskforceCore ai profiles', () => {
|
|
|
393
866
|
mergeId: second.profile.id
|
|
394
867
|
});
|
|
395
868
|
expect(merged.id).toBe(first.profile.id);
|
|
396
|
-
// Duplicate profile should
|
|
869
|
+
// Duplicate profile should leave the active roster but remain archived for history and sync.
|
|
397
870
|
const profiles = core.listAiProfiles('workspace-1');
|
|
398
871
|
expect(profiles.some((p) => p.id === second.profile.id)).toBe(false);
|
|
399
872
|
expect(profiles.some((p) => p.id === first.profile.id)).toBe(true);
|
|
873
|
+
const archivedProfiles = core.listAiProfiles('workspace-1', { includeArchived: true });
|
|
874
|
+
const archivedDuplicate = archivedProfiles.find((p) => p.id === second.profile.id);
|
|
875
|
+
expect(archivedDuplicate).toMatchObject({
|
|
876
|
+
id: second.profile.id,
|
|
877
|
+
archivedReason: 'merged_duplicate',
|
|
878
|
+
mergedIntoProfileId: first.profile.id,
|
|
879
|
+
archivedBy: first.profile.id
|
|
880
|
+
});
|
|
881
|
+
expect(archivedDuplicate?.archivedAt).toBeTruthy();
|
|
400
882
|
// Task assignee should point to the kept profile
|
|
401
883
|
const updatedTask = core.getTask(task.id, 'workspace-1');
|
|
402
884
|
expect(updatedTask?.assignee).toBe(first.profile.id);
|
|
403
885
|
// Comment author should point to the kept profile
|
|
404
886
|
expect(updatedTask?.comments?.[0]?.author).toBe(first.profile.id);
|
|
405
887
|
});
|
|
888
|
+
it('archives AI profiles out of the active roster while preserving history and freeing seats', () => {
|
|
889
|
+
const projectRoot = createProjectRoot();
|
|
890
|
+
createdRoots.push(projectRoot);
|
|
891
|
+
const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
892
|
+
core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
|
|
893
|
+
enableAiProfileSeatLimit(core, 'workspace-1', 'owner-1', 1);
|
|
894
|
+
const first = core.resolveAiProfile({
|
|
895
|
+
workspaceId: 'workspace-1',
|
|
896
|
+
name: 'Codex',
|
|
897
|
+
surfaceType: 'coding_tool'
|
|
898
|
+
});
|
|
899
|
+
const archived = core.archiveAiProfile({
|
|
900
|
+
workspaceId: 'workspace-1',
|
|
901
|
+
profileId: first.profile.id,
|
|
902
|
+
archivedBy: 'owner-1',
|
|
903
|
+
reason: 'manual_archive'
|
|
904
|
+
});
|
|
905
|
+
expect(archived.id).toBe(first.profile.id);
|
|
906
|
+
expect(archived.archivedReason).toBe('manual_archive');
|
|
907
|
+
expect(archived.archivedBy).toBe('owner-1');
|
|
908
|
+
expect(archived.archivedAt).toBeTruthy();
|
|
909
|
+
expect(core.listAiProfiles('workspace-1')).toEqual([]);
|
|
910
|
+
expect(core.listAssignableActors('workspace-1').some((actor) => actor.id === first.profile.id)).toBe(false);
|
|
911
|
+
const historicalActor = core.resolveWorkspaceActor('workspace-1', first.profile.id);
|
|
912
|
+
expect(historicalActor?.id).toBe(first.profile.id);
|
|
913
|
+
expect(historicalActor?.label).toBe('Codex');
|
|
914
|
+
expect(historicalActor?.meta).toEqual(expect.objectContaining({
|
|
915
|
+
archivedReason: 'manual_archive'
|
|
916
|
+
}));
|
|
917
|
+
const second = core.resolveAiProfile({
|
|
918
|
+
workspaceId: 'workspace-1',
|
|
919
|
+
name: 'Claude',
|
|
920
|
+
surfaceType: 'coding_tool'
|
|
921
|
+
});
|
|
922
|
+
expect(second.created).toBe(true);
|
|
923
|
+
const archivedProfiles = core.listAiProfiles('workspace-1', { includeArchived: true });
|
|
924
|
+
expect(archivedProfiles.map((profile) => profile.name)).toEqual(['Claude', 'Codex']);
|
|
925
|
+
const archivedEntry = archivedProfiles.find((profile) => profile.id === first.profile.id);
|
|
926
|
+
expect(archivedEntry?.archivedReason).toBe('manual_archive');
|
|
927
|
+
expect(archivedEntry?.rosterStateUpdatedAt).toBe(archivedEntry?.archivedAt);
|
|
928
|
+
});
|
|
929
|
+
it('keeps an archived AI profile archived when sync replays an equal-watermark active copy', () => {
|
|
930
|
+
const projectRoot = createProjectRoot();
|
|
931
|
+
createdRoots.push(projectRoot);
|
|
932
|
+
const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
933
|
+
core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
|
|
934
|
+
const resolved = core.resolveAiProfile({
|
|
935
|
+
workspaceId: 'workspace-1',
|
|
936
|
+
name: 'Codex T-714 Verifier',
|
|
937
|
+
surfaceType: 'coding_tool'
|
|
938
|
+
});
|
|
939
|
+
const archived = core.archiveAiProfile({
|
|
940
|
+
workspaceId: 'workspace-1',
|
|
941
|
+
profileId: resolved.profile.id,
|
|
942
|
+
archivedBy: 'owner-1',
|
|
943
|
+
reason: 'manual_archive'
|
|
944
|
+
});
|
|
945
|
+
const replayedActive = core.upsertAiProfileForSync({
|
|
946
|
+
...archived,
|
|
947
|
+
archivedAt: null,
|
|
948
|
+
archivedBy: null,
|
|
949
|
+
archivedReason: null,
|
|
950
|
+
mergedIntoProfileId: null,
|
|
951
|
+
updatedAt: archived.updatedAt
|
|
952
|
+
});
|
|
953
|
+
expect(replayedActive.archivedAt).toBeTruthy();
|
|
954
|
+
expect(core.listAiProfiles('workspace-1')).toEqual([]);
|
|
955
|
+
const stored = core.listAiProfiles('workspace-1', { includeArchived: true })
|
|
956
|
+
.find((profile) => profile.id === archived.id);
|
|
957
|
+
expect(stored?.archivedAt).toBe(archived.archivedAt);
|
|
958
|
+
expect(stored?.archivedReason).toBe('manual_archive');
|
|
959
|
+
});
|
|
960
|
+
it('accepts newer synced seat scope for the same AI profile id', () => {
|
|
961
|
+
const projectRoot = createProjectRoot();
|
|
962
|
+
createdRoots.push(projectRoot);
|
|
963
|
+
const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
964
|
+
core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
|
|
965
|
+
const contaminated = core.resolveAiProfile({
|
|
966
|
+
workspaceId: 'workspace-1',
|
|
967
|
+
name: 'Codex',
|
|
968
|
+
surfaceType: 'coding_tool'
|
|
969
|
+
}, { trustedSeatScope: 'cloud_metered' });
|
|
970
|
+
const repairedAt = new Date(Date.parse(contaminated.profile.updatedAt) + 1000).toISOString();
|
|
971
|
+
const repaired = core.upsertAiProfileForSync({
|
|
972
|
+
...contaminated.profile,
|
|
973
|
+
seatScope: 'local_unmetered',
|
|
974
|
+
updatedAt: repairedAt,
|
|
975
|
+
lastActiveAt: repairedAt
|
|
976
|
+
});
|
|
977
|
+
expect(repaired.id).toBe(contaminated.profile.id);
|
|
978
|
+
expect(repaired.seatScope).toBe('local_unmetered');
|
|
979
|
+
const staleReplay = core.upsertAiProfileForSync({
|
|
980
|
+
...contaminated.profile,
|
|
981
|
+
seatScope: 'cloud_metered',
|
|
982
|
+
updatedAt: contaminated.profile.updatedAt
|
|
983
|
+
});
|
|
984
|
+
expect(staleReplay.seatScope).toBe('local_unmetered');
|
|
985
|
+
});
|
|
986
|
+
it('keeps an archived AI profile archived when sync replays a newer metadata update with an older lifecycle timestamp', () => {
|
|
987
|
+
const projectRoot = createProjectRoot();
|
|
988
|
+
createdRoots.push(projectRoot);
|
|
989
|
+
const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
990
|
+
core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
|
|
991
|
+
const resolved = core.resolveAiProfile({
|
|
992
|
+
workspaceId: 'workspace-1',
|
|
993
|
+
name: 'Antigravity/jCodeMunch',
|
|
994
|
+
surfaceType: 'coding_tool'
|
|
995
|
+
});
|
|
996
|
+
const archived = core.archiveAiProfile({
|
|
997
|
+
workspaceId: 'workspace-1',
|
|
998
|
+
profileId: resolved.profile.id,
|
|
999
|
+
archivedBy: 'owner-1',
|
|
1000
|
+
reason: 'manual_archive'
|
|
1001
|
+
});
|
|
1002
|
+
const replayedActive = core.upsertAiProfileForSync({
|
|
1003
|
+
...archived,
|
|
1004
|
+
description: 'Cloud-side metadata changed after the archive write was missed.',
|
|
1005
|
+
archivedAt: null,
|
|
1006
|
+
archivedBy: null,
|
|
1007
|
+
archivedReason: null,
|
|
1008
|
+
mergedIntoProfileId: null,
|
|
1009
|
+
rosterStateUpdatedAt: resolved.profile.rosterStateUpdatedAt,
|
|
1010
|
+
updatedAt: '2099-03-22T10:30:00.000Z'
|
|
1011
|
+
});
|
|
1012
|
+
expect(replayedActive.archivedAt).toBeTruthy();
|
|
1013
|
+
const stored = core.listAiProfiles('workspace-1', { includeArchived: true })
|
|
1014
|
+
.find((profile) => profile.id === archived.id);
|
|
1015
|
+
expect(stored?.archivedAt).toBe(archived.archivedAt);
|
|
1016
|
+
expect(stored?.description).toBe('Cloud-side metadata changed after the archive write was missed.');
|
|
1017
|
+
});
|
|
1018
|
+
it('repairs stale archived roster lifecycle timestamps before applying active sync snapshots', () => {
|
|
1019
|
+
const projectRoot = createProjectRoot();
|
|
1020
|
+
createdRoots.push(projectRoot);
|
|
1021
|
+
const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
1022
|
+
core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
|
|
1023
|
+
const resolved = core.resolveAiProfile({
|
|
1024
|
+
workspaceId: 'workspace-1',
|
|
1025
|
+
name: 'Codex',
|
|
1026
|
+
surfaceType: 'coding_tool'
|
|
1027
|
+
});
|
|
1028
|
+
const archived = core.archiveAiProfile({
|
|
1029
|
+
workspaceId: 'workspace-1',
|
|
1030
|
+
profileId: resolved.profile.id,
|
|
1031
|
+
archivedBy: 'owner-1',
|
|
1032
|
+
reason: 'manual_archive'
|
|
1033
|
+
});
|
|
1034
|
+
core.db.prepare(`
|
|
1035
|
+
UPDATE ai_profiles
|
|
1036
|
+
SET roster_state_updated_at = ?
|
|
1037
|
+
WHERE tenant_id = ? AND id = ?
|
|
1038
|
+
`).run(resolved.profile.createdAt, 'default', archived.id);
|
|
1039
|
+
const replayedActive = core.upsertAiProfileForSync({
|
|
1040
|
+
...archived,
|
|
1041
|
+
description: 'Cloud active snapshot with newer metadata but no lifecycle marker.',
|
|
1042
|
+
archivedAt: null,
|
|
1043
|
+
archivedBy: null,
|
|
1044
|
+
archivedReason: null,
|
|
1045
|
+
mergedIntoProfileId: null,
|
|
1046
|
+
rosterStateUpdatedAt: null,
|
|
1047
|
+
updatedAt: '2099-03-22T10:30:00.000Z'
|
|
1048
|
+
});
|
|
1049
|
+
expect(replayedActive.archivedAt).toBe(archived.archivedAt);
|
|
1050
|
+
expect(replayedActive.rosterStateUpdatedAt).toBe(archived.archivedAt);
|
|
1051
|
+
const stored = core.listAiProfiles('workspace-1', { includeArchived: true })
|
|
1052
|
+
.find((profile) => profile.id === archived.id);
|
|
1053
|
+
expect(stored?.archivedAt).toBe(archived.archivedAt);
|
|
1054
|
+
expect(stored?.description).toBe('Cloud active snapshot with newer metadata but no lifecycle marker.');
|
|
1055
|
+
expect(core.listAiProfiles('workspace-1')).toEqual([]);
|
|
1056
|
+
});
|
|
1057
|
+
it('allows sync to reactivate an archived AI profile when lifecycle timestamp is newer', () => {
|
|
1058
|
+
const projectRoot = createProjectRoot();
|
|
1059
|
+
createdRoots.push(projectRoot);
|
|
1060
|
+
const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
1061
|
+
core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
|
|
1062
|
+
const resolved = core.resolveAiProfile({
|
|
1063
|
+
workspaceId: 'workspace-1',
|
|
1064
|
+
name: 'Claude Code',
|
|
1065
|
+
surfaceType: 'coding_tool'
|
|
1066
|
+
});
|
|
1067
|
+
const archived = core.archiveAiProfile({
|
|
1068
|
+
workspaceId: 'workspace-1',
|
|
1069
|
+
profileId: resolved.profile.id,
|
|
1070
|
+
archivedBy: 'owner-1',
|
|
1071
|
+
reason: 'manual_archive'
|
|
1072
|
+
});
|
|
1073
|
+
const reactivated = core.upsertAiProfileForSync({
|
|
1074
|
+
...archived,
|
|
1075
|
+
archivedAt: null,
|
|
1076
|
+
archivedBy: null,
|
|
1077
|
+
archivedReason: null,
|
|
1078
|
+
mergedIntoProfileId: null,
|
|
1079
|
+
rosterStateUpdatedAt: '2099-03-22T10:45:00.000Z',
|
|
1080
|
+
updatedAt: '2099-03-22T10:45:00.000Z'
|
|
1081
|
+
});
|
|
1082
|
+
expect(reactivated.archivedAt).toBeNull();
|
|
1083
|
+
expect(core.listAiProfiles('workspace-1').map((profile) => profile.id)).toEqual([resolved.profile.id]);
|
|
1084
|
+
});
|
|
1085
|
+
it('reactivates an archived AI profile when resolveAiProfile is called explicitly', () => {
|
|
1086
|
+
const projectRoot = createProjectRoot();
|
|
1087
|
+
createdRoots.push(projectRoot);
|
|
1088
|
+
const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
1089
|
+
core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
|
|
1090
|
+
const initial = core.resolveAiProfile({
|
|
1091
|
+
workspaceId: 'workspace-1',
|
|
1092
|
+
name: 'Codex',
|
|
1093
|
+
surfaceType: 'coding_tool',
|
|
1094
|
+
}, { trustedSeatScope: 'cloud_metered' });
|
|
1095
|
+
core.archiveAiProfile({
|
|
1096
|
+
workspaceId: 'workspace-1',
|
|
1097
|
+
profileId: initial.profile.id,
|
|
1098
|
+
archivedBy: 'owner-1',
|
|
1099
|
+
reason: 'manual_archive'
|
|
1100
|
+
});
|
|
1101
|
+
const reactivatedByName = core.resolveAiProfile({
|
|
1102
|
+
workspaceId: 'workspace-1',
|
|
1103
|
+
name: 'Codex',
|
|
1104
|
+
surfaceType: 'coding_tool'
|
|
1105
|
+
}, { trustedSeatScope: 'cloud_metered' });
|
|
1106
|
+
expect(reactivatedByName.created).toBe(false);
|
|
1107
|
+
expect(reactivatedByName.profile.id).toBe(initial.profile.id);
|
|
1108
|
+
expect(reactivatedByName.profile.archivedAt).toBeNull();
|
|
1109
|
+
expect(reactivatedByName.profile.archivedReason).toBeNull();
|
|
1110
|
+
expect(core.listAiProfiles('workspace-1')).toHaveLength(1);
|
|
1111
|
+
core.archiveAiProfile({
|
|
1112
|
+
workspaceId: 'workspace-1',
|
|
1113
|
+
profileId: initial.profile.id,
|
|
1114
|
+
archivedBy: 'owner-1',
|
|
1115
|
+
reason: 'manual_archive'
|
|
1116
|
+
});
|
|
1117
|
+
const reactivatedByToken = core.resolveAiProfile({
|
|
1118
|
+
workspaceId: 'workspace-1',
|
|
1119
|
+
name: 'Codex',
|
|
1120
|
+
profileToken: initial.profile.profileToken,
|
|
1121
|
+
surfaceType: 'coding_tool'
|
|
1122
|
+
}, { trustedSeatScope: 'cloud_metered' });
|
|
1123
|
+
expect(reactivatedByToken.created).toBe(false);
|
|
1124
|
+
expect(reactivatedByToken.profile.id).toBe(initial.profile.id);
|
|
1125
|
+
expect(reactivatedByToken.profile.profileToken).toBe(initial.profile.profileToken);
|
|
1126
|
+
expect(reactivatedByToken.profile.archivedAt).toBeNull();
|
|
1127
|
+
expect(core.listAiProfiles('workspace-1')).toHaveLength(1);
|
|
1128
|
+
});
|
|
1129
|
+
it('rejects archiving an AI profile that still owns open tasks', () => {
|
|
1130
|
+
const projectRoot = createProjectRoot();
|
|
1131
|
+
createdRoots.push(projectRoot);
|
|
1132
|
+
const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
1133
|
+
core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
|
|
1134
|
+
const profile = core.resolveAiProfile({
|
|
1135
|
+
workspaceId: 'workspace-1',
|
|
1136
|
+
name: 'Antigravity',
|
|
1137
|
+
surfaceType: 'agent'
|
|
1138
|
+
});
|
|
1139
|
+
core.createTask({
|
|
1140
|
+
title: 'Still assigned',
|
|
1141
|
+
status: 'in-progress',
|
|
1142
|
+
assignee: profile.profile.id
|
|
1143
|
+
}, { workspaceId: 'workspace-1' });
|
|
1144
|
+
try {
|
|
1145
|
+
core.archiveAiProfile({
|
|
1146
|
+
workspaceId: 'workspace-1',
|
|
1147
|
+
profileId: profile.profile.id,
|
|
1148
|
+
archivedBy: 'owner-1',
|
|
1149
|
+
reason: 'manual_archive'
|
|
1150
|
+
});
|
|
1151
|
+
expect.unreachable('Expected archiveAiProfile to reject open-task ownership');
|
|
1152
|
+
}
|
|
1153
|
+
catch (error) {
|
|
1154
|
+
expect(error.code).toBe('AI_PROFILE_HAS_OPEN_TASKS');
|
|
1155
|
+
expect(error.statusCode).toBe(409);
|
|
1156
|
+
expect(error.details).toEqual({
|
|
1157
|
+
workspaceId: 'workspace-1',
|
|
1158
|
+
profileId: profile.profile.id,
|
|
1159
|
+
openTaskCount: 1
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
expect(core.listAiProfiles('workspace-1')).toHaveLength(1);
|
|
1163
|
+
expect(core.listAiProfiles('workspace-1', { includeArchived: true })[0]?.archivedAt).toBeNull();
|
|
1164
|
+
});
|
|
406
1165
|
it('canonicalizes AI names to profile ids when creating tasks and comments', () => {
|
|
407
1166
|
const projectRoot = createProjectRoot();
|
|
408
1167
|
createdRoots.push(projectRoot);
|
|
@@ -458,7 +1217,7 @@ describe('TaskforceCore ai profiles', () => {
|
|
|
458
1217
|
const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
|
|
459
1218
|
const reloaded = reloadedCore.getTask(task.id, 'workspace-1');
|
|
460
1219
|
const profiles = reloadedCore.listAiProfiles('workspace-1');
|
|
461
|
-
expect(profiles.map((profile) => profile.name)).toEqual(['
|
|
1220
|
+
expect(profiles.map((profile) => profile.name)).toEqual(['Antigravity', 'Claude']);
|
|
462
1221
|
const survivingAntigravityId = profiles.find((profile) => profile.name === 'Antigravity')?.id;
|
|
463
1222
|
expect(reloaded?.createdBy).toBe(claude.profile.id);
|
|
464
1223
|
expect(reloaded?.assignee).toBe(survivingAntigravityId);
|