@taskforcehq/taskforce 0.3.170 → 0.3.301
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 +71 -21
- package/dist/Taskforce.module.css +3394 -5981
- package/dist/TaskforceCore.CategoryFallback.test.js +5 -2
- package/dist/TaskforceCore.filter-persistence.test.js +402 -0
- package/dist/TaskforceCore.js +388 -71
- package/dist/TaskforceCore.routing.test.js +311 -0
- package/dist/TaskforceCore.test.js +3344 -615
- package/dist/TaskforceWidget.js +3 -2
- package/dist/__tests__/localizationCatalogs.test.js +48 -31
- package/dist/__tests__/workflowSimulation.test.js +2 -2
- package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.d.ts +60 -0
- package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.js +324 -0
- package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.test.js +278 -0
- package/dist/annotatedAttachments/payload.d.ts +2 -0
- package/dist/annotatedAttachments/payload.js +54 -0
- package/dist/annotatedAttachments/payload.test.js +67 -0
- package/dist/annotatedAttachments/service.d.ts +49 -0
- package/dist/annotatedAttachments/service.js +121 -0
- package/dist/annotatedAttachments/types.d.ts +86 -0
- package/dist/cli.js +74 -25
- package/dist/compat/deploymentCompat.d.ts +12 -0
- package/dist/compat/deploymentCompat.js +43 -0
- package/dist/compat/workspaceSyncCompat.d.ts +6 -0
- package/dist/compat/workspaceSyncCompat.js +79 -0
- package/dist/compat/workspaceSyncCompat.test.js +87 -0
- package/dist/components/features/AgentsModule.d.ts +5 -0
- package/dist/components/features/AgentsModule.js +131 -0
- package/dist/components/features/AgentsModule.test.js +102 -0
- package/dist/components/features/AnnotatedAttachmentWorkspace.d.ts +24 -0
- package/dist/components/features/AnnotatedAttachmentWorkspace.js +1942 -0
- package/dist/components/features/AnnotatedAttachmentWorkspace.test.js +2831 -0
- package/dist/components/features/DocumentGenerateModal.js +8 -16
- package/dist/components/features/DocumentIndex.d.ts +3 -4
- package/dist/components/features/DocumentIndex.js +22 -10
- package/dist/components/features/DocumentViewer.d.ts +6 -2
- package/dist/components/features/DocumentViewer.js +1000 -18
- package/dist/components/features/DocumentWorkspace.d.ts +40 -2
- package/dist/components/features/DocumentWorkspace.js +96 -16
- package/dist/components/features/DocumentWorkspace.test.js +1202 -0
- package/dist/components/features/InitiativesModule.d.ts +22 -0
- package/dist/components/features/InitiativesModule.js +33 -0
- package/dist/components/features/TaskSettings.d.ts +41 -32
- package/dist/components/features/TaskSettings.js +798 -313
- package/dist/components/features/TaskSettings.test.js +1221 -47
- package/dist/components/features/WorkflowsModule.d.ts +27 -0
- package/dist/components/features/WorkflowsModule.js +21 -0
- package/dist/components/features/documentWorkspaceFilters.d.ts +10 -0
- package/dist/components/features/documentWorkspaceFilters.js +11 -0
- package/dist/components/features/settings/CategoryManager.d.ts +2 -0
- package/dist/components/features/settings/CategoryManager.js +15 -8
- package/dist/components/features/settings/PriorityManager.d.ts +18 -0
- package/dist/components/features/settings/PriorityManager.js +51 -0
- package/dist/components/features/settings/TaxonomyEditor.d.ts +2 -1
- package/dist/components/features/settings/TaxonomyEditor.js +41 -18
- package/dist/components/features/settings/TaxonomyLibraryManager.d.ts +26 -0
- package/dist/components/features/settings/TaxonomyLibraryManager.js +301 -0
- package/dist/components/features/settings/TaxonomyLibraryManager.test.js +141 -0
- package/dist/components/features/settings/TaxonomyManager.d.ts +35 -12
- package/dist/components/features/settings/TaxonomyManager.js +59 -27
- package/dist/components/features/settings/TypeManager.d.ts +11 -0
- package/dist/components/features/settings/TypeManager.js +48 -11
- package/dist/components/features/settings/{WorkflowEditorPrototype.d.ts → WorkflowEditor.d.ts} +2 -2
- package/dist/components/features/settings/{WorkflowEditorPrototype.js → WorkflowEditor.js} +3 -3
- package/dist/components/task/TaskActionHeader.d.ts +12 -2
- package/dist/components/task/TaskActionHeader.js +64 -8
- package/dist/components/task/TaskActionHeader.test.js +73 -0
- package/dist/components/task/TaskCard.d.ts +14 -16
- package/dist/components/task/TaskCard.js +144 -174
- package/dist/components/task/TaskCard.test.js +416 -64
- package/dist/components/task/TaskContextUpload.d.ts +6 -5
- package/dist/components/task/TaskContextUpload.js +199 -64
- package/dist/components/task/TaskContextUpload.test.js +331 -14
- package/dist/components/task/TaskForm.d.ts +23 -29
- package/dist/components/task/TaskForm.js +348 -169
- package/dist/components/task/TaskForm.test.js +419 -23
- package/dist/components/task/TaskKanban.d.ts +20 -21
- package/dist/components/task/TaskKanban.js +176 -316
- package/dist/components/task/TaskKanban.planning-drop.test.js +93 -0
- package/dist/components/task/TaskList.d.ts +22 -11
- package/dist/components/task/TaskList.js +98 -12
- package/dist/components/task/TaskList.test.js +206 -7
- package/dist/components/task/TaskReferenceBadge.d.ts +12 -0
- package/dist/components/task/TaskReferenceBadge.js +5 -0
- package/dist/components/task/TaskStatusActions.d.ts +12 -0
- package/dist/components/task/TaskStatusActions.js +18 -0
- package/dist/components/task/TaskStatusActions.test.js +45 -0
- package/dist/components/ui/ImageReferenceBadge.d.ts +12 -0
- package/dist/components/ui/ImageReferenceBadge.js +5 -0
- package/dist/components/ui/LevelSelector.d.ts +2 -1
- package/dist/components/ui/LevelSelector.js +2 -2
- package/dist/components/ui/Markdown.d.ts +1 -2
- package/dist/components/ui/Markdown.js +2 -13
- package/dist/components/ui/Modal.d.ts +3 -2
- package/dist/components/ui/Modal.js +4 -3
- package/dist/components/ui/MultiSelectFilter.d.ts +3 -1
- package/dist/components/ui/MultiSelectFilter.js +2 -2
- package/dist/components/ui/ReferenceBadgeButton.d.ts +13 -0
- package/dist/components/ui/ReferenceBadgeButton.js +6 -0
- package/dist/components/ui/TaxonomyDropdown.d.ts +7 -1
- package/dist/components/ui/TaxonomyDropdown.js +58 -23
- package/dist/components/ui/TopNoticeLayer.d.ts +7 -0
- package/dist/components/ui/TopNoticeLayer.js +14 -0
- package/dist/components/ui/TopNoticeLayer.test.js +25 -0
- package/dist/components/views/AppShellHeader.d.ts +13 -0
- package/dist/components/views/AppShellHeader.js +19 -0
- package/dist/components/views/AppShellHeader.test.js +21 -0
- package/dist/components/views/PlanComparisonPage.d.ts +73 -0
- package/dist/components/views/PlanComparisonPage.js +196 -0
- package/dist/components/views/PlanComparisonPage.test.js +251 -0
- package/dist/components/views/PlansPage.d.ts +14 -0
- package/dist/components/views/PlansPage.js +355 -0
- package/dist/components/views/StandaloneLayout.d.ts +2 -2
- package/dist/components/views/StandaloneLayout.js +2227 -1188
- package/dist/components/views/WidgetView.d.ts +2 -2
- package/dist/components/views/WidgetView.js +149 -43
- package/dist/components/views/chrome/WorkspaceChromeSections.d.ts +57 -0
- package/dist/components/views/chrome/WorkspaceChromeSections.js +38 -0
- package/dist/components/views/chrome/WorkspaceChromeSections.test.js +54 -0
- package/dist/components/views/chrome/workspaceChrome.d.ts +31 -0
- package/dist/components/views/chrome/workspaceChrome.js +108 -0
- package/dist/components/views/chrome/workspaceChrome.test.js +52 -0
- package/dist/components/views/panels/DocumentFilterToolbar.d.ts +10 -0
- package/dist/components/views/panels/DocumentFilterToolbar.js +7 -0
- package/dist/components/views/panels/DocumentFilterToolbar.test.js +19 -0
- package/dist/components/views/panels/FilterToolbar.d.ts +68 -0
- package/dist/components/views/panels/FilterToolbar.js +21 -0
- package/dist/components/views/panels/FilterToolbar.test.d.ts +1 -0
- package/dist/components/views/panels/FilterToolbar.test.js +45 -0
- package/dist/components/views/panels/FilterToolbarControls.d.ts +47 -0
- package/dist/components/views/panels/FilterToolbarControls.js +61 -0
- package/dist/components/views/panels/PlanningDrawer.d.ts +115 -0
- package/dist/components/views/panels/PlanningDrawer.js +220 -0
- package/dist/components/views/panels/ScheduleSidebar.d.ts +52 -0
- package/dist/components/views/panels/ScheduleSidebar.js +127 -0
- package/dist/components/views/panels/scheduleUtils.d.ts +14 -0
- package/dist/components/views/panels/scheduleUtils.js +79 -0
- package/dist/components/views/teamManagementAccess.d.ts +6 -0
- package/dist/components/views/teamManagementAccess.js +18 -0
- package/dist/components/views/teamManagementAccess.test.d.ts +1 -0
- package/dist/components/views/teamManagementAccess.test.js +44 -0
- package/dist/config/clientDeployment.d.ts +2 -0
- package/dist/config/clientDeployment.js +13 -0
- package/dist/config/cloudEnvironment.d.ts +15 -0
- package/dist/config/cloudEnvironment.js +60 -0
- package/dist/config/deployment.d.ts +20 -0
- package/dist/config/deployment.js +67 -0
- package/dist/config/deployment.test.d.ts +1 -0
- package/dist/config/deployment.test.js +88 -0
- package/dist/config/envSchema.js +36 -8
- package/dist/config/envSchema.test.js +28 -0
- package/dist/core/AiProfileService.d.ts +66 -0
- package/dist/core/AiProfileService.js +320 -0
- package/dist/core/AiProfileService.test.d.ts +1 -0
- package/dist/core/AiProfileService.test.js +164 -0
- package/dist/core/AiProfiles.test.d.ts +1 -0
- package/dist/core/AiProfiles.test.js +449 -0
- package/dist/core/AttachmentLinkService.d.ts +15 -0
- package/dist/core/AttachmentLinkService.js +157 -0
- package/dist/core/AuthTokenService.d.ts +44 -0
- package/dist/core/AuthTokenService.js +207 -0
- package/dist/core/CreatedBy.test.js +134 -6
- package/dist/core/DeletedTaskLifecycleService.d.ts +33 -0
- package/dist/core/DeletedTaskLifecycleService.js +158 -0
- package/dist/core/DeletedTaskTrash.test.d.ts +1 -0
- package/dist/core/DeletedTaskTrash.test.js +104 -0
- package/dist/core/EntitlementsPolicy.test.js +78 -5
- package/dist/core/GlobalSettingsService.d.ts +16 -0
- package/dist/core/GlobalSettingsService.js +370 -0
- package/dist/core/JsonBackupRuntime.test.d.ts +1 -0
- package/dist/core/JsonBackupRuntime.test.js +155 -0
- package/dist/core/McpTokenService.d.ts +118 -0
- package/dist/core/McpTokenService.js +921 -0
- package/dist/core/PlanEntitlementService.d.ts +201 -0
- package/dist/core/PlanEntitlementService.js +1442 -0
- package/dist/core/PlanVersionPolicy.test.d.ts +1 -0
- package/dist/core/PlanVersionPolicy.test.js +178 -0
- package/dist/core/PlanningEntities.test.d.ts +1 -0
- package/dist/core/PlanningEntities.test.js +95 -0
- package/dist/core/ScheduleFields.test.js +15 -19
- package/dist/core/SignupMonetizationSettings.test.d.ts +1 -0
- package/dist/core/SignupMonetizationSettings.test.js +159 -0
- package/dist/core/Stats.test.d.ts +1 -0
- package/dist/core/Stats.test.js +36 -0
- package/dist/core/SyncReconciliationService.d.ts +93 -0
- package/dist/core/SyncReconciliationService.js +406 -0
- package/dist/core/SystemAdmin.test.js +578 -13
- package/dist/core/TaskActivityService.d.ts +21 -0
- package/dist/core/TaskActivityService.js +130 -0
- package/dist/core/TaskAttachmentsCanonical.test.d.ts +1 -0
- package/dist/core/TaskAttachmentsCanonical.test.js +306 -0
- package/dist/core/TaskAuditTimeline.test.d.ts +1 -0
- package/dist/core/TaskAuditTimeline.test.js +132 -0
- package/dist/core/TaskTaxonomyValidation.test.js +338 -15
- package/dist/core/Taskforce.d.ts +930 -183
- package/dist/core/Taskforce.ids.test.js +252 -0
- package/dist/core/Taskforce.js +3848 -2532
- package/dist/core/Taskforce.mcpAuth.test.d.ts +1 -0
- package/dist/core/Taskforce.mcpAuth.test.js +343 -0
- package/dist/core/Taskforce.realtimeMutation.test.js +57 -2
- package/dist/core/TaxonomySettingsService.d.ts +18 -0
- package/dist/core/TaxonomySettingsService.js +162 -0
- package/dist/core/WorkspaceDelete.test.js +32 -13
- package/dist/core/WorkspaceLifecycleService.d.ts +11 -0
- package/dist/core/WorkspaceLifecycleService.js +88 -0
- package/dist/core/WorkspacePermissions.test.js +95 -0
- package/dist/core/WorkspacePlanMode.test.js +114 -1
- package/dist/core/WorkspacePreferenceService.d.ts +41 -0
- package/dist/core/WorkspacePreferenceService.js +246 -0
- package/dist/core/WorkspaceSyncSettings.test.d.ts +1 -0
- package/dist/core/WorkspaceSyncSettings.test.js +65 -0
- package/dist/core/__tests__/TaskAttachmentsCanonical.test.d.ts +1 -0
- package/dist/core/__tests__/TaskAttachmentsCanonical.test.js +212 -0
- package/dist/core/shared.d.ts +36 -0
- package/dist/core/shared.js +128 -0
- package/dist/core/types.d.ts +614 -0
- package/dist/core/types.js +27 -0
- package/dist/documentReviews/DocumentReviewCommentStore.d.ts +56 -0
- package/dist/documentReviews/DocumentReviewCommentStore.js +230 -0
- package/dist/documentReviews/DocumentReviewSessionStore.d.ts +52 -0
- package/dist/documentReviews/DocumentReviewSessionStore.js +193 -0
- package/dist/documentReviews/DocumentReviewSessionStore.test.d.ts +1 -0
- package/dist/documentReviews/DocumentReviewSessionStore.test.js +127 -0
- package/dist/documentReviews/service.d.ts +68 -0
- package/dist/documentReviews/service.js +210 -0
- package/dist/documentReviews/types.d.ts +58 -0
- package/dist/documentReviews/types.js +1 -0
- package/dist/hooks/auth/useAuthGuardPolicy.d.ts +2 -0
- package/dist/hooks/auth/useAuthGuardPolicy.js +1 -0
- package/dist/hooks/tasks/useCurrentTaskRelations.d.ts +15 -0
- package/dist/hooks/tasks/useCurrentTaskRelations.js +37 -0
- package/dist/hooks/tasks/useTaskActions.d.ts +3 -0
- package/dist/hooks/tasks/useTaskActions.js +2 -0
- package/dist/hooks/tasks/useTaskEditorNavigation.d.ts +65 -0
- package/dist/hooks/tasks/useTaskEditorNavigation.js +235 -0
- package/dist/hooks/tasks/useTaskFilters.d.ts +2 -0
- package/dist/hooks/tasks/useTaskFilters.js +2 -0
- package/dist/hooks/tasks/useTaskFormActions.d.ts +58 -0
- package/dist/hooks/tasks/useTaskFormActions.js +365 -0
- package/dist/hooks/tasks/useTaskRelationships.d.ts +16 -0
- package/dist/hooks/tasks/useTaskRelationships.js +64 -0
- package/dist/hooks/ui/useResourceExport.d.ts +19 -0
- package/dist/hooks/ui/useResourceExport.js +84 -0
- package/dist/hooks/ui/useSettingsModel.d.ts +203 -0
- package/dist/hooks/ui/useSettingsModel.js +125 -0
- package/dist/hooks/ui/useSettingsPersistence.d.ts +36 -0
- package/dist/hooks/ui/useSettingsPersistence.js +254 -0
- package/dist/hooks/ui/useSettingsTaxonomyManagement.d.ts +119 -0
- package/dist/hooks/ui/useSettingsTaxonomyManagement.js +536 -0
- package/dist/hooks/ui/useUiNotice.d.ts +1 -0
- package/dist/hooks/ui/useUiNotice.js +1 -0
- package/dist/hooks/ui/useWorkflowTemplates.d.ts +37 -0
- package/dist/hooks/ui/useWorkflowTemplates.js +111 -0
- package/dist/hooks/ui/useZenMode.d.ts +5 -0
- package/dist/hooks/ui/useZenMode.js +32 -0
- package/dist/hooks/useFilterSort.d.ts +60 -0
- package/dist/hooks/useFilterSort.js +362 -0
- package/dist/hooks/useFilterSort.test.d.ts +1 -0
- package/dist/hooks/useFilterSort.test.js +333 -0
- package/dist/hooks/useRealtimeSync.d.ts +1 -0
- package/dist/hooks/useRealtimeSync.js +24 -9
- package/dist/hooks/useRealtimeSync.test.js +28 -1
- package/dist/hooks/useSyncOrchestrator.aiProfiles.test.d.ts +1 -0
- package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +34 -0
- package/dist/hooks/useSyncOrchestrator.d.ts +65 -103
- package/dist/hooks/useSyncOrchestrator.js +1538 -670
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +700 -174
- package/dist/hooks/useTaskData.d.ts +35 -0
- package/dist/hooks/useTaskData.js +243 -0
- package/dist/hooks/useTaskData.test.d.ts +1 -0
- package/dist/hooks/useTaskData.test.js +180 -0
- package/dist/hooks/useTaskMutations.d.ts +46 -0
- package/dist/hooks/useTaskMutations.js +472 -0
- package/dist/hooks/useTaskMutations.test.d.ts +1 -0
- package/dist/hooks/useTaskMutations.test.js +80 -0
- package/dist/hooks/useTaskforce.contract.test.d.ts +1 -0
- package/dist/hooks/useTaskforce.contract.test.js +7 -0
- package/dist/hooks/useTaskforce.d.ts +260 -295
- package/dist/hooks/useTaskforce.js +1954 -2623
- package/dist/hooks/useTaskforce.priority-filters.test.js +6 -11
- package/dist/hooks/useTaskforce.sync-behavior.test.d.ts +1 -0
- package/dist/hooks/useTaskforce.sync-behavior.test.js +1120 -0
- package/dist/hooks/useUiNotice.d.ts +16 -0
- package/dist/hooks/useUiNotice.js +49 -0
- package/dist/hooks/useWorkspaceSyncController.d.ts +23 -46
- package/dist/hooks/useWorkspaceSyncController.js +280 -461
- package/dist/hooks/useWorkspaceSyncController.test.js +106 -199
- package/dist/hooks/workspace/useTaskforceApiRouting.d.ts +8 -0
- package/dist/hooks/workspace/useTaskforceApiRouting.js +91 -0
- package/dist/hooks/workspace/useTaskforceApiRouting.test.d.ts +1 -0
- package/dist/hooks/workspace/useTaskforceApiRouting.test.js +60 -0
- package/dist/hooks/workspace/useWorkspaceSyncController.d.ts +1 -0
- package/dist/hooks/workspace/useWorkspaceSyncController.js +1 -0
- package/dist/hooks/workspace/workspaceLocalState.d.ts +30 -0
- package/dist/hooks/workspace/workspaceLocalState.js +101 -0
- package/dist/localization/locales/en-US.d.ts +1 -8
- package/dist/localization/locales/en-US.js +4 -11
- package/dist/localization/locales/es-419.js +2 -9
- package/dist/localization/locales/pt-BR.js +2 -9
- package/dist/localization/localizationCatalogs.test.d.ts +1 -0
- package/dist/localization/localizationCatalogs.test.js +37 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +9 -15
- package/dist/mcp/clientIntegrations.d.ts +33 -0
- package/dist/mcp/clientIntegrations.js +199 -0
- package/dist/mcp/clientIntegrations.test.d.ts +1 -0
- package/dist/mcp/clientIntegrations.test.js +110 -0
- package/dist/mcp/index.d.ts +2 -0
- package/dist/mcp/index.js +2 -0
- package/dist/mcp/optionResolution.test.js +6 -2
- package/dist/mcp/runtime.d.ts +120 -0
- package/dist/mcp/runtime.js +5668 -0
- package/dist/mcp/runtime.test.d.ts +1 -0
- package/dist/mcp/runtime.test.js +3299 -0
- package/dist/mcp/server.js +2 -1793
- package/dist/mcp/structuredCommentValidation.test.d.ts +1 -0
- package/dist/{__tests__ → mcp}/structuredCommentValidation.test.js +1 -1
- package/dist/mcp/structuredDocValidation.js +1 -2
- package/dist/mcp/structuredDocValidation.test.d.ts +1 -0
- package/dist/{__tests__ → mcp}/structuredDocValidation.test.js +9 -4
- package/dist/mcp/toolRegistry.d.ts +570 -0
- package/dist/mcp/toolRegistry.js +90 -0
- package/dist/mcp/toolRegistry.test.d.ts +1 -0
- package/dist/mcp/toolRegistry.test.js +30 -0
- package/dist/migrations/billingSchemaParity.test.js +111 -0
- package/dist/migrations/taskSchemaMigrations.d.ts +5 -0
- package/dist/migrations/taskSchemaMigrations.js +681 -39
- package/dist/migrations/taskSchemaMigrations.test.d.ts +1 -0
- package/dist/migrations/taskSchemaMigrations.test.js +210 -0
- package/dist/plugins/vite.js +13 -7
- package/dist/public/apple-touch-icon.png +0 -0
- package/dist/public/archive/phoenix/apple-touch-icon.png +0 -0
- package/dist/public/archive/phoenix/favicon.ico +0 -0
- package/dist/public/archive/phoenix/site.webmanifest +1 -0
- package/dist/public/favicon-96x96.png +0 -0
- package/dist/public/favicon.ico +0 -0
- package/dist/public/favicon.svg +17 -0
- package/dist/public/site.webmanifest +21 -1
- package/dist/public/web-app-manifest-192x192.png +0 -0
- package/dist/public/web-app-manifest-512x512.png +0 -0
- package/dist/releaseVersion.test.d.ts +1 -0
- package/dist/{__tests__/releaseVersion.test.js → releaseVersion.test.js} +1 -1
- package/dist/resources/templates/WORKFLOW_V2_POLICY.md +1 -1
- package/dist/resources/templates/workflow-sources/workflowDocs.mjs +62 -52
- package/dist/resources/templates/workflows/collaborate.yaml +11 -5
- package/dist/resources/templates/workflows/do.yaml +8 -2
- package/dist/resources/templates/workflows/evaluate.yaml +13 -7
- package/dist/resources/templates/workflows/execute.yaml +40 -23
- package/dist/resources/templates/workflows/plan.yaml +15 -8
- package/dist/resources/templates/workflows/review.yaml +14 -8
- package/dist/runtime/appGateDefinitions.d.ts +32 -0
- package/dist/runtime/appGateDefinitions.js +32 -0
- package/dist/runtime/appGates.d.ts +29 -0
- package/dist/runtime/appGates.js +53 -0
- package/dist/runtime/appGates.test.d.ts +1 -0
- package/dist/runtime/appGates.test.js +42 -0
- package/dist/runtime/migrationFlags.d.ts +1 -0
- package/dist/runtime/migrationFlags.js +1 -0
- package/dist/server/annotatedAttachmentsRoutes.test.d.ts +1 -0
- package/dist/server/annotatedAttachmentsRoutes.test.js +473 -0
- package/dist/server/auth.js +14 -2
- package/dist/server/auth.test.js +21 -1
- package/dist/server/cors.js +11 -5
- package/dist/server/cors.test.js +6 -0
- package/dist/server/documentReviewRoutes.test.d.ts +1 -0
- package/dist/server/documentReviewRoutes.test.js +157 -0
- package/dist/server/index.cookieProxy.test.js +46 -1
- package/dist/server/index.d.ts +20 -0
- package/dist/server/index.js +168 -41
- package/dist/server/index.rateLimit.test.js +6 -0
- package/dist/server/index.test.js +133 -2
- package/dist/server/mockStripe.d.ts +72 -0
- package/dist/server/mockStripe.js +357 -0
- package/dist/server/realtimeSyncWs.js +4 -0
- package/dist/server/routes/admin.d.ts +114 -0
- package/dist/server/routes/admin.js +2226 -0
- package/dist/server/routes/annotatedAttachments.d.ts +24 -0
- package/dist/server/routes/annotatedAttachments.js +235 -0
- package/dist/server/routes/auth.d.ts +61 -0
- package/dist/server/routes/auth.js +1296 -0
- package/dist/server/{routes.billing.d.ts → routes/billing.d.ts} +3 -2
- package/dist/server/routes/billing.js +2001 -0
- package/dist/server/routes/billing.test.d.ts +1 -0
- package/dist/server/routes/billing.test.js +1631 -0
- package/dist/server/routes/documentReviews.d.ts +11 -0
- package/dist/server/routes/documentReviews.js +191 -0
- package/dist/server/routes/documents.d.ts +118 -0
- package/dist/server/routes/documents.js +1113 -0
- package/dist/server/routes/resources.d.ts +25 -0
- package/dist/server/routes/resources.js +225 -0
- package/dist/server/routes/shared.d.ts +35 -0
- package/dist/server/routes/shared.js +305 -0
- package/dist/server/routes/shared.test.d.ts +1 -0
- package/dist/server/routes/shared.test.js +32 -0
- package/dist/server/{routes.sync.d.ts → routes/sync.d.ts} +5 -5
- package/dist/server/routes/sync.integration.test.d.ts +1 -0
- package/dist/server/routes/sync.integration.test.js +2003 -0
- package/dist/server/routes/sync.js +3143 -0
- package/dist/server/routes/tasks.d.ts +72 -0
- package/dist/server/routes/tasks.js +1252 -0
- package/dist/server/routes/workspaces.d.ts +32 -0
- package/dist/server/routes/workspaces.js +307 -0
- package/dist/server/routes.d.ts +8 -9
- package/dist/server/routes.js +968 -4092
- package/dist/server/routes.test.js +9043 -2415
- package/dist/server/runtimeMode.d.ts +1 -1
- package/dist/server/runtimeMode.js +3 -18
- package/dist/server/runtimeMode.test.js +3 -3
- package/dist/server/securityHeaders.d.ts +1 -1
- package/dist/server/securityHeaders.js +65 -6
- package/dist/server/securityHeaders.test.js +40 -2
- package/dist/services/templateEngine.test.d.ts +1 -0
- package/dist/{__tests__ → services}/templateEngine.test.js +1 -1
- package/dist/services/templates.test.d.ts +1 -0
- package/dist/{__tests__ → services}/templates.test.js +1 -1
- package/dist/services/workflowDocumentFixtures.test.d.ts +1 -0
- package/dist/{__tests__ → services}/workflowDocumentFixtures.test.js +4 -4
- package/dist/services/workflowExportMatrix.test.d.ts +1 -0
- package/dist/{__tests__ → services}/workflowExportMatrix.test.js +1 -1
- package/dist/services/workflowExportSnapshots.test.d.ts +1 -0
- package/dist/services/workflowGeneration.test.d.ts +1 -0
- package/dist/services/workflowSimulation.test.d.ts +1 -0
- package/dist/services/workflowSimulation.test.js +113 -0
- package/dist/services/workflowV2Policy.test.d.ts +1 -0
- package/dist/{__tests__ → services}/workflowV2Policy.test.js +2 -2
- package/dist/shared/aiProfileSurfaceType.d.ts +9 -0
- package/dist/shared/aiProfileSurfaceType.js +49 -0
- package/dist/shared/customTaxonomyLibrary.d.ts +11 -0
- package/dist/shared/customTaxonomyLibrary.js +276 -0
- package/dist/shared/customTaxonomyLibrary.test.d.ts +1 -0
- package/dist/shared/customTaxonomyLibrary.test.js +20 -0
- package/dist/shared/taxonomyCatalog.d.ts +19 -0
- package/dist/shared/taxonomyCatalog.js +36 -0
- package/dist/shared/taxonomyDefaults.d.ts +29 -0
- package/dist/shared/taxonomyDefaults.js +26 -0
- package/dist/shared/taxonomyLibrary.d.ts +15 -0
- package/dist/shared/taxonomyLibrary.js +98 -0
- package/dist/shared/taxonomyLibrary.test.d.ts +1 -0
- package/dist/shared/taxonomyLibrary.test.js +26 -0
- package/dist/shared/taxonomyState.d.ts +36 -0
- package/dist/shared/taxonomyState.js +73 -0
- package/dist/shared/taxonomyState.test.d.ts +1 -0
- package/dist/shared/taxonomyState.test.js +71 -0
- package/dist/shared/workflowDefaults.d.ts +9 -0
- package/dist/shared/workflowDefaults.js +11 -0
- package/dist/storage/documentIntegrity.d.ts +2 -0
- package/dist/storage/documentIntegrity.js +34 -17
- package/dist/storage/documentPurge.d.ts +2 -1
- package/dist/storage/documentPurge.js +35 -8
- package/dist/storage/index.d.ts +4 -0
- package/dist/storage/index.js +2 -0
- package/dist/storage/objectStorage.d.ts +1 -1
- package/dist/storage/objectStorageClient.d.ts +6 -4
- package/dist/storage/objectStorageClient.js +77 -29
- package/dist/storage/objectStorageClient.test.d.ts +1 -0
- package/dist/storage/objectStorageClient.test.js +57 -0
- package/dist/storage/objectStorageConfig.js +3 -1
- package/dist/storage/objectStorageConfig.test.d.ts +1 -0
- package/dist/storage/objectStorageConfig.test.js +38 -0
- package/dist/storage/postgresAdapter.d.ts +5 -0
- package/dist/storage/postgresAdapter.js +2 -2
- package/dist/storage/postgresAdapter.test.d.ts +1 -0
- package/dist/storage/postgresAdapter.test.js +29 -0
- package/dist/storage/taskAssetStore.d.ts +7 -4
- package/dist/storage/taskAssetStore.js +65 -60
- package/dist/storage/workspaceAssetStore.d.ts +122 -0
- package/dist/storage/workspaceAssetStore.js +496 -0
- package/dist/storage/workspaceAssetStore.test.d.ts +1 -0
- package/dist/storage/workspaceAssetStore.test.js +501 -0
- package/dist/sync/cloudSyncApi.d.ts +33 -2
- package/dist/sync/cloudSyncApi.js +37 -15
- package/dist/sync/cloudSyncApi.test.js +60 -3
- package/dist/sync/index.d.ts +2 -0
- package/dist/sync/index.js +3 -0
- package/dist/sync/syncService.d.ts +25 -0
- package/dist/sync/syncService.js +184 -16
- package/dist/sync/syncService.test.js +289 -25
- package/dist/sync/workspaceRepair.d.ts +114 -0
- package/dist/sync/workspaceRepair.js +566 -0
- package/dist/sync/workspaceSyncModel.d.ts +230 -2
- package/dist/sync/workspaceSyncModel.js +456 -17
- package/dist/sync/workspaceSyncModel.test.js +642 -0
- package/dist/sync/workspaceSyncState.d.ts +12 -0
- package/dist/sync/workspaceSyncState.js +12 -0
- package/dist/sync/workspaceSyncSurface.d.ts +33 -0
- package/dist/sync/workspaceSyncSurface.js +74 -0
- package/dist/types.d.ts +143 -18
- package/dist/types.js +9 -24
- package/dist/ui/assets/AgentsModule-Bdq3T1Ts.js +1 -0
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-BaS2VwIr.css +1 -0
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-jKUVo-PJ.js +3 -0
- package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +1 -0
- package/dist/ui/assets/DocumentWorkspace-DuQa-uIC.js +250 -0
- package/dist/ui/assets/InitiativesModule-C8Pi2VTk.js +1 -0
- package/dist/ui/assets/PlansPage-B6BDECSi.js +1 -0
- package/dist/ui/assets/PlansPage-BlVC_lRq.css +1 -0
- package/dist/ui/assets/TaskSettings-BwB9NtmJ.css +1 -0
- package/dist/ui/assets/TaskSettings-DUJgRTPU.js +8 -0
- package/dist/ui/assets/WorkflowsModule-b6TLuHwJ.js +5 -0
- package/dist/ui/assets/index-BiiF1cLQ.css +1 -0
- package/dist/ui/assets/index-xBWLPOEb.js +6 -0
- package/dist/ui/assets/vendor-dnd-DRzYolkg.js +5 -0
- package/dist/ui/assets/{vendor-icons-ZFG3SGwo.js → vendor-icons-QZyhEwgT.js} +1 -1
- package/dist/ui/assets/{vendor-markdown-CKYkQHfj.js → vendor-markdown-BUxTU7dS.js} +1 -1
- package/dist/ui/assets/vendor-react-CKJs5o3c.js +61 -0
- package/dist/ui/assets/vendor-router-BbWMxlnO.js +3 -0
- package/dist/ui/branding/logos/logo.png +0 -0
- package/dist/ui/branding/logos/logo_dark.svg +27 -0
- package/dist/ui/branding/logos/logo_favicon.svg +35 -0
- package/dist/ui/branding/logos/logo_light.svg +27 -0
- package/dist/ui/favicon/apple-touch-icon.png +0 -0
- package/dist/ui/favicon/archive/phoenix/apple-touch-icon.png +0 -0
- package/dist/ui/favicon/archive/phoenix/favicon.ico +0 -0
- package/dist/ui/favicon/archive/phoenix/site.webmanifest +1 -0
- package/dist/ui/favicon/favicon-96x96.png +0 -0
- package/dist/ui/favicon/favicon.ico +0 -0
- package/dist/ui/favicon/favicon.svg +17 -0
- package/dist/ui/favicon/site.webmanifest +21 -1
- package/dist/ui/favicon/web-app-manifest-192x192.png +0 -0
- package/dist/ui/favicon/web-app-manifest-512x512.png +0 -0
- package/dist/ui/index.html +10 -7
- package/dist/ui/product/screen_01.png +0 -0
- package/dist/ui/product/screen_02.png +0 -0
- package/dist/ui/product/screen_03.png +0 -0
- package/dist/utils/annotatedAttachments.d.ts +20 -0
- package/dist/utils/annotatedAttachments.js +74 -0
- package/dist/utils/annotatedAttachments.test.d.ts +1 -0
- package/dist/utils/annotatedAttachments.test.js +108 -0
- package/dist/utils/assignees.d.ts +26 -0
- package/dist/utils/assignees.js +132 -0
- package/dist/utils/canonicalAssetKind.d.ts +16 -0
- package/dist/utils/canonicalAssetKind.js +39 -0
- package/dist/utils/canonicalAssetKind.test.d.ts +1 -0
- package/dist/utils/canonicalAssetKind.test.js +24 -0
- package/dist/utils/coalescedRefresh.d.ts +5 -0
- package/dist/utils/coalescedRefresh.js +16 -0
- package/dist/utils/coalescedRefresh.test.d.ts +1 -0
- package/dist/utils/coalescedRefresh.test.js +30 -0
- package/dist/utils/constants.d.ts +16 -3
- package/dist/utils/constants.icons.test.js +2 -1
- package/dist/utils/constants.js +70 -13
- package/dist/utils/contextAssetEvents.d.ts +7 -0
- package/dist/utils/contextAssetEvents.js +6 -0
- package/dist/utils/contextFiles.d.ts +14 -3
- package/dist/utils/contextFiles.js +40 -8
- package/dist/utils/documentManagerApi.d.ts +6 -0
- package/dist/utils/documentManagerApi.js +19 -0
- package/dist/utils/documentNames.d.ts +24 -0
- package/dist/utils/documentNames.js +91 -0
- package/dist/utils/documentReferences.d.ts +9 -0
- package/dist/utils/documentReferences.js +11 -0
- package/dist/utils/entityReferences.d.ts +9 -0
- package/dist/utils/entityReferences.js +32 -0
- package/dist/utils/imageReferences.d.ts +9 -0
- package/dist/utils/imageReferences.js +11 -0
- package/dist/utils/planParser.d.ts +2 -2
- package/dist/utils/planParser.js +11 -11
- package/dist/utils/planParser.test.d.ts +1 -0
- package/dist/{__tests__ → utils}/planParser.test.js +22 -22
- package/dist/utils/planningReferences.d.ts +17 -0
- package/dist/utils/planningReferences.js +30 -0
- package/dist/utils/planningReferences.test.d.ts +1 -0
- package/dist/utils/planningReferences.test.js +17 -0
- package/dist/utils/taskActivity.d.ts +9 -0
- package/dist/utils/taskActivity.js +147 -0
- package/dist/utils/taskActivity.test.d.ts +1 -0
- package/dist/utils/taskActivity.test.js +34 -0
- package/dist/utils/taskDraftDefaults.d.ts +21 -0
- package/dist/utils/taskDraftDefaults.js +40 -0
- package/dist/utils/taskDraftDefaults.test.d.ts +1 -0
- package/dist/utils/taskDraftDefaults.test.js +34 -0
- package/dist/utils/taskNormalization.d.ts +25 -0
- package/dist/utils/taskNormalization.js +161 -0
- package/dist/utils/taskNormalization.test.d.ts +1 -0
- package/dist/utils/taskNormalization.test.js +158 -0
- package/dist/utils/taskReferences.d.ts +5 -0
- package/dist/utils/taskReferences.js +11 -0
- package/dist/utils/taskforceApiClient.d.ts +2 -0
- package/dist/utils/taskforceApiClient.js +101 -3
- package/dist/utils/taskforceApiClient.test.d.ts +1 -0
- package/dist/utils/taskforceApiClient.test.js +44 -0
- package/dist/utils/taxonomyFilterOptions.d.ts +2 -0
- package/dist/utils/taxonomyFilterOptions.js +17 -0
- package/dist/utils/taxonomyFormDefaults.d.ts +3 -0
- package/dist/utils/taxonomyFormDefaults.js +28 -0
- package/dist/utils/taxonomyFormDefaults.test.d.ts +1 -0
- package/dist/utils/taxonomyFormDefaults.test.js +63 -0
- package/dist/utils/taxonomyLifecycle.d.ts +3 -0
- package/dist/utils/taxonomyLifecycle.js +17 -0
- package/dist/utils/taxonomySorting.d.ts +7 -0
- package/dist/utils/taxonomySorting.js +74 -0
- package/dist/utils/theme.d.ts +15 -0
- package/dist/utils/theme.js +23 -0
- package/dist/utils/uiNotice.d.ts +7 -0
- package/dist/utils/uiNotice.js +6 -0
- package/dist/utils/uiNotice.test.d.ts +1 -0
- package/dist/utils/uiNotice.test.js +18 -0
- package/dist/utils/workspaceIdentity.d.ts +14 -0
- package/dist/utils/workspaceIdentity.js +21 -0
- package/dist/utils/workspaceIdentity.test.js +30 -1
- package/dist/utils/workspaceSyncPresentation.d.ts +20 -0
- package/dist/utils/workspaceSyncPresentation.js +21 -0
- package/dist/utils/workspaceSyncPresentation.test.d.ts +1 -0
- package/dist/utils/workspaceSyncPresentation.test.js +36 -0
- package/package.json +30 -23
- package/scripts/bootstrap-playwright-auth.mjs +155 -0
- package/scripts/check-compat-boundaries.mjs +41 -0
- package/scripts/check-env.ts +9 -2
- package/scripts/debug-login.mjs +32 -0
- package/scripts/deploy-prod.sh +41 -0
- package/scripts/deploy-staging.sh +23 -0
- package/scripts/migrate-planning-model.ts +233 -0
- package/scripts/migrate-task-data-assets.mjs +408 -0
- package/scripts/playwright-auth.sh +11 -0
- package/scripts/run-dev-proxy.sh +2 -0
- package/scripts/update-homebrew-formula.sh +2 -2
- package/scripts/with-esbuild.sh +11 -0
- package/src/resources/templates/WORKFLOW_V2_POLICY.md +1 -1
- package/src/resources/templates/workflow-sources/workflowDocs.mjs +62 -52
- package/src/resources/templates/workflows/collaborate.yaml +11 -5
- package/src/resources/templates/workflows/do.yaml +8 -2
- package/src/resources/templates/workflows/evaluate.yaml +13 -7
- package/src/resources/templates/workflows/execute.yaml +40 -23
- package/src/resources/templates/workflows/plan.yaml +15 -8
- package/src/resources/templates/workflows/review.yaml +14 -8
- package/dist/TaskforceScreenshot.d.ts +0 -11
- package/dist/TaskforceScreenshot.js +0 -113
- package/dist/compat/priorityCompat.d.ts +0 -2
- package/dist/compat/priorityCompat.js +0 -24
- package/dist/components/features/settings/TaxonomyManager 2.d.ts +0 -52
- package/dist/components/features/settings/TaxonomyManager 2.js +0 -75
- package/dist/components/task/TaskKanban 2.d.ts +0 -76
- package/dist/components/task/TaskKanban 2.js +0 -1004
- package/dist/components/task/TaskScreenshotSection.d.ts +0 -15
- package/dist/components/task/TaskScreenshotSection.js +0 -22
- package/dist/components/views/StandaloneLayout 2.d.ts +0 -8
- package/dist/components/views/StandaloneLayout 2.js +0 -1660
- package/dist/core/InitiativeTemplates.test.js +0 -50
- package/dist/core/ParentLifecycle.test.js +0 -98
- package/dist/migrations/clientMigrations.d.ts +0 -9
- package/dist/migrations/clientMigrations.js +0 -74
- package/dist/migrations/flags.d.ts +0 -6
- package/dist/migrations/flags.js +0 -8
- package/dist/migrations/taskMigrations.d.ts +0 -71
- package/dist/migrations/taskMigrations.js +0 -236
- package/dist/migrations/taskMigrations.test.js +0 -34
- package/dist/server/routes.billing.js +0 -991
- package/dist/server/routes.billing.test.js +0 -387
- package/dist/server/routes.sync.integration.test.js +0 -470
- package/dist/server/routes.sync.js +0 -1141
- package/dist/soak/syncSoakHarness.d.ts +0 -6
- package/dist/soak/syncSoakHarness.js +0 -10
- package/dist/ui/assets/index-0W5vyCpn.css +0 -1
- package/dist/ui/assets/index-MCZuNNfn.js +0 -23
- package/dist/ui/assets/logo_white_01-0Vx-iIIv.png +0 -0
- package/dist/ui/assets/vendor-dnd-BI1K77C1.js +0 -5
- package/dist/ui/assets/vendor-react-DdIDsh7f.js +0 -9
- package/dist/ui/assets/vendor-router-BtiNU7Dl.js +0 -3
- package/dist/utils/taskHierarchy.d.ts +0 -4
- package/dist/utils/taskHierarchy.js +0 -69
- package/dist/utils/taskHierarchy.test.js +0 -38
- package/dist/utils/taskParenting.d.ts +0 -3
- package/dist/utils/taskParenting.js +0 -24
- package/dist/utils/taskParenting.test.js +0 -21
- package/dist/utils/taskRelations.d.ts +0 -6
- package/dist/utils/taskRelations.js +0 -24
- package/dist/utils/taskRelations.test.js +0 -34
- package/scripts/check-compat-boundaries.js +0 -48
- /package/dist/{__tests__/planParser.test.d.ts → TaskforceCore.filter-persistence.test.d.ts} +0 -0
- /package/dist/{__tests__/releaseVersion.test.d.ts → TaskforceCore.routing.test.d.ts} +0 -0
- /package/dist/{__tests__/structuredCommentValidation.test.d.ts → annotatedAttachments/AnnotatedAttachmentSessionStore.test.d.ts} +0 -0
- /package/dist/{__tests__/structuredDocValidation.test.d.ts → annotatedAttachments/payload.test.d.ts} +0 -0
- /package/dist/{__tests__/templateEngine.test.d.ts → annotatedAttachments/types.js} +0 -0
- /package/dist/{__tests__/templates.test.d.ts → compat/workspaceSyncCompat.test.d.ts} +0 -0
- /package/dist/{__tests__/workflowDocumentFixtures.test.d.ts → components/features/AgentsModule.test.d.ts} +0 -0
- /package/dist/{__tests__/workflowExportMatrix.test.d.ts → components/features/AnnotatedAttachmentWorkspace.test.d.ts} +0 -0
- /package/dist/{__tests__/workflowExportSnapshots.test.d.ts → components/features/DocumentWorkspace.test.d.ts} +0 -0
- /package/dist/{__tests__/workflowGeneration.test.d.ts → components/features/settings/TaxonomyLibraryManager.test.d.ts} +0 -0
- /package/dist/{__tests__/workflowV2Policy.test.d.ts → components/task/TaskActionHeader.test.d.ts} +0 -0
- /package/dist/{core/InitiativeTemplates.test.d.ts → components/task/TaskKanban.planning-drop.test.d.ts} +0 -0
- /package/dist/{core/ParentLifecycle.test.d.ts → components/task/TaskStatusActions.test.d.ts} +0 -0
- /package/dist/{migrations/taskMigrations.test.d.ts → components/ui/TopNoticeLayer.test.d.ts} +0 -0
- /package/dist/{server/routes.billing.test.d.ts → components/views/AppShellHeader.test.d.ts} +0 -0
- /package/dist/{server/routes.sync.integration.test.d.ts → components/views/PlanComparisonPage.test.d.ts} +0 -0
- /package/dist/{utils/taskHierarchy.test.d.ts → components/views/chrome/WorkspaceChromeSections.test.d.ts} +0 -0
- /package/dist/{utils/taskParenting.test.d.ts → components/views/chrome/workspaceChrome.test.d.ts} +0 -0
- /package/dist/{utils/taskRelations.test.d.ts → components/views/panels/DocumentFilterToolbar.test.d.ts} +0 -0
- /package/dist/public/{android-chrome-192x192.png → archive/phoenix/android-chrome-192x192.png} +0 -0
- /package/dist/public/{android-chrome-512x512.png → archive/phoenix/android-chrome-512x512.png} +0 -0
- /package/dist/public/{favicon-16x16.png → archive/phoenix/favicon-16x16.png} +0 -0
- /package/dist/public/{favicon-32x32.png → archive/phoenix/favicon-32x32.png} +0 -0
- /package/dist/public/{favicon.png → archive/phoenix/favicon.png} +0 -0
- /package/dist/{__tests__ → services}/workflowExportSnapshots.test.js +0 -0
- /package/dist/{__tests__ → services}/workflowGeneration.test.js +0 -0
- /package/dist/ui/favicon/{android-chrome-192x192.png → archive/phoenix/android-chrome-192x192.png} +0 -0
- /package/dist/ui/favicon/{android-chrome-512x512.png → archive/phoenix/android-chrome-512x512.png} +0 -0
- /package/dist/ui/favicon/{favicon-16x16.png → archive/phoenix/favicon-16x16.png} +0 -0
- /package/dist/ui/favicon/{favicon-32x32.png → archive/phoenix/favicon-32x32.png} +0 -0
- /package/dist/ui/favicon/{favicon.png → archive/phoenix/favicon.png} +0 -0
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { render, screen, waitFor } from '@testing-library/react';
|
|
2
|
+
import { act, render, screen, waitFor, within } from '@testing-library/react';
|
|
3
3
|
import userEvent from '@testing-library/user-event';
|
|
4
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
4
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
5
5
|
import { MemoryRouter } from 'react-router-dom';
|
|
6
6
|
import { TaskforceCore } from './TaskforceCore';
|
|
7
7
|
import fs from 'fs';
|
|
8
8
|
import path from 'path';
|
|
9
|
+
import modalStyles from './components/ui/Modal.module.css';
|
|
9
10
|
const mockTasks = [
|
|
10
11
|
{
|
|
11
12
|
id: 'task-20260130-abc123',
|
|
@@ -32,19 +33,63 @@ const mockConfig = {
|
|
|
32
33
|
const mockCategories = {
|
|
33
34
|
categories: [{ value: 'general', label: 'General' }]
|
|
34
35
|
};
|
|
36
|
+
function jsonResponse(body, status = 200) {
|
|
37
|
+
return Promise.resolve({
|
|
38
|
+
ok: status >= 200 && status < 300,
|
|
39
|
+
status,
|
|
40
|
+
statusText: '',
|
|
41
|
+
headers: {
|
|
42
|
+
get: () => null
|
|
43
|
+
},
|
|
44
|
+
text: () => Promise.resolve(JSON.stringify(body)),
|
|
45
|
+
json: () => Promise.resolve(body)
|
|
46
|
+
});
|
|
47
|
+
}
|
|
35
48
|
describe('TaskforceCore Integration', () => {
|
|
36
49
|
beforeEach(() => {
|
|
37
50
|
localStorage.clear();
|
|
38
51
|
vi.stubGlobal('fetch', vi.fn((url) => {
|
|
52
|
+
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
53
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'local' } }) });
|
|
54
|
+
}
|
|
55
|
+
if (url.includes('/api/taskforce/ui-state?key=app')) {
|
|
56
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true, state: {} }) });
|
|
57
|
+
}
|
|
58
|
+
if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
|
|
59
|
+
return Promise.resolve({
|
|
60
|
+
ok: true,
|
|
61
|
+
json: () => Promise.resolve({
|
|
62
|
+
success: true,
|
|
63
|
+
state: {
|
|
64
|
+
version: 2,
|
|
65
|
+
enabled: false,
|
|
66
|
+
phase: 'idle',
|
|
67
|
+
pullCursor: null,
|
|
68
|
+
lastPullAt: null,
|
|
69
|
+
lastPushAt: null,
|
|
70
|
+
lastSyncedAt: null
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
});
|
|
74
|
+
}
|
|
39
75
|
if (url.includes('/api/taskforce/config')) {
|
|
40
76
|
return Promise.resolve({ ok: true, json: () => Promise.resolve(mockConfig) });
|
|
41
77
|
}
|
|
78
|
+
if (url.includes('/api/taskforce/global-settings')) {
|
|
79
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
80
|
+
}
|
|
42
81
|
if (url.includes('/api/taskforce/categories')) {
|
|
43
82
|
return Promise.resolve({ ok: true, json: () => Promise.resolve(mockCategories) });
|
|
44
83
|
}
|
|
45
84
|
if (url.includes('/api/taskforce/tasks')) {
|
|
46
85
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
|
|
47
86
|
}
|
|
87
|
+
if (url.includes('/api/taskforce/archive')) {
|
|
88
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ archived: [] }) });
|
|
89
|
+
}
|
|
90
|
+
if (url.includes('/api/taskforce/data-version')) {
|
|
91
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ dataVersion: 1 }) });
|
|
92
|
+
}
|
|
48
93
|
if (url.includes('/api/taskforce/types')) {
|
|
49
94
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ types: [] }) });
|
|
50
95
|
}
|
|
@@ -60,6 +105,55 @@ describe('TaskforceCore Integration', () => {
|
|
|
60
105
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
61
106
|
}));
|
|
62
107
|
});
|
|
108
|
+
afterEach(() => {
|
|
109
|
+
delete globalThis.__DEBUG_MODE__;
|
|
110
|
+
});
|
|
111
|
+
it('keeps board content fully visible when zen mode is enabled', async () => {
|
|
112
|
+
const user = userEvent.setup();
|
|
113
|
+
let fullscreenElement = null;
|
|
114
|
+
const requestFullscreen = vi.fn(async function () {
|
|
115
|
+
fullscreenElement = this;
|
|
116
|
+
Object.defineProperty(document, 'fullscreenElement', {
|
|
117
|
+
configurable: true,
|
|
118
|
+
get: () => fullscreenElement
|
|
119
|
+
});
|
|
120
|
+
document.dispatchEvent(new Event('fullscreenchange'));
|
|
121
|
+
});
|
|
122
|
+
const exitFullscreen = vi.fn(async () => {
|
|
123
|
+
fullscreenElement = null;
|
|
124
|
+
Object.defineProperty(document, 'fullscreenElement', {
|
|
125
|
+
configurable: true,
|
|
126
|
+
get: () => fullscreenElement
|
|
127
|
+
});
|
|
128
|
+
document.dispatchEvent(new Event('fullscreenchange'));
|
|
129
|
+
});
|
|
130
|
+
Object.defineProperty(document, 'fullscreenElement', {
|
|
131
|
+
configurable: true,
|
|
132
|
+
get: () => fullscreenElement
|
|
133
|
+
});
|
|
134
|
+
Object.defineProperty(document.documentElement, 'requestFullscreen', {
|
|
135
|
+
configurable: true,
|
|
136
|
+
value: requestFullscreen
|
|
137
|
+
});
|
|
138
|
+
Object.defineProperty(document, 'exitFullscreen', {
|
|
139
|
+
configurable: true,
|
|
140
|
+
value: exitFullscreen
|
|
141
|
+
});
|
|
142
|
+
const { container } = render(_jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }));
|
|
143
|
+
await waitFor(() => {
|
|
144
|
+
expect(screen.getByTitle('Visible active tasks / total active tasks')).toHaveTextContent('1/1');
|
|
145
|
+
});
|
|
146
|
+
const boardContent = container.querySelector('[class*="standaloneContent"]');
|
|
147
|
+
expect(boardContent).not.toBeNull();
|
|
148
|
+
expect(boardContent).not.toHaveStyle({ opacity: '0.1' });
|
|
149
|
+
await user.click(screen.getByTitle(/enter zen mode/i));
|
|
150
|
+
await waitFor(() => {
|
|
151
|
+
expect(requestFullscreen).toHaveBeenCalledTimes(1);
|
|
152
|
+
});
|
|
153
|
+
expect(screen.getByTitle(/exit zen mode/i)).toBeInTheDocument();
|
|
154
|
+
expect(boardContent).not.toHaveStyle({ opacity: '0.1' });
|
|
155
|
+
expect(screen.getByTitle('Visible active tasks / total active tasks')).toHaveTextContent('1/1');
|
|
156
|
+
});
|
|
63
157
|
it('5.1 & 5.3 Renders main header and task list', async () => {
|
|
64
158
|
render(_jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }));
|
|
65
159
|
// Header check
|
|
@@ -71,6 +165,68 @@ describe('TaskforceCore Integration', () => {
|
|
|
71
165
|
expect(screen.getByText('1/1')).toBeInTheDocument();
|
|
72
166
|
});
|
|
73
167
|
});
|
|
168
|
+
it('shows the header sync badge as off when local runtime is signed out', async () => {
|
|
169
|
+
vi.stubGlobal('fetch', vi.fn((url) => {
|
|
170
|
+
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
171
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'local' } }) });
|
|
172
|
+
}
|
|
173
|
+
if (url.includes('/api/taskforce/ui-state?key=app')) {
|
|
174
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true, state: {} }) });
|
|
175
|
+
}
|
|
176
|
+
if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
|
|
177
|
+
return Promise.resolve({
|
|
178
|
+
ok: true,
|
|
179
|
+
json: () => Promise.resolve({
|
|
180
|
+
success: true,
|
|
181
|
+
state: {
|
|
182
|
+
version: 2,
|
|
183
|
+
enabled: true,
|
|
184
|
+
phase: 'active',
|
|
185
|
+
pullCursor: null,
|
|
186
|
+
lastPullAt: '2026-03-15T19:30:00.000Z',
|
|
187
|
+
lastPushAt: '2026-03-15T19:31:00.000Z',
|
|
188
|
+
lastSyncedAt: '2026-03-15T19:31:00.000Z'
|
|
189
|
+
}
|
|
190
|
+
})
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
if (url.includes('/api/taskforce/config')) {
|
|
194
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve(mockConfig) });
|
|
195
|
+
}
|
|
196
|
+
if (url.includes('/api/taskforce/global-settings')) {
|
|
197
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
198
|
+
}
|
|
199
|
+
if (url.includes('/api/taskforce/categories')) {
|
|
200
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve(mockCategories) });
|
|
201
|
+
}
|
|
202
|
+
if (url.includes('/api/taskforce/tasks')) {
|
|
203
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
|
|
204
|
+
}
|
|
205
|
+
if (url.includes('/api/taskforce/archive')) {
|
|
206
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ archived: [] }) });
|
|
207
|
+
}
|
|
208
|
+
if (url.includes('/api/taskforce/data-version')) {
|
|
209
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ dataVersion: 1 }) });
|
|
210
|
+
}
|
|
211
|
+
if (url.includes('/api/taskforce/types')) {
|
|
212
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ types: [] }) });
|
|
213
|
+
}
|
|
214
|
+
if (url.includes('/api/taskforce/priorities')) {
|
|
215
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ priorities: [] }) });
|
|
216
|
+
}
|
|
217
|
+
if (url.includes('/api/taskforce/approaches')) {
|
|
218
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ approaches: [] }) });
|
|
219
|
+
}
|
|
220
|
+
if (url.includes('/api/taskforce/specialists')) {
|
|
221
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
|
|
222
|
+
}
|
|
223
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
224
|
+
}));
|
|
225
|
+
render(_jsx(TaskforceCore, {}));
|
|
226
|
+
await waitFor(() => {
|
|
227
|
+
expect(screen.getByTitle(/Sync manager: Off/)).toBeInTheDocument();
|
|
228
|
+
});
|
|
229
|
+
});
|
|
74
230
|
it('5.2 Does not render the widget floating button', async () => {
|
|
75
231
|
const { container } = render(_jsx(TaskforceCore, {}));
|
|
76
232
|
expect(screen.queryByTitle('Open Taskforce')).not.toBeInTheDocument();
|
|
@@ -87,6 +243,267 @@ describe('TaskforceCore Integration', () => {
|
|
|
87
243
|
expect(onCountChange).toHaveBeenCalledWith(1);
|
|
88
244
|
});
|
|
89
245
|
});
|
|
246
|
+
it('unblocks cloud loading before slow reference data finishes', async () => {
|
|
247
|
+
let resolveCategories = () => { };
|
|
248
|
+
const categoriesGate = new Promise((resolve) => {
|
|
249
|
+
resolveCategories = resolve;
|
|
250
|
+
});
|
|
251
|
+
vi.stubGlobal('fetch', vi.fn((url) => {
|
|
252
|
+
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
253
|
+
return jsonResponse({
|
|
254
|
+
config: {
|
|
255
|
+
runtimeMode: 'cloud',
|
|
256
|
+
baseUrl: 'https://app.taskforce.example',
|
|
257
|
+
apiBaseUrl: 'https://app.taskforce.example/api',
|
|
258
|
+
workspaceMode: 'multi-cloud',
|
|
259
|
+
workspaceSwitchingEnabled: true,
|
|
260
|
+
authSource: 'cloud'
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
265
|
+
return jsonResponse({
|
|
266
|
+
authenticated: true,
|
|
267
|
+
runtimeMode: 'cloud',
|
|
268
|
+
authRequiredForApi: true,
|
|
269
|
+
email: 'member@example.com',
|
|
270
|
+
userId: 'user-1',
|
|
271
|
+
workspaceId: 'workspace-1',
|
|
272
|
+
betaAccess: true
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
if (url.includes('/api/taskforce/workspaces')) {
|
|
276
|
+
return jsonResponse({
|
|
277
|
+
success: true,
|
|
278
|
+
workspaces: [{
|
|
279
|
+
id: 'workspace-1',
|
|
280
|
+
slug: 'workspace-1',
|
|
281
|
+
name: 'Fast Workspace',
|
|
282
|
+
description: null,
|
|
283
|
+
role: 'owner',
|
|
284
|
+
status: 'active',
|
|
285
|
+
betaAccess: true
|
|
286
|
+
}],
|
|
287
|
+
currentWorkspaceId: 'workspace-1'
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
if (url.includes('/api/taskforce/account/access-status')) {
|
|
291
|
+
return jsonResponse({
|
|
292
|
+
gate: 'ok',
|
|
293
|
+
canAccessApp: true,
|
|
294
|
+
canAccessPlans: true
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
if (url.includes('/api/taskforce/ui-state?key=app')) {
|
|
298
|
+
return jsonResponse({ success: true, state: {} });
|
|
299
|
+
}
|
|
300
|
+
if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
|
|
301
|
+
return jsonResponse({
|
|
302
|
+
success: true,
|
|
303
|
+
state: {
|
|
304
|
+
version: 2,
|
|
305
|
+
enabled: false,
|
|
306
|
+
phase: 'idle',
|
|
307
|
+
pullCursor: null,
|
|
308
|
+
lastPullAt: null,
|
|
309
|
+
lastPushAt: null,
|
|
310
|
+
lastSyncedAt: null
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
if (url.includes('/api/taskforce/config')) {
|
|
315
|
+
return jsonResponse({
|
|
316
|
+
runtimeMode: 'cloud',
|
|
317
|
+
storagePath: '',
|
|
318
|
+
shortcut: 'Alt+T',
|
|
319
|
+
projectName: 'Fast Workspace',
|
|
320
|
+
setupState: {
|
|
321
|
+
globalSetupState: 'complete',
|
|
322
|
+
workspaceSetupState: 'complete',
|
|
323
|
+
workspaceId: 'workspace-1'
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
if (url.includes('/api/taskforce/tasks')) {
|
|
328
|
+
return jsonResponse({ tasks: mockTasks });
|
|
329
|
+
}
|
|
330
|
+
if (url.includes('/api/taskforce/archive')) {
|
|
331
|
+
return jsonResponse({ archived: [] });
|
|
332
|
+
}
|
|
333
|
+
if (url.includes('/api/taskforce/version')) {
|
|
334
|
+
return jsonResponse({ ok: true, build: { version: '1.0.0' } });
|
|
335
|
+
}
|
|
336
|
+
if (url.includes('/api/taskforce/categories')) {
|
|
337
|
+
return categoriesGate.then(() => jsonResponse(mockCategories));
|
|
338
|
+
}
|
|
339
|
+
if (url.includes('/api/taskforce/types')) {
|
|
340
|
+
return jsonResponse({ types: [] });
|
|
341
|
+
}
|
|
342
|
+
if (url.includes('/api/taskforce/priorities')) {
|
|
343
|
+
return jsonResponse({ priorities: [] });
|
|
344
|
+
}
|
|
345
|
+
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
346
|
+
return jsonResponse({ templates: [] });
|
|
347
|
+
}
|
|
348
|
+
if (url.includes('/api/taskforce/initiative-templates')) {
|
|
349
|
+
return jsonResponse({ templates: [] });
|
|
350
|
+
}
|
|
351
|
+
if (url.includes('/api/taskforce/environments')) {
|
|
352
|
+
return jsonResponse({ environments: [] });
|
|
353
|
+
}
|
|
354
|
+
if (url.includes('/api/taskforce/approaches')) {
|
|
355
|
+
return jsonResponse({ approaches: [] });
|
|
356
|
+
}
|
|
357
|
+
if (url.includes('/api/taskforce/taxonomies')) {
|
|
358
|
+
return jsonResponse({ taxonomies: [] });
|
|
359
|
+
}
|
|
360
|
+
if (url.includes('/api/taskforce/specialists')) {
|
|
361
|
+
return jsonResponse({ specialists: [] });
|
|
362
|
+
}
|
|
363
|
+
if (url.includes('/api/taskforce/data-version')) {
|
|
364
|
+
return jsonResponse({ dataVersion: 1 });
|
|
365
|
+
}
|
|
366
|
+
return jsonResponse({});
|
|
367
|
+
}));
|
|
368
|
+
render(_jsx(TaskforceCore, { config: {} }));
|
|
369
|
+
await waitFor(() => {
|
|
370
|
+
expect(screen.queryByText('Loading Taskforce')).not.toBeInTheDocument();
|
|
371
|
+
expect(screen.getByText('Fast Workspace')).toBeInTheDocument();
|
|
372
|
+
expect(screen.getByText('1/1')).toBeInTheDocument();
|
|
373
|
+
});
|
|
374
|
+
resolveCategories();
|
|
375
|
+
await waitFor(() => {
|
|
376
|
+
expect(screen.getByText('1/1')).toBeInTheDocument();
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
it('deduplicates cloud bootstrap workspace, task, and workflow fetches', async () => {
|
|
380
|
+
const fetchMock = vi.fn((url) => {
|
|
381
|
+
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
382
|
+
return jsonResponse({
|
|
383
|
+
config: {
|
|
384
|
+
runtimeMode: 'cloud',
|
|
385
|
+
baseUrl: 'https://app.taskforce.example',
|
|
386
|
+
apiBaseUrl: 'https://app.taskforce.example/api',
|
|
387
|
+
workspaceMode: 'multi-cloud',
|
|
388
|
+
workspaceSwitchingEnabled: true,
|
|
389
|
+
authSource: 'cloud'
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
394
|
+
return jsonResponse({
|
|
395
|
+
authenticated: true,
|
|
396
|
+
runtimeMode: 'cloud',
|
|
397
|
+
authRequiredForApi: true,
|
|
398
|
+
email: 'member@example.com',
|
|
399
|
+
userId: 'user-1',
|
|
400
|
+
workspaceId: 'workspace-1',
|
|
401
|
+
betaAccess: true
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
if (url.includes('/api/taskforce/workspaces')) {
|
|
405
|
+
return jsonResponse({
|
|
406
|
+
success: true,
|
|
407
|
+
workspaces: [{
|
|
408
|
+
id: 'workspace-1',
|
|
409
|
+
slug: 'workspace-1',
|
|
410
|
+
name: 'Fast Workspace',
|
|
411
|
+
description: null,
|
|
412
|
+
role: 'owner',
|
|
413
|
+
status: 'active',
|
|
414
|
+
betaAccess: true
|
|
415
|
+
}],
|
|
416
|
+
currentWorkspaceId: 'workspace-1'
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
if (url.includes('/api/taskforce/account/access-status')) {
|
|
420
|
+
return jsonResponse({
|
|
421
|
+
gate: 'ok',
|
|
422
|
+
canAccessApp: true,
|
|
423
|
+
canAccessPlans: true
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
if (url.includes('/api/taskforce/ui-state?key=app')) {
|
|
427
|
+
return jsonResponse({ success: true, state: {} });
|
|
428
|
+
}
|
|
429
|
+
if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
|
|
430
|
+
return jsonResponse({
|
|
431
|
+
success: true,
|
|
432
|
+
state: {
|
|
433
|
+
version: 2,
|
|
434
|
+
enabled: false,
|
|
435
|
+
phase: 'idle',
|
|
436
|
+
pullCursor: null,
|
|
437
|
+
lastPullAt: null,
|
|
438
|
+
lastPushAt: null,
|
|
439
|
+
lastSyncedAt: null
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
if (url.includes('/api/taskforce/config')) {
|
|
444
|
+
return jsonResponse({
|
|
445
|
+
runtimeMode: 'cloud',
|
|
446
|
+
storagePath: '',
|
|
447
|
+
shortcut: 'Alt+T',
|
|
448
|
+
projectName: 'Fast Workspace',
|
|
449
|
+
setupState: {
|
|
450
|
+
globalSetupState: 'complete',
|
|
451
|
+
workspaceSetupState: 'complete',
|
|
452
|
+
workspaceId: 'workspace-1'
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
if (url.includes('/api/taskforce/tasks')) {
|
|
457
|
+
return jsonResponse({ tasks: mockTasks });
|
|
458
|
+
}
|
|
459
|
+
if (url.includes('/api/taskforce/archive')) {
|
|
460
|
+
return jsonResponse({ archived: [] });
|
|
461
|
+
}
|
|
462
|
+
if (url.includes('/api/taskforce/version')) {
|
|
463
|
+
return jsonResponse({ ok: true, build: { version: '1.0.0' } });
|
|
464
|
+
}
|
|
465
|
+
if (url.includes('/api/taskforce/categories')) {
|
|
466
|
+
return jsonResponse(mockCategories);
|
|
467
|
+
}
|
|
468
|
+
if (url.includes('/api/taskforce/types')) {
|
|
469
|
+
return jsonResponse({ types: [] });
|
|
470
|
+
}
|
|
471
|
+
if (url.includes('/api/taskforce/priorities')) {
|
|
472
|
+
return jsonResponse({ priorities: [] });
|
|
473
|
+
}
|
|
474
|
+
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
475
|
+
return jsonResponse({ templates: [] });
|
|
476
|
+
}
|
|
477
|
+
if (url.includes('/api/taskforce/initiative-templates')) {
|
|
478
|
+
return jsonResponse({ templates: [] });
|
|
479
|
+
}
|
|
480
|
+
if (url.includes('/api/taskforce/environments')) {
|
|
481
|
+
return jsonResponse({ environments: [] });
|
|
482
|
+
}
|
|
483
|
+
if (url.includes('/api/taskforce/approaches')) {
|
|
484
|
+
return jsonResponse({ approaches: [] });
|
|
485
|
+
}
|
|
486
|
+
if (url.includes('/api/taskforce/taxonomies')) {
|
|
487
|
+
return jsonResponse({ taxonomies: [] });
|
|
488
|
+
}
|
|
489
|
+
if (url.includes('/api/taskforce/specialists')) {
|
|
490
|
+
return jsonResponse({ specialists: [] });
|
|
491
|
+
}
|
|
492
|
+
if (url.includes('/api/taskforce/data-version')) {
|
|
493
|
+
return jsonResponse({ dataVersion: 1 });
|
|
494
|
+
}
|
|
495
|
+
return jsonResponse({});
|
|
496
|
+
});
|
|
497
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
498
|
+
render(_jsx(TaskforceCore, { config: {} }));
|
|
499
|
+
await waitFor(() => {
|
|
500
|
+
expect(screen.getByText('Fast Workspace')).toBeInTheDocument();
|
|
501
|
+
expect(screen.getByText('1/1')).toBeInTheDocument();
|
|
502
|
+
});
|
|
503
|
+
const requestedUrls = fetchMock.mock.calls.map(([url]) => String(url));
|
|
504
|
+
expect(requestedUrls.filter((url) => url.includes('/api/taskforce/workspaces'))).toHaveLength(1);
|
|
505
|
+
expect(requestedUrls.filter((url) => url.includes('/api/taskforce/workflow-templates'))).toHaveLength(1);
|
|
506
|
+
});
|
|
90
507
|
it('5.7 Deep Link: Opens specific task when short ID is provided', async () => {
|
|
91
508
|
render(_jsx(TaskforceCore, { initialTaskId: "abc123" }));
|
|
92
509
|
await waitFor(() => {
|
|
@@ -96,6 +513,73 @@ describe('TaskforceCore Integration', () => {
|
|
|
96
513
|
expect(screen.getByDisplayValue('Integration Task 1')).toBeInTheDocument();
|
|
97
514
|
});
|
|
98
515
|
});
|
|
516
|
+
it('does not prompt for unsaved changes when closing an untouched task with defaulted approach', async () => {
|
|
517
|
+
const user = userEvent.setup();
|
|
518
|
+
render(_jsx(TaskforceCore, { initialTaskId: "abc123" }));
|
|
519
|
+
await waitFor(() => {
|
|
520
|
+
expect(screen.getByText('Update')).toBeInTheDocument();
|
|
521
|
+
expect(screen.getByDisplayValue('Integration Task 1')).toBeInTheDocument();
|
|
522
|
+
});
|
|
523
|
+
await user.click(screen.getByTitle('Close'));
|
|
524
|
+
await waitFor(() => {
|
|
525
|
+
expect(screen.queryByText('You have unsaved changes. Would you like to save them?')).not.toBeInTheDocument();
|
|
526
|
+
expect(screen.queryByDisplayValue('Integration Task 1')).not.toBeInTheDocument();
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
it('does not send createdAt or createdBy when updating an existing task', async () => {
|
|
530
|
+
const user = userEvent.setup();
|
|
531
|
+
const fetchMock = vi.fn((url, init) => {
|
|
532
|
+
if (url.includes('/api/taskforce/config')) {
|
|
533
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve(mockConfig) });
|
|
534
|
+
}
|
|
535
|
+
if (url.includes('/api/taskforce/categories')) {
|
|
536
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve(mockCategories) });
|
|
537
|
+
}
|
|
538
|
+
if (url.includes('/api/taskforce/tasks')) {
|
|
539
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
|
|
540
|
+
}
|
|
541
|
+
if (url.includes('/api/taskforce/types')) {
|
|
542
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ types: [] }) });
|
|
543
|
+
}
|
|
544
|
+
if (url.includes('/api/taskforce/priorities')) {
|
|
545
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ priorities: [] }) });
|
|
546
|
+
}
|
|
547
|
+
if (url.includes('/api/taskforce/approaches')) {
|
|
548
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ approaches: [] }) });
|
|
549
|
+
}
|
|
550
|
+
if (url.includes('/api/taskforce/specialists')) {
|
|
551
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
|
|
552
|
+
}
|
|
553
|
+
if (url.includes(`/api/taskforce/task/${mockTasks[0].id}`) && init?.method === 'PATCH') {
|
|
554
|
+
return Promise.resolve({
|
|
555
|
+
ok: true,
|
|
556
|
+
json: () => Promise.resolve({
|
|
557
|
+
...mockTasks[0],
|
|
558
|
+
title: 'Integration Task 1 Updated',
|
|
559
|
+
updatedAt: new Date().toISOString()
|
|
560
|
+
})
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
564
|
+
});
|
|
565
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
566
|
+
render(_jsx(TaskforceCore, { initialTaskId: "abc123" }));
|
|
567
|
+
await waitFor(() => {
|
|
568
|
+
expect(screen.getByDisplayValue('Integration Task 1')).toBeInTheDocument();
|
|
569
|
+
});
|
|
570
|
+
const titleInput = screen.getByDisplayValue('Integration Task 1');
|
|
571
|
+
await user.clear(titleInput);
|
|
572
|
+
await user.type(titleInput, 'Integration Task 1 Updated');
|
|
573
|
+
await user.click(screen.getByText('Update'));
|
|
574
|
+
await waitFor(() => {
|
|
575
|
+
const patchCall = fetchMock.mock.calls.find((call) => String(call[0]).includes(`/api/taskforce/task/${mockTasks[0].id}`)
|
|
576
|
+
&& call[1]?.method === 'PATCH');
|
|
577
|
+
expect(patchCall).toBeTruthy();
|
|
578
|
+
const body = JSON.parse(String(patchCall?.[1]?.body || '{}'));
|
|
579
|
+
expect(body.createdAt).toBeUndefined();
|
|
580
|
+
expect(body.createdBy).toBeUndefined();
|
|
581
|
+
});
|
|
582
|
+
});
|
|
99
583
|
it('Navigation: Can switch to Settings', async () => {
|
|
100
584
|
render(_jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }));
|
|
101
585
|
await waitFor(() => {
|
|
@@ -220,134 +704,112 @@ describe('TaskforceCore Integration', () => {
|
|
|
220
704
|
});
|
|
221
705
|
expect(fetchMock.mock.calls.some((call) => call[0] === '/api/taskforce/archive')).toBe(true);
|
|
222
706
|
});
|
|
223
|
-
it('
|
|
707
|
+
it('prefers server app ui-state over stale local storage for groupBy on bootstrap', async () => {
|
|
708
|
+
localStorage.setItem('taskforce.uiState.app.v1.bootstrap', JSON.stringify({ groupBy: 'category' }));
|
|
224
709
|
const fetchMock = vi.fn((url) => {
|
|
225
|
-
if (url.includes('/api/taskforce/
|
|
710
|
+
if (url.includes('/api/taskforce/config')) {
|
|
226
711
|
return Promise.resolve({
|
|
227
712
|
ok: true,
|
|
228
713
|
json: () => Promise.resolve({
|
|
229
|
-
|
|
714
|
+
...mockConfig,
|
|
230
715
|
runtimeMode: 'cloud',
|
|
231
|
-
authRequiredForApi:
|
|
716
|
+
authRequiredForApi: false,
|
|
717
|
+
userId: 'user-1',
|
|
718
|
+
workspaceId: 'default'
|
|
232
719
|
})
|
|
233
720
|
});
|
|
234
721
|
}
|
|
235
|
-
if (url.includes('/api/taskforce/
|
|
722
|
+
if (url.includes('/api/taskforce/categories')) {
|
|
236
723
|
return Promise.resolve({
|
|
237
724
|
ok: true,
|
|
238
725
|
json: () => Promise.resolve({
|
|
239
|
-
|
|
240
|
-
runtimeMode: 'cloud',
|
|
241
|
-
authRequiredForApi: true,
|
|
242
|
-
setupState: {
|
|
243
|
-
globalSetupState: 'present',
|
|
244
|
-
workspaceSetupState: 'present',
|
|
245
|
-
runtimeMode: 'cloud',
|
|
246
|
-
workspaceId: 'default',
|
|
247
|
-
mode: 'core',
|
|
248
|
-
forceSetup: false,
|
|
249
|
-
forceGlobalSetup: false,
|
|
250
|
-
forceWorkspaceSetup: false
|
|
251
|
-
},
|
|
252
|
-
runtimeCapabilities: {
|
|
253
|
-
runtimeMode: 'cloud',
|
|
254
|
-
supportsCloudAuth: true,
|
|
255
|
-
supportsAdminManagedSetupOverrides: false,
|
|
256
|
-
supportsPackageScriptSetupOverrides: false
|
|
257
|
-
}
|
|
726
|
+
categories: [{ value: 'general', label: 'General' }]
|
|
258
727
|
})
|
|
259
728
|
});
|
|
260
729
|
}
|
|
261
730
|
if (url.includes('/api/taskforce/tasks')) {
|
|
262
|
-
return Promise.resolve({
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
731
|
+
return Promise.resolve({
|
|
732
|
+
ok: true,
|
|
733
|
+
json: () => Promise.resolve({
|
|
734
|
+
tasks: [
|
|
735
|
+
{
|
|
736
|
+
id: 'task-pri-1',
|
|
737
|
+
title: 'Priority One Active',
|
|
738
|
+
description: null,
|
|
739
|
+
category: 'general',
|
|
740
|
+
type: 'feature',
|
|
741
|
+
priority: 1,
|
|
742
|
+
createdAt: new Date().toISOString(),
|
|
743
|
+
completed: false,
|
|
744
|
+
status: 'task'
|
|
745
|
+
},
|
|
746
|
+
{
|
|
747
|
+
id: 'task-pri-2',
|
|
748
|
+
title: 'Priority Two Active',
|
|
749
|
+
description: null,
|
|
750
|
+
category: 'general',
|
|
751
|
+
type: 'feature',
|
|
752
|
+
priority: 2,
|
|
753
|
+
createdAt: new Date().toISOString(),
|
|
754
|
+
completed: false,
|
|
755
|
+
status: 'task'
|
|
756
|
+
}
|
|
757
|
+
]
|
|
758
|
+
})
|
|
759
|
+
});
|
|
266
760
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
vi.stubGlobal('fetch', fetchMock);
|
|
270
|
-
const { container } = render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, {}) }));
|
|
271
|
-
await waitFor(() => {
|
|
272
|
-
expect(screen.getByPlaceholderText('Email')).toBeInTheDocument();
|
|
273
|
-
expect(screen.getByRole('button', { name: 'Sign In' })).toBeInTheDocument();
|
|
274
|
-
expect(screen.getByRole('button', { name: 'Register' })).toBeInTheDocument();
|
|
275
|
-
expect(screen.queryByRole('button', { name: 'Create Account' })).not.toBeInTheDocument();
|
|
276
|
-
expect(screen.queryByRole('button', { name: 'Close sign in' })).not.toBeInTheDocument();
|
|
277
|
-
});
|
|
278
|
-
expect(container.querySelector('[aria-pressed]')).not.toBeInTheDocument();
|
|
279
|
-
});
|
|
280
|
-
it('shows updated setup-bootstrap copy while startup checks are in progress', async () => {
|
|
281
|
-
const hangingConfig = new Promise(() => { });
|
|
282
|
-
const fetchMock = vi.fn((url) => {
|
|
283
|
-
if (url.includes('/api/taskforce/config')) {
|
|
284
|
-
return hangingConfig;
|
|
761
|
+
if (url.includes('/api/taskforce/archive')) {
|
|
762
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ archived: [] }) });
|
|
285
763
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
});
|
|
296
|
-
it('keeps account menu in bootstrap-pending state until runtime config resolves, then shows signed-in identity immediately', async () => {
|
|
297
|
-
const user = userEvent.setup();
|
|
298
|
-
let resolveRuntimeConfig = null;
|
|
299
|
-
const fetchMock = vi.fn((url) => {
|
|
300
|
-
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
301
|
-
return new Promise((resolve) => {
|
|
302
|
-
resolveRuntimeConfig = resolve;
|
|
764
|
+
if (url.includes('/api/taskforce/priorities')) {
|
|
765
|
+
return Promise.resolve({
|
|
766
|
+
ok: true,
|
|
767
|
+
json: () => Promise.resolve({
|
|
768
|
+
priorities: [
|
|
769
|
+
{ value: 1, label: 'P1' },
|
|
770
|
+
{ value: 2, label: 'P2' }
|
|
771
|
+
]
|
|
772
|
+
})
|
|
303
773
|
});
|
|
304
774
|
}
|
|
305
|
-
if (url.includes('
|
|
775
|
+
if (url.includes('/api/taskforce/types')) {
|
|
776
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ types: [{ value: 'feature', label: 'Feature' }] }) });
|
|
777
|
+
}
|
|
778
|
+
if (url.includes('/api/taskforce/ui-state?key=app')) {
|
|
306
779
|
return Promise.resolve({
|
|
307
780
|
ok: true,
|
|
308
781
|
json: () => Promise.resolve({
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
782
|
+
success: true,
|
|
783
|
+
state: {
|
|
784
|
+
groupBy: 'priority',
|
|
785
|
+
hasInitedFilters: true,
|
|
786
|
+
filterPriorities: [1, 2],
|
|
787
|
+
filterCategories: ['general'],
|
|
788
|
+
filterTypes: ['feature'],
|
|
789
|
+
filterStatus: ['task', 'on-hold', 'in-progress', 'review', 'done', 'cancelled']
|
|
790
|
+
}
|
|
316
791
|
})
|
|
317
792
|
});
|
|
318
793
|
}
|
|
319
|
-
if (url.includes('/api/taskforce/
|
|
794
|
+
if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
|
|
320
795
|
return Promise.resolve({
|
|
321
796
|
ok: true,
|
|
322
797
|
json: () => Promise.resolve({
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
mode: 'core',
|
|
333
|
-
forceSetup: false,
|
|
334
|
-
forceGlobalSetup: false,
|
|
335
|
-
forceWorkspaceSetup: false
|
|
798
|
+
success: true,
|
|
799
|
+
state: {
|
|
800
|
+
version: 2,
|
|
801
|
+
enabled: false,
|
|
802
|
+
phase: 'idle',
|
|
803
|
+
pullCursor: null,
|
|
804
|
+
lastPullAt: null,
|
|
805
|
+
lastPushAt: null,
|
|
806
|
+
lastSyncedAt: null
|
|
336
807
|
}
|
|
337
808
|
})
|
|
338
809
|
});
|
|
339
810
|
}
|
|
340
|
-
if (url.includes('/api/taskforce/
|
|
341
|
-
return Promise.resolve({ ok: true, json: () => Promise.resolve(
|
|
342
|
-
}
|
|
343
|
-
if (url.includes('/api/taskforce/tasks')) {
|
|
344
|
-
return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
|
|
345
|
-
}
|
|
346
|
-
if (url.includes('/api/taskforce/types')) {
|
|
347
|
-
return Promise.resolve({ ok: true, json: () => Promise.resolve({ types: [] }) });
|
|
348
|
-
}
|
|
349
|
-
if (url.includes('/api/taskforce/priorities')) {
|
|
350
|
-
return Promise.resolve({ ok: true, json: () => Promise.resolve({ priorities: [] }) });
|
|
811
|
+
if (url.includes('/api/taskforce/ui-state')) {
|
|
812
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
|
|
351
813
|
}
|
|
352
814
|
if (url.includes('/api/taskforce/approaches')) {
|
|
353
815
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ approaches: [] }) });
|
|
@@ -355,50 +817,25 @@ describe('TaskforceCore Integration', () => {
|
|
|
355
817
|
if (url.includes('/api/taskforce/specialists')) {
|
|
356
818
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
|
|
357
819
|
}
|
|
358
|
-
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
359
|
-
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
360
|
-
}
|
|
361
820
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
362
821
|
});
|
|
363
822
|
vi.stubGlobal('fetch', fetchMock);
|
|
364
|
-
render(_jsx(TaskforceCore, {
|
|
365
|
-
await waitFor(() => {
|
|
366
|
-
expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
|
|
367
|
-
});
|
|
368
|
-
await user.click(screen.getByTitle(/Account/i));
|
|
369
|
-
expect(screen.getByText('Checking sign-in status...')).toBeInTheDocument();
|
|
370
|
-
expect(screen.queryByText('Sign In / Register')).not.toBeInTheDocument();
|
|
371
|
-
if (resolveRuntimeConfig) {
|
|
372
|
-
resolveRuntimeConfig({
|
|
373
|
-
ok: true,
|
|
374
|
-
json: () => Promise.resolve({
|
|
375
|
-
success: true,
|
|
376
|
-
config: {
|
|
377
|
-
baseUrl: 'https://cloud.example.com',
|
|
378
|
-
apiBaseUrl: 'https://cloud.example.com',
|
|
379
|
-
cloudAuthBaseUrl: 'https://cloud.example.com',
|
|
380
|
-
wsBaseUrl: null,
|
|
381
|
-
runtimeMode: 'local'
|
|
382
|
-
}
|
|
383
|
-
})
|
|
384
|
-
});
|
|
385
|
-
}
|
|
823
|
+
render(_jsx(TaskforceCore, { mode: "standalone" }));
|
|
386
824
|
await waitFor(() => {
|
|
387
|
-
expect(screen.
|
|
388
|
-
expect(screen.
|
|
825
|
+
expect(screen.getByTitle('Add task to P1')).toBeInTheDocument();
|
|
826
|
+
expect(screen.getByTitle('Add task to P2')).toBeInTheDocument();
|
|
389
827
|
});
|
|
390
|
-
expect(
|
|
391
|
-
expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('/api/taskforce/session/workspace'))).toBe(false);
|
|
828
|
+
expect(screen.queryByTitle('Add task to General')).not.toBeInTheDocument();
|
|
392
829
|
});
|
|
393
|
-
it('
|
|
830
|
+
it('shows full-page login when cloud auth is required and user is unauthenticated', async () => {
|
|
394
831
|
const fetchMock = vi.fn((url) => {
|
|
395
832
|
if (url.includes('/api/taskforce/auth/session')) {
|
|
396
833
|
return Promise.resolve({
|
|
397
834
|
ok: true,
|
|
398
835
|
json: () => Promise.resolve({
|
|
399
836
|
authenticated: false,
|
|
400
|
-
runtimeMode: '
|
|
401
|
-
authRequiredForApi:
|
|
837
|
+
runtimeMode: 'cloud',
|
|
838
|
+
authRequiredForApi: true
|
|
402
839
|
})
|
|
403
840
|
});
|
|
404
841
|
}
|
|
@@ -407,48 +844,55 @@ describe('TaskforceCore Integration', () => {
|
|
|
407
844
|
ok: true,
|
|
408
845
|
json: () => Promise.resolve({
|
|
409
846
|
...mockConfig,
|
|
410
|
-
runtimeMode: '
|
|
411
|
-
authRequiredForApi:
|
|
847
|
+
runtimeMode: 'cloud',
|
|
848
|
+
authRequiredForApi: true,
|
|
412
849
|
setupState: {
|
|
413
850
|
globalSetupState: 'present',
|
|
414
851
|
workspaceSetupState: 'present',
|
|
415
|
-
runtimeMode: '
|
|
416
|
-
workspaceId: '
|
|
852
|
+
runtimeMode: 'cloud',
|
|
853
|
+
workspaceId: 'default',
|
|
417
854
|
mode: 'core',
|
|
418
855
|
forceSetup: false,
|
|
419
856
|
forceGlobalSetup: false,
|
|
420
857
|
forceWorkspaceSetup: false
|
|
858
|
+
},
|
|
859
|
+
runtimeCapabilities: {
|
|
860
|
+
runtimeMode: 'cloud',
|
|
861
|
+
supportsCloudAuth: true,
|
|
862
|
+
supportsAdminManagedSetupOverrides: false,
|
|
863
|
+
supportsPackageScriptSetupOverrides: false
|
|
421
864
|
}
|
|
422
865
|
})
|
|
423
866
|
});
|
|
424
867
|
}
|
|
868
|
+
if (url.includes('/api/taskforce/tasks')) {
|
|
869
|
+
return Promise.resolve({ ok: false, status: 401, json: () => Promise.resolve({ error: 'Unauthorized' }) });
|
|
870
|
+
}
|
|
425
871
|
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
426
872
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
427
873
|
}
|
|
428
874
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
429
875
|
});
|
|
430
876
|
vi.stubGlobal('fetch', fetchMock);
|
|
431
|
-
const { container } = render(_jsx(MemoryRouter, { initialEntries: ['/
|
|
877
|
+
const { container } = render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, {}) }));
|
|
432
878
|
await waitFor(() => {
|
|
433
|
-
expect(screen.getByText('Create your Taskforce account.')).toBeInTheDocument();
|
|
434
879
|
expect(screen.getByPlaceholderText('Email')).toBeInTheDocument();
|
|
435
|
-
expect(screen.getByRole('button', { name: 'Create Account' })).toBeInTheDocument();
|
|
436
880
|
expect(screen.getByRole('button', { name: 'Sign In' })).toBeInTheDocument();
|
|
437
|
-
expect(screen.
|
|
438
|
-
expect(screen.
|
|
881
|
+
expect(screen.getByRole('button', { name: 'Register' })).toBeInTheDocument();
|
|
882
|
+
expect(screen.queryByRole('button', { name: 'Create Account' })).not.toBeInTheDocument();
|
|
883
|
+
expect(screen.queryByRole('button', { name: 'Close sign in' })).not.toBeInTheDocument();
|
|
439
884
|
});
|
|
440
885
|
expect(container.querySelector('[aria-pressed]')).not.toBeInTheDocument();
|
|
441
886
|
});
|
|
442
|
-
it('allows
|
|
443
|
-
const user = userEvent.setup();
|
|
887
|
+
it('allows public access to /pricing in cloud mode without login redirect', async () => {
|
|
444
888
|
const fetchMock = vi.fn((url) => {
|
|
445
889
|
if (url.includes('/api/taskforce/auth/session')) {
|
|
446
890
|
return Promise.resolve({
|
|
447
891
|
ok: true,
|
|
448
892
|
json: () => Promise.resolve({
|
|
449
893
|
authenticated: false,
|
|
450
|
-
runtimeMode: '
|
|
451
|
-
authRequiredForApi:
|
|
894
|
+
runtimeMode: 'cloud',
|
|
895
|
+
authRequiredForApi: true
|
|
452
896
|
})
|
|
453
897
|
});
|
|
454
898
|
}
|
|
@@ -457,237 +901,277 @@ describe('TaskforceCore Integration', () => {
|
|
|
457
901
|
ok: true,
|
|
458
902
|
json: () => Promise.resolve({
|
|
459
903
|
...mockConfig,
|
|
460
|
-
runtimeMode: '
|
|
461
|
-
authRequiredForApi:
|
|
904
|
+
runtimeMode: 'cloud',
|
|
905
|
+
authRequiredForApi: true,
|
|
462
906
|
setupState: {
|
|
463
907
|
globalSetupState: 'present',
|
|
464
908
|
workspaceSetupState: 'present',
|
|
465
|
-
runtimeMode: '
|
|
466
|
-
workspaceId: '
|
|
909
|
+
runtimeMode: 'cloud',
|
|
910
|
+
workspaceId: 'default',
|
|
467
911
|
mode: 'core',
|
|
468
912
|
forceSetup: false,
|
|
469
913
|
forceGlobalSetup: false,
|
|
470
914
|
forceWorkspaceSetup: false
|
|
915
|
+
},
|
|
916
|
+
runtimeCapabilities: {
|
|
917
|
+
runtimeMode: 'cloud',
|
|
918
|
+
supportsCloudAuth: true,
|
|
919
|
+
supportsAdminManagedSetupOverrides: false,
|
|
920
|
+
supportsPackageScriptSetupOverrides: false
|
|
471
921
|
}
|
|
472
922
|
})
|
|
473
923
|
});
|
|
474
924
|
}
|
|
475
|
-
if (url.includes('/api/taskforce/
|
|
476
|
-
return Promise.resolve({
|
|
925
|
+
if (url.includes('/api/taskforce/public/pricing')) {
|
|
926
|
+
return Promise.resolve({
|
|
927
|
+
ok: true,
|
|
928
|
+
json: () => Promise.resolve({
|
|
929
|
+
plans: [
|
|
930
|
+
{
|
|
931
|
+
planId: 'pro',
|
|
932
|
+
displayName: 'Pro Plan test mode',
|
|
933
|
+
status: 'active',
|
|
934
|
+
features: [],
|
|
935
|
+
pricing: { month: null, year: null },
|
|
936
|
+
defaultVersion: null
|
|
937
|
+
}
|
|
938
|
+
]
|
|
939
|
+
})
|
|
940
|
+
});
|
|
477
941
|
}
|
|
478
942
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
479
943
|
});
|
|
480
944
|
vi.stubGlobal('fetch', fetchMock);
|
|
481
|
-
render(_jsx(MemoryRouter, { initialEntries: ['/
|
|
482
|
-
await waitFor(() => {
|
|
483
|
-
expect(screen.getByText('Request a password reset link.')).toBeInTheDocument();
|
|
484
|
-
expect(screen.getByRole('button', { name: 'Sign In' })).toBeInTheDocument();
|
|
485
|
-
expect(screen.getByRole('button', { name: 'Register' })).toBeInTheDocument();
|
|
486
|
-
expect(screen.queryByRole('button', { name: 'Accept Invite' })).not.toBeInTheDocument();
|
|
487
|
-
expect(screen.queryByRole('button', { name: /Resend Verification/ })).not.toBeInTheDocument();
|
|
488
|
-
});
|
|
489
|
-
await user.click(screen.getByRole('button', { name: 'Sign In' }));
|
|
945
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/pricing'], children: _jsx(TaskforceCore, {}) }));
|
|
490
946
|
await waitFor(() => {
|
|
491
|
-
expect(screen.
|
|
492
|
-
expect(screen.
|
|
947
|
+
expect(screen.queryByPlaceholderText('Email')).not.toBeInTheDocument();
|
|
948
|
+
expect(screen.getByText('Pro Plan test mode')).toBeInTheDocument();
|
|
949
|
+
expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
|
|
493
950
|
});
|
|
494
951
|
});
|
|
495
|
-
it('
|
|
496
|
-
const
|
|
497
|
-
let selectedMode = null;
|
|
498
|
-
const fetchMock = vi.fn((url, init) => {
|
|
952
|
+
it('redirects authenticated users with pending plan selection to the plans page', async () => {
|
|
953
|
+
const fetchMock = vi.fn((url) => {
|
|
499
954
|
if (url.includes('/api/taskforce/auth/session')) {
|
|
500
955
|
return Promise.resolve({
|
|
501
956
|
ok: true,
|
|
502
957
|
json: () => Promise.resolve({
|
|
503
958
|
authenticated: true,
|
|
504
|
-
runtimeMode: '
|
|
505
|
-
authRequiredForApi:
|
|
959
|
+
runtimeMode: 'cloud',
|
|
960
|
+
authRequiredForApi: true
|
|
506
961
|
})
|
|
507
962
|
});
|
|
508
963
|
}
|
|
509
964
|
if (url.includes('/api/taskforce/config')) {
|
|
510
|
-
const mode = selectedMode;
|
|
511
965
|
return Promise.resolve({
|
|
512
966
|
ok: true,
|
|
513
967
|
json: () => Promise.resolve({
|
|
514
968
|
...mockConfig,
|
|
515
|
-
runtimeMode: '
|
|
516
|
-
authRequiredForApi:
|
|
969
|
+
runtimeMode: 'cloud',
|
|
970
|
+
authRequiredForApi: true,
|
|
971
|
+
workspaceSwitchingEnabled: true,
|
|
517
972
|
setupState: {
|
|
518
|
-
globalSetupState:
|
|
973
|
+
globalSetupState: 'present',
|
|
519
974
|
workspaceSetupState: 'present',
|
|
520
|
-
runtimeMode: '
|
|
521
|
-
workspaceId: '
|
|
522
|
-
mode:
|
|
975
|
+
runtimeMode: 'cloud',
|
|
976
|
+
workspaceId: 'workspace-1',
|
|
977
|
+
mode: 'core',
|
|
523
978
|
forceSetup: false,
|
|
524
|
-
forceGlobalSetup:
|
|
979
|
+
forceGlobalSetup: false,
|
|
525
980
|
forceWorkspaceSetup: false
|
|
526
|
-
},
|
|
527
|
-
runtimeCapabilities: {
|
|
528
|
-
runtimeMode: 'local',
|
|
529
|
-
supportsCloudAuth: false,
|
|
530
|
-
supportsAdminManagedSetupOverrides: false,
|
|
531
|
-
supportsPackageScriptSetupOverrides: true
|
|
532
981
|
}
|
|
533
982
|
})
|
|
534
983
|
});
|
|
535
984
|
}
|
|
536
|
-
if (url.includes('/api/taskforce/
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
985
|
+
if (url.includes('/api/taskforce/workspaces')) {
|
|
986
|
+
return Promise.resolve({
|
|
987
|
+
ok: true,
|
|
988
|
+
json: () => Promise.resolve({
|
|
989
|
+
workspaces: [
|
|
990
|
+
{
|
|
991
|
+
id: 'workspace-1',
|
|
992
|
+
slug: 'workspace-1',
|
|
993
|
+
name: 'Workspace 1',
|
|
994
|
+
description: null,
|
|
995
|
+
role: 'owner',
|
|
996
|
+
status: 'active',
|
|
997
|
+
betaAccess: true
|
|
998
|
+
}
|
|
999
|
+
]
|
|
1000
|
+
})
|
|
1001
|
+
});
|
|
540
1002
|
}
|
|
541
|
-
if (url.includes('/api/taskforce/
|
|
542
|
-
return Promise.resolve({
|
|
1003
|
+
if (url.includes('/api/taskforce/account/access-status')) {
|
|
1004
|
+
return Promise.resolve({
|
|
1005
|
+
ok: true,
|
|
1006
|
+
json: () => Promise.resolve({
|
|
1007
|
+
gate: 'plan_selection_required',
|
|
1008
|
+
hasEntitlement: false,
|
|
1009
|
+
canAccessApp: false,
|
|
1010
|
+
canAccessPlans: true,
|
|
1011
|
+
message: 'Choose a plan to continue.'
|
|
1012
|
+
})
|
|
1013
|
+
});
|
|
543
1014
|
}
|
|
544
|
-
if (url.includes('/api/taskforce/
|
|
545
|
-
return Promise.resolve({
|
|
1015
|
+
if (url.includes('/api/taskforce/public/pricing')) {
|
|
1016
|
+
return Promise.resolve({
|
|
1017
|
+
ok: true,
|
|
1018
|
+
json: () => Promise.resolve({
|
|
1019
|
+
plans: [
|
|
1020
|
+
{
|
|
1021
|
+
planId: 'starter',
|
|
1022
|
+
displayName: 'Starter',
|
|
1023
|
+
status: 'active',
|
|
1024
|
+
features: [],
|
|
1025
|
+
pricing: {
|
|
1026
|
+
month: { pricingType: 'free', stripePriceId: null, unitAmount: 0, currency: null, interval: 'month' },
|
|
1027
|
+
year: null
|
|
1028
|
+
},
|
|
1029
|
+
defaultVersion: {
|
|
1030
|
+
planVersionId: 'starter-v1',
|
|
1031
|
+
versionNumber: 1,
|
|
1032
|
+
seatLimit: null,
|
|
1033
|
+
metadata: {}
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
]
|
|
1037
|
+
})
|
|
1038
|
+
});
|
|
546
1039
|
}
|
|
547
|
-
|
|
548
|
-
});
|
|
549
|
-
vi.stubGlobal('fetch', fetchMock);
|
|
550
|
-
render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, {}) }));
|
|
551
|
-
await waitFor(() => {
|
|
552
|
-
expect(screen.getByText('Global Setup Required')).toBeInTheDocument();
|
|
553
|
-
});
|
|
554
|
-
await user.click(screen.getByRole('radio', { name: /operations mode/i }));
|
|
555
|
-
await user.click(screen.getByRole('button', { name: 'Save Global Setup' }));
|
|
556
|
-
await waitFor(() => {
|
|
557
|
-
expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
|
|
558
|
-
});
|
|
559
|
-
expect(fetchMock).toHaveBeenCalledWith('/api/taskforce/global-settings', expect.objectContaining({ method: 'POST' }));
|
|
560
|
-
});
|
|
561
|
-
it('shows workspace setup route when required and persists workspace profile', async () => {
|
|
562
|
-
const user = userEvent.setup();
|
|
563
|
-
let workspaceSaved = false;
|
|
564
|
-
const fetchMock = vi.fn((url, init) => {
|
|
565
|
-
if (url.includes('/api/taskforce/auth/session')) {
|
|
1040
|
+
if (url.includes('/api/taskforce/billing/status')) {
|
|
566
1041
|
return Promise.resolve({
|
|
567
1042
|
ok: true,
|
|
568
1043
|
json: () => Promise.resolve({
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
1044
|
+
planId: 'starter',
|
|
1045
|
+
planName: 'Starter',
|
|
1046
|
+
entitlementState: 'active'
|
|
572
1047
|
})
|
|
573
1048
|
});
|
|
574
1049
|
}
|
|
575
|
-
if (url.includes('/api/taskforce/
|
|
1050
|
+
if (url.includes('/api/taskforce/billing/status')) {
|
|
576
1051
|
return Promise.resolve({
|
|
577
1052
|
ok: true,
|
|
578
1053
|
json: () => Promise.resolve({
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
setupState: {
|
|
583
|
-
globalSetupState: 'present',
|
|
584
|
-
workspaceSetupState: workspaceSaved ? 'present' : 'missing',
|
|
585
|
-
runtimeMode: 'local',
|
|
586
|
-
workspaceId: 'default',
|
|
587
|
-
mode: 'core',
|
|
588
|
-
forceSetup: false,
|
|
589
|
-
forceGlobalSetup: false,
|
|
590
|
-
forceWorkspaceSetup: true,
|
|
591
|
-
workspace: workspaceSaved ? { id: 'default', slug: 'default', name: 'Delta Project', description: 'Workspace desc' } : null,
|
|
592
|
-
suggestedWorkspaceName: 'Suggested Project'
|
|
593
|
-
},
|
|
594
|
-
runtimeCapabilities: {
|
|
595
|
-
runtimeMode: 'local',
|
|
596
|
-
supportsCloudAuth: false,
|
|
597
|
-
supportsAdminManagedSetupOverrides: false,
|
|
598
|
-
supportsPackageScriptSetupOverrides: true
|
|
599
|
-
}
|
|
1054
|
+
planId: 'starter',
|
|
1055
|
+
planName: 'Starter',
|
|
1056
|
+
entitlementState: 'active'
|
|
600
1057
|
})
|
|
601
1058
|
});
|
|
602
1059
|
}
|
|
603
|
-
if (url.includes('/api/taskforce/
|
|
604
|
-
|
|
605
|
-
if (body?.name === 'Delta Project')
|
|
606
|
-
workspaceSaved = true;
|
|
607
|
-
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
|
|
1060
|
+
if (url.includes('/api/taskforce/categories')) {
|
|
1061
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve(mockCategories) });
|
|
608
1062
|
}
|
|
609
1063
|
if (url.includes('/api/taskforce/tasks')) {
|
|
610
1064
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
|
|
611
1065
|
}
|
|
612
|
-
if (url.includes('/api/taskforce/
|
|
613
|
-
return Promise.resolve({ ok: true, json: () => Promise.resolve({
|
|
1066
|
+
if (url.includes('/api/taskforce/types')) {
|
|
1067
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ types: [] }) });
|
|
1068
|
+
}
|
|
1069
|
+
if (url.includes('/api/taskforce/priorities')) {
|
|
1070
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ priorities: [] }) });
|
|
1071
|
+
}
|
|
1072
|
+
if (url.includes('/api/taskforce/approaches')) {
|
|
1073
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ approaches: [] }) });
|
|
1074
|
+
}
|
|
1075
|
+
if (url.includes('/api/taskforce/specialists')) {
|
|
1076
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
|
|
614
1077
|
}
|
|
615
1078
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
616
1079
|
});
|
|
617
1080
|
vi.stubGlobal('fetch', fetchMock);
|
|
618
1081
|
render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, {}) }));
|
|
619
1082
|
await waitFor(() => {
|
|
620
|
-
expect(screen.
|
|
621
|
-
|
|
622
|
-
const nameInput = screen.getByPlaceholderText('Project name');
|
|
623
|
-
await user.clear(nameInput);
|
|
624
|
-
await user.type(nameInput, 'Delta Project');
|
|
625
|
-
await user.type(screen.getByPlaceholderText('Project description (optional)'), 'Workspace desc');
|
|
626
|
-
await user.click(screen.getByRole('button', { name: 'Save Project Setup' }));
|
|
627
|
-
await waitFor(() => {
|
|
628
|
-
expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
|
|
629
|
-
});
|
|
630
|
-
expect(fetchMock).toHaveBeenCalledWith('/api/taskforce/workspace-profile', expect.objectContaining({ method: 'POST' }));
|
|
631
|
-
const workspaceSyncStateCall = fetchMock.mock.calls.find((call) => {
|
|
632
|
-
if (!String(call[0]).includes('/api/taskforce/ui-state'))
|
|
633
|
-
return false;
|
|
634
|
-
const init = call[1];
|
|
635
|
-
if (String(init?.method || '').toUpperCase() !== 'POST')
|
|
636
|
-
return false;
|
|
637
|
-
const body = JSON.parse(String(init?.body || '{}'));
|
|
638
|
-
return body?.stateKey === 'workspace-sync';
|
|
1083
|
+
expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
|
|
1084
|
+
expect(screen.getByText('Choose a plan to continue.')).toBeInTheDocument();
|
|
639
1085
|
});
|
|
640
|
-
const workspaceSyncStateBody = JSON.parse(String(workspaceSyncStateCall?.[1]?.body || '{}'));
|
|
641
|
-
expect(workspaceSyncStateBody.stateKey).toBe('workspace-sync');
|
|
642
|
-
expect(workspaceSyncStateBody.patch?.cloudSyncEnabled).toBe(false);
|
|
643
1086
|
});
|
|
644
|
-
it('
|
|
645
|
-
let configCalls = 0;
|
|
1087
|
+
it('redirects authenticated users with a blocking account access gate to the plans page', async () => {
|
|
646
1088
|
const fetchMock = vi.fn((url) => {
|
|
647
1089
|
if (url.includes('/api/taskforce/auth/session')) {
|
|
648
1090
|
return Promise.resolve({
|
|
649
1091
|
ok: true,
|
|
650
1092
|
json: () => Promise.resolve({
|
|
651
|
-
authenticated:
|
|
652
|
-
runtimeMode: '
|
|
653
|
-
authRequiredForApi:
|
|
1093
|
+
authenticated: true,
|
|
1094
|
+
runtimeMode: 'cloud',
|
|
1095
|
+
authRequiredForApi: true
|
|
654
1096
|
})
|
|
655
1097
|
});
|
|
656
1098
|
}
|
|
657
1099
|
if (url.includes('/api/taskforce/config')) {
|
|
658
|
-
configCalls += 1;
|
|
659
|
-
const firstPass = configCalls === 1;
|
|
660
1100
|
return Promise.resolve({
|
|
661
1101
|
ok: true,
|
|
662
1102
|
json: () => Promise.resolve({
|
|
663
1103
|
...mockConfig,
|
|
664
|
-
runtimeMode: '
|
|
665
|
-
authRequiredForApi:
|
|
666
|
-
|
|
667
|
-
workspaceId: 'workspace-local-refresh',
|
|
1104
|
+
runtimeMode: 'cloud',
|
|
1105
|
+
authRequiredForApi: true,
|
|
1106
|
+
workspaceSwitchingEnabled: true,
|
|
668
1107
|
setupState: {
|
|
669
1108
|
globalSetupState: 'present',
|
|
670
|
-
workspaceSetupState:
|
|
671
|
-
runtimeMode: '
|
|
672
|
-
workspaceId: 'workspace-
|
|
1109
|
+
workspaceSetupState: 'present',
|
|
1110
|
+
runtimeMode: 'cloud',
|
|
1111
|
+
workspaceId: 'workspace-1',
|
|
673
1112
|
mode: 'core',
|
|
674
1113
|
forceSetup: false,
|
|
675
1114
|
forceGlobalSetup: false,
|
|
676
|
-
forceWorkspaceSetup:
|
|
677
|
-
workspace: firstPass
|
|
678
|
-
? null
|
|
679
|
-
: { id: 'workspace-local-refresh', slug: 'workspace-local-refresh', name: 'Local Refresh Project', description: null },
|
|
680
|
-
suggestedWorkspaceName: 'Local Refresh Project'
|
|
681
|
-
},
|
|
682
|
-
runtimeCapabilities: {
|
|
683
|
-
runtimeMode: 'local',
|
|
684
|
-
supportsCloudAuth: true,
|
|
685
|
-
supportsAdminManagedSetupOverrides: false,
|
|
686
|
-
supportsPackageScriptSetupOverrides: true
|
|
1115
|
+
forceWorkspaceSetup: false
|
|
687
1116
|
}
|
|
688
1117
|
})
|
|
689
1118
|
});
|
|
690
1119
|
}
|
|
1120
|
+
if (url.includes('/api/taskforce/workspaces')) {
|
|
1121
|
+
return Promise.resolve({
|
|
1122
|
+
ok: true,
|
|
1123
|
+
json: () => Promise.resolve({
|
|
1124
|
+
workspaces: [
|
|
1125
|
+
{
|
|
1126
|
+
id: 'workspace-1',
|
|
1127
|
+
slug: 'workspace-1',
|
|
1128
|
+
name: 'Workspace 1',
|
|
1129
|
+
description: null,
|
|
1130
|
+
role: 'owner',
|
|
1131
|
+
status: 'active',
|
|
1132
|
+
betaAccess: true
|
|
1133
|
+
}
|
|
1134
|
+
]
|
|
1135
|
+
})
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
if (url.includes('/api/taskforce/account/access-status')) {
|
|
1139
|
+
return Promise.resolve({
|
|
1140
|
+
ok: true,
|
|
1141
|
+
json: () => Promise.resolve({
|
|
1142
|
+
gate: 'misconfigured',
|
|
1143
|
+
hasEntitlement: false,
|
|
1144
|
+
canAccessApp: false,
|
|
1145
|
+
canAccessPlans: true,
|
|
1146
|
+
message: 'Signup monetization is misconfigured.'
|
|
1147
|
+
})
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
if (url.includes('/api/taskforce/public/pricing')) {
|
|
1151
|
+
return Promise.resolve({
|
|
1152
|
+
ok: true,
|
|
1153
|
+
json: () => Promise.resolve({
|
|
1154
|
+
plans: [
|
|
1155
|
+
{
|
|
1156
|
+
planId: 'starter',
|
|
1157
|
+
displayName: 'Starter',
|
|
1158
|
+
status: 'active',
|
|
1159
|
+
features: [],
|
|
1160
|
+
pricing: {
|
|
1161
|
+
month: { pricingType: 'free', stripePriceId: null, unitAmount: 0, currency: null, interval: 'month' },
|
|
1162
|
+
year: null
|
|
1163
|
+
},
|
|
1164
|
+
defaultVersion: {
|
|
1165
|
+
planVersionId: 'starter-v1',
|
|
1166
|
+
versionNumber: 1,
|
|
1167
|
+
seatLimit: null,
|
|
1168
|
+
metadata: {}
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
]
|
|
1172
|
+
})
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
691
1175
|
if (url.includes('/api/taskforce/categories')) {
|
|
692
1176
|
return Promise.resolve({ ok: true, json: () => Promise.resolve(mockCategories) });
|
|
693
1177
|
}
|
|
@@ -706,30 +1190,24 @@ describe('TaskforceCore Integration', () => {
|
|
|
706
1190
|
if (url.includes('/api/taskforce/specialists')) {
|
|
707
1191
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
|
|
708
1192
|
}
|
|
709
|
-
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
710
|
-
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
711
|
-
}
|
|
712
1193
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
713
1194
|
});
|
|
714
1195
|
vi.stubGlobal('fetch', fetchMock);
|
|
715
1196
|
render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, {}) }));
|
|
716
1197
|
await waitFor(() => {
|
|
717
|
-
expect(
|
|
1198
|
+
expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
|
|
1199
|
+
expect(screen.getByText('Signup monetization is misconfigured. Contact support or a system administrator.')).toBeInTheDocument();
|
|
718
1200
|
});
|
|
719
|
-
expect(screen.getByRole('button', { name: /add task/i })).toBeInTheDocument();
|
|
720
|
-
expect(screen.queryByText('Workspace Setup Required')).not.toBeInTheDocument();
|
|
721
1201
|
});
|
|
722
|
-
it('
|
|
723
|
-
const
|
|
724
|
-
let workspaceSaved = false;
|
|
725
|
-
const fetchMock = vi.fn((url, init) => {
|
|
1202
|
+
it('suppresses stale missing-entitlement gate messaging when billing status is already active', async () => {
|
|
1203
|
+
const fetchMock = vi.fn((url) => {
|
|
726
1204
|
if (url.includes('/api/taskforce/auth/session')) {
|
|
727
1205
|
return Promise.resolve({
|
|
728
1206
|
ok: true,
|
|
729
1207
|
json: () => Promise.resolve({
|
|
730
1208
|
authenticated: true,
|
|
731
|
-
runtimeMode: '
|
|
732
|
-
authRequiredForApi:
|
|
1209
|
+
runtimeMode: 'cloud',
|
|
1210
|
+
authRequiredForApi: true
|
|
733
1211
|
})
|
|
734
1212
|
});
|
|
735
1213
|
}
|
|
@@ -738,116 +1216,154 @@ describe('TaskforceCore Integration', () => {
|
|
|
738
1216
|
ok: true,
|
|
739
1217
|
json: () => Promise.resolve({
|
|
740
1218
|
...mockConfig,
|
|
741
|
-
runtimeMode: '
|
|
742
|
-
authRequiredForApi:
|
|
1219
|
+
runtimeMode: 'cloud',
|
|
1220
|
+
authRequiredForApi: true,
|
|
1221
|
+
workspaceSwitchingEnabled: true,
|
|
743
1222
|
setupState: {
|
|
744
1223
|
globalSetupState: 'present',
|
|
745
|
-
workspaceSetupState:
|
|
746
|
-
runtimeMode: '
|
|
747
|
-
workspaceId:
|
|
1224
|
+
workspaceSetupState: 'present',
|
|
1225
|
+
runtimeMode: 'cloud',
|
|
1226
|
+
workspaceId: 'workspace-1',
|
|
748
1227
|
mode: 'core',
|
|
749
1228
|
forceSetup: false,
|
|
750
1229
|
forceGlobalSetup: false,
|
|
751
|
-
forceWorkspaceSetup:
|
|
752
|
-
workspace: workspaceSaved
|
|
753
|
-
? { id: 'workspace-cloud-123', slug: 'workspace-cloud-123', name: 'Cloud Project 123', description: 'Cloud desc' }
|
|
754
|
-
: null,
|
|
755
|
-
suggestedWorkspaceName: 'Suggested Project'
|
|
756
|
-
},
|
|
757
|
-
runtimeCapabilities: {
|
|
758
|
-
runtimeMode: 'local',
|
|
759
|
-
supportsCloudAuth: true,
|
|
760
|
-
supportsAdminManagedSetupOverrides: false,
|
|
761
|
-
supportsPackageScriptSetupOverrides: true
|
|
1230
|
+
forceWorkspaceSetup: false
|
|
762
1231
|
}
|
|
763
1232
|
})
|
|
764
1233
|
});
|
|
765
1234
|
}
|
|
766
|
-
if (url.includes('
|
|
1235
|
+
if (url.includes('/api/taskforce/workspaces')) {
|
|
767
1236
|
return Promise.resolve({
|
|
768
1237
|
ok: true,
|
|
769
1238
|
json: () => Promise.resolve({
|
|
770
|
-
success: true,
|
|
771
1239
|
workspaces: [
|
|
772
1240
|
{
|
|
773
|
-
id: 'workspace-
|
|
774
|
-
|
|
775
|
-
|
|
1241
|
+
id: 'workspace-1',
|
|
1242
|
+
slug: 'workspace-1',
|
|
1243
|
+
name: 'Workspace 1',
|
|
1244
|
+
description: null,
|
|
1245
|
+
role: 'owner',
|
|
1246
|
+
status: 'active',
|
|
1247
|
+
betaAccess: true
|
|
776
1248
|
}
|
|
777
1249
|
]
|
|
778
1250
|
})
|
|
779
1251
|
});
|
|
780
1252
|
}
|
|
781
|
-
if (url.includes('/api/taskforce/
|
|
782
|
-
const body = JSON.parse(String(init?.body || '{}'));
|
|
783
|
-
if (body?.workspaceId === 'workspace-cloud-123')
|
|
784
|
-
workspaceSaved = true;
|
|
1253
|
+
if (url.includes('/api/taskforce/account/access-status')) {
|
|
785
1254
|
return Promise.resolve({
|
|
786
1255
|
ok: true,
|
|
787
1256
|
json: () => Promise.resolve({
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
1257
|
+
gate: 'missing_entitlement',
|
|
1258
|
+
hasEntitlement: false,
|
|
1259
|
+
canAccessApp: false,
|
|
1260
|
+
canAccessPlans: true,
|
|
1261
|
+
message: 'No entitlement is linked to this account.'
|
|
792
1262
|
})
|
|
793
1263
|
});
|
|
794
1264
|
}
|
|
795
|
-
if (url.includes('/api/taskforce/
|
|
796
|
-
return Promise.resolve({
|
|
1265
|
+
if (url.includes('/api/taskforce/public/pricing')) {
|
|
1266
|
+
return Promise.resolve({
|
|
1267
|
+
ok: true,
|
|
1268
|
+
json: () => Promise.resolve({
|
|
1269
|
+
plans: [
|
|
1270
|
+
{
|
|
1271
|
+
planId: 'starter',
|
|
1272
|
+
displayName: 'Starter',
|
|
1273
|
+
status: 'active',
|
|
1274
|
+
features: [],
|
|
1275
|
+
pricing: {
|
|
1276
|
+
month: { pricingType: 'free', stripePriceId: null, unitAmount: 0, currency: null, interval: 'month' },
|
|
1277
|
+
year: null
|
|
1278
|
+
},
|
|
1279
|
+
defaultVersion: {
|
|
1280
|
+
planVersionId: 'starter-v1',
|
|
1281
|
+
versionNumber: 1,
|
|
1282
|
+
seatLimit: null,
|
|
1283
|
+
metadata: {}
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
]
|
|
1287
|
+
})
|
|
1288
|
+
});
|
|
797
1289
|
}
|
|
798
|
-
if (url.includes('/api/taskforce/
|
|
799
|
-
return Promise.resolve({
|
|
1290
|
+
if (url.includes('/api/taskforce/billing/status')) {
|
|
1291
|
+
return Promise.resolve({
|
|
1292
|
+
ok: true,
|
|
1293
|
+
json: () => Promise.resolve({
|
|
1294
|
+
planId: 'starter',
|
|
1295
|
+
planName: 'Starter',
|
|
1296
|
+
planVersionId: 'starter-v1',
|
|
1297
|
+
entitlementState: 'active'
|
|
1298
|
+
})
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
if (url.includes('/api/taskforce/categories')) {
|
|
1302
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve(mockCategories) });
|
|
800
1303
|
}
|
|
801
1304
|
if (url.includes('/api/taskforce/tasks')) {
|
|
802
1305
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
|
|
803
1306
|
}
|
|
804
|
-
if (url.includes('/api/taskforce/
|
|
805
|
-
return Promise.resolve({ ok: true, json: () => Promise.resolve({
|
|
1307
|
+
if (url.includes('/api/taskforce/types')) {
|
|
1308
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ types: [] }) });
|
|
806
1309
|
}
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
1310
|
+
if (url.includes('/api/taskforce/priorities')) {
|
|
1311
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ priorities: [] }) });
|
|
1312
|
+
}
|
|
1313
|
+
if (url.includes('/api/taskforce/approaches')) {
|
|
1314
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ approaches: [] }) });
|
|
1315
|
+
}
|
|
1316
|
+
if (url.includes('/api/taskforce/specialists')) {
|
|
1317
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
|
|
1318
|
+
}
|
|
1319
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
1320
|
+
});
|
|
1321
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
1322
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/?screen=plans&gate=missing_entitlement'], children: _jsx(TaskforceCore, {}) }));
|
|
815
1323
|
await waitFor(() => {
|
|
816
|
-
expect(screen.getByRole('
|
|
1324
|
+
expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
|
|
1325
|
+
expect(screen.getByText('Starter')).toBeInTheDocument();
|
|
817
1326
|
});
|
|
818
|
-
await user.selectOptions(screen.getByRole('combobox'), 'workspace-cloud-123');
|
|
819
|
-
await user.click(screen.getByRole('button', { name: 'Save Project Setup' }));
|
|
820
1327
|
await waitFor(() => {
|
|
821
|
-
expect(screen.
|
|
1328
|
+
expect(screen.queryByText('No entitlement is linked to this account.')).not.toBeInTheDocument();
|
|
822
1329
|
});
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
const
|
|
827
|
-
if (
|
|
828
|
-
return
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
return false;
|
|
832
|
-
const body = JSON.parse(String(init?.body || '{}'));
|
|
833
|
-
return body?.stateKey === 'workspace-sync';
|
|
1330
|
+
});
|
|
1331
|
+
it('shows updated setup-bootstrap copy while startup checks are in progress', async () => {
|
|
1332
|
+
const hangingConfig = new Promise(() => { });
|
|
1333
|
+
const fetchMock = vi.fn((url) => {
|
|
1334
|
+
if (url.includes('/api/taskforce/config')) {
|
|
1335
|
+
return hangingConfig;
|
|
1336
|
+
}
|
|
1337
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
834
1338
|
});
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
1339
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
1340
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/setup?step=workspace'], children: _jsx(TaskforceCore, {}) }));
|
|
1341
|
+
await waitFor(() => {
|
|
1342
|
+
expect(screen.getByText('Loading Taskforce')).toBeInTheDocument();
|
|
1343
|
+
expect(screen.getByText(/Verifying session|Checking account, workspace access, and setup status/)).toBeInTheDocument();
|
|
1344
|
+
});
|
|
1345
|
+
expect(screen.queryByText('Loading Setup')).not.toBeInTheDocument();
|
|
839
1346
|
});
|
|
840
|
-
it('
|
|
1347
|
+
it('keeps account menu in bootstrap-pending state until runtime config resolves, then shows signed-in identity immediately', async () => {
|
|
841
1348
|
const user = userEvent.setup();
|
|
842
|
-
let
|
|
843
|
-
const fetchMock = vi.fn((url
|
|
844
|
-
if (url.includes('/api/taskforce/auth/
|
|
1349
|
+
let resolveRuntimeConfig = null;
|
|
1350
|
+
const fetchMock = vi.fn((url) => {
|
|
1351
|
+
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
1352
|
+
return new Promise((resolve) => {
|
|
1353
|
+
resolveRuntimeConfig = resolve;
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
if (url.includes('https://cloud.example.com/api/taskforce/auth/session')) {
|
|
845
1357
|
return Promise.resolve({
|
|
846
1358
|
ok: true,
|
|
847
1359
|
json: () => Promise.resolve({
|
|
848
1360
|
authenticated: true,
|
|
849
1361
|
runtimeMode: 'local',
|
|
850
|
-
authRequiredForApi: false
|
|
1362
|
+
authRequiredForApi: false,
|
|
1363
|
+
email: 'member@example.com',
|
|
1364
|
+
userId: 'user-member',
|
|
1365
|
+
workspaceId: 'workspace-local-active',
|
|
1366
|
+
betaAccess: true
|
|
851
1367
|
})
|
|
852
1368
|
});
|
|
853
1369
|
}
|
|
@@ -858,101 +1374,115 @@ describe('TaskforceCore Integration', () => {
|
|
|
858
1374
|
...mockConfig,
|
|
859
1375
|
runtimeMode: 'local',
|
|
860
1376
|
authRequiredForApi: false,
|
|
1377
|
+
workspaceId: 'workspace-local-active',
|
|
861
1378
|
setupState: {
|
|
862
1379
|
globalSetupState: 'present',
|
|
863
|
-
workspaceSetupState:
|
|
1380
|
+
workspaceSetupState: 'present',
|
|
864
1381
|
runtimeMode: 'local',
|
|
865
|
-
workspaceId:
|
|
1382
|
+
workspaceId: 'workspace-local-active',
|
|
866
1383
|
mode: 'core',
|
|
867
1384
|
forceSetup: false,
|
|
868
1385
|
forceGlobalSetup: false,
|
|
869
|
-
forceWorkspaceSetup:
|
|
870
|
-
workspace: workspaceSaved
|
|
871
|
-
? { id: 'workspace-local-setup-1', slug: 'workspace-local-setup-1', name: 'Local Project', description: 'Local desc' }
|
|
872
|
-
: null,
|
|
873
|
-
suggestedWorkspaceName: 'Suggested Project'
|
|
874
|
-
},
|
|
875
|
-
runtimeCapabilities: {
|
|
876
|
-
runtimeMode: 'local',
|
|
877
|
-
supportsCloudAuth: true,
|
|
878
|
-
supportsAdminManagedSetupOverrides: false,
|
|
879
|
-
supportsPackageScriptSetupOverrides: true
|
|
1386
|
+
forceWorkspaceSetup: false
|
|
880
1387
|
}
|
|
881
1388
|
})
|
|
882
1389
|
});
|
|
883
1390
|
}
|
|
884
|
-
if (url.includes('/api/taskforce/
|
|
885
|
-
workspaceSaved = true;
|
|
1391
|
+
if (url.includes('/api/taskforce/billing/status')) {
|
|
886
1392
|
return Promise.resolve({
|
|
887
1393
|
ok: true,
|
|
888
1394
|
json: () => Promise.resolve({
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
}
|
|
1395
|
+
planId: 'starter',
|
|
1396
|
+
planName: 'Starter',
|
|
1397
|
+
entitlementState: 'active'
|
|
893
1398
|
})
|
|
894
1399
|
});
|
|
895
1400
|
}
|
|
896
|
-
if (url.includes('/api/taskforce/
|
|
897
|
-
return Promise.resolve({ ok: true, json: () => Promise.resolve(
|
|
1401
|
+
if (url.includes('/api/taskforce/categories')) {
|
|
1402
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve(mockCategories) });
|
|
898
1403
|
}
|
|
899
1404
|
if (url.includes('/api/taskforce/tasks')) {
|
|
900
1405
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
|
|
901
1406
|
}
|
|
1407
|
+
if (url.includes('/api/taskforce/types')) {
|
|
1408
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ types: [] }) });
|
|
1409
|
+
}
|
|
1410
|
+
if (url.includes('/api/taskforce/priorities')) {
|
|
1411
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ priorities: [] }) });
|
|
1412
|
+
}
|
|
1413
|
+
if (url.includes('/api/taskforce/approaches')) {
|
|
1414
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ approaches: [] }) });
|
|
1415
|
+
}
|
|
1416
|
+
if (url.includes('/api/taskforce/specialists')) {
|
|
1417
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
|
|
1418
|
+
}
|
|
902
1419
|
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
903
1420
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
904
1421
|
}
|
|
905
1422
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
906
1423
|
});
|
|
907
1424
|
vi.stubGlobal('fetch', fetchMock);
|
|
908
|
-
render(_jsx(
|
|
1425
|
+
render(_jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }));
|
|
1426
|
+
const accountButton = await screen.findByTitle(/Account/i);
|
|
1427
|
+
await user.click(accountButton);
|
|
1428
|
+
expect(screen.getByText('Checking sign-in status...')).toBeInTheDocument();
|
|
1429
|
+
expect(screen.queryByText('Sign In / Register')).not.toBeInTheDocument();
|
|
1430
|
+
if (resolveRuntimeConfig) {
|
|
1431
|
+
resolveRuntimeConfig({
|
|
1432
|
+
ok: true,
|
|
1433
|
+
json: () => Promise.resolve({
|
|
1434
|
+
success: true,
|
|
1435
|
+
config: {
|
|
1436
|
+
baseUrl: 'https://cloud.example.com',
|
|
1437
|
+
apiBaseUrl: 'https://cloud.example.com',
|
|
1438
|
+
cloudAuthBaseUrl: 'https://cloud.example.com',
|
|
1439
|
+
wsBaseUrl: null,
|
|
1440
|
+
runtimeMode: 'local'
|
|
1441
|
+
}
|
|
1442
|
+
})
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
909
1445
|
await waitFor(() => {
|
|
910
|
-
expect(screen.
|
|
1446
|
+
expect(screen.queryByText(/Signed in as/i)).not.toBeInTheDocument();
|
|
1447
|
+
expect(screen.getByText('member@example.com')).toBeInTheDocument();
|
|
1448
|
+
expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('/api/taskforce/billing/status'))).toBe(true);
|
|
911
1449
|
});
|
|
912
1450
|
await waitFor(() => {
|
|
913
|
-
expect(screen.
|
|
1451
|
+
expect(screen.getByText('Subscription')).toBeInTheDocument();
|
|
1452
|
+
expect(screen.getByText('Starter')).toBeInTheDocument();
|
|
1453
|
+
expect(screen.getByText('Runtime')).toBeInTheDocument();
|
|
1454
|
+
expect(screen.getByText('LOCAL')).toBeInTheDocument();
|
|
1455
|
+
expect(screen.getByText('Environment')).toBeInTheDocument();
|
|
1456
|
+
expect(screen.getByText('CLOUD.EXAMPLE.COM')).toBeInTheDocument();
|
|
1457
|
+
expect(screen.queryByText(/Current Plan:/i)).not.toBeInTheDocument();
|
|
914
1458
|
});
|
|
915
|
-
await user.click(screen.getByRole('
|
|
916
|
-
const nameInput = screen.getByPlaceholderText('Project name');
|
|
917
|
-
await user.clear(nameInput);
|
|
918
|
-
await user.type(nameInput, 'Local Project');
|
|
919
|
-
await user.click(screen.getByRole('button', { name: 'Save Project Setup' }));
|
|
1459
|
+
await user.click(screen.getByRole('button', { name: /Subscription Starter/i }));
|
|
920
1460
|
await waitFor(() => {
|
|
921
|
-
expect(screen.
|
|
922
|
-
});
|
|
923
|
-
const workspaceSyncStateCall = fetchMock.mock.calls.find((call) => {
|
|
924
|
-
if (!String(call[0]).includes('/api/taskforce/ui-state'))
|
|
925
|
-
return false;
|
|
926
|
-
const req = call[1];
|
|
927
|
-
if (String(req?.method || '').toUpperCase() !== 'POST')
|
|
928
|
-
return false;
|
|
929
|
-
const body = JSON.parse(String(req?.body || '{}'));
|
|
930
|
-
return body?.stateKey === 'workspace-sync';
|
|
1461
|
+
expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
|
|
931
1462
|
});
|
|
932
|
-
|
|
933
|
-
expect(
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
expect(workspaceSyncStateBody.patch?.onboardingCompleted).toBe(false);
|
|
937
|
-
});
|
|
938
|
-
it('completes local first-run setup, survives refresh, and provisions cloud workspace on sync', async () => {
|
|
1463
|
+
expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('/api/taskforce/workspaces'))).toBe(false);
|
|
1464
|
+
expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('/api/taskforce/session/workspace'))).toBe(false);
|
|
1465
|
+
}, 10000);
|
|
1466
|
+
it('shows connected environment in the account menu', async () => {
|
|
939
1467
|
const user = userEvent.setup();
|
|
940
|
-
let
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
if (url.includes('/api/taskforce/auth/session')) {
|
|
1468
|
+
let resolveRuntimeConfig = null;
|
|
1469
|
+
const fetchMock = vi.fn((url) => {
|
|
1470
|
+
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
1471
|
+
return new Promise((resolve) => {
|
|
1472
|
+
resolveRuntimeConfig = resolve;
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1475
|
+
if (url.includes('https://cloud.example.com/api/taskforce/auth/session')) {
|
|
949
1476
|
return Promise.resolve({
|
|
950
1477
|
ok: true,
|
|
951
1478
|
json: () => Promise.resolve({
|
|
952
1479
|
authenticated: true,
|
|
953
1480
|
runtimeMode: 'local',
|
|
954
1481
|
authRequiredForApi: false,
|
|
955
|
-
|
|
1482
|
+
email: 'member@example.com',
|
|
1483
|
+
userId: 'user-member',
|
|
1484
|
+
workspaceId: 'workspace-local-active',
|
|
1485
|
+
betaAccess: true
|
|
956
1486
|
})
|
|
957
1487
|
});
|
|
958
1488
|
}
|
|
@@ -963,115 +1493,2140 @@ describe('TaskforceCore Integration', () => {
|
|
|
963
1493
|
...mockConfig,
|
|
964
1494
|
runtimeMode: 'local',
|
|
965
1495
|
authRequiredForApi: false,
|
|
966
|
-
workspaceId:
|
|
1496
|
+
workspaceId: 'workspace-local-active',
|
|
967
1497
|
setupState: {
|
|
968
1498
|
globalSetupState: 'present',
|
|
969
|
-
workspaceSetupState:
|
|
1499
|
+
workspaceSetupState: 'present',
|
|
970
1500
|
runtimeMode: 'local',
|
|
971
|
-
workspaceId:
|
|
1501
|
+
workspaceId: 'workspace-local-active',
|
|
972
1502
|
mode: 'core',
|
|
973
1503
|
forceSetup: false,
|
|
974
1504
|
forceGlobalSetup: false,
|
|
975
|
-
forceWorkspaceSetup:
|
|
976
|
-
workspace: workspaceSaved
|
|
977
|
-
? { id: 'workspace-local-flow-1', slug: 'workspace-local-flow-1', name: 'Local Flow Project', description: 'Flow desc' }
|
|
978
|
-
: null,
|
|
979
|
-
suggestedWorkspaceName: 'Suggested Project'
|
|
980
|
-
},
|
|
981
|
-
runtimeCapabilities: {
|
|
982
|
-
runtimeMode: 'local',
|
|
983
|
-
supportsCloudAuth: true,
|
|
984
|
-
supportsAdminManagedSetupOverrides: false,
|
|
985
|
-
supportsPackageScriptSetupOverrides: true
|
|
1505
|
+
forceWorkspaceSetup: false
|
|
986
1506
|
}
|
|
987
1507
|
})
|
|
988
1508
|
});
|
|
989
1509
|
}
|
|
990
|
-
if (url.includes('/api/taskforce/
|
|
991
|
-
workspaceSaved = true;
|
|
1510
|
+
if (url.includes('/api/taskforce/billing/status')) {
|
|
992
1511
|
return Promise.resolve({
|
|
993
1512
|
ok: true,
|
|
994
1513
|
json: () => Promise.resolve({
|
|
995
|
-
|
|
996
|
-
|
|
1514
|
+
planId: 'starter',
|
|
1515
|
+
planName: 'Starter',
|
|
1516
|
+
entitlementState: 'active'
|
|
997
1517
|
})
|
|
998
1518
|
});
|
|
999
1519
|
}
|
|
1000
|
-
if (url.includes('/api/taskforce/
|
|
1520
|
+
if (url.includes('/api/taskforce/categories')) {
|
|
1521
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve(mockCategories) });
|
|
1522
|
+
}
|
|
1523
|
+
if (url.includes('/api/taskforce/tasks')) {
|
|
1524
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
|
|
1525
|
+
}
|
|
1526
|
+
if (url.includes('/api/taskforce/types')) {
|
|
1527
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ types: [] }) });
|
|
1528
|
+
}
|
|
1529
|
+
if (url.includes('/api/taskforce/priorities')) {
|
|
1530
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ priorities: [] }) });
|
|
1531
|
+
}
|
|
1532
|
+
if (url.includes('/api/taskforce/approaches')) {
|
|
1533
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ approaches: [] }) });
|
|
1534
|
+
}
|
|
1535
|
+
if (url.includes('/api/taskforce/specialists')) {
|
|
1536
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
|
|
1537
|
+
}
|
|
1538
|
+
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
1539
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
1540
|
+
}
|
|
1541
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
1542
|
+
});
|
|
1543
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
1544
|
+
render(_jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }));
|
|
1545
|
+
const accountButton = await screen.findByTitle(/Account/i);
|
|
1546
|
+
await user.click(accountButton);
|
|
1547
|
+
expect(screen.getByText('Checking sign-in status...')).toBeInTheDocument();
|
|
1548
|
+
if (resolveRuntimeConfig) {
|
|
1549
|
+
resolveRuntimeConfig({
|
|
1550
|
+
ok: true,
|
|
1551
|
+
json: () => Promise.resolve({
|
|
1552
|
+
success: true,
|
|
1553
|
+
config: {
|
|
1554
|
+
baseUrl: 'https://cloud.example.com',
|
|
1555
|
+
apiBaseUrl: 'https://cloud.example.com',
|
|
1556
|
+
cloudAuthBaseUrl: 'https://cloud.example.com',
|
|
1557
|
+
wsBaseUrl: null,
|
|
1558
|
+
runtimeMode: 'local'
|
|
1559
|
+
}
|
|
1560
|
+
})
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
await waitFor(() => {
|
|
1564
|
+
expect(screen.getByText('Subscription')).toBeInTheDocument();
|
|
1565
|
+
expect(screen.getByText('Starter')).toBeInTheDocument();
|
|
1566
|
+
expect(screen.getByText('Runtime')).toBeInTheDocument();
|
|
1567
|
+
expect(screen.getByText('LOCAL')).toBeInTheDocument();
|
|
1568
|
+
expect(screen.getByText('Environment')).toBeInTheDocument();
|
|
1569
|
+
expect(screen.getByText('CLOUD.EXAMPLE.COM')).toBeInTheDocument();
|
|
1570
|
+
});
|
|
1571
|
+
}, 10000);
|
|
1572
|
+
it('allows local runtime to open shared /login and register flow', async () => {
|
|
1573
|
+
const fetchMock = vi.fn((url) => {
|
|
1574
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
1001
1575
|
return Promise.resolve({
|
|
1002
1576
|
ok: true,
|
|
1003
1577
|
json: () => Promise.resolve({
|
|
1004
|
-
|
|
1005
|
-
|
|
1578
|
+
authenticated: false,
|
|
1579
|
+
runtimeMode: 'local',
|
|
1580
|
+
authRequiredForApi: false
|
|
1006
1581
|
})
|
|
1007
1582
|
});
|
|
1008
1583
|
}
|
|
1009
|
-
if (url.includes('/api/taskforce/
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1584
|
+
if (url.includes('/api/taskforce/config')) {
|
|
1585
|
+
return Promise.resolve({
|
|
1586
|
+
ok: true,
|
|
1587
|
+
json: () => Promise.resolve({
|
|
1588
|
+
...mockConfig,
|
|
1589
|
+
runtimeMode: 'local',
|
|
1590
|
+
authRequiredForApi: false,
|
|
1591
|
+
setupState: {
|
|
1592
|
+
globalSetupState: 'present',
|
|
1593
|
+
workspaceSetupState: 'present',
|
|
1594
|
+
runtimeMode: 'local',
|
|
1595
|
+
workspaceId: 'workspace-local-active',
|
|
1596
|
+
mode: 'core',
|
|
1597
|
+
forceSetup: false,
|
|
1598
|
+
forceGlobalSetup: false,
|
|
1599
|
+
forceWorkspaceSetup: false
|
|
1600
|
+
}
|
|
1601
|
+
})
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
1605
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
1606
|
+
}
|
|
1607
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
1608
|
+
});
|
|
1609
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
1610
|
+
const { container } = render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
|
|
1611
|
+
await waitFor(() => {
|
|
1612
|
+
expect(screen.getByText('Create your Taskforce account.')).toBeInTheDocument();
|
|
1613
|
+
expect(screen.getByPlaceholderText('Email')).toBeInTheDocument();
|
|
1614
|
+
expect(screen.getByPlaceholderText('Display name')).toBeInTheDocument();
|
|
1615
|
+
expect(screen.getByRole('button', { name: 'Create Account' })).toBeInTheDocument();
|
|
1616
|
+
expect(screen.getByRole('button', { name: 'Sign In' })).toBeInTheDocument();
|
|
1617
|
+
expect(screen.queryByRole('button', { name: 'Register' })).not.toBeInTheDocument();
|
|
1618
|
+
expect(screen.getByRole('button', { name: 'Close sign in' })).toBeInTheDocument();
|
|
1619
|
+
});
|
|
1620
|
+
expect(container.querySelector('[aria-pressed]')).not.toBeInTheDocument();
|
|
1621
|
+
});
|
|
1622
|
+
it('shows debug registration autofill only when debug mode is enabled', async () => {
|
|
1623
|
+
const fetchMock = vi.fn((url) => {
|
|
1624
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
1625
|
+
return Promise.resolve({
|
|
1626
|
+
ok: true,
|
|
1627
|
+
json: () => Promise.resolve({
|
|
1628
|
+
authenticated: false,
|
|
1629
|
+
runtimeMode: 'local',
|
|
1630
|
+
authRequiredForApi: false
|
|
1631
|
+
})
|
|
1632
|
+
});
|
|
1633
|
+
}
|
|
1634
|
+
if (url.includes('/api/taskforce/config')) {
|
|
1635
|
+
return Promise.resolve({
|
|
1636
|
+
ok: true,
|
|
1637
|
+
json: () => Promise.resolve({
|
|
1638
|
+
...mockConfig,
|
|
1639
|
+
runtimeMode: 'local',
|
|
1640
|
+
authRequiredForApi: false,
|
|
1641
|
+
setupState: {
|
|
1642
|
+
globalSetupState: 'present',
|
|
1643
|
+
workspaceSetupState: 'present',
|
|
1644
|
+
runtimeMode: 'local',
|
|
1645
|
+
workspaceId: 'workspace-local-active',
|
|
1646
|
+
mode: 'core',
|
|
1647
|
+
forceSetup: false,
|
|
1648
|
+
forceGlobalSetup: false,
|
|
1649
|
+
forceWorkspaceSetup: false
|
|
1650
|
+
}
|
|
1651
|
+
})
|
|
1652
|
+
});
|
|
1653
|
+
}
|
|
1654
|
+
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
1655
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
1656
|
+
}
|
|
1657
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
1658
|
+
});
|
|
1659
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
1660
|
+
const firstRender = render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
|
|
1661
|
+
await waitFor(() => {
|
|
1662
|
+
expect(screen.getByText('Create your Taskforce account.')).toBeInTheDocument();
|
|
1663
|
+
});
|
|
1664
|
+
expect(screen.queryByRole('button', { name: 'Fill Form' })).not.toBeInTheDocument();
|
|
1665
|
+
firstRender.unmount();
|
|
1666
|
+
globalThis.__DEBUG_MODE__ = true;
|
|
1667
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
|
|
1668
|
+
expect(await screen.findByRole('button', { name: 'Fill Form' })).toBeInTheDocument();
|
|
1669
|
+
});
|
|
1670
|
+
it('fills and submits generated debug registration credentials through the normal register flow', async () => {
|
|
1671
|
+
globalThis.__DEBUG_MODE__ = true;
|
|
1672
|
+
const user = userEvent.setup();
|
|
1673
|
+
const nowSpy = vi.spyOn(Date, 'now').mockReturnValue(1735689600000);
|
|
1674
|
+
const fetchMock = vi.fn((input, init) => {
|
|
1675
|
+
const url = String(input);
|
|
1676
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
1677
|
+
return Promise.resolve({
|
|
1678
|
+
ok: true,
|
|
1679
|
+
json: () => Promise.resolve({
|
|
1680
|
+
authenticated: false,
|
|
1681
|
+
runtimeMode: 'local',
|
|
1682
|
+
authRequiredForApi: false
|
|
1683
|
+
})
|
|
1684
|
+
});
|
|
1685
|
+
}
|
|
1686
|
+
if (url.includes('/api/taskforce/config')) {
|
|
1687
|
+
return Promise.resolve({
|
|
1688
|
+
ok: true,
|
|
1689
|
+
json: () => Promise.resolve({
|
|
1690
|
+
...mockConfig,
|
|
1691
|
+
runtimeMode: 'local',
|
|
1692
|
+
authRequiredForApi: false,
|
|
1693
|
+
setupState: {
|
|
1694
|
+
globalSetupState: 'present',
|
|
1695
|
+
workspaceSetupState: 'present',
|
|
1696
|
+
runtimeMode: 'local',
|
|
1697
|
+
workspaceId: 'workspace-local-active',
|
|
1698
|
+
mode: 'core',
|
|
1699
|
+
forceSetup: false,
|
|
1700
|
+
forceGlobalSetup: false,
|
|
1701
|
+
forceWorkspaceSetup: false
|
|
1702
|
+
}
|
|
1703
|
+
})
|
|
1704
|
+
});
|
|
1705
|
+
}
|
|
1706
|
+
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
1707
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
1708
|
+
}
|
|
1709
|
+
if (url.includes('/api/taskforce/auth/register')) {
|
|
1710
|
+
return Promise.resolve({
|
|
1711
|
+
ok: true,
|
|
1712
|
+
json: () => Promise.resolve({
|
|
1713
|
+
success: true,
|
|
1714
|
+
workspaceSetupRequired: false,
|
|
1715
|
+
verificationRequired: true,
|
|
1716
|
+
emailSent: true
|
|
1717
|
+
})
|
|
1718
|
+
});
|
|
1719
|
+
}
|
|
1720
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
1721
|
+
});
|
|
1722
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
1723
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
|
|
1724
|
+
const emailInput = await screen.findByPlaceholderText('Email');
|
|
1725
|
+
const displayNameInput = screen.getByPlaceholderText('Display name');
|
|
1726
|
+
const passwordInput = screen.getByPlaceholderText('Password');
|
|
1727
|
+
await user.click(screen.getByRole('button', { name: 'Fill Form' }));
|
|
1728
|
+
expect(String(emailInput.value)).toMatch(/^debug\+[a-z0-9]+@example\.com$/);
|
|
1729
|
+
expect(String(displayNameInput.value)).toMatch(/^Debug [A-Z0-9]{4}$/);
|
|
1730
|
+
expect(passwordInput).toHaveValue('DebugPass123!');
|
|
1731
|
+
await user.click(screen.getByRole('button', { name: 'Create Account' }));
|
|
1732
|
+
await waitFor(() => {
|
|
1733
|
+
const registerCall = fetchMock.mock.calls.find((call) => String(call[0]).includes('/api/taskforce/auth/register'));
|
|
1734
|
+
expect(registerCall).toBeTruthy();
|
|
1735
|
+
const payload = JSON.parse(String(registerCall?.[1]?.body || '{}'));
|
|
1736
|
+
expect(String(registerCall?.[0] || '')).toContain('/api/taskforce/auth/register');
|
|
1737
|
+
const init = registerCall?.[1];
|
|
1738
|
+
expect(String(init?.method || '')).toBe('POST');
|
|
1739
|
+
expect(String(init?.credentials || '')).toBe('include');
|
|
1740
|
+
expect(payload.email).toBe(String(emailInput.value));
|
|
1741
|
+
expect(payload.displayName).toBe(String(displayNameInput.value));
|
|
1742
|
+
expect(payload.password).toBe('DebugPass123!');
|
|
1743
|
+
});
|
|
1744
|
+
nowSpy.mockRestore();
|
|
1745
|
+
});
|
|
1746
|
+
it('does not switch to verify mode after registration when verification is not required', async () => {
|
|
1747
|
+
const user = userEvent.setup();
|
|
1748
|
+
const fetchMock = vi.fn((input) => {
|
|
1749
|
+
const url = String(input);
|
|
1750
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
1751
|
+
return Promise.resolve({
|
|
1752
|
+
ok: true,
|
|
1753
|
+
json: () => Promise.resolve({
|
|
1754
|
+
authenticated: false,
|
|
1755
|
+
runtimeMode: 'local',
|
|
1756
|
+
authRequiredForApi: false
|
|
1757
|
+
})
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
if (url.includes('/api/taskforce/config')) {
|
|
1761
|
+
return Promise.resolve({
|
|
1762
|
+
ok: true,
|
|
1763
|
+
json: () => Promise.resolve({
|
|
1764
|
+
...mockConfig,
|
|
1765
|
+
runtimeMode: 'local',
|
|
1766
|
+
authRequiredForApi: false,
|
|
1767
|
+
setupState: {
|
|
1768
|
+
globalSetupState: 'present',
|
|
1769
|
+
workspaceSetupState: 'present',
|
|
1770
|
+
runtimeMode: 'local',
|
|
1771
|
+
workspaceId: 'workspace-local-active',
|
|
1772
|
+
mode: 'core',
|
|
1773
|
+
forceSetup: false,
|
|
1774
|
+
forceGlobalSetup: false,
|
|
1775
|
+
forceWorkspaceSetup: false
|
|
1776
|
+
}
|
|
1777
|
+
})
|
|
1778
|
+
});
|
|
1779
|
+
}
|
|
1780
|
+
if (url.includes('/api/taskforce/public/pricing')) {
|
|
1781
|
+
return Promise.resolve({
|
|
1782
|
+
ok: true,
|
|
1783
|
+
json: () => Promise.resolve({
|
|
1784
|
+
plans: []
|
|
1785
|
+
})
|
|
1786
|
+
});
|
|
1787
|
+
}
|
|
1788
|
+
if (url.includes('/api/taskforce/auth/register')) {
|
|
1789
|
+
return Promise.resolve({
|
|
1790
|
+
ok: true,
|
|
1791
|
+
json: () => Promise.resolve({
|
|
1792
|
+
success: true,
|
|
1793
|
+
workspaceSetupRequired: false,
|
|
1794
|
+
verificationRequired: false
|
|
1795
|
+
})
|
|
1796
|
+
});
|
|
1797
|
+
}
|
|
1798
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
1799
|
+
});
|
|
1800
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
1801
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2Fplans'], children: _jsx(TaskforceCore, {}) }));
|
|
1802
|
+
await user.type(await screen.findByPlaceholderText('Email'), 'local@example.com');
|
|
1803
|
+
await user.type(screen.getByPlaceholderText('Display name'), 'Local User');
|
|
1804
|
+
await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
|
|
1805
|
+
await user.click(screen.getByRole('button', { name: 'Create Account' }));
|
|
1806
|
+
await waitFor(() => {
|
|
1807
|
+
expect(screen.getByPlaceholderText('Search tasks...')).toBeInTheDocument();
|
|
1808
|
+
});
|
|
1809
|
+
expect(screen.queryByPlaceholderText('Verification token')).not.toBeInTheDocument();
|
|
1810
|
+
expect(screen.queryByRole('button', { name: 'Create Account' })).not.toBeInTheDocument();
|
|
1811
|
+
});
|
|
1812
|
+
it('allows returning to sign-in/register from forgot mode', async () => {
|
|
1813
|
+
const user = userEvent.setup();
|
|
1814
|
+
const fetchMock = vi.fn((url) => {
|
|
1815
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
1816
|
+
return Promise.resolve({
|
|
1817
|
+
ok: true,
|
|
1818
|
+
json: () => Promise.resolve({
|
|
1819
|
+
authenticated: false,
|
|
1820
|
+
runtimeMode: 'local',
|
|
1821
|
+
authRequiredForApi: false
|
|
1822
|
+
})
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1825
|
+
if (url.includes('/api/taskforce/config')) {
|
|
1826
|
+
return Promise.resolve({
|
|
1827
|
+
ok: true,
|
|
1828
|
+
json: () => Promise.resolve({
|
|
1829
|
+
...mockConfig,
|
|
1830
|
+
runtimeMode: 'local',
|
|
1831
|
+
authRequiredForApi: false,
|
|
1832
|
+
setupState: {
|
|
1833
|
+
globalSetupState: 'present',
|
|
1834
|
+
workspaceSetupState: 'present',
|
|
1835
|
+
runtimeMode: 'local',
|
|
1836
|
+
workspaceId: 'workspace-local-active',
|
|
1837
|
+
mode: 'core',
|
|
1838
|
+
forceSetup: false,
|
|
1839
|
+
forceGlobalSetup: false,
|
|
1840
|
+
forceWorkspaceSetup: false
|
|
1841
|
+
}
|
|
1842
|
+
})
|
|
1843
|
+
});
|
|
1844
|
+
}
|
|
1845
|
+
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
1846
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
1847
|
+
}
|
|
1848
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
1849
|
+
});
|
|
1850
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
1851
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=forgot&next=%2F'], children: _jsx(TaskforceCore, {}) }));
|
|
1852
|
+
await waitFor(() => {
|
|
1853
|
+
expect(screen.getByText('Request a password reset link.')).toBeInTheDocument();
|
|
1854
|
+
expect(screen.getByRole('button', { name: 'Sign In' })).toBeInTheDocument();
|
|
1855
|
+
expect(screen.getByRole('button', { name: 'Register' })).toBeInTheDocument();
|
|
1856
|
+
expect(screen.queryByRole('button', { name: 'Accept Invite' })).not.toBeInTheDocument();
|
|
1857
|
+
expect(screen.queryByRole('button', { name: /Resend Verification/ })).not.toBeInTheDocument();
|
|
1858
|
+
});
|
|
1859
|
+
await user.click(screen.getByRole('button', { name: 'Sign In' }));
|
|
1860
|
+
await waitFor(() => {
|
|
1861
|
+
expect(screen.getByText('Sign in with your account credentials.')).toBeInTheDocument();
|
|
1862
|
+
expect(screen.getByRole('button', { name: 'Register' })).toBeInTheDocument();
|
|
1863
|
+
});
|
|
1864
|
+
});
|
|
1865
|
+
it('redirects register users to plans when signup plan configuration blocks account creation', async () => {
|
|
1866
|
+
const user = userEvent.setup();
|
|
1867
|
+
const fetchMock = vi.fn((input) => {
|
|
1868
|
+
const url = String(input);
|
|
1869
|
+
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
1870
|
+
return Promise.resolve({
|
|
1871
|
+
ok: true,
|
|
1872
|
+
json: () => Promise.resolve({
|
|
1873
|
+
success: true,
|
|
1874
|
+
config: {
|
|
1875
|
+
baseUrl: 'http://localhost:5174',
|
|
1876
|
+
apiBaseUrl: 'http://localhost:5174',
|
|
1877
|
+
cloudAuthBaseUrl: 'http://localhost:5174',
|
|
1878
|
+
runtimeMode: 'local',
|
|
1879
|
+
authSource: 'cloud',
|
|
1880
|
+
workspaceMode: 'single-local',
|
|
1881
|
+
workspaceSwitchingEnabled: false,
|
|
1882
|
+
cloudAuthViaLocalProxy: true
|
|
1883
|
+
}
|
|
1884
|
+
})
|
|
1885
|
+
});
|
|
1886
|
+
}
|
|
1887
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
1888
|
+
return Promise.resolve({
|
|
1889
|
+
ok: true,
|
|
1890
|
+
json: () => Promise.resolve({
|
|
1891
|
+
authenticated: false,
|
|
1892
|
+
runtimeMode: 'local',
|
|
1893
|
+
authRequiredForApi: false
|
|
1894
|
+
})
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
if (url.includes('/api/taskforce/config')) {
|
|
1898
|
+
return Promise.resolve({
|
|
1899
|
+
ok: true,
|
|
1900
|
+
json: () => Promise.resolve({
|
|
1901
|
+
...mockConfig,
|
|
1902
|
+
runtimeMode: 'local',
|
|
1903
|
+
authRequiredForApi: false,
|
|
1904
|
+
setupState: {
|
|
1905
|
+
globalSetupState: 'present',
|
|
1906
|
+
workspaceSetupState: 'present',
|
|
1907
|
+
runtimeMode: 'local',
|
|
1908
|
+
workspaceId: 'workspace-local-active',
|
|
1909
|
+
mode: 'core',
|
|
1910
|
+
forceSetup: false,
|
|
1911
|
+
forceGlobalSetup: false,
|
|
1912
|
+
forceWorkspaceSetup: false
|
|
1913
|
+
}
|
|
1914
|
+
})
|
|
1915
|
+
});
|
|
1916
|
+
}
|
|
1917
|
+
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
1918
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
1919
|
+
}
|
|
1920
|
+
if (url.includes('/api/taskforce/public/pricing')) {
|
|
1921
|
+
return Promise.resolve({
|
|
1922
|
+
ok: true,
|
|
1923
|
+
json: () => Promise.resolve({
|
|
1924
|
+
asOf: '2026-03-16T00:00:00.000Z',
|
|
1925
|
+
plans: [
|
|
1926
|
+
{
|
|
1927
|
+
planId: 'free',
|
|
1928
|
+
displayName: 'Free',
|
|
1929
|
+
status: 'active',
|
|
1930
|
+
defaultVersion: { planVersionId: 'free-v1', versionNumber: 1, seatLimit: 1, metadata: {} },
|
|
1931
|
+
features: [],
|
|
1932
|
+
pricing: {
|
|
1933
|
+
month: { planVersionId: 'free-v1', interval: 'month', pricingType: 'free', stripePriceId: 'free:free-v1:month', displayAmount: '$0.00', unitAmount: 0, currency: 'usd' },
|
|
1934
|
+
year: { planVersionId: 'free-v1', interval: 'year', pricingType: 'free', stripePriceId: 'free:free-v1:year', displayAmount: '$0.00', unitAmount: 0, currency: 'usd' }
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
]
|
|
1938
|
+
})
|
|
1939
|
+
});
|
|
1940
|
+
}
|
|
1941
|
+
if (url.includes('/api/taskforce/auth/register')) {
|
|
1942
|
+
return Promise.resolve({
|
|
1943
|
+
ok: false,
|
|
1944
|
+
json: () => Promise.resolve({
|
|
1945
|
+
success: false,
|
|
1946
|
+
code: 'SIGNUP_PLAN_NOT_ENABLED',
|
|
1947
|
+
error: 'Configured default signup plan version \"team-10-v1\" belongs to a disabled plan.'
|
|
1948
|
+
})
|
|
1949
|
+
});
|
|
1950
|
+
}
|
|
1951
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
1952
|
+
});
|
|
1953
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
1954
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
|
|
1955
|
+
await user.type(await screen.findByPlaceholderText('Email'), 'local@example.com');
|
|
1956
|
+
await user.type(screen.getByPlaceholderText('Display name'), 'Local User');
|
|
1957
|
+
await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
|
|
1958
|
+
await user.click(screen.getByRole('button', { name: 'Create Account' }));
|
|
1959
|
+
await waitFor(() => {
|
|
1960
|
+
expect(screen.getByText('Choose a plan to continue.')).toBeInTheDocument();
|
|
1961
|
+
expect(screen.getByText('Subscription Plans')).toBeInTheDocument();
|
|
1962
|
+
});
|
|
1963
|
+
});
|
|
1964
|
+
it('shows sign-in continuation for existing-user invites', async () => {
|
|
1965
|
+
const fetchMock = vi.fn((url) => {
|
|
1966
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
1967
|
+
return Promise.resolve({
|
|
1968
|
+
ok: true,
|
|
1969
|
+
json: () => Promise.resolve({
|
|
1970
|
+
authenticated: false,
|
|
1971
|
+
runtimeMode: 'cloud',
|
|
1972
|
+
authRequiredForApi: true
|
|
1973
|
+
})
|
|
1974
|
+
});
|
|
1975
|
+
}
|
|
1976
|
+
if (url.includes('/api/taskforce/config')) {
|
|
1977
|
+
return Promise.resolve({
|
|
1978
|
+
ok: true,
|
|
1979
|
+
json: () => Promise.resolve({
|
|
1980
|
+
...mockConfig,
|
|
1981
|
+
runtimeMode: 'cloud',
|
|
1982
|
+
authRequiredForApi: true,
|
|
1983
|
+
setupState: {
|
|
1984
|
+
globalSetupState: 'present',
|
|
1985
|
+
workspaceSetupState: 'present',
|
|
1986
|
+
runtimeMode: 'cloud',
|
|
1987
|
+
workspaceId: 'workspace-1',
|
|
1988
|
+
mode: 'core',
|
|
1989
|
+
forceSetup: false,
|
|
1990
|
+
forceGlobalSetup: false,
|
|
1991
|
+
forceWorkspaceSetup: false
|
|
1992
|
+
}
|
|
1993
|
+
})
|
|
1994
|
+
});
|
|
1995
|
+
}
|
|
1996
|
+
if (url.includes('/api/taskforce/auth/invite/inspect')) {
|
|
1997
|
+
return Promise.resolve({
|
|
1998
|
+
ok: true,
|
|
1999
|
+
json: () => Promise.resolve({
|
|
2000
|
+
success: true,
|
|
2001
|
+
state: 'valid',
|
|
2002
|
+
email: 'existing@example.com',
|
|
2003
|
+
workspaceId: 'workspace-1',
|
|
2004
|
+
inviteeKind: 'existing_user',
|
|
2005
|
+
passwordRequired: false
|
|
2006
|
+
})
|
|
2007
|
+
});
|
|
2008
|
+
}
|
|
2009
|
+
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
2010
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
2011
|
+
}
|
|
2012
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
2013
|
+
});
|
|
2014
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
2015
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=invite&token=invite-token&next=%2F'], children: _jsx(TaskforceCore, {}) }));
|
|
2016
|
+
await waitFor(() => {
|
|
2017
|
+
expect(screen.getByText('Sign in to join this workspace.')).toBeInTheDocument();
|
|
2018
|
+
expect(screen.getByDisplayValue('existing@example.com')).toBeInTheDocument();
|
|
2019
|
+
expect(screen.getByPlaceholderText('Password')).toBeInTheDocument();
|
|
2020
|
+
expect(screen.getByRole('button', { name: 'Sign In to Continue' })).toBeInTheDocument();
|
|
2021
|
+
});
|
|
2022
|
+
});
|
|
2023
|
+
it('shows password creation for new-user invites', async () => {
|
|
2024
|
+
const fetchMock = vi.fn((url) => {
|
|
2025
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
2026
|
+
return Promise.resolve({
|
|
2027
|
+
ok: true,
|
|
2028
|
+
json: () => Promise.resolve({
|
|
2029
|
+
authenticated: false,
|
|
2030
|
+
runtimeMode: 'cloud',
|
|
2031
|
+
authRequiredForApi: true
|
|
2032
|
+
})
|
|
2033
|
+
});
|
|
2034
|
+
}
|
|
2035
|
+
if (url.includes('/api/taskforce/config')) {
|
|
2036
|
+
return Promise.resolve({
|
|
2037
|
+
ok: true,
|
|
2038
|
+
json: () => Promise.resolve({
|
|
2039
|
+
...mockConfig,
|
|
2040
|
+
runtimeMode: 'cloud',
|
|
2041
|
+
authRequiredForApi: true,
|
|
2042
|
+
setupState: {
|
|
2043
|
+
globalSetupState: 'present',
|
|
2044
|
+
workspaceSetupState: 'present',
|
|
2045
|
+
runtimeMode: 'cloud',
|
|
2046
|
+
workspaceId: 'workspace-1',
|
|
2047
|
+
mode: 'core',
|
|
2048
|
+
forceSetup: false,
|
|
2049
|
+
forceGlobalSetup: false,
|
|
2050
|
+
forceWorkspaceSetup: false
|
|
2051
|
+
}
|
|
2052
|
+
})
|
|
2053
|
+
});
|
|
2054
|
+
}
|
|
2055
|
+
if (url.includes('/api/taskforce/auth/invite/inspect')) {
|
|
2056
|
+
return Promise.resolve({
|
|
2057
|
+
ok: true,
|
|
2058
|
+
json: () => Promise.resolve({
|
|
2059
|
+
success: true,
|
|
2060
|
+
state: 'valid',
|
|
2061
|
+
email: 'new@example.com',
|
|
2062
|
+
workspaceId: 'workspace-1',
|
|
2063
|
+
inviteeKind: 'new_user',
|
|
2064
|
+
passwordRequired: true
|
|
2065
|
+
})
|
|
2066
|
+
});
|
|
2067
|
+
}
|
|
2068
|
+
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
2069
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
2070
|
+
}
|
|
2071
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
2072
|
+
});
|
|
2073
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
2074
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=invite&token=invite-token&next=%2F'], children: _jsx(TaskforceCore, {}) }));
|
|
2075
|
+
await waitFor(() => {
|
|
2076
|
+
expect(screen.getByText('Create your account to join this workspace.')).toBeInTheDocument();
|
|
2077
|
+
expect(screen.getByPlaceholderText('Create password')).toBeInTheDocument();
|
|
2078
|
+
expect(screen.getByRole('button', { name: 'Create Account and Join' })).toBeInTheDocument();
|
|
2079
|
+
});
|
|
2080
|
+
});
|
|
2081
|
+
it('shows global setup route when forced and persists selected mode', async () => {
|
|
2082
|
+
const user = userEvent.setup();
|
|
2083
|
+
let selectedMode = null;
|
|
2084
|
+
const fetchMock = vi.fn((url, init) => {
|
|
2085
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
2086
|
+
return Promise.resolve({
|
|
2087
|
+
ok: true,
|
|
2088
|
+
json: () => Promise.resolve({
|
|
2089
|
+
authenticated: true,
|
|
2090
|
+
runtimeMode: 'local',
|
|
2091
|
+
authRequiredForApi: false
|
|
2092
|
+
})
|
|
2093
|
+
});
|
|
2094
|
+
}
|
|
2095
|
+
if (url.includes('/api/taskforce/config')) {
|
|
2096
|
+
const mode = selectedMode;
|
|
2097
|
+
return Promise.resolve({
|
|
2098
|
+
ok: true,
|
|
2099
|
+
json: () => Promise.resolve({
|
|
2100
|
+
...mockConfig,
|
|
2101
|
+
runtimeMode: 'local',
|
|
2102
|
+
authRequiredForApi: false,
|
|
2103
|
+
setupState: {
|
|
2104
|
+
globalSetupState: mode ? 'present' : 'missing',
|
|
2105
|
+
workspaceSetupState: 'present',
|
|
2106
|
+
runtimeMode: 'local',
|
|
2107
|
+
workspaceId: 'default',
|
|
2108
|
+
mode: mode || 'core',
|
|
2109
|
+
forceSetup: false,
|
|
2110
|
+
forceGlobalSetup: true,
|
|
2111
|
+
forceWorkspaceSetup: false
|
|
2112
|
+
},
|
|
2113
|
+
runtimeCapabilities: {
|
|
2114
|
+
runtimeMode: 'local',
|
|
2115
|
+
supportsCloudAuth: false,
|
|
2116
|
+
supportsAdminManagedSetupOverrides: false,
|
|
2117
|
+
supportsPackageScriptSetupOverrides: true
|
|
2118
|
+
}
|
|
2119
|
+
})
|
|
2120
|
+
});
|
|
2121
|
+
}
|
|
2122
|
+
if (url.includes('/api/taskforce/global-settings')) {
|
|
2123
|
+
const body = JSON.parse(String(init?.body || '{}'));
|
|
2124
|
+
selectedMode = body?.setup?.mode === 'operations' ? 'operations' : 'core';
|
|
2125
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
|
|
2126
|
+
}
|
|
2127
|
+
if (url.includes('/api/taskforce/tasks')) {
|
|
2128
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
|
|
2129
|
+
}
|
|
2130
|
+
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
2131
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
2132
|
+
}
|
|
2133
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
2134
|
+
});
|
|
2135
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
2136
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, {}) }));
|
|
2137
|
+
await waitFor(() => {
|
|
2138
|
+
expect(screen.getByText('Global Setup Required')).toBeInTheDocument();
|
|
2139
|
+
});
|
|
2140
|
+
await user.click(screen.getByRole('radio', { name: /operations mode/i }));
|
|
2141
|
+
await user.click(screen.getByRole('button', { name: 'Save Global Setup' }));
|
|
2142
|
+
await waitFor(() => {
|
|
2143
|
+
expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
|
|
2144
|
+
});
|
|
2145
|
+
expect(fetchMock).toHaveBeenCalledWith('/api/taskforce/global-settings', expect.objectContaining({ method: 'POST' }));
|
|
2146
|
+
});
|
|
2147
|
+
it('shows workspace setup route when required and persists workspace profile', async () => {
|
|
2148
|
+
const user = userEvent.setup();
|
|
2149
|
+
let workspaceSaved = false;
|
|
2150
|
+
const fetchMock = vi.fn((url, init) => {
|
|
2151
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
2152
|
+
return Promise.resolve({
|
|
2153
|
+
ok: true,
|
|
2154
|
+
json: () => Promise.resolve({
|
|
2155
|
+
authenticated: true,
|
|
2156
|
+
runtimeMode: 'local',
|
|
2157
|
+
authRequiredForApi: false
|
|
2158
|
+
})
|
|
2159
|
+
});
|
|
2160
|
+
}
|
|
2161
|
+
if (url.includes('/api/taskforce/config')) {
|
|
2162
|
+
return Promise.resolve({
|
|
2163
|
+
ok: true,
|
|
2164
|
+
json: () => Promise.resolve({
|
|
2165
|
+
...mockConfig,
|
|
2166
|
+
runtimeMode: 'local',
|
|
2167
|
+
authRequiredForApi: false,
|
|
2168
|
+
setupState: {
|
|
2169
|
+
globalSetupState: 'present',
|
|
2170
|
+
workspaceSetupState: workspaceSaved ? 'present' : 'missing',
|
|
2171
|
+
runtimeMode: 'local',
|
|
2172
|
+
workspaceId: 'default',
|
|
2173
|
+
mode: 'core',
|
|
2174
|
+
forceSetup: false,
|
|
2175
|
+
forceGlobalSetup: false,
|
|
2176
|
+
forceWorkspaceSetup: true,
|
|
2177
|
+
workspace: workspaceSaved ? { id: 'default', slug: 'default', name: 'Delta Project', description: 'Workspace desc' } : null,
|
|
2178
|
+
suggestedWorkspaceName: 'Suggested Project'
|
|
2179
|
+
},
|
|
2180
|
+
runtimeCapabilities: {
|
|
2181
|
+
runtimeMode: 'local',
|
|
2182
|
+
supportsCloudAuth: false,
|
|
2183
|
+
supportsAdminManagedSetupOverrides: false,
|
|
2184
|
+
supportsPackageScriptSetupOverrides: true
|
|
2185
|
+
}
|
|
2186
|
+
})
|
|
2187
|
+
});
|
|
2188
|
+
}
|
|
2189
|
+
if (url.includes('/api/taskforce/workspace-profile')) {
|
|
2190
|
+
const body = JSON.parse(String(init?.body || '{}'));
|
|
2191
|
+
if (body?.name === 'Delta Project')
|
|
2192
|
+
workspaceSaved = true;
|
|
2193
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
|
|
2194
|
+
}
|
|
2195
|
+
if (url.includes('/api/taskforce/tasks')) {
|
|
2196
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
|
|
2197
|
+
}
|
|
2198
|
+
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
2199
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
2200
|
+
}
|
|
2201
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
2202
|
+
});
|
|
2203
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
2204
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, {}) }));
|
|
2205
|
+
await waitFor(() => {
|
|
2206
|
+
expect(screen.getByText('Workspace Setup Required')).toBeInTheDocument();
|
|
2207
|
+
});
|
|
2208
|
+
const nameInput = screen.getByPlaceholderText('Project name');
|
|
2209
|
+
await user.clear(nameInput);
|
|
2210
|
+
await user.type(nameInput, 'Delta Project');
|
|
2211
|
+
await user.type(screen.getByPlaceholderText('Project description (optional)'), 'Workspace desc');
|
|
2212
|
+
await user.click(screen.getByRole('button', { name: 'Save Project Setup' }));
|
|
2213
|
+
await waitFor(() => {
|
|
2214
|
+
expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
|
|
2215
|
+
});
|
|
2216
|
+
expect(fetchMock).toHaveBeenCalledWith('/api/taskforce/workspace-profile', expect.objectContaining({ method: 'POST' }));
|
|
2217
|
+
const workspaceSyncStateCall = fetchMock.mock.calls.find((call) => {
|
|
2218
|
+
if (!String(call[0]).includes('/api/taskforce/ui-state'))
|
|
2219
|
+
return false;
|
|
2220
|
+
const init = call[1];
|
|
2221
|
+
if (String(init?.method || '').toUpperCase() !== 'POST')
|
|
2222
|
+
return false;
|
|
2223
|
+
const body = JSON.parse(String(init?.body || '{}'));
|
|
2224
|
+
return body?.stateKey === 'workspace-sync';
|
|
2225
|
+
});
|
|
2226
|
+
const workspaceSyncStateBody = JSON.parse(String(workspaceSyncStateCall?.[1]?.body || '{}'));
|
|
2227
|
+
expect(workspaceSyncStateBody.stateKey).toBe('workspace-sync');
|
|
2228
|
+
expect(workspaceSyncStateBody.patch?.enabled).toBe(false);
|
|
2229
|
+
});
|
|
2230
|
+
it('applies the selected starter taxonomy pack during workspace setup', async () => {
|
|
2231
|
+
const user = userEvent.setup();
|
|
2232
|
+
let workspaceSaved = false;
|
|
2233
|
+
const fetchMock = vi.fn((url, init) => {
|
|
2234
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
2235
|
+
return jsonResponse({
|
|
2236
|
+
authenticated: true,
|
|
2237
|
+
runtimeMode: 'local',
|
|
2238
|
+
authRequiredForApi: false
|
|
2239
|
+
});
|
|
2240
|
+
}
|
|
2241
|
+
if (url.includes('/api/taskforce/config')) {
|
|
2242
|
+
return jsonResponse({
|
|
2243
|
+
...mockConfig,
|
|
2244
|
+
runtimeMode: 'local',
|
|
2245
|
+
authRequiredForApi: false,
|
|
2246
|
+
setupState: {
|
|
2247
|
+
globalSetupState: 'present',
|
|
2248
|
+
workspaceSetupState: workspaceSaved ? 'present' : 'missing',
|
|
2249
|
+
runtimeMode: 'local',
|
|
2250
|
+
workspaceId: workspaceSaved ? 'workspace-starter-1' : 'default',
|
|
2251
|
+
mode: 'core',
|
|
2252
|
+
forceSetup: false,
|
|
2253
|
+
forceGlobalSetup: false,
|
|
2254
|
+
forceWorkspaceSetup: true,
|
|
2255
|
+
workspace: workspaceSaved
|
|
2256
|
+
? { id: 'workspace-starter-1', slug: 'workspace-starter-1', name: 'Starter Workspace', description: 'Starter desc' }
|
|
2257
|
+
: null,
|
|
2258
|
+
suggestedWorkspaceName: 'Starter Workspace'
|
|
2259
|
+
},
|
|
2260
|
+
runtimeCapabilities: {
|
|
2261
|
+
runtimeMode: 'local',
|
|
2262
|
+
supportsCloudAuth: false,
|
|
2263
|
+
supportsAdminManagedSetupOverrides: false,
|
|
2264
|
+
supportsPackageScriptSetupOverrides: true
|
|
2265
|
+
}
|
|
2266
|
+
});
|
|
2267
|
+
}
|
|
2268
|
+
if (url.includes('/api/taskforce/taxonomy-library')) {
|
|
2269
|
+
return jsonResponse({
|
|
2270
|
+
packs: [
|
|
2271
|
+
{
|
|
2272
|
+
id: 'general',
|
|
2273
|
+
label: 'General',
|
|
2274
|
+
description: 'Neutral starter pack.',
|
|
2275
|
+
isDefaultStarter: true,
|
|
2276
|
+
categories: [{ value: 'general', label: 'General', icon: 'Inbox', color: 'slate-400' }],
|
|
2277
|
+
types: [{ value: 'task', label: 'Task', icon: 'CheckSquare', color: 'blue-500' }],
|
|
2278
|
+
priorities: [{ value: 1, label: 'Low', icon: 'ArrowDown', color: 'amber-200' }]
|
|
2279
|
+
},
|
|
2280
|
+
{
|
|
2281
|
+
id: 'software-development',
|
|
2282
|
+
label: 'Software Development',
|
|
2283
|
+
description: 'Software starter pack.',
|
|
2284
|
+
categories: [{ value: 'engineering', label: 'Engineering', icon: 'Code', color: 'blue-500' }],
|
|
2285
|
+
types: [
|
|
2286
|
+
{ value: 'feature', label: 'Feature', icon: 'Star', color: 'teal-500' },
|
|
2287
|
+
{ value: 'bug', label: 'Bug', icon: 'Bug', color: 'red-500' }
|
|
2288
|
+
],
|
|
2289
|
+
priorities: [
|
|
2290
|
+
{ value: 1, label: 'Low', icon: 'ArrowDown', color: 'amber-200' },
|
|
2291
|
+
{ value: 2, label: 'Medium', icon: 'Minus', color: 'yellow-500' },
|
|
2292
|
+
{ value: 3, label: 'High', icon: 'ArrowUp', color: 'orange-500' },
|
|
2293
|
+
{ value: 4, label: 'Critical', icon: 'AlertTriangle', color: 'red-500' }
|
|
2294
|
+
]
|
|
2295
|
+
}
|
|
2296
|
+
]
|
|
2297
|
+
});
|
|
2298
|
+
}
|
|
2299
|
+
if (url.includes('/api/taskforce/workspace-profile')) {
|
|
2300
|
+
const body = JSON.parse(String(init?.body || '{}'));
|
|
2301
|
+
if (body?.name === 'Starter Workspace')
|
|
2302
|
+
workspaceSaved = true;
|
|
2303
|
+
return jsonResponse({
|
|
2304
|
+
success: true,
|
|
2305
|
+
workspace: {
|
|
2306
|
+
id: 'workspace-starter-1'
|
|
2307
|
+
}
|
|
2308
|
+
});
|
|
2309
|
+
}
|
|
2310
|
+
if (url.includes('/api/taskforce/categories?workspaceId=workspace-starter-1')) {
|
|
2311
|
+
return jsonResponse({ success: true });
|
|
2312
|
+
}
|
|
2313
|
+
if (url.includes('/api/taskforce/types?workspaceId=workspace-starter-1')) {
|
|
2314
|
+
return jsonResponse({ success: true });
|
|
2315
|
+
}
|
|
2316
|
+
if (url.includes('/api/taskforce/priorities?workspaceId=workspace-starter-1')) {
|
|
2317
|
+
return jsonResponse({ success: true });
|
|
2318
|
+
}
|
|
2319
|
+
if (url.includes('/api/taskforce/ui-state')) {
|
|
2320
|
+
return jsonResponse({ success: true });
|
|
2321
|
+
}
|
|
2322
|
+
if (url.includes('/api/taskforce/tasks')) {
|
|
2323
|
+
return jsonResponse({ tasks: mockTasks });
|
|
2324
|
+
}
|
|
2325
|
+
if (url.includes('/api/taskforce/archive')) {
|
|
2326
|
+
return jsonResponse({ archived: [] });
|
|
2327
|
+
}
|
|
2328
|
+
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
2329
|
+
return jsonResponse({ templates: [] });
|
|
2330
|
+
}
|
|
2331
|
+
return jsonResponse({});
|
|
2332
|
+
});
|
|
2333
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
2334
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, {}) }));
|
|
2335
|
+
await waitFor(() => {
|
|
2336
|
+
expect(screen.getByText('Workspace Setup Required')).toBeInTheDocument();
|
|
2337
|
+
});
|
|
2338
|
+
await user.click(screen.getByRole('radio', { name: /software development/i }));
|
|
2339
|
+
await user.click(screen.getByRole('button', { name: 'Save Project Setup' }));
|
|
2340
|
+
await waitFor(() => {
|
|
2341
|
+
expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
|
|
2342
|
+
});
|
|
2343
|
+
const categoryApplyCall = fetchMock.mock.calls.find((call) => (String(call[0]).includes('/api/taskforce/categories?workspaceId=workspace-starter-1')
|
|
2344
|
+
&& String(call[1]?.method || '').toUpperCase() === 'POST'));
|
|
2345
|
+
const typeApplyCall = fetchMock.mock.calls.find((call) => (String(call[0]).includes('/api/taskforce/types?workspaceId=workspace-starter-1')
|
|
2346
|
+
&& String(call[1]?.method || '').toUpperCase() === 'POST'));
|
|
2347
|
+
const priorityApplyCall = fetchMock.mock.calls.find((call) => (String(call[0]).includes('/api/taskforce/priorities?workspaceId=workspace-starter-1')
|
|
2348
|
+
&& String(call[1]?.method || '').toUpperCase() === 'POST'));
|
|
2349
|
+
expect(categoryApplyCall).toBeDefined();
|
|
2350
|
+
expect(typeApplyCall).toBeDefined();
|
|
2351
|
+
expect(priorityApplyCall).toBeDefined();
|
|
2352
|
+
expect(new Headers(categoryApplyCall?.[1]?.headers).get('x-taskforce-workspace-id')).toBe('workspace-starter-1');
|
|
2353
|
+
expect(new Headers(typeApplyCall?.[1]?.headers).get('x-taskforce-workspace-id')).toBe('workspace-starter-1');
|
|
2354
|
+
expect(new Headers(priorityApplyCall?.[1]?.headers).get('x-taskforce-workspace-id')).toBe('workspace-starter-1');
|
|
2355
|
+
});
|
|
2356
|
+
it('recovers from transient local workspace-missing setup state after scope resolves on refresh', async () => {
|
|
2357
|
+
let configCalls = 0;
|
|
2358
|
+
const fetchMock = vi.fn((url) => {
|
|
2359
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
2360
|
+
return Promise.resolve({
|
|
2361
|
+
ok: true,
|
|
2362
|
+
json: () => Promise.resolve({
|
|
2363
|
+
authenticated: false,
|
|
2364
|
+
runtimeMode: 'local',
|
|
2365
|
+
authRequiredForApi: false
|
|
2366
|
+
})
|
|
2367
|
+
});
|
|
2368
|
+
}
|
|
2369
|
+
if (url.includes('/api/taskforce/config')) {
|
|
2370
|
+
configCalls += 1;
|
|
2371
|
+
const firstPass = configCalls === 1;
|
|
2372
|
+
return Promise.resolve({
|
|
2373
|
+
ok: true,
|
|
2374
|
+
json: () => Promise.resolve({
|
|
2375
|
+
...mockConfig,
|
|
2376
|
+
runtimeMode: 'local',
|
|
2377
|
+
authRequiredForApi: false,
|
|
2378
|
+
projectRoot: '/mock/local-project-refresh',
|
|
2379
|
+
workspaceId: 'workspace-local-refresh',
|
|
2380
|
+
setupState: {
|
|
2381
|
+
globalSetupState: 'present',
|
|
2382
|
+
workspaceSetupState: firstPass ? 'missing' : 'present',
|
|
2383
|
+
runtimeMode: 'local',
|
|
2384
|
+
workspaceId: 'workspace-local-refresh',
|
|
2385
|
+
mode: 'core',
|
|
2386
|
+
forceSetup: false,
|
|
2387
|
+
forceGlobalSetup: false,
|
|
2388
|
+
forceWorkspaceSetup: firstPass,
|
|
2389
|
+
workspace: firstPass
|
|
2390
|
+
? null
|
|
2391
|
+
: { id: 'workspace-local-refresh', slug: 'workspace-local-refresh', name: 'Local Refresh Project', description: null },
|
|
2392
|
+
suggestedWorkspaceName: 'Local Refresh Project'
|
|
2393
|
+
},
|
|
2394
|
+
runtimeCapabilities: {
|
|
2395
|
+
runtimeMode: 'local',
|
|
2396
|
+
supportsCloudAuth: true,
|
|
2397
|
+
supportsAdminManagedSetupOverrides: false,
|
|
2398
|
+
supportsPackageScriptSetupOverrides: true
|
|
2399
|
+
}
|
|
2400
|
+
})
|
|
2401
|
+
});
|
|
2402
|
+
}
|
|
2403
|
+
if (url.includes('/api/taskforce/categories')) {
|
|
2404
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve(mockCategories) });
|
|
2405
|
+
}
|
|
2406
|
+
if (url.includes('/api/taskforce/tasks')) {
|
|
2407
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
|
|
2408
|
+
}
|
|
2409
|
+
if (url.includes('/api/taskforce/types')) {
|
|
2410
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ types: [] }) });
|
|
2411
|
+
}
|
|
2412
|
+
if (url.includes('/api/taskforce/priorities')) {
|
|
2413
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ priorities: [] }) });
|
|
2414
|
+
}
|
|
2415
|
+
if (url.includes('/api/taskforce/approaches')) {
|
|
2416
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ approaches: [] }) });
|
|
2417
|
+
}
|
|
2418
|
+
if (url.includes('/api/taskforce/specialists')) {
|
|
2419
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
|
|
2420
|
+
}
|
|
2421
|
+
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
2422
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
2423
|
+
}
|
|
2424
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
2425
|
+
});
|
|
2426
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
2427
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, {}) }));
|
|
2428
|
+
await waitFor(() => {
|
|
2429
|
+
expect(configCalls).toBeGreaterThan(1);
|
|
2430
|
+
});
|
|
2431
|
+
expect(screen.getByRole('button', { name: /add task/i })).toBeInTheDocument();
|
|
2432
|
+
expect(screen.queryByText('Workspace Setup Required')).not.toBeInTheDocument();
|
|
2433
|
+
});
|
|
2434
|
+
it('allows workspace setup to sync from selected cloud project into matching local workspace id', async () => {
|
|
2435
|
+
const user = userEvent.setup();
|
|
2436
|
+
let workspaceSaved = false;
|
|
2437
|
+
const fetchMock = vi.fn((url, init) => {
|
|
2438
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
2439
|
+
return Promise.resolve({
|
|
2440
|
+
ok: true,
|
|
2441
|
+
json: () => Promise.resolve({
|
|
2442
|
+
authenticated: true,
|
|
2443
|
+
runtimeMode: 'local',
|
|
2444
|
+
authRequiredForApi: false
|
|
2445
|
+
})
|
|
2446
|
+
});
|
|
2447
|
+
}
|
|
2448
|
+
if (url.includes('/api/taskforce/config')) {
|
|
2449
|
+
return Promise.resolve({
|
|
2450
|
+
ok: true,
|
|
2451
|
+
json: () => Promise.resolve({
|
|
2452
|
+
...mockConfig,
|
|
2453
|
+
runtimeMode: 'local',
|
|
2454
|
+
authRequiredForApi: false,
|
|
2455
|
+
setupState: {
|
|
2456
|
+
globalSetupState: 'present',
|
|
2457
|
+
workspaceSetupState: workspaceSaved ? 'present' : 'missing',
|
|
2458
|
+
runtimeMode: 'local',
|
|
2459
|
+
workspaceId: workspaceSaved ? 'workspace-cloud-123' : 'default',
|
|
2460
|
+
mode: 'core',
|
|
2461
|
+
forceSetup: false,
|
|
2462
|
+
forceGlobalSetup: false,
|
|
2463
|
+
forceWorkspaceSetup: true,
|
|
2464
|
+
workspace: workspaceSaved
|
|
2465
|
+
? { id: 'workspace-cloud-123', slug: 'workspace-cloud-123', name: 'Cloud Project 123', description: 'Cloud desc' }
|
|
2466
|
+
: null,
|
|
2467
|
+
suggestedWorkspaceName: 'Suggested Project'
|
|
2468
|
+
},
|
|
2469
|
+
runtimeCapabilities: {
|
|
2470
|
+
runtimeMode: 'local',
|
|
2471
|
+
supportsCloudAuth: true,
|
|
2472
|
+
supportsAdminManagedSetupOverrides: false,
|
|
2473
|
+
supportsPackageScriptSetupOverrides: true
|
|
2474
|
+
}
|
|
2475
|
+
})
|
|
2476
|
+
});
|
|
2477
|
+
}
|
|
2478
|
+
if (url.includes('https://cloud.example.com/api/taskforce/workspaces')) {
|
|
2479
|
+
return Promise.resolve({
|
|
2480
|
+
ok: true,
|
|
2481
|
+
json: () => Promise.resolve({
|
|
2482
|
+
success: true,
|
|
2483
|
+
workspaces: [
|
|
2484
|
+
{
|
|
2485
|
+
id: 'workspace-cloud-123',
|
|
2486
|
+
name: 'Cloud Project 123',
|
|
2487
|
+
description: 'Cloud desc'
|
|
2488
|
+
}
|
|
2489
|
+
]
|
|
2490
|
+
})
|
|
2491
|
+
});
|
|
2492
|
+
}
|
|
2493
|
+
if (url.includes('/api/taskforce/workspace-profile')) {
|
|
2494
|
+
const body = JSON.parse(String(init?.body || '{}'));
|
|
2495
|
+
if (body?.workspaceId === 'workspace-cloud-123')
|
|
2496
|
+
workspaceSaved = true;
|
|
2497
|
+
return Promise.resolve({
|
|
2498
|
+
ok: true,
|
|
2499
|
+
json: () => Promise.resolve({
|
|
2500
|
+
success: true,
|
|
2501
|
+
workspace: {
|
|
2502
|
+
id: 'workspace-cloud-123'
|
|
2503
|
+
}
|
|
2504
|
+
})
|
|
2505
|
+
});
|
|
2506
|
+
}
|
|
2507
|
+
if (url.includes('/api/taskforce/session/workspace')) {
|
|
2508
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
|
|
2509
|
+
}
|
|
2510
|
+
if (url.includes('/api/taskforce/ui-state')) {
|
|
2511
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
|
|
2512
|
+
}
|
|
2513
|
+
if (url.includes('/api/taskforce/tasks')) {
|
|
2514
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
|
|
2515
|
+
}
|
|
2516
|
+
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
2517
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
2518
|
+
}
|
|
2519
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
2520
|
+
});
|
|
2521
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
2522
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/setup?step=workspace&source=cloud'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
|
|
2523
|
+
await waitFor(() => {
|
|
2524
|
+
expect(screen.getByText('Workspace Setup Required')).toBeInTheDocument();
|
|
2525
|
+
});
|
|
2526
|
+
await user.click(screen.getByRole('radio', { name: /sync existing cloud project/i }));
|
|
2527
|
+
await waitFor(() => {
|
|
2528
|
+
expect(screen.getByRole('option', { name: /Cloud Project 123/ })).toBeInTheDocument();
|
|
2529
|
+
});
|
|
2530
|
+
await user.selectOptions(screen.getByRole('combobox'), 'workspace-cloud-123');
|
|
2531
|
+
await user.click(screen.getByRole('button', { name: 'Save Project Setup' }));
|
|
2532
|
+
await waitFor(() => {
|
|
2533
|
+
expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
|
|
2534
|
+
});
|
|
2535
|
+
const workspaceProfileCall = fetchMock.mock.calls.find((call) => String(call[0]).includes('/api/taskforce/workspace-profile'));
|
|
2536
|
+
const workspaceProfileBody = JSON.parse(String(workspaceProfileCall?.[1]?.body || '{}'));
|
|
2537
|
+
expect(workspaceProfileBody.workspaceId).toBe('workspace-cloud-123');
|
|
2538
|
+
const workspaceSyncStateCall = fetchMock.mock.calls.find((call) => {
|
|
2539
|
+
if (!String(call[0]).includes('/api/taskforce/ui-state'))
|
|
2540
|
+
return false;
|
|
2541
|
+
const init = call[1];
|
|
2542
|
+
if (String(init?.method || '').toUpperCase() !== 'POST')
|
|
2543
|
+
return false;
|
|
2544
|
+
const body = JSON.parse(String(init?.body || '{}'));
|
|
2545
|
+
return body?.stateKey === 'workspace-sync';
|
|
2546
|
+
});
|
|
2547
|
+
const workspaceSyncStateBody = JSON.parse(String(workspaceSyncStateCall?.[1]?.body || '{}'));
|
|
2548
|
+
expect(workspaceSyncStateBody.stateKey).toBe('workspace-sync');
|
|
2549
|
+
expect(workspaceSyncStateBody.patch?.cloudSyncEnabled).toBe(true);
|
|
2550
|
+
expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('https://cloud.example.com/api/taskforce/workspaces'))).toBe(true);
|
|
2551
|
+
});
|
|
2552
|
+
it('enables workspace cloud sync from local setup when signed in and checkbox is selected', async () => {
|
|
2553
|
+
const user = userEvent.setup();
|
|
2554
|
+
let workspaceSaved = false;
|
|
2555
|
+
const fetchMock = vi.fn((url, init) => {
|
|
2556
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
2557
|
+
return Promise.resolve({
|
|
2558
|
+
ok: true,
|
|
2559
|
+
json: () => Promise.resolve({
|
|
2560
|
+
authenticated: true,
|
|
2561
|
+
runtimeMode: 'local',
|
|
2562
|
+
authRequiredForApi: false
|
|
2563
|
+
})
|
|
2564
|
+
});
|
|
2565
|
+
}
|
|
2566
|
+
if (url.includes('/api/taskforce/config')) {
|
|
2567
|
+
return Promise.resolve({
|
|
2568
|
+
ok: true,
|
|
2569
|
+
json: () => Promise.resolve({
|
|
2570
|
+
...mockConfig,
|
|
2571
|
+
runtimeMode: 'local',
|
|
2572
|
+
authRequiredForApi: false,
|
|
2573
|
+
setupState: {
|
|
2574
|
+
globalSetupState: 'present',
|
|
2575
|
+
workspaceSetupState: workspaceSaved ? 'present' : 'missing',
|
|
2576
|
+
runtimeMode: 'local',
|
|
2577
|
+
workspaceId: workspaceSaved ? 'workspace-local-setup-1' : 'default',
|
|
2578
|
+
mode: 'core',
|
|
2579
|
+
forceSetup: false,
|
|
2580
|
+
forceGlobalSetup: false,
|
|
2581
|
+
forceWorkspaceSetup: true,
|
|
2582
|
+
workspace: workspaceSaved
|
|
2583
|
+
? { id: 'workspace-local-setup-1', slug: 'workspace-local-setup-1', name: 'Local Project', description: 'Local desc' }
|
|
2584
|
+
: null,
|
|
2585
|
+
suggestedWorkspaceName: 'Suggested Project'
|
|
2586
|
+
},
|
|
2587
|
+
runtimeCapabilities: {
|
|
2588
|
+
runtimeMode: 'local',
|
|
2589
|
+
supportsCloudAuth: true,
|
|
2590
|
+
supportsAdminManagedSetupOverrides: false,
|
|
2591
|
+
supportsPackageScriptSetupOverrides: true
|
|
2592
|
+
}
|
|
2593
|
+
})
|
|
2594
|
+
});
|
|
2595
|
+
}
|
|
2596
|
+
if (url.includes('/api/taskforce/workspace-profile')) {
|
|
2597
|
+
workspaceSaved = true;
|
|
2598
|
+
return Promise.resolve({
|
|
2599
|
+
ok: true,
|
|
2600
|
+
json: () => Promise.resolve({
|
|
2601
|
+
success: true,
|
|
2602
|
+
workspace: {
|
|
2603
|
+
id: 'workspace-local-setup-1'
|
|
2604
|
+
}
|
|
2605
|
+
})
|
|
2606
|
+
});
|
|
2607
|
+
}
|
|
2608
|
+
if (url.includes('/api/taskforce/ui-state')) {
|
|
2609
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
|
|
2610
|
+
}
|
|
2611
|
+
if (url.includes('/api/taskforce/tasks')) {
|
|
2612
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
|
|
2613
|
+
}
|
|
2614
|
+
if (url.includes('/api/taskforce/workflow-templates')) {
|
|
2615
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
2616
|
+
}
|
|
2617
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
2618
|
+
});
|
|
2619
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
2620
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/setup?step=workspace'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
|
|
2621
|
+
await waitFor(() => {
|
|
2622
|
+
expect(screen.getByText('Workspace Setup Required')).toBeInTheDocument();
|
|
2623
|
+
});
|
|
2624
|
+
await waitFor(() => {
|
|
2625
|
+
expect(screen.getByRole('checkbox', { name: /sync to cloud after setup/i })).toBeInTheDocument();
|
|
2626
|
+
});
|
|
2627
|
+
await user.click(screen.getByRole('checkbox', { name: /sync to cloud after setup/i }));
|
|
2628
|
+
const nameInput = screen.getByPlaceholderText('Project name');
|
|
2629
|
+
await user.clear(nameInput);
|
|
2630
|
+
await user.type(nameInput, 'Local Project');
|
|
2631
|
+
await user.click(screen.getByRole('button', { name: 'Save Project Setup' }));
|
|
2632
|
+
await waitFor(() => {
|
|
2633
|
+
expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
|
|
2634
|
+
});
|
|
2635
|
+
const workspaceSyncStateCall = fetchMock.mock.calls.find((call) => {
|
|
2636
|
+
if (!String(call[0]).includes('/api/taskforce/ui-state'))
|
|
2637
|
+
return false;
|
|
2638
|
+
const req = call[1];
|
|
2639
|
+
if (String(req?.method || '').toUpperCase() !== 'POST')
|
|
2640
|
+
return false;
|
|
2641
|
+
const body = JSON.parse(String(req?.body || '{}'));
|
|
2642
|
+
return body?.stateKey === 'workspace-sync';
|
|
2643
|
+
});
|
|
2644
|
+
const workspaceSyncStateBody = JSON.parse(String(workspaceSyncStateCall?.[1]?.body || '{}'));
|
|
2645
|
+
expect(workspaceSyncStateBody.patch?.cloudSyncEnabled).toBe(true);
|
|
2646
|
+
expect(workspaceSyncStateBody.patch?.sourceOfTruth).toBe('local');
|
|
2647
|
+
// I6: onboarding now starts as false; the orchestrator marks it true after first pull.
|
|
2648
|
+
expect(workspaceSyncStateBody.patch?.onboardingCompleted).toBe(false);
|
|
2649
|
+
});
|
|
2650
|
+
it('creates a new cloud workspace only after guided setup submit', async () => {
|
|
2651
|
+
const user = userEvent.setup();
|
|
2652
|
+
let currentWorkspaceId = 'workspace-cloud-active';
|
|
2653
|
+
let workspaceCreated = false;
|
|
2654
|
+
const fetchMock = vi.fn((url, init) => {
|
|
2655
|
+
const method = String(init?.method || 'GET').toUpperCase();
|
|
2656
|
+
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
2657
|
+
return jsonResponse({ config: { runtimeMode: 'cloud', workspaceMode: 'multi-cloud', authSource: 'cloud' } });
|
|
2658
|
+
}
|
|
2659
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
2660
|
+
return jsonResponse({
|
|
2661
|
+
authenticated: true,
|
|
2662
|
+
runtimeMode: 'cloud',
|
|
2663
|
+
authRequiredForApi: true,
|
|
2664
|
+
userId: 'user-cloud',
|
|
2665
|
+
email: 'owner@example.com',
|
|
2666
|
+
workspaceId: currentWorkspaceId,
|
|
2667
|
+
betaAccess: true
|
|
2668
|
+
});
|
|
2669
|
+
}
|
|
2670
|
+
if (url.includes('/api/taskforce/config')) {
|
|
2671
|
+
return jsonResponse({
|
|
2672
|
+
...mockConfig,
|
|
2673
|
+
runtimeMode: 'cloud',
|
|
2674
|
+
authRequiredForApi: false,
|
|
2675
|
+
workspaceId: currentWorkspaceId,
|
|
2676
|
+
setupState: {
|
|
2677
|
+
globalSetupState: 'present',
|
|
2678
|
+
workspaceSetupState: 'present',
|
|
2679
|
+
runtimeMode: 'cloud',
|
|
2680
|
+
workspaceId: currentWorkspaceId,
|
|
2681
|
+
mode: 'core',
|
|
2682
|
+
forceSetup: false,
|
|
2683
|
+
forceGlobalSetup: false,
|
|
2684
|
+
forceWorkspaceSetup: false,
|
|
2685
|
+
workspace: {
|
|
2686
|
+
id: currentWorkspaceId,
|
|
2687
|
+
slug: currentWorkspaceId,
|
|
2688
|
+
name: currentWorkspaceId === 'workspace-cloud-new' ? 'Cloud New Project' : 'Cloud Active',
|
|
2689
|
+
description: currentWorkspaceId === 'workspace-cloud-new' ? 'New workspace description' : 'Active workspace'
|
|
2690
|
+
},
|
|
2691
|
+
suggestedWorkspaceName: ''
|
|
2692
|
+
},
|
|
2693
|
+
runtimeCapabilities: {
|
|
2694
|
+
runtimeMode: 'cloud',
|
|
2695
|
+
supportsCloudAuth: true,
|
|
2696
|
+
supportsAdminManagedSetupOverrides: true,
|
|
2697
|
+
supportsPackageScriptSetupOverrides: false
|
|
2698
|
+
}
|
|
2699
|
+
});
|
|
2700
|
+
}
|
|
2701
|
+
if (url.includes('/api/taskforce/taxonomy-library')) {
|
|
2702
|
+
return jsonResponse({
|
|
2703
|
+
packs: [{
|
|
2704
|
+
id: 'general',
|
|
2705
|
+
label: 'General',
|
|
2706
|
+
description: 'General starter',
|
|
2707
|
+
categories: [{ value: 'general', label: 'General' }],
|
|
2708
|
+
types: [{ value: 'task', label: 'Task' }],
|
|
2709
|
+
priorities: [
|
|
2710
|
+
{ value: 1, label: 'Low' },
|
|
2711
|
+
{ value: 2, label: 'Medium' },
|
|
2712
|
+
{ value: 3, label: 'High' }
|
|
2713
|
+
],
|
|
2714
|
+
isDefaultStarter: true
|
|
2715
|
+
}]
|
|
2716
|
+
});
|
|
2717
|
+
}
|
|
2718
|
+
if (url.includes('/api/taskforce/workspaces') && method === 'POST') {
|
|
2719
|
+
workspaceCreated = true;
|
|
2720
|
+
currentWorkspaceId = 'workspace-cloud-new';
|
|
2721
|
+
return jsonResponse({
|
|
2722
|
+
success: true,
|
|
2723
|
+
workspace: {
|
|
2724
|
+
id: 'workspace-cloud-new',
|
|
2725
|
+
slug: 'workspace-cloud-new',
|
|
2726
|
+
name: 'Cloud New Project',
|
|
2727
|
+
description: 'New workspace description',
|
|
2728
|
+
role: 'owner'
|
|
2729
|
+
}
|
|
2730
|
+
});
|
|
2731
|
+
}
|
|
2732
|
+
if (url.includes('/api/taskforce/workspaces')) {
|
|
2733
|
+
return jsonResponse({
|
|
2734
|
+
success: true,
|
|
2735
|
+
currentWorkspaceId,
|
|
2736
|
+
workspaces: [
|
|
2737
|
+
{ id: 'workspace-cloud-active', slug: 'workspace-cloud-active', name: 'Cloud Active', role: 'owner' },
|
|
2738
|
+
...(workspaceCreated
|
|
2739
|
+
? [{ id: 'workspace-cloud-new', slug: 'workspace-cloud-new', name: 'Cloud New Project', role: 'owner' }]
|
|
2740
|
+
: [])
|
|
2741
|
+
]
|
|
2742
|
+
});
|
|
2743
|
+
}
|
|
2744
|
+
if (url.includes('/api/taskforce/session/workspace') && method === 'POST') {
|
|
2745
|
+
const body = JSON.parse(String(init?.body || '{}'));
|
|
2746
|
+
currentWorkspaceId = String(body?.workspaceId || currentWorkspaceId);
|
|
2747
|
+
return jsonResponse({ success: true, workspaceId: currentWorkspaceId });
|
|
2748
|
+
}
|
|
2749
|
+
if (url.includes('/api/taskforce/ui-state')) {
|
|
2750
|
+
return jsonResponse({ success: true });
|
|
2751
|
+
}
|
|
2752
|
+
if (url.includes('/api/taskforce/tasks'))
|
|
2753
|
+
return jsonResponse({ tasks: [] });
|
|
2754
|
+
if (url.includes('/api/taskforce/archive'))
|
|
2755
|
+
return jsonResponse({ archived: [] });
|
|
2756
|
+
if (url.includes('/api/taskforce/categories'))
|
|
2757
|
+
return jsonResponse(mockCategories);
|
|
2758
|
+
if (url.includes('/api/taskforce/types'))
|
|
2759
|
+
return jsonResponse({ types: [] });
|
|
2760
|
+
if (url.includes('/api/taskforce/priorities'))
|
|
2761
|
+
return jsonResponse({ priorities: [] });
|
|
2762
|
+
if (url.includes('/api/taskforce/specialists'))
|
|
2763
|
+
return jsonResponse({ specialists: [] });
|
|
2764
|
+
if (url.includes('/api/taskforce/workflow-templates'))
|
|
2765
|
+
return jsonResponse({ templates: [] });
|
|
2766
|
+
return jsonResponse({});
|
|
2767
|
+
});
|
|
2768
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
2769
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/setup?step=workspace&intent=create-workspace'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
|
|
2770
|
+
await waitFor(() => {
|
|
2771
|
+
expect(screen.getByText('Set up the new workspace before it is created.')).toBeInTheDocument();
|
|
2772
|
+
});
|
|
2773
|
+
await user.clear(screen.getByPlaceholderText('Project name'));
|
|
2774
|
+
await user.type(screen.getByPlaceholderText('Project name'), 'Cloud New Project');
|
|
2775
|
+
await user.clear(screen.getByPlaceholderText('Project description (optional)'));
|
|
2776
|
+
await user.type(screen.getByPlaceholderText('Project description (optional)'), 'New workspace description');
|
|
2777
|
+
await user.click(screen.getByRole('button', { name: 'Save Project Setup' }));
|
|
2778
|
+
await waitFor(() => {
|
|
2779
|
+
expect(workspaceCreated).toBe(true);
|
|
2780
|
+
});
|
|
2781
|
+
const createCall = fetchMock.mock.calls.find((call) => String(call[0]).includes('/api/taskforce/workspaces') && String(call[1]?.method || 'GET').toUpperCase() === 'POST');
|
|
2782
|
+
const createBody = JSON.parse(String(createCall?.[1]?.body || '{}'));
|
|
2783
|
+
expect(createBody.name).toBe('Cloud New Project');
|
|
2784
|
+
expect(createBody.description).toBe('New workspace description');
|
|
2785
|
+
expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('/api/taskforce/workspace-profile'))).toBe(false);
|
|
2786
|
+
expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('/api/taskforce/bulk-update-fields'))).toBe(false);
|
|
2787
|
+
const typeApplyCall = fetchMock.mock.calls.find((call) => (String(call[0]).includes('/api/taskforce/types?workspaceId=workspace-cloud-new')
|
|
2788
|
+
&& String(call[1]?.method || '').toUpperCase() === 'POST'));
|
|
2789
|
+
const typeApplyBody = JSON.parse(String(typeApplyCall?.[1]?.body || '{}'));
|
|
2790
|
+
expect(typeApplyBody.types).toEqual([{ value: 'task', label: 'Task' }]);
|
|
2791
|
+
await waitFor(() => {
|
|
2792
|
+
expect(screen.getByTitle('Workspace ID: workspace-cloud-new')).toBeInTheDocument();
|
|
2793
|
+
});
|
|
2794
|
+
});
|
|
2795
|
+
it('cancels voluntary cloud workspace creation back to the active workspace', async () => {
|
|
2796
|
+
const user = userEvent.setup();
|
|
2797
|
+
const fetchMock = vi.fn((url) => {
|
|
2798
|
+
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
2799
|
+
return jsonResponse({ config: { runtimeMode: 'cloud', workspaceMode: 'multi-cloud', authSource: 'cloud' } });
|
|
2800
|
+
}
|
|
2801
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
2802
|
+
return jsonResponse({
|
|
2803
|
+
authenticated: true,
|
|
2804
|
+
runtimeMode: 'cloud',
|
|
2805
|
+
authRequiredForApi: false,
|
|
2806
|
+
userId: 'user-cloud',
|
|
2807
|
+
email: 'owner@example.com',
|
|
2808
|
+
workspaceId: 'workspace-cloud-active',
|
|
2809
|
+
betaAccess: true
|
|
2810
|
+
});
|
|
2811
|
+
}
|
|
2812
|
+
if (url.includes('/api/taskforce/config')) {
|
|
2813
|
+
return jsonResponse({
|
|
2814
|
+
...mockConfig,
|
|
2815
|
+
runtimeMode: 'cloud',
|
|
2816
|
+
authRequiredForApi: false,
|
|
2817
|
+
workspaceId: 'workspace-cloud-active',
|
|
2818
|
+
setupState: {
|
|
2819
|
+
globalSetupState: 'present',
|
|
2820
|
+
workspaceSetupState: 'present',
|
|
2821
|
+
runtimeMode: 'cloud',
|
|
2822
|
+
workspaceId: 'workspace-cloud-active',
|
|
2823
|
+
mode: 'core',
|
|
2824
|
+
forceSetup: false,
|
|
2825
|
+
forceGlobalSetup: false,
|
|
2826
|
+
forceWorkspaceSetup: false,
|
|
2827
|
+
workspace: {
|
|
2828
|
+
id: 'workspace-cloud-active',
|
|
2829
|
+
slug: 'workspace-cloud-active',
|
|
2830
|
+
name: 'Cloud Active',
|
|
2831
|
+
description: 'Active workspace'
|
|
2832
|
+
},
|
|
2833
|
+
suggestedWorkspaceName: ''
|
|
2834
|
+
},
|
|
2835
|
+
runtimeCapabilities: {
|
|
2836
|
+
runtimeMode: 'cloud',
|
|
2837
|
+
supportsCloudAuth: true,
|
|
2838
|
+
supportsAdminManagedSetupOverrides: true,
|
|
2839
|
+
supportsPackageScriptSetupOverrides: false
|
|
2840
|
+
}
|
|
2841
|
+
});
|
|
2842
|
+
}
|
|
2843
|
+
if (url.includes('/api/taskforce/taxonomy-library')) {
|
|
2844
|
+
return jsonResponse({
|
|
2845
|
+
packs: [{
|
|
2846
|
+
id: 'general',
|
|
2847
|
+
label: 'General',
|
|
2848
|
+
description: 'General starter',
|
|
2849
|
+
categories: [{ value: 'general', label: 'General' }],
|
|
2850
|
+
types: [{ value: 'task', label: 'Task' }],
|
|
2851
|
+
priorities: [{ value: 1, label: 'Low' }],
|
|
2852
|
+
isDefaultStarter: true
|
|
2853
|
+
}]
|
|
2854
|
+
});
|
|
2855
|
+
}
|
|
2856
|
+
if (url.includes('/api/taskforce/workspaces')) {
|
|
2857
|
+
return jsonResponse({
|
|
2858
|
+
success: true,
|
|
2859
|
+
currentWorkspaceId: 'workspace-cloud-active',
|
|
2860
|
+
workspaces: [
|
|
2861
|
+
{ id: 'workspace-cloud-active', slug: 'workspace-cloud-active', name: 'Cloud Active', role: 'owner' }
|
|
2862
|
+
]
|
|
2863
|
+
});
|
|
2864
|
+
}
|
|
2865
|
+
if (url.includes('/api/taskforce/tasks'))
|
|
2866
|
+
return jsonResponse({ tasks: [] });
|
|
2867
|
+
if (url.includes('/api/taskforce/archive'))
|
|
2868
|
+
return jsonResponse({ archived: [] });
|
|
2869
|
+
if (url.includes('/api/taskforce/categories'))
|
|
2870
|
+
return jsonResponse(mockCategories);
|
|
2871
|
+
if (url.includes('/api/taskforce/types'))
|
|
2872
|
+
return jsonResponse({ types: [] });
|
|
2873
|
+
if (url.includes('/api/taskforce/priorities'))
|
|
2874
|
+
return jsonResponse({ priorities: [] });
|
|
2875
|
+
if (url.includes('/api/taskforce/specialists'))
|
|
2876
|
+
return jsonResponse({ specialists: [] });
|
|
2877
|
+
if (url.includes('/api/taskforce/workflow-templates'))
|
|
2878
|
+
return jsonResponse({ templates: [] });
|
|
2879
|
+
if (url.includes('/api/taskforce/ui-state'))
|
|
2880
|
+
return jsonResponse({ success: true });
|
|
2881
|
+
return jsonResponse({});
|
|
2882
|
+
});
|
|
2883
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
2884
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/setup?step=workspace&intent=create-workspace'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
|
|
2885
|
+
await waitFor(() => {
|
|
2886
|
+
expect(screen.getByText('Set up the new workspace before it is created.')).toBeInTheDocument();
|
|
2887
|
+
});
|
|
2888
|
+
await user.click(screen.getByRole('button', { name: 'Cancel' }));
|
|
2889
|
+
await waitFor(() => {
|
|
2890
|
+
expect(screen.getByTitle('Workspace ID: workspace-cloud-active')).toBeInTheDocument();
|
|
2891
|
+
});
|
|
2892
|
+
expect(screen.queryByText('Set up the new workspace before it is created.')).not.toBeInTheDocument();
|
|
2893
|
+
});
|
|
2894
|
+
it('completes local first-run setup, survives refresh, and provisions cloud workspace on sync', async () => {
|
|
2895
|
+
const user = userEvent.setup();
|
|
2896
|
+
let workspaceSaved = false;
|
|
2897
|
+
let workspaceSyncState = {
|
|
2898
|
+
cloudSyncEnabled: false,
|
|
2899
|
+
sourceOfTruth: null,
|
|
2900
|
+
onboardingCompleted: false
|
|
2901
|
+
};
|
|
2902
|
+
let cloudWorkspaceCreated = false;
|
|
2903
|
+
const fetchMock = vi.fn((url, init) => {
|
|
2904
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
2905
|
+
return Promise.resolve({
|
|
2906
|
+
ok: true,
|
|
2907
|
+
json: () => Promise.resolve({
|
|
2908
|
+
authenticated: true,
|
|
2909
|
+
runtimeMode: 'local',
|
|
2910
|
+
authRequiredForApi: false,
|
|
2911
|
+
workspaceId: workspaceSaved ? 'workspace-local-flow-1' : 'default'
|
|
2912
|
+
})
|
|
2913
|
+
});
|
|
2914
|
+
}
|
|
2915
|
+
if (url.includes('/api/taskforce/config')) {
|
|
2916
|
+
return Promise.resolve({
|
|
2917
|
+
ok: true,
|
|
2918
|
+
json: () => Promise.resolve({
|
|
2919
|
+
...mockConfig,
|
|
2920
|
+
runtimeMode: 'local',
|
|
2921
|
+
authRequiredForApi: false,
|
|
2922
|
+
workspaceId: workspaceSaved ? 'workspace-local-flow-1' : 'default',
|
|
2923
|
+
setupState: {
|
|
2924
|
+
globalSetupState: 'present',
|
|
2925
|
+
workspaceSetupState: workspaceSaved ? 'present' : 'missing',
|
|
2926
|
+
runtimeMode: 'local',
|
|
2927
|
+
workspaceId: workspaceSaved ? 'workspace-local-flow-1' : 'default',
|
|
2928
|
+
mode: 'core',
|
|
2929
|
+
forceSetup: false,
|
|
2930
|
+
forceGlobalSetup: false,
|
|
2931
|
+
forceWorkspaceSetup: !workspaceSaved,
|
|
2932
|
+
workspace: workspaceSaved
|
|
2933
|
+
? { id: 'workspace-local-flow-1', slug: 'workspace-local-flow-1', name: 'Local Flow Project', description: 'Flow desc' }
|
|
2934
|
+
: null,
|
|
2935
|
+
suggestedWorkspaceName: 'Suggested Project'
|
|
2936
|
+
},
|
|
2937
|
+
runtimeCapabilities: {
|
|
2938
|
+
runtimeMode: 'local',
|
|
2939
|
+
supportsCloudAuth: true,
|
|
2940
|
+
supportsAdminManagedSetupOverrides: false,
|
|
2941
|
+
supportsPackageScriptSetupOverrides: true
|
|
2942
|
+
}
|
|
2943
|
+
})
|
|
2944
|
+
});
|
|
2945
|
+
}
|
|
2946
|
+
if (url.includes('/api/taskforce/workspace-profile')) {
|
|
2947
|
+
workspaceSaved = true;
|
|
2948
|
+
return Promise.resolve({
|
|
2949
|
+
ok: true,
|
|
2950
|
+
json: () => Promise.resolve({
|
|
2951
|
+
success: true,
|
|
2952
|
+
workspace: { id: 'workspace-local-flow-1' }
|
|
2953
|
+
})
|
|
2954
|
+
});
|
|
2955
|
+
}
|
|
2956
|
+
if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
|
|
2957
|
+
return Promise.resolve({
|
|
2958
|
+
ok: true,
|
|
2959
|
+
json: () => Promise.resolve({
|
|
2960
|
+
success: true,
|
|
2961
|
+
state: workspaceSyncState
|
|
2962
|
+
})
|
|
2963
|
+
});
|
|
2964
|
+
}
|
|
2965
|
+
if (url.includes('/api/taskforce/ui-state') && String(init?.method || '').toUpperCase() === 'POST') {
|
|
2966
|
+
const body = JSON.parse(String(init?.body || '{}'));
|
|
2967
|
+
if (body?.stateKey === 'workspace-sync' && body?.patch && typeof body.patch === 'object') {
|
|
2968
|
+
// Merge patch fields only (mirrors server behaviour) so partial patches
|
|
2969
|
+
// like { onboardingCompleted: true } don't reset cloudSyncEnabled to false.
|
|
2970
|
+
if (body.patch.cloudSyncEnabled !== undefined)
|
|
2971
|
+
workspaceSyncState.cloudSyncEnabled = Boolean(body.patch.cloudSyncEnabled);
|
|
2972
|
+
if (body.patch.sourceOfTruth !== undefined)
|
|
2973
|
+
workspaceSyncState.sourceOfTruth = (body.patch.sourceOfTruth === 'cloud' || body.patch.sourceOfTruth === 'local') ? body.patch.sourceOfTruth : null;
|
|
1018
2974
|
if (body.patch.onboardingCompleted !== undefined)
|
|
1019
2975
|
workspaceSyncState.onboardingCompleted = Boolean(body.patch.onboardingCompleted);
|
|
1020
2976
|
}
|
|
1021
2977
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
|
|
1022
2978
|
}
|
|
1023
|
-
if (url.includes('/api/taskforce/sync/workspace/handshake')) {
|
|
1024
|
-
if (!cloudWorkspaceCreated) {
|
|
1025
|
-
return Promise.resolve({
|
|
1026
|
-
ok: false,
|
|
1027
|
-
status: 409,
|
|
1028
|
-
json: () => Promise.resolve({
|
|
1029
|
-
success: false,
|
|
1030
|
-
code: 'WORKSPACE_ID_MISMATCH',
|
|
1031
|
-
error: 'Workspace mismatch'
|
|
1032
|
-
})
|
|
1033
|
-
});
|
|
1034
|
-
}
|
|
2979
|
+
if (url.includes('/api/taskforce/sync/workspace/handshake')) {
|
|
2980
|
+
if (!cloudWorkspaceCreated) {
|
|
2981
|
+
return Promise.resolve({
|
|
2982
|
+
ok: false,
|
|
2983
|
+
status: 409,
|
|
2984
|
+
json: () => Promise.resolve({
|
|
2985
|
+
success: false,
|
|
2986
|
+
code: 'WORKSPACE_ID_MISMATCH',
|
|
2987
|
+
error: 'Workspace mismatch'
|
|
2988
|
+
})
|
|
2989
|
+
});
|
|
2990
|
+
}
|
|
2991
|
+
return Promise.resolve({
|
|
2992
|
+
ok: true,
|
|
2993
|
+
json: () => Promise.resolve({
|
|
2994
|
+
success: true,
|
|
2995
|
+
localWorkspaceId: 'workspace-local-flow-1',
|
|
2996
|
+
cloudWorkspaceId: 'workspace-local-flow-1'
|
|
2997
|
+
})
|
|
2998
|
+
});
|
|
2999
|
+
}
|
|
3000
|
+
if (url.includes('/api/taskforce/sync/workspace/provision')) {
|
|
3001
|
+
cloudWorkspaceCreated = true;
|
|
3002
|
+
return Promise.resolve({
|
|
3003
|
+
ok: true,
|
|
3004
|
+
status: 201,
|
|
3005
|
+
json: () => Promise.resolve({
|
|
3006
|
+
success: true,
|
|
3007
|
+
existing: false,
|
|
3008
|
+
workspace: { id: 'workspace-local-flow-1', name: 'Local Flow Project' }
|
|
3009
|
+
})
|
|
3010
|
+
});
|
|
3011
|
+
}
|
|
3012
|
+
if (url.includes('/api/taskforce/sync/workspace/pull')) {
|
|
3013
|
+
return Promise.resolve({
|
|
3014
|
+
ok: true,
|
|
3015
|
+
json: () => Promise.resolve({
|
|
3016
|
+
success: true,
|
|
3017
|
+
workspaceId: 'workspace-local-flow-1',
|
|
3018
|
+
changes: [],
|
|
3019
|
+
nextCursor: '',
|
|
3020
|
+
hasMore: false
|
|
3021
|
+
})
|
|
3022
|
+
});
|
|
3023
|
+
}
|
|
3024
|
+
if (url.includes('/api/taskforce/sync/workspace/push')) {
|
|
3025
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
|
|
3026
|
+
}
|
|
3027
|
+
if (url.includes('/api/taskforce/categories'))
|
|
3028
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve(mockCategories) });
|
|
3029
|
+
if (url.includes('/api/taskforce/tasks'))
|
|
3030
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
|
|
3031
|
+
if (url.includes('/api/taskforce/archive'))
|
|
3032
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ archived: [] }) });
|
|
3033
|
+
if (url.includes('/api/taskforce/types'))
|
|
3034
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ types: [] }) });
|
|
3035
|
+
if (url.includes('/api/taskforce/priorities'))
|
|
3036
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ priorities: [] }) });
|
|
3037
|
+
if (url.includes('/api/taskforce/approaches'))
|
|
3038
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ approaches: [] }) });
|
|
3039
|
+
if (url.includes('/api/taskforce/specialists'))
|
|
3040
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
|
|
3041
|
+
if (url.includes('/api/taskforce/workflow-templates'))
|
|
3042
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
3043
|
+
if (url.includes('/api/taskforce/sync/workspace/documents'))
|
|
3044
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ documents: [] }) });
|
|
3045
|
+
if (url.includes('/api/taskforce/sync/workspace/annotated-attachments'))
|
|
3046
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ sessions: [] }) });
|
|
3047
|
+
if (url.includes('/api/taskforce/sync/user-settings'))
|
|
3048
|
+
return jsonResponse({ settings: {}, updatedAt: '2026-03-15T14:28:16.000Z' });
|
|
3049
|
+
if (url.includes('/api/taskforce/sync/workspace/handshake'))
|
|
3050
|
+
return jsonResponse({ success: true });
|
|
3051
|
+
if (url.includes('/api/taskforce/sync/workspace/pull'))
|
|
3052
|
+
return jsonResponse({ success: true, changes: [], hasMore: false, nextCursor: null });
|
|
3053
|
+
if (url.includes('/api/taskforce/sync/workspace/push'))
|
|
3054
|
+
return jsonResponse({ success: true, syncedAt: '2026-03-15T14:28:16.000Z' });
|
|
3055
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
3056
|
+
});
|
|
3057
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
3058
|
+
const initialRender = render(_jsx(MemoryRouter, { initialEntries: ['/setup?step=workspace'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
|
|
3059
|
+
await waitFor(() => {
|
|
3060
|
+
expect(screen.getByText('Workspace Setup Required')).toBeInTheDocument();
|
|
3061
|
+
});
|
|
3062
|
+
await waitFor(() => {
|
|
3063
|
+
expect(screen.getByRole('checkbox', { name: /sync to cloud after setup/i })).toBeInTheDocument();
|
|
3064
|
+
});
|
|
3065
|
+
await user.click(screen.getByRole('checkbox', { name: /sync to cloud after setup/i }));
|
|
3066
|
+
const nameInput = screen.getByPlaceholderText('Project name');
|
|
3067
|
+
await user.clear(nameInput);
|
|
3068
|
+
await user.type(nameInput, 'Local Flow Project');
|
|
3069
|
+
await user.click(screen.getByRole('button', { name: 'Save Project Setup' }));
|
|
3070
|
+
await waitFor(() => {
|
|
3071
|
+
expect(workspaceSaved).toBe(true);
|
|
3072
|
+
});
|
|
3073
|
+
initialRender.unmount();
|
|
3074
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
|
|
3075
|
+
await waitFor(() => {
|
|
3076
|
+
expect(screen.queryByText('Workspace Setup Required')).not.toBeInTheDocument();
|
|
3077
|
+
expect(screen.getByRole('button', { name: /add task/i })).toBeInTheDocument();
|
|
3078
|
+
});
|
|
3079
|
+
expect(cloudWorkspaceCreated).toBe(false);
|
|
3080
|
+
});
|
|
3081
|
+
it('shows local runtime Sign In / Register action and routes to shared login', async () => {
|
|
3082
|
+
const standaloneLayoutPath = path.join(process.cwd(), 'src/components/views/StandaloneLayout.tsx');
|
|
3083
|
+
const standaloneLayoutSource = fs.readFileSync(standaloneLayoutPath, 'utf8');
|
|
3084
|
+
expect(standaloneLayoutSource).toContain('Sign In / Register');
|
|
3085
|
+
expect(standaloneLayoutSource).toContain('openSharedLoginRoute(\'login\')');
|
|
3086
|
+
expect(standaloneLayoutSource).toContain('navigate(`/login?mode=${mode}&next=${encodeURIComponent(target)}`)');
|
|
3087
|
+
});
|
|
3088
|
+
it('keeps workspace management cloud-only while allowing cloud-backed team management from local runtime', () => {
|
|
3089
|
+
const standaloneLayoutPath = path.join(process.cwd(), 'src/components/views/StandaloneLayout.tsx');
|
|
3090
|
+
const standaloneLayoutSource = fs.readFileSync(standaloneLayoutPath, 'utf8');
|
|
3091
|
+
const useTaskforcePath = path.join(process.cwd(), 'src/hooks/useTaskforce.ts');
|
|
3092
|
+
const useTaskforceSource = fs.readFileSync(useTaskforcePath, 'utf8');
|
|
3093
|
+
expect(standaloneLayoutSource).toContain("const canManageCloudWorkspaces = runtimeMode === 'cloud' && workspaceSwitchingEnabled && isAuthenticated;");
|
|
3094
|
+
expect(standaloneLayoutSource).toContain("const canAccessCloudTeamManagement = isAuthenticated && (runtimeMode === 'cloud' || cloudAuthConfigured);");
|
|
3095
|
+
expect(standaloneLayoutSource).toContain("const teamManagementWorkspaceId = String(authWorkspaceId || currentWorkspaceId || '').trim();");
|
|
3096
|
+
expect(standaloneLayoutSource).toContain('computeCanOpenTeamManagement(canAccessCloudTeamManagement, currentWorkspaceRole, teamPlanMode)');
|
|
3097
|
+
expect(useTaskforceSource).toContain("const [authWorkspaceId, setAuthWorkspaceId] = useState<string>('');");
|
|
3098
|
+
});
|
|
3099
|
+
it('routes local plans and billing UI through the connected cloud billing base', () => {
|
|
3100
|
+
const standaloneLayoutPath = path.join(process.cwd(), 'src/components/views/StandaloneLayout.tsx');
|
|
3101
|
+
const standaloneLayoutSource = fs.readFileSync(standaloneLayoutPath, 'utf8');
|
|
3102
|
+
expect(standaloneLayoutSource).toContain("const billingApiBaseUrl = useMemo(() => {");
|
|
3103
|
+
expect(standaloneLayoutSource).toContain("const res = await fetch(buildBillingApiUrl('/api/taskforce/billing/status'), {");
|
|
3104
|
+
expect(standaloneLayoutSource).toContain("apiBaseUrl={billingApiBaseUrl}");
|
|
3105
|
+
});
|
|
3106
|
+
it('uses shared /login flow for local sign in instead of a duplicate modal', async () => {
|
|
3107
|
+
const standaloneLayoutPath = path.join(process.cwd(), 'src/components/views/StandaloneLayout.tsx');
|
|
3108
|
+
const standaloneLayoutSource = fs.readFileSync(standaloneLayoutPath, 'utf8');
|
|
3109
|
+
const useTaskforcePath = path.join(process.cwd(), 'src/hooks/useTaskforce.ts');
|
|
3110
|
+
const useTaskforceSource = fs.readFileSync(useTaskforcePath, 'utf8');
|
|
3111
|
+
expect(standaloneLayoutSource).not.toContain('showCloudSignInModal');
|
|
3112
|
+
expect(standaloneLayoutSource).not.toContain('cloudSignInEmail');
|
|
3113
|
+
expect(useTaskforceSource).toContain("resolveCloudAuthUrl('/api/taskforce/auth/login')");
|
|
3114
|
+
expect(useTaskforceSource).toContain("return { success: false, error: 'Sign in failed.'");
|
|
3115
|
+
});
|
|
3116
|
+
it('does not promote auth state from generic task fetch success', async () => {
|
|
3117
|
+
const useTaskforcePath = path.join(process.cwd(), 'src/hooks/useTaskforce.ts');
|
|
3118
|
+
const useTaskforceSource = fs.readFileSync(useTaskforcePath, 'utf8');
|
|
3119
|
+
// Auth state should be derived from session/login flows, not inferred from /tasks success.
|
|
3120
|
+
expect(useTaskforceSource).not.toContain('setIsAuthenticated(true);');
|
|
3121
|
+
});
|
|
3122
|
+
it('uses production cloud fallback for local runtime auth/sync endpoints', async () => {
|
|
3123
|
+
const useTaskforcePath = path.join(process.cwd(), 'src/hooks/useTaskforce.ts');
|
|
3124
|
+
const useTaskforceSource = fs.readFileSync(useTaskforcePath, 'utf8');
|
|
3125
|
+
const appMainPath = path.join(process.cwd(), 'apps/app/main.tsx');
|
|
3126
|
+
const appMainSource = fs.readFileSync(appMainPath, 'utf8');
|
|
3127
|
+
expect(useTaskforceSource).toContain("return 'https://app.taskforcehq.ai';");
|
|
3128
|
+
expect(appMainSource).toContain('resolveClientDeploymentConfig');
|
|
3129
|
+
expect(appMainSource).toContain('return undefined;');
|
|
3130
|
+
});
|
|
3131
|
+
it('passes runtime mode and raw api config through to the document workspace shell', () => {
|
|
3132
|
+
const standaloneLayoutPath = path.join(process.cwd(), 'src/components/views/StandaloneLayout.tsx');
|
|
3133
|
+
const standaloneLayoutSource = fs.readFileSync(standaloneLayoutPath, 'utf8');
|
|
3134
|
+
const docsShellMatch = standaloneLayoutSource.match(/<DocumentWorkspaceShell[\s\S]*?\/>/);
|
|
3135
|
+
const docsShellSource = docsShellMatch?.[0] || '';
|
|
3136
|
+
expect(docsShellSource).toContain("runtimeMode={runtimeMode}");
|
|
3137
|
+
expect(docsShellSource).toContain("apiBaseUrl={props.config?.apiBaseUrl || ''}");
|
|
3138
|
+
expect(docsShellSource).toContain("cloudAuthBaseUrl={props.config?.cloudAuthBaseUrl || ''}");
|
|
3139
|
+
});
|
|
3140
|
+
it('keeps local-only runtime behavior when signed out (no workspace cloud pull calls)', async () => {
|
|
3141
|
+
const fetchMock = vi.fn((url) => {
|
|
3142
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
3143
|
+
return Promise.resolve({
|
|
3144
|
+
ok: true,
|
|
3145
|
+
json: () => Promise.resolve({
|
|
3146
|
+
authenticated: false,
|
|
3147
|
+
runtimeMode: 'local',
|
|
3148
|
+
authRequiredForApi: false
|
|
3149
|
+
})
|
|
3150
|
+
});
|
|
3151
|
+
}
|
|
3152
|
+
if (url.includes('/api/taskforce/config')) {
|
|
3153
|
+
return Promise.resolve({
|
|
3154
|
+
ok: true,
|
|
3155
|
+
json: () => Promise.resolve({
|
|
3156
|
+
...mockConfig,
|
|
3157
|
+
runtimeMode: 'local',
|
|
3158
|
+
authRequiredForApi: false,
|
|
3159
|
+
workspaceId: 'workspace-local-active'
|
|
3160
|
+
})
|
|
3161
|
+
});
|
|
3162
|
+
}
|
|
3163
|
+
if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
|
|
3164
|
+
return Promise.resolve({
|
|
3165
|
+
ok: true,
|
|
3166
|
+
json: () => Promise.resolve({
|
|
3167
|
+
success: true,
|
|
3168
|
+
state: {
|
|
3169
|
+
cloudSyncEnabled: true,
|
|
3170
|
+
sourceOfTruth: 'cloud',
|
|
3171
|
+
onboardingCompleted: true,
|
|
3172
|
+
pullCursor: ''
|
|
3173
|
+
}
|
|
3174
|
+
})
|
|
3175
|
+
});
|
|
3176
|
+
}
|
|
3177
|
+
if (url.includes('/api/taskforce/categories'))
|
|
3178
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve(mockCategories) });
|
|
3179
|
+
if (url.includes('/api/taskforce/tasks'))
|
|
3180
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
|
|
3181
|
+
if (url.includes('/api/taskforce/archive'))
|
|
3182
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ archived: [] }) });
|
|
3183
|
+
if (url.includes('/api/taskforce/types'))
|
|
3184
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ types: [] }) });
|
|
3185
|
+
if (url.includes('/api/taskforce/priorities'))
|
|
3186
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ priorities: [] }) });
|
|
3187
|
+
if (url.includes('/api/taskforce/approaches'))
|
|
3188
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ approaches: [] }) });
|
|
3189
|
+
if (url.includes('/api/taskforce/specialists'))
|
|
3190
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
|
|
3191
|
+
if (url.includes('/api/taskforce/workflow-templates'))
|
|
3192
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
3193
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
3194
|
+
});
|
|
3195
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
3196
|
+
render(_jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }));
|
|
3197
|
+
await waitFor(() => {
|
|
3198
|
+
expect(screen.getByTitle(/Account/)).toBeInTheDocument();
|
|
3199
|
+
});
|
|
3200
|
+
await waitFor(() => {
|
|
3201
|
+
const uiStateCalls = fetchMock.mock.calls.filter((call) => typeof call[0] === 'string' && String(call[0]).includes('/api/taskforce/ui-state?key=workspace-sync'));
|
|
3202
|
+
expect(uiStateCalls.length).toBeGreaterThan(0);
|
|
3203
|
+
});
|
|
3204
|
+
const pullCalls = fetchMock.mock.calls.filter((call) => typeof call[0] === 'string' && String(call[0]).includes('/sync/workspace/pull'));
|
|
3205
|
+
expect(pullCalls.length).toBe(0);
|
|
3206
|
+
});
|
|
3207
|
+
it('does not pull from cloud when workspace sync toggle is disabled', async () => {
|
|
3208
|
+
const fetchMock = vi.fn((url) => {
|
|
3209
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
3210
|
+
return Promise.resolve({
|
|
3211
|
+
ok: true,
|
|
3212
|
+
json: () => Promise.resolve({
|
|
3213
|
+
authenticated: true,
|
|
3214
|
+
runtimeMode: 'local',
|
|
3215
|
+
authRequiredForApi: false
|
|
3216
|
+
})
|
|
3217
|
+
});
|
|
3218
|
+
}
|
|
3219
|
+
if (url.includes('/api/taskforce/config')) {
|
|
3220
|
+
return Promise.resolve({
|
|
3221
|
+
ok: true,
|
|
3222
|
+
json: () => Promise.resolve({
|
|
3223
|
+
...mockConfig,
|
|
3224
|
+
runtimeMode: 'local',
|
|
3225
|
+
authRequiredForApi: false,
|
|
3226
|
+
workspaceId: 'workspace-local-active'
|
|
3227
|
+
})
|
|
3228
|
+
});
|
|
3229
|
+
}
|
|
3230
|
+
if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
|
|
3231
|
+
return Promise.resolve({
|
|
3232
|
+
ok: true,
|
|
3233
|
+
json: () => Promise.resolve({
|
|
3234
|
+
success: true,
|
|
3235
|
+
state: {
|
|
3236
|
+
cloudSyncEnabled: false,
|
|
3237
|
+
sourceOfTruth: 'cloud',
|
|
3238
|
+
onboardingCompleted: true,
|
|
3239
|
+
pullCursor: ''
|
|
3240
|
+
}
|
|
3241
|
+
})
|
|
3242
|
+
});
|
|
3243
|
+
}
|
|
3244
|
+
if (url.includes('/api/taskforce/categories'))
|
|
3245
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve(mockCategories) });
|
|
3246
|
+
if (url.includes('/api/taskforce/tasks'))
|
|
3247
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
|
|
3248
|
+
if (url.includes('/api/taskforce/archive'))
|
|
3249
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ archived: [] }) });
|
|
3250
|
+
if (url.includes('/api/taskforce/types'))
|
|
3251
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ types: [] }) });
|
|
3252
|
+
if (url.includes('/api/taskforce/priorities'))
|
|
3253
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ priorities: [] }) });
|
|
3254
|
+
if (url.includes('/api/taskforce/approaches'))
|
|
3255
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ approaches: [] }) });
|
|
3256
|
+
if (url.includes('/api/taskforce/specialists'))
|
|
3257
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
|
|
3258
|
+
if (url.includes('/api/taskforce/workflow-templates'))
|
|
3259
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
3260
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
3261
|
+
});
|
|
3262
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
3263
|
+
render(_jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }));
|
|
3264
|
+
await waitFor(() => {
|
|
3265
|
+
expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
|
|
3266
|
+
});
|
|
3267
|
+
await waitFor(() => {
|
|
3268
|
+
const uiStateCalls = fetchMock.mock.calls.filter((call) => typeof call[0] === 'string' && String(call[0]).includes('/api/taskforce/ui-state?key=workspace-sync'));
|
|
3269
|
+
expect(uiStateCalls.length).toBeGreaterThan(0);
|
|
3270
|
+
});
|
|
3271
|
+
const pullCalls = fetchMock.mock.calls.filter((call) => typeof call[0] === 'string' && String(call[0]).includes('/sync/workspace/pull'));
|
|
3272
|
+
expect(pullCalls.length).toBe(0);
|
|
3273
|
+
});
|
|
3274
|
+
it('does not repeatedly reload workspace-sync ui-state on equivalent rerenders', async () => {
|
|
3275
|
+
let workspaceSyncReads = 0;
|
|
3276
|
+
const fetchMock = vi.fn((url) => {
|
|
3277
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
3278
|
+
return Promise.resolve({
|
|
3279
|
+
ok: true,
|
|
3280
|
+
json: () => Promise.resolve({
|
|
3281
|
+
authenticated: true,
|
|
3282
|
+
runtimeMode: 'local',
|
|
3283
|
+
authRequiredForApi: false
|
|
3284
|
+
})
|
|
3285
|
+
});
|
|
3286
|
+
}
|
|
3287
|
+
if (url.includes('/api/taskforce/config')) {
|
|
3288
|
+
return Promise.resolve({
|
|
3289
|
+
ok: true,
|
|
3290
|
+
json: () => Promise.resolve({
|
|
3291
|
+
...mockConfig,
|
|
3292
|
+
runtimeMode: 'local',
|
|
3293
|
+
authRequiredForApi: false,
|
|
3294
|
+
workspaceId: 'workspace-local-active'
|
|
3295
|
+
})
|
|
3296
|
+
});
|
|
3297
|
+
}
|
|
3298
|
+
if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
|
|
3299
|
+
workspaceSyncReads += 1;
|
|
3300
|
+
return Promise.resolve({
|
|
3301
|
+
ok: true,
|
|
3302
|
+
json: () => Promise.resolve({
|
|
3303
|
+
success: true,
|
|
3304
|
+
state: {
|
|
3305
|
+
cloudSyncEnabled: true,
|
|
3306
|
+
sourceOfTruth: 'local',
|
|
3307
|
+
onboardingCompleted: true,
|
|
3308
|
+
pullCursor: ''
|
|
3309
|
+
}
|
|
3310
|
+
})
|
|
3311
|
+
});
|
|
3312
|
+
}
|
|
3313
|
+
if (url.includes('/api/taskforce/ui-state?key=app')) {
|
|
3314
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true, state: {} }) });
|
|
3315
|
+
}
|
|
3316
|
+
if (url.includes('/api/taskforce/categories'))
|
|
3317
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve(mockCategories) });
|
|
3318
|
+
if (url.includes('/api/taskforce/tasks'))
|
|
3319
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
|
|
3320
|
+
if (url.includes('/api/taskforce/archive'))
|
|
3321
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ archived: [] }) });
|
|
3322
|
+
if (url.includes('/api/taskforce/types'))
|
|
3323
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ types: [] }) });
|
|
3324
|
+
if (url.includes('/api/taskforce/priorities'))
|
|
3325
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ priorities: [] }) });
|
|
3326
|
+
if (url.includes('/api/taskforce/approaches'))
|
|
3327
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ approaches: [] }) });
|
|
3328
|
+
if (url.includes('/api/taskforce/specialists'))
|
|
3329
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
|
|
3330
|
+
if (url.includes('/api/taskforce/workflow-templates'))
|
|
3331
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
3332
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
3333
|
+
});
|
|
3334
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
3335
|
+
const { rerender } = render(_jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }));
|
|
3336
|
+
await waitFor(() => {
|
|
3337
|
+
expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
|
|
3338
|
+
});
|
|
3339
|
+
await waitFor(() => {
|
|
3340
|
+
expect(workspaceSyncReads).toBeGreaterThan(0);
|
|
3341
|
+
});
|
|
3342
|
+
const baselineReads = workspaceSyncReads;
|
|
3343
|
+
rerender(_jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }));
|
|
3344
|
+
await new Promise((resolve) => window.setTimeout(resolve, 150));
|
|
3345
|
+
expect(workspaceSyncReads).toBe(baselineReads);
|
|
3346
|
+
});
|
|
3347
|
+
it('sync status modal focuses on the lean summary instead of internal sync fields', async () => {
|
|
3348
|
+
const user = userEvent.setup();
|
|
3349
|
+
const fetchMock = vi.fn((url) => {
|
|
3350
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
3351
|
+
return Promise.resolve({
|
|
3352
|
+
ok: true,
|
|
3353
|
+
json: () => Promise.resolve({
|
|
3354
|
+
authenticated: true,
|
|
3355
|
+
runtimeMode: 'local',
|
|
3356
|
+
authRequiredForApi: false
|
|
3357
|
+
})
|
|
3358
|
+
});
|
|
3359
|
+
}
|
|
3360
|
+
if (url.includes('/api/taskforce/config')) {
|
|
3361
|
+
return Promise.resolve({
|
|
3362
|
+
ok: true,
|
|
3363
|
+
json: () => Promise.resolve({
|
|
3364
|
+
...mockConfig,
|
|
3365
|
+
runtimeMode: 'local',
|
|
3366
|
+
authRequiredForApi: false,
|
|
3367
|
+
workspaceId: 'workspace-local-active'
|
|
3368
|
+
})
|
|
3369
|
+
});
|
|
3370
|
+
}
|
|
3371
|
+
if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
|
|
3372
|
+
return Promise.resolve({
|
|
3373
|
+
ok: true,
|
|
3374
|
+
json: () => Promise.resolve({
|
|
3375
|
+
success: true,
|
|
3376
|
+
state: {
|
|
3377
|
+
version: 2,
|
|
3378
|
+
enabled: true,
|
|
3379
|
+
phase: 'active',
|
|
3380
|
+
pullCursor: null,
|
|
3381
|
+
lastPullAt: '2026-03-15T14:28:16.000Z',
|
|
3382
|
+
lastPushAt: '2026-03-15T14:28:16.000Z',
|
|
3383
|
+
lastSyncedAt: '2026-03-15T14:28:16.000Z'
|
|
3384
|
+
}
|
|
3385
|
+
})
|
|
3386
|
+
});
|
|
3387
|
+
}
|
|
3388
|
+
if (url.includes('/api/taskforce/ui-state?key=app')) {
|
|
3389
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true, state: {} }) });
|
|
3390
|
+
}
|
|
3391
|
+
if (url.includes('/api/taskforce/categories'))
|
|
3392
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve(mockCategories) });
|
|
3393
|
+
if (url.includes('/api/taskforce/tasks'))
|
|
3394
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
|
|
3395
|
+
if (url.includes('/api/taskforce/archive'))
|
|
3396
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ archived: [] }) });
|
|
3397
|
+
if (url.includes('/api/taskforce/types'))
|
|
3398
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ types: [] }) });
|
|
3399
|
+
if (url.includes('/api/taskforce/priorities'))
|
|
3400
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ priorities: [] }) });
|
|
3401
|
+
if (url.includes('/api/taskforce/approaches'))
|
|
3402
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ approaches: [] }) });
|
|
3403
|
+
if (url.includes('/api/taskforce/specialists'))
|
|
3404
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
|
|
3405
|
+
if (url.includes('/api/taskforce/workflow-templates'))
|
|
3406
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
3407
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
3408
|
+
});
|
|
3409
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
3410
|
+
render(_jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }));
|
|
3411
|
+
await waitFor(() => {
|
|
3412
|
+
expect(screen.getByTitle(/Sync manager:/i)).toBeInTheDocument();
|
|
3413
|
+
});
|
|
3414
|
+
await user.click(screen.getByTitle(/Sync manager:/i));
|
|
3415
|
+
await waitFor(() => {
|
|
3416
|
+
expect(screen.getByText('Sync Manager')).toBeInTheDocument();
|
|
3417
|
+
const dialog = screen.getByText('Sync Manager').closest(`.${modalStyles.modal}`);
|
|
3418
|
+
expect(within(dialog).getByText('Status')).toBeInTheDocument();
|
|
3419
|
+
expect(within(dialog).getByText('Summary')).toBeInTheDocument();
|
|
3420
|
+
expect(within(dialog).getByText('Last successful sync')).toBeInTheDocument();
|
|
3421
|
+
expect(within(dialog).getByText('Last pull from cloud')).toBeInTheDocument();
|
|
3422
|
+
expect(within(dialog).getByText('Last push to cloud')).toBeInTheDocument();
|
|
3423
|
+
expect(within(dialog).getByText('Pending local changes')).toBeInTheDocument();
|
|
3424
|
+
expect(within(dialog).getByText('Last error')).toBeInTheDocument();
|
|
3425
|
+
expect(within(dialog).getByText('Recommended action')).toBeInTheDocument();
|
|
3426
|
+
expect(within(dialog).getByText('Local sync diagnostics')).toBeInTheDocument();
|
|
3427
|
+
expect(within(dialog).getByText('Recent sync events')).toBeInTheDocument();
|
|
3428
|
+
expect(within(dialog).queryByText('Sync enabled')).not.toBeInTheDocument();
|
|
3429
|
+
expect(within(dialog).queryByText('Phase')).not.toBeInTheDocument();
|
|
3430
|
+
expect(within(dialog).queryByText('Runtime mode')).not.toBeInTheDocument();
|
|
3431
|
+
expect(within(dialog).queryByText('Sync source')).not.toBeInTheDocument();
|
|
3432
|
+
});
|
|
3433
|
+
});
|
|
3434
|
+
it('sync status modal shows sync controls, bootstrap summary, and copy action', async () => {
|
|
3435
|
+
const user = userEvent.setup();
|
|
3436
|
+
const writeText = vi.fn().mockResolvedValue(undefined);
|
|
3437
|
+
Object.defineProperty(navigator, 'clipboard', {
|
|
3438
|
+
configurable: true,
|
|
3439
|
+
value: { writeText }
|
|
3440
|
+
});
|
|
3441
|
+
const uiStatePosts = [];
|
|
3442
|
+
const fetchMock = vi.fn((url, init) => {
|
|
3443
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
3444
|
+
return Promise.resolve({
|
|
3445
|
+
ok: true,
|
|
3446
|
+
json: () => Promise.resolve({
|
|
3447
|
+
authenticated: true,
|
|
3448
|
+
runtimeMode: 'local',
|
|
3449
|
+
authRequiredForApi: false
|
|
3450
|
+
})
|
|
3451
|
+
});
|
|
3452
|
+
}
|
|
3453
|
+
if (url.includes('/api/taskforce/config')) {
|
|
3454
|
+
return Promise.resolve({
|
|
3455
|
+
ok: true,
|
|
3456
|
+
json: () => Promise.resolve({
|
|
3457
|
+
...mockConfig,
|
|
3458
|
+
runtimeMode: 'local',
|
|
3459
|
+
authRequiredForApi: false,
|
|
3460
|
+
workspaceId: 'workspace-local-active'
|
|
3461
|
+
})
|
|
3462
|
+
});
|
|
3463
|
+
}
|
|
3464
|
+
if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
|
|
3465
|
+
return Promise.resolve({
|
|
3466
|
+
ok: true,
|
|
3467
|
+
json: () => Promise.resolve({
|
|
3468
|
+
success: true,
|
|
3469
|
+
state: {
|
|
3470
|
+
version: 2,
|
|
3471
|
+
enabled: true,
|
|
3472
|
+
phase: 'provision-local',
|
|
3473
|
+
pullCursor: null,
|
|
3474
|
+
lastPullAt: null,
|
|
3475
|
+
lastPushAt: null,
|
|
3476
|
+
lastSyncedAt: null
|
|
3477
|
+
}
|
|
3478
|
+
})
|
|
3479
|
+
});
|
|
3480
|
+
}
|
|
3481
|
+
if (url.includes('/api/taskforce/ui-state') && String(init?.method || '').toUpperCase() === 'POST') {
|
|
3482
|
+
uiStatePosts.push(JSON.parse(String(init?.body || '{}')));
|
|
3483
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
|
|
3484
|
+
}
|
|
3485
|
+
if (url.includes('/api/taskforce/ui-state?key=app')) {
|
|
3486
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true, state: {} }) });
|
|
3487
|
+
}
|
|
3488
|
+
if (url.includes('/api/taskforce/categories'))
|
|
3489
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve(mockCategories) });
|
|
3490
|
+
if (url.includes('/api/taskforce/tasks'))
|
|
3491
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: mockTasks }) });
|
|
3492
|
+
if (url.includes('/api/taskforce/archive'))
|
|
3493
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ archived: [] }) });
|
|
3494
|
+
if (url.includes('/api/taskforce/types'))
|
|
3495
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ types: [] }) });
|
|
3496
|
+
if (url.includes('/api/taskforce/priorities'))
|
|
3497
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ priorities: [] }) });
|
|
3498
|
+
if (url.includes('/api/taskforce/approaches'))
|
|
3499
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ approaches: [] }) });
|
|
3500
|
+
if (url.includes('/api/taskforce/specialists'))
|
|
3501
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
|
|
3502
|
+
if (url.includes('/api/taskforce/workflow-templates'))
|
|
3503
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
3504
|
+
if (url.includes('/api/taskforce/sync/workspace/documents'))
|
|
3505
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ documents: [] }) });
|
|
3506
|
+
if (url.includes('/api/taskforce/sync/workspace/annotated-attachments'))
|
|
3507
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ sessions: [] }) });
|
|
3508
|
+
if (url.includes('/api/taskforce/sync/events')) {
|
|
3509
|
+
return Promise.resolve({
|
|
3510
|
+
ok: true,
|
|
3511
|
+
json: () => Promise.resolve({
|
|
3512
|
+
events: [
|
|
3513
|
+
{
|
|
3514
|
+
occurredAt: '2026-03-15T14:28:17.000Z',
|
|
3515
|
+
eventType: 'apply',
|
|
3516
|
+
status: 'error',
|
|
3517
|
+
errorMessage: 'Detected asset reference number mismatch during normal sync.',
|
|
3518
|
+
details: {
|
|
3519
|
+
path: 'workspaces/workspace-local-active/assets/images/hero.png',
|
|
3520
|
+
existingReferenceNumber: 12,
|
|
3521
|
+
incomingReferenceNumber: 18
|
|
3522
|
+
}
|
|
3523
|
+
},
|
|
3524
|
+
{
|
|
3525
|
+
occurredAt: '2026-03-15T14:28:16.000Z',
|
|
3526
|
+
eventType: 'push',
|
|
3527
|
+
status: 'success',
|
|
3528
|
+
changeCount: 1,
|
|
3529
|
+
requestMs: 42
|
|
3530
|
+
}
|
|
3531
|
+
]
|
|
3532
|
+
})
|
|
3533
|
+
});
|
|
3534
|
+
}
|
|
3535
|
+
if (url.includes('/api/taskforce/sync/user-settings'))
|
|
3536
|
+
return jsonResponse({ settings: {}, updatedAt: '2026-02-26T11:55:54.000Z' });
|
|
3537
|
+
if (url.includes('/api/taskforce/sync/workspace/handshake'))
|
|
3538
|
+
return jsonResponse({ success: true });
|
|
3539
|
+
if (url.includes('/api/taskforce/sync/workspace/push'))
|
|
3540
|
+
return new Promise(() => { });
|
|
3541
|
+
if (url.includes('/api/taskforce/sync/workspace/pull'))
|
|
3542
|
+
return jsonResponse({ changes: [], nextCursor: null, hasMore: false });
|
|
3543
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
3544
|
+
});
|
|
3545
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
3546
|
+
render(_jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }));
|
|
3547
|
+
await waitFor(() => {
|
|
3548
|
+
expect(screen.getByTitle(/Sync manager:/i)).toBeInTheDocument();
|
|
3549
|
+
});
|
|
3550
|
+
await user.click(screen.getByTitle(/Sync manager:/i));
|
|
3551
|
+
await screen.findByText('Sync Manager');
|
|
3552
|
+
const dialog = screen.getByText('Sync Manager').closest(`.${modalStyles.modal}`);
|
|
3553
|
+
expect(within(dialog).getByText('Current Session')).toBeInTheDocument();
|
|
3554
|
+
expect(within(dialog).getByText('Activity')).toBeInTheDocument();
|
|
3555
|
+
expect(within(dialog).getByText(/first upload to cloud/i)).toBeInTheDocument();
|
|
3556
|
+
expect(within(dialog).getByRole('switch', { name: 'Enable Sync' })).toBeChecked();
|
|
3557
|
+
expect(within(dialog).getByRole('button', { name: 'Copy Report' })).toBeInTheDocument();
|
|
3558
|
+
expect(within(dialog).getByText('AI profile snapshot')).toBeInTheDocument();
|
|
3559
|
+
expect(within(dialog).getByText('Last pushed AI profiles')).toBeInTheDocument();
|
|
3560
|
+
expect(within(dialog).getByText(/push succeeded \(1 change\) in 42ms/i)).toBeInTheDocument();
|
|
3561
|
+
expect(within(dialog).getByText('Identifier Integrity')).toBeInTheDocument();
|
|
3562
|
+
expect(within(dialog).getByText('workspaces/workspace-local-active/assets/images/hero.png')).toBeInTheDocument();
|
|
3563
|
+
expect(within(dialog).getByText('Existing 12 vs incoming 18')).toBeInTheDocument();
|
|
3564
|
+
expect(within(dialog).queryByText('Current Status')).not.toBeInTheDocument();
|
|
3565
|
+
expect(within(dialog).queryByRole('button', { name: 'Repair' })).toBeInTheDocument();
|
|
3566
|
+
await user.click(within(dialog).getByRole('button', { name: 'Copy Report' }));
|
|
3567
|
+
await waitFor(() => {
|
|
3568
|
+
expect(writeText).toHaveBeenCalledTimes(1);
|
|
3569
|
+
});
|
|
3570
|
+
expect(String(writeText.mock.calls[0][0])).toContain('Workspace ID: workspace-local-active');
|
|
3571
|
+
expect(String(writeText.mock.calls[0][0])).toContain('Summary:');
|
|
3572
|
+
expect(String(writeText.mock.calls[0][0])).not.toContain('Sync enabled:');
|
|
3573
|
+
expect(String(writeText.mock.calls[0][0])).toContain('Sync stage: Initial cloud upload');
|
|
3574
|
+
expect(String(writeText.mock.calls[0][0])).toContain('Recent sync events');
|
|
3575
|
+
expect(String(writeText.mock.calls[0][0])).toContain('AI profile snapshot:');
|
|
3576
|
+
expect(String(writeText.mock.calls[0][0])).toContain('2026-03-15T14:28:16.000Z push succeeded (1 change) in 42ms');
|
|
3577
|
+
await user.click(within(dialog).getByRole('switch', { name: 'Enable Sync' }));
|
|
3578
|
+
await waitFor(() => {
|
|
3579
|
+
expect(uiStatePosts.some((entry) => entry?.patch?.enabled === false)).toBe(true);
|
|
3580
|
+
});
|
|
3581
|
+
});
|
|
3582
|
+
it('sync status modal shows active cloud apply instead of synced during cloud bootstrap', async () => {
|
|
3583
|
+
const user = userEvent.setup();
|
|
3584
|
+
const fetchMock = vi.fn((url) => {
|
|
3585
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
1035
3586
|
return Promise.resolve({
|
|
1036
3587
|
ok: true,
|
|
1037
3588
|
json: () => Promise.resolve({
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
3589
|
+
authenticated: true,
|
|
3590
|
+
runtimeMode: 'local',
|
|
3591
|
+
authRequiredForApi: false
|
|
1041
3592
|
})
|
|
1042
3593
|
});
|
|
1043
3594
|
}
|
|
1044
|
-
if (url.includes('/api/taskforce/
|
|
1045
|
-
cloudWorkspaceCreated = true;
|
|
3595
|
+
if (url.includes('/api/taskforce/config')) {
|
|
1046
3596
|
return Promise.resolve({
|
|
1047
3597
|
ok: true,
|
|
1048
|
-
status: 201,
|
|
1049
3598
|
json: () => Promise.resolve({
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
3599
|
+
...mockConfig,
|
|
3600
|
+
runtimeMode: 'local',
|
|
3601
|
+
authRequiredForApi: false,
|
|
3602
|
+
workspaceId: 'workspace-local-active'
|
|
1053
3603
|
})
|
|
1054
3604
|
});
|
|
1055
3605
|
}
|
|
1056
|
-
if (url.includes('/api/taskforce/sync
|
|
3606
|
+
if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
|
|
1057
3607
|
return Promise.resolve({
|
|
1058
3608
|
ok: true,
|
|
1059
3609
|
json: () => Promise.resolve({
|
|
1060
3610
|
success: true,
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
3611
|
+
state: {
|
|
3612
|
+
version: 2,
|
|
3613
|
+
enabled: true,
|
|
3614
|
+
phase: 'attach-cloud',
|
|
3615
|
+
pullCursor: null,
|
|
3616
|
+
lastPullAt: null,
|
|
3617
|
+
lastPushAt: null,
|
|
3618
|
+
lastSyncedAt: null
|
|
3619
|
+
}
|
|
1065
3620
|
})
|
|
1066
3621
|
});
|
|
1067
3622
|
}
|
|
1068
|
-
if (url.includes('/api/taskforce/
|
|
1069
|
-
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
|
|
3623
|
+
if (url.includes('/api/taskforce/ui-state?key=app')) {
|
|
3624
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true, state: {} }) });
|
|
1070
3625
|
}
|
|
1071
3626
|
if (url.includes('/api/taskforce/categories'))
|
|
1072
3627
|
return Promise.resolve({ ok: true, json: () => Promise.resolve(mockCategories) });
|
|
1073
3628
|
if (url.includes('/api/taskforce/tasks'))
|
|
1074
|
-
return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks:
|
|
3629
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ tasks: [] }) });
|
|
1075
3630
|
if (url.includes('/api/taskforce/archive'))
|
|
1076
3631
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ archived: [] }) });
|
|
1077
3632
|
if (url.includes('/api/taskforce/types'))
|
|
@@ -1084,71 +3639,39 @@ describe('TaskforceCore Integration', () => {
|
|
|
1084
3639
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
|
|
1085
3640
|
if (url.includes('/api/taskforce/workflow-templates'))
|
|
1086
3641
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
3642
|
+
if (url.includes('/api/taskforce/sync/workspace/documents'))
|
|
3643
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ documents: [] }) });
|
|
3644
|
+
if (url.includes('/api/taskforce/sync/workspace/annotated-attachments'))
|
|
3645
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ sessions: [] }) });
|
|
3646
|
+
if (url.includes('/api/taskforce/sync/user-settings'))
|
|
3647
|
+
return jsonResponse({ settings: {}, updatedAt: '2026-02-26T11:55:54.000Z' });
|
|
3648
|
+
if (url.includes('/api/taskforce/sync/workspace/handshake'))
|
|
3649
|
+
return jsonResponse({ success: true });
|
|
3650
|
+
if (url.includes('/api/taskforce/sync/workspace/pull'))
|
|
3651
|
+
return new Promise(() => { });
|
|
1087
3652
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
1088
3653
|
});
|
|
1089
3654
|
vi.stubGlobal('fetch', fetchMock);
|
|
1090
|
-
|
|
1091
|
-
await waitFor(() => {
|
|
1092
|
-
expect(screen.getByText('Workspace Setup Required')).toBeInTheDocument();
|
|
1093
|
-
});
|
|
1094
|
-
await waitFor(() => {
|
|
1095
|
-
expect(screen.getByRole('checkbox', { name: /sync to cloud after setup/i })).toBeInTheDocument();
|
|
1096
|
-
});
|
|
1097
|
-
await user.click(screen.getByRole('checkbox', { name: /sync to cloud after setup/i }));
|
|
1098
|
-
const nameInput = screen.getByPlaceholderText('Project name');
|
|
1099
|
-
await user.clear(nameInput);
|
|
1100
|
-
await user.type(nameInput, 'Local Flow Project');
|
|
1101
|
-
await user.click(screen.getByRole('button', { name: 'Save Project Setup' }));
|
|
1102
|
-
await waitFor(() => {
|
|
1103
|
-
expect(workspaceSaved).toBe(true);
|
|
1104
|
-
});
|
|
1105
|
-
initialRender.unmount();
|
|
1106
|
-
render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }) }));
|
|
3655
|
+
render(_jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }));
|
|
1107
3656
|
await waitFor(() => {
|
|
1108
|
-
expect(screen.
|
|
1109
|
-
expect(screen.getByRole('button', { name: /add task/i })).toBeInTheDocument();
|
|
3657
|
+
expect(screen.getByTitle(/Sync manager:/i)).toBeInTheDocument();
|
|
1110
3658
|
});
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
expect(
|
|
1117
|
-
expect(standaloneLayoutSource).toContain('openSharedLoginRoute(\'login\')');
|
|
1118
|
-
expect(standaloneLayoutSource).toContain('navigate(`/login?mode=${mode}&next=${encodeURIComponent(target)}`)');
|
|
1119
|
-
});
|
|
1120
|
-
it('uses shared /login flow for local sign in instead of a duplicate modal', async () => {
|
|
1121
|
-
const standaloneLayoutPath = path.join(process.cwd(), 'src/components/views/StandaloneLayout.tsx');
|
|
1122
|
-
const standaloneLayoutSource = fs.readFileSync(standaloneLayoutPath, 'utf8');
|
|
1123
|
-
const useTaskforcePath = path.join(process.cwd(), 'src/hooks/useTaskforce.ts');
|
|
1124
|
-
const useTaskforceSource = fs.readFileSync(useTaskforcePath, 'utf8');
|
|
1125
|
-
expect(standaloneLayoutSource).not.toContain('showCloudSignInModal');
|
|
1126
|
-
expect(standaloneLayoutSource).not.toContain('cloudSignInEmail');
|
|
1127
|
-
expect(useTaskforceSource).toContain("resolveCloudAuthUrl('/api/taskforce/auth/login')");
|
|
1128
|
-
expect(useTaskforceSource).toContain("return { success: false, error: 'Sign in failed.'");
|
|
1129
|
-
});
|
|
1130
|
-
it('does not promote auth state from generic task fetch success', async () => {
|
|
1131
|
-
const useTaskforcePath = path.join(process.cwd(), 'src/hooks/useTaskforce.ts');
|
|
1132
|
-
const useTaskforceSource = fs.readFileSync(useTaskforcePath, 'utf8');
|
|
1133
|
-
// Auth state should be derived from session/login flows, not inferred from /tasks success.
|
|
1134
|
-
expect(useTaskforceSource).not.toContain('setIsAuthenticated(true);');
|
|
1135
|
-
});
|
|
1136
|
-
it('uses production cloud fallback for local runtime auth/sync endpoints', async () => {
|
|
1137
|
-
const useTaskforcePath = path.join(process.cwd(), 'src/hooks/useTaskforce.ts');
|
|
1138
|
-
const useTaskforceSource = fs.readFileSync(useTaskforcePath, 'utf8');
|
|
1139
|
-
const appMainPath = path.join(process.cwd(), 'apps/app/main.tsx');
|
|
1140
|
-
const appMainSource = fs.readFileSync(appMainPath, 'utf8');
|
|
1141
|
-
expect(useTaskforceSource).toContain("return 'https://app.taskforcehq.ai';");
|
|
1142
|
-
expect(appMainSource).toContain('const envBase = String(import.meta.env.VITE_TASKFORCE_BASE_URL || \'\').trim();');
|
|
1143
|
-
expect(appMainSource).toContain('return undefined;');
|
|
3659
|
+
await user.click(screen.getByTitle(/Sync manager:/i));
|
|
3660
|
+
await screen.findByText('Sync Manager');
|
|
3661
|
+
const dialog = screen.getByText('Sync Manager').closest(`.${modalStyles.modal}`);
|
|
3662
|
+
expect(within(dialog).getByText(/cloud workspace into this device/i)).toBeInTheDocument();
|
|
3663
|
+
expect(within(dialog).getAllByText('Syncing').length).toBeGreaterThan(0);
|
|
3664
|
+
expect(within(dialog).queryByText('Healthy')).not.toBeInTheDocument();
|
|
1144
3665
|
});
|
|
1145
|
-
it('keeps
|
|
1146
|
-
const
|
|
3666
|
+
it('keeps stale legacy cloud-first sync state in attach-cloud until a pull completes', async () => {
|
|
3667
|
+
const user = userEvent.setup();
|
|
3668
|
+
const uiStatePosts = [];
|
|
3669
|
+
const fetchMock = vi.fn((url, init) => {
|
|
1147
3670
|
if (url.includes('/api/taskforce/auth/session')) {
|
|
1148
3671
|
return Promise.resolve({
|
|
1149
3672
|
ok: true,
|
|
1150
3673
|
json: () => Promise.resolve({
|
|
1151
|
-
authenticated:
|
|
3674
|
+
authenticated: true,
|
|
1152
3675
|
runtimeMode: 'local',
|
|
1153
3676
|
authRequiredForApi: false
|
|
1154
3677
|
})
|
|
@@ -1173,12 +3696,22 @@ describe('TaskforceCore Integration', () => {
|
|
|
1173
3696
|
state: {
|
|
1174
3697
|
cloudSyncEnabled: true,
|
|
1175
3698
|
sourceOfTruth: 'cloud',
|
|
1176
|
-
onboardingCompleted:
|
|
1177
|
-
pullCursor: ''
|
|
3699
|
+
onboardingCompleted: false,
|
|
3700
|
+
pullCursor: '',
|
|
3701
|
+
lastPullAt: null,
|
|
3702
|
+
lastPushAt: '2026-03-15T14:28:16.000Z',
|
|
3703
|
+
lastSyncedAt: '2026-03-15T14:28:16.000Z'
|
|
1178
3704
|
}
|
|
1179
3705
|
})
|
|
1180
3706
|
});
|
|
1181
3707
|
}
|
|
3708
|
+
if (url.includes('/api/taskforce/ui-state') && String(init?.method || '').toUpperCase() === 'POST') {
|
|
3709
|
+
uiStatePosts.push(JSON.parse(String(init?.body || '{}')));
|
|
3710
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
|
|
3711
|
+
}
|
|
3712
|
+
if (url.includes('/api/taskforce/ui-state?key=app')) {
|
|
3713
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true, state: {} }) });
|
|
3714
|
+
}
|
|
1182
3715
|
if (url.includes('/api/taskforce/categories'))
|
|
1183
3716
|
return Promise.resolve({ ok: true, json: () => Promise.resolve(mockCategories) });
|
|
1184
3717
|
if (url.includes('/api/taskforce/tasks'))
|
|
@@ -1195,21 +3728,31 @@ describe('TaskforceCore Integration', () => {
|
|
|
1195
3728
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
|
|
1196
3729
|
if (url.includes('/api/taskforce/workflow-templates'))
|
|
1197
3730
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
3731
|
+
if (url.includes('/api/taskforce/sync/workspace/documents'))
|
|
3732
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ documents: [] }) });
|
|
3733
|
+
if (url.includes('/api/taskforce/sync/workspace/annotated-attachments'))
|
|
3734
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ sessions: [] }) });
|
|
3735
|
+
if (url.includes('/api/taskforce/sync/user-settings'))
|
|
3736
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ settings: {}, updatedAt: '2026-03-15T14:28:16.000Z' }) });
|
|
1198
3737
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
1199
3738
|
});
|
|
1200
3739
|
vi.stubGlobal('fetch', fetchMock);
|
|
1201
3740
|
render(_jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }));
|
|
1202
3741
|
await waitFor(() => {
|
|
1203
|
-
expect(screen.getByTitle(/
|
|
3742
|
+
expect(screen.getByTitle(/Sync manager:/i)).toBeInTheDocument();
|
|
1204
3743
|
});
|
|
3744
|
+
await user.click(screen.getByTitle(/Sync manager:/i));
|
|
3745
|
+
await screen.findByText('Sync Manager');
|
|
3746
|
+
const dialog = screen.getByText('Sync Manager').closest(`.${modalStyles.modal}`);
|
|
3747
|
+
expect(within(dialog).getAllByText('3/15/2026, 11:28:16 AM').length).toBeGreaterThan(0);
|
|
3748
|
+
expect(within(dialog).queryByText('Sync source')).not.toBeInTheDocument();
|
|
3749
|
+
expect(within(dialog).getByText('attach-cloud')).toBeInTheDocument();
|
|
1205
3750
|
await waitFor(() => {
|
|
1206
|
-
|
|
1207
|
-
expect(uiStateCalls.length).toBeGreaterThan(0);
|
|
3751
|
+
expect(uiStatePosts.some((entry) => entry?.patch?.version === 2 && entry?.patch?.phase === 'attach-cloud')).toBe(true);
|
|
1208
3752
|
});
|
|
1209
|
-
const pullCalls = fetchMock.mock.calls.filter((call) => typeof call[0] === 'string' && String(call[0]).includes('/sync/workspace/pull'));
|
|
1210
|
-
expect(pullCalls.length).toBe(0);
|
|
1211
3753
|
});
|
|
1212
|
-
it('
|
|
3754
|
+
it('sync status modal uses workspace sync timestamps instead of user-global sync timestamps', async () => {
|
|
3755
|
+
const user = userEvent.setup();
|
|
1213
3756
|
const fetchMock = vi.fn((url) => {
|
|
1214
3757
|
if (url.includes('/api/taskforce/auth/session')) {
|
|
1215
3758
|
return Promise.resolve({
|
|
@@ -1238,14 +3781,20 @@ describe('TaskforceCore Integration', () => {
|
|
|
1238
3781
|
json: () => Promise.resolve({
|
|
1239
3782
|
success: true,
|
|
1240
3783
|
state: {
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
pullCursor:
|
|
3784
|
+
version: 2,
|
|
3785
|
+
enabled: true,
|
|
3786
|
+
phase: 'active',
|
|
3787
|
+
pullCursor: null,
|
|
3788
|
+
lastPullAt: null,
|
|
3789
|
+
lastPushAt: '2026-03-15T14:28:16.000Z',
|
|
3790
|
+
lastSyncedAt: '2026-03-15T14:28:16.000Z'
|
|
1245
3791
|
}
|
|
1246
3792
|
})
|
|
1247
3793
|
});
|
|
1248
3794
|
}
|
|
3795
|
+
if (url.includes('/api/taskforce/ui-state?key=app')) {
|
|
3796
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true, state: {} }) });
|
|
3797
|
+
}
|
|
1249
3798
|
if (url.includes('/api/taskforce/categories'))
|
|
1250
3799
|
return Promise.resolve({ ok: true, json: () => Promise.resolve(mockCategories) });
|
|
1251
3800
|
if (url.includes('/api/taskforce/tasks'))
|
|
@@ -1262,22 +3811,36 @@ describe('TaskforceCore Integration', () => {
|
|
|
1262
3811
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
|
|
1263
3812
|
if (url.includes('/api/taskforce/workflow-templates'))
|
|
1264
3813
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
3814
|
+
if (url.includes('/api/taskforce/sync/workspace/documents'))
|
|
3815
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ documents: [] }) });
|
|
3816
|
+
if (url.includes('/api/taskforce/sync/workspace/annotated-attachments'))
|
|
3817
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ sessions: [] }) });
|
|
3818
|
+
if (url.includes('/api/taskforce/sync/user-settings')) {
|
|
3819
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ settings: {}, updatedAt: '2026-02-26T11:55:54.000Z' }) });
|
|
3820
|
+
}
|
|
3821
|
+
if (url.includes('/api/taskforce/sync/workspace/handshake')) {
|
|
3822
|
+
return jsonResponse({ success: true });
|
|
3823
|
+
}
|
|
1265
3824
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
1266
3825
|
});
|
|
1267
3826
|
vi.stubGlobal('fetch', fetchMock);
|
|
1268
3827
|
render(_jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }));
|
|
1269
3828
|
await waitFor(() => {
|
|
1270
|
-
expect(screen.getByTitle(/
|
|
1271
|
-
});
|
|
1272
|
-
await waitFor(() => {
|
|
1273
|
-
const uiStateCalls = fetchMock.mock.calls.filter((call) => typeof call[0] === 'string' && String(call[0]).includes('/api/taskforce/ui-state?key=workspace-sync'));
|
|
1274
|
-
expect(uiStateCalls.length).toBeGreaterThan(0);
|
|
3829
|
+
expect(screen.getByTitle(/Sync manager:/i)).toBeInTheDocument();
|
|
1275
3830
|
});
|
|
1276
|
-
|
|
1277
|
-
|
|
3831
|
+
await user.click(screen.getByTitle(/Sync manager:/i));
|
|
3832
|
+
await screen.findByText('Sync Manager');
|
|
3833
|
+
const dialog = screen.getByText('Sync Manager').closest(`.${modalStyles.modal}`);
|
|
3834
|
+
expect(within(dialog).getAllByText('3/15/2026, 11:28:16 AM').length).toBeGreaterThan(0);
|
|
3835
|
+
expect(within(dialog).queryByText('2/26/2026, 7:55:54 AM')).not.toBeInTheDocument();
|
|
1278
3836
|
});
|
|
1279
|
-
it('
|
|
1280
|
-
|
|
3837
|
+
it('sync status modal shows a recommended action for attach-cloud bootstrap', async () => {
|
|
3838
|
+
const user = userEvent.setup();
|
|
3839
|
+
const writeText = vi.fn().mockResolvedValue(undefined);
|
|
3840
|
+
Object.defineProperty(navigator, 'clipboard', {
|
|
3841
|
+
configurable: true,
|
|
3842
|
+
value: { writeText }
|
|
3843
|
+
});
|
|
1281
3844
|
const fetchMock = vi.fn((url) => {
|
|
1282
3845
|
if (url.includes('/api/taskforce/auth/session')) {
|
|
1283
3846
|
return Promise.resolve({
|
|
@@ -1301,16 +3864,18 @@ describe('TaskforceCore Integration', () => {
|
|
|
1301
3864
|
});
|
|
1302
3865
|
}
|
|
1303
3866
|
if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
|
|
1304
|
-
workspaceSyncReads += 1;
|
|
1305
3867
|
return Promise.resolve({
|
|
1306
3868
|
ok: true,
|
|
1307
3869
|
json: () => Promise.resolve({
|
|
1308
3870
|
success: true,
|
|
1309
3871
|
state: {
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
pullCursor:
|
|
3872
|
+
version: 2,
|
|
3873
|
+
enabled: true,
|
|
3874
|
+
phase: 'attach-cloud',
|
|
3875
|
+
pullCursor: null,
|
|
3876
|
+
lastPullAt: null,
|
|
3877
|
+
lastPushAt: null,
|
|
3878
|
+
lastSyncedAt: null
|
|
1314
3879
|
}
|
|
1315
3880
|
})
|
|
1316
3881
|
});
|
|
@@ -1334,33 +3899,72 @@ describe('TaskforceCore Integration', () => {
|
|
|
1334
3899
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
|
|
1335
3900
|
if (url.includes('/api/taskforce/workflow-templates'))
|
|
1336
3901
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
3902
|
+
if (url.includes('/api/taskforce/sync/workspace/documents'))
|
|
3903
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ documents: [] }) });
|
|
3904
|
+
if (url.includes('/api/taskforce/sync/workspace/annotated-attachments'))
|
|
3905
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ sessions: [] }) });
|
|
3906
|
+
if (url.includes('/api/taskforce/sync/events')) {
|
|
3907
|
+
return Promise.resolve({
|
|
3908
|
+
ok: true,
|
|
3909
|
+
json: () => Promise.resolve({
|
|
3910
|
+
events: [
|
|
3911
|
+
{
|
|
3912
|
+
occurredAt: '2026-03-15T14:28:16.000Z',
|
|
3913
|
+
eventType: 'pull',
|
|
3914
|
+
status: 'success',
|
|
3915
|
+
changeCount: 1,
|
|
3916
|
+
requestMs: 35
|
|
3917
|
+
}
|
|
3918
|
+
]
|
|
3919
|
+
})
|
|
3920
|
+
});
|
|
3921
|
+
}
|
|
3922
|
+
if (url.includes('/api/taskforce/sync/user-settings'))
|
|
3923
|
+
return jsonResponse({ settings: {}, updatedAt: '2026-03-15T14:28:16.000Z' });
|
|
3924
|
+
if (url.includes('/api/taskforce/sync/workspace/pull'))
|
|
3925
|
+
return new Promise(() => { });
|
|
3926
|
+
if (url.includes('/api/taskforce/sync/workspace/push'))
|
|
3927
|
+
return jsonResponse({ success: true, syncedAt: '2026-03-15T14:28:16.000Z' });
|
|
3928
|
+
if (url.includes('/api/taskforce/sync/workspace/handshake'))
|
|
3929
|
+
return jsonResponse({ success: true });
|
|
1337
3930
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
1338
3931
|
});
|
|
1339
3932
|
vi.stubGlobal('fetch', fetchMock);
|
|
1340
|
-
|
|
3933
|
+
render(_jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }));
|
|
1341
3934
|
await waitFor(() => {
|
|
1342
|
-
expect(screen.getByTitle(/
|
|
3935
|
+
expect(screen.getByTitle(/Sync manager:/i)).toBeInTheDocument();
|
|
1343
3936
|
});
|
|
3937
|
+
await user.click(screen.getByTitle(/Sync manager:/i));
|
|
3938
|
+
await screen.findByText('Sync Manager');
|
|
3939
|
+
const dialog = screen.getByText('Sync Manager').closest(`.${modalStyles.modal}`);
|
|
3940
|
+
expect(within(dialog).getByText('Sync stage')).toBeInTheDocument();
|
|
3941
|
+
expect(within(dialog).getByText('Recommended action')).toBeInTheDocument();
|
|
3942
|
+
expect(within(dialog).getByText(/Keep this window open for the first cloud pull/i)).toBeInTheDocument();
|
|
3943
|
+
await user.click(within(dialog).getByRole('button', { name: 'Copy Report' }));
|
|
1344
3944
|
await waitFor(() => {
|
|
1345
|
-
expect(
|
|
3945
|
+
expect(writeText).toHaveBeenCalledTimes(1);
|
|
1346
3946
|
});
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
expect(
|
|
3947
|
+
expect(String(writeText.mock.calls[0][0])).toContain('Sync stage: Initial cloud pull');
|
|
3948
|
+
expect(String(writeText.mock.calls[0][0])).toContain('Recommended action: Keep this window open for the first cloud pull. If it does not move, press Repair.');
|
|
3949
|
+
expect(String(writeText.mock.calls[0][0])).toContain('Recent sync events');
|
|
3950
|
+
expect(String(writeText.mock.calls[0][0])).toContain('2026-03-15T14:28:16.000Z pull succeeded (1 change) in 35ms');
|
|
1351
3951
|
});
|
|
1352
|
-
it('
|
|
1353
|
-
|
|
3952
|
+
it('waits for auth session resolution before starting background workspace pull sync', async () => {
|
|
3953
|
+
let resolveSession;
|
|
3954
|
+
const sessionGate = new Promise((resolve) => {
|
|
3955
|
+
resolveSession = resolve;
|
|
3956
|
+
});
|
|
3957
|
+
let pullCallCount = 0;
|
|
1354
3958
|
const fetchMock = vi.fn((url) => {
|
|
1355
3959
|
if (url.includes('/api/taskforce/auth/session')) {
|
|
1356
|
-
return
|
|
3960
|
+
return sessionGate.then(() => ({
|
|
1357
3961
|
ok: true,
|
|
1358
3962
|
json: () => Promise.resolve({
|
|
1359
3963
|
authenticated: true,
|
|
1360
3964
|
runtimeMode: 'local',
|
|
1361
3965
|
authRequiredForApi: false
|
|
1362
3966
|
})
|
|
1363
|
-
});
|
|
3967
|
+
}));
|
|
1364
3968
|
}
|
|
1365
3969
|
if (url.includes('/api/taskforce/config')) {
|
|
1366
3970
|
return Promise.resolve({
|
|
@@ -1379,10 +3983,13 @@ describe('TaskforceCore Integration', () => {
|
|
|
1379
3983
|
json: () => Promise.resolve({
|
|
1380
3984
|
success: true,
|
|
1381
3985
|
state: {
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
pullCursor:
|
|
3986
|
+
version: 2,
|
|
3987
|
+
enabled: true,
|
|
3988
|
+
phase: 'attach-cloud',
|
|
3989
|
+
pullCursor: null,
|
|
3990
|
+
lastPullAt: null,
|
|
3991
|
+
lastPushAt: null,
|
|
3992
|
+
lastSyncedAt: null
|
|
1386
3993
|
}
|
|
1387
3994
|
})
|
|
1388
3995
|
});
|
|
@@ -1406,19 +4013,35 @@ describe('TaskforceCore Integration', () => {
|
|
|
1406
4013
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
|
|
1407
4014
|
if (url.includes('/api/taskforce/workflow-templates'))
|
|
1408
4015
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
4016
|
+
if (url.includes('/api/taskforce/sync/workspace/documents'))
|
|
4017
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ documents: [] }) });
|
|
4018
|
+
if (url.includes('/api/taskforce/sync/workspace/annotated-attachments'))
|
|
4019
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ sessions: [] }) });
|
|
4020
|
+
if (url.includes('/api/taskforce/sync/user-settings'))
|
|
4021
|
+
return jsonResponse({ settings: {}, updatedAt: '2026-03-18T12:00:00.000Z' });
|
|
4022
|
+
if (url.includes('/api/taskforce/sync/workspace/handshake'))
|
|
4023
|
+
return jsonResponse({ success: true });
|
|
4024
|
+
if (url.includes('/api/taskforce/sync/workspace/pull')) {
|
|
4025
|
+
pullCallCount += 1;
|
|
4026
|
+
return jsonResponse({ success: true, changes: [], hasMore: false, nextCursor: null });
|
|
4027
|
+
}
|
|
1409
4028
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
|
1410
4029
|
});
|
|
1411
4030
|
vi.stubGlobal('fetch', fetchMock);
|
|
1412
4031
|
render(_jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }));
|
|
1413
4032
|
await waitFor(() => {
|
|
1414
|
-
expect(screen.
|
|
4033
|
+
expect(screen.getByText('Test Project')).toBeInTheDocument();
|
|
4034
|
+
});
|
|
4035
|
+
await new Promise((resolve) => window.setTimeout(resolve, 200));
|
|
4036
|
+
expect(pullCallCount).toBe(0);
|
|
4037
|
+
expect(screen.queryByText('Authentication required. Sign in to continue.')).not.toBeInTheDocument();
|
|
4038
|
+
await act(async () => {
|
|
4039
|
+
if (resolveSession)
|
|
4040
|
+
resolveSession();
|
|
4041
|
+
await Promise.resolve();
|
|
1415
4042
|
});
|
|
1416
|
-
await user.click(screen.getByTitle(/Cloud sync status:/i));
|
|
1417
4043
|
await waitFor(() => {
|
|
1418
|
-
expect(
|
|
1419
|
-
expect(screen.getByText('local')).toBeInTheDocument();
|
|
1420
|
-
expect(screen.getByText('Sync source of truth')).toBeInTheDocument();
|
|
1421
|
-
expect(screen.getByText('cloud')).toBeInTheDocument();
|
|
4044
|
+
expect(pullCallCount).toBeGreaterThan(0);
|
|
1422
4045
|
});
|
|
1423
4046
|
});
|
|
1424
4047
|
it('uses apply-local route with workspace header when cloud pull merges into local workspace', async () => {
|
|
@@ -1426,8 +4049,7 @@ describe('TaskforceCore Integration', () => {
|
|
|
1426
4049
|
const cloudSyncApiSource = fs.readFileSync(cloudSyncApiPath, 'utf8');
|
|
1427
4050
|
expect(cloudSyncApiSource).toContain("'/api/taskforce/sync/workspace/apply-local'");
|
|
1428
4051
|
expect(cloudSyncApiSource).toContain("'x-taskforce-workspace-id': workspaceId");
|
|
1429
|
-
expect(cloudSyncApiSource).toContain("
|
|
1430
|
-
expect(cloudSyncApiSource).toContain("return '/taskforce/api/taskforce/sync/workspace/apply-local';");
|
|
4052
|
+
expect(cloudSyncApiSource).not.toContain("/taskforce/api/taskforce/sync/workspace/apply-local");
|
|
1431
4053
|
});
|
|
1432
4054
|
it('pulls cloud tasks into the active non-default local workspace', async () => {
|
|
1433
4055
|
const cloudSyncApiPath = path.join(process.cwd(), 'src/sync/cloudSyncApi.ts');
|
|
@@ -1529,7 +4151,7 @@ describe('TaskforceCore Integration', () => {
|
|
|
1529
4151
|
vi.stubGlobal('fetch', fetchMock);
|
|
1530
4152
|
render(_jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }));
|
|
1531
4153
|
await waitFor(() => {
|
|
1532
|
-
expect(screen.getByTitle(/
|
|
4154
|
+
expect(screen.getByTitle(/Sync manager:/i)).toBeInTheDocument();
|
|
1533
4155
|
});
|
|
1534
4156
|
const pullCalls = fetchMock.mock.calls.filter((call) => typeof call[0] === 'string' && String(call[0]).includes('/sync/workspace/pull'));
|
|
1535
4157
|
expect(pullCalls.length).toBe(0);
|
|
@@ -1641,7 +4263,7 @@ describe('TaskforceCore Integration', () => {
|
|
|
1641
4263
|
expect(screen.getByText('0/0')).toBeInTheDocument();
|
|
1642
4264
|
});
|
|
1643
4265
|
});
|
|
1644
|
-
it
|
|
4266
|
+
it('retries pull automatically after transient cloud failure and applies changes on reconnect', async () => {
|
|
1645
4267
|
const cloudTask = {
|
|
1646
4268
|
id: 'task-retry-reconnect-1',
|
|
1647
4269
|
title: 'Recovered after retry',
|
|
@@ -1711,25 +4333,22 @@ describe('TaskforceCore Integration', () => {
|
|
|
1711
4333
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
|
|
1712
4334
|
if (url.includes('/api/taskforce/workflow-templates'))
|
|
1713
4335
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
1714
|
-
if (url.includes('
|
|
1715
|
-
return
|
|
4336
|
+
if (url.includes('/api/taskforce/sync/workspace/handshake')) {
|
|
4337
|
+
return jsonResponse({ success: true });
|
|
1716
4338
|
}
|
|
1717
|
-
if (url.includes('
|
|
4339
|
+
if (url.includes('/api/taskforce/sync/workspace/pull')) {
|
|
1718
4340
|
pullAttempt += 1;
|
|
1719
4341
|
if (pullAttempt === 1) {
|
|
1720
|
-
return
|
|
4342
|
+
return jsonResponse({ error: 'unavailable' }, 503);
|
|
1721
4343
|
}
|
|
1722
|
-
return
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
nextCursor: 'cursor-retry-success',
|
|
1728
|
-
hasMore: false
|
|
1729
|
-
})
|
|
4344
|
+
return jsonResponse({
|
|
4345
|
+
success: true,
|
|
4346
|
+
changes: [{ op: 'upsert', archived: false, task: cloudTask }],
|
|
4347
|
+
nextCursor: 'cursor-retry-success',
|
|
4348
|
+
hasMore: false
|
|
1730
4349
|
});
|
|
1731
4350
|
}
|
|
1732
|
-
if (url
|
|
4351
|
+
if (url.includes('/api/taskforce/sync/workspace/apply-local')) {
|
|
1733
4352
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ success: true }) });
|
|
1734
4353
|
}
|
|
1735
4354
|
if (url.includes('/api/taskforce/ui-state'))
|
|
@@ -1751,7 +4370,7 @@ describe('TaskforceCore Integration', () => {
|
|
|
1751
4370
|
expect(applyCalls.length).toBeGreaterThanOrEqual(1);
|
|
1752
4371
|
}, { timeout: 5000 });
|
|
1753
4372
|
});
|
|
1754
|
-
it
|
|
4373
|
+
it('retries pull automatically after transient apply-local failures and recovers', async () => {
|
|
1755
4374
|
const cloudTask = {
|
|
1756
4375
|
id: 'task-retry-apply-failure-1',
|
|
1757
4376
|
title: 'Apply retry recovered',
|
|
@@ -1816,22 +4435,19 @@ describe('TaskforceCore Integration', () => {
|
|
|
1816
4435
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ specialists: [] }) });
|
|
1817
4436
|
if (url.includes('/api/taskforce/workflow-templates'))
|
|
1818
4437
|
return Promise.resolve({ ok: true, json: () => Promise.resolve({ templates: [] }) });
|
|
1819
|
-
if (url.includes('
|
|
1820
|
-
return
|
|
4438
|
+
if (url.includes('/api/taskforce/sync/workspace/handshake')) {
|
|
4439
|
+
return jsonResponse({ success: true });
|
|
1821
4440
|
}
|
|
1822
|
-
if (url.includes('
|
|
4441
|
+
if (url.includes('/api/taskforce/sync/workspace/pull')) {
|
|
1823
4442
|
pullAttempt += 1;
|
|
1824
|
-
return
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
nextCursor: `cursor-apply-retry-${pullAttempt}`,
|
|
1830
|
-
hasMore: false
|
|
1831
|
-
})
|
|
4443
|
+
return jsonResponse({
|
|
4444
|
+
success: true,
|
|
4445
|
+
changes: [{ op: 'upsert', archived: false, task: cloudTask }],
|
|
4446
|
+
nextCursor: `cursor-apply-retry-${pullAttempt}`,
|
|
4447
|
+
hasMore: false
|
|
1832
4448
|
});
|
|
1833
4449
|
}
|
|
1834
|
-
if (url
|
|
4450
|
+
if (url.includes('/api/taskforce/sync/workspace/apply-local')) {
|
|
1835
4451
|
applyAttempt += 1;
|
|
1836
4452
|
if (applyAttempt <= 2) {
|
|
1837
4453
|
return Promise.resolve({ ok: false, status: 503, json: () => Promise.resolve({ error: 'temp apply failure' }) });
|
|
@@ -1944,12 +4560,12 @@ describe('TaskforceCore Integration', () => {
|
|
|
1944
4560
|
expect(pullUrls.length).toBeGreaterThan(0);
|
|
1945
4561
|
});
|
|
1946
4562
|
expect(pullUrls[0]).toContain('cursor=cursor-existing');
|
|
1947
|
-
await user.click(screen.getByTitle(/
|
|
4563
|
+
await user.click(screen.getByTitle(/Sync manager:/i));
|
|
1948
4564
|
await waitFor(() => {
|
|
1949
|
-
expect(screen.getByRole('button', { name: '
|
|
4565
|
+
expect(screen.getByRole('button', { name: 'Repair' })).toBeInTheDocument();
|
|
1950
4566
|
});
|
|
1951
4567
|
const pullCountBeforeReset = pullUrls.length;
|
|
1952
|
-
await user.click(screen.getByRole('button', { name: '
|
|
4568
|
+
await user.click(screen.getByRole('button', { name: 'Repair' }));
|
|
1953
4569
|
await waitFor(() => {
|
|
1954
4570
|
expect(pullUrls.length).toBeGreaterThan(pullCountBeforeReset);
|
|
1955
4571
|
});
|
|
@@ -2129,4 +4745,117 @@ describe('TaskforceCore Integration', () => {
|
|
|
2129
4745
|
expect(screen.getByTitle('Workspace ID: workspace-cloud-active')).toBeInTheDocument();
|
|
2130
4746
|
});
|
|
2131
4747
|
});
|
|
4748
|
+
it('opens edit profile from the account menu and saves the updated display name', async () => {
|
|
4749
|
+
const user = userEvent.setup();
|
|
4750
|
+
let savedDisplayName = 'Original Name';
|
|
4751
|
+
const fetchMock = vi.fn((input, init) => {
|
|
4752
|
+
const url = String(input);
|
|
4753
|
+
if (url.includes('/api/taskforce/auth/runtime-config')) {
|
|
4754
|
+
return jsonResponse({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'local' } });
|
|
4755
|
+
}
|
|
4756
|
+
if (url.includes('/api/taskforce/auth/session')) {
|
|
4757
|
+
return jsonResponse({
|
|
4758
|
+
authenticated: true,
|
|
4759
|
+
runtimeMode: 'local',
|
|
4760
|
+
authRequiredForApi: false,
|
|
4761
|
+
userId: 'user-1',
|
|
4762
|
+
email: 'user@example.com',
|
|
4763
|
+
displayName: savedDisplayName,
|
|
4764
|
+
workspaceId: 'workspace-local-active'
|
|
4765
|
+
});
|
|
4766
|
+
}
|
|
4767
|
+
if (url.includes('/api/taskforce/ui-state?key=app')) {
|
|
4768
|
+
return jsonResponse({ success: true, state: {} });
|
|
4769
|
+
}
|
|
4770
|
+
if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
|
|
4771
|
+
return jsonResponse({
|
|
4772
|
+
success: true,
|
|
4773
|
+
state: {
|
|
4774
|
+
version: 2,
|
|
4775
|
+
enabled: false,
|
|
4776
|
+
phase: 'idle',
|
|
4777
|
+
pullCursor: null,
|
|
4778
|
+
lastPullAt: null,
|
|
4779
|
+
lastPushAt: null,
|
|
4780
|
+
lastSyncedAt: null
|
|
4781
|
+
}
|
|
4782
|
+
});
|
|
4783
|
+
}
|
|
4784
|
+
if (url.includes('/api/taskforce/config')) {
|
|
4785
|
+
return jsonResponse({
|
|
4786
|
+
...mockConfig,
|
|
4787
|
+
runtimeMode: 'local',
|
|
4788
|
+
authRequiredForApi: false,
|
|
4789
|
+
workspaceId: 'workspace-local-active',
|
|
4790
|
+
setupState: {
|
|
4791
|
+
globalSetupState: 'present',
|
|
4792
|
+
workspaceSetupState: 'present',
|
|
4793
|
+
runtimeMode: 'local',
|
|
4794
|
+
workspaceId: 'workspace-local-active',
|
|
4795
|
+
mode: 'core',
|
|
4796
|
+
forceSetup: false,
|
|
4797
|
+
forceGlobalSetup: false,
|
|
4798
|
+
forceWorkspaceSetup: false
|
|
4799
|
+
}
|
|
4800
|
+
});
|
|
4801
|
+
}
|
|
4802
|
+
if (url.includes('/api/taskforce/categories'))
|
|
4803
|
+
return jsonResponse(mockCategories);
|
|
4804
|
+
if (url.includes('/api/taskforce/tasks'))
|
|
4805
|
+
return jsonResponse({ tasks: [] });
|
|
4806
|
+
if (url.includes('/api/taskforce/archive'))
|
|
4807
|
+
return jsonResponse({ archived: [] });
|
|
4808
|
+
if (url.includes('/api/taskforce/types'))
|
|
4809
|
+
return jsonResponse({ types: [] });
|
|
4810
|
+
if (url.includes('/api/taskforce/priorities'))
|
|
4811
|
+
return jsonResponse({ priorities: [] });
|
|
4812
|
+
if (url.includes('/api/taskforce/approaches'))
|
|
4813
|
+
return jsonResponse({ approaches: [] });
|
|
4814
|
+
if (url.includes('/api/taskforce/specialists'))
|
|
4815
|
+
return jsonResponse({ specialists: [] });
|
|
4816
|
+
if (url.includes('/api/taskforce/workflow-templates'))
|
|
4817
|
+
return jsonResponse({ templates: [] });
|
|
4818
|
+
if (url.includes('/api/taskforce/workspace/assignee-options'))
|
|
4819
|
+
return jsonResponse({ options: [] });
|
|
4820
|
+
if (url.includes('/api/taskforce/auth/profile')) {
|
|
4821
|
+
const body = JSON.parse(String(init?.body || '{}'));
|
|
4822
|
+
savedDisplayName = String(body?.displayName || '').trim() || savedDisplayName;
|
|
4823
|
+
return jsonResponse({
|
|
4824
|
+
success: true,
|
|
4825
|
+
profile: {
|
|
4826
|
+
userId: 'user-1',
|
|
4827
|
+
email: 'user@example.com',
|
|
4828
|
+
displayName: savedDisplayName
|
|
4829
|
+
}
|
|
4830
|
+
});
|
|
4831
|
+
}
|
|
4832
|
+
return jsonResponse({});
|
|
4833
|
+
});
|
|
4834
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
4835
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, {}) }));
|
|
4836
|
+
await waitFor(() => {
|
|
4837
|
+
expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
|
|
4838
|
+
});
|
|
4839
|
+
await user.click(screen.getByTitle(/Account/i));
|
|
4840
|
+
await user.click(await screen.findByRole('menuitem', { name: 'Edit Profile' }));
|
|
4841
|
+
const dialog = await screen.findByText('Edit Profile');
|
|
4842
|
+
const modal = dialog.closest(`.${modalStyles.modal}`);
|
|
4843
|
+
expect(within(modal).getByDisplayValue('Original Name')).toBeInTheDocument();
|
|
4844
|
+
expect(within(modal).getByDisplayValue('user@example.com')).toBeDisabled();
|
|
4845
|
+
const displayNameInput = within(modal).getByPlaceholderText('Display name');
|
|
4846
|
+
await user.clear(displayNameInput);
|
|
4847
|
+
await user.type(displayNameInput, 'Updated Name');
|
|
4848
|
+
await user.click(within(modal).getByRole('button', { name: 'Save Profile' }));
|
|
4849
|
+
await waitFor(() => {
|
|
4850
|
+
const profileCall = fetchMock.mock.calls.find((call) => String(call[0]).includes('/api/taskforce/auth/profile'));
|
|
4851
|
+
expect(profileCall).toBeTruthy();
|
|
4852
|
+
const payload = JSON.parse(String(profileCall?.[1]?.body || '{}'));
|
|
4853
|
+
expect(payload.displayName).toBe('Updated Name');
|
|
4854
|
+
});
|
|
4855
|
+
await waitFor(() => {
|
|
4856
|
+
expect(screen.queryByText('Edit Profile')).not.toBeInTheDocument();
|
|
4857
|
+
});
|
|
4858
|
+
await user.click(screen.getByTitle(/Account/i));
|
|
4859
|
+
expect(await screen.findByText('Updated Name')).toBeInTheDocument();
|
|
4860
|
+
});
|
|
2132
4861
|
});
|