@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,84 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { TaskforceCore } from './Taskforce';
|
|
6
|
+
function makeProjectRoot() {
|
|
7
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'taskforce-plan-feature-catalog-'));
|
|
8
|
+
}
|
|
9
|
+
describe('TaskforceCore plan feature catalog marketing copy', () => {
|
|
10
|
+
it('persists public marketing copy overrides and falls back after clearing them', () => {
|
|
11
|
+
const projectRoot = makeProjectRoot();
|
|
12
|
+
const core = new TaskforceCore({
|
|
13
|
+
projectRoot,
|
|
14
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
15
|
+
});
|
|
16
|
+
const initialFeature = core.listPlanFeatureCatalog().find((feature) => feature.featureKey === 'collaboration.ai_profiles');
|
|
17
|
+
expect(initialFeature?.label).toBe('AI Profiles');
|
|
18
|
+
expect(initialFeature?.publicLabel ?? null).toBeNull();
|
|
19
|
+
expect(initialFeature?.publicDescription ?? null).toBeNull();
|
|
20
|
+
expect(initialFeature?.publicDisplayOrder).toBe(20);
|
|
21
|
+
const updated = core.updatePlanFeatureCatalogMarketingCopy({
|
|
22
|
+
featureKey: 'collaboration.ai_profiles',
|
|
23
|
+
publicLabel: 'AI teammates',
|
|
24
|
+
publicDescription: 'Add reusable AI teammates to your workspace.',
|
|
25
|
+
publicDisplayOrder: 24
|
|
26
|
+
});
|
|
27
|
+
expect(updated.publicLabel).toBe('AI teammates');
|
|
28
|
+
expect(updated.publicDescription).toBe('Add reusable AI teammates to your workspace.');
|
|
29
|
+
expect(updated.publicDisplayOrder).toBe(24);
|
|
30
|
+
const persisted = core.listPlanFeatureCatalog().find((feature) => feature.featureKey === 'collaboration.ai_profiles');
|
|
31
|
+
expect(persisted?.publicLabel).toBe('AI teammates');
|
|
32
|
+
expect(persisted?.publicDescription).toBe('Add reusable AI teammates to your workspace.');
|
|
33
|
+
expect(persisted?.publicDisplayOrder).toBe(24);
|
|
34
|
+
const cleared = core.updatePlanFeatureCatalogMarketingCopy({
|
|
35
|
+
featureKey: 'collaboration.ai_profiles',
|
|
36
|
+
publicLabel: ' ',
|
|
37
|
+
publicDescription: '',
|
|
38
|
+
publicDisplayOrder: null
|
|
39
|
+
});
|
|
40
|
+
expect(cleared.publicLabel ?? null).toBeNull();
|
|
41
|
+
expect(cleared.publicDescription ?? null).toBeNull();
|
|
42
|
+
expect(cleared.publicDisplayOrder).toBe(20);
|
|
43
|
+
const fallback = core.listPlanFeatureCatalog().find((feature) => feature.featureKey === 'collaboration.ai_profiles');
|
|
44
|
+
expect(fallback?.label).toBe('AI Profiles');
|
|
45
|
+
expect(fallback?.publicLabel ?? null).toBeNull();
|
|
46
|
+
expect(fallback?.publicDescription ?? null).toBeNull();
|
|
47
|
+
expect(fallback?.publicDisplayOrder).toBe(20);
|
|
48
|
+
core.close();
|
|
49
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
50
|
+
});
|
|
51
|
+
it('migrates legacy workspace feature catalog overrides to the canonical key', () => {
|
|
52
|
+
const projectRoot = makeProjectRoot();
|
|
53
|
+
const core = new TaskforceCore({
|
|
54
|
+
projectRoot,
|
|
55
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
56
|
+
});
|
|
57
|
+
const db = core.db;
|
|
58
|
+
db.prepare(`
|
|
59
|
+
INSERT INTO plan_feature_catalog_overrides (
|
|
60
|
+
tenant_id,
|
|
61
|
+
feature_key,
|
|
62
|
+
public_label,
|
|
63
|
+
public_description,
|
|
64
|
+
public_display_order,
|
|
65
|
+
created_at,
|
|
66
|
+
updated_at
|
|
67
|
+
) VALUES (?, 'workspace.switching', ?, ?, ?, ?, ?)
|
|
68
|
+
`).run('default', 'Workspace access', 'Let a user create and switch between multiple workspaces.', 14, '2026-01-01T00:00:00.000Z', '2026-01-01T00:00:00.000Z');
|
|
69
|
+
core.planEntitlementService.normalizeLegacyPlanDataIfNeeded();
|
|
70
|
+
const feature = core.listPlanFeatureCatalog().find((item) => item.featureKey === 'workspace.workspaces');
|
|
71
|
+
expect(feature?.publicLabel).toBe('Workspace access');
|
|
72
|
+
expect(feature?.publicDescription).toBe('Let a user create and switch between multiple workspaces.');
|
|
73
|
+
expect(feature?.publicDisplayOrder).toBe(14);
|
|
74
|
+
const rows = db.prepare(`
|
|
75
|
+
SELECT feature_key
|
|
76
|
+
FROM plan_feature_catalog_overrides
|
|
77
|
+
WHERE tenant_id = ?
|
|
78
|
+
`).all('default');
|
|
79
|
+
expect(rows.some((row) => row.feature_key === 'workspace.switching')).toBe(false);
|
|
80
|
+
expect(rows.some((row) => row.feature_key === 'workspace.workspaces')).toBe(true);
|
|
81
|
+
core.close();
|
|
82
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -141,11 +141,11 @@ describe('TaskforceCore plan version policy', () => {
|
|
|
141
141
|
const version = core.updatePlanVersionFeatures({
|
|
142
142
|
planVersionId: 'free-v1',
|
|
143
143
|
features: [
|
|
144
|
-
{ featureKey: 'workspace.
|
|
144
|
+
{ featureKey: 'workspace.workspaces', access: 'limited', config: { maxWorkspaces: 0 } },
|
|
145
145
|
{ featureKey: 'context.uploads', access: 'limited', config: { storageLimitMb: -50 } }
|
|
146
146
|
]
|
|
147
147
|
});
|
|
148
|
-
expect(version.features['workspace.
|
|
148
|
+
expect(version.features['workspace.workspaces']?.config).toEqual({ maxWorkspaces: 1 });
|
|
149
149
|
expect(version.features['context.uploads']?.config).toEqual({ storageLimitMb: 1 });
|
|
150
150
|
}
|
|
151
151
|
finally {
|
|
@@ -153,6 +153,39 @@ describe('TaskforceCore plan version policy', () => {
|
|
|
153
153
|
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
154
154
|
}
|
|
155
155
|
});
|
|
156
|
+
it('migrates legacy workspace feature keys to the canonical key', () => {
|
|
157
|
+
const projectRoot = makeProjectRoot();
|
|
158
|
+
const core = openCore(projectRoot);
|
|
159
|
+
try {
|
|
160
|
+
core.updatePlanVersionFeatures({
|
|
161
|
+
planVersionId: 'free-v1',
|
|
162
|
+
features: [
|
|
163
|
+
{ featureKey: 'workspace.workspaces', access: 'limited', config: { maxWorkspaces: 1 } }
|
|
164
|
+
]
|
|
165
|
+
});
|
|
166
|
+
const db = core.db;
|
|
167
|
+
db.prepare(`
|
|
168
|
+
UPDATE plan_feature_matrix
|
|
169
|
+
SET feature_key = 'workspace.switching'
|
|
170
|
+
WHERE tenant_id = ? AND plan_version_id = ? AND feature_key = 'workspace.workspaces'
|
|
171
|
+
`).run('default', 'free-v1');
|
|
172
|
+
core.planEntitlementService.normalizeLegacyPlanDataIfNeeded();
|
|
173
|
+
const version = core.getPlanVersionFeatures('free', 'free-v1');
|
|
174
|
+
expect(version.features['workspace.workspaces']).toBeTruthy();
|
|
175
|
+
expect(version.features['workspace.switching']).toBeUndefined();
|
|
176
|
+
const rows = db.prepare(`
|
|
177
|
+
SELECT feature_key
|
|
178
|
+
FROM plan_feature_matrix
|
|
179
|
+
WHERE tenant_id = ? AND plan_version_id = ?
|
|
180
|
+
`).all('default', 'free-v1');
|
|
181
|
+
expect(rows.some((row) => row.feature_key === 'workspace.switching')).toBe(false);
|
|
182
|
+
expect(rows.some((row) => row.feature_key === 'workspace.workspaces')).toBe(true);
|
|
183
|
+
}
|
|
184
|
+
finally {
|
|
185
|
+
core.close();
|
|
186
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
187
|
+
}
|
|
188
|
+
});
|
|
156
189
|
it('starts new plans disabled before pricing is configured', () => {
|
|
157
190
|
const projectRoot = makeProjectRoot();
|
|
158
191
|
const core = openCore(projectRoot);
|
|
@@ -92,4 +92,56 @@ describe('TaskforceCore planning entity persistence', () => {
|
|
|
92
92
|
expect(clearedTask?.workstreamId).toBeNull();
|
|
93
93
|
core.close();
|
|
94
94
|
});
|
|
95
|
+
it('builds lightweight planning bootstrap summaries without hydrating full task details', () => {
|
|
96
|
+
const core = createCore();
|
|
97
|
+
const initiative = core.createInitiative({ title: 'Launch initiative' });
|
|
98
|
+
const deliveryWorkstream = core.createWorkstream({
|
|
99
|
+
title: 'Delivery',
|
|
100
|
+
initiativeId: initiative.id,
|
|
101
|
+
});
|
|
102
|
+
const billingWorkstream = core.createWorkstream({
|
|
103
|
+
title: 'Billing',
|
|
104
|
+
initiativeId: initiative.id,
|
|
105
|
+
});
|
|
106
|
+
core.createTask({
|
|
107
|
+
title: 'Ship onboarding copy',
|
|
108
|
+
workstreamId: deliveryWorkstream.id,
|
|
109
|
+
status: 'task',
|
|
110
|
+
});
|
|
111
|
+
core.createTask({
|
|
112
|
+
title: 'Review launch checklist',
|
|
113
|
+
workstreamId: deliveryWorkstream.id,
|
|
114
|
+
status: 'done',
|
|
115
|
+
});
|
|
116
|
+
core.createTask({
|
|
117
|
+
title: 'Update billing note',
|
|
118
|
+
workstreamId: billingWorkstream.id,
|
|
119
|
+
status: 'cancelled',
|
|
120
|
+
});
|
|
121
|
+
core.createTask({
|
|
122
|
+
title: 'Unlinked task',
|
|
123
|
+
status: 'task',
|
|
124
|
+
});
|
|
125
|
+
const bootstrap = core.listPlanningBootstrap();
|
|
126
|
+
const deliverySummary = bootstrap.workstreamTaskSummaries.find((entry) => entry.workstreamId === deliveryWorkstream.id);
|
|
127
|
+
const billingSummary = bootstrap.workstreamTaskSummaries.find((entry) => entry.workstreamId === billingWorkstream.id);
|
|
128
|
+
expect(bootstrap.initiatives.map((entry) => entry.id)).toEqual([initiative.id]);
|
|
129
|
+
expect(bootstrap.workstreams.map((entry) => entry.id)).toEqual([deliveryWorkstream.id, billingWorkstream.id]);
|
|
130
|
+
expect(deliverySummary).toMatchObject({
|
|
131
|
+
workstreamId: deliveryWorkstream.id,
|
|
132
|
+
taskCount: 2,
|
|
133
|
+
completedTaskCount: 1,
|
|
134
|
+
});
|
|
135
|
+
expect(deliverySummary?.tasks).toEqual(expect.arrayContaining([
|
|
136
|
+
expect.objectContaining({ title: 'Ship onboarding copy', status: 'task' }),
|
|
137
|
+
expect.objectContaining({ title: 'Review launch checklist', status: 'done' }),
|
|
138
|
+
]));
|
|
139
|
+
expect(billingSummary).toMatchObject({
|
|
140
|
+
workstreamId: billingWorkstream.id,
|
|
141
|
+
taskCount: 1,
|
|
142
|
+
completedTaskCount: 1,
|
|
143
|
+
});
|
|
144
|
+
expect(bootstrap.workstreamTaskSummaries.some((entry) => entry.tasks.some((task) => task.title === 'Unlinked task'))).toBe(false);
|
|
145
|
+
core.close();
|
|
146
|
+
});
|
|
95
147
|
});
|
|
@@ -31,7 +31,7 @@ function openCore(projectRoot) {
|
|
|
31
31
|
core.updatePlanVersionFeatures({
|
|
32
32
|
planVersionId: 'free-v1',
|
|
33
33
|
features: [
|
|
34
|
-
{ featureKey: 'workspace.
|
|
34
|
+
{ featureKey: 'workspace.workspaces', access: 'enabled' }
|
|
35
35
|
]
|
|
36
36
|
});
|
|
37
37
|
insertFreePriceMapping(core, 'free-v1');
|
|
@@ -44,7 +44,7 @@ describe('TaskforceCore onboarding policy', () => {
|
|
|
44
44
|
try {
|
|
45
45
|
expect(core.getOnboardingPolicy()).toEqual({
|
|
46
46
|
mode: 'require_plan_selection',
|
|
47
|
-
|
|
47
|
+
planId: null
|
|
48
48
|
});
|
|
49
49
|
}
|
|
50
50
|
finally {
|
|
@@ -68,13 +68,13 @@ describe('TaskforceCore onboarding policy', () => {
|
|
|
68
68
|
core.updatePlanVersionFeatures({
|
|
69
69
|
planVersionId: 'free-v1',
|
|
70
70
|
features: [
|
|
71
|
-
{ featureKey: 'workspace.
|
|
71
|
+
{ featureKey: 'workspace.workspaces', access: 'enabled' }
|
|
72
72
|
]
|
|
73
73
|
});
|
|
74
74
|
try {
|
|
75
75
|
expect(core.getOnboardingPolicy()).toEqual({
|
|
76
76
|
mode: 'require_plan_selection',
|
|
77
|
-
|
|
77
|
+
planId: null
|
|
78
78
|
});
|
|
79
79
|
}
|
|
80
80
|
finally {
|
|
@@ -82,16 +82,16 @@ describe('TaskforceCore onboarding policy', () => {
|
|
|
82
82
|
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
83
83
|
}
|
|
84
84
|
});
|
|
85
|
-
it('fails validation for auto_grant_free without a plan
|
|
85
|
+
it('fails validation for auto_grant_free without a plan id', () => {
|
|
86
86
|
const projectRoot = makeProjectRoot();
|
|
87
87
|
const core = openCore(projectRoot);
|
|
88
88
|
try {
|
|
89
89
|
expect(core.validateOnboardingPolicy({
|
|
90
90
|
mode: 'auto_grant_free',
|
|
91
|
-
|
|
91
|
+
planId: null
|
|
92
92
|
})).toEqual(expect.objectContaining({
|
|
93
93
|
valid: false,
|
|
94
|
-
code: '
|
|
94
|
+
code: 'ONBOARDING_PLAN_REQUIRED'
|
|
95
95
|
}));
|
|
96
96
|
}
|
|
97
97
|
finally {
|
|
@@ -99,16 +99,16 @@ describe('TaskforceCore onboarding policy', () => {
|
|
|
99
99
|
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
100
100
|
}
|
|
101
101
|
});
|
|
102
|
-
it('fails validation when the configured
|
|
102
|
+
it('fails validation when the configured plan does not exist', () => {
|
|
103
103
|
const projectRoot = makeProjectRoot();
|
|
104
104
|
const core = openCore(projectRoot);
|
|
105
105
|
try {
|
|
106
106
|
expect(core.validateOnboardingPolicy({
|
|
107
107
|
mode: 'auto_grant_free',
|
|
108
|
-
|
|
108
|
+
planId: 'missing'
|
|
109
109
|
})).toEqual(expect.objectContaining({
|
|
110
110
|
valid: false,
|
|
111
|
-
code: '
|
|
111
|
+
code: 'ONBOARDING_PLAN_NOT_FOUND'
|
|
112
112
|
}));
|
|
113
113
|
}
|
|
114
114
|
finally {
|
|
@@ -116,14 +116,14 @@ describe('TaskforceCore onboarding policy', () => {
|
|
|
116
116
|
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
117
117
|
}
|
|
118
118
|
});
|
|
119
|
-
it('fails validation when the configured
|
|
119
|
+
it('fails validation when the configured plan is disabled', () => {
|
|
120
120
|
const projectRoot = makeProjectRoot();
|
|
121
121
|
const core = openCore(projectRoot);
|
|
122
122
|
try {
|
|
123
123
|
core.upsertPlanCatalog({ planId: 'free', displayName: 'Free', status: 'archived' });
|
|
124
124
|
expect(core.validateOnboardingPolicy({
|
|
125
125
|
mode: 'auto_grant_free',
|
|
126
|
-
|
|
126
|
+
planId: 'free'
|
|
127
127
|
})).toEqual(expect.objectContaining({
|
|
128
128
|
valid: false,
|
|
129
129
|
code: 'ONBOARDING_PLAN_NOT_ENABLED'
|
|
@@ -134,14 +134,14 @@ describe('TaskforceCore onboarding policy', () => {
|
|
|
134
134
|
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
135
135
|
}
|
|
136
136
|
});
|
|
137
|
-
it('fails validation when auto_grant_free points at a version
|
|
137
|
+
it('fails validation when auto_grant_free points at a plan whose default version lacks free pricing', () => {
|
|
138
138
|
const projectRoot = makeProjectRoot();
|
|
139
139
|
const core = openCore(projectRoot);
|
|
140
140
|
try {
|
|
141
141
|
core.upsertPlanCatalog({ planId: 'team-5', displayName: 'Team 5', status: 'active' });
|
|
142
142
|
expect(core.validateOnboardingPolicy({
|
|
143
143
|
mode: 'auto_grant_free',
|
|
144
|
-
|
|
144
|
+
planId: 'team-5'
|
|
145
145
|
})).toEqual(expect.objectContaining({
|
|
146
146
|
valid: false,
|
|
147
147
|
code: 'ONBOARDING_PLAN_MUST_BE_FREE'
|
|
@@ -152,13 +152,13 @@ describe('TaskforceCore onboarding policy', () => {
|
|
|
152
152
|
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
153
153
|
}
|
|
154
154
|
});
|
|
155
|
-
it('passes validation for require_plan_selection without a plan
|
|
155
|
+
it('passes validation for require_plan_selection without a plan id', () => {
|
|
156
156
|
const projectRoot = makeProjectRoot();
|
|
157
157
|
const core = openCore(projectRoot);
|
|
158
158
|
try {
|
|
159
159
|
expect(core.validateOnboardingPolicy({
|
|
160
160
|
mode: 'require_plan_selection',
|
|
161
|
-
|
|
161
|
+
planId: null
|
|
162
162
|
})).toEqual({ valid: true });
|
|
163
163
|
}
|
|
164
164
|
finally {
|
|
@@ -172,7 +172,7 @@ describe('TaskforceCore onboarding policy', () => {
|
|
|
172
172
|
try {
|
|
173
173
|
const created = core.registerPasswordUser({
|
|
174
174
|
email: 'implicit-onboarding@example.com',
|
|
175
|
-
password: '
|
|
175
|
+
password: 'ValidPassphrase123!'
|
|
176
176
|
});
|
|
177
177
|
expect(created.planSelectionRequired).toBe(true);
|
|
178
178
|
expect(created.checkoutPending).toBe(false);
|
|
@@ -201,15 +201,15 @@ describe('TaskforceCore onboarding policy', () => {
|
|
|
201
201
|
billing: {
|
|
202
202
|
onboardingPolicy: {
|
|
203
203
|
mode: 'require_plan_selection',
|
|
204
|
-
|
|
204
|
+
planId: null
|
|
205
205
|
}
|
|
206
206
|
}
|
|
207
207
|
});
|
|
208
208
|
const db = core.getDatabaseAdapter();
|
|
209
209
|
db.prepare(`
|
|
210
|
-
INSERT INTO users (id, tenant_id, email, display_name,
|
|
211
|
-
VALUES (?, ?, ?, ?,
|
|
212
|
-
`).run('pending-u1', tenantId, 'pending@example.com', 'Pending',
|
|
210
|
+
INSERT INTO users (id, tenant_id, email, display_name, is_disabled, created_at, updated_at)
|
|
211
|
+
VALUES (?, ?, ?, ?, 0, ?, ?)
|
|
212
|
+
`).run('pending-u1', tenantId, 'pending@example.com', 'Pending', now, now);
|
|
213
213
|
db.prepare(`
|
|
214
214
|
INSERT INTO user_billing (user_id, tenant_id, entitlement_state, created_at, updated_at)
|
|
215
215
|
VALUES (?, ?, ?, ?, ?)
|
|
@@ -248,9 +248,9 @@ describe('TaskforceCore onboarding policy', () => {
|
|
|
248
248
|
core.upsertPlanCatalog({ planId: 'basic', displayName: 'Basic', status: 'active' });
|
|
249
249
|
const db = core.getDatabaseAdapter();
|
|
250
250
|
db.prepare(`
|
|
251
|
-
INSERT INTO users (id, tenant_id, email, display_name,
|
|
252
|
-
VALUES (?, ?, ?, ?,
|
|
253
|
-
`).run('checkout-u1', tenantId, 'checkout@example.com', 'Checkout',
|
|
251
|
+
INSERT INTO users (id, tenant_id, email, display_name, is_disabled, created_at, updated_at)
|
|
252
|
+
VALUES (?, ?, ?, ?, 0, ?, ?)
|
|
253
|
+
`).run('checkout-u1', tenantId, 'checkout@example.com', 'Checkout', now, now);
|
|
254
254
|
core.transitionCanonicalCommercialState({
|
|
255
255
|
accountId: 'checkout-u1',
|
|
256
256
|
planVersionId: 'basic-v1',
|
|
@@ -286,9 +286,9 @@ describe('TaskforceCore onboarding policy', () => {
|
|
|
286
286
|
core.upsertPlanCatalog({ planId: 'basic', displayName: 'Basic', status: 'active' });
|
|
287
287
|
const db = core.getDatabaseAdapter();
|
|
288
288
|
db.prepare(`
|
|
289
|
-
INSERT INTO users (id, tenant_id, email, display_name,
|
|
290
|
-
VALUES (?, ?, ?, ?,
|
|
291
|
-
`).run('expired-checkout-u1', tenantId, 'expired@example.com', 'Expired Checkout',
|
|
289
|
+
INSERT INTO users (id, tenant_id, email, display_name, is_disabled, created_at, updated_at)
|
|
290
|
+
VALUES (?, ?, ?, ?, 0, ?, ?)
|
|
291
|
+
`).run('expired-checkout-u1', tenantId, 'expired@example.com', 'Expired Checkout', now, now);
|
|
292
292
|
core.transitionCanonicalCommercialState({
|
|
293
293
|
accountId: 'expired-checkout-u1',
|
|
294
294
|
planVersionId: 'basic-v1',
|
|
@@ -340,4 +340,176 @@ describe('TaskforceCore onboarding policy', () => {
|
|
|
340
340
|
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
341
341
|
}
|
|
342
342
|
});
|
|
343
|
+
it('treats an expired trialing entitlement as requiring plan reselection', () => {
|
|
344
|
+
const projectRoot = makeProjectRoot();
|
|
345
|
+
const tenantId = path.basename(projectRoot);
|
|
346
|
+
const core = openCore(projectRoot);
|
|
347
|
+
try {
|
|
348
|
+
const now = new Date().toISOString();
|
|
349
|
+
const expiredAt = new Date(Date.now() - (60 * 1000)).toISOString();
|
|
350
|
+
core.upsertPlanCatalog({ planId: 'basic', displayName: 'Basic', status: 'active' });
|
|
351
|
+
const db = core.getDatabaseAdapter();
|
|
352
|
+
db.prepare(`
|
|
353
|
+
INSERT INTO users (id, tenant_id, email, display_name, is_disabled, created_at, updated_at)
|
|
354
|
+
VALUES (?, ?, ?, ?, 0, ?, ?)
|
|
355
|
+
`).run('expired-trial-u1', tenantId, 'expired-trial@example.com', 'Expired Trial', now, now);
|
|
356
|
+
core.transitionCanonicalCommercialState({
|
|
357
|
+
accountId: 'expired-trial-u1',
|
|
358
|
+
planVersionId: 'basic-v1',
|
|
359
|
+
state: 'trialing',
|
|
360
|
+
source: 'system',
|
|
361
|
+
updatedBy: 'trial-start',
|
|
362
|
+
billingMirrorPlanId: 'basic',
|
|
363
|
+
effectiveAt: now,
|
|
364
|
+
effectiveUntil: expiredAt,
|
|
365
|
+
createdAt: now,
|
|
366
|
+
updatedAt: now
|
|
367
|
+
});
|
|
368
|
+
expect(core.getAccountAccessStatus('expired-trial-u1')).toEqual(expect.objectContaining({
|
|
369
|
+
gate: 'plan_selection_required',
|
|
370
|
+
canAccessApp: false,
|
|
371
|
+
canAccessPlans: true,
|
|
372
|
+
planSelectionRequired: true,
|
|
373
|
+
message: 'Choose a plan to continue.'
|
|
374
|
+
}));
|
|
375
|
+
expect(core.getAccountEntitlement('expired-trial-u1')).toEqual(expect.objectContaining({
|
|
376
|
+
state: 'pending_plan_selection',
|
|
377
|
+
effectiveUntil: null
|
|
378
|
+
}));
|
|
379
|
+
const entitlementRow = db.prepare(`
|
|
380
|
+
SELECT entitlement_state, effective_until, updated_by
|
|
381
|
+
FROM account_entitlements
|
|
382
|
+
WHERE tenant_id = ? AND account_id = ?
|
|
383
|
+
LIMIT 1
|
|
384
|
+
`).get(tenantId, 'expired-trial-u1');
|
|
385
|
+
expect(entitlementRow).toEqual(expect.objectContaining({
|
|
386
|
+
entitlement_state: 'pending_plan_selection',
|
|
387
|
+
effective_until: null,
|
|
388
|
+
updated_by: 'trial-expired'
|
|
389
|
+
}));
|
|
390
|
+
const billingRow = db.prepare(`
|
|
391
|
+
SELECT entitlement_state, tier, effective_until
|
|
392
|
+
FROM user_billing
|
|
393
|
+
WHERE tenant_id = ? AND user_id = ?
|
|
394
|
+
LIMIT 1
|
|
395
|
+
`).get(tenantId, 'expired-trial-u1');
|
|
396
|
+
expect(billingRow).toEqual(expect.objectContaining({
|
|
397
|
+
entitlement_state: 'pending_plan_selection',
|
|
398
|
+
tier: '',
|
|
399
|
+
effective_until: null
|
|
400
|
+
}));
|
|
401
|
+
}
|
|
402
|
+
finally {
|
|
403
|
+
core.close();
|
|
404
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
it('auto-heals a pending-plan-selection entitlement from an active Stripe billing mirror', () => {
|
|
408
|
+
const projectRoot = makeProjectRoot();
|
|
409
|
+
const tenantId = path.basename(projectRoot);
|
|
410
|
+
const core = openCore(projectRoot);
|
|
411
|
+
try {
|
|
412
|
+
const now = new Date().toISOString();
|
|
413
|
+
const nextRenewal = new Date(Date.now() + (24 * 60 * 60 * 1000)).toISOString();
|
|
414
|
+
core.upsertPlanCatalog({ planId: 'team-5', displayName: 'Team 5', status: 'active' });
|
|
415
|
+
const db = core.getDatabaseAdapter();
|
|
416
|
+
db.prepare(`
|
|
417
|
+
INSERT INTO users (id, tenant_id, email, display_name, is_disabled, created_at, updated_at)
|
|
418
|
+
VALUES (?, ?, ?, ?, 0, ?, ?)
|
|
419
|
+
`).run('mirror-heal-u1', tenantId, 'mirror-heal@example.com', 'Mirror Heal', now, now);
|
|
420
|
+
core.transitionCanonicalCommercialState({
|
|
421
|
+
accountId: 'mirror-heal-u1',
|
|
422
|
+
planVersionId: 'free-v1',
|
|
423
|
+
state: 'pending_plan_selection',
|
|
424
|
+
source: 'backfill',
|
|
425
|
+
updatedBy: 'backfill',
|
|
426
|
+
billingMirrorPlanId: null,
|
|
427
|
+
effectiveAt: now,
|
|
428
|
+
createdAt: now,
|
|
429
|
+
updatedAt: now
|
|
430
|
+
});
|
|
431
|
+
db.prepare(`
|
|
432
|
+
UPDATE user_billing
|
|
433
|
+
SET stripe_customer_id = ?, stripe_subscription_id = ?, entitlement_state = ?, tier = ?, effective_until = ?, updated_at = ?
|
|
434
|
+
WHERE tenant_id = ? AND user_id = ?
|
|
435
|
+
`).run('cus_heal', 'sub_heal', 'active', 'team-5', nextRenewal, now, tenantId, 'mirror-heal-u1');
|
|
436
|
+
expect(core.getAccountAccessStatus('mirror-heal-u1')).toEqual(expect.objectContaining({
|
|
437
|
+
gate: 'ok',
|
|
438
|
+
canAccessApp: true,
|
|
439
|
+
planSelectionRequired: false
|
|
440
|
+
}));
|
|
441
|
+
expect(core.getAccountEntitlement('mirror-heal-u1')).toEqual(expect.objectContaining({
|
|
442
|
+
state: 'active',
|
|
443
|
+
planId: 'team-5',
|
|
444
|
+
planVersionId: 'team-5-v1',
|
|
445
|
+
effectiveUntil: nextRenewal
|
|
446
|
+
}));
|
|
447
|
+
const entitlementRow = db.prepare(`
|
|
448
|
+
SELECT entitlement_state, plan_version_id, effective_until, updated_by
|
|
449
|
+
FROM account_entitlements
|
|
450
|
+
WHERE tenant_id = ? AND account_id = ?
|
|
451
|
+
LIMIT 1
|
|
452
|
+
`).get(tenantId, 'mirror-heal-u1');
|
|
453
|
+
expect(entitlementRow).toEqual(expect.objectContaining({
|
|
454
|
+
entitlement_state: 'active',
|
|
455
|
+
plan_version_id: 'team-5-v1',
|
|
456
|
+
effective_until: nextRenewal,
|
|
457
|
+
updated_by: 'billing-mirror-auto-heal'
|
|
458
|
+
}));
|
|
459
|
+
}
|
|
460
|
+
finally {
|
|
461
|
+
core.close();
|
|
462
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
it('auto-heals a pending-plan-selection entitlement from Stripe status and price mapping when mirror state is stale', () => {
|
|
466
|
+
const projectRoot = makeProjectRoot();
|
|
467
|
+
const tenantId = path.basename(projectRoot);
|
|
468
|
+
const core = openCore(projectRoot);
|
|
469
|
+
try {
|
|
470
|
+
const now = new Date().toISOString();
|
|
471
|
+
const nextRenewal = new Date(Date.now() + (24 * 60 * 60 * 1000)).toISOString();
|
|
472
|
+
core.upsertPlanCatalog({ planId: 'team-5', displayName: 'Team 5', status: 'active' });
|
|
473
|
+
const db = core.getDatabaseAdapter();
|
|
474
|
+
db.prepare(`
|
|
475
|
+
INSERT INTO users (id, tenant_id, email, display_name, is_disabled, created_at, updated_at)
|
|
476
|
+
VALUES (?, ?, ?, ?, 0, ?, ?)
|
|
477
|
+
`).run('mirror-price-heal-u1', tenantId, 'mirror-price-heal@example.com', 'Mirror Price Heal', now, now);
|
|
478
|
+
core.transitionCanonicalCommercialState({
|
|
479
|
+
accountId: 'mirror-price-heal-u1',
|
|
480
|
+
planVersionId: 'free-v1',
|
|
481
|
+
state: 'pending_plan_selection',
|
|
482
|
+
source: 'backfill',
|
|
483
|
+
updatedBy: 'checkout-expired',
|
|
484
|
+
billingMirrorPlanId: null,
|
|
485
|
+
effectiveAt: now,
|
|
486
|
+
createdAt: now,
|
|
487
|
+
updatedAt: now
|
|
488
|
+
});
|
|
489
|
+
db.prepare(`
|
|
490
|
+
UPDATE user_billing
|
|
491
|
+
SET stripe_customer_id = ?, stripe_subscription_id = ?, stripe_price_id = ?, stripe_status = ?, entitlement_state = ?, tier = ?, effective_until = ?, updated_at = ?
|
|
492
|
+
WHERE tenant_id = ? AND user_id = ?
|
|
493
|
+
`).run('cus_heal_2', 'sub_heal_2', 'price_team_5_month', 'active', 'pending_plan_selection', 'team-5', nextRenewal, now, tenantId, 'mirror-price-heal-u1');
|
|
494
|
+
db.prepare(`
|
|
495
|
+
INSERT INTO billing_price_mappings (tenant_id, plan_version_id, billing_interval, stripe_price_id, active, created_at, updated_at)
|
|
496
|
+
VALUES (?, ?, ?, ?, 1, ?, ?)
|
|
497
|
+
`).run(tenantId, 'team-5-v1', 'month', 'price_team_5_month', now, now);
|
|
498
|
+
expect(core.getAccountAccessStatus('mirror-price-heal-u1')).toEqual(expect.objectContaining({
|
|
499
|
+
gate: 'ok',
|
|
500
|
+
canAccessApp: true,
|
|
501
|
+
planSelectionRequired: false
|
|
502
|
+
}));
|
|
503
|
+
expect(core.getAccountEntitlement('mirror-price-heal-u1')).toEqual(expect.objectContaining({
|
|
504
|
+
state: 'active',
|
|
505
|
+
planId: 'team-5',
|
|
506
|
+
planVersionId: 'team-5-v1',
|
|
507
|
+
effectiveUntil: nextRenewal
|
|
508
|
+
}));
|
|
509
|
+
}
|
|
510
|
+
finally {
|
|
511
|
+
core.close();
|
|
512
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
513
|
+
}
|
|
514
|
+
});
|
|
343
515
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DEFAULT_TYPE_VALUE } from '../shared/taxonomyDefaults.js';
|
|
2
|
+
import { buildTaskSyncChangeKey } from '../sync/taskSyncChangeKey.js';
|
|
2
3
|
export function listTasksForSyncService(deps, workspaceId, cursor, limit) {
|
|
3
4
|
const normalizedWorkspaceId = deps.normalizeWorkspaceId(workspaceId);
|
|
4
5
|
const safeLimit = Math.max(1, Math.min(10000, Math.floor(limit)));
|
|
@@ -67,72 +68,8 @@ export function listWorkspaceSyncDeletesSinceService(deps, workspaceId, cursor,
|
|
|
67
68
|
}))
|
|
68
69
|
.filter((row) => row.taskId.length > 0 && Number.isFinite(Date.parse(row.deletedAt)));
|
|
69
70
|
}
|
|
70
|
-
function normalizeConflictTaxonomies(value) {
|
|
71
|
-
if (!value || typeof value !== 'object')
|
|
72
|
-
return {};
|
|
73
|
-
const source = value;
|
|
74
|
-
return Object.keys(source).sort().reduce((acc, key) => {
|
|
75
|
-
const raw = source[key];
|
|
76
|
-
if (Array.isArray(raw)) {
|
|
77
|
-
acc[key] = raw.map((entry) => String(entry ?? '')).sort();
|
|
78
|
-
}
|
|
79
|
-
else if (raw === null || raw === undefined) {
|
|
80
|
-
acc[key] = '';
|
|
81
|
-
}
|
|
82
|
-
else {
|
|
83
|
-
acc[key] = String(raw);
|
|
84
|
-
}
|
|
85
|
-
return acc;
|
|
86
|
-
}, {});
|
|
87
|
-
}
|
|
88
|
-
function normalizeConflictChecklistItems(value) {
|
|
89
|
-
if (!Array.isArray(value))
|
|
90
|
-
return [];
|
|
91
|
-
return value
|
|
92
|
-
.map((item) => {
|
|
93
|
-
const entry = item && typeof item === 'object' ? item : {};
|
|
94
|
-
return {
|
|
95
|
-
id: String(entry.id || '').trim(),
|
|
96
|
-
title: String(entry.title || '').trim(),
|
|
97
|
-
isCompleted: Boolean(entry.isCompleted),
|
|
98
|
-
order: entry.order === undefined || entry.order === null ? '' : String(entry.order),
|
|
99
|
-
};
|
|
100
|
-
})
|
|
101
|
-
.sort((left, right) => {
|
|
102
|
-
const leftOrder = Number.parseInt(left.order, 10);
|
|
103
|
-
const rightOrder = Number.parseInt(right.order, 10);
|
|
104
|
-
if (Number.isFinite(leftOrder) && Number.isFinite(rightOrder) && leftOrder !== rightOrder) {
|
|
105
|
-
return leftOrder - rightOrder;
|
|
106
|
-
}
|
|
107
|
-
if (left.title !== right.title)
|
|
108
|
-
return left.title.localeCompare(right.title);
|
|
109
|
-
return left.id.localeCompare(right.id);
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
71
|
function buildSyncConflictKey(task, archivedValue) {
|
|
113
|
-
|
|
114
|
-
return JSON.stringify({
|
|
115
|
-
id: String(source.id || ''),
|
|
116
|
-
title: String(source.title || ''),
|
|
117
|
-
description: source.description === null || source.description === undefined ? '' : String(source.description),
|
|
118
|
-
category: String(source.category || ''),
|
|
119
|
-
type: String(source.type || ''),
|
|
120
|
-
priority: source.priority === undefined || source.priority === null ? '' : String(source.priority),
|
|
121
|
-
complexity: source.complexity === undefined || source.complexity === null ? '' : String(source.complexity),
|
|
122
|
-
status: String(source.status || ''),
|
|
123
|
-
approach: source.approach === undefined || source.approach === null ? '' : String(source.approach),
|
|
124
|
-
canceledReason: source.canceledReason === undefined || source.canceledReason === null ? '' : String(source.canceledReason),
|
|
125
|
-
scheduledDate: source.scheduledDate === undefined || source.scheduledDate === null ? '' : String(source.scheduledDate),
|
|
126
|
-
dueDate: source.dueDate === undefined || source.dueDate === null ? '' : String(source.dueDate),
|
|
127
|
-
scheduledWeekKey: source.scheduledWeekKey === undefined || source.scheduledWeekKey === null ? '' : String(source.scheduledWeekKey),
|
|
128
|
-
orderInDay: source.orderInDay === undefined || source.orderInDay === null ? '' : String(source.orderInDay),
|
|
129
|
-
workstreamId: source.workstreamId === undefined || source.workstreamId === null ? '' : String(source.workstreamId),
|
|
130
|
-
referenceNumber: source.referenceNumber === undefined || source.referenceNumber === null ? '' : String(source.referenceNumber),
|
|
131
|
-
assignee: source.assignee === undefined || source.assignee === null ? '' : String(source.assignee),
|
|
132
|
-
taxonomies: normalizeConflictTaxonomies(source.taxonomies),
|
|
133
|
-
checklistItems: normalizeConflictChecklistItems(source.checklistItems),
|
|
134
|
-
isArchived: archivedValue ? '1' : '0'
|
|
135
|
-
});
|
|
72
|
+
return buildTaskSyncChangeKey(task, archivedValue);
|
|
136
73
|
}
|
|
137
74
|
export function applyWorkspaceSyncSnapshotService(deps, input, workspaceId) {
|
|
138
75
|
const normalizedWorkspaceId = deps.normalizeWorkspaceId(workspaceId);
|