@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
|
@@ -7,6 +7,28 @@ import { TaskforceCore } from './TaskforceCore';
|
|
|
7
7
|
import fs from 'fs';
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import modalStyles from './components/ui/Modal.module.css';
|
|
10
|
+
vi.mock('react-avatar-editor', async () => {
|
|
11
|
+
const React = await import('react');
|
|
12
|
+
return {
|
|
13
|
+
default: React.forwardRef((_props, ref) => {
|
|
14
|
+
React.useImperativeHandle(ref, () => ({
|
|
15
|
+
getImageScaledToCanvas: () => ({
|
|
16
|
+
width: 1024,
|
|
17
|
+
height: 1024,
|
|
18
|
+
getContext: () => ({
|
|
19
|
+
drawImage: () => undefined
|
|
20
|
+
}),
|
|
21
|
+
toBlob: (callback, type) => {
|
|
22
|
+
callback(new Blob([new Uint8Array([1, 2, 3, 4])], { type: type || 'image/webp' }));
|
|
23
|
+
}
|
|
24
|
+
}),
|
|
25
|
+
getImage: () => document.createElement('canvas'),
|
|
26
|
+
getCroppingRect: () => ({ x: 0, y: 0, width: 1, height: 1 })
|
|
27
|
+
}));
|
|
28
|
+
return _jsx("div", { "data-testid": "avatar-editor" });
|
|
29
|
+
})
|
|
30
|
+
};
|
|
31
|
+
});
|
|
10
32
|
const mockTasks = [
|
|
11
33
|
{
|
|
12
34
|
id: 'task-20260130-abc123',
|
|
@@ -830,6 +852,9 @@ describe('TaskforceCore Integration', () => {
|
|
|
830
852
|
});
|
|
831
853
|
it('shows full-page login when cloud auth is required and user is unauthenticated', async () => {
|
|
832
854
|
const fetchMock = vi.fn((url) => {
|
|
855
|
+
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
856
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'cloud' } }) });
|
|
857
|
+
}
|
|
833
858
|
if (url.includes('/api/taskforce/auth/session')) {
|
|
834
859
|
return Promise.resolve({
|
|
835
860
|
ok: true,
|
|
@@ -929,8 +954,8 @@ describe('TaskforceCore Integration', () => {
|
|
|
929
954
|
json: () => Promise.resolve({
|
|
930
955
|
plans: [
|
|
931
956
|
{
|
|
932
|
-
planId: '
|
|
933
|
-
displayName: '
|
|
957
|
+
planId: 'basic',
|
|
958
|
+
displayName: 'Basic Plan test mode',
|
|
934
959
|
status: 'active',
|
|
935
960
|
features: [],
|
|
936
961
|
pricing: { month: null, year: null },
|
|
@@ -946,7 +971,7 @@ describe('TaskforceCore Integration', () => {
|
|
|
946
971
|
render(_jsx(MemoryRouter, { initialEntries: ['/pricing'], children: _jsx(TaskforceCore, {}) }));
|
|
947
972
|
await waitFor(() => {
|
|
948
973
|
expect(screen.queryByPlaceholderText('Email')).not.toBeInTheDocument();
|
|
949
|
-
expect(screen.getByText('
|
|
974
|
+
expect(screen.getByText('Basic Plan test mode')).toBeInTheDocument();
|
|
950
975
|
expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
|
|
951
976
|
});
|
|
952
977
|
});
|
|
@@ -1019,8 +1044,8 @@ describe('TaskforceCore Integration', () => {
|
|
|
1019
1044
|
json: () => Promise.resolve({
|
|
1020
1045
|
plans: [
|
|
1021
1046
|
{
|
|
1022
|
-
planId: '
|
|
1023
|
-
displayName: '
|
|
1047
|
+
planId: 'free',
|
|
1048
|
+
displayName: 'Free',
|
|
1024
1049
|
status: 'active',
|
|
1025
1050
|
features: [],
|
|
1026
1051
|
pricing: {
|
|
@@ -1028,7 +1053,7 @@ describe('TaskforceCore Integration', () => {
|
|
|
1028
1053
|
year: null
|
|
1029
1054
|
},
|
|
1030
1055
|
defaultVersion: {
|
|
1031
|
-
planVersionId: '
|
|
1056
|
+
planVersionId: 'free-v1',
|
|
1032
1057
|
versionNumber: 1,
|
|
1033
1058
|
seatLimit: null,
|
|
1034
1059
|
metadata: {}
|
|
@@ -1042,8 +1067,8 @@ describe('TaskforceCore Integration', () => {
|
|
|
1042
1067
|
return Promise.resolve({
|
|
1043
1068
|
ok: true,
|
|
1044
1069
|
json: () => Promise.resolve({
|
|
1045
|
-
planId: '
|
|
1046
|
-
planName: '
|
|
1070
|
+
planId: 'free',
|
|
1071
|
+
planName: 'Free',
|
|
1047
1072
|
entitlementState: 'active'
|
|
1048
1073
|
})
|
|
1049
1074
|
});
|
|
@@ -1052,8 +1077,8 @@ describe('TaskforceCore Integration', () => {
|
|
|
1052
1077
|
return Promise.resolve({
|
|
1053
1078
|
ok: true,
|
|
1054
1079
|
json: () => Promise.resolve({
|
|
1055
|
-
planId: '
|
|
1056
|
-
planName: '
|
|
1080
|
+
planId: 'free',
|
|
1081
|
+
planName: 'Free',
|
|
1057
1082
|
entitlementState: 'active'
|
|
1058
1083
|
})
|
|
1059
1084
|
});
|
|
@@ -1082,7 +1107,6 @@ describe('TaskforceCore Integration', () => {
|
|
|
1082
1107
|
render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, {}) }));
|
|
1083
1108
|
await waitFor(() => {
|
|
1084
1109
|
expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
|
|
1085
|
-
expect(screen.getByText('Choose a plan to continue.')).toBeInTheDocument();
|
|
1086
1110
|
});
|
|
1087
1111
|
});
|
|
1088
1112
|
it('redirects authenticated users with a blocking account access gate to the plans page', async () => {
|
|
@@ -1154,8 +1178,8 @@ describe('TaskforceCore Integration', () => {
|
|
|
1154
1178
|
json: () => Promise.resolve({
|
|
1155
1179
|
plans: [
|
|
1156
1180
|
{
|
|
1157
|
-
planId: '
|
|
1158
|
-
displayName: '
|
|
1181
|
+
planId: 'free',
|
|
1182
|
+
displayName: 'Free',
|
|
1159
1183
|
status: 'active',
|
|
1160
1184
|
features: [],
|
|
1161
1185
|
pricing: {
|
|
@@ -1163,7 +1187,7 @@ describe('TaskforceCore Integration', () => {
|
|
|
1163
1187
|
year: null
|
|
1164
1188
|
},
|
|
1165
1189
|
defaultVersion: {
|
|
1166
|
-
planVersionId: '
|
|
1190
|
+
planVersionId: 'free-v1',
|
|
1167
1191
|
versionNumber: 1,
|
|
1168
1192
|
seatLimit: null,
|
|
1169
1193
|
metadata: {}
|
|
@@ -1269,8 +1293,8 @@ describe('TaskforceCore Integration', () => {
|
|
|
1269
1293
|
json: () => Promise.resolve({
|
|
1270
1294
|
plans: [
|
|
1271
1295
|
{
|
|
1272
|
-
planId: '
|
|
1273
|
-
displayName: '
|
|
1296
|
+
planId: 'free',
|
|
1297
|
+
displayName: 'Free',
|
|
1274
1298
|
status: 'active',
|
|
1275
1299
|
features: [],
|
|
1276
1300
|
pricing: {
|
|
@@ -1278,7 +1302,7 @@ describe('TaskforceCore Integration', () => {
|
|
|
1278
1302
|
year: null
|
|
1279
1303
|
},
|
|
1280
1304
|
defaultVersion: {
|
|
1281
|
-
planVersionId: '
|
|
1305
|
+
planVersionId: 'free-v1',
|
|
1282
1306
|
versionNumber: 1,
|
|
1283
1307
|
seatLimit: null,
|
|
1284
1308
|
metadata: {}
|
|
@@ -1292,9 +1316,9 @@ describe('TaskforceCore Integration', () => {
|
|
|
1292
1316
|
return Promise.resolve({
|
|
1293
1317
|
ok: true,
|
|
1294
1318
|
json: () => Promise.resolve({
|
|
1295
|
-
planId: '
|
|
1296
|
-
planName: '
|
|
1297
|
-
planVersionId: '
|
|
1319
|
+
planId: 'free',
|
|
1320
|
+
planName: 'Free',
|
|
1321
|
+
planVersionId: 'free-v1',
|
|
1298
1322
|
entitlementState: 'active'
|
|
1299
1323
|
})
|
|
1300
1324
|
});
|
|
@@ -1323,7 +1347,7 @@ describe('TaskforceCore Integration', () => {
|
|
|
1323
1347
|
render(_jsx(MemoryRouter, { initialEntries: ['/?screen=plans&gate=missing_entitlement'], children: _jsx(TaskforceCore, {}) }));
|
|
1324
1348
|
await waitFor(() => {
|
|
1325
1349
|
expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
|
|
1326
|
-
expect(screen.getByText('
|
|
1350
|
+
expect(screen.getByText('Free')).toBeInTheDocument();
|
|
1327
1351
|
});
|
|
1328
1352
|
await waitFor(() => {
|
|
1329
1353
|
expect(screen.queryByText('No entitlement is linked to this account.')).not.toBeInTheDocument();
|
|
@@ -1393,8 +1417,8 @@ describe('TaskforceCore Integration', () => {
|
|
|
1393
1417
|
return Promise.resolve({
|
|
1394
1418
|
ok: true,
|
|
1395
1419
|
json: () => Promise.resolve({
|
|
1396
|
-
planId: '
|
|
1397
|
-
planName: '
|
|
1420
|
+
planId: 'free',
|
|
1421
|
+
planName: 'Free',
|
|
1398
1422
|
entitlementState: 'active'
|
|
1399
1423
|
})
|
|
1400
1424
|
});
|
|
@@ -1450,14 +1474,14 @@ describe('TaskforceCore Integration', () => {
|
|
|
1450
1474
|
});
|
|
1451
1475
|
await waitFor(() => {
|
|
1452
1476
|
expect(screen.getByText('Subscription')).toBeInTheDocument();
|
|
1453
|
-
expect(screen.getByText('
|
|
1477
|
+
expect(screen.getByText('Free')).toBeInTheDocument();
|
|
1454
1478
|
expect(screen.getByText('Runtime')).toBeInTheDocument();
|
|
1455
1479
|
expect(screen.getByText('LOCAL')).toBeInTheDocument();
|
|
1456
1480
|
expect(screen.getByText('Environment')).toBeInTheDocument();
|
|
1457
1481
|
expect(screen.getByText('CLOUD.EXAMPLE.COM')).toBeInTheDocument();
|
|
1458
1482
|
expect(screen.queryByText(/Current Plan:/i)).not.toBeInTheDocument();
|
|
1459
1483
|
});
|
|
1460
|
-
await user.click(screen.getByRole('button', { name: /Subscription
|
|
1484
|
+
await user.click(screen.getByRole('button', { name: /Subscription Free/i }));
|
|
1461
1485
|
await waitFor(() => {
|
|
1462
1486
|
expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
|
|
1463
1487
|
});
|
|
@@ -1512,8 +1536,8 @@ describe('TaskforceCore Integration', () => {
|
|
|
1512
1536
|
return Promise.resolve({
|
|
1513
1537
|
ok: true,
|
|
1514
1538
|
json: () => Promise.resolve({
|
|
1515
|
-
planId: '
|
|
1516
|
-
planName: '
|
|
1539
|
+
planId: 'free',
|
|
1540
|
+
planName: 'Free',
|
|
1517
1541
|
entitlementState: 'active'
|
|
1518
1542
|
})
|
|
1519
1543
|
});
|
|
@@ -1563,7 +1587,7 @@ describe('TaskforceCore Integration', () => {
|
|
|
1563
1587
|
}
|
|
1564
1588
|
await waitFor(() => {
|
|
1565
1589
|
expect(screen.getByText('Subscription')).toBeInTheDocument();
|
|
1566
|
-
expect(screen.getByText('
|
|
1590
|
+
expect(screen.getByText('Free')).toBeInTheDocument();
|
|
1567
1591
|
expect(screen.getByText('Runtime')).toBeInTheDocument();
|
|
1568
1592
|
expect(screen.getByText('LOCAL')).toBeInTheDocument();
|
|
1569
1593
|
expect(screen.getByText('Environment')).toBeInTheDocument();
|
|
@@ -1590,9 +1614,9 @@ describe('TaskforceCore Integration', () => {
|
|
|
1590
1614
|
userId: 'user-new',
|
|
1591
1615
|
displayName: 'New User',
|
|
1592
1616
|
workspaceId: 'workspace-local-active',
|
|
1593
|
-
planId: '
|
|
1594
|
-
planVersionId: '
|
|
1595
|
-
planName: '
|
|
1617
|
+
planId: 'free',
|
|
1618
|
+
planVersionId: 'free-v1',
|
|
1619
|
+
planName: 'Free',
|
|
1596
1620
|
teamManagementAllowed: false,
|
|
1597
1621
|
teamPlanMode: 'personal',
|
|
1598
1622
|
workspaceRole: 'member'
|
|
@@ -1690,11 +1714,11 @@ describe('TaskforceCore Integration', () => {
|
|
|
1690
1714
|
return jsonResponse({
|
|
1691
1715
|
plans: [
|
|
1692
1716
|
{
|
|
1693
|
-
planId: '
|
|
1694
|
-
displayName: '
|
|
1717
|
+
planId: 'free',
|
|
1718
|
+
displayName: 'Free',
|
|
1695
1719
|
status: 'active',
|
|
1696
1720
|
defaultVersion: {
|
|
1697
|
-
planVersionId: '
|
|
1721
|
+
planVersionId: 'free-v1',
|
|
1698
1722
|
versionNumber: 1,
|
|
1699
1723
|
seatLimit: null,
|
|
1700
1724
|
metadata: {}
|
|
@@ -1786,11 +1810,12 @@ describe('TaskforceCore Integration', () => {
|
|
|
1786
1810
|
if (url.includes('/api/taskforce/workspace/assignee-options')) {
|
|
1787
1811
|
return jsonResponse({ assignees: [] });
|
|
1788
1812
|
}
|
|
1789
|
-
if (url.includes('/api/taskforce/
|
|
1790
|
-
return jsonResponse(
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1813
|
+
if (url.includes('/api/taskforce/planning/bootstrap')) {
|
|
1814
|
+
return jsonResponse({
|
|
1815
|
+
initiatives: [],
|
|
1816
|
+
workstreams: [],
|
|
1817
|
+
workstreamTaskSummaries: [],
|
|
1818
|
+
});
|
|
1794
1819
|
}
|
|
1795
1820
|
return jsonResponse({});
|
|
1796
1821
|
});
|
|
@@ -1817,7 +1842,8 @@ describe('TaskforceCore Integration', () => {
|
|
|
1817
1842
|
await user.click(screen.getByRole('button', { name: 'Register' }));
|
|
1818
1843
|
await user.type(await screen.findByPlaceholderText('Email'), accounts.newcomer.email);
|
|
1819
1844
|
await user.type(screen.getByPlaceholderText('Display name'), accounts.newcomer.displayName);
|
|
1820
|
-
await user.type(screen.getByPlaceholderText('Password'), '
|
|
1845
|
+
await user.type(screen.getByPlaceholderText('Password'), 'GoodPassphrase123!');
|
|
1846
|
+
await user.type(screen.getByPlaceholderText('Confirm password'), 'GoodPassphrase123!');
|
|
1821
1847
|
await user.click(screen.getByRole('button', { name: 'Create Account' }));
|
|
1822
1848
|
await waitFor(() => {
|
|
1823
1849
|
expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
|
|
@@ -1825,7 +1851,7 @@ describe('TaskforceCore Integration', () => {
|
|
|
1825
1851
|
await user.click(screen.getByTitle(/Account/i));
|
|
1826
1852
|
await waitFor(() => {
|
|
1827
1853
|
expect(screen.getByText(accounts.newcomer.email)).toBeInTheDocument();
|
|
1828
|
-
expect(screen.getByText('
|
|
1854
|
+
expect(screen.getByText('Free')).toBeInTheDocument();
|
|
1829
1855
|
});
|
|
1830
1856
|
await user.click(screen.getByRole('menuitem', { name: 'Sign Out' }));
|
|
1831
1857
|
await waitFor(() => {
|
|
@@ -1846,7 +1872,7 @@ describe('TaskforceCore Integration', () => {
|
|
|
1846
1872
|
await waitFor(() => {
|
|
1847
1873
|
expect(screen.getByText(accounts.ross.email)).toBeInTheDocument();
|
|
1848
1874
|
expect(screen.getByText('Team 5')).toBeInTheDocument();
|
|
1849
|
-
expect(screen.queryByText('
|
|
1875
|
+
expect(screen.queryByText('Free')).not.toBeInTheDocument();
|
|
1850
1876
|
expect(screen.getByRole('menuitem', { name: 'Team Management' })).toBeInTheDocument();
|
|
1851
1877
|
});
|
|
1852
1878
|
await user.click(screen.getByRole('button', { name: /Subscription Team 5/i }));
|
|
@@ -1899,6 +1925,7 @@ describe('TaskforceCore Integration', () => {
|
|
|
1899
1925
|
expect(screen.getByText('Create your Taskforce account.')).toBeInTheDocument();
|
|
1900
1926
|
expect(screen.getByLabelText('Email')).toBeInTheDocument();
|
|
1901
1927
|
expect(screen.getByLabelText('Display name')).toBeInTheDocument();
|
|
1928
|
+
expect(screen.getByLabelText('Confirm password')).toBeInTheDocument();
|
|
1902
1929
|
expect(screen.getByPlaceholderText('Email')).toBeInTheDocument();
|
|
1903
1930
|
expect(screen.getByPlaceholderText('Display name')).toBeInTheDocument();
|
|
1904
1931
|
expect(screen.getByRole('button', { name: 'Create Account' })).toBeInTheDocument();
|
|
@@ -1910,6 +1937,56 @@ describe('TaskforceCore Integration', () => {
|
|
|
1910
1937
|
});
|
|
1911
1938
|
expect(container.querySelector('[aria-pressed]')).not.toBeInTheDocument();
|
|
1912
1939
|
});
|
|
1940
|
+
it('blocks registration when the confirmation does not match', async () => {
|
|
1941
|
+
const user = userEvent.setup();
|
|
1942
|
+
const fetchMock = vi.fn((url) => {
|
|
1943
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
1944
|
+
return Promise.resolve({
|
|
1945
|
+
ok: true,
|
|
1946
|
+
json: () => Promise.resolve({
|
|
1947
|
+
authenticated: false,
|
|
1948
|
+
runtimeMode: 'cloud',
|
|
1949
|
+
authRequiredForApi: false
|
|
1950
|
+
})
|
|
1951
|
+
});
|
|
1952
|
+
}
|
|
1953
|
+
if (url.includes('/api/taskforce/config')) {
|
|
1954
|
+
return Promise.resolve({
|
|
1955
|
+
ok: true,
|
|
1956
|
+
json: () => Promise.resolve({
|
|
1957
|
+
...mockConfig,
|
|
1958
|
+
runtimeMode: 'cloud',
|
|
1959
|
+
authRequiredForApi: false,
|
|
1960
|
+
setupState: {
|
|
1961
|
+
globalSetupState: 'present',
|
|
1962
|
+
workspaceSetupState: 'present',
|
|
1963
|
+
runtimeMode: 'cloud',
|
|
1964
|
+
workspaceId: 'workspace-local-active',
|
|
1965
|
+
mode: 'core',
|
|
1966
|
+
forceSetup: false,
|
|
1967
|
+
forceGlobalSetup: false,
|
|
1968
|
+
forceWorkspaceSetup: false
|
|
1969
|
+
}
|
|
1970
|
+
})
|
|
1971
|
+
});
|
|
1972
|
+
}
|
|
1973
|
+
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
1974
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
1975
|
+
}
|
|
1976
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
1977
|
+
});
|
|
1978
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
1979
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
|
|
1980
|
+
await user.type(await screen.findByPlaceholderText('Email'), 'mismatch@example.com');
|
|
1981
|
+
await user.type(screen.getByPlaceholderText('Display name'), 'Mismatch User');
|
|
1982
|
+
await user.type(screen.getByPlaceholderText('Password'), 'GoodPassphrase123!');
|
|
1983
|
+
await user.type(screen.getByPlaceholderText('Confirm password'), 'DifferentPassphrase123!');
|
|
1984
|
+
await user.click(screen.getByRole('button', { name: 'Create Account' }));
|
|
1985
|
+
await waitFor(() => {
|
|
1986
|
+
expect(screen.getByText('Passwords do not match.')).toBeInTheDocument();
|
|
1987
|
+
});
|
|
1988
|
+
expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('/api/taskforce/auth/register'))).toBe(false);
|
|
1989
|
+
});
|
|
1913
1990
|
it('keeps the verification screen focused on verification and lets the user request a fresh email token', async () => {
|
|
1914
1991
|
const fetchMock = vi.fn((url) => {
|
|
1915
1992
|
if (url.includes('/api/taskforce/auth/session')) {
|
|
@@ -2034,6 +2111,69 @@ describe('TaskforceCore Integration', () => {
|
|
|
2034
2111
|
expect(screen.getByRole('button', { name: /Resend Verification/ })).toBeDisabled();
|
|
2035
2112
|
expect(fetchMock.mock.calls.some(([url]) => String(url).includes('/api/taskforce/auth/verify-email/request'))).toBe(true);
|
|
2036
2113
|
});
|
|
2114
|
+
it('shows neutral verification copy when resend succeeds without attempting delivery', async () => {
|
|
2115
|
+
const user = userEvent.setup();
|
|
2116
|
+
const fetchMock = vi.fn((input) => {
|
|
2117
|
+
const url = String(input);
|
|
2118
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
2119
|
+
return Promise.resolve({
|
|
2120
|
+
ok: true,
|
|
2121
|
+
json: () => Promise.resolve({
|
|
2122
|
+
authenticated: false,
|
|
2123
|
+
runtimeMode: 'cloud',
|
|
2124
|
+
authRequiredForApi: false
|
|
2125
|
+
})
|
|
2126
|
+
});
|
|
2127
|
+
}
|
|
2128
|
+
if (url.includes('/api/taskforce/config')) {
|
|
2129
|
+
return Promise.resolve({
|
|
2130
|
+
ok: true,
|
|
2131
|
+
json: () => Promise.resolve({
|
|
2132
|
+
...mockConfig,
|
|
2133
|
+
runtimeMode: 'cloud',
|
|
2134
|
+
authRequiredForApi: false,
|
|
2135
|
+
setupState: {
|
|
2136
|
+
globalSetupState: 'present',
|
|
2137
|
+
workspaceSetupState: 'present',
|
|
2138
|
+
runtimeMode: 'cloud',
|
|
2139
|
+
workspaceId: 'workspace-local-active',
|
|
2140
|
+
mode: 'core',
|
|
2141
|
+
forceSetup: false,
|
|
2142
|
+
forceGlobalSetup: false,
|
|
2143
|
+
forceWorkspaceSetup: false
|
|
2144
|
+
}
|
|
2145
|
+
})
|
|
2146
|
+
});
|
|
2147
|
+
}
|
|
2148
|
+
if (url.includes('/api/taskforce/auth/verify-email/request')) {
|
|
2149
|
+
return Promise.resolve({
|
|
2150
|
+
ok: true,
|
|
2151
|
+
json: () => Promise.resolve({
|
|
2152
|
+
success: true,
|
|
2153
|
+
emailSent: false,
|
|
2154
|
+
deliveryAttempted: false,
|
|
2155
|
+
verificationToken: null
|
|
2156
|
+
})
|
|
2157
|
+
});
|
|
2158
|
+
}
|
|
2159
|
+
if (url.includes('/api/taskforce/public/pricing')) {
|
|
2160
|
+
return Promise.resolve({
|
|
2161
|
+
ok: true,
|
|
2162
|
+
json: () => Promise.resolve({
|
|
2163
|
+
plans: []
|
|
2164
|
+
})
|
|
2165
|
+
});
|
|
2166
|
+
}
|
|
2167
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
2168
|
+
});
|
|
2169
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
2170
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=verify'], children: _jsx(TaskforceCore, {}) }));
|
|
2171
|
+
await user.type(await screen.findByPlaceholderText('Email'), 'verify@example.com');
|
|
2172
|
+
await user.click(screen.getByRole('button', { name: 'Resend Verification' }));
|
|
2173
|
+
await waitFor(() => {
|
|
2174
|
+
expect(screen.getByText('If that account exists and still needs verification, we sent an email.')).toBeInTheDocument();
|
|
2175
|
+
});
|
|
2176
|
+
});
|
|
2037
2177
|
it('does not switch to verify mode after registration when verification is not required', async () => {
|
|
2038
2178
|
const user = userEvent.setup();
|
|
2039
2179
|
let authenticated = false;
|
|
@@ -2138,7 +2278,8 @@ describe('TaskforceCore Integration', () => {
|
|
|
2138
2278
|
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2Fplans'], children: _jsx(TaskforceCore, {}) }));
|
|
2139
2279
|
await user.type(await screen.findByPlaceholderText('Email'), 'local@example.com');
|
|
2140
2280
|
await user.type(screen.getByPlaceholderText('Display name'), 'Local User');
|
|
2141
|
-
await user.type(screen.getByPlaceholderText('Password'), '
|
|
2281
|
+
await user.type(screen.getByPlaceholderText('Password'), 'GoodPassphrase123!');
|
|
2282
|
+
await user.type(screen.getByPlaceholderText('Confirm password'), 'GoodPassphrase123!');
|
|
2142
2283
|
await user.click(screen.getByRole('button', { name: 'Create Account' }));
|
|
2143
2284
|
await waitFor(() => {
|
|
2144
2285
|
expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
|
|
@@ -2228,11 +2369,11 @@ describe('TaskforceCore Integration', () => {
|
|
|
2228
2369
|
if (url.includes('/api/taskforce/public/pricing')) {
|
|
2229
2370
|
return jsonResponse({
|
|
2230
2371
|
plans: [{
|
|
2231
|
-
planId: '
|
|
2232
|
-
displayName: '
|
|
2372
|
+
planId: 'free',
|
|
2373
|
+
displayName: 'Free',
|
|
2233
2374
|
status: 'active',
|
|
2234
2375
|
defaultVersion: {
|
|
2235
|
-
planVersionId: '
|
|
2376
|
+
planVersionId: 'free-v1',
|
|
2236
2377
|
versionNumber: 1,
|
|
2237
2378
|
seatLimit: null,
|
|
2238
2379
|
metadata: {}
|
|
@@ -2240,10 +2381,10 @@ describe('TaskforceCore Integration', () => {
|
|
|
2240
2381
|
features: [],
|
|
2241
2382
|
pricing: {
|
|
2242
2383
|
month: {
|
|
2243
|
-
planVersionId: '
|
|
2384
|
+
planVersionId: 'free-v1',
|
|
2244
2385
|
interval: 'month',
|
|
2245
2386
|
pricingType: 'free',
|
|
2246
|
-
stripePriceId: 'free:
|
|
2387
|
+
stripePriceId: 'free:free-v1:month',
|
|
2247
2388
|
displayAmount: '$0.00',
|
|
2248
2389
|
unitAmount: 0,
|
|
2249
2390
|
currency: 'usd'
|
|
@@ -2296,11 +2437,11 @@ describe('TaskforceCore Integration', () => {
|
|
|
2296
2437
|
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
|
|
2297
2438
|
await user.type(await screen.findByPlaceholderText('Email'), 'pending@example.com');
|
|
2298
2439
|
await user.type(screen.getByPlaceholderText('Display name'), 'Pending User');
|
|
2299
|
-
await user.type(screen.getByPlaceholderText('Password'), '
|
|
2440
|
+
await user.type(screen.getByPlaceholderText('Password'), 'GoodPassphrase123!');
|
|
2441
|
+
await user.type(screen.getByPlaceholderText('Confirm password'), 'GoodPassphrase123!');
|
|
2300
2442
|
await user.click(screen.getByRole('button', { name: 'Create Account' }));
|
|
2301
2443
|
await waitFor(() => {
|
|
2302
2444
|
expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
|
|
2303
|
-
expect(screen.getByText('Choose a plan to continue.')).toBeInTheDocument();
|
|
2304
2445
|
});
|
|
2305
2446
|
expect(fetchMock.mock.calls.some(([url]) => String(url).includes('/api/taskforce/billing/checkout-session'))).toBe(false);
|
|
2306
2447
|
});
|
|
@@ -2412,11 +2553,148 @@ describe('TaskforceCore Integration', () => {
|
|
|
2412
2553
|
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
|
|
2413
2554
|
await user.type(await screen.findByPlaceholderText('Email'), 'brandnew@example.com');
|
|
2414
2555
|
await user.type(screen.getByPlaceholderText('Display name'), 'Brand New');
|
|
2415
|
-
await user.type(screen.getByPlaceholderText('Password'), '
|
|
2556
|
+
await user.type(screen.getByPlaceholderText('Password'), 'GoodPassphrase123!');
|
|
2557
|
+
await user.type(screen.getByPlaceholderText('Confirm password'), 'GoodPassphrase123!');
|
|
2416
2558
|
await user.click(screen.getByRole('button', { name: 'Create Account' }));
|
|
2417
2559
|
await waitFor(() => {
|
|
2418
2560
|
expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
|
|
2419
|
-
|
|
2561
|
+
});
|
|
2562
|
+
expect(screen.queryByText('Configure your new workspace.')).not.toBeInTheDocument();
|
|
2563
|
+
});
|
|
2564
|
+
it('keeps registration on plan selection while the post-auth cloud session is still resolving for a commercially gated user with no workspace', async () => {
|
|
2565
|
+
const user = userEvent.setup();
|
|
2566
|
+
let authenticated = false;
|
|
2567
|
+
let authSessionCallCount = 0;
|
|
2568
|
+
let releaseInitialSession = null;
|
|
2569
|
+
const initialSessionGate = new Promise((resolve) => {
|
|
2570
|
+
releaseInitialSession = resolve;
|
|
2571
|
+
});
|
|
2572
|
+
const fetchMock = vi.fn((input) => {
|
|
2573
|
+
const url = String(input);
|
|
2574
|
+
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
2575
|
+
return jsonResponse({
|
|
2576
|
+
config: {
|
|
2577
|
+
baseUrl: 'https://app.taskforce.example',
|
|
2578
|
+
apiBaseUrl: 'https://app.taskforce.example/api',
|
|
2579
|
+
cloudAuthBaseUrl: 'https://app.taskforce.example',
|
|
2580
|
+
runtimeMode: 'cloud',
|
|
2581
|
+
authSource: 'cloud',
|
|
2582
|
+
workspaceMode: 'multi-cloud',
|
|
2583
|
+
workspaceSwitchingEnabled: true,
|
|
2584
|
+
authRequiredForApi: true
|
|
2585
|
+
}
|
|
2586
|
+
});
|
|
2587
|
+
}
|
|
2588
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
2589
|
+
authSessionCallCount += 1;
|
|
2590
|
+
const payload = () => ({
|
|
2591
|
+
authenticated,
|
|
2592
|
+
runtimeMode: 'cloud',
|
|
2593
|
+
authRequiredForApi: true,
|
|
2594
|
+
userId: authenticated ? 'user-gated-pending' : null,
|
|
2595
|
+
workspaceId: null,
|
|
2596
|
+
betaAccess: true
|
|
2597
|
+
});
|
|
2598
|
+
if (authSessionCallCount === 1) {
|
|
2599
|
+
return initialSessionGate.then(() => jsonResponse(payload()));
|
|
2600
|
+
}
|
|
2601
|
+
return jsonResponse(payload());
|
|
2602
|
+
}
|
|
2603
|
+
if (url.includes('/api/taskforce/config')) {
|
|
2604
|
+
return jsonResponse({
|
|
2605
|
+
...mockConfig,
|
|
2606
|
+
runtimeMode: 'cloud',
|
|
2607
|
+
authRequiredForApi: true,
|
|
2608
|
+
workspaceSwitchingEnabled: true,
|
|
2609
|
+
setupState: {
|
|
2610
|
+
globalSetupState: 'present',
|
|
2611
|
+
workspaceSetupState: 'missing',
|
|
2612
|
+
runtimeMode: 'cloud',
|
|
2613
|
+
workspaceId: 'default',
|
|
2614
|
+
mode: 'core',
|
|
2615
|
+
forceSetup: true,
|
|
2616
|
+
forceGlobalSetup: false,
|
|
2617
|
+
forceWorkspaceSetup: true
|
|
2618
|
+
}
|
|
2619
|
+
});
|
|
2620
|
+
}
|
|
2621
|
+
if (url.includes('/api/taskforce/workspaces')) {
|
|
2622
|
+
return jsonResponse({
|
|
2623
|
+
success: true,
|
|
2624
|
+
workspaces: [],
|
|
2625
|
+
currentWorkspaceId: 'default'
|
|
2626
|
+
});
|
|
2627
|
+
}
|
|
2628
|
+
if (url.includes('/api/taskforce/account/access-status')) {
|
|
2629
|
+
return jsonResponse({
|
|
2630
|
+
gate: 'plan_selection_required',
|
|
2631
|
+
hasEntitlement: true,
|
|
2632
|
+
canAccessApp: false,
|
|
2633
|
+
canAccessPlans: true,
|
|
2634
|
+
message: 'Choose a plan to continue.'
|
|
2635
|
+
});
|
|
2636
|
+
}
|
|
2637
|
+
if (url.includes('/api/taskforce/account/profile-summary')) {
|
|
2638
|
+
return jsonResponse({
|
|
2639
|
+
entitlementState: 'pending_plan_selection',
|
|
2640
|
+
gate: 'plan_selection_required',
|
|
2641
|
+
message: 'Choose a plan to continue.',
|
|
2642
|
+
workspaceId: null
|
|
2643
|
+
});
|
|
2644
|
+
}
|
|
2645
|
+
if (url.includes('/api/taskforce/public/pricing')) {
|
|
2646
|
+
return jsonResponse({ plans: [] });
|
|
2647
|
+
}
|
|
2648
|
+
if (url.includes('/api/taskforce/auth/register')) {
|
|
2649
|
+
authenticated = true;
|
|
2650
|
+
return jsonResponse({
|
|
2651
|
+
success: true,
|
|
2652
|
+
workspaceSetupRequired: true,
|
|
2653
|
+
verificationRequired: false,
|
|
2654
|
+
planSelectionRequired: true,
|
|
2655
|
+
commercialState: 'pending_plan_selection'
|
|
2656
|
+
});
|
|
2657
|
+
}
|
|
2658
|
+
if (url.includes('/api/taskforce/categories'))
|
|
2659
|
+
return jsonResponse(mockCategories);
|
|
2660
|
+
if (url.includes('/api/taskforce/tasks'))
|
|
2661
|
+
return jsonResponse({ tasks: mockTasks });
|
|
2662
|
+
if (url.includes('/api/taskforce/archive'))
|
|
2663
|
+
return jsonResponse({ archived: [] });
|
|
2664
|
+
if (url.includes('/api/taskforce/data-version'))
|
|
2665
|
+
return jsonResponse({ dataVersion: 1 });
|
|
2666
|
+
if (url.includes('/api/taskforce/types'))
|
|
2667
|
+
return jsonResponse({ types: [] });
|
|
2668
|
+
if (url.includes('/api/taskforce/priorities'))
|
|
2669
|
+
return jsonResponse({ priorities: [] });
|
|
2670
|
+
if (url.includes('/api/taskforce/approaches'))
|
|
2671
|
+
return jsonResponse({ approaches: [] });
|
|
2672
|
+
if (url.includes('/api/taskforce/specialists'))
|
|
2673
|
+
return jsonResponse({ specialists: [] });
|
|
2674
|
+
if (url.includes('/api/taskforce/workflow-templates'))
|
|
2675
|
+
return jsonResponse({ templates: [] });
|
|
2676
|
+
return jsonResponse({});
|
|
2677
|
+
});
|
|
2678
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
2679
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
|
|
2680
|
+
await user.type(await screen.findByPlaceholderText('Email'), 'gated-pending@example.com');
|
|
2681
|
+
await user.type(screen.getByPlaceholderText('Display name'), 'Gated Pending');
|
|
2682
|
+
await user.type(screen.getByPlaceholderText('Password'), 'GoodPassphrase123!');
|
|
2683
|
+
await user.type(screen.getByPlaceholderText('Confirm password'), 'GoodPassphrase123!');
|
|
2684
|
+
await user.click(screen.getByRole('button', { name: 'Create Account' }));
|
|
2685
|
+
await act(async () => {
|
|
2686
|
+
await Promise.resolve();
|
|
2687
|
+
});
|
|
2688
|
+
await waitFor(() => {
|
|
2689
|
+
expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
|
|
2690
|
+
});
|
|
2691
|
+
expect(screen.queryByText('Configure your new workspace.')).not.toBeInTheDocument();
|
|
2692
|
+
await act(async () => {
|
|
2693
|
+
releaseInitialSession?.();
|
|
2694
|
+
await Promise.resolve();
|
|
2695
|
+
});
|
|
2696
|
+
await waitFor(() => {
|
|
2697
|
+
expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
|
|
2420
2698
|
});
|
|
2421
2699
|
expect(screen.queryByText('Configure your new workspace.')).not.toBeInTheDocument();
|
|
2422
2700
|
});
|
|
@@ -2493,9 +2771,9 @@ describe('TaskforceCore Integration', () => {
|
|
|
2493
2771
|
}
|
|
2494
2772
|
if (url.includes('/api/taskforce/account/profile-summary')) {
|
|
2495
2773
|
return jsonResponse({
|
|
2496
|
-
planId: '
|
|
2497
|
-
planName: '
|
|
2498
|
-
planVersionId: '
|
|
2774
|
+
planId: 'basic',
|
|
2775
|
+
planName: 'Basic',
|
|
2776
|
+
planVersionId: 'basic-v1',
|
|
2499
2777
|
entitlementState: 'checkout_pending',
|
|
2500
2778
|
gate: 'checkout_pending',
|
|
2501
2779
|
message: 'Complete checkout to continue.',
|
|
@@ -2505,11 +2783,11 @@ describe('TaskforceCore Integration', () => {
|
|
|
2505
2783
|
if (url.includes('/api/taskforce/public/pricing')) {
|
|
2506
2784
|
return jsonResponse({
|
|
2507
2785
|
plans: [{
|
|
2508
|
-
planId: '
|
|
2509
|
-
displayName: '
|
|
2786
|
+
planId: 'basic',
|
|
2787
|
+
displayName: 'Basic',
|
|
2510
2788
|
status: 'active',
|
|
2511
2789
|
defaultVersion: {
|
|
2512
|
-
planVersionId: '
|
|
2790
|
+
planVersionId: 'basic-v1',
|
|
2513
2791
|
versionNumber: 1,
|
|
2514
2792
|
seatLimit: null,
|
|
2515
2793
|
metadata: {}
|
|
@@ -2517,19 +2795,19 @@ describe('TaskforceCore Integration', () => {
|
|
|
2517
2795
|
features: [],
|
|
2518
2796
|
pricing: {
|
|
2519
2797
|
month: {
|
|
2520
|
-
planVersionId: '
|
|
2798
|
+
planVersionId: 'basic-v1',
|
|
2521
2799
|
interval: 'month',
|
|
2522
2800
|
pricingType: 'stripe',
|
|
2523
|
-
stripePriceId: '
|
|
2801
|
+
stripePriceId: 'price_basic_month',
|
|
2524
2802
|
displayAmount: '$19.00',
|
|
2525
2803
|
unitAmount: 1900,
|
|
2526
2804
|
currency: 'usd'
|
|
2527
2805
|
},
|
|
2528
2806
|
year: {
|
|
2529
|
-
planVersionId: '
|
|
2807
|
+
planVersionId: 'basic-v1',
|
|
2530
2808
|
interval: 'year',
|
|
2531
2809
|
pricingType: 'stripe',
|
|
2532
|
-
stripePriceId: '
|
|
2810
|
+
stripePriceId: 'price_basic_year',
|
|
2533
2811
|
displayAmount: '$190.00',
|
|
2534
2812
|
unitAmount: 19000,
|
|
2535
2813
|
currency: 'usd'
|
|
@@ -2584,10 +2862,11 @@ describe('TaskforceCore Integration', () => {
|
|
|
2584
2862
|
return jsonResponse({});
|
|
2585
2863
|
});
|
|
2586
2864
|
vi.stubGlobal('fetch', fetchMock);
|
|
2587
|
-
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&planId=
|
|
2865
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&planId=basic&planVersionId=basic-v1&interval=year&next=%2F'], children: _jsx(TaskforceCore, {}) }));
|
|
2588
2866
|
await user.type(await screen.findByPlaceholderText('Email'), 'checkout@example.com');
|
|
2589
2867
|
await user.type(screen.getByPlaceholderText('Display name'), 'Checkout User');
|
|
2590
|
-
await user.type(screen.getByPlaceholderText('Password'), '
|
|
2868
|
+
await user.type(screen.getByPlaceholderText('Password'), 'GoodPassphrase123!');
|
|
2869
|
+
await user.type(screen.getByPlaceholderText('Confirm password'), 'GoodPassphrase123!');
|
|
2591
2870
|
await user.click(screen.getByRole('button', { name: 'Create Account' }));
|
|
2592
2871
|
await waitFor(() => {
|
|
2593
2872
|
expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
|
|
@@ -2720,7 +2999,8 @@ describe('TaskforceCore Integration', () => {
|
|
|
2720
2999
|
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
|
|
2721
3000
|
await user.type(await screen.findByPlaceholderText('Email'), 'free-user@example.com');
|
|
2722
3001
|
await user.type(screen.getByPlaceholderText('Display name'), 'Free User');
|
|
2723
|
-
await user.type(screen.getByPlaceholderText('Password'), '
|
|
3002
|
+
await user.type(screen.getByPlaceholderText('Password'), 'GoodPassphrase123!');
|
|
3003
|
+
await user.type(screen.getByPlaceholderText('Confirm password'), 'GoodPassphrase123!');
|
|
2724
3004
|
await user.click(screen.getByRole('button', { name: 'Create Account' }));
|
|
2725
3005
|
await act(async () => {
|
|
2726
3006
|
await Promise.resolve();
|
|
@@ -2846,7 +3126,8 @@ describe('TaskforceCore Integration', () => {
|
|
|
2846
3126
|
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
|
|
2847
3127
|
await user.type(await screen.findByPlaceholderText('Email'), 'existing-workspace@example.com');
|
|
2848
3128
|
await user.type(screen.getByPlaceholderText('Display name'), 'Existing Workspace User');
|
|
2849
|
-
await user.type(screen.getByPlaceholderText('Password'), '
|
|
3129
|
+
await user.type(screen.getByPlaceholderText('Password'), 'GoodPassphrase123!');
|
|
3130
|
+
await user.type(screen.getByPlaceholderText('Confirm password'), 'GoodPassphrase123!');
|
|
2850
3131
|
await user.click(screen.getByRole('button', { name: 'Create Account' }));
|
|
2851
3132
|
await act(async () => {
|
|
2852
3133
|
await Promise.resolve();
|
|
@@ -2916,7 +3197,8 @@ describe('TaskforceCore Integration', () => {
|
|
|
2916
3197
|
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2Fplans'], children: _jsx(TaskforceCore, {}) }));
|
|
2917
3198
|
await user.type(await screen.findByPlaceholderText('Email'), 'local@example.com');
|
|
2918
3199
|
await user.type(screen.getByPlaceholderText('Display name'), 'Local User');
|
|
2919
|
-
await user.type(screen.getByPlaceholderText('Password'), '
|
|
3200
|
+
await user.type(screen.getByPlaceholderText('Password'), 'GoodPassphrase123!');
|
|
3201
|
+
await user.type(screen.getByPlaceholderText('Confirm password'), 'GoodPassphrase123!');
|
|
2920
3202
|
await user.click(screen.getByRole('button', { name: 'Create Account' }));
|
|
2921
3203
|
await waitFor(() => {
|
|
2922
3204
|
expect(screen.getByPlaceholderText('Verification token')).toBeInTheDocument();
|
|
@@ -2924,6 +3206,156 @@ describe('TaskforceCore Integration', () => {
|
|
|
2924
3206
|
expect(screen.queryByRole('button', { name: 'Create Account' })).not.toBeInTheDocument();
|
|
2925
3207
|
expect(screen.queryByText('Subscription Plans')).not.toBeInTheDocument();
|
|
2926
3208
|
});
|
|
3209
|
+
it('keeps verified registration on plan selection when commercial onboarding is still blocked', async () => {
|
|
3210
|
+
const user = userEvent.setup();
|
|
3211
|
+
let authenticated = false;
|
|
3212
|
+
const fetchMock = vi.fn((input) => {
|
|
3213
|
+
const url = String(input);
|
|
3214
|
+
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
3215
|
+
return jsonResponse({
|
|
3216
|
+
config: {
|
|
3217
|
+
baseUrl: 'https://app.taskforce.example',
|
|
3218
|
+
apiBaseUrl: 'https://app.taskforce.example/api',
|
|
3219
|
+
cloudAuthBaseUrl: 'https://app.taskforce.example',
|
|
3220
|
+
runtimeMode: 'cloud',
|
|
3221
|
+
authSource: 'cloud',
|
|
3222
|
+
workspaceMode: 'multi-cloud',
|
|
3223
|
+
workspaceSwitchingEnabled: true,
|
|
3224
|
+
authRequiredForApi: true
|
|
3225
|
+
}
|
|
3226
|
+
});
|
|
3227
|
+
}
|
|
3228
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
3229
|
+
return jsonResponse({
|
|
3230
|
+
authenticated,
|
|
3231
|
+
runtimeMode: 'cloud',
|
|
3232
|
+
authRequiredForApi: true,
|
|
3233
|
+
userId: authenticated ? 'user-verify' : null,
|
|
3234
|
+
workspaceId: authenticated ? 'default' : null,
|
|
3235
|
+
betaAccess: true
|
|
3236
|
+
});
|
|
3237
|
+
}
|
|
3238
|
+
if (url.includes('/api/taskforce/config')) {
|
|
3239
|
+
return jsonResponse({
|
|
3240
|
+
...mockConfig,
|
|
3241
|
+
runtimeMode: 'cloud',
|
|
3242
|
+
authRequiredForApi: true,
|
|
3243
|
+
workspaceSwitchingEnabled: true,
|
|
3244
|
+
setupState: {
|
|
3245
|
+
globalSetupState: 'present',
|
|
3246
|
+
workspaceSetupState: authenticated ? 'missing' : 'present',
|
|
3247
|
+
runtimeMode: 'cloud',
|
|
3248
|
+
workspaceId: 'default',
|
|
3249
|
+
mode: 'core',
|
|
3250
|
+
forceSetup: false,
|
|
3251
|
+
forceGlobalSetup: false,
|
|
3252
|
+
forceWorkspaceSetup: false
|
|
3253
|
+
}
|
|
3254
|
+
});
|
|
3255
|
+
}
|
|
3256
|
+
if (url.includes('/api/taskforce/workspaces')) {
|
|
3257
|
+
return jsonResponse({
|
|
3258
|
+
success: true,
|
|
3259
|
+
workspaces: [],
|
|
3260
|
+
currentWorkspaceId: null
|
|
3261
|
+
});
|
|
3262
|
+
}
|
|
3263
|
+
if (url.includes('/api/taskforce/account/access-status')) {
|
|
3264
|
+
return jsonResponse({
|
|
3265
|
+
gate: 'plan_selection_required',
|
|
3266
|
+
hasEntitlement: true,
|
|
3267
|
+
canAccessApp: false,
|
|
3268
|
+
canAccessPlans: true,
|
|
3269
|
+
message: 'Choose a plan to continue.'
|
|
3270
|
+
});
|
|
3271
|
+
}
|
|
3272
|
+
if (url.includes('/api/taskforce/account/profile-summary')) {
|
|
3273
|
+
return jsonResponse({
|
|
3274
|
+
entitlementState: 'pending_plan_selection',
|
|
3275
|
+
gate: 'plan_selection_required',
|
|
3276
|
+
message: 'Choose a plan to continue.',
|
|
3277
|
+
workspaceId: null
|
|
3278
|
+
});
|
|
3279
|
+
}
|
|
3280
|
+
if (url.includes('/api/taskforce/public/pricing')) {
|
|
3281
|
+
return jsonResponse({
|
|
3282
|
+
plans: [{
|
|
3283
|
+
planId: 'free',
|
|
3284
|
+
displayName: 'Free',
|
|
3285
|
+
status: 'active',
|
|
3286
|
+
defaultVersion: {
|
|
3287
|
+
planVersionId: 'free-v1',
|
|
3288
|
+
versionNumber: 1,
|
|
3289
|
+
seatLimit: null,
|
|
3290
|
+
metadata: {}
|
|
3291
|
+
},
|
|
3292
|
+
features: [],
|
|
3293
|
+
pricing: {
|
|
3294
|
+
month: {
|
|
3295
|
+
planVersionId: 'free-v1',
|
|
3296
|
+
interval: 'month',
|
|
3297
|
+
pricingType: 'free',
|
|
3298
|
+
stripePriceId: 'free:free-v1:month',
|
|
3299
|
+
displayAmount: '$0.00',
|
|
3300
|
+
unitAmount: 0,
|
|
3301
|
+
currency: 'usd'
|
|
3302
|
+
},
|
|
3303
|
+
year: null
|
|
3304
|
+
}
|
|
3305
|
+
}]
|
|
3306
|
+
});
|
|
3307
|
+
}
|
|
3308
|
+
if (url.includes('/api/taskforce/auth/register')) {
|
|
3309
|
+
return jsonResponse({
|
|
3310
|
+
success: true,
|
|
3311
|
+
workspaceSetupRequired: true,
|
|
3312
|
+
verificationRequired: true,
|
|
3313
|
+
verificationToken: 'verify-token',
|
|
3314
|
+
emailSent: true,
|
|
3315
|
+
planSelectionRequired: true,
|
|
3316
|
+
commercialState: 'pending_plan_selection'
|
|
3317
|
+
});
|
|
3318
|
+
}
|
|
3319
|
+
if (url.includes('/api/taskforce/auth/verify-email/confirm')) {
|
|
3320
|
+
authenticated = true;
|
|
3321
|
+
return jsonResponse({ success: true });
|
|
3322
|
+
}
|
|
3323
|
+
if (url.includes('/api/taskforce/categories'))
|
|
3324
|
+
return jsonResponse(mockCategories);
|
|
3325
|
+
if (url.includes('/api/taskforce/tasks'))
|
|
3326
|
+
return jsonResponse({ tasks: mockTasks });
|
|
3327
|
+
if (url.includes('/api/taskforce/archive'))
|
|
3328
|
+
return jsonResponse({ archived: [] });
|
|
3329
|
+
if (url.includes('/api/taskforce/data-version'))
|
|
3330
|
+
return jsonResponse({ dataVersion: 1 });
|
|
3331
|
+
if (url.includes('/api/taskforce/types'))
|
|
3332
|
+
return jsonResponse({ types: [] });
|
|
3333
|
+
if (url.includes('/api/taskforce/priorities'))
|
|
3334
|
+
return jsonResponse({ priorities: [] });
|
|
3335
|
+
if (url.includes('/api/taskforce/approaches'))
|
|
3336
|
+
return jsonResponse({ approaches: [] });
|
|
3337
|
+
if (url.includes('/api/taskforce/specialists'))
|
|
3338
|
+
return jsonResponse({ specialists: [] });
|
|
3339
|
+
if (url.includes('/api/taskforce/workflow-templates'))
|
|
3340
|
+
return jsonResponse({ templates: [] });
|
|
3341
|
+
return jsonResponse({});
|
|
3342
|
+
});
|
|
3343
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
3344
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
|
|
3345
|
+
await user.type(await screen.findByPlaceholderText('Email'), 'verify-gated@example.com');
|
|
3346
|
+
await user.type(screen.getByPlaceholderText('Display name'), 'Verify Gated User');
|
|
3347
|
+
await user.type(screen.getByPlaceholderText('Password'), 'GoodPassphrase123!');
|
|
3348
|
+
await user.type(screen.getByPlaceholderText('Confirm password'), 'GoodPassphrase123!');
|
|
3349
|
+
await user.click(screen.getByRole('button', { name: 'Create Account' }));
|
|
3350
|
+
await waitFor(() => {
|
|
3351
|
+
expect(screen.getByPlaceholderText('Verification token')).toHaveValue('verify-token');
|
|
3352
|
+
});
|
|
3353
|
+
await user.click(screen.getByRole('button', { name: 'Verify Email' }));
|
|
3354
|
+
await waitFor(() => {
|
|
3355
|
+
expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
|
|
3356
|
+
});
|
|
3357
|
+
expect(screen.queryByText('Configure your new workspace.')).not.toBeInTheDocument();
|
|
3358
|
+
});
|
|
2927
3359
|
it('allows returning to sign-in/register from forgot mode in local runtime', async () => {
|
|
2928
3360
|
const user = userEvent.setup();
|
|
2929
3361
|
const fetchMock = vi.fn((url) => {
|
|
@@ -2977,6 +3409,162 @@ describe('TaskforceCore Integration', () => {
|
|
|
2977
3409
|
expect(screen.getByRole('button', { name: 'Register' })).toBeInTheDocument();
|
|
2978
3410
|
});
|
|
2979
3411
|
});
|
|
3412
|
+
it('shows neutral password reset copy when reset delivery was not attempted', async () => {
|
|
3413
|
+
const user = userEvent.setup();
|
|
3414
|
+
const fetchMock = vi.fn((input) => {
|
|
3415
|
+
const url = String(input);
|
|
3416
|
+
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
3417
|
+
return Promise.resolve({
|
|
3418
|
+
ok: true,
|
|
3419
|
+
json: () => Promise.resolve({
|
|
3420
|
+
success: true,
|
|
3421
|
+
config: {
|
|
3422
|
+
baseUrl: 'https://app.taskforce.example',
|
|
3423
|
+
apiBaseUrl: 'https://app.taskforce.example/api',
|
|
3424
|
+
cloudAuthBaseUrl: 'https://app.taskforce.example',
|
|
3425
|
+
runtimeMode: 'cloud',
|
|
3426
|
+
authSource: 'cloud',
|
|
3427
|
+
workspaceMode: 'multi-cloud',
|
|
3428
|
+
workspaceSwitchingEnabled: true,
|
|
3429
|
+
cloudAuthViaLocalProxy: false
|
|
3430
|
+
}
|
|
3431
|
+
})
|
|
3432
|
+
});
|
|
3433
|
+
}
|
|
3434
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
3435
|
+
return Promise.resolve({
|
|
3436
|
+
ok: true,
|
|
3437
|
+
json: () => Promise.resolve({
|
|
3438
|
+
authenticated: false,
|
|
3439
|
+
runtimeMode: 'cloud',
|
|
3440
|
+
authRequiredForApi: false
|
|
3441
|
+
})
|
|
3442
|
+
});
|
|
3443
|
+
}
|
|
3444
|
+
if (url.includes('/api/taskforce/config')) {
|
|
3445
|
+
return Promise.resolve({
|
|
3446
|
+
ok: true,
|
|
3447
|
+
json: () => Promise.resolve({
|
|
3448
|
+
...mockConfig,
|
|
3449
|
+
runtimeMode: 'cloud',
|
|
3450
|
+
authRequiredForApi: false,
|
|
3451
|
+
setupState: {
|
|
3452
|
+
globalSetupState: 'present',
|
|
3453
|
+
workspaceSetupState: 'present',
|
|
3454
|
+
runtimeMode: 'cloud',
|
|
3455
|
+
workspaceId: 'workspace-local-active',
|
|
3456
|
+
mode: 'core',
|
|
3457
|
+
forceSetup: false,
|
|
3458
|
+
forceGlobalSetup: false,
|
|
3459
|
+
forceWorkspaceSetup: false
|
|
3460
|
+
}
|
|
3461
|
+
})
|
|
3462
|
+
});
|
|
3463
|
+
}
|
|
3464
|
+
if (url.includes('/api/taskforce/auth/password-reset/request')) {
|
|
3465
|
+
return Promise.resolve({
|
|
3466
|
+
ok: true,
|
|
3467
|
+
json: () => Promise.resolve({
|
|
3468
|
+
success: true,
|
|
3469
|
+
emailSent: false,
|
|
3470
|
+
deliveryAttempted: false,
|
|
3471
|
+
resetToken: null
|
|
3472
|
+
})
|
|
3473
|
+
});
|
|
3474
|
+
}
|
|
3475
|
+
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
3476
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
3477
|
+
}
|
|
3478
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
3479
|
+
});
|
|
3480
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
3481
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=forgot&next=%2F'], children: _jsx(TaskforceCore, {}) }));
|
|
3482
|
+
await user.type(await screen.findByPlaceholderText('Email'), 'nobody@example.com');
|
|
3483
|
+
await user.click(screen.getByRole('button', { name: 'Send Reset Link' }));
|
|
3484
|
+
await waitFor(() => {
|
|
3485
|
+
expect(screen.getByText('If that account exists, we sent password reset instructions.')).toBeInTheDocument();
|
|
3486
|
+
});
|
|
3487
|
+
});
|
|
3488
|
+
it('keeps forgot-password users on the request form when reset delivery fails', async () => {
|
|
3489
|
+
const user = userEvent.setup();
|
|
3490
|
+
const fetchMock = vi.fn((input) => {
|
|
3491
|
+
const url = String(input);
|
|
3492
|
+
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
3493
|
+
return Promise.resolve({
|
|
3494
|
+
ok: true,
|
|
3495
|
+
json: () => Promise.resolve({
|
|
3496
|
+
success: true,
|
|
3497
|
+
config: {
|
|
3498
|
+
baseUrl: 'https://app.taskforce.example',
|
|
3499
|
+
apiBaseUrl: 'https://app.taskforce.example/api',
|
|
3500
|
+
cloudAuthBaseUrl: 'https://app.taskforce.example',
|
|
3501
|
+
runtimeMode: 'cloud',
|
|
3502
|
+
authSource: 'cloud',
|
|
3503
|
+
workspaceMode: 'multi-cloud',
|
|
3504
|
+
workspaceSwitchingEnabled: true,
|
|
3505
|
+
cloudAuthViaLocalProxy: false
|
|
3506
|
+
}
|
|
3507
|
+
})
|
|
3508
|
+
});
|
|
3509
|
+
}
|
|
3510
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
3511
|
+
return Promise.resolve({
|
|
3512
|
+
ok: true,
|
|
3513
|
+
json: () => Promise.resolve({
|
|
3514
|
+
authenticated: false,
|
|
3515
|
+
runtimeMode: 'cloud',
|
|
3516
|
+
authRequiredForApi: false
|
|
3517
|
+
})
|
|
3518
|
+
});
|
|
3519
|
+
}
|
|
3520
|
+
if (url.includes('/api/taskforce/config')) {
|
|
3521
|
+
return Promise.resolve({
|
|
3522
|
+
ok: true,
|
|
3523
|
+
json: () => Promise.resolve({
|
|
3524
|
+
...mockConfig,
|
|
3525
|
+
runtimeMode: 'cloud',
|
|
3526
|
+
authRequiredForApi: false,
|
|
3527
|
+
setupState: {
|
|
3528
|
+
globalSetupState: 'present',
|
|
3529
|
+
workspaceSetupState: 'present',
|
|
3530
|
+
runtimeMode: 'cloud',
|
|
3531
|
+
workspaceId: 'workspace-local-active',
|
|
3532
|
+
mode: 'core',
|
|
3533
|
+
forceSetup: false,
|
|
3534
|
+
forceGlobalSetup: false,
|
|
3535
|
+
forceWorkspaceSetup: false
|
|
3536
|
+
}
|
|
3537
|
+
})
|
|
3538
|
+
});
|
|
3539
|
+
}
|
|
3540
|
+
if (url.includes('/api/taskforce/auth/password-reset/request')) {
|
|
3541
|
+
return Promise.resolve({
|
|
3542
|
+
ok: true,
|
|
3543
|
+
json: () => Promise.resolve({
|
|
3544
|
+
success: true,
|
|
3545
|
+
emailSent: false,
|
|
3546
|
+
deliveryAttempted: true,
|
|
3547
|
+
emailError: 'SMTP temporary failure',
|
|
3548
|
+
resetToken: null
|
|
3549
|
+
})
|
|
3550
|
+
});
|
|
3551
|
+
}
|
|
3552
|
+
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
3553
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
3554
|
+
}
|
|
3555
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
3556
|
+
});
|
|
3557
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
3558
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=forgot&next=%2F'], children: _jsx(TaskforceCore, {}) }));
|
|
3559
|
+
await user.type(await screen.findByPlaceholderText('Email'), 'owner@example.com');
|
|
3560
|
+
await user.click(screen.getByRole('button', { name: 'Send Reset Link' }));
|
|
3561
|
+
await waitFor(() => {
|
|
3562
|
+
expect(screen.getByText('Password reset email failed to send. SMTP temporary failure')).toBeInTheDocument();
|
|
3563
|
+
});
|
|
3564
|
+
expect(screen.getByText('Request a password reset link.')).toBeInTheDocument();
|
|
3565
|
+
expect(screen.queryByText('Enter the password reset token and choose a new password.')).not.toBeInTheDocument();
|
|
3566
|
+
expect(screen.getByRole('button', { name: 'Send Reset Link' })).toBeInTheDocument();
|
|
3567
|
+
});
|
|
2980
3568
|
it('redirects register users to plans when signup plan configuration blocks account creation', async () => {
|
|
2981
3569
|
const user = userEvent.setup();
|
|
2982
3570
|
const fetchMock = vi.fn((input) => {
|
|
@@ -3069,7 +3657,8 @@ describe('TaskforceCore Integration', () => {
|
|
|
3069
3657
|
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
|
|
3070
3658
|
await user.type(await screen.findByPlaceholderText('Email'), 'local@example.com');
|
|
3071
3659
|
await user.type(screen.getByPlaceholderText('Display name'), 'Local User');
|
|
3072
|
-
await user.type(screen.getByPlaceholderText('Password'), '
|
|
3660
|
+
await user.type(screen.getByPlaceholderText('Password'), 'GoodPassphrase123!');
|
|
3661
|
+
await user.type(screen.getByPlaceholderText('Confirm password'), 'GoodPassphrase123!');
|
|
3073
3662
|
await user.click(screen.getByRole('button', { name: 'Create Account' }));
|
|
3074
3663
|
await waitFor(() => {
|
|
3075
3664
|
expect(screen.getByText('Choose a plan to continue.')).toBeInTheDocument();
|
|
@@ -3088,39 +3677,295 @@ describe('TaskforceCore Integration', () => {
|
|
|
3088
3677
|
})
|
|
3089
3678
|
});
|
|
3090
3679
|
}
|
|
3091
|
-
if (url.includes('/api/taskforce/config')) {
|
|
3680
|
+
if (url.includes('/api/taskforce/config')) {
|
|
3681
|
+
return Promise.resolve({
|
|
3682
|
+
ok: true,
|
|
3683
|
+
json: () => Promise.resolve({
|
|
3684
|
+
...mockConfig,
|
|
3685
|
+
runtimeMode: 'cloud',
|
|
3686
|
+
authRequiredForApi: true,
|
|
3687
|
+
setupState: {
|
|
3688
|
+
globalSetupState: 'present',
|
|
3689
|
+
workspaceSetupState: 'present',
|
|
3690
|
+
runtimeMode: 'cloud',
|
|
3691
|
+
workspaceId: 'workspace-1',
|
|
3692
|
+
mode: 'core',
|
|
3693
|
+
forceSetup: false,
|
|
3694
|
+
forceGlobalSetup: false,
|
|
3695
|
+
forceWorkspaceSetup: false
|
|
3696
|
+
}
|
|
3697
|
+
})
|
|
3698
|
+
});
|
|
3699
|
+
}
|
|
3700
|
+
if (url.includes('/api/taskforce/auth/invite/inspect')) {
|
|
3701
|
+
return Promise.resolve({
|
|
3702
|
+
ok: true,
|
|
3703
|
+
json: () => Promise.resolve({
|
|
3704
|
+
success: true,
|
|
3705
|
+
state: 'valid',
|
|
3706
|
+
email: 'existing@example.com',
|
|
3707
|
+
workspaceId: 'workspace-1',
|
|
3708
|
+
inviteeKind: 'existing_user',
|
|
3709
|
+
passwordRequired: false
|
|
3710
|
+
})
|
|
3711
|
+
});
|
|
3712
|
+
}
|
|
3713
|
+
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
3714
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
3715
|
+
}
|
|
3716
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
3717
|
+
});
|
|
3718
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
3719
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=invite&token=invite-token&next=%2F'], children: _jsx(TaskforceCore, {}) }));
|
|
3720
|
+
await waitFor(() => {
|
|
3721
|
+
expect(screen.getByText('Sign in to join this workspace.')).toBeInTheDocument();
|
|
3722
|
+
expect(screen.getByDisplayValue('existing@example.com')).toBeInTheDocument();
|
|
3723
|
+
expect(screen.getByPlaceholderText('Password')).toBeInTheDocument();
|
|
3724
|
+
expect(screen.getByRole('button', { name: 'Sign In to Continue' })).toBeInTheDocument();
|
|
3725
|
+
});
|
|
3726
|
+
});
|
|
3727
|
+
it('uses invite join after invite-mode sign-in even if invite reinspection has not refreshed yet', async () => {
|
|
3728
|
+
let authenticated = false;
|
|
3729
|
+
let inviteInspectCalls = 0;
|
|
3730
|
+
const fetchMock = vi.fn((url) => {
|
|
3731
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
3732
|
+
return Promise.resolve({
|
|
3733
|
+
ok: true,
|
|
3734
|
+
json: () => Promise.resolve({
|
|
3735
|
+
authenticated,
|
|
3736
|
+
runtimeMode: 'cloud',
|
|
3737
|
+
authRequiredForApi: true,
|
|
3738
|
+
userId: authenticated ? 'user-existing' : 'anonymous',
|
|
3739
|
+
workspaceId: authenticated ? 'workspace-home' : 'default',
|
|
3740
|
+
email: authenticated ? 'existing@example.com' : null
|
|
3741
|
+
})
|
|
3742
|
+
});
|
|
3743
|
+
}
|
|
3744
|
+
if (url.includes('/api/taskforce/config')) {
|
|
3745
|
+
return Promise.resolve({
|
|
3746
|
+
ok: true,
|
|
3747
|
+
json: () => Promise.resolve({
|
|
3748
|
+
...mockConfig,
|
|
3749
|
+
runtimeMode: 'cloud',
|
|
3750
|
+
authRequiredForApi: true,
|
|
3751
|
+
setupState: {
|
|
3752
|
+
globalSetupState: 'present',
|
|
3753
|
+
workspaceSetupState: 'present',
|
|
3754
|
+
runtimeMode: 'cloud',
|
|
3755
|
+
workspaceId: authenticated ? 'workspace-home' : 'default',
|
|
3756
|
+
mode: 'core',
|
|
3757
|
+
forceSetup: false,
|
|
3758
|
+
forceGlobalSetup: false,
|
|
3759
|
+
forceWorkspaceSetup: false
|
|
3760
|
+
}
|
|
3761
|
+
})
|
|
3762
|
+
});
|
|
3763
|
+
}
|
|
3764
|
+
if (url.includes('/api/taskforce/auth/invite/inspect')) {
|
|
3765
|
+
inviteInspectCalls += 1;
|
|
3766
|
+
if (authenticated && inviteInspectCalls > 1) {
|
|
3767
|
+
return new Promise(() => undefined);
|
|
3768
|
+
}
|
|
3769
|
+
return Promise.resolve({
|
|
3770
|
+
ok: true,
|
|
3771
|
+
json: () => Promise.resolve({
|
|
3772
|
+
success: true,
|
|
3773
|
+
state: 'valid',
|
|
3774
|
+
email: 'existing@example.com',
|
|
3775
|
+
workspaceId: 'workspace-1',
|
|
3776
|
+
inviteeKind: 'existing_user',
|
|
3777
|
+
passwordRequired: false
|
|
3778
|
+
})
|
|
3779
|
+
});
|
|
3780
|
+
}
|
|
3781
|
+
if (url.includes('/api/taskforce/auth/login')) {
|
|
3782
|
+
authenticated = true;
|
|
3783
|
+
return Promise.resolve({
|
|
3784
|
+
ok: true,
|
|
3785
|
+
json: () => Promise.resolve({ success: true })
|
|
3786
|
+
});
|
|
3787
|
+
}
|
|
3788
|
+
if (url.includes('/api/taskforce/auth/invite/join')) {
|
|
3789
|
+
return Promise.resolve({
|
|
3790
|
+
ok: true,
|
|
3791
|
+
json: () => Promise.resolve({ success: true, workspaceId: 'workspace-1' })
|
|
3792
|
+
});
|
|
3793
|
+
}
|
|
3794
|
+
if (url.includes('/api/taskforce/workspaces')) {
|
|
3795
|
+
return Promise.resolve({
|
|
3796
|
+
ok: true,
|
|
3797
|
+
json: () => Promise.resolve({
|
|
3798
|
+
currentWorkspaceId: authenticated ? 'workspace-home' : null,
|
|
3799
|
+
workspaces: authenticated
|
|
3800
|
+
? [{ id: 'workspace-home', name: 'Home workspace', role: 'owner' }]
|
|
3801
|
+
: []
|
|
3802
|
+
})
|
|
3803
|
+
});
|
|
3804
|
+
}
|
|
3805
|
+
if (url.includes('/api/taskforce/tasks')) {
|
|
3806
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: [], config: {} }) });
|
|
3807
|
+
}
|
|
3808
|
+
if (url.includes('/api/taskforce/account/access-status')) {
|
|
3809
|
+
return Promise.resolve({
|
|
3810
|
+
ok: true,
|
|
3811
|
+
json: () => Promise.resolve({
|
|
3812
|
+
success: true,
|
|
3813
|
+
authenticated,
|
|
3814
|
+
commercialState: 'active',
|
|
3815
|
+
planSelectionRequired: false,
|
|
3816
|
+
checkoutPending: false
|
|
3817
|
+
})
|
|
3818
|
+
});
|
|
3819
|
+
}
|
|
3820
|
+
if (url.includes('/api/taskforce/account/profile-summary')) {
|
|
3821
|
+
return Promise.resolve({
|
|
3822
|
+
ok: true,
|
|
3823
|
+
json: () => Promise.resolve({
|
|
3824
|
+
success: true,
|
|
3825
|
+
currentWorkspaceId: authenticated ? 'workspace-home' : null,
|
|
3826
|
+
workspaces: authenticated ? [{ id: 'workspace-home', name: 'Home workspace' }] : []
|
|
3827
|
+
})
|
|
3828
|
+
});
|
|
3829
|
+
}
|
|
3830
|
+
if (url.includes('/api/taskforce/session/workspace')) {
|
|
3831
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true, workspaceId: 'workspace-1' }) });
|
|
3832
|
+
}
|
|
3833
|
+
if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
|
|
3834
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true, state: null }) });
|
|
3835
|
+
}
|
|
3836
|
+
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
3837
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
3838
|
+
}
|
|
3839
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
3840
|
+
});
|
|
3841
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
3842
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=invite&token=invite-token&next=%2F'], children: _jsx(TaskforceCore, {}) }));
|
|
3843
|
+
await waitFor(() => {
|
|
3844
|
+
expect(screen.getByRole('button', { name: 'Sign In to Continue' })).toBeInTheDocument();
|
|
3845
|
+
});
|
|
3846
|
+
await userEvent.type(screen.getByPlaceholderText('Password'), 'ExistingUserPass123!');
|
|
3847
|
+
await userEvent.click(screen.getByTestId('auth-submit-button'));
|
|
3848
|
+
await waitFor(() => {
|
|
3849
|
+
expect(screen.getByText('Signed in. Review the invite details to continue.')).toBeInTheDocument();
|
|
3850
|
+
expect(screen.getByRole('button', { name: 'Join Workspace' })).toBeInTheDocument();
|
|
3851
|
+
});
|
|
3852
|
+
await userEvent.click(screen.getByTestId('auth-submit-button'));
|
|
3853
|
+
await waitFor(() => {
|
|
3854
|
+
expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('/api/taskforce/auth/invite/join'))).toBe(true);
|
|
3855
|
+
});
|
|
3856
|
+
expect(fetchMock.mock.calls.filter((call) => String(call[0]).includes('/api/taskforce/auth/login'))).toHaveLength(1);
|
|
3857
|
+
});
|
|
3858
|
+
it('keeps existing-user invites recoverable when the wrong account signs in first', async () => {
|
|
3859
|
+
let authenticated = false;
|
|
3860
|
+
let sessionEmail = null;
|
|
3861
|
+
const fetchMock = vi.fn((url, init) => {
|
|
3862
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
3863
|
+
return Promise.resolve({
|
|
3864
|
+
ok: true,
|
|
3865
|
+
json: () => Promise.resolve({
|
|
3866
|
+
authenticated,
|
|
3867
|
+
runtimeMode: 'cloud',
|
|
3868
|
+
authRequiredForApi: true,
|
|
3869
|
+
userId: authenticated ? 'user-other' : 'anonymous',
|
|
3870
|
+
workspaceId: authenticated ? 'workspace-home' : 'default',
|
|
3871
|
+
email: sessionEmail
|
|
3872
|
+
})
|
|
3873
|
+
});
|
|
3874
|
+
}
|
|
3875
|
+
if (url.includes('/api/taskforce/config')) {
|
|
3876
|
+
return Promise.resolve({
|
|
3877
|
+
ok: true,
|
|
3878
|
+
json: () => Promise.resolve({
|
|
3879
|
+
...mockConfig,
|
|
3880
|
+
runtimeMode: 'cloud',
|
|
3881
|
+
authRequiredForApi: true,
|
|
3882
|
+
setupState: {
|
|
3883
|
+
globalSetupState: 'present',
|
|
3884
|
+
workspaceSetupState: 'present',
|
|
3885
|
+
runtimeMode: 'cloud',
|
|
3886
|
+
workspaceId: authenticated ? 'workspace-home' : 'default',
|
|
3887
|
+
mode: 'core',
|
|
3888
|
+
forceSetup: false,
|
|
3889
|
+
forceGlobalSetup: false,
|
|
3890
|
+
forceWorkspaceSetup: false
|
|
3891
|
+
}
|
|
3892
|
+
})
|
|
3893
|
+
});
|
|
3894
|
+
}
|
|
3895
|
+
if (url.includes('/api/taskforce/auth/invite/inspect')) {
|
|
3896
|
+
return Promise.resolve({
|
|
3897
|
+
ok: true,
|
|
3898
|
+
json: () => Promise.resolve({
|
|
3899
|
+
success: true,
|
|
3900
|
+
state: 'valid',
|
|
3901
|
+
email: 'existing@example.com',
|
|
3902
|
+
workspaceId: 'workspace-1',
|
|
3903
|
+
inviteeKind: 'existing_user',
|
|
3904
|
+
passwordRequired: false
|
|
3905
|
+
})
|
|
3906
|
+
});
|
|
3907
|
+
}
|
|
3908
|
+
if (url.includes('/api/taskforce/auth/login')) {
|
|
3909
|
+
const body = typeof init?.body === 'string' ? JSON.parse(init.body) : {};
|
|
3910
|
+
authenticated = true;
|
|
3911
|
+
sessionEmail = String(body?.email || '').trim().toLowerCase() || null;
|
|
3912
|
+
return Promise.resolve({
|
|
3913
|
+
ok: true,
|
|
3914
|
+
json: () => Promise.resolve({ success: true })
|
|
3915
|
+
});
|
|
3916
|
+
}
|
|
3917
|
+
if (url.includes('/api/taskforce/auth/invite/join')) {
|
|
3918
|
+
return Promise.resolve({
|
|
3919
|
+
ok: false,
|
|
3920
|
+
json: () => Promise.resolve({
|
|
3921
|
+
success: false,
|
|
3922
|
+
code: 'INVITE_ACCOUNT_MISMATCH',
|
|
3923
|
+
error: 'Sign in with the invited account to join this workspace.'
|
|
3924
|
+
})
|
|
3925
|
+
});
|
|
3926
|
+
}
|
|
3927
|
+
if (url.includes('/api/taskforce/workspaces')) {
|
|
3928
|
+
return Promise.resolve({
|
|
3929
|
+
ok: true,
|
|
3930
|
+
json: () => Promise.resolve({
|
|
3931
|
+
currentWorkspaceId: authenticated ? 'workspace-home' : null,
|
|
3932
|
+
workspaces: authenticated
|
|
3933
|
+
? [{ id: 'workspace-home', name: 'Home workspace', role: 'owner' }]
|
|
3934
|
+
: []
|
|
3935
|
+
})
|
|
3936
|
+
});
|
|
3937
|
+
}
|
|
3938
|
+
if (url.includes('/api/taskforce/tasks')) {
|
|
3939
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: [], config: {} }) });
|
|
3940
|
+
}
|
|
3941
|
+
if (url.includes('/api/taskforce/account/access-status')) {
|
|
3092
3942
|
return Promise.resolve({
|
|
3093
3943
|
ok: true,
|
|
3094
3944
|
json: () => Promise.resolve({
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
workspaceSetupState: 'present',
|
|
3101
|
-
runtimeMode: 'cloud',
|
|
3102
|
-
workspaceId: 'workspace-1',
|
|
3103
|
-
mode: 'core',
|
|
3104
|
-
forceSetup: false,
|
|
3105
|
-
forceGlobalSetup: false,
|
|
3106
|
-
forceWorkspaceSetup: false
|
|
3107
|
-
}
|
|
3945
|
+
success: true,
|
|
3946
|
+
authenticated,
|
|
3947
|
+
commercialState: 'active',
|
|
3948
|
+
planSelectionRequired: false,
|
|
3949
|
+
checkoutPending: false
|
|
3108
3950
|
})
|
|
3109
3951
|
});
|
|
3110
3952
|
}
|
|
3111
|
-
if (url.includes('/api/taskforce/
|
|
3953
|
+
if (url.includes('/api/taskforce/account/profile-summary')) {
|
|
3112
3954
|
return Promise.resolve({
|
|
3113
3955
|
ok: true,
|
|
3114
3956
|
json: () => Promise.resolve({
|
|
3115
3957
|
success: true,
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
workspaceId: 'workspace-1',
|
|
3119
|
-
inviteeKind: 'existing_user',
|
|
3120
|
-
passwordRequired: false
|
|
3958
|
+
currentWorkspaceId: authenticated ? 'workspace-home' : null,
|
|
3959
|
+
workspaces: authenticated ? [{ id: 'workspace-home', name: 'Home workspace' }] : []
|
|
3121
3960
|
})
|
|
3122
3961
|
});
|
|
3123
3962
|
}
|
|
3963
|
+
if (url.includes('/api/taskforce/session/workspace')) {
|
|
3964
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true, workspaceId: 'workspace-home' }) });
|
|
3965
|
+
}
|
|
3966
|
+
if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
|
|
3967
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true, state: null }) });
|
|
3968
|
+
}
|
|
3124
3969
|
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
3125
3970
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
3126
3971
|
}
|
|
@@ -3129,11 +3974,20 @@ describe('TaskforceCore Integration', () => {
|
|
|
3129
3974
|
vi.stubGlobal('fetch', fetchMock);
|
|
3130
3975
|
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=invite&token=invite-token&next=%2F'], children: _jsx(TaskforceCore, {}) }));
|
|
3131
3976
|
await waitFor(() => {
|
|
3132
|
-
expect(screen.
|
|
3977
|
+
expect(screen.getByRole('button', { name: 'Sign In to Continue' })).toBeInTheDocument();
|
|
3978
|
+
});
|
|
3979
|
+
await userEvent.clear(screen.getByPlaceholderText('Email'));
|
|
3980
|
+
await userEvent.type(screen.getByPlaceholderText('Email'), 'other@example.com');
|
|
3981
|
+
await userEvent.type(screen.getByPlaceholderText('Password'), 'OtherUserPass123!');
|
|
3982
|
+
await userEvent.click(screen.getByTestId('auth-submit-button'));
|
|
3983
|
+
await waitFor(() => {
|
|
3984
|
+
expect(screen.getByText('Sign in with the invited account to join this workspace.')).toBeInTheDocument();
|
|
3133
3985
|
expect(screen.getByDisplayValue('existing@example.com')).toBeInTheDocument();
|
|
3134
3986
|
expect(screen.getByPlaceholderText('Password')).toBeInTheDocument();
|
|
3135
|
-
expect(screen.
|
|
3987
|
+
expect(screen.queryByRole('button', { name: 'Join Workspace' })).not.toBeInTheDocument();
|
|
3136
3988
|
});
|
|
3989
|
+
expect(fetchMock.mock.calls.filter((call) => String(call[0]).includes('/api/taskforce/auth/login'))).toHaveLength(1);
|
|
3990
|
+
expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('/api/taskforce/auth/invite/join'))).toBe(false);
|
|
3137
3991
|
});
|
|
3138
3992
|
it('shows password creation for new-user invites', async () => {
|
|
3139
3993
|
const fetchMock = vi.fn((url) => {
|
|
@@ -3190,6 +4044,7 @@ describe('TaskforceCore Integration', () => {
|
|
|
3190
4044
|
await waitFor(() => {
|
|
3191
4045
|
expect(screen.getByText('Create your account to join this workspace.')).toBeInTheDocument();
|
|
3192
4046
|
expect(screen.getByPlaceholderText('Create password')).toBeInTheDocument();
|
|
4047
|
+
expect(screen.getByPlaceholderText('Confirm password')).toBeInTheDocument();
|
|
3193
4048
|
expect(screen.getByRole('button', { name: 'Create Account and Join' })).toBeInTheDocument();
|
|
3194
4049
|
});
|
|
3195
4050
|
});
|
|
@@ -3887,9 +4742,229 @@ describe('TaskforceCore Integration', () => {
|
|
|
3887
4742
|
expect(workspaceSyncStateBody.stateKey).toBe('workspace-sync');
|
|
3888
4743
|
expect(workspaceSyncStateBody.patch?.enabled).toBe(true);
|
|
3889
4744
|
expect(workspaceSyncStateBody.patch?.phase).toBe('attach-cloud');
|
|
4745
|
+
expect(workspaceSyncStateBody.patch?.setupIntent).toBe('attach-cloud-import');
|
|
3890
4746
|
expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('https://cloud.example.com/api/taskforce/workspaces'))).toBe(true);
|
|
3891
4747
|
expect(screen.queryByTitle(/Account/i)).not.toBeInTheDocument();
|
|
3892
4748
|
});
|
|
4749
|
+
it('keeps attach-cloud import blocking after refresh when setup intent is persisted', async () => {
|
|
4750
|
+
const fetchMock = vi.fn((url) => {
|
|
4751
|
+
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
4752
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'cloud' } }) });
|
|
4753
|
+
}
|
|
4754
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
4755
|
+
return Promise.resolve({
|
|
4756
|
+
ok: true,
|
|
4757
|
+
json: () => Promise.resolve({
|
|
4758
|
+
authenticated: true,
|
|
4759
|
+
runtimeMode: 'local',
|
|
4760
|
+
authRequiredForApi: false
|
|
4761
|
+
})
|
|
4762
|
+
});
|
|
4763
|
+
}
|
|
4764
|
+
if (url.includes('/api/taskforce/config')) {
|
|
4765
|
+
return Promise.resolve({
|
|
4766
|
+
ok: true,
|
|
4767
|
+
json: () => Promise.resolve({
|
|
4768
|
+
...mockConfig,
|
|
4769
|
+
runtimeMode: 'local',
|
|
4770
|
+
authRequiredForApi: false,
|
|
4771
|
+
workspaceId: 'workspace-cloud-123',
|
|
4772
|
+
setupState: {
|
|
4773
|
+
globalSetupState: 'present',
|
|
4774
|
+
workspaceSetupState: 'missing',
|
|
4775
|
+
runtimeMode: 'local',
|
|
4776
|
+
workspaceId: 'workspace-cloud-123',
|
|
4777
|
+
mode: 'core',
|
|
4778
|
+
forceSetup: false,
|
|
4779
|
+
forceGlobalSetup: false,
|
|
4780
|
+
forceWorkspaceSetup: true,
|
|
4781
|
+
workspace: null,
|
|
4782
|
+
suggestedWorkspaceName: 'Cloud Project 123'
|
|
4783
|
+
},
|
|
4784
|
+
runtimeCapabilities: {
|
|
4785
|
+
runtimeMode: 'local',
|
|
4786
|
+
supportsCloudAuth: true,
|
|
4787
|
+
supportsAdminManagedSetupOverrides: false,
|
|
4788
|
+
supportsPackageScriptSetupOverrides: true
|
|
4789
|
+
}
|
|
4790
|
+
})
|
|
4791
|
+
});
|
|
4792
|
+
}
|
|
4793
|
+
if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
|
|
4794
|
+
return Promise.resolve({
|
|
4795
|
+
ok: true,
|
|
4796
|
+
json: () => Promise.resolve({
|
|
4797
|
+
success: true,
|
|
4798
|
+
state: {
|
|
4799
|
+
version: 2,
|
|
4800
|
+
enabled: true,
|
|
4801
|
+
phase: 'attach-cloud',
|
|
4802
|
+
setupIntent: 'attach-cloud-import',
|
|
4803
|
+
pullCursor: null,
|
|
4804
|
+
lastPullAt: null,
|
|
4805
|
+
lastPushAt: null,
|
|
4806
|
+
lastSyncedAt: null
|
|
4807
|
+
}
|
|
4808
|
+
})
|
|
4809
|
+
});
|
|
4810
|
+
}
|
|
4811
|
+
if (url.includes('https://cloud.example.com/api/taskforce/workspaces')) {
|
|
4812
|
+
return Promise.resolve({
|
|
4813
|
+
ok: true,
|
|
4814
|
+
json: () => Promise.resolve({
|
|
4815
|
+
success: true,
|
|
4816
|
+
workspaces: [
|
|
4817
|
+
{
|
|
4818
|
+
id: 'workspace-cloud-123',
|
|
4819
|
+
name: 'Cloud Project 123',
|
|
4820
|
+
description: 'Cloud desc'
|
|
4821
|
+
}
|
|
4822
|
+
]
|
|
4823
|
+
})
|
|
4824
|
+
});
|
|
4825
|
+
}
|
|
4826
|
+
if (url.includes('/api/taskforce/ui-state?key=app')) {
|
|
4827
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true, state: {} }) });
|
|
4828
|
+
}
|
|
4829
|
+
if (url.includes('/api/taskforce/tasks')) {
|
|
4830
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
|
|
4831
|
+
}
|
|
4832
|
+
if (url.includes('/api/taskforce/archive')) {
|
|
4833
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ archived: [] }) });
|
|
4834
|
+
}
|
|
4835
|
+
if (url.includes('/api/taskforce/categories')) {
|
|
4836
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve(mockCategories) });
|
|
4837
|
+
}
|
|
4838
|
+
if (url.includes('/api/taskforce/global-settings')) {
|
|
4839
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
4840
|
+
}
|
|
4841
|
+
if (url.includes('/api/taskforce/data-version')) {
|
|
4842
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ dataVersion: 1 }) });
|
|
4843
|
+
}
|
|
4844
|
+
if (url.includes('/api/taskforce/types'))
|
|
4845
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ types: [] }) });
|
|
4846
|
+
if (url.includes('/api/taskforce/priorities'))
|
|
4847
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ priorities: [] }) });
|
|
4848
|
+
if (url.includes('/api/taskforce/approaches'))
|
|
4849
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ approaches: [] }) });
|
|
4850
|
+
if (url.includes('/api/taskforce/specialists'))
|
|
4851
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
|
|
4852
|
+
if (url.includes('/api/taskforce/workflow-templates'))
|
|
4853
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
4854
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
4855
|
+
});
|
|
4856
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
4857
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
|
|
4858
|
+
await waitFor(() => {
|
|
4859
|
+
expect(screen.getByText('Configure your new workspace.')).toBeInTheDocument();
|
|
4860
|
+
});
|
|
4861
|
+
await waitFor(() => {
|
|
4862
|
+
expect(screen.getByText('Importing "Cloud Project 123" from cloud into local.')).toBeInTheDocument();
|
|
4863
|
+
});
|
|
4864
|
+
});
|
|
4865
|
+
it('clears persisted attach-cloud import intent after the first successful import', async () => {
|
|
4866
|
+
const uiStatePosts = [];
|
|
4867
|
+
const fetchMock = vi.fn((url, init) => {
|
|
4868
|
+
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
4869
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'cloud' } }) });
|
|
4870
|
+
}
|
|
4871
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
4872
|
+
return Promise.resolve({
|
|
4873
|
+
ok: true,
|
|
4874
|
+
json: () => Promise.resolve({
|
|
4875
|
+
authenticated: true,
|
|
4876
|
+
runtimeMode: 'local',
|
|
4877
|
+
authRequiredForApi: false
|
|
4878
|
+
})
|
|
4879
|
+
});
|
|
4880
|
+
}
|
|
4881
|
+
if (url.includes('/api/taskforce/config')) {
|
|
4882
|
+
return Promise.resolve({
|
|
4883
|
+
ok: true,
|
|
4884
|
+
json: () => Promise.resolve({
|
|
4885
|
+
...mockConfig,
|
|
4886
|
+
runtimeMode: 'local',
|
|
4887
|
+
authRequiredForApi: false,
|
|
4888
|
+
workspaceId: 'workspace-cloud-123'
|
|
4889
|
+
})
|
|
4890
|
+
});
|
|
4891
|
+
}
|
|
4892
|
+
if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
|
|
4893
|
+
return Promise.resolve({
|
|
4894
|
+
ok: true,
|
|
4895
|
+
json: () => Promise.resolve({
|
|
4896
|
+
success: true,
|
|
4897
|
+
state: {
|
|
4898
|
+
version: 2,
|
|
4899
|
+
enabled: true,
|
|
4900
|
+
phase: 'active',
|
|
4901
|
+
setupIntent: 'attach-cloud-import',
|
|
4902
|
+
pullCursor: 'cursor-1',
|
|
4903
|
+
lastPullAt: '2026-04-15T12:00:00.000Z',
|
|
4904
|
+
lastPushAt: null,
|
|
4905
|
+
lastSyncedAt: '2026-04-15T12:00:00.000Z'
|
|
4906
|
+
}
|
|
4907
|
+
})
|
|
4908
|
+
});
|
|
4909
|
+
}
|
|
4910
|
+
if (url.includes('/api/taskforce/ui-state') && String(init?.method || '').toUpperCase() === 'POST') {
|
|
4911
|
+
uiStatePosts.push(JSON.parse(String(init?.body || '{}')));
|
|
4912
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
|
|
4913
|
+
}
|
|
4914
|
+
if (url.includes('https://cloud.example.com/api/taskforce/workspaces')) {
|
|
4915
|
+
return Promise.resolve({
|
|
4916
|
+
ok: true,
|
|
4917
|
+
json: () => Promise.resolve({
|
|
4918
|
+
success: true,
|
|
4919
|
+
workspaces: [
|
|
4920
|
+
{
|
|
4921
|
+
id: 'workspace-cloud-123',
|
|
4922
|
+
name: 'Cloud Project 123',
|
|
4923
|
+
description: 'Cloud desc'
|
|
4924
|
+
}
|
|
4925
|
+
]
|
|
4926
|
+
})
|
|
4927
|
+
});
|
|
4928
|
+
}
|
|
4929
|
+
if (url.includes('/api/taskforce/ui-state?key=app')) {
|
|
4930
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true, state: {} }) });
|
|
4931
|
+
}
|
|
4932
|
+
if (url.includes('/api/taskforce/tasks')) {
|
|
4933
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
|
|
4934
|
+
}
|
|
4935
|
+
if (url.includes('/api/taskforce/archive')) {
|
|
4936
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ archived: [] }) });
|
|
4937
|
+
}
|
|
4938
|
+
if (url.includes('/api/taskforce/categories')) {
|
|
4939
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve(mockCategories) });
|
|
4940
|
+
}
|
|
4941
|
+
if (url.includes('/api/taskforce/global-settings')) {
|
|
4942
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
4943
|
+
}
|
|
4944
|
+
if (url.includes('/api/taskforce/data-version')) {
|
|
4945
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ dataVersion: 1 }) });
|
|
4946
|
+
}
|
|
4947
|
+
if (url.includes('/api/taskforce/types'))
|
|
4948
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ types: [] }) });
|
|
4949
|
+
if (url.includes('/api/taskforce/priorities'))
|
|
4950
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ priorities: [] }) });
|
|
4951
|
+
if (url.includes('/api/taskforce/approaches'))
|
|
4952
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ approaches: [] }) });
|
|
4953
|
+
if (url.includes('/api/taskforce/specialists'))
|
|
4954
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
|
|
4955
|
+
if (url.includes('/api/taskforce/workflow-templates'))
|
|
4956
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
4957
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
4958
|
+
});
|
|
4959
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
4960
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/setup?step=workspace&source=cloud'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
|
|
4961
|
+
await waitFor(() => {
|
|
4962
|
+
expect(screen.getByText('Test Project')).toBeInTheDocument();
|
|
4963
|
+
});
|
|
4964
|
+
await waitFor(() => {
|
|
4965
|
+
expect(uiStatePosts.some((entry) => entry?.stateKey === 'workspace-sync' && entry?.patch?.setupIntent === null)).toBe(true);
|
|
4966
|
+
});
|
|
4967
|
+
});
|
|
3893
4968
|
it('shows sign-in and register actions on signed-out cloud workspace setup and disables connect', async () => {
|
|
3894
4969
|
const fetchMock = vi.fn((url) => {
|
|
3895
4970
|
if (url.includes('/api/taskforce/auth/session')) {
|
|
@@ -4214,10 +5289,11 @@ describe('TaskforceCore Integration', () => {
|
|
|
4214
5289
|
expect(screen.getByRole('checkbox', { name: /sync to cloud after setup/i })).toBeInTheDocument();
|
|
4215
5290
|
});
|
|
4216
5291
|
await user.click(screen.getByRole('checkbox', { name: /sync to cloud after setup/i }));
|
|
4217
|
-
const nameInput = screen.getByPlaceholderText('
|
|
5292
|
+
const nameInput = screen.getByPlaceholderText('Workspace name');
|
|
4218
5293
|
await user.clear(nameInput);
|
|
4219
5294
|
await user.type(nameInput, 'Local Project');
|
|
4220
|
-
await user.click(screen.getByRole('button', { name: '
|
|
5295
|
+
await user.click(screen.getByRole('button', { name: 'Continue' }));
|
|
5296
|
+
await user.click(screen.getByRole('button', { name: 'Create Workspace' }));
|
|
4221
5297
|
await waitFor(() => {
|
|
4222
5298
|
expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
|
|
4223
5299
|
});
|
|
@@ -4231,10 +5307,11 @@ describe('TaskforceCore Integration', () => {
|
|
|
4231
5307
|
return body?.stateKey === 'workspace-sync';
|
|
4232
5308
|
});
|
|
4233
5309
|
const workspaceSyncStateBody = JSON.parse(String(workspaceSyncStateCall?.[1]?.body || '{}'));
|
|
4234
|
-
expect(workspaceSyncStateBody.patch?.
|
|
4235
|
-
expect(workspaceSyncStateBody.patch?.
|
|
4236
|
-
|
|
4237
|
-
expect(workspaceSyncStateBody.patch?.
|
|
5310
|
+
expect(workspaceSyncStateBody.patch?.version).toBe(2);
|
|
5311
|
+
expect(workspaceSyncStateBody.patch?.enabled).toBe(true);
|
|
5312
|
+
expect(workspaceSyncStateBody.patch?.phase).toBe('provision-local');
|
|
5313
|
+
expect(workspaceSyncStateBody.patch?.setupIntent).toBeNull();
|
|
5314
|
+
expect(workspaceSyncStateBody.patch?.pullCursor).toBeNull();
|
|
4238
5315
|
});
|
|
4239
5316
|
it('creates a new cloud workspace only after guided setup submit', async () => {
|
|
4240
5317
|
const user = userEvent.setup();
|
|
@@ -4708,6 +5785,15 @@ describe('TaskforceCore Integration', () => {
|
|
|
4708
5785
|
expect(useTaskforceSource).toContain("|| path.startsWith('/api/taskforce/account/')");
|
|
4709
5786
|
expect(useTaskforceSource).toContain("|| path.startsWith('/api/taskforce/billing/')");
|
|
4710
5787
|
});
|
|
5788
|
+
it('keeps plans on a dedicated surface while preserving the faster exit flow', () => {
|
|
5789
|
+
const standaloneLayoutPath = path.join(process.cwd(), 'src/components/views/StandaloneLayout.tsx');
|
|
5790
|
+
const standaloneLayoutSource = fs.readFileSync(standaloneLayoutPath, 'utf8');
|
|
5791
|
+
expect(standaloneLayoutSource).toContain('{!isPlansScreen && <WorkspaceHeaderActions actions={universalFilterActions} />}');
|
|
5792
|
+
expect(standaloneLayoutSource).toContain('{!isPlansScreen && showFilters && workspaceFilterBar}');
|
|
5793
|
+
expect(standaloneLayoutSource).not.toMatch(/\{isPlansScreen \? \([\s\S]*\{workspaceToolRail\}[\s\S]*<PlansPage/);
|
|
5794
|
+
expect(standaloneLayoutSource).toContain('closePlansScreen();');
|
|
5795
|
+
expect(standaloneLayoutSource).toContain('void (async () => {');
|
|
5796
|
+
});
|
|
4711
5797
|
it('uses shared /login flow for local sign in instead of a duplicate modal', async () => {
|
|
4712
5798
|
const standaloneLayoutPath = path.join(process.cwd(), 'src/components/views/StandaloneLayout.tsx');
|
|
4713
5799
|
const standaloneLayoutSource = fs.readFileSync(standaloneLayoutPath, 'utf8');
|
|
@@ -4892,11 +5978,12 @@ describe('TaskforceCore Integration', () => {
|
|
|
4892
5978
|
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
4893
5979
|
return jsonResponse({ templates: [] });
|
|
4894
5980
|
}
|
|
4895
|
-
if (url.includes('/api/taskforce/
|
|
4896
|
-
return jsonResponse(
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
5981
|
+
if (url.includes('/api/taskforce/planning/bootstrap')) {
|
|
5982
|
+
return jsonResponse({
|
|
5983
|
+
initiatives: [],
|
|
5984
|
+
workstreams: [],
|
|
5985
|
+
workstreamTaskSummaries: [],
|
|
5986
|
+
});
|
|
4900
5987
|
}
|
|
4901
5988
|
return jsonResponse({});
|
|
4902
5989
|
});
|
|
@@ -5238,9 +6325,9 @@ describe('TaskforceCore Integration', () => {
|
|
|
5238
6325
|
occurredAt: '2026-03-15T14:28:17.000Z',
|
|
5239
6326
|
eventType: 'apply',
|
|
5240
6327
|
status: 'error',
|
|
5241
|
-
errorMessage: 'Detected
|
|
6328
|
+
errorMessage: 'Detected task reference number mismatch during normal sync.',
|
|
5242
6329
|
details: {
|
|
5243
|
-
|
|
6330
|
+
taskId: 'task-1',
|
|
5244
6331
|
existingReferenceNumber: 12,
|
|
5245
6332
|
incomingReferenceNumber: 18
|
|
5246
6333
|
}
|
|
@@ -6474,6 +7561,7 @@ describe('TaskforceCore Integration', () => {
|
|
|
6474
7561
|
const user = userEvent.setup();
|
|
6475
7562
|
let savedDisplayName = 'Original Name';
|
|
6476
7563
|
let savedAvatarUrl = '';
|
|
7564
|
+
const cloudAuthBaseUrl = 'https://performance-app.taskforcehq.ai';
|
|
6477
7565
|
const fetchMock = vi.fn((input, init) => {
|
|
6478
7566
|
const url = String(input);
|
|
6479
7567
|
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
@@ -6513,6 +7601,9 @@ describe('TaskforceCore Integration', () => {
|
|
|
6513
7601
|
...mockConfig,
|
|
6514
7602
|
runtimeMode: 'local',
|
|
6515
7603
|
authRequiredForApi: false,
|
|
7604
|
+
apiBaseUrl: cloudAuthBaseUrl,
|
|
7605
|
+
cloudAuthBaseUrl,
|
|
7606
|
+
cloudAuthViaLocalProxy: true,
|
|
6516
7607
|
workspaceId: 'workspace-local-active',
|
|
6517
7608
|
setupState: {
|
|
6518
7609
|
globalSetupState: 'present',
|
|
@@ -6548,13 +7639,12 @@ describe('TaskforceCore Integration', () => {
|
|
|
6548
7639
|
return jsonResponse({
|
|
6549
7640
|
success: true,
|
|
6550
7641
|
draftId: 'avatar-draft-1',
|
|
6551
|
-
uploadUrl: 'https://upload.example.com/avatar-draft-1',
|
|
6552
7642
|
relativePath: 'users/user-1/profile/avatar/avatar-draft-1-avatar.png',
|
|
6553
7643
|
path: '/api/taskforce/context/users%2Fuser-1%2Fprofile%2Favatar%2Favatar-draft-1-avatar.png'
|
|
6554
7644
|
});
|
|
6555
7645
|
}
|
|
6556
|
-
if (url.includes('
|
|
6557
|
-
return
|
|
7646
|
+
if (url.includes('/api/taskforce/auth/profile/avatar/upload')) {
|
|
7647
|
+
return jsonResponse({ success: true, draftId: 'avatar-draft-1' });
|
|
6558
7648
|
}
|
|
6559
7649
|
if (url.includes('/api/taskforce/auth/profile/avatar/finalize')) {
|
|
6560
7650
|
return jsonResponse({
|
|
@@ -6595,8 +7685,21 @@ describe('TaskforceCore Integration', () => {
|
|
|
6595
7685
|
const displayNameInput = within(modal).getByPlaceholderText('Display name');
|
|
6596
7686
|
await user.clear(displayNameInput);
|
|
6597
7687
|
await user.type(displayNameInput, 'Updated Name');
|
|
6598
|
-
const
|
|
7688
|
+
const getContextSpy = vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue({
|
|
7689
|
+
drawImage: () => undefined
|
|
7690
|
+
});
|
|
7691
|
+
const toBlobSpy = vi.spyOn(HTMLCanvasElement.prototype, 'toBlob').mockImplementation(function toBlob(callback, type) {
|
|
7692
|
+
callback(new Blob([new Uint8Array([1, 2, 3, 4])], { type: type || 'image/webp' }));
|
|
7693
|
+
});
|
|
7694
|
+
await user.click(within(modal).getByLabelText('Edit profile photo'));
|
|
7695
|
+
const avatarManager = await screen.findByText('Edit Profile Photo');
|
|
7696
|
+
const avatarManagerModal = avatarManager.closest(`.${modalStyles.modal}`);
|
|
7697
|
+
const avatarInput = within(avatarManagerModal).getByLabelText('Choose profile photo');
|
|
6599
7698
|
await user.upload(avatarInput, new File(['avatar'], 'avatar.png', { type: 'image/png' }));
|
|
7699
|
+
await user.click(within(avatarManagerModal).getByRole('button', { name: 'Apply Photo' }));
|
|
7700
|
+
await waitFor(() => {
|
|
7701
|
+
expect(screen.queryByText('Edit Profile Photo')).not.toBeInTheDocument();
|
|
7702
|
+
});
|
|
6600
7703
|
await user.click(within(modal).getByRole('button', { name: 'Save Profile' }));
|
|
6601
7704
|
await waitFor(() => {
|
|
6602
7705
|
const profileCall = fetchMock.mock.calls.find((call) => {
|
|
@@ -6608,6 +7711,17 @@ describe('TaskforceCore Integration', () => {
|
|
|
6608
7711
|
expect(payload.displayName).toBe('Updated Name');
|
|
6609
7712
|
expect(payload.avatarDraftId).toBe('avatar-draft-1');
|
|
6610
7713
|
});
|
|
7714
|
+
expect(fetchMock).toHaveBeenCalledWith('/api/taskforce/auth/profile/avatar/init', expect.objectContaining({ method: 'POST' }));
|
|
7715
|
+
const avatarUploadCall = fetchMock.mock.calls.find((call) => String(call[0]) === '/api/taskforce/auth/profile/avatar/upload');
|
|
7716
|
+
expect(avatarUploadCall).toBeTruthy();
|
|
7717
|
+
expect(avatarUploadCall?.[1]).toEqual(expect.objectContaining({ method: 'POST' }));
|
|
7718
|
+
const avatarUploadHeaders = new Headers(avatarUploadCall?.[1]?.headers);
|
|
7719
|
+
expect(avatarUploadHeaders.get('Content-Type')).toBe('image/webp');
|
|
7720
|
+
expect(avatarUploadHeaders.get('x-taskforce-avatar-draft-id')).toBe('avatar-draft-1');
|
|
7721
|
+
expect(fetchMock).toHaveBeenCalledWith('/api/taskforce/auth/profile/avatar/finalize', expect.objectContaining({ method: 'POST' }));
|
|
7722
|
+
expect(fetchMock.mock.calls.some((call) => String(call[0]).startsWith(`${cloudAuthBaseUrl}/api/taskforce/auth/profile/avatar/`))).toBe(false);
|
|
7723
|
+
getContextSpy.mockRestore();
|
|
7724
|
+
toBlobSpy.mockRestore();
|
|
6611
7725
|
await waitFor(() => {
|
|
6612
7726
|
expect(screen.queryByText('Edit Profile')).not.toBeInTheDocument();
|
|
6613
7727
|
});
|